From 00735395ca539bde780a75e8848c49a880105813 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 6 Aug 2026 22:53:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BF=AE=E5=A4=8D=E6=AD=A3?= =?UTF-8?q?=E5=9C=A8=E6=89=A7=E8=A1=8C=E7=9A=84=E4=BC=9A=E8=AF=9D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=B3=84=E9=9C=B2=E5=88=B0=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleSchedulerMessage (Tier 1 路由) 原先无条件吞掉所有可转换的 chat 消息,导致用户打开定时任务历史视图时,当前主会话正在执行 的 stream_delta/assistant_response 等实时消息被错误追加到 schedulerView.messages。 修复:调度器视图是历史会话查看器,只接收不带 topic_id 的历史 加载消息;实时流式消息(stream_delta/stream_end)和带 topic_id 的实时消息 fall through 到 Tier 2/3 路由到主视图,不再泄露。 - useSchedulerView.ts: handleSchedulerMessage 增加流式和 topic_id 过滤,实时消息 fall through - useChat.test.ts: 新增用例 16 验证流式和带 topic_id 消息不进入 schedulerView,无 topic_id 历史消息仍正常进入 --- web/src/hooks/chat/useSchedulerView.ts | 16 ++++++++++++- web/src/hooks/useChat.test.ts | 33 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/web/src/hooks/chat/useSchedulerView.ts b/web/src/hooks/chat/useSchedulerView.ts index 5b406e4..8e08d7e 100644 --- a/web/src/hooks/chat/useSchedulerView.ts +++ b/web/src/hooks/chat/useSchedulerView.ts @@ -75,11 +75,25 @@ export function useSchedulerView(): UseSchedulerViewReturn { setSchedulerView(null); }, []); - /** Tier 1 路由:调度器视图激活时,chat 消息追加到 schedulerView;非 chat 消息 fall through */ + /** Tier 1 路由:调度器视图激活时,仅接收属于该定时任务会话的历史消息。 + * 实时流式消息(stream_delta/stream_end)必然属于当前正在执行的会话,不走历史视图; + * 带 topic_id 的消息属于实时执行的会话(历史加载的消息不带 topic_id),也不进入。 + * 两类消息均 fall through 到 Tier 2/3 路由到主视图,避免"正在执行的会话泄露到定时任务视图"。 */ const handleSchedulerMessage = useCallback((message: WsOutbound): boolean => { const currentSchedulerView = schedulerViewRef.current; if (!currentSchedulerView) return false; + // 实时流式消息不进入调度器历史视图(历史加载不会产生流式) + if (message.type === 'stream_delta' || message.type === 'stream_end') { + return false; + } + + // 带 topic_id 的消息属于实时执行的会话,不进入历史视图 + const topicId = (message as { topic_id?: string }).topic_id; + if (topicId) { + return false; + } + const chatMsg = serverMessageToChatMessage(message); if (chatMsg) { setSchedulerView((prev) => diff --git a/web/src/hooks/useChat.test.ts b/web/src/hooks/useChat.test.ts index ad32b61..515fffa 100644 --- a/web/src/hooks/useChat.test.ts +++ b/web/src/hooks/useChat.test.ts @@ -388,6 +388,39 @@ describe('useChat - handleServerMessage characterization', () => { expect(result.current.messages.find((m) => m.id === 'm1')).toBeUndefined(); }); + it('16. scheduler view active: realtime stream_delta and topic-tagged messages do NOT leak into schedulerView', () => { + const { result } = renderUseChat(); + const lookup: SchedulerJobSessionLookup = { channel: 'scheduler', chat_id: 'job-chat' }; + act(() => result.current.enterSchedulerJobView(lookup, 'job1', 'job desc')); + expect(result.current.schedulerView).not.toBeNull(); + + // 实时流式消息不应进入历史视图(即使 schedulerView 激活) + const realtimeStream: StreamDelta = { + type: 'stream_delta', + id: 'leak1', + delta: 'leaking content', + topic_id: 't-realtime', + }; + act(() => result.current.handleServerMessage(realtimeStream)); + expect(result.current.schedulerView?.messages.find((m) => m.id === 'leak1')).toBeUndefined(); + + // 带 topic_id 的 assistant_response 也不应进入历史视图 + const taggedAssistant: AssistantResponse = { + type: 'assistant_response', + id: 'leak2', + content: 'tagged response', + role: 'assistant', + topic_id: 't-realtime', + }; + act(() => result.current.handleServerMessage(taggedAssistant)); + expect(result.current.schedulerView?.messages.find((m) => m.id === 'leak2')).toBeUndefined(); + + // 无 topic_id 的历史消息仍正常进入(保持用例 11 的行为) + act(() => result.current.handleServerMessage(assistantResponse)); + expect(result.current.schedulerView?.messages).toHaveLength(1); + expect(result.current.schedulerView?.messages[0].content).toBe('Hello world'); + }); + it('12. tool_result with tool_name=todo_write triggers a list_todos command in main view', () => { const { result, sendMessage } = renderUseChat(); const todoWriteResult: ToolResult = {