From bd06f96d07ebf90cde0f2d1910c4b32e8ee73994 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 20 Jul 2026 14:25:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(server,static):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E7=8E=AF=E5=A2=83/=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=E5=AF=B9=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E4=B8=8D=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对话级 terminal 隔离后,模式切换请求未携带 conversation_id, 落在工作区级服务 terminal 上;而任务实际运行在对话级 terminal, 且 load_conversation 每次都会从对话 metadata 恢复创建时冻结的模式, 导致旧对话模式永远无法改变、新对话继承服务 terminal 的瞬时模式。 - 前端 permission.ts:权限/执行环境/网络权限的 POST 与 GET 均携带当前 conversation_id,路由到对话级 terminal 并持久化到正确对话 - 后端 permission.py:切换时同步工作区级服务 terminal 的内存态 (persist=False),保证新建对话继承最新模式而非过期模式 - 附带修复:运行期间切换的 pending 队列此前排在错误的 terminal 上, 任务工具循环永远消费不到 --- server/chat/permission.py | 34 ++++++++++++++++++++- static/src/app/methods/ui/permission.ts | 39 +++++++++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/server/chat/permission.py b/server/chat/permission.py index 41198331..6f62ab55 100644 --- a/server/chat/permission.py +++ b/server/chat/permission.py @@ -46,7 +46,7 @@ from core.web_terminal import WebTerminal from config.model_profiles import get_model_context_window from server.auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username -from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker +from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker, get_user_resources from server.security import rate_limited, prune_socket_tokens from server.utils_common import debug_log from server.state import PROJECT_MAX_STORAGE_MB, THINKING_FAILURE_KEYWORDS, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS @@ -60,6 +60,32 @@ import re UPLOAD_FOLDER_NAME = ".astrion/user_upload" +def _sync_workspace_terminal_mode(username: str, workspace, kind: str, mode: str) -> None: + """把模式切换同步到工作区级服务 terminal(仅内存态,不持久化)。 + + 对话级隔离后,携带 conversation_id 的切换请求落在对话级 terminal 上, + 工作区级 terminal 仍保持旧模式;而新建对话会以工作区 terminal 的当前 + 模式冻结进 metadata(create_new_conversation),导致新对话继承过期模式。 + 这里同步内存态即可:persist=False,避免误写工作区 terminal 上的陈旧对话。 + """ + try: + ws_terminal, _ws = get_user_resources( + username, + workspace_id=getattr(workspace, "workspace_id", None), + conversation_id=None, + ) + if not ws_terminal: + return + if kind == "permission_mode": + ws_terminal.set_permission_mode(mode, persist=False) + elif kind == "execution_mode": + ws_terminal.set_execution_mode(mode) + elif kind == "network_permission": + ws_terminal.set_network_permission(mode) + except Exception: + pass + + def _path_conflicts_with_deny_list(path: str) -> Optional[str]: """检查用户授权路径是否与内置 deny 列表冲突,返回错误信息或 None。""" if not path: @@ -125,6 +151,7 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user # 避免「A→B→A」来回切换时中间值被覆盖后仍插入多余消息。 try: terminal.queue_permission_mode_change(target_mode) + _sync_workspace_terminal_mode(username, workspace, "permission_mode", target_mode) except Exception as exc: return jsonify({ "success": False, @@ -153,6 +180,7 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user "permission_mode": applied_mode, "pending_permission_mode": None, }) + _sync_workspace_terminal_mode(username, workspace, "permission_mode", applied_mode) except Exception as exc: return jsonify({ "success": False, @@ -228,6 +256,7 @@ def update_execution_mode(terminal: WebTerminal, workspace: UserWorkspace, usern # 避免「A→B→A」来回切换时中间值被覆盖后仍插入多余消息。 try: terminal.queue_execution_mode_change(target_mode) + _sync_workspace_terminal_mode(username, workspace, "execution_mode", target_mode) except Exception as exc: return jsonify({"success": False, "error": str(exc), "message": "更新执行环境失败"}), 500 status = terminal.get_status() @@ -254,6 +283,7 @@ def update_execution_mode(terminal: WebTerminal, workspace: UserWorkspace, usern "execution_mode": state.get("mode", target_mode), "pending_execution_mode": None, }) + _sync_workspace_terminal_mode(username, workspace, "execution_mode", state.get("mode", target_mode)) except Exception as exc: return jsonify({"success": False, "error": str(exc), "message": "更新执行环境失败"}), 500 status = terminal.get_status() @@ -315,6 +345,7 @@ def update_network_permission(terminal: WebTerminal, workspace: UserWorkspace, u if is_running: try: terminal.queue_network_permission_change(target_mode) + _sync_workspace_terminal_mode(username, workspace, "network_permission", target_mode) except Exception as exc: return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500 status = terminal.get_status() @@ -337,6 +368,7 @@ def update_network_permission(terminal: WebTerminal, workspace: UserWorkspace, u "network_permission": applied, "pending_network_permission": None, }) + _sync_workspace_terminal_mode(username, workspace, "network_permission", applied) except Exception as exc: return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500 status = terminal.get_status() diff --git a/static/src/app/methods/ui/permission.ts b/static/src/app/methods/ui/permission.ts index 0dde088e..e04957a3 100644 --- a/static/src/app/methods/ui/permission.ts +++ b/static/src/app/methods/ui/permission.ts @@ -70,7 +70,13 @@ export const permissionMethods = { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode: target }) + body: JSON.stringify({ + mode: target, + // 对话级隔离:携带当前对话 ID,让后端把模式设置到对话级 terminal + // (任务实际运行的实例),并持久化到当前对话 metadata; + // /new 页面无对话时回退到工作区级 terminal(新对话创建时继承)。 + ...(this.currentConversationId ? { conversation_id: this.currentConversationId } : {}) + }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { @@ -106,7 +112,13 @@ export const permissionMethods = { const response = await fetch('/api/execution-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode: target }) + body: JSON.stringify({ + mode: target, + // 对话级隔离:携带当前对话 ID,让后端把模式设置到对话级 terminal + // (任务实际运行的实例),并持久化到当前对话 metadata; + // /new 页面无对话时回退到工作区级 terminal(新对话创建时继承)。 + ...(this.currentConversationId ? { conversation_id: this.currentConversationId } : {}) + }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { @@ -151,7 +163,13 @@ export const permissionMethods = { const response = await fetch('/api/network-permission', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode: target }) + body: JSON.stringify({ + mode: target, + // 对话级隔离:携带当前对话 ID,让后端把模式设置到对话级 terminal + // (任务实际运行的实例),并持久化到当前对话 metadata; + // /new 页面无对话时回退到工作区级 terminal(新对话创建时继承)。 + ...(this.currentConversationId ? { conversation_id: this.currentConversationId } : {}) + }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { @@ -179,7 +197,10 @@ export const permissionMethods = { }, async fetchNetworkPermission() { try { - const response = await fetch('/api/network-permission'); + const query = this.currentConversationId + ? `?conversation_id=${encodeURIComponent(this.currentConversationId)}` + : ''; + const response = await fetch(`/api/network-permission${query}`); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { return; @@ -195,7 +216,10 @@ export const permissionMethods = { }, async fetchPermissionMode() { try { - const response = await fetch('/api/permission-mode'); + const query = this.currentConversationId + ? `?conversation_id=${encodeURIComponent(this.currentConversationId)}` + : ''; + const response = await fetch(`/api/permission-mode${query}`); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { return; @@ -212,7 +236,10 @@ export const permissionMethods = { try { const prevExecutionMode = this.currentExecutionMode; const prevDirectUntil = this.executionModeDirectUntil; - const response = await fetch('/api/execution-mode'); + const query = this.currentConversationId + ? `?conversation_id=${encodeURIComponent(this.currentConversationId)}` + : ''; + const response = await fetch(`/api/execution-mode${query}`); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.success) { return;