From 2b0581db712bca320894124d13c6752647945ae4 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 3 Apr 2026 22:12:57 +0800 Subject: [PATCH] fix: centralize UUID generation and validate query thread_id for skillBootstrap; ensure single execution and correct thread dir --- .../app/workspace/chats/[thread_id]/page.tsx | 85 ++++++++++++++++++- .../workspace/chats/use-thread-chat.ts | 16 ++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 909ffbe0..c9f1b3f6 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { ArtifactTrigger } from "@/components/workspace/artifacts"; @@ -24,15 +25,29 @@ import { Welcome } from "@/components/workspace/welcome"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; import { useThreadSettings } from "@/core/settings"; +import { bootstrapRemoteSkill } from "@/core/skills"; import { useThreadStream } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; +import { uuid } from "@/core/utils/uuid"; import { env } from "@/env"; import { cn } from "@/lib/utils"; +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + export default function ChatPage() { const { t } = useI18n(); const [showFollowups, setShowFollowups] = useState(false); - const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + const searchParams = useSearchParams(); const generatedThreadIdRef = useRef(""); + if (!generatedThreadIdRef.current) { + const queryThreadId = searchParams.get("thread_id")?.trim(); + generatedThreadIdRef.current = + queryThreadId && UUID_REGEX.test(queryThreadId) ? queryThreadId : uuid(); + } + + const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat({ + newThreadId: generatedThreadIdRef.current, + }); const [settings, setSettings] = useThreadSettings(threadId); const [mounted, setMounted] = useState(false); useSpecificChatMode(); @@ -42,6 +57,26 @@ export default function ChatPage() { }, []); const { showNotification } = useNotification(); + const skillBootstrappedKeyRef = useRef(null); + const isBootstrappingRef = useRef(false); + + const skillBootstrap = useMemo(() => { + const skillIdRaw = searchParams.get("skill_id")?.trim(); + if (!skillIdRaw) return undefined; + + const contentId = Number(skillIdRaw); + if (!Number.isFinite(contentId)) return undefined; + + const languageTypeRaw = + searchParams.get("languageType")?.trim() ?? + searchParams.get("language_type")?.trim(); + const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0; + + return { + contentId, + languageType: Number.isFinite(languageType) ? languageType : 0, + }; + }, [searchParams]); const [thread, sendMessage, isUploading] = useThreadStream({ threadId: isNewThread ? undefined : threadId, @@ -70,6 +105,52 @@ export default function ChatPage() { }, }); + useEffect(() => { + if (!threadId || !skillBootstrap?.contentId) { + isBootstrappingRef.current = false; + return; + } + + const languageType = skillBootstrap.languageType ?? 0; + const initKey = `${threadId}:${skillBootstrap.contentId}:${languageType}`; + if (skillBootstrappedKeyRef.current === initKey || isBootstrappingRef.current) { + return; + } + + let cancelled = false; + + const runBootstrap = async () => { + isBootstrappingRef.current = true; + try { + await bootstrapRemoteSkill({ + thread_id: threadId, + content_id: skillBootstrap.contentId, + language_type: languageType, + target_dir: "/mnt/user-data/uploads/skill", + clear_target: true, + }); + + if (!cancelled) { + skillBootstrappedKeyRef.current = initKey; + } + } catch (error) { + if (!cancelled) { + const message = + error instanceof Error ? error.message : "Skill initialization failed"; + showNotification("Skill initialization failed", { body: message }); + } + } finally { + isBootstrappingRef.current = false; + } + }; + + void runBootstrap(); + + return () => { + cancelled = true; + }; + }, [threadId, skillBootstrap, showNotification]); + const handleSubmit = useCallback( (message: PromptInputMessage) => { void sendMessage(threadId, message); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index b3164485..1b555cc5 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -1,17 +1,23 @@ "use client"; import { useParams, usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { uuid } from "@/core/utils/uuid"; -export function useThreadChat() { +type UseThreadChatOptions = { + newThreadId?: string; +}; + +export function useThreadChat(options?: UseThreadChatOptions) { const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const pathname = usePathname(); + const fallbackNewThreadIdRef = useRef(options?.newThreadId ?? uuid()); + const fallbackNewThreadId = options?.newThreadId ?? fallbackNewThreadIdRef.current; const searchParams = useSearchParams(); const [threadId, setThreadId] = useState(() => { - return threadIdFromPath === "new" ? uuid() : threadIdFromPath; + return threadIdFromPath === "new" ? fallbackNewThreadId : threadIdFromPath; }); const [isNewThread, setIsNewThread] = useState( @@ -21,9 +27,9 @@ export function useThreadChat() { useEffect(() => { if (pathname.endsWith("/new")) { setIsNewThread(true); - setThreadId(uuid()); + setThreadId(fallbackNewThreadId); } - }, [pathname]); + }, [pathname, fallbackNewThreadId]); const isMock = searchParams.get("mock") === "true"; return { threadId, isNewThread, setIsNewThread, isMock }; }