diff --git a/core/web_terminal.py b/core/web_terminal.py index d7d2c9cb..8669abcf 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -35,7 +35,11 @@ class WebTerminal(MainTerminal): 对话级 terminal(_bound_conversation_id 非空):加载绑定的对话; 加载失败不 fallback 到“最近对话”(避免串对话),保持无对话状态, 由任务执行链路的 ensure_conversation_loaded 显式处理。 - 工作区级 terminal(未绑定):保持原有“最近对话”行为。 + 对话级 terminal 只服务绑定对话,必须恢复该对话保存的模型 + (restore_model=True),否则重启后新建的对话级 terminal 会用默认 + 模型跑任务/回写 metadata,把用户切换的模型覆盖掉。 + 工作区级 terminal(未绑定):保持原有“最近对话”行为,且刻意不恢复 + 模型(restore_model=False),避免 /new 页面显示旧对话的模型。 """ if self.context_manager.current_conversation_id: return @@ -44,7 +48,7 @@ class WebTerminal(MainTerminal): if bound_id: if not str(bound_id).startswith("conv_"): bound_id = f"conv_{bound_id}" - result = self.load_conversation(bound_id, restore_model=False) + result = self.load_conversation(bound_id, restore_model=True) if result.get("success"): print(f"[WebTerminal] 已加载绑定对话: {bound_id}") else: diff --git a/server/chat/settings.py b/server/chat/settings.py index bff2326c..9d1b4498 100644 --- a/server/chat/settings.py +++ b/server/chat/settings.py @@ -164,23 +164,38 @@ def update_model(terminal: WebTerminal, workspace: UserWorkspace, username: str) session["run_mode"] = terminal.run_mode session["thinking_mode"] = terminal.thinking_mode - # 更新当前对话元数据 + # 更新对话元数据:仅当请求显式携带 conversation_id 时持久化。 + # 对话级隔离后,未带 conversation_id 时拿到的是工作区级 terminal, + # 其 current_conversation_id 可能是陈旧对话(安全导航/切换后), + # 盲目保存会把模型写到错误对话,或用陈旧 history 覆盖新消息。 + # /new 页面(无 conversation_id)不持久化,新对话在首个任务时 + # 由前端随任务传入的 model_key 落盘。 + requested_cid = str(data.get("conversation_id") or "").strip() + if requested_cid and not requested_cid.startswith("conv_"): + requested_cid = f"conv_{requested_cid}" ctx = terminal.context_manager - if ctx.current_conversation_id: - try: - ctx.conversation_manager.save_conversation( - conversation_id=ctx.current_conversation_id, - messages=ctx.conversation_history, - project_path=str(ctx.project_path), - todo_list=ctx.todo_list, - thinking_mode=terminal.thinking_mode, - run_mode=terminal.run_mode, - model_key=terminal.model_key, - has_images=getattr(ctx, "has_images", False), - has_videos=getattr(ctx, "has_videos", False) + current_cid = getattr(ctx, "current_conversation_id", None) + if requested_cid and current_cid: + if current_cid == requested_cid: + try: + ctx.conversation_manager.save_conversation( + conversation_id=current_cid, + messages=ctx.conversation_history, + project_path=str(ctx.project_path), + todo_list=ctx.todo_list, + thinking_mode=terminal.thinking_mode, + run_mode=terminal.run_mode, + model_key=terminal.model_key, + has_images=getattr(ctx, "has_images", False), + has_videos=getattr(ctx, "has_videos", False) + ) + except Exception as exc: + print(f"[API] 保存模型到对话失败: {exc}") + else: + print( + f"[API] 跳过模型保存:请求对话 {requested_cid} " + f"与 terminal 当前对话 {current_cid} 不一致" ) - except Exception as exc: - print(f"[API] 保存模型到对话失败: {exc}") status = terminal.get_status() socketio.emit('status_update', status, room=f"user_{username}") diff --git a/server/context.py b/server/context.py index 322aa054..9a54cd3d 100644 --- a/server/context.py +++ b/server/context.py @@ -122,16 +122,28 @@ def _apply_workspace_personalization_preferences(terminal: WebTerminal, workspac if isinstance(raw_session_model, str) and raw_session_model.strip(): session_model = raw_session_model.strip() + # 对话级 terminal(_bound_conversation_id)的模型由绑定加载权威恢复 + # (对话文件 metadata.model_key),session 级模型是全局的最后选择, + # 不能覆盖到某个具体对话的 terminal 上,否则重启后进入对话会被 + # session 里的其它模型回写(模型回变默认的 bug)。 + is_conversation_bound = bool(getattr(terminal, "_bound_conversation_id", None)) + # default_model 是“新会话初始偏好”,不能在每次 /api/status、任务创建、 # 加载资源时覆盖用户已经在当前会话里手动切换的模型。 - if session_model and session_model != getattr(terminal, "model_key", None): + if ( + session_model + and not is_conversation_bound + and session_model != getattr(terminal, "model_key", None) + ): try: terminal.set_model(session_model) except Exception as exc: debug_log(f"[Personalization] 恢复会话模型失败: {session_model} ({exc})") - apply_default_model = not bool(session_model) and not bool( - getattr(terminal, "_workspace_default_model_applied", False) + apply_default_model = ( + not is_conversation_bound + and not bool(session_model) + and not bool(getattr(terminal, "_workspace_default_model_applied", False)) ) terminal.apply_personalization_preferences(config, apply_default_model=apply_default_model) if apply_default_model: diff --git a/static/src/app/methods/ui/model.ts b/static/src/app/methods/ui/model.ts index 224599be..2c3d1c16 100644 --- a/static/src/app/methods/ui/model.ts +++ b/static/src/app/methods/ui/model.ts @@ -72,10 +72,16 @@ export const modelMethods = { } const prev = this.currentModelKey; try { + // 携带当前对话 id:对话级隔离后,后端需要把模型设置到该对话专属的 + // 对话级 terminal 并保存到正确的对话文件,避免写到工作区级 terminal + // 的陈旧对话(否则重启后模型回变为默认)。 const resp = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model_key: key }) + body: JSON.stringify({ + model_key: key, + conversation_id: this.currentConversationId || undefined + }) }); const payload = await resp.json(); if (!resp.ok || !payload.success) {