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(
null,
"",
`/workspace/agents/${agent_name}/chats/${threadId}`,
`/workspace/agents/${agent_name}/chats/new?isnew=false&thread_id=${threadId}`,
);
},
onFinish: (state) => {

View File

@ -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<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 初始化状态
const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false);
const skillBootstrappedKeyRef = useRef<string | null>(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()) {

View File

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

View File

@ -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");

View File

@ -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")) {

View File

@ -168,6 +168,12 @@ export function InputBox({
const [isFocused, setIsFocused] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(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 (
<div className="flex items-center gap-2">
<Tooltip content={t.inputBox.selectSkill}>
@ -1055,6 +1074,14 @@ function IframeSkillDialogButton({
</svg>
</PromptInputButton>
</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 && (
<Badge
variant="secondary"

View File

@ -59,15 +59,15 @@ export function RecentChatList() {
deleteThread({ threadId });
if (threadId === threadIdFromPath) {
const threadIndex = threads.findIndex((t) => 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);

View File

@ -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: [] },
});
},
});
}

View File

@ -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) {

View File

@ -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 };