From 3d4521f37f8cf12b712b98a8cc5c52ad4c426d35 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Wed, 18 Mar 2026 18:25:02 +0800 Subject: [PATCH] =?UTF-8?q?debug:=20=E4=BF=AE=E5=A4=8D=E6=96=B0=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=98=AFquery=E5=8F=82=E6=95=B0=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[agent_name]/chats/[thread_id]/page.tsx | 2 +- .../app/workspace/chats/[thread_id]/page.tsx | 27 +++++++++++++-- frontend/src/app/workspace/layout.tsx | 2 +- frontend/src/app/workspace/page.tsx | 4 ++- .../workspace/chats/use-thread-chat.ts | 31 +++++++++++++---- .../src/components/workspace/input-box.tsx | 27 +++++++++++++++ .../components/workspace/recent-chat-list.tsx | 10 +++--- frontend/src/core/threads/hooks.ts | 17 ++++++++-- frontend/src/core/threads/utils.ts | 2 +- frontend/src/hooks/use-iframe-skill.ts | 34 +++++++++++++++++++ 10 files changed, 136 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx index a44ad3fa..127a09cc 100644 --- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx @@ -47,7 +47,7 @@ export default function AgentChatPage() { history.replaceState( null, "", - `/workspace/agents/${agent_name}/chats/${threadId}`, + `/workspace/agents/${agent_name}/chats/new?isnew=false&thread_id=${threadId}`, ); }, onFinish: (state) => { diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index e6170b31..2c9ed583 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -32,7 +32,7 @@ import { useNotification } from "@/core/notification/hooks"; import { useLocalSettings } from "@/core/settings"; // [移植自 main 分支 ef9a071] 导入 skill 初始化 API import { bootstrapRemoteSkill } from "@/core/skills"; -import { useThreadStream } from "@/core/threads/hooks"; +import { useThreadStream, useTruncateThread } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; import { cn } from "@/lib/utils"; @@ -71,6 +71,21 @@ export default function ChatPage() { const { showNotification } = useNotification(); + // 截断会话 hook + const truncateThread = useTruncateThread(); + // 跟踪已截断的 threadId,避免重复截断 + const truncatedThreadIdRef = useRef(null); + + // 当 isnew=false 且有 thread_id 时,截断会话历史 + useEffect(() => { + // 只在非新会话且需要复用 thread_id 时截断 + if (!createNewSession && threadId && truncatedThreadIdRef.current !== threadId) { + console.log("[ChatPage] Truncating thread:", threadId); + truncatedThreadIdRef.current = threadId; + truncateThread.mutate({ threadId }); + } + }, [createNewSession, threadId, truncateThread]); + // [移植自 main 分支 ef9a071] skill 初始化状态 const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false); const skillBootstrappedKeyRef = useRef(null); @@ -180,7 +195,9 @@ export default function ChatPage() { }, [threadId, skillBootstrap, showNotification]); const [thread, sendMessage] = useThreadStream({ - threadId: isNewThread ? undefined : threadId, + // [修复] 使用 createNewSession 而不是 isNewThread 来决定是否创建新会话 + // isnew=false 时应该复用现有 threadId,不应该是 undefined + threadId: createNewSession ? undefined : threadId, context: settings.context, isMock, // [移植自 main 分支 4119fdc] 传递 uploadTarget @@ -188,7 +205,11 @@ export default function ChatPage() { onStart: () => { setIsNewThread(false); // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. - history.replaceState(null, "", `/workspace/chats/${threadId}`); + history.replaceState( + null, + "", + `/workspace/chats/new?isnew=false&thread_id=${threadId}`, + ); }, onFinish: (state) => { if (document.hidden || !document.hasFocus()) { diff --git a/frontend/src/app/workspace/layout.tsx b/frontend/src/app/workspace/layout.tsx index e770ec59..a41802f3 100644 --- a/frontend/src/app/workspace/layout.tsx +++ b/frontend/src/app/workspace/layout.tsx @@ -37,7 +37,7 @@ export default function WorkspaceLayout({ onOpenChange={handleOpenChange} > {/* TODO: !!!!必须注释!!!!! */} - {/* */} + {children} diff --git a/frontend/src/app/workspace/page.tsx b/frontend/src/app/workspace/page.tsx index db0ab425..cd665d31 100644 --- a/frontend/src/app/workspace/page.tsx +++ b/frontend/src/app/workspace/page.tsx @@ -13,7 +13,9 @@ export default function WorkspacePage() { }) .find((thread) => thread.isDirectory() && !thread.name.startsWith(".")); if (firstThread) { - return redirect(`/workspace/chats/${firstThread.name}`); + return redirect( + `/workspace/chats/new?isnew=false&thread_id=${firstThread.name}`, + ); } } return redirect("/workspace/chats/new"); diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index 7bf5ca56..6b5c8a61 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -77,12 +77,17 @@ export function useThreadChat(): ThreadChatResult { return result; }, [threadIdFromPath, searchParams]); - // [移植自 main 分支 e2fdfa7] UI模式仅依赖路由:/workspace/chats/new 总是"新页面"模式 + // [移植自 main 分支 e2fdfa7] UI模式:如果有 thread_id 参数则认为是已有会话 const isNewThread = useMemo(() => { - const result = threadIdFromPath === "new"; - console.log("[useThreadChat] isNewThread:", result); - return result; - }, [threadIdFromPath]); + if (threadIdFromPath !== "new") { + return false; + } + // 有 thread_id 参数说明是复用现有会话 + if (queryThreadId) { + return false; + } + return true; + }, [threadIdFromPath, queryThreadId]); // [移植自 main 分支 4119fdc] 获取上传目标 const uploadTarget = useMemo(() => { @@ -212,12 +217,15 @@ export function useThreadChat(): ThreadChatResult { if (threadIdFromPath === "new") { // [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id threadIdRef.current = queryThreadId || uuid(); - isNewThreadRef.current = true; + // 如果有 queryThreadId,说明是复用现有会话,不是新会话 + isNewThreadRef.current = !queryThreadId; console.log( "[useThreadChat] initial threadId (new route):", threadIdRef.current, "queryThreadId:", queryThreadId, + "isNewThread:", + isNewThreadRef.current, ); } else { threadIdRef.current = threadIdFromPath; @@ -232,6 +240,17 @@ export function useThreadChat(): ThreadChatResult { const [threadId, setThreadId] = useState(threadIdRef.current); const [isNewThreadState, setIsNewThread] = useState(isNewThreadRef.current); + // 监听 queryThreadId 变化,更新 threadId + useEffect(() => { + // 当 URL 中的 thread_id 参数变化时,更新 threadId + if (queryThreadId && queryThreadId !== threadId) { + console.log("[useThreadChat] queryThreadId changed, updating threadId:", queryThreadId); + threadIdRef.current = queryThreadId; + setThreadId(queryThreadId); + setIsNewThread(false); + } + }, [queryThreadId, threadId]); + useEffect(() => { console.log("[useThreadChat] useEffect: pathname changed to:", pathname); if (pathname.endsWith("/new")) { diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 7e174af6..f6160678 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -168,6 +168,12 @@ export function InputBox({ const [isFocused, setIsFocused] = useState(false); const containerRef = useRef(null); + // isNewThread 变化时重置 isFocused 状态 + useEffect(() => { + setIsFocused(false); + onFocusChange?.(false); + }, [isNewThread, onFocusChange]); + // isNewThread 时禁用收缩,始终保持展开 const effectiveIsFocused = isNewThread || isFocused; @@ -1035,6 +1041,19 @@ function IframeSkillDialogButton({ }) { const { t } = useI18n(); + // TODO: 测试按钮,模拟宿主页发送 postMessage,测试完成后删除 + const handleTestPostMessage = () => { + const testMessage = { + type: "selectedSkill", + id: 5, + title: "文档处理", + }; + console.log("[Test] Simulating postMessage from parent:", testMessage); + window.dispatchEvent( + new MessageEvent("message", { data: testMessage }) + ); + }; + return (
@@ -1055,6 +1074,14 @@ function IframeSkillDialogButton({ + {/* TODO: 测试按钮,测试完成后删除 + */} {selectedSkill && ( t.thread_id === threadId); - let nextThreadId = "new"; + let nextPath = "/workspace/chats/new"; if (threadIndex > -1) { if (threads[threadIndex + 1]) { - nextThreadId = threads[threadIndex + 1]!.thread_id; + nextPath = `/workspace/chats/new?isnew=false&thread_id=${threads[threadIndex + 1]!.thread_id}`; } else if (threads[threadIndex - 1]) { - nextThreadId = threads[threadIndex - 1]!.thread_id; + nextPath = `/workspace/chats/new?isnew=false&thread_id=${threads[threadIndex - 1]!.thread_id}`; } } - void router.push(`/workspace/chats/${nextThreadId}`); + void router.push(nextPath); } }, [deleteThread, router, threadIdFromPath, threads], @@ -100,7 +100,7 @@ export function RecentChatList() { window.location.hostname === "127.0.0.1"; // On localhost: use Vercel URL; On production: use current origin const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin; - const shareUrl = `${baseUrl}/workspace/chats/${threadId}`; + const shareUrl = `${baseUrl}/workspace/chats/new?isnew=false&thread_id=${threadId}`; try { await navigator.clipboard.writeText(shareUrl); toast.success(t.clipboard.linkCopied); diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index b9e0fc75..c5dd9266 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -71,8 +71,8 @@ export function useThreadStream({ useEffect(() => { const normalizedThreadId = threadId ?? null; - if (!normalizedThreadId) { - // Just reset for new thread creation when threadId becomes null/undefined + // 当 threadId 变化时,更新 onStreamThreadId 以重新获取数据 + if (normalizedThreadId !== threadIdRef.current) { startedRef.current = false; setOnStreamThreadId(normalizedThreadId); } @@ -533,3 +533,16 @@ export function useRenameThread() { }, }); } + +// 截断会话历史(清空消息) +export function useTruncateThread() { + const apiClient = getAPIClient(); + return useMutation({ + mutationFn: async ({ threadId }: { threadId: string }) => { + // 通过更新 state,设置 messages 为空数组来截断会话 + await apiClient.threads.updateState(threadId, { + values: { messages: [] }, + }); + }, + }); +} diff --git a/frontend/src/core/threads/utils.ts b/frontend/src/core/threads/utils.ts index 22510fa8..ea361a0f 100644 --- a/frontend/src/core/threads/utils.ts +++ b/frontend/src/core/threads/utils.ts @@ -3,7 +3,7 @@ import type { Message } from "@langchain/langgraph-sdk"; import type { AgentThread } from "./types"; export function pathOfThread(threadId: string) { - return `/workspace/chats/${threadId}`; + return `/workspace/chats/new?isnew=false&thread_id=${threadId}`; } export function textOfMessage(message: Message) { diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index d6c0ef4f..c00fdd1d 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -21,6 +21,22 @@ interface UseIframeSkillReturn { clearSkill: () => void; } +// 来自宿主页的 postMessage 类型 +interface SelectedSkillMessage { + type: "selectedSkill"; + id: number; + title: string; +} + +function isSelectedSkillMessage(data: unknown): data is SelectedSkillMessage { + return ( + typeof data === "object" && + data !== null && + (data as SelectedSkillMessage).type === "selectedSkill" && + typeof (data as SelectedSkillMessage).id === "number" + ); +} + export function useIframeSkill(): UseIframeSkillReturn { const searchParams = useSearchParams(); const skillIdFromQuery = searchParams.get("skill_id"); @@ -43,6 +59,24 @@ export function useIframeSkill(): UseIframeSkillReturn { } }, [skillIdFromQuery, titleFromQuery]); + // 监听来自宿主页的 postMessage + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (!isSelectedSkillMessage(event.data)) { + return; + } + const { id, title } = event.data; + console.log("[useIframeSkill] Received selectedSkill from parent:", { + id, + title, + }); + setSelectedSkill({ skill_id: String(id), title }); + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, []); + // 发送选择预定义 skill const sendSelectSkill = useCallback((skill_id: string) => { const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };