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;