import { useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect, useCallback, useRef } from "react"; import { toast } from "sonner"; import { POST_MESSAGE_TYPES, RECEIVE_MESSAGE_TYPES, isSelectedSkillMessage, isSelectedSkillsMessage, type SelectedSkillPayloadItem, sendToParent, } from "@/core/iframe-messages"; import { bootstrapRemoteSkill } from "@/core/skills/api"; // Skill 数据类型 interface SkillData { skill_id: string; title: string; } // Hook 返回类型 interface UseIframeSkillReturn { selectedSkill: SkillData | null; isBootstrapping: boolean; sendSelectSkill: (selectedSkills: SelectedSkillPayloadItem[]) => void; bootstrapAndLockSkills: (params: { selectedSkills: SelectedSkillPayloadItem[]; title: string; }) => Promise; openSkillDialog: () => void; clearSkill: () => void; } interface UseIframeSkillOptions { threadId?: string | null; } export function useIframeSkill( options?: UseIframeSkillOptions, ): UseIframeSkillReturn { const router = useRouter(); const searchParams = useSearchParams(); const threadIdFromQuery = searchParams.get("thread_id"); const threadId = options?.threadId?.trim() || threadIdFromQuery; const isChattingFromQuery = searchParams.get("is_chatting"); const lastThreadIdRef = useRef(null); const [selectedSkill, setSelectedSkill] = useState(null); const [isBootstrapping, setIsBootstrapping] = useState(false); // 1. 监听 query 参数变化(临时禁用) // TODO: 当前 skill 仅通过 iframe postMessage 传递,暂不从 URL 读取 skill_id/title。 // useEffect(() => { // const skillIdFromQuery = searchParams.get("skill_id"); // const titleFromQuery = searchParams.get("title"); // if (skillIdFromQuery && titleFromQuery) { // setSelectedSkill({ skill_id: skillIdFromQuery, title: titleFromQuery }); // } // }, [searchParams]); // 0. 监听 query 中 is_chatting=true 且带 thread_id 时跳转到 thread 页面 // useEffect(() => { // if (!threadId) return; // if (isChattingFromQuery !== "true") return; // if (lastThreadIdRef.current === threadId) return; // lastThreadIdRef.current = threadId; // router.replace(`/workspace/chats/${threadId}`); // }, [isChattingFromQuery, router, threadId]); // 2. 监听宿主页 postMessage useEffect(() => { const handleMessage = (event: MessageEvent) => { if (isSelectedSkillMessage(event.data)) { const { id, title } = event.data; setSelectedSkill({ skill_id: String(id), title }); return; } if (isSelectedSkillsMessage(event.data)) { const first = event.data.selectedSkills[0]; if (!first) { setSelectedSkill(null); return; } setSelectedSkill({ skill_id: String(first.id), title: first.name }); return; } if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) { console.warn("[useIframeSkill] 忽略非法 selectedSkill 消息", event.data); } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, []); // 发送选择预定义 skill const sendSelectSkill = useCallback((selectedSkills: SelectedSkillPayloadItem[]) => { const message = { type: POST_MESSAGE_TYPES.SELECT_SKILLS, selectedSkills }; console.log("[useIframeSkill] sendSelectSkill:", message); sendToParent(message); }, []); const bootstrapAndLockSkills = useCallback( async ({ selectedSkills, title, }: { selectedSkills: SelectedSkillPayloadItem[]; title: string; }) => { if (!threadId) { toast.error("技能加载失败", { description: "缺少 thread_id,无法初始化技能", }); return false; } const content_ids = Array.from( new Set( selectedSkills .map((item) => Number(String(item.id).trim())) .filter((id) => Number.isFinite(id) && id > 0), ), ); if (content_ids.length === 0) { toast.error("技能加载失败", { description: "无效的 skill_id", }); return false; } const languageTypeRaw = searchParams.get("languageType")?.trim() ?? searchParams.get("language_type")?.trim(); const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0; setIsBootstrapping(true); toast.loading(`正在加载技能「${title}」...`, { id: "suggest-skill-bootstrap" }); try { const result = await bootstrapRemoteSkill({ thread_id: threadId, content_ids, language_type: languageType, target_dir: "/mnt/user-data/uploads/skill", clear_target: true, }); toast.dismiss("suggest-skill-bootstrap"); if (!result.success) { toast.error(`技能「${title}」加载失败`, { description: result.message || "未知错误", }); return false; } sendSelectSkill(selectedSkills); setSelectedSkill({ skill_id: String(content_ids[0]), title }); toast.success(`技能「${title}」加载成功`, { description: result.message || `已创建 ${result.created_files} 个文件`, }); return true; } catch (error) { toast.dismiss("suggest-skill-bootstrap"); const message = error instanceof Error ? error.message : "网络请求失败"; toast.error(`技能「${title}」加载失败`, { description: message, }); return false; } finally { setIsBootstrapping(false); } }, [searchParams, sendSelectSkill, threadId], ); // 打开 skill 选择对话框 const openSkillDialog = useCallback(() => { const message = { type: POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true, } as const; console.log("[useIframeSkill] openSkillDialog:", message); sendToParent(message); }, []); // 清除选中并发送空 selectedSkills 数组给主页 const clearSkill = useCallback(() => { setSelectedSkill(null); // 发送空数组给主页,通知取消选择 const message = { type: POST_MESSAGE_TYPES.SELECT_SKILLS, selectedSkills: [] }; console.log("[useIframeSkill] clearSkill, sending selectedSkills=[]:", message); sendToParent(message); }, []); return { selectedSkill, isBootstrapping, sendSelectSkill, bootstrapAndLockSkills, openSkillDialog, clearSkill, }; }