fix(P0): 恢复 handleNewThread 立即创建线程,移除 ensureThread 懒创建
Trigger auto deployment for soc-langgraph / build-and-deploy (push) Failing after 19s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 49s

根因:ensureThread 异步 setCurrentThreadId 后 React 状态未同步刷新,
useStream 仍绑定 undefined threadId,thread.submit() 提交到错误线程。

修复:
- handleNewThread 恢复为 await client.threads.create()(立即创建)
- doSubmit 直接使用 currentThreadId(已同步可用)
- 移除 ensureThread、skipNextSwitchRef 等懒创建相关逻辑
- 空线程问题后续通过侧边栏过滤解决

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-15 03:08:25 +08:00
co-authored by Claude Opus 4.6
parent fd84f07f1d
commit 99b3cda23c
2 changed files with 17 additions and 26 deletions
+8 -2
View File
@@ -17,7 +17,7 @@ export function useConversation(
const [resetLoading, setResetLoading] = useState(false);
const [threadLoadFailed, setThreadLoadFailed] = useState(false);
const handleNewThread = useCallback(() => {
const handleNewThread = useCallback(async () => {
if (currentThreadId) {
try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ }
}
@@ -25,9 +25,15 @@ export function useConversation(
setHistoricalMessages([]);
setHistoricalUi([]);
setThreadLoadFailed(false);
try {
const t = await client.threads.create();
setThreads((prev) => [t as ThreadItem, ...prev]);
setCurrentThreadId(t.thread_id);
} catch {
setCurrentThreadId(null);
}
setSidebarOpen(false);
}, [currentThreadId, input, setInput]);
}, [currentThreadId, input, client, setInput]);
const handleSelectThread = useCallback(async (threadId: string) => {
if (currentThreadId) {
+8 -23
View File
@@ -43,7 +43,7 @@ function App() {
historicalMessages, historicalUi,
resetLoading, threadLoadFailed,
handleNewThread, handleSelectThread, handleDeleteThread, handleResetThread,
applyThreadTitle, ensureThread,
applyThreadTitle,
} = useConversation(client, input, setInput);
// Canvas panel state
@@ -63,8 +63,6 @@ function App() {
type ConcurrentStatus = "idle" | "generating" | "stopping" | "cancelling";
const [concurrentStatus, setConcurrentStatus] = useState<ConcurrentStatus>("idle");
const wasStoppedRef = useRef(false);
// Skip the switchThread useEffect when doSubmit already called it manually
const skipNextSwitchRef = useRef(false);
interface SubmitPayload {
text: string;
@@ -97,13 +95,8 @@ function App() {
return () => window.removeEventListener("open-canvas", handler);
}, []);
// Clear error and stale stream state when thread switches.
// Skip when doSubmit set the flag (new thread created during submit — don't clear mid-stream).
// Clear error and stale stream state when thread switches
useEffect(() => {
if (skipNextSwitchRef.current) {
skipNextSwitchRef.current = false;
return;
}
thread.switchThread(currentThreadId);
}, [currentThreadId]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -211,21 +204,13 @@ function App() {
setInput("");
setSourceLabel(null);
// Lazy thread creation: create a real thread only when the user first sends a message
const activeThreadId = await ensureThread();
if (!activeThreadId) {
if (!currentThreadId) {
setConcurrentStatus("idle");
return;
}
// For a newly created thread, currentThreadId just changed via setCurrentThreadId
// but React state hasn't flushed yet. Skip the useEffect switchThread call to avoid
// clearing thread.messages mid-stream. useStream picks up the new threadId via prop.
if (!currentThreadId) {
skipNextSwitchRef.current = true;
}
try { localStorage.removeItem(`draft_${activeThreadId}`); } catch { /* ignore */ }
updateThreadLastActive(activeThreadId);
try { localStorage.removeItem(`draft_${currentThreadId}`); } catch { /* ignore */ }
updateThreadLastActive(currentThreadId);
const enabledTools =
tools.size > 0
@@ -269,9 +254,9 @@ function App() {
// Scroll to bottom after submitting so user message stays visible
setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: "smooth" }), 100);
if (activeThreadId && text) {
const existingTitle = threads.find((t) => t.thread_id === activeThreadId)?.metadata?.title as string | undefined;
applyThreadTitle(activeThreadId, text, activeMessages.length, existingTitle);
if (currentThreadId && text) {
const existingTitle = threads.find((t) => t.thread_id === currentThreadId)?.metadata?.title as string | undefined;
applyThreadTitle(currentThreadId, text, activeMessages.length, existingTitle);
}
}