From 04bd93646bd76992a77f9cbd3720f11064cabd70 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 29 Jul 2026 12:36:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20/new=20=E9=A1=B5=E9=9D=A2=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=E8=BF=90=E8=A1=8C=E4=B8=AD=E5=AF=B9=E8=AF=9D=E7=9A=84?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=8E=A5=E7=AE=A1=E4=B8=8E=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对话运行期间进入 /new 会把最近一次用户输入与后续 AI 输出/工具 调用渲染到空白页,刷新/重复进入均复现,任务结束后才正常。 根因是显式新建路由(currentConversationId 为空)下三层隔离缺失: - restoreTaskState 经 loadRunningTask(null) 接管任意运行中任务并 全量重放事件、恢复轮询(刷新后复现的根因) - handleTaskEvent 的对话不匹配守卫要求当前对话 id 为真才生效, 空 id 被绕过 - socket 的 thinking/text 六个流式处理器不做 conversation_id 检查 修复:显式新建路由下 restoreTaskState 直接返回;事件守卫把 "空 id + 显式新建路由"判为不匹配;流式处理器补对话守卫。 从 /new 发送消息不受影响(先建对话改路由,后才启动轮询)。 --- .../app/methods/taskPolling/compression.ts | 17 ++++++++++ .../src/app/methods/taskPolling/lifecycle.ts | 18 ++++++++--- static/src/composables/useLegacySocket.ts | 32 +++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/static/src/app/methods/taskPolling/compression.ts b/static/src/app/methods/taskPolling/compression.ts index 24e3c5ef..4c188971 100644 --- a/static/src/app/methods/taskPolling/compression.ts +++ b/static/src/app/methods/taskPolling/compression.ts @@ -112,6 +112,23 @@ export const compressionMethods = { messagesLen: Array.isArray(this.messages) ? this.messages.length : -1 }); + // 显式新建对话路由(/new、/multiagent/new):运行中的任务属于上一个对话, + // 用户主动选择了空白页。这里不能接管任务/重放事件/恢复轮询—— + // loadRunningTask(null) 会匹配任意运行中任务,一旦接管,该对话的 + // 用户输入/AI输出/工具调用会被全量重放到空白页(刷新也复现)。 + // 侧边栏运行标识由 refreshRunningWorkspaceTasks 独立维护,不受影响。 + if ( + !this.currentConversationId && + typeof this.isExplicitNewConversationRoute === 'function' && + this.isExplicitNewConversationRoute() + ) { + restoreDebugLog('restore:skip-explicit-new-route', { + path: typeof window !== 'undefined' ? window.location.pathname : '' + }); + this._restoreTaskStateWaitCount = 0; + return; + } + try { const { useTaskStore } = await import('../../../stores/task'); const taskStore = useTaskStore(); diff --git a/static/src/app/methods/taskPolling/lifecycle.ts b/static/src/app/methods/taskPolling/lifecycle.ts index f7550825..3442d916 100644 --- a/static/src/app/methods/taskPolling/lifecycle.ts +++ b/static/src/app/methods/taskPolling/lifecycle.ts @@ -70,12 +70,20 @@ export const lifecycleMethods = { 'shallow_compression', 'compression_finished' ]); - if ( - !crossConversationAllowed.has(eventType) && + // 显式新建路由(/new、/multiagent/new)上 currentConversationId 为空是常态: + // 空 id 不等于“无归属”,此时任何携带 conversation_id 的事件都属于其他对话, + // 必须丢弃,否则运行中对话的用户输入/AI输出/工具调用会渲染到空白页。 + const onExplicitNewRoute = + typeof this.isExplicitNewConversationRoute === 'function' && + this.isExplicitNewConversationRoute(); + const eventConversationMismatch = !!( eventData.conversation_id && - this.currentConversationId - ) { - if (eventData.conversation_id !== this.currentConversationId) { + (this.currentConversationId + ? eventData.conversation_id !== this.currentConversationId + : onExplicitNewRoute) + ); + if (!crossConversationAllowed.has(eventType) && eventConversationMismatch) { + { if ( [ 'ai_message_start', diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 635bd0ed..8f94572c 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -1098,7 +1098,12 @@ export async function initializeLegacySocket(ctx: any) { }); // 思考流开始 - ctx.socket.on('thinking_start', () => { + ctx.socket.on('thinking_start', (data) => { + // 对话定向事件:只响应当前对话,避免运行中对话的流式输出渲染到 /new 等空白页。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + socketLog('跳过thinking_start(对话不匹配)', data.conversation_id); + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { socketLog('思考开始 (轮询模式,跳过)'); @@ -1125,6 +1130,10 @@ export async function initializeLegacySocket(ctx: any) { // 思考内容块 ctx.socket.on('thinking_chunk', (data) => { + // 对话定向事件:只响应当前对话。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { return; @@ -1147,6 +1156,11 @@ export async function initializeLegacySocket(ctx: any) { // 思考结束 ctx.socket.on('thinking_end', (data) => { + // 对话定向事件:只响应当前对话。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + socketLog('跳过thinking_end(对话不匹配)', data.conversation_id); + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { socketLog('思考结束 (轮询模式,跳过)'); @@ -1171,7 +1185,12 @@ export async function initializeLegacySocket(ctx: any) { }); // 文本流开始 - ctx.socket.on('text_start', () => { + ctx.socket.on('text_start', (data) => { + // 对话定向事件:只响应当前对话。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + socketLog('跳过text_start(对话不匹配)', data.conversation_id); + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { socketLog('文本开始 (轮询模式,跳过)'); @@ -1191,6 +1210,10 @@ export async function initializeLegacySocket(ctx: any) { // 文本内容块 ctx.socket.on('text_chunk', (data) => { + // 对话定向事件:只响应当前对话。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { return; @@ -1228,6 +1251,11 @@ export async function initializeLegacySocket(ctx: any) { // 文本结束 ctx.socket.on('text_end', (data) => { + // 对话定向事件:只响应当前对话。 + if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { + socketLog('跳过text_end(对话不匹配)', data.conversation_id); + return; + } // 轮询模式下跳过 WebSocket 事件 if (ctx.usePollingMode && !ctx.waitingForSubAgent) { socketLog('文本结束 (轮询模式,跳过)');