fix(server,static): 网络权限实例级快照与快捷窗口跨对话残留消闪
网络权限此前以进程级 os.environ 为唯一真相:每个 WebTerminal 初始化 都把 env 重置为默认值(切 full 后任何新 terminal 创建即失效),多实例 并发互相覆盖;后台指令与持久终端 shell 直接读 env,导致权限不生效或 跨工作区串扰。 - 唯一真相改为 WebTerminal 实例属性,init/set 不再写 env - 后台 run_command: create_background_command 快照传入执行线程 - 持久终端 shell: 注入 network_permission_getter 实时取所属终端实例的值 - 快捷窗口: backgroundCommand/subAgent store 在 fetch 前同步剔除 conversation_id 不属于当前对话的残留记录,消除「旧对话数据闪一下 再清空」造成的“任务被停了”误解(后端任务实际一直在运行) 验证: py_compile 通过;冒烟 6 用例 OK;前端构建成功
This commit is contained in:
parent
be061bec78
commit
165c771cc7
@ -129,6 +129,7 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
terminal_display_size=TERMINAL_DISPLAY_SIZE,
|
||||
broadcast_callback=None, # CLI模式不需要广播
|
||||
container_session=container_session,
|
||||
network_permission_getter=self.get_network_permission,
|
||||
)
|
||||
# 让 run_command 复用终端容器,保持环境一致
|
||||
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
|
||||
@ -264,7 +265,9 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
if default_permission not in {"restricted", "full", "none"}:
|
||||
default_permission = "restricted"
|
||||
self.host_network_permission = default_permission
|
||||
os.environ["HOST_SANDBOX_NETWORK_PERMISSION"] = default_permission
|
||||
# 注意:不再写入 os.environ——网络权限的唯一真相是本实例属性;
|
||||
# 进程级 env 会被多个 terminal 实例互相覆盖,曾导致后台指令/
|
||||
# 持久终端读到其他实例的权限(不生效或跨工作区串扰)。
|
||||
|
||||
def get_network_permission(self) -> str:
|
||||
return getattr(self, "host_network_permission", "restricted")
|
||||
@ -274,7 +277,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
if normalized not in {"restricted", "full", "none"}:
|
||||
raise ValueError("无效网络权限,仅支持 restricted / full / none")
|
||||
self.host_network_permission = normalized
|
||||
os.environ["HOST_SANDBOX_NETWORK_PERMISSION"] = normalized
|
||||
return normalized
|
||||
|
||||
def get_pending_runtime_modes(self) -> Dict[str, Optional[str]]:
|
||||
|
||||
@ -1683,6 +1683,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
timeout=timeout_value,
|
||||
conversation_id=getattr(self.context_manager, "current_conversation_id", None),
|
||||
wait_seconds=5.0,
|
||||
network_permission=network_permission,
|
||||
)
|
||||
else:
|
||||
if timeout_value is None or float(timeout_value) <= 0:
|
||||
|
||||
@ -120,6 +120,9 @@ class WebTerminal(MainTerminal):
|
||||
# 导致每次创建对话级 terminal 都输出“关闭所有终端会话”噪音日志。
|
||||
if self.terminal_manager is not None:
|
||||
self.terminal_manager.broadcast = message_callback
|
||||
# 复用父类实例时也要确保 getter 已注入(父类创建时已传入,防御旧实例)
|
||||
if getattr(self.terminal_manager, "network_permission_getter", None) is None:
|
||||
self.terminal_manager.network_permission_getter = self.get_network_permission
|
||||
else:
|
||||
self.terminal_manager = TerminalManager(
|
||||
project_path=project_path,
|
||||
@ -127,7 +130,8 @@ class WebTerminal(MainTerminal):
|
||||
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
||||
terminal_display_size=TERMINAL_DISPLAY_SIZE,
|
||||
broadcast_callback=message_callback,
|
||||
container_session=self.container_session
|
||||
container_session=self.container_session,
|
||||
network_permission_getter=self.get_network_permission,
|
||||
)
|
||||
# 让 run_command 与实时终端共享同一容器环境
|
||||
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
|
||||
|
||||
@ -41,6 +41,7 @@ class BackgroundCommandManager:
|
||||
timeout: Optional[int],
|
||||
conversation_id: Optional[str],
|
||||
wait_seconds: float = 5.0,
|
||||
network_permission: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""启动后台命令;先等待一小段时间返回已有输出。"""
|
||||
if timeout is None or float(timeout) <= 0:
|
||||
@ -114,6 +115,9 @@ class BackgroundCommandManager:
|
||||
"session": session_override or getattr(terminal_ops, "container_session", None),
|
||||
"python_env": getattr(terminal_ops, "_python_env", None) or {},
|
||||
"host_execution_mode": getattr(terminal_ops, "host_execution_mode", "sandbox"),
|
||||
# 网络权限必须按调用方(对话级 terminal)快照传入,不能读进程级
|
||||
# 环境变量——env 会被其他 terminal 实例覆盖,导致权限不生效或跨工作区串扰。
|
||||
"network_permission": network_permission,
|
||||
},
|
||||
name=f"bg-run-command-{command_id}",
|
||||
daemon=True,
|
||||
@ -181,6 +185,7 @@ class BackgroundCommandManager:
|
||||
session,
|
||||
python_env: Dict[str, str],
|
||||
host_execution_mode: str = "sandbox",
|
||||
network_permission: Optional[str] = None,
|
||||
) -> None:
|
||||
start_ts = time.time()
|
||||
process: Optional[subprocess.Popen] = None
|
||||
@ -230,8 +235,13 @@ class BackgroundCommandManager:
|
||||
if use_shell:
|
||||
use_host_sandbox = host_execution_mode != "direct"
|
||||
if use_host_sandbox and host_sandbox_enabled():
|
||||
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
plan = build_host_sandbox_plan(command, work_path, env, network_permission=network_permission)
|
||||
# 优先使用创建时快照的权限;缺省才回落环境变量(兼容旧调用方)。
|
||||
effective_network_permission = (
|
||||
str(network_permission).strip().lower()
|
||||
if network_permission
|
||||
else os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
)
|
||||
plan = build_host_sandbox_plan(command, work_path, env, network_permission=effective_network_permission)
|
||||
cmd_args = plan.command
|
||||
pass_fds = ()
|
||||
seccomp_fd = None
|
||||
|
||||
@ -77,6 +77,7 @@ class PersistentTerminalBase:
|
||||
project_path: Optional[str] = None,
|
||||
sandbox_mode: Optional[str] = None,
|
||||
sandbox_options: Optional[Dict] = None,
|
||||
network_permission_getter: Optional[Callable] = None,
|
||||
):
|
||||
"""
|
||||
初始化持久化终端
|
||||
@ -97,6 +98,9 @@ class PersistentTerminalBase:
|
||||
self.broadcast = broadcast_callback
|
||||
self.max_buffer_size = max_buffer_size
|
||||
self.display_size = display_size
|
||||
# 网络权限来源:优先由所属 WebTerminal 注入 getter 实时取值;
|
||||
# 不可用时才回落进程级环境变量(兼容 toolbox_container 等独立使用方)。
|
||||
self.network_permission_getter = network_permission_getter
|
||||
|
||||
# 进程相关
|
||||
self.process = None
|
||||
@ -163,6 +167,18 @@ class PersistentTerminalBase:
|
||||
self._sandbox_bin_path = None
|
||||
self._owns_container = False
|
||||
|
||||
def _resolve_network_permission(self) -> str:
|
||||
"""取当前网络权限设置:getter 实时值优先,环境变量兼容回落。"""
|
||||
getter = getattr(self, "network_permission_getter", None)
|
||||
if callable(getter):
|
||||
try:
|
||||
value = getter()
|
||||
if value:
|
||||
return str(value).strip().lower()
|
||||
except Exception:
|
||||
pass
|
||||
return os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保进程被关闭"""
|
||||
if hasattr(self, 'is_running') and self.is_running:
|
||||
|
||||
@ -152,7 +152,8 @@ class StartMixin:
|
||||
env['LC_ALL'] = 'en_US.UTF-8'
|
||||
|
||||
try:
|
||||
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
# 实时读取所属终端实例的权限设置,而非进程级 env(多 terminal 并存时 env 会被覆盖)
|
||||
network_permission = self._resolve_network_permission()
|
||||
plan = build_host_sandbox_shell_plan(
|
||||
self.working_dir, env, network_permission=network_permission
|
||||
)
|
||||
|
||||
@ -75,6 +75,7 @@ class TerminalManager:
|
||||
sandbox_mode: Optional[str] = None,
|
||||
sandbox_options: Optional[Dict] = None,
|
||||
container_session: Optional["ContainerHandle"] = None,
|
||||
network_permission_getter: Optional[Callable] = None,
|
||||
):
|
||||
"""
|
||||
初始化终端管理器
|
||||
@ -123,6 +124,9 @@ class TerminalManager:
|
||||
if sandbox_options and sandbox_options.get("container_name"):
|
||||
self.sandbox_mode = "docker"
|
||||
self._apply_container_session(container_session)
|
||||
# 网络权限 getter(由所属 WebTerminal 注入):新建持久终端时透传,
|
||||
# 使 shell 沙箱实时跟随该终端实例的权限设置而非进程级 env。
|
||||
self.network_permission_getter = network_permission_getter
|
||||
# 终端命令超时工具(兼容 macOS 无 coreutils 的情况)
|
||||
self._timeout_bin = shutil.which("timeout") or shutil.which("gtimeout")
|
||||
|
||||
@ -248,7 +252,8 @@ class TerminalManager:
|
||||
display_size=self.terminal_display_size,
|
||||
project_path=str(self.project_path),
|
||||
sandbox_mode=self.sandbox_mode,
|
||||
sandbox_options=sandbox_options
|
||||
sandbox_options=sandbox_options,
|
||||
network_permission_getter=self.network_permission_getter
|
||||
)
|
||||
|
||||
# 启动终端
|
||||
|
||||
@ -5,6 +5,7 @@ interface BackgroundCommand {
|
||||
command_id: string;
|
||||
status?: string;
|
||||
command?: string;
|
||||
conversation_id?: string;
|
||||
notice_pending?: boolean;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
@ -60,6 +61,12 @@ export const useBackgroundCommandStore = defineStore('backgroundCommand', {
|
||||
return;
|
||||
}
|
||||
const query = convQuery();
|
||||
// 同步剔除不属于当前对话的残留记录:切对话/工作区后旧对话数据
|
||||
// 会先渲染一拍(「闪一下」)再被 fetch 覆盖清空,造成“指令被停了”的误解。
|
||||
// 属于当前对话的记录保留,避免布局「先收起再展开」闪烁。
|
||||
this.commands = this.commands.filter(
|
||||
(item) => !item.conversation_id || item.conversation_id === convId
|
||||
);
|
||||
const resp = await fetch(
|
||||
`/api/background_commands?limit=200${query ? `&${query}` : ''}`
|
||||
);
|
||||
|
||||
@ -61,6 +61,12 @@ export const useSubAgentStore = defineStore('subAgent', {
|
||||
this.subAgents = [];
|
||||
return;
|
||||
}
|
||||
// 同步剔除不属于当前对话的残留记录:切对话/工作区后旧对话数据
|
||||
// 会先渲染一拍再被 fetch 覆盖,造成“任务被停了”的误解。
|
||||
// 属于当前对话的记录保留,避免布局「先收起再展开」闪烁。
|
||||
this.subAgents = this.subAgents.filter(
|
||||
(item) => !item.conversation_id || item.conversation_id === convId
|
||||
);
|
||||
let resp;
|
||||
if (conversationStore.multiAgentMode) {
|
||||
resp = await fetch(`/api/multiagent/active_sub_agents?conversation_id=${encodeURIComponent(convId)}`);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user