fix(model): 修复重启后进入对话模型回变为默认模型

对话级隔离后存在四处模型覆盖/错存路径:
1. 对话级 terminal 绑定加载用 restore_model=False,重启后新建的
   对话级 terminal 丢失对话保存的模型 -> 绑定加载恢复模型
2. /api/model 把工作区级 terminal 的陈旧 current_conversation_id
   当目标保存,模型写到错误对话或丢失(且用陈旧 history 全量保存
   有覆盖新消息风险)-> 仅当请求显式携带 conversation_id 且与
   terminal 当前对话一致时才持久化
3. 前端切换模型不携带 conversation_id -> handleModelSelect 带上
4. _apply_workspace_personalization_preferences 把 session 全局模型
   /个性化默认模型覆盖到对话级 terminal -> 对话级 terminal 跳过,
   其模型由绑定加载权威确定
This commit is contained in:
JOJO 2026-07-20 02:20:06 +08:00
parent a4eb688814
commit 1289c31dfe
4 changed files with 58 additions and 21 deletions

View File

@ -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:

View File

@ -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}")

View File

@ -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_keysession 级模型是全局的最后选择,
# 不能覆盖到某个具体对话的 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:

View File

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