# Iframe Skill 选择功能实现计划 ## Context 实现 iframe 与宿主页之间的 skill 选择通信功能: 1. 点击预选 Skill 的 Suggestion 按钮时,向宿主页发送 `{ type: 'selectSkill', skill_id }`,宿主页返回 `{ skill_id, title }` 2. 点击 IframeSkillDialogButton 时,向宿主页发送 `{ type: 'openSkillDialog', openSkillDialog: true }`,宿主页返回用户选择的 `{ skill_id, title }` 3. 在 IframeSkillDialogButton 右侧显示可删除的 Tag 标签(显示 title) 目前 skill 为单选,但设计需考虑后期多选扩展。 ## 现有代码分析 ### 关键文件 - `src/components/workspace/input-box.tsx` - 包含 `IframeSkillDialogButton` (L942-958) 和 `SuggestionList` (L854-921) - `src/components/ui/badge.tsx` - Badge 组件,可用于创建可删除 Tag - `src/hooks/use-mobile.ts` - 现有 hook 示例 - `src/core/i18n/locales/types.ts` (L85-89) - suggestions 类型定义 - `src/core/i18n/locales/zh-CN.ts` (L101-127) - 中文 suggestion 数据 ### IframeSkillDialogButton 现状 ```tsx // L942-958: 目前只是一个按钮,点击时打开文件对话框 function IframeSkillDialogButton({ className }: { className?: string }) { const { t } = useI18n(); const attachments = usePromptInputAttachments(); return ( attachments.openFileDialog()}> {/* SVG icon */} ); } ``` 位置:在 input-box.tsx L496 处使用:`` ### SuggestionList 现状 ```tsx // L854-921: 渲染预定义的建议按钮列表 {t.inputBox.suggestions.map((suggestion) => ( handleSuggestionClick(suggestion.prompt)} /> ))} ``` ## Files to Modify ### 1. 新建 Hook: `src/hooks/use-iframe-skill.ts` 创建 iframe skill 通信 hook: ```typescript import { useState, useEffect, useCallback } from 'react'; // 消息类型常量 const MESSAGE_TYPES = { SELECT_SKILL: 'selectSkill', OPEN_SKILL_DIALOG: 'openSkillDialog', } as const; // Skill 数据类型 interface SkillData { skill_id: string; title: string; } // Hook 返回类型 interface UseIframeSkillReturn { selectedSkill: SkillData | null; sendSelectSkill: (skill_id: string) => void; openSkillDialog: () => void; clearSkill: () => void; } // 从 URL query 参数解析 skill 数据 function parseSkillFromQuery(): SkillData | null { const params = new URLSearchParams(window.location.search); const skill_id = params.get('skill_id'); const title = params.get('title'); if (skill_id && title) { return { skill_id, title }; } return null; } export function useIframeSkill(): UseIframeSkillReturn { const [selectedSkill, setSelectedSkill] = useState(() => parseSkillFromQuery()); // 发送选择预定义 skill const sendSelectSkill = useCallback((skill_id: string) => { window.parent.postMessage({ type: MESSAGE_TYPES.SELECT_SKILL, skill_id }, '*'); }, []); // 打开 skill 选择对话框 const openSkillDialog = useCallback(() => { window.parent.postMessage({ type: MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true }, '*'); }, []); // 清除选中 const clearSkill = useCallback(() => { setSelectedSkill(null); }, []); // 监听 URL 变化(宿主页通过修改 query 参数返回数据) useEffect(() => { const handleUrlChange = () => { const skill = parseSkillFromQuery(); if (skill) { setSelectedSkill(skill); } }; // 监听 popstate 事件(URL 变化时触发) window.addEventListener('popstate', handleUrlChange); // 使用 MutationObserver 监听 URL 变化(某些情况下 popstate 不触发) const originalPushState = history.pushState; history.pushState = function(...args) { originalPushState.apply(history, args); handleUrlChange(); }; return () => { window.removeEventListener('popstate', handleUrlChange); history.pushState = originalPushState; }; }, []); return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill }; } ``` ### 2. 修改类型定义: `src/core/i18n/locales/types.ts` (L85-89) 扩展 suggestions 类型,添加可选的 `skill_id` 字段: ```typescript // 原代码 suggestions: { suggestion: string; prompt: string; icon: LucideIcon; }[]; // 修改为 suggestions: { suggestion: string; prompt: string; icon: LucideIcon; skill_id?: string; // 新增:可选的 skill_id,用于 iframe 通信 }[]; ``` ### 3. 修改 i18n 数据: `src/core/i18n/locales/zh-CN.ts` (L101-127) 为需要发送 skill_id 的 suggestion 添加字段: ```typescript suggestions: [ { suggestion: "论文写作", prompt: "撰写一篇关于[主题]的学术论文...", icon: PenLineIcon, skill_id: "paper-writing", // 新增 }, // ... 其他建议类似添加 ], ``` ### 4. 修改组件: `src/components/workspace/input-box.tsx` #### 4.1 导入 hook (文件顶部) ```typescript import { useIframeSkill } from "@/hooks/use-iframe-skill"; import { Badge } from "@/components/ui/badge"; import { XIcon } from "lucide-react"; ``` #### 4.2 修改 IframeSkillDialogButton (L942-958) ```typescript function IframeSkillDialogButton({ className }: { className?: string }) { const { t } = useI18n(); const { selectedSkill, openSkillDialog, clearSkill } = useIframeSkill(); return (
{/* SVG icon 保持不变 */} {/* 显示选中的 skill Tag */} {selectedSkill && ( {selectedSkill.title} )}
); } ``` #### 4.3 修改 SuggestionList (L854-921) ```typescript function SuggestionList() { const { t } = useI18n(); const { textInput } = usePromptInputController(); const { sendSelectSkill } = useIframeSkill(); const handleSuggestionClick = useCallback( (suggestion: { prompt: string; skill_id?: string }) => { // 如果有 skill_id,发送给宿主页 if (suggestion.skill_id) { sendSelectSkill(suggestion.skill_id); return; // 可选:是否继续填充 prompt } // 原有逻辑 if (!suggestion.prompt) return; textInput.setInput(suggestion.prompt); // ... 其余代码不变 }, [textInput, sendSelectSkill], ); return ( {t.inputBox.suggestions.map((suggestion) => ( handleSuggestionClick(suggestion)} /> ))} {/* DropdownMenu 部分不变 */} ); } ``` ## Message Protocol ### 发送到宿主页 (iframe → parent) 使用 `window.parent.postMessage()`: ```typescript // 选择预定义 skill { type: 'selectSkill', skill_id: string } // 打开 skill 选择对话框 { type: 'openSkillDialog', openSkillDialog: true } ``` ### 宿主页返回 (parent → iframe) 宿主页通过修改 iframe 的 URL query 参数返回数据: ``` ?skill_id=xxx&title=xxx ``` iframe 监听 URL 变化获取数据。 ## Implementation Steps 1. **创建 useIframeSkill hook** - 新建文件 `src/hooks/use-iframe-skill.ts` - 实现 postMessage 发送和 message 事件监听 2. **扩展 i18n 类型** - 修改 `src/core/i18n/locales/types.ts` - 添加 skill_id 可选字段 3. **修改 SuggestionList** - 导入 useIframeSkill hook - 修改 handleSuggestionClick,检测 skill_id 时发送消息 4. **修改 IframeSkillDialogButton** - 导入 useIframeSkill hook 和 Badge 组件 - 点击调用 openSkillDialog - 显示选中的 skill Tag,支持删除 5. **添加 i18n 数据** - 为 zh-CN.ts 和 en-US.ts 的 suggestions 添加 skill_id ## Verification 1. 在 iframe 环境中测试: - 点击带 skill_id 的 Suggestion,确认消息发送(可在宿主页控制台查看) - 模拟宿主页返回 `{ skill_id: 'test', title: '测试技能' }`,确认 Tag 显示正确 - 点击 IframeSkillDialogButton,确认 openSkillDialog 消息发送 - 点击 Tag 的 X 按钮,确认删除功能正常 ## Notes - 目前 skill 为单选,selectedSkill 类型为 `SkillData | null` - 后期扩展多选时,改为 `SkillData[]` 数组类型 - iframe → 宿主页:使用 `postMessage` - 宿主页 → iframe:通过修改 URL query 参数 `?skill_id=xxx&title=xxx` - hook 通过监听 URL 变化(popstate + history.pushState 劫持)获取宿主页返回的数据