debug: 修复新会话是query参数的问题

This commit is contained in:
肖应宇 2026-03-18 18:25:02 +08:00
parent 917f0ef591
commit 3d4521f37f
10 changed files with 136 additions and 20 deletions

View File

@ -47,7 +47,7 @@ export default function AgentChatPage() {
history.replaceState( history.replaceState(
null, null,
"", "",
`/workspace/agents/${agent_name}/chats/${threadId}`, `/workspace/agents/${agent_name}/chats/new?isnew=false&thread_id=${threadId}`,
); );
}, },
onFinish: (state) => { onFinish: (state) => {

View File

@ -32,7 +32,7 @@ import { useNotification } from "@/core/notification/hooks";
import { useLocalSettings } from "@/core/settings"; import { useLocalSettings } from "@/core/settings";
// [移植自 main 分支 ef9a071] 导入 skill 初始化 API // [移植自 main 分支 ef9a071] 导入 skill 初始化 API
import { bootstrapRemoteSkill } from "@/core/skills"; 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 { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env"; import { env } from "@/env";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -71,6 +71,21 @@ export default function ChatPage() {
const { showNotification } = useNotification(); const { showNotification } = useNotification();
// 截断会话 hook
const truncateThread = useTruncateThread();
// 跟踪已截断的 threadId避免重复截断
const truncatedThreadIdRef = useRef<string | null>(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 初始化状态 // [移植自 main 分支 ef9a071] skill 初始化状态
const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false); const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false);
const skillBootstrappedKeyRef = useRef<string | null>(null); const skillBootstrappedKeyRef = useRef<string | null>(null);
@ -180,7 +195,9 @@ export default function ChatPage() {
}, [threadId, skillBootstrap, showNotification]); }, [threadId, skillBootstrap, showNotification]);
const [thread, sendMessage] = useThreadStream({ const [thread, sendMessage] = useThreadStream({
threadId: isNewThread ? undefined : threadId, // [修复] 使用 createNewSession 而不是 isNewThread 来决定是否创建新会话
// isnew=false 时应该复用现有 threadId不应该是 undefined
threadId: createNewSession ? undefined : threadId,
context: settings.context, context: settings.context,
isMock, isMock,
// [移植自 main 分支 4119fdc] 传递 uploadTarget // [移植自 main 分支 4119fdc] 传递 uploadTarget
@ -188,7 +205,11 @@ export default function ChatPage() {
onStart: () => { onStart: () => {
setIsNewThread(false); 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. // ! 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) => { onFinish: (state) => {
if (document.hidden || !document.hasFocus()) { if (document.hidden || !document.hasFocus()) {

View File

@ -37,7 +37,7 @@ export default function WorkspaceLayout({
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
> >
{/* TODO: !!!!必须注释!!!!! */} {/* TODO: !!!!必须注释!!!!! */}
{/* <WorkspaceSidebar className="" /> */} <WorkspaceSidebar className="" />
<SidebarInset className="min-w-0">{children}</SidebarInset> <SidebarInset className="min-w-0">{children}</SidebarInset>
</SidebarProvider> </SidebarProvider>
<Toaster /> <Toaster />

View File

@ -13,7 +13,9 @@ export default function WorkspacePage() {
}) })
.find((thread) => thread.isDirectory() && !thread.name.startsWith(".")); .find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
if (firstThread) { if (firstThread) {
return redirect(`/workspace/chats/${firstThread.name}`); return redirect(
`/workspace/chats/new?isnew=false&thread_id=${firstThread.name}`,
);
} }
} }
return redirect("/workspace/chats/new"); return redirect("/workspace/chats/new");

View File

@ -77,12 +77,17 @@ export function useThreadChat(): ThreadChatResult {
return result; return result;
}, [threadIdFromPath, searchParams]); }, [threadIdFromPath, searchParams]);
// [移植自 main 分支 e2fdfa7] UI模式仅依赖路由:/workspace/chats/new 总是"新页面"模式 // [移植自 main 分支 e2fdfa7] UI模式:如果有 thread_id 参数则认为是已有会话
const isNewThread = useMemo(() => { const isNewThread = useMemo(() => {
const result = threadIdFromPath === "new"; if (threadIdFromPath !== "new") {
console.log("[useThreadChat] isNewThread:", result); return false;
return result; }
}, [threadIdFromPath]); // 有 thread_id 参数说明是复用现有会话
if (queryThreadId) {
return false;
}
return true;
}, [threadIdFromPath, queryThreadId]);
// [移植自 main 分支 4119fdc] 获取上传目标 // [移植自 main 分支 4119fdc] 获取上传目标
const uploadTarget = useMemo(() => { const uploadTarget = useMemo(() => {
@ -212,12 +217,15 @@ export function useThreadChat(): ThreadChatResult {
if (threadIdFromPath === "new") { if (threadIdFromPath === "new") {
// [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id // [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id
threadIdRef.current = queryThreadId || uuid(); threadIdRef.current = queryThreadId || uuid();
isNewThreadRef.current = true; // 如果有 queryThreadId说明是复用现有会话不是新会话
isNewThreadRef.current = !queryThreadId;
console.log( console.log(
"[useThreadChat] initial threadId (new route):", "[useThreadChat] initial threadId (new route):",
threadIdRef.current, threadIdRef.current,
"queryThreadId:", "queryThreadId:",
queryThreadId, queryThreadId,
"isNewThread:",
isNewThreadRef.current,
); );
} else { } else {
threadIdRef.current = threadIdFromPath; threadIdRef.current = threadIdFromPath;
@ -232,6 +240,17 @@ export function useThreadChat(): ThreadChatResult {
const [threadId, setThreadId] = useState(threadIdRef.current); const [threadId, setThreadId] = useState(threadIdRef.current);
const [isNewThreadState, setIsNewThread] = useState(isNewThreadRef.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(() => { useEffect(() => {
console.log("[useThreadChat] useEffect: pathname changed to:", pathname); console.log("[useThreadChat] useEffect: pathname changed to:", pathname);
if (pathname.endsWith("/new")) { if (pathname.endsWith("/new")) {

View File

@ -168,6 +168,12 @@ export function InputBox({
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
// isNewThread 变化时重置 isFocused 状态
useEffect(() => {
setIsFocused(false);
onFocusChange?.(false);
}, [isNewThread, onFocusChange]);
// isNewThread 时禁用收缩,始终保持展开 // isNewThread 时禁用收缩,始终保持展开
const effectiveIsFocused = isNewThread || isFocused; const effectiveIsFocused = isNewThread || isFocused;
@ -1035,6 +1041,19 @@ function IframeSkillDialogButton({
}) { }) {
const { t } = useI18n(); 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 ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tooltip content={t.inputBox.selectSkill}> <Tooltip content={t.inputBox.selectSkill}>
@ -1055,6 +1074,14 @@ function IframeSkillDialogButton({
</svg> </svg>
</PromptInputButton> </PromptInputButton>
</Tooltip> </Tooltip>
{/* TODO:
<button
type="button"
onClick={handleTestPostMessage}
className="rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600"
>
postMessage
</button> */}
{selectedSkill && ( {selectedSkill && (
<Badge <Badge
variant="secondary" variant="secondary"

View File

@ -59,15 +59,15 @@ export function RecentChatList() {
deleteThread({ threadId }); deleteThread({ threadId });
if (threadId === threadIdFromPath) { if (threadId === threadIdFromPath) {
const threadIndex = threads.findIndex((t) => t.thread_id === threadId); const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
let nextThreadId = "new"; let nextPath = "/workspace/chats/new";
if (threadIndex > -1) { if (threadIndex > -1) {
if (threads[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]) { } 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], [deleteThread, router, threadIdFromPath, threads],
@ -100,7 +100,7 @@ export function RecentChatList() {
window.location.hostname === "127.0.0.1"; window.location.hostname === "127.0.0.1";
// On localhost: use Vercel URL; On production: use current origin // On localhost: use Vercel URL; On production: use current origin
const baseUrl = isLocalhost ? VERCEL_URL : window.location.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 { try {
await navigator.clipboard.writeText(shareUrl); await navigator.clipboard.writeText(shareUrl);
toast.success(t.clipboard.linkCopied); toast.success(t.clipboard.linkCopied);

View File

@ -71,8 +71,8 @@ export function useThreadStream({
useEffect(() => { useEffect(() => {
const normalizedThreadId = threadId ?? null; const normalizedThreadId = threadId ?? null;
if (!normalizedThreadId) { // 当 threadId 变化时,更新 onStreamThreadId 以重新获取数据
// Just reset for new thread creation when threadId becomes null/undefined if (normalizedThreadId !== threadIdRef.current) {
startedRef.current = false; startedRef.current = false;
setOnStreamThreadId(normalizedThreadId); 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: [] },
});
},
});
}

View File

@ -3,7 +3,7 @@ import type { Message } from "@langchain/langgraph-sdk";
import type { AgentThread } from "./types"; import type { AgentThread } from "./types";
export function pathOfThread(threadId: string) { export function pathOfThread(threadId: string) {
return `/workspace/chats/${threadId}`; return `/workspace/chats/new?isnew=false&thread_id=${threadId}`;
} }
export function textOfMessage(message: Message) { export function textOfMessage(message: Message) {

View File

@ -21,6 +21,22 @@ interface UseIframeSkillReturn {
clearSkill: () => void; 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 { export function useIframeSkill(): UseIframeSkillReturn {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const skillIdFromQuery = searchParams.get("skill_id"); const skillIdFromQuery = searchParams.get("skill_id");
@ -43,6 +59,24 @@ export function useIframeSkill(): UseIframeSkillReturn {
} }
}, [skillIdFromQuery, titleFromQuery]); }, [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 // 发送选择预定义 skill
const sendSelectSkill = useCallback((skill_id: string) => { const sendSelectSkill = useCallback((skill_id: string) => {
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id }; const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };