Files
deerflow-code/frontend-web/src/pages/EmbedChatPage.tsx
T
董宏凯andCursor b81d200eca fix: 协助卡停轮与提交防 409,隐藏配置向导会话,并限制岗位会商产物重复写入
澄清中间件改为 ToolMessage + after_model 停轮,前端提交前取消残留 run;系统/向导 thread_type 从对话列表排除;岗位总结席位加 write_file 上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-23 20:29:59 +08:00

173 lines
5.7 KiB
TypeScript

"use client";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { loginByUsername } from "@/core/auth/api";
import { setStoredAuth } from "@/core/auth";
import { resolveEmbedChatParams } from "@/core/embed/params";
import { registerEmbedSession } from "@/core/embed/status-api";
import { useThreadSettings } from "@/core/settings";
import { stopActiveThreadBeforeClarificationReply, useThreadStream } from "@/core/threads/hooks";
import { uuid } from "@/core/utils/uuid";
import { ChatBox } from "@/components/workspace/chats";
import {
MessageList,
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
} from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { useModels } from "@/core/models/hooks";
import { cn } from "@/lib/utils";
const EMBED_ROUTE_BASE = "/embed/chats";
export default function EmbedChatPage() {
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
const searchParams = useSearchParams();
// 主题不在这里处理 —— ?theme= 由 AppProviders 通过 forcedTheme 强制
// (不落 localStorage),避免污染主站用户自己选的主题。
const embedParams = resolveEmbedChatParams(searchParams.toString());
const [authReady, setAuthReady] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
const [threadId, setThreadId] = useState(() =>
threadIdFromPath === "new" ? uuid() : (threadIdFromPath ?? uuid()),
);
const [isNewThread, setIsNewThread] = useState(threadIdFromPath === "new");
const hasAutoSentRef = useRef(false);
const lastRegisteredThreadRef = useRef<string | null>(null);
useEffect(() => {
if (threadIdFromPath === "new" || !threadIdFromPath) {
return;
}
setIsNewThread(false);
setThreadId(threadIdFromPath);
}, [threadIdFromPath]);
const [settings] = useThreadSettings(threadId);
const { tokenUsageEnabled } = useModels();
useEffect(() => {
let cancelled = false;
async function runLogin() {
setAuthError(null);
try {
const auth = await loginByUsername(embedParams.username);
if (cancelled) return;
setStoredAuth(auth);
setAuthReady(true);
} catch (error) {
if (cancelled) return;
setAuthError(
error instanceof Error ? error.message : "Login request failed.",
);
}
}
void runLogin();
return () => {
cancelled = true;
};
}, [embedParams.username]);
const registerSessionForThread = useCallback(
(resolvedThreadId: string) => {
if (!embedParams.sessionId || lastRegisteredThreadRef.current === resolvedThreadId) {
return;
}
lastRegisteredThreadRef.current = resolvedThreadId;
void registerEmbedSession(embedParams.sessionId, resolvedThreadId).catch(
(error) => {
console.error("[embed] failed to register session mapping", error);
lastRegisteredThreadRef.current = null;
},
);
},
[embedParams.sessionId],
);
const [thread, sendMessage] = useThreadStream({
threadId: authReady && !isNewThread ? threadId : undefined,
context: settings.context,
isMock: false,
onStart: (createdThreadId) => {
setThreadId(createdThreadId);
setIsNewThread(false);
registerSessionForThread(createdThreadId);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${EMBED_ROUTE_BASE}/${createdThreadId}`,
);
},
});
useEffect(() => {
if (!authReady || !threadId) {
return;
}
registerSessionForThread(threadId);
}, [authReady, threadId, registerSessionForThread]);
const handleClarificationSubmit = useCallback(
async (answer: string) => {
await stopActiveThreadBeforeClarificationReply(threadId, thread);
await sendMessage(threadId, { text: answer, files: [] });
},
[sendMessage, thread, threadId],
);
useEffect(() => {
if (!authReady || !embedParams.message || hasAutoSentRef.current) {
return;
}
hasAutoSentRef.current = true;
void sendMessage(threadId, { text: embedParams.message, files: [] });
}, [authReady, embedParams.message, sendMessage, threadId]);
if (!embedParams.sessionId) {
return (
<div className="flex size-full items-center justify-center bg-background p-6 text-center text-sm text-red-600">
缺少参数 sessionId:父页面需在 iframe URL 中传入由己方生成的会话 id,用于查询问答是否结束。
</div>
);
}
if (authError) {
return (
<div className="flex size-full items-center justify-center bg-background p-6 text-sm text-red-600">
登录失败:{authError}
</div>
);
}
if (!authReady) {
return (
<div className="flex size-full items-center justify-center bg-background p-6 text-sm text-muted-foreground">
正在进入问答…
</div>
);
}
return (
<ThreadContext.Provider value={{ thread, isMock: false }}>
<ChatBox threadId={threadId}>
<div className="relative flex size-full min-h-0">
<main className="flex min-h-0 max-w-full grow flex-col">
<div className="flex size-full justify-center">
<MessageList
className={cn("size-full")}
threadId={threadId}
thread={thread}
paddingBottom={MESSAGE_LIST_DEFAULT_PADDING_BOTTOM}
tokenUsageEnabled={tokenUsageEnabled}
onClarificationSubmit={handleClarificationSubmit}
/>
</div>
</main>
</div>
</ChatBox>
</ThreadContext.Provider>
);
}