fix(chat): /new 页面隔离运行中对话的任务接管与事件推送

对话运行期间进入 /new 会把最近一次用户输入与后续 AI 输出/工具
调用渲染到空白页,刷新/重复进入均复现,任务结束后才正常。

根因是显式新建路由(currentConversationId 为空)下三层隔离缺失:
- restoreTaskState 经 loadRunningTask(null) 接管任意运行中任务并
  全量重放事件、恢复轮询(刷新后复现的根因)
- handleTaskEvent 的对话不匹配守卫要求当前对话 id 为真才生效,
  空 id 被绕过
- socket 的 thinking/text 六个流式处理器不做 conversation_id 检查

修复:显式新建路由下 restoreTaskState 直接返回;事件守卫把
"空 id + 显式新建路由"判为不匹配;流式处理器补对话守卫。
从 /new 发送消息不受影响(先建对话改路由,后才启动轮询)。
This commit is contained in:
JOJO 2026-07-29 12:36:03 +08:00
parent 4596c67967
commit 04bd93646b
3 changed files with 60 additions and 7 deletions

View File

@ -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();

View File

@ -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',

View File

@ -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('文本结束 (轮询模式,跳过)');