## 模型逻辑清理 早期把模型 API 端点/密钥/模型 ID 硬编码的残留(AGENT_API_* / THINKING_* / TITLE_* 三件套)已彻底移除。模型配置统一由 config/custom_models.json (经 model_profiles 解析为档案)描述,运行时通过 apply_profile 注入; 没有任何可用模型时按既定行为报错。 - config/api.py: 删 9 个三件套符号,仅保留 DEFAULT_RESPONSE_MAX_TOKENS - utils/api_client.py: client 改为空配置起步,移除三件套 import - server/chat_flow_helpers.py: 删死 import(TITLE_* 符号) ## 部署级配置外置(按"决策权归谁"分类) config/*.json 不再一概锚定源码树: - 程序能力(docker_risk_markers / skill_hints)留源码树,随版本演进 - 部署者自定义(custom_models / host_workspaces / auto_approval / goal_review / forbidden_commands / host_sandbox_policy)外置到 ~/.agents/<mode>/config/ 新增统一解析机制(config/paths.py): - DEPLOY_CONFIG_DIR(默认 ~/.agents/<mode>/config,可用环境变量覆盖) - resolve_deploy_config():只读,回退链 部署目录→源码树.json→源码树.example (开发环境不必先跑 setup 也能用源码树种子) - deploy_config_path():写回用,稳定指向部署目录 6 个加载点改用上述解析;顺带修复 approval_agent / goal_review_agent 的 DEBUG_TRANSCRIPT_DIR 仍指旧源码树 logs/ 的漏迁问题。 ## 安全与运维 - 含密钥/机器特定的 5 个文件停止 git 追踪(git rm --cached,本地保留) 并加入 .gitignore,仓库仅留 .example 种子;forbidden_commands 保留 追踪作默认黑名单 - scripts/setup.py: 模型配置写到部署目录(用与 paths 一致的独立推导, 不 import config 以免过早锁定路径) - scripts/migrate_runtime_data.py: 新增 config/*.json → 部署目录迁移 (备份 + 不覆盖已存在) ## 关联:P1 配置收敛 + P2 首启向导 - config/server.py: Web 端口/监听地址/debug/NODE_BIN/PYTHON_BIN 单一事实源 - 消灭 8091 多处重复定义(state.py 死代码、app_legacy、main.py 各读各的) - 修复 sub_agent_manager 命令数组硬编码 "node" → NODE_BIN(便携包内置 Node 的前提) - scripts/setup.py: 终端首启向导(模式/端口/管理员/密钥/模型) ## 测试 test_config_paths_resolution 更新以反映新行为(host_workspaces 锚定部署 目录、新增 DEPLOY_CONFIG_DIR 用例);全部离线用例通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
201 lines
6.9 KiB
Python
201 lines
6.9 KiB
Python
"""共享状态与常量,供各子模块使用。"""
|
|
from __future__ import annotations
|
|
import os
|
|
import threading
|
|
from collections import defaultdict, deque
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
|
|
from config import LOGS_DIR, PROJECT_MAX_STORAGE_BYTES, PROJECT_MAX_STORAGE_MB, WEB_SERVER_PORT
|
|
from core.web_terminal import WebTerminal
|
|
from modules.custom_tool_registry import CustomToolRegistry
|
|
from modules.mcp_server_registry import MCPServerRegistry
|
|
from modules.mcp_client_manager import MCPClientManager
|
|
from modules.usage_tracker import UsageTracker
|
|
from modules.user_container_manager import UserContainerManager
|
|
from modules.user_manager import UserManager
|
|
from modules.api_user_manager import ApiUserManager
|
|
from modules.tool_approval_manager import ToolApprovalManager
|
|
from modules.user_question_manager import UserQuestionManager
|
|
|
|
# 全局实例
|
|
user_manager = UserManager()
|
|
api_user_manager = ApiUserManager()
|
|
custom_tool_registry = CustomToolRegistry()
|
|
mcp_server_registry = MCPServerRegistry()
|
|
mcp_client_manager = MCPClientManager(mcp_server_registry)
|
|
container_manager = UserContainerManager()
|
|
user_terminals: Dict[str, WebTerminal] = {}
|
|
terminal_rooms: Dict[str, set] = {}
|
|
connection_users: Dict[str, str] = {}
|
|
HOST_ACTIVE_WORKSPACE_LOCK = threading.RLock()
|
|
HOST_ACTIVE_WORKSPACE_ID: Optional[str] = None
|
|
HOST_ACTIVE_WORKSPACE_PATH: Optional[str] = None
|
|
HOST_ACTIVE_WORKSPACE_VERSION: int = 0
|
|
RECENT_UPLOAD_EVENT_LIMIT = 150
|
|
RECENT_UPLOAD_FEED_LIMIT = 60
|
|
stop_flags: Dict[str, Dict[str, Any]] = {}
|
|
active_polling_tasks: Dict[str, bool] = {} # conversation_id -> is_polling
|
|
tool_approval_manager = ToolApprovalManager()
|
|
user_question_manager = UserQuestionManager()
|
|
|
|
# 监控/限流/用量
|
|
MONITOR_FILE_TOOLS = {'write_file', 'edit_file'}
|
|
MONITOR_MEMORY_TOOLS = {'update_memory'}
|
|
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
|
|
MONITOR_MEMORY_ENTRY_LIMIT = 256
|
|
RATE_LIMIT_BUCKETS: Dict[str, deque] = defaultdict(deque)
|
|
FAILURE_TRACKERS: Dict[str, Dict[str, float]] = {}
|
|
pending_socket_tokens: Dict[str, Dict[str, Any]] = {}
|
|
usage_trackers: Dict[str, UsageTracker] = {}
|
|
active_login_nonces: Dict[str, set] = defaultdict(set)
|
|
|
|
MONITOR_SNAPSHOT_CACHE: Dict[str, Dict[str, Any]] = {}
|
|
MONITOR_SNAPSHOT_CACHE_LIMIT = 120
|
|
RECENT_UPLOAD_EVENT_LIMIT = 150
|
|
RECENT_UPLOAD_FEED_LIMIT = 60
|
|
|
|
# 路径与缓存设置(依赖项目配置)
|
|
PROJECT_STORAGE_CACHE: Dict[str, Dict[str, Any]] = {}
|
|
PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_TTL", "30"))
|
|
|
|
# 其他配置
|
|
DEFAULT_PORT = WEB_SERVER_PORT
|
|
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
|
CSRF_HEADER_NAME = "X-CSRF-Token"
|
|
CSRF_SESSION_KEY = "_csrf_token"
|
|
CSRF_SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
|
|
CSRF_PROTECTED_PATHS = {"/login", "/register", "/logout", "/host-login"}
|
|
CSRF_PROTECTED_PREFIXES = ("/api/",)
|
|
CSRF_EXEMPT_PATHS = {"/api/csrf-token"}
|
|
FAILED_LOGIN_LIMIT = 5
|
|
FAILED_LOGIN_LOCK_SECONDS = 300
|
|
SOCKET_TOKEN_TTL_SECONDS = 45
|
|
USER_IDLE_TIMEOUT_SECONDS = int(os.environ.get("USER_IDLE_TIMEOUT_SECONDS", "900"))
|
|
LAST_ACTIVE_FILE = Path(LOGS_DIR).expanduser().resolve() / "last_active.json"
|
|
_last_active_lock = threading.Lock()
|
|
_last_active_cache: Dict[str, float] = {}
|
|
_idle_reaper_started = False
|
|
TITLE_PROMPT_PATH = Path(__file__).resolve().parent.parent / "prompts" / "title_generation_prompt.txt"
|
|
|
|
# 项目存储限制常量也会被使用
|
|
PROJECT_MAX_STORAGE_BYTES = PROJECT_MAX_STORAGE_BYTES
|
|
PROJECT_MAX_STORAGE_MB = PROJECT_MAX_STORAGE_MB
|
|
|
|
__all__ = [
|
|
"user_manager",
|
|
"api_user_manager",
|
|
"custom_tool_registry",
|
|
"mcp_server_registry",
|
|
"mcp_client_manager",
|
|
"container_manager",
|
|
"user_terminals",
|
|
"terminal_rooms",
|
|
"connection_users",
|
|
"HOST_ACTIVE_WORKSPACE_LOCK",
|
|
"HOST_ACTIVE_WORKSPACE_ID",
|
|
"HOST_ACTIVE_WORKSPACE_PATH",
|
|
"HOST_ACTIVE_WORKSPACE_VERSION",
|
|
"stop_flags",
|
|
"MONITOR_FILE_TOOLS",
|
|
"MONITOR_MEMORY_TOOLS",
|
|
"MONITOR_SNAPSHOT_CHAR_LIMIT",
|
|
"MONITOR_MEMORY_ENTRY_LIMIT",
|
|
"RATE_LIMIT_BUCKETS",
|
|
"FAILURE_TRACKERS",
|
|
"pending_socket_tokens",
|
|
"usage_trackers",
|
|
"active_login_nonces",
|
|
"tool_approval_manager",
|
|
"user_question_manager",
|
|
"MONITOR_SNAPSHOT_CACHE",
|
|
"MONITOR_SNAPSHOT_CACHE_LIMIT",
|
|
"PROJECT_STORAGE_CACHE",
|
|
"PROJECT_STORAGE_CACHE_TTL_SECONDS",
|
|
"DEFAULT_PORT",
|
|
"THINKING_FAILURE_KEYWORDS",
|
|
"CSRF_HEADER_NAME",
|
|
"CSRF_SESSION_KEY",
|
|
"CSRF_SAFE_METHODS",
|
|
"CSRF_PROTECTED_PATHS",
|
|
"CSRF_PROTECTED_PREFIXES",
|
|
"CSRF_EXEMPT_PATHS",
|
|
"FAILED_LOGIN_LIMIT",
|
|
"FAILED_LOGIN_LOCK_SECONDS",
|
|
"SOCKET_TOKEN_TTL_SECONDS",
|
|
"USER_IDLE_TIMEOUT_SECONDS",
|
|
"LAST_ACTIVE_FILE",
|
|
"_last_active_lock",
|
|
"_last_active_cache",
|
|
"_idle_reaper_started",
|
|
"TITLE_PROMPT_PATH",
|
|
"PROJECT_MAX_STORAGE_BYTES",
|
|
"PROJECT_MAX_STORAGE_MB",
|
|
"RECENT_UPLOAD_EVENT_LIMIT",
|
|
"RECENT_UPLOAD_FEED_LIMIT",
|
|
"make_stop_keys",
|
|
"get_stop_flag",
|
|
"set_stop_flag",
|
|
"clear_stop_flag",
|
|
"get_last_active_ts",
|
|
]
|
|
|
|
|
|
def get_last_active_ts(username: str, fallback: Optional[float] = None) -> Optional[float]:
|
|
"""
|
|
返回最近活跃时间,优先使用缓存;当容器句柄中的时间更新、更晚时,自动刷新缓存。
|
|
这样避免“缓存过旧导致刚触碰的容器被立即回收”的问题。
|
|
"""
|
|
fallback_val: Optional[float]
|
|
try:
|
|
fallback_val = float(fallback) if fallback is not None else None
|
|
except (TypeError, ValueError):
|
|
fallback_val = None
|
|
|
|
with _last_active_lock:
|
|
cached = _last_active_cache.get(username)
|
|
try:
|
|
cached_val = float(cached) if cached is not None else None
|
|
except (TypeError, ValueError):
|
|
cached_val = None
|
|
|
|
# 若没有缓存,或句柄时间更新、更晚,则刷新缓存
|
|
if cached_val is None:
|
|
if fallback_val is not None:
|
|
_last_active_cache[username] = fallback_val
|
|
return fallback_val
|
|
|
|
if fallback_val is not None and fallback_val > cached_val:
|
|
_last_active_cache[username] = fallback_val
|
|
return fallback_val
|
|
|
|
return cached_val
|
|
|
|
|
|
# ====== 停止标志辅助 ======
|
|
def make_stop_keys(client_sid: Optional[str] = None, username: Optional[str] = None):
|
|
keys = []
|
|
if client_sid:
|
|
keys.append(client_sid)
|
|
if username:
|
|
keys.append(f"user:{username}")
|
|
return keys
|
|
|
|
|
|
def set_stop_flag(client_sid: Optional[str], username: Optional[str], entry: Dict[str, Any]):
|
|
for k in make_stop_keys(client_sid, username):
|
|
stop_flags[k] = entry
|
|
|
|
|
|
def get_stop_flag(client_sid: Optional[str], username: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
for k in make_stop_keys(client_sid, username):
|
|
val = stop_flags.get(k)
|
|
if val:
|
|
return val
|
|
return None
|
|
|
|
|
|
def clear_stop_flag(client_sid: Optional[str], username: Optional[str]):
|
|
for k in make_stop_keys(client_sid, username):
|
|
stop_flags.pop(k, None)
|