From 08f7adb0fd0bc25e2529ba0e974fc48c2177b01f Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 2 Sep 2026 23:39:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(containers):=20=E9=85=8D=E9=A2=9D=E5=8F=AA?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=20docker=20=E5=8F=A5=E6=9F=84=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20host=20=E6=A8=A1=E5=BC=8F=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E5=88=97=E8=A1=A8=20503?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ensure_container 的全局/每用户配额闸门仅在创建 docker 句柄时执行, host 句柄(会话标识,不占容器池)不再计入任何配额,修复 host 模式 下第 4 个工作区起 GET /api/conversations 返回 503 的问题 - _has_capacity / has_capacity 全局上限计数同步只统计 docker 句柄 - MAX_ACTIVE_CONTAINERS_PER_USER 默认值 3 -> 8 - admin 面板 available_slots 展示对齐为 docker 句柄计数 --- config/terminal.py | 3 ++- modules/user_container_manager.py | 36 +++++++++++++++++++------------ server/admin.py | 4 +++- server/conversation_stats.py | 3 ++- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/config/terminal.py b/config/terminal.py index 47337ce1..a29a506c 100644 --- a/config/terminal.py +++ b/config/terminal.py @@ -57,7 +57,8 @@ LINUX_SAFETY = os.environ.get("LINUX_SAFETY", "0") not in {"0", "false", "False" TOOLBOX_TERMINAL_IDLE_SECONDS = int(os.environ.get("TOOLBOX_TERMINAL_IDLE_SECONDS", "900")) MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8")) # 每用户同时活跃的容器上限(防单用户多工作区占满全局容器池,2026-09-02 审计新增) -MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "3")) +# 仅统计 docker 句柄(host 句柄不计入);默认值 8 与全局上限对齐(2026-09-02 调整) +MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "8")) HOST_EXECUTION_MODE_DEFAULT = os.environ.get("HOST_EXECUTION_MODE_DEFAULT", "sandbox").strip().lower() # 沙箱可写路径的「部署通道」(逗号分隔)。路径授权只有两个来源: # config/host_sandbox_policy.json(前端「路径授权」UI)+ 本变量(真·环境变量), diff --git a/modules/user_container_manager.py b/modules/user_container_manager.py index 44b6097a..7603432c 100644 --- a/modules/user_container_manager.py +++ b/modules/user_container_manager.py @@ -158,18 +158,22 @@ class UserContainerManager: handle.touch() return handle - if not self._has_capacity(key): - raise RuntimeError(tr("container_mgr.quota_exhausted")) + # 配额检查仅针对 docker 句柄:host 句柄只是会话标识,不占用容器池资源, + # 创建 host 句柄时直接跳过全局/每用户两道配额闸门 + if mode == "docker": + if not self._has_capacity(key): + raise RuntimeError(tr("container_mgr.quota_exhausted")) - # 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户 - per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER - if per_user_limit > 0: - owned = sum( - 1 for k in self._containers - if k == username_norm or k.startswith(f"{username_norm}::") - ) - if owned >= per_user_limit: - raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit)) + # 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户 + per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER + if per_user_limit > 0: + owned = sum( + 1 for k, h in self._containers.items() + if (k == username_norm or k.startswith(f"{username_norm}::")) + and h.mode == "docker" + ) + if owned >= per_user_limit: + raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit)) # Important: create container using the cache key so each workspace gets its own container name. handle = self._create_handle(key, workspace, mode) @@ -193,7 +197,8 @@ class UserContainerManager: return True if self.max_containers <= 0: return True - return len(self._containers) < self.max_containers + docker_total = sum(1 for h in self._containers.values() if h.mode == "docker") + return docker_total < self.max_containers def get_handle(self, container_key: str) -> Optional[ContainerHandle]: key = self._normalize_username(container_key) @@ -293,8 +298,11 @@ class UserContainerManager: def _has_capacity(self, username: str) -> bool: if self.max_containers <= 0: return True - existing = 1 if username in self._containers else 0 - return (len(self._containers) - existing) < self.max_containers + # 全局上限同样只统计 docker 句柄(host 句柄不占用容器池) + docker_total = sum(1 for h in self._containers.values() if h.mode == "docker") + existing_handle = self._containers.get(username) + existing = 1 if (existing_handle and existing_handle.mode == "docker") else 0 + return (docker_total - existing) < self.max_containers def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle: if mode != "docker": diff --git a/server/admin.py b/server/admin.py index 4207ef14..48a4d982 100644 --- a/server/admin.py +++ b/server/admin.py @@ -419,7 +419,9 @@ def build_api_admin_dashboard_snapshot(): max_containers = getattr(container_manager, "max_containers", None) or len(handle_map) available_slots = None if max_containers: - available_slots = max(0, max_containers - len(handle_map)) + # 可用余量只对 docker 句柄计数(host 句柄不占用容器池配额) + docker_total = sum(1 for val in handle_map.values() if val.get("mode") == "docker") + available_slots = max(0, max_containers - docker_total) upload_events = collect_upload_events() uploads_summary = summarize_upload_events(upload_events, quarantine_total_bytes) diff --git a/server/conversation_stats.py b/server/conversation_stats.py index 8f49d7c4..4b599bdc 100644 --- a/server/conversation_stats.py +++ b/server/conversation_stats.py @@ -312,7 +312,8 @@ def collect_container_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[s "host": active_total - docker_count, "issues": failure_count, "max_containers": container_manager.max_containers, - "available_slots": max(0, container_manager.max_containers - active_total) if container_manager.max_containers > 0 else None, + # 可用余量只对 docker 句柄计数(host 句柄不占用容器池配额) + "available_slots": max(0, container_manager.max_containers - docker_count) if container_manager.max_containers > 0 else None, "avg_cpu_percent": round(sum(cpu_values) / len(cpu_values), 2) if cpu_values else None, "avg_mem_percent": round(sum(mem_percent_values) / len(mem_percent_values), 2) if mem_percent_values else None, "total_mem_used_bytes": total_mem_used,