diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index 10561850..b3c7fea7 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -49,6 +49,12 @@ function parseStoredSkills(raw: string | null): SkillData[] { } } +function removeSkillsByIdsFromList(skills: SkillData[], skillIds: string[]): SkillData[] { + if (skillIds.length === 0) return skills; + const idSet = new Set(skillIds.map((id) => String(id))); + return skills.filter((skill) => !idSet.has(String(skill.skill_id))); +} + // Hook 返回类型 interface UseIframeSkillReturn { selectedSkill: SkillData | null; @@ -81,6 +87,45 @@ export function useIframeSkill( const [selectedSkills, setSelectedSkills] = useState([]); const [isBootstrapping, setIsBootstrapping] = useState(false); + const removeFailedSkills = useCallback( + (skillIds: string[]) => { + if (skillIds.length === 0) return; + + // 1) 回滚内存状态:移除失败 skill,避免展示错误 tag + setSelectedSkills((prev) => { + const next = removeSkillsByIdsFromList(prev, skillIds); + setSelectedSkill(next[0] ?? null); + return next; + }); + + // 2) 回滚 localStorage(latest + thread) + const latestSkills = parseStoredSkills( + window.localStorage.getItem(STORAGE_KEYS.latest), + ); + const nextLatestSkills = removeSkillsByIdsFromList(latestSkills, skillIds); + if (nextLatestSkills.length > 0) { + window.localStorage.setItem( + STORAGE_KEYS.latest, + JSON.stringify(nextLatestSkills), + ); + } else { + window.localStorage.removeItem(STORAGE_KEYS.latest); + } + + const threadKey = getThreadStorageKey(threadId); + if (threadKey) { + const threadSkills = parseStoredSkills(window.localStorage.getItem(threadKey)); + const nextThreadSkills = removeSkillsByIdsFromList(threadSkills, skillIds); + if (nextThreadSkills.length > 0) { + window.localStorage.setItem(threadKey, JSON.stringify(nextThreadSkills)); + } else { + window.localStorage.removeItem(threadKey); + } + } + }, + [threadId], + ); + // 1. 监听 query 参数变化(临时禁用) // TODO: 当前 skill 仅通过 iframe postMessage 传递,暂不从 URL 读取 skill_id/title。 // useEffect(() => { @@ -223,6 +268,8 @@ export function useIframeSkill( toast.dismiss("suggest-skill-bootstrap"); if (!result.success) { + const failedIds = selectedSkills.map((item) => String(item.id).trim()); + removeFailedSkills(failedIds); toast.error(`技能「${title}」加载失败`, { description: result.message || "未知错误", }); @@ -243,6 +290,8 @@ export function useIframeSkill( return true; } catch (error) { + const failedIds = selectedSkills.map((item) => String(item.id).trim()); + removeFailedSkills(failedIds); toast.dismiss("suggest-skill-bootstrap"); const message = error instanceof Error ? error.message : "网络请求失败"; @@ -254,7 +303,7 @@ export function useIframeSkill( setIsBootstrapping(false); } }, - [searchParams, sendSelectSkill, threadId], + [removeFailedSkills, searchParams, sendSelectSkill, threadId], ); // 打开 skill 选择对话框