fix(web): 修复正在执行的会话消息泄露到定时任务视图

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 历史消息仍正常进入
This commit is contained in:
oudecheng 2026-08-06 22:53:00 +08:00
parent f8c984aef4
commit 00735395ca
2 changed files with 48 additions and 1 deletions

View File

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

View File

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