fix: refine host mode controls and kimi-k2.5 support

This commit is contained in:
JOJO 2026-01-28 11:19:50 +08:00
parent 8a7cc5d9c6
commit 60d27e9c1c
8 changed files with 79 additions and 32 deletions

View File

@ -40,6 +40,7 @@ TERMINAL_SANDBOX_ENV = {
if key.startswith(_env_prefix) if key.startswith(_env_prefix)
} }
TERMINAL_SANDBOX_REQUIRE = os.environ.get("TERMINAL_SANDBOX_REQUIRE", "0") not in {"0", "false", "False"} TERMINAL_SANDBOX_REQUIRE = os.environ.get("TERMINAL_SANDBOX_REQUIRE", "0") not in {"0", "false", "False"}
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")) 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")) MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8"))
@ -65,6 +66,7 @@ __all__ = [
"TERMINAL_SANDBOX_NAME_PREFIX", "TERMINAL_SANDBOX_NAME_PREFIX",
"TERMINAL_SANDBOX_ENV", "TERMINAL_SANDBOX_ENV",
"TERMINAL_SANDBOX_REQUIRE", "TERMINAL_SANDBOX_REQUIRE",
"LINUX_SAFETY",
"TOOLBOX_TERMINAL_IDLE_SECONDS", "TOOLBOX_TERMINAL_IDLE_SECONDS",
"MAX_ACTIVE_USER_CONTAINERS", "MAX_ACTIVE_USER_CONTAINERS",
] ]

View File

@ -210,7 +210,15 @@ class MainTerminal:
else: else:
self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace" self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
def _is_host_mode(self) -> bool:
"""判定当前是否运行在宿主机模式,用于豁免配额等限制。"""
if self.container_session and getattr(self.container_session, "mode", None) != "docker":
return True
return (TERMINAL_SANDBOX_MODE or "").lower() == "host"
def record_model_call(self, is_thinking: bool): def record_model_call(self, is_thinking: bool):
if self._is_host_mode():
return True, {}
tracker = getattr(self, "usage_tracker", None) tracker = getattr(self, "usage_tracker", None)
if not tracker: if not tracker:
return True, {} return True, {}
@ -224,6 +232,8 @@ class MainTerminal:
return True, {} return True, {}
def record_search_call(self): def record_search_call(self):
if self._is_host_mode():
return True, {}
tracker = getattr(self, "usage_tracker", None) tracker = getattr(self, "usage_tracker", None)
if not tracker: if not tracker:
return True, {} return True, {}

View File

@ -29,6 +29,7 @@ from config import (
TERMINAL_SANDBOX_NETWORK, TERMINAL_SANDBOX_NETWORK,
TERMINAL_SANDBOX_REQUIRE, TERMINAL_SANDBOX_REQUIRE,
LOGS_DIR, LOGS_DIR,
LINUX_SAFETY,
) )
from modules.container_monitor import collect_stats, inspect_state from modules.container_monitor import collect_stats, inspect_state
@ -110,7 +111,13 @@ class UserContainerManager:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Public API # Public API
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def ensure_container(self, username: str, workspace_path: str, container_key: Optional[str] = None) -> ContainerHandle: def ensure_container(
self,
username: str,
workspace_path: str,
container_key: Optional[str] = None,
preferred_mode: Optional[str] = None,
) -> ContainerHandle:
"""为指定“容器键”确保一个容器。 """为指定“容器键”确保一个容器。
- username业务用户名用于日志/权限 - username业务用户名用于日志/权限
@ -122,10 +129,21 @@ class UserContainerManager:
workspace = str(Path(workspace_path).expanduser().resolve()) workspace = str(Path(workspace_path).expanduser().resolve())
Path(workspace).mkdir(parents=True, exist_ok=True) Path(workspace).mkdir(parents=True, exist_ok=True)
mode = (preferred_mode or self.sandbox_mode or "host").lower()
# 安全模式:当 LINUX_SAFETY 开启且要求 host 时,强制回退 docker
if mode == "host" and LINUX_SAFETY:
mode = "docker"
with self._lock: with self._lock:
handle = self._containers.get(key) handle = self._containers.get(key)
if handle: if handle:
if handle.mode == "docker" and not self._is_container_running(handle): # 模式不同或 docker 挂掉时重建
if handle.mode != mode:
self._containers.pop(key, None)
if handle.mode == "docker":
self._kill_container(handle.container_name, handle.sandbox_bin)
handle = None
elif handle.mode == "docker" and not self._is_container_running(handle):
self._containers.pop(key, None) self._containers.pop(key, None)
self._kill_container(handle.container_name, handle.sandbox_bin) self._kill_container(handle.container_name, handle.sandbox_bin)
handle = None handle = None
@ -138,7 +156,7 @@ class UserContainerManager:
raise RuntimeError("资源繁忙:容器配额已用尽,请稍候再试。") raise RuntimeError("资源繁忙:容器配额已用尽,请稍候再试。")
# Important: create container using the cache key so each workspace gets its own container name. # Important: create container using the cache key so each workspace gets its own container name.
handle = self._create_handle(key, workspace) handle = self._create_handle(key, workspace, mode)
self._containers[key] = handle self._containers[key] = handle
return handle return handle
@ -262,8 +280,8 @@ class UserContainerManager:
existing = 1 if username in self._containers else 0 existing = 1 if username in self._containers else 0
return (len(self._containers) - existing) < self.max_containers return (len(self._containers) - existing) < self.max_containers
def _create_handle(self, username: str, workspace: str) -> ContainerHandle: def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle:
if self.sandbox_mode != "docker": if mode != "docker":
return self._host_handle(username, workspace) return self._host_handle(username, workspace)
docker_path = shutil.which(self.sandbox_bin or "docker") docker_path = shutil.which(self.sandbox_bin or "docker")

View File

@ -210,7 +210,7 @@ def admin_dashboard_snapshot_api():
if uname: if uname:
handles = state.container_manager.list_containers() handles = state.container_manager.list_containers()
if uname not in handles: if uname not in handles:
state.container_manager.ensure_container(uname, str(state.user_manager.ensure_user_workspace(uname).project_path)) state.container_manager.ensure_container(uname, str(state.user_manager.ensure_user_workspace(uname).project_path), preferred_mode="docker")
except Exception as ensure_exc: except Exception as ensure_exc:
logging.getLogger(__name__).warning("ensure_container for admin failed: %s", ensure_exc) logging.getLogger(__name__).warning("ensure_container for admin failed: %s", ensure_exc)

View File

@ -849,7 +849,7 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm
return None, None return None, None
record = get_current_user_record() record = get_current_user_record()
workspace = user_manager.ensure_user_workspace(username) workspace = user_manager.ensure_user_workspace(username)
container_handle = container_manager.ensure_container(username, str(workspace.project_path)) container_handle = container_manager.ensure_container(username, str(workspace.project_path), preferred_mode="docker")
usage_tracker = get_or_create_usage_tracker(username, workspace) usage_tracker = get_or_create_usage_tracker(username, workspace)
terminal = user_terminals.get(username) terminal = user_terminals.get(username)
if not terminal: if not terminal:

View File

@ -11,6 +11,7 @@ from config import (
DATA_DIR, DATA_DIR,
LOGS_DIR, LOGS_DIR,
UPLOAD_QUARANTINE_SUBDIR, UPLOAD_QUARANTINE_SUBDIR,
LINUX_SAFETY,
) )
from .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username from .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username
@ -36,6 +37,12 @@ def issue_csrf_token():
return response return response
@auth_bp.route('/api/host-mode-enabled', methods=['GET'])
def host_mode_enabled():
enabled = (TERMINAL_SANDBOX_MODE or "").lower() == "host" and not LINUX_SAFETY
return jsonify({"success": True, "enabled": enabled})
@auth_bp.route('/login', methods=['GET', 'POST']) @auth_bp.route('/login', methods=['GET', 'POST'])
def login(): def login():
if request.method == 'GET': if request.method == 'GET':
@ -92,7 +99,7 @@ def login():
session.permanent = True session.permanent = True
clear_failures("login", identifier=client_ip) clear_failures("login", identifier=client_ip)
try: try:
state.container_manager.ensure_container(record.username, str(workspace.project_path)) state.container_manager.ensure_container(record.username, str(workspace.project_path), preferred_mode="docker")
except RuntimeError as exc: except RuntimeError as exc:
session.clear() session.clear()
return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503 return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503
@ -136,7 +143,7 @@ def host_login():
# 预先创建宿主机模式的终端/容器句柄host 模式不会启动 Docker # 预先创建宿主机模式的终端/容器句柄host 模式不会启动 Docker
try: try:
state.container_manager.ensure_container("host", str(host_path), container_key="host") state.container_manager.ensure_container("host", str(host_path), container_key="host", preferred_mode="host")
except RuntimeError as exc: except RuntimeError as exc:
session.clear() session.clear()
return jsonify({"success": False, "error": str(exc)}), 503 return jsonify({"success": False, "error": str(exc)}), 503

View File

@ -83,8 +83,8 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
workspace.workspace_id = "host" workspace.workspace_id = "host"
term_key = "host" term_key = "host"
container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key) container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key, preferred_mode="host")
usage_tracker = get_or_create_usage_tracker("host", workspace) usage_tracker = None # 宿主机模式不计配额
terminal = state.user_terminals.get(term_key) terminal = state.user_terminals.get(term_key)
if not terminal: if not terminal:
run_mode = session.get('run_mode') if has_request_context() else None run_mode = session.get('run_mode') if has_request_context() else None
@ -138,7 +138,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
except Exception: except Exception:
pass pass
term_key = _make_terminal_key(username, getattr(workspace, "workspace_id", None) if is_api_user else None) term_key = _make_terminal_key(username, getattr(workspace, "workspace_id", None) if is_api_user else None)
container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key) container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key, preferred_mode="docker")
usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace) usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace)
terminal = state.user_terminals.get(term_key) terminal = state.user_terminals.get(term_key)
if not terminal: if not terminal:

View File

@ -115,7 +115,7 @@
<input type="password" id="password" autocomplete="current-password" /> <input type="password" id="password" autocomplete="current-password" />
</div> </div>
<button id="login-btn">登录</button> <button id="login-btn">登录</button>
<button id="host-btn" class="ghost-btn" title="仅当服务器配置为宿主机模式时可用">宿主机模式(免登录)</button> <button id="host-btn" class="ghost-btn" title="仅当服务器配置为宿主机模式且未启用安全保护时可用" style="display:none">宿主机模式(免登录)</button>
<div class="error" id="error"></div> <div class="error" id="error"></div>
<div class="link"> <div class="link">
还没有账号?<a href="/register">点击注册</a> 还没有账号?<a href="/register">点击注册</a>
@ -171,26 +171,36 @@
}); });
const hostBtn = document.getElementById('host-btn'); const hostBtn = document.getElementById('host-btn');
hostBtn.addEventListener('click', async () => { // 按钮可见性由后端配置决定
hostBtn.disabled = true; fetch('/api/host-mode-enabled')
errorEl.textContent = ''; .then(resp => resp.json())
try { .then(data => {
const resp = await fetch('/host-login', { method: 'POST' }); if (data && data.success && data.enabled) {
if (resp.status === 503) { hostBtn.style.display = 'block';
window.location.href = '/resource_busy';
return;
}
const data = await resp.json();
if (data.success) {
window.location.href = '/';
} else {
errorEl.textContent = data.error || '宿主机模式不可用';
}
} catch (err) {
errorEl.textContent = '网络错误,请重试';
} finally {
hostBtn.disabled = false;
} }
})
.catch(() => {});
hostBtn.addEventListener('click', async () => {
hostBtn.disabled = true;
errorEl.textContent = '';
try {
const resp = await fetch('/host-login', { method: 'POST' });
if (resp.status === 503) {
window.location.href = '/resource_busy';
return;
}
const data = await resp.json();
if (data.success) {
window.location.href = '/';
} else {
errorEl.textContent = data.error || '宿主机模式不可用';
}
} catch (err) {
errorEl.textContent = '网络错误,请重试';
} finally {
hostBtn.disabled = false;
}
}); });
</script> </script>
</body> </body>