fix(chat): 显式新建对话路由隔离后端残留上下文

/new 与 /multiagent/new 上不再被 terminal 状态快照、对话广播事件
拽回上一个对话;个性化默认值在显式新建路由下保持权威。
This commit is contained in:
JOJO 2026-07-28 18:43:19 +08:00
parent 3c66c6b05f
commit 0aa8af8618
3 changed files with 77 additions and 12 deletions

View File

@ -132,10 +132,15 @@ export const resourceMethods = {
// - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复), // - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复),
// status 快照不得覆盖terminal 状态可能滞后于对话加载) // status 快照不得覆盖terminal 状态可能滞后于对话加载)
// - 空对话/首页terminal 状态即权威,从 status 同步 // - 空对话/首页terminal 状态即权威,从 status 同步
// - 例外:显式新建路由(/new、/multiagent/new上 terminal 状态属于上一个
// 对话的残留上下文,不同步(个性化默认值是这里的权威,见 loadInitialData
const convId = this.currentConversationId; const convId = this.currentConversationId;
const hasConversation = const hasConversation =
typeof convId === 'string' && convId.length > 0 && !convId.startsWith('temp_'); typeof convId === 'string' && convId.length > 0 && !convId.startsWith('temp_');
if (!hasConversation) { const onExplicitNewRoute =
typeof this.isExplicitNewConversationRoute === 'function' &&
this.isExplicitNewConversationRoute();
if (!hasConversation && !onExplicitNewRoute) {
if (status && typeof status.thinking_mode !== 'undefined') { if (status && typeof status.thinking_mode !== 'undefined') {
this.thinkingMode = !!status.thinking_mode; this.thinkingMode = !!status.thinking_mode;
} }

View File

@ -266,12 +266,22 @@ export const socketMethods = {
// 仅在“无当前对话”的新会话入口应用默认模型/模式; // 仅在“无当前对话”的新会话入口应用默认模型/模式;
// 对于已存在对话(含刷新恢复/URL指定对话必须保留对话自身设置。 // 对于已存在对话(含刷新恢复/URL指定对话必须保留对话自身设置。
// 注意:显式新建路由(/new、/multiagent/new后端的“当前对话”
// 只是上一个对话的残留上下文,不等于本页要创建新对话的意图,
// 因此此时也必须应用个性化默认值(与“新建空对话”按钮行为一致)。
const statusConversationIdForDefaults = statusData?.conversation?.current_id; const statusConversationIdForDefaults = statusData?.conversation?.current_id;
const hasActiveConversation = const hasActiveConversation =
typeof statusConversationIdForDefaults === 'string' && typeof statusConversationIdForDefaults === 'string' &&
statusConversationIdForDefaults.length > 0 && statusConversationIdForDefaults.length > 0 &&
!statusConversationIdForDefaults.startsWith('temp_'); !statusConversationIdForDefaults.startsWith('temp_');
if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) { const isExplicitNewRouteForDefaults =
typeof this.isExplicitNewConversationRoute === 'function' &&
this.isExplicitNewConversationRoute();
if (
personalizationStore.loaded &&
!this.currentConversationId &&
(!hasActiveConversation || isExplicitNewRouteForDefaults)
) {
const defaultRunMode = personalizationStore.form.default_run_mode; const defaultRunMode = personalizationStore.form.default_run_mode;
const defaultModel = personalizationStore.form.default_model; const defaultModel = personalizationStore.form.default_model;
const defaultReasoningEffort = personalizationStore.form.default_reasoning_effort; const defaultReasoningEffort = personalizationStore.form.default_reasoning_effort;
@ -324,15 +334,16 @@ export const socketMethods = {
const runningConversationId = await this.getRunningTaskConversationId(); const runningConversationId = await this.getRunningTaskConversationId();
const statusConversationId = statusData.conversation && statusData.conversation.current_id; const statusConversationId = statusData.conversation && statusData.conversation.current_id;
const resumeConversationId = statusConversationId || runningConversationId; const resumeConversationId = statusConversationId || runningConversationId;
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
const currentPath = window.location.pathname.replace(/^\/+/, ''); const currentPath = window.location.pathname.replace(/^\/+/, '');
const isMultiAgentNewRoute = currentPath === 'multiagent/new' || currentPath === 'multiagent'; const isMultiAgentNewRoute = currentPath === 'multiagent/new' || currentPath === 'multiagent';
// 显式新建对话路由(/new、/multiagent/new永不自动恢复运行中的对话
// 用户主动点击"新建对话"就是明确要空白页,运行中任务可从侧边栏随时切回。
if ( if (
resumeConversationId && resumeConversationId &&
!this.currentConversationId && !this.currentConversationId &&
!isMultiAgentNewRoute && !isMultiAgentNewRoute &&
(!isExplicitNewRoute || shouldResumeOnNewRoute) !isExplicitNewRoute
) { ) {
this.skipConversationHistoryReload = true; this.skipConversationHistoryReload = true;
// 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载 // 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载

View File

@ -772,8 +772,16 @@ export async function initializeLegacySocket(ctx: any) {
// 初始化期间不修改 currentConversationId避免与 bootstrapRoute 冲突 // 初始化期间不修改 currentConversationId避免与 bootstrapRoute 冲突
if (ctx.initialRouteResolved) { if (ctx.initialRouteResolved) {
// 用户停留在显式新建对话页(/new、/multiagent/new不被其他对话的
// 广播事件(如运行中任务的标题生成)拽走视图,仅同步列表数据。
const stayOnNewRoute =
!data.cleared &&
typeof ctx.isExplicitNewConversationRoute === 'function' &&
ctx.isExplicitNewConversationRoute();
if (!stayOnNewRoute) {
ctx.currentConversationId = data.conversation_id; ctx.currentConversationId = data.conversation_id;
ctx.currentConversationTitle = data.title || ''; ctx.currentConversationTitle = data.title || '';
}
// 更新对话列表中的标题 // 更新对话列表中的标题
if (data.title && Array.isArray(ctx.conversations)) { if (data.title && Array.isArray(ctx.conversations)) {
@ -897,19 +905,27 @@ export async function initializeLegacySocket(ctx: any) {
// 监听状态更新事件 // 监听状态更新事件
ctx.socket.on('status_update', (status) => { ctx.socket.on('status_update', (status) => {
ctx.applyStatusSnapshot(status); ctx.applyStatusSnapshot(status);
// 显式新建路由(/new、/multiagent/newstatus 快照反映的是后端 terminal
// 残留的旧对话上下文,既不能把它设成当前对话(防跳回),也不能让它
// 覆盖刚应用的个性化默认运行模式。
const onExplicitNewRoute =
typeof ctx.isExplicitNewConversationRoute === 'function' &&
ctx.isExplicitNewConversationRoute();
// 只有在初始化完成后,才允许 status_update 修改 currentConversationId // 只有在初始化完成后,才允许 status_update 修改 currentConversationId
// 避免与 bootstrapRoute 冲突 // 避免与 bootstrapRoute 冲突
if (status.conversation && status.conversation.current_id) { if (status.conversation && status.conversation.current_id) {
if (ctx.initialRouteResolved && !ctx.currentConversationId) { if (ctx.initialRouteResolved && !ctx.currentConversationId && !onExplicitNewRoute) {
// 初始化完成且当前没有对话ID才设置 // 初始化完成且当前没有对话ID才设置
ctx.currentConversationId = status.conversation.current_id; ctx.currentConversationId = status.conversation.current_id;
} }
} }
if (!onExplicitNewRoute) {
if (typeof status.run_mode === 'string') { if (typeof status.run_mode === 'string') {
ctx.runMode = status.run_mode; ctx.runMode = status.run_mode;
} else if (typeof status.thinking_mode !== 'undefined') { } else if (typeof status.thinking_mode !== 'undefined') {
ctx.runMode = status.thinking_mode ? 'thinking' : 'fast'; ctx.runMode = status.thinking_mode ? 'thinking' : 'fast';
} }
}
}); });
// 用户消息(后台子智能体完成后自动触发) // 用户消息(后台子智能体完成后自动触发)
@ -1068,7 +1084,12 @@ export async function initializeLegacySocket(ctx: any) {
}); });
// AI消息开始已废弃改用 REST API 轮询) // AI消息开始已废弃改用 REST API 轮询)
ctx.socket.on('ai_message_start', () => { ctx.socket.on('ai_message_start', (data) => {
// 用户级广播:只响应当前对话的 AI 消息开始事件。
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过ai_message_start(对话不匹配)', data.conversation_id);
return;
}
// 只处理子智能体模式 // 只处理子智能体模式
if (ctx.waitingForSubAgent) { if (ctx.waitingForSubAgent) {
ctx.waitingForSubAgent = false; ctx.waitingForSubAgent = false;
@ -1600,6 +1621,11 @@ export async function initializeLegacySocket(ctx: any) {
// 停止请求确认 // 停止请求确认
ctx.socket.on('stop_requested', (data) => { ctx.socket.on('stop_requested', (data) => {
// 用户级广播:只清理当前对话的 pending 工具状态。
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过stop_requested(对话不匹配)', data.conversation_id);
return;
}
socketLog('停止请求已接收:', data.message); socketLog('停止请求已接收:', data.message);
// 可以显示提示信息 // 可以显示提示信息
try { try {
@ -1613,6 +1639,13 @@ export async function initializeLegacySocket(ctx: any) {
// 任务停止 // 任务停止
ctx.socket.on('task_stopped', (data) => { ctx.socket.on('task_stopped', (data) => {
// 多对话并行运行:任务生命周期事件是用户级广播,只响应当前对话的,
// 否则其他对话的停止会误清本对话的运行态(侧边栏标识由
// refreshRunningWorkspaceTasks 独立轮询维护,不受影响)。
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过task_stopped(对话不匹配)', data.conversation_id);
return;
}
socketLog('任务已停止:', data.message); socketLog('任务已停止:', data.message);
const hasRunningSubAgents = !!data?.has_running_sub_agents; const hasRunningSubAgents = !!data?.has_running_sub_agents;
const hasRunningBackgroundCommands = !!data?.has_running_background_commands; const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
@ -1634,6 +1667,11 @@ export async function initializeLegacySocket(ctx: any) {
// 任务完成重点更新Token统计 // 任务完成重点更新Token统计
ctx.socket.on('task_complete', (data) => { ctx.socket.on('task_complete', (data) => {
// 同 task_stopped用户级广播只响应当前对话的任务完成事件。
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过task_complete(对话不匹配)', data.conversation_id);
return;
}
socketLog('任务完成', data); socketLog('任务完成', data);
const hasRunningMultiAgent = !!data?.has_running_multi_agent; const hasRunningMultiAgent = !!data?.has_running_multi_agent;
@ -1660,6 +1698,12 @@ export async function initializeLegacySocket(ctx: any) {
// 子智能体等待状态 // 子智能体等待状态
ctx.socket.on('sub_agent_waiting', (data) => { ctx.socket.on('sub_agent_waiting', (data) => {
// 用户级广播:只响应当前对话的等待事件,避免把其他对话的
// waitingForSubAgent 标志安到本对话上(会阻塞 resetAllStates
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过sub_agent_waiting(对话不匹配)', data.conversation_id);
return;
}
socketLog('等待子智能体完成:', data); socketLog('等待子智能体完成:', data);
// 多智能体模式下,子智能体 idle/running 是常态,不阻塞主对话输入区 // 多智能体模式下,子智能体 idle/running 是常态,不阻塞主对话输入区
@ -1711,6 +1755,11 @@ export async function initializeLegacySocket(ctx: any) {
if (!data || !data.content) { if (!data || !data.content) {
return; return;
} }
// 用户级广播:其他对话的系统提示不应串进当前对话视图。
if (data.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过system_message(对话不匹配)', data.conversation_id);
return;
}
ctx.appendSystemAction(data.content); ctx.appendSystemAction(data.content);
}); });