Compare commits

...

2 Commits

Author SHA1 Message Date
aa50a72d86 fix(chat): /new页面切换工作模式被运行中对话误拦
- 运行中判定改基于请求显式携带的 conversation_id,不再用工作区级
  terminal 的残留焦点(_ensure_conversation 自动加载最近对话);
  /new 页面无 cid 直接放行(新对话默认模式与运行中对话无关)
- /new 路径切换改 persist=False,避免误写残留焦点对话的 metadata
  (plan 联动权限曾一并污染),与 _sync_workspace_terminal_mode 同策略

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
2026-09-09 02:19:30 +08:00
e5a988f00f fix(frontend): status快照模式字段按对话来源隔离,修复跨terminal广播污染
status_update 是用户级房间广播,工作区级/其他对话/其他标签页的 terminal
触发时都会携带各自的模式值;applyStatusSnapshot 此前对 work_mode/
permission_mode/execution_mode/network_permission 无条件覆盖,plan 锁对话的
UI 会被其他 terminal 的广播误解锁,用户点击切换被后端 plan 锁拒绝(500)。
现仅当快照 conversation.current_id 与当前查看对话匹配(含双空:/new 页面
↔ 工作区级 terminal)时才应用模式字段;对话权威值仍由进入对话时的
fetchWorkMode 等对话级 fetch 保证。
2026-09-09 00:33:01 +08:00
2 changed files with 73 additions and 43 deletions

View File

@ -433,15 +433,27 @@ def update_work_mode(terminal: WebTerminal, workspace: UserWorkspace, username:
"error": tr("chat_permission.invalid_work_mode") "error": tr("chat_permission.invalid_work_mode")
}), 400 }), 400
# 请求显式携带的 conversation_idquery > body与 with_terminal 路由同优先级)。
# 判定必须基于请求而非 terminal.context_manager.current_conversation_id
# /new 页面不带 cid 时路由到工作区级 terminal其焦点残留为「最近对话」
# _ensure_conversation 自动加载最近对话恢复焦点),按残留焦点判定会把
# 最近对话误判为「本对话」,其运行中会误拦 /new 页面切换(新对话默认模式
# 与任何运行中对话无关)。
req_conv_id = (request.args.get("conversation_id") or "").strip() or None
if not req_conv_id and isinstance(data, dict):
req_conv_id = (data.get("conversation_id") or "").strip() or None
# 运行中拒绝切换(无 pending 队列——运行模式不存在「工具结果后插入」的路径)。 # 运行中拒绝切换(无 pending 队列——运行模式不存在「工具结果后插入」的路径)。
# 运行模式是对话级状态,只检测本对话是否有运行中任务;其他对话运行不影响。 # 运行模式是对话级状态,只检测本对话是否有运行中任务;其他对话运行不影响;
# /new 页面(无 cid不检查直接放行。
if req_conv_id:
try: try:
from server.tasks import task_manager from server.tasks import task_manager
current_conv = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None) req_conv_norm = req_conv_id.removeprefix("conv_")
if current_conv:
conv_running = [ conv_running = [
r for r in task_manager.list_tasks(username) r for r in task_manager.list_tasks(username)
if r.status in {"pending", "running", "cancel_requested"} and r.conversation_id == current_conv if r.status in {"pending", "running", "cancel_requested"}
and (r.conversation_id or "").removeprefix("conv_") == req_conv_norm
] ]
if conv_running: if conv_running:
return jsonify({ return jsonify({
@ -454,7 +466,14 @@ def update_work_mode(terminal: WebTerminal, workspace: UserWorkspace, username:
previous_permission = terminal.get_permission_mode() if hasattr(terminal, "get_permission_mode") else None previous_permission = terminal.get_permission_mode() if hasattr(terminal, "get_permission_mode") else None
try: try:
result = terminal.switch_work_mode(target_mode) if req_conv_id:
# 对话内切换:持久化到该对话 metadata对话级唯一真相
result = terminal.switch_work_mode(target_mode, conversation_id=req_conv_id)
else:
# /new 页面:仅设工作区级内存态(新对话继承冻结读 terminal 当前值),
# persist=False 避免误写工作区 terminal 残留焦点对话的 metadata
# (与 _sync_workspace_terminal_mode 同策略)。
result = terminal.switch_work_mode(target_mode, persist=False)
except Exception as exc: except Exception as exc:
return jsonify({ return jsonify({
"success": False, "success": False,

View File

@ -161,11 +161,19 @@ export const resourceMethods = {
this.modelSet(status.model_key); this.modelSet(status.model_key);
} }
} }
// 运行模式四件套work_mode/permission_mode/execution_mode/network_permission
// 来源隔离status_update 是用户级房间广播,工作区级 / 其他对话 / 其他标签页
// 的 terminal 触发时都会携带它自己的模式值;不做隔离时别的 terminal 的快照会
// 覆盖当前对话的显示plan 锁对话的 UI 会被误解锁,点击切换又被后端 500 拒绝)。
// 仅当快照对应当前查看的对话(含双空:/new 页面 ↔ 工作区级 terminal才应用
// 对话权威值由 load.ts 进入对话时的对话级 fetchfetchWorkMode 等)保证。
const statusConvIdRaw = status?.conversation?.current_id;
const statusConvId = typeof statusConvIdRaw === 'string' ? statusConvIdRaw : '';
const modeSourceMatches = hasConversation ? statusConvId === convId : !statusConvId;
if (modeSourceMatches) {
if (status && typeof status.permission_mode === 'string') { if (status && typeof status.permission_mode === 'string') {
this.currentPermissionMode = status.permission_mode; this.currentPermissionMode = status.permission_mode;
} }
// 运行模式work_modeterminal 快照权威。对话加载后 load.ts 会再调
// fetchWorkMode 以对话 metadata 为准修正一次,两者一致,不会跳变。
if (status && typeof status.work_mode === 'string') { if (status && typeof status.work_mode === 'string') {
this.currentWorkMode = status.work_mode; this.currentWorkMode = status.work_mode;
} }
@ -186,7 +194,9 @@ export const resourceMethods = {
this.currentNetworkPermission = status.network_permission; this.currentNetworkPermission = status.network_permission;
} }
this.networkPermissionEnabled = this.networkPermissionEnabled =
typeof status.network_permission_enabled === 'boolean' ? status.network_permission_enabled : false; typeof status.network_permission_enabled === 'boolean'
? status.network_permission_enabled
: false;
if (typeof pendingModes.network_permission === 'string') { if (typeof pendingModes.network_permission === 'string') {
this.pendingNetworkPermission = pendingModes.network_permission; this.pendingNetworkPermission = pendingModes.network_permission;
} else { } else {
@ -197,6 +207,7 @@ export const resourceMethods = {
} else { } else {
this.pendingExecutionMode = ''; this.pendingExecutionMode = '';
} }
}
// has_images/has_videos 遵循与上面模型/运行模式相同的权威规则: // has_images/has_videos 遵循与上面模型/运行模式相同的权威规则:
// 有打开对话时才从 status 同步;空对话/显式新建路由(/new上 status 里的值 // 有打开对话时才从 status 同步;空对话/显式新建路由(/new上 status 里的值
// 是 terminal 残留的旧对话上下文,不能应用——空对话语义上无图片/视频, // 是 terminal 残留的旧对话上下文,不能应用——空对话语义上无图片/视频,