fix(server,static): 修复执行环境/权限模式切换对当前对话不生效

对话级 terminal 隔离后,模式切换请求未携带 conversation_id,
落在工作区级服务 terminal 上;而任务实际运行在对话级 terminal,
且 load_conversation 每次都会从对话 metadata 恢复创建时冻结的模式,
导致旧对话模式永远无法改变、新对话继承服务 terminal 的瞬时模式。

- 前端 permission.ts:权限/执行环境/网络权限的 POST 与 GET
  均携带当前 conversation_id,路由到对话级 terminal 并持久化到正确对话
- 后端 permission.py:切换时同步工作区级服务 terminal 的内存态
  (persist=False),保证新建对话继承最新模式而非过期模式
- 附带修复:运行期间切换的 pending 队列此前排在错误的 terminal 上,
  任务工具循环永远消费不到
This commit is contained in:
JOJO 2026-07-20 14:25:49 +08:00
parent 8628954f30
commit bd06f96d07
2 changed files with 66 additions and 7 deletions

View File

@ -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 的当前
模式冻结进 metadatacreate_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()

View File

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