refactor(config): 移除硬编码模型残留,部署级配置外置到 ~/.agents
## 模型逻辑清理 早期把模型 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>
This commit is contained in:
parent
eaa5e3bee9
commit
b8a51c1c63
14
.env.example
14
.env.example
@ -31,6 +31,20 @@ AGENT_ADMIN_PASSWORD_HASH=
|
|||||||
# === Web Session =============================================================
|
# === Web Session =============================================================
|
||||||
WEB_SECRET_KEY=replace-with-random-hex
|
WEB_SECRET_KEY=replace-with-random-hex
|
||||||
|
|
||||||
|
# === Web 服务 ================================================================
|
||||||
|
# 监听端口(统一来源,main.py / server.app / state 均读取此项)
|
||||||
|
WEB_SERVER_PORT=8091
|
||||||
|
# 监听地址:多用户/服务器用 0.0.0.0;单机便携包建议 127.0.0.1 只听本机
|
||||||
|
WEB_SERVER_HOST=0.0.0.0
|
||||||
|
# 调试模式(同时控制 Flask reloader),生产保持 0
|
||||||
|
WEB_SERVER_DEBUG=0
|
||||||
|
|
||||||
|
# === 运行环境二进制(便携 release 包用,开发环境留默认即可)==================
|
||||||
|
# 子智能体使用的 node 可执行文件;留 "node" 表示走系统 PATH
|
||||||
|
AGENT_NODE_BIN=node
|
||||||
|
# Python 解释器路径;留空表示使用当前进程的 sys.executable
|
||||||
|
AGENT_PYTHON_BIN=
|
||||||
|
|
||||||
# === 终端容器沙箱 =============================================================
|
# === 终端容器沙箱 =============================================================
|
||||||
# 模式:docker / host
|
# 模式:docker / host
|
||||||
TERMINAL_SANDBOX_MODE=docker
|
TERMINAL_SANDBOX_MODE=docker
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -40,9 +40,12 @@ _experiments/
|
|||||||
# Ignore docs
|
# Ignore docs
|
||||||
doc/
|
doc/
|
||||||
|
|
||||||
# Host workspace config (local, use .example)
|
# 部署级配置(含密钥引用 / 机器特定,本地保留,仓库仅留 .example 种子)
|
||||||
config/host_workspaces.json
|
config/host_workspaces.json
|
||||||
config/custom_models.json
|
config/custom_models.json
|
||||||
|
config/auto_approval.json
|
||||||
|
config/goal_review.json
|
||||||
|
config/host_sandbox_policy.json
|
||||||
|
|
||||||
# Claude Code local artifacts (settings, worktrees)
|
# Claude Code local artifacts (settings, worktrees)
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
@ -55,7 +55,9 @@
|
|||||||
|
|
||||||
具体目录变量支持相对路径(相对仓库根目录展开)、绝对路径与 `~`。
|
具体目录变量支持相对路径(相对仓库根目录展开)、绝对路径与 `~`。
|
||||||
|
|
||||||
> 注意:`config/*.json`(`custom_models.json`、`host_workspaces.json`、`auto_approval.json` 等)属于**配置而非运行态数据**,仍锚定源码树,不随运行态根目录迁移。`prompts/`、`agentskills/` 同理。
|
> 注意:`config/*.json` 分两类:
|
||||||
|
> - **程序能力**(`docker_risk_markers.json`、`skill_hints.json`):是程序行为的一部分,随版本演进,仍锚定源码树。`prompts/`、`agentskills/` 同理。
|
||||||
|
> - **部署级配置**(`custom_models`、`host_workspaces`、`auto_approval`、`goal_review`、`forbidden_commands`、`host_sandbox_policy`):因部署/机器而异或含密钥,外置到 `~/.agents/<mode>/config/`(即 `DEPLOY_CONFIG_DIR`,可单独用该环境变量覆盖)。读取走 `config.resolve_deploy_config(name)`,回退链:部署目录 → 源码树 `.json` → 源码树 `.json.example`,因此开发环境不必先跑 setup 也能用源码树种子。含密钥/机器特定的 5 个(除 `forbidden_commands`)不纳入 git,仓库仅留 `.example`。
|
||||||
|
|
||||||
### 1.5.3 日志策略(2026-06)
|
### 1.5.3 日志策略(2026-06)
|
||||||
|
|
||||||
|
|||||||
@ -159,7 +159,7 @@ tail -f ~/.agents/web/logs/container_stats.log
|
|||||||
2. 模式根变量 `AGENTS_HOST_HOME` / `AGENTS_WEB_HOME`(整体搬迁该模式数据)
|
2. 模式根变量 `AGENTS_HOST_HOME` / `AGENTS_WEB_HOME`(整体搬迁该模式数据)
|
||||||
3. 兜底 `~/.agents/<mode>`
|
3. 兜底 `~/.agents/<mode>`
|
||||||
|
|
||||||
> `config/*.json`、`prompts/`、`agentskills/` 属于配置/资源,仍锚定源码树,不随运行态迁移。
|
> `config/*.json` 分两类:**程序能力**(`docker_risk_markers.json`、`skill_hints.json`)与 `prompts/`、`agentskills/` 一样锚定源码树;**部署级配置**(`custom_models`、`host_workspaces`、`auto_approval`、`goal_review`、`forbidden_commands`、`host_sandbox_policy`)外置到 `~/.agents/<mode>/config/`(`DEPLOY_CONFIG_DIR`),读取走 `config.resolve_deploy_config(name)`,回退链:部署目录 → 源码树 `.json` → 源码树 `.json.example`。含密钥/机器特定的 5 个不进 git,仓库仅留 `.example`。
|
||||||
|
|
||||||
**日志策略**:
|
**日志策略**:
|
||||||
- API 请求体 dump 默认**关闭**,`AGENT_API_DUMP_ENABLED=1` 开启。
|
- API 请求体 dump 默认**关闭**,`AGENT_API_DUMP_ENABLED=1` 开启。
|
||||||
|
|||||||
@ -50,6 +50,7 @@ from . import uploads as _uploads
|
|||||||
from . import sub_agent as _sub_agent
|
from . import sub_agent as _sub_agent
|
||||||
from . import custom_tools as _custom_tools
|
from . import custom_tools as _custom_tools
|
||||||
from . import mcp as _mcp
|
from . import mcp as _mcp
|
||||||
|
from . import server as _server
|
||||||
|
|
||||||
from .api import *
|
from .api import *
|
||||||
from .search import *
|
from .search import *
|
||||||
@ -67,9 +68,10 @@ from .uploads import *
|
|||||||
from .sub_agent import *
|
from .sub_agent import *
|
||||||
from .custom_tools import *
|
from .custom_tools import *
|
||||||
from .mcp import *
|
from .mcp import *
|
||||||
|
from .server import *
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
for module in (_api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp):
|
for module in (_api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp, _server):
|
||||||
__all__ += getattr(module, "__all__", [])
|
__all__ += getattr(module, "__all__", [])
|
||||||
|
|
||||||
del _api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp
|
del _api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp, _server
|
||||||
|
|||||||
@ -1,43 +1,19 @@
|
|||||||
"""API 和外部服务配置。"""
|
"""API 和外部服务配置。
|
||||||
|
|
||||||
|
模型的 API 端点 / 密钥 / 模型 ID 现已统一由 ``config/custom_models.json``
|
||||||
|
(经 ``config/model_profiles.py`` 解析为模型档案)描述,并在运行时通过
|
||||||
|
``DeepSeekClient.apply_profile`` 注入。旧的 ``AGENT_API_BASE_URL`` /
|
||||||
|
``AGENT_API_KEY`` / ``AGENT_MODEL_ID``(以及 THINKING / TITLE 三件套)属于早期
|
||||||
|
把模型逻辑硬编码在程序里的残留,已移除。没有任何可用模型时,
|
||||||
|
``model_profiles.get_default_model_key`` 会直接报错提示去配置模型。
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def _env(name: str, default: str = "", required: bool = False) -> str:
|
# 默认响应 token 限制(与具体模型无关,保留为全局上限)
|
||||||
value = os.environ.get(name, default)
|
|
||||||
if required and not value:
|
|
||||||
raise RuntimeError(f"缺少必要环境变量: {name}")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
API_BASE_URL = _env("AGENT_API_BASE_URL", "https://api.example.com")
|
|
||||||
API_KEY = _env("AGENT_API_KEY", "")
|
|
||||||
MODEL_ID = _env("AGENT_MODEL_ID", "")
|
|
||||||
|
|
||||||
# 推理模型配置(智能思考模式使用)
|
|
||||||
THINKING_API_BASE_URL = _env("AGENT_THINKING_API_BASE_URL", API_BASE_URL)
|
|
||||||
THINKING_API_KEY = _env("AGENT_THINKING_API_KEY", API_KEY)
|
|
||||||
THINKING_MODEL_ID = _env("AGENT_THINKING_MODEL_ID", MODEL_ID)
|
|
||||||
|
|
||||||
from .search import TAVILY_API_KEY
|
|
||||||
|
|
||||||
# 对话标题生成模型(可选,留空则回退到基础快速模型)
|
|
||||||
TITLE_API_BASE_URL = _env("AGENT_TITLE_API_BASE_URL", API_BASE_URL)
|
|
||||||
TITLE_API_KEY = _env("AGENT_TITLE_API_KEY", API_KEY)
|
|
||||||
TITLE_MODEL_ID = _env("AGENT_TITLE_MODEL_ID", MODEL_ID)
|
|
||||||
|
|
||||||
# 默认响应 token 限制
|
|
||||||
DEFAULT_RESPONSE_MAX_TOKENS = int(os.environ.get("AGENT_DEFAULT_RESPONSE_MAX_TOKENS", "32768"))
|
DEFAULT_RESPONSE_MAX_TOKENS = int(os.environ.get("AGENT_DEFAULT_RESPONSE_MAX_TOKENS", "32768"))
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"API_BASE_URL",
|
|
||||||
"API_KEY",
|
|
||||||
"MODEL_ID",
|
|
||||||
"DEFAULT_RESPONSE_MAX_TOKENS",
|
"DEFAULT_RESPONSE_MAX_TOKENS",
|
||||||
"THINKING_API_BASE_URL",
|
|
||||||
"THINKING_API_KEY",
|
|
||||||
"THINKING_MODEL_ID",
|
|
||||||
"TITLE_API_BASE_URL",
|
|
||||||
"TITLE_API_KEY",
|
|
||||||
"TITLE_MODEL_ID",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "auto-approval-agent",
|
"name": "auto-approval-agent",
|
||||||
"url": "https://opencode.ai/zen/go/v1",
|
"url": "https://api.example.com/v1",
|
||||||
"key": "${API_KEY_DEEPSEEK}",
|
"key": "${API_KEY_DEEPSEEK}",
|
||||||
"model": "deepseek-v4-flash",
|
"model": "your-model-id",
|
||||||
"extra_params": {
|
"extra_params": {
|
||||||
"thinking": {
|
"thinking": {
|
||||||
"type": "enabled"
|
"type": "enabled"
|
||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "goal-review-agent",
|
"name": "goal-review-agent",
|
||||||
"url": "https://opencode.ai/zen/go/v1",
|
"url": "https://api.example.com/v1",
|
||||||
"key": "${API_KEY_DEEPSEEK}",
|
"key": "${API_KEY_DEEPSEEK}",
|
||||||
"model": "deepseek-v4-flash",
|
"model": "your-model-id",
|
||||||
"extra_params": {
|
"extra_params": {
|
||||||
"thinking": {
|
"thinking": {
|
||||||
"type": "enabled"
|
"type": "enabled"
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"macos_writable_paths": [
|
|
||||||
"/Users/jojo/Desktop"
|
|
||||||
],
|
|
||||||
"macos_readable_extra_paths": [
|
|
||||||
"/Users/jojo/Desktop"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
config/host_sandbox_policy.json.example
Normal file
4
config/host_sandbox_policy.json.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"macos_writable_paths": [],
|
||||||
|
"macos_readable_extra_paths": []
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from .paths import resolve_deploy_config
|
||||||
|
|
||||||
|
|
||||||
def _env_optional(name: str) -> Optional[str]:
|
def _env_optional(name: str) -> Optional[str]:
|
||||||
value = os.environ.get(name)
|
value = os.environ.get(name)
|
||||||
@ -160,7 +162,7 @@ def _build_custom_profile(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def _load_custom_models() -> Dict[str, Dict[str, Any]]:
|
def _load_custom_models() -> Dict[str, Dict[str, Any]]:
|
||||||
custom_path = Path(__file__).resolve().parent / "custom_models.json"
|
custom_path = Path(resolve_deploy_config("custom_models.json"))
|
||||||
if not custom_path.exists():
|
if not custom_path.exists():
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -16,8 +16,16 @@
|
|||||||
整体搬迁该模式下的全部运行态数据;
|
整体搬迁该模式下的全部运行态数据;
|
||||||
3. 兜底默认值 ``~/.agents/<mode>``。
|
3. 兜底默认值 ``~/.agents/<mode>``。
|
||||||
|
|
||||||
注意:``config/*.json``(custom_models、host_workspaces、auto_approval 等)属于
|
注意:``config/*.json`` 分两类,不再一概锚定源码树:
|
||||||
配置而非运行态数据,仍锚定在源码树内,不随运行态根目录迁移。
|
|
||||||
|
- **开发者配置 / 程序能力**(``docker_risk_markers.json`` / ``skill_hints.json``):
|
||||||
|
是程序行为的一部分,随版本演进,仍锚定源码树。
|
||||||
|
- **部署者自定义配置**(``custom_models`` / ``host_workspaces`` /
|
||||||
|
``auto_approval`` / ``goal_review`` / ``forbidden_commands`` /
|
||||||
|
``host_sandbox_policy``):因部署/机器而异、或含密钥,外置到
|
||||||
|
``~/.agents/<mode>/config/``(即 ``DEPLOY_CONFIG_DIR``)。读取走
|
||||||
|
``resolve_deploy_config``,回退链为:部署目录 -> 源码树 ``.json`` ->
|
||||||
|
源码树 ``.json.example``。这样开发环境不必先跑 setup 也能用源码树种子。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -85,6 +93,44 @@ USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
|
|||||||
# API 专用用户与工作区(与网页用户隔离)
|
# API 专用用户与工作区(与网页用户隔离)
|
||||||
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
|
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
|
||||||
|
|
||||||
|
# ── 部署级配置目录(部署者自定义的 config/*.json 外置到这里)──
|
||||||
|
# 默认 ~/.agents/<mode>/config,可用 DEPLOY_CONFIG_DIR 覆盖。
|
||||||
|
DEPLOY_CONFIG_DIR = _runtime_dir("DEPLOY_CONFIG_DIR", "config")
|
||||||
|
# 源码树内的配置种子目录(首次运行/开发环境的默认内容来源)。
|
||||||
|
_SEED_CONFIG_DIR = _REPO_ROOT / "config"
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_config_path(filename: str) -> str:
|
||||||
|
"""部署级配置文件应写入/读取的位置(``~/.agents/<mode>/config`` 下)。
|
||||||
|
|
||||||
|
用于会被运行时写回的文件(如 host_workspaces / host_sandbox_policy),
|
||||||
|
始终指向部署目录,不回退源码树。
|
||||||
|
"""
|
||||||
|
return str(Path(DEPLOY_CONFIG_DIR) / filename)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_deploy_config(filename: str) -> str:
|
||||||
|
"""解析部署级配置的只读路径,回退链(高→低):
|
||||||
|
|
||||||
|
1. ``~/.agents/<mode>/config/<filename>``(部署者真实配置)
|
||||||
|
2. 源码树 ``config/<filename>``(仓库内种子)
|
||||||
|
3. 源码树 ``config/<filename>.example``(示例兜底)
|
||||||
|
|
||||||
|
都不存在时返回部署目录下的目标路径,由调用方处理「文件不存在」。
|
||||||
|
仅用于定位,不在此做拷贝(拷贝交给 setup / 迁移脚本)。
|
||||||
|
"""
|
||||||
|
deployed = Path(DEPLOY_CONFIG_DIR) / filename
|
||||||
|
if deployed.exists():
|
||||||
|
return str(deployed)
|
||||||
|
seed = _SEED_CONFIG_DIR / filename
|
||||||
|
if seed.exists():
|
||||||
|
return str(seed)
|
||||||
|
example = _SEED_CONFIG_DIR / f"{filename}.example"
|
||||||
|
if example.exists():
|
||||||
|
return str(example)
|
||||||
|
return str(deployed)
|
||||||
|
|
||||||
|
|
||||||
# ── 启动工作区 ──
|
# ── 启动工作区 ──
|
||||||
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录。
|
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录。
|
||||||
DEFAULT_PROJECT_PATH = _resolve_repo_path(os.environ.get("DEFAULT_PROJECT_PATH", ""), "./project")
|
DEFAULT_PROJECT_PATH = _resolve_repo_path(os.environ.get("DEFAULT_PROJECT_PATH", ""), "./project")
|
||||||
@ -95,8 +141,12 @@ HOST_PROJECT_PATH = _resolve_repo_path(os.environ.get("HOST_PROJECT_PATH", ""),
|
|||||||
PROMPTS_DIR = _resolve_repo_path(os.environ.get("PROMPTS_DIR", ""), "./prompts")
|
PROMPTS_DIR = _resolve_repo_path(os.environ.get("PROMPTS_DIR", ""), "./prompts")
|
||||||
AGENT_SKILLS_DIR = _resolve_repo_path(os.environ.get("AGENT_SKILLS_DIR", ""), "./agentskills")
|
AGENT_SKILLS_DIR = _resolve_repo_path(os.environ.get("AGENT_SKILLS_DIR", ""), "./agentskills")
|
||||||
WORKSPACE_SKILLS_DIRNAME = ".agents/skills"
|
WORKSPACE_SKILLS_DIRNAME = ".agents/skills"
|
||||||
# 宿主机模式工作区配置文件(JSON,属于配置,锚定源码树)。
|
# 宿主机模式工作区配置文件(JSON)。属于部署级配置且会被运行时写回,
|
||||||
HOST_WORKSPACES_FILE = _resolve_repo_path(os.environ.get("HOST_WORKSPACES_FILE", ""), "./config/host_workspaces.json")
|
# 因此指向部署目录 ~/.agents/<mode>/config(HOST_WORKSPACES_FILE 显式设置时优先)。
|
||||||
|
HOST_WORKSPACES_FILE = _resolve_repo_path(
|
||||||
|
os.environ.get("HOST_WORKSPACES_FILE", ""),
|
||||||
|
deploy_config_path("host_workspaces.json"),
|
||||||
|
)
|
||||||
|
|
||||||
# ── 基于 DATA_DIR 派生的数据文件 ──
|
# ── 基于 DATA_DIR 派生的数据文件 ──
|
||||||
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
||||||
@ -124,4 +174,7 @@ __all__ = [
|
|||||||
"API_USERS_DB_FILE",
|
"API_USERS_DB_FILE",
|
||||||
"API_TOKENS_FILE",
|
"API_TOKENS_FILE",
|
||||||
"API_USAGE_FILE",
|
"API_USAGE_FILE",
|
||||||
|
"DEPLOY_CONFIG_DIR",
|
||||||
|
"deploy_config_path",
|
||||||
|
"resolve_deploy_config",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,9 +3,15 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from .paths import resolve_deploy_config
|
||||||
|
|
||||||
|
|
||||||
def _load_forbidden_commands() -> list[str]:
|
def _load_forbidden_commands() -> list[str]:
|
||||||
"""从独立 JSON 文件加载命令拦截关键词。"""
|
"""从独立 JSON 文件加载命令拦截关键词。
|
||||||
|
|
||||||
|
属于部署级配置(部署者可调宽松/严苛),优先读 ~/.agents/<mode>/config,
|
||||||
|
回退源码树种子。
|
||||||
|
"""
|
||||||
fallback = [
|
fallback = [
|
||||||
"rm -rf /",
|
"rm -rf /",
|
||||||
"rm -rf ~",
|
"rm -rf ~",
|
||||||
@ -15,7 +21,7 @@ def _load_forbidden_commands() -> list[str]:
|
|||||||
"kill -9",
|
"kill -9",
|
||||||
"dd if=",
|
"dd if=",
|
||||||
]
|
]
|
||||||
cfg_path = Path(__file__).resolve().parent / "forbidden_commands.json"
|
cfg_path = Path(resolve_deploy_config("forbidden_commands.json"))
|
||||||
if not cfg_path.exists():
|
if not cfg_path.exists():
|
||||||
return fallback
|
return fallback
|
||||||
try:
|
try:
|
||||||
|
|||||||
42
config/server.py
Normal file
42
config/server.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Web 服务与运行环境配置(单一事实源)。
|
||||||
|
|
||||||
|
集中所有"部署/运行"相关配置,避免端口、监听地址、运行二进制路径等散落多处、
|
||||||
|
各读各的。全项目应统一从 ``config`` 导入这里导出的常量,禁止再硬编码。
|
||||||
|
|
||||||
|
解析优先级:环境变量 > 此处默认值;命令行参数(如 ``--port``)可在入口层再覆盖。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def _env(name: str, default: str = "") -> str:
|
||||||
|
return str(os.environ.get(name, "") or "").strip() or default
|
||||||
|
|
||||||
|
|
||||||
|
def _flag(name: str, default: str = "0") -> bool:
|
||||||
|
return _env(name, default).lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
|
|
||||||
|
# === Web 服务 ===============================================================
|
||||||
|
# 监听端口(历史上散落在 main.py / server.app_legacy / server.state / cli)。
|
||||||
|
WEB_SERVER_PORT = int(_env("WEB_SERVER_PORT", "8091"))
|
||||||
|
# 监听地址:多用户/服务器用 0.0.0.0;单机便携包建议设 127.0.0.1 只听本机。
|
||||||
|
WEB_SERVER_HOST = _env("WEB_SERVER_HOST", "0.0.0.0")
|
||||||
|
# 调试模式(同时控制 Flask reloader)。
|
||||||
|
WEB_SERVER_DEBUG = _flag("WEB_SERVER_DEBUG", "0")
|
||||||
|
|
||||||
|
# === 运行环境二进制 =========================================================
|
||||||
|
# 便携 release 包启动脚本会注入这两个绝对路径,使主程序与子智能体使用内置运行时。
|
||||||
|
# 留空/默认时回退到系统 PATH 上的 node(开发环境或系统已安装时)。
|
||||||
|
NODE_BIN = _env("AGENT_NODE_BIN", "node")
|
||||||
|
# Python 解释器路径;留空表示使用当前进程的 sys.executable。
|
||||||
|
PYTHON_BIN = _env("AGENT_PYTHON_BIN", "")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"WEB_SERVER_PORT",
|
||||||
|
"WEB_SERVER_HOST",
|
||||||
|
"WEB_SERVER_DEBUG",
|
||||||
|
"NODE_BIN",
|
||||||
|
"PYTHON_BIN",
|
||||||
|
]
|
||||||
2
main.py
2
main.py
@ -195,7 +195,7 @@ class AgentSystem:
|
|||||||
|
|
||||||
print(f"\n{OUTPUT_FORMATS['success']} 正在启动Web服务器...")
|
print(f"\n{OUTPUT_FORMATS['success']} 正在启动Web服务器...")
|
||||||
print(f"{OUTPUT_FORMATS['info']} 项目路径: {self.project_path}")
|
print(f"{OUTPUT_FORMATS['info']} 项目路径: {self.project_path}")
|
||||||
port = int(os.environ.get("WEB_SERVER_PORT", "8091"))
|
port = WEB_SERVER_PORT
|
||||||
|
|
||||||
# 运行服务器(这会阻塞)
|
# 运行服务器(这会阻塞)
|
||||||
run_server(
|
run_server(
|
||||||
|
|||||||
@ -9,13 +9,16 @@ from typing import Any, Callable, Dict, List, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from config import resolve_deploy_config, LOGS_DIR
|
||||||
|
|
||||||
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "auto_approval.json"
|
|
||||||
|
# 部署级配置(审批智能体 url/key/model,含密钥引用)→ 优先 ~/.agents/<mode>/config
|
||||||
|
CONFIG_PATH = Path(resolve_deploy_config("auto_approval.json"))
|
||||||
DEFAULT_MAX_ROUNDS = 3
|
DEFAULT_MAX_ROUNDS = 3
|
||||||
DEFAULT_TIMEOUT_SECONDS = 60
|
DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
DEFAULT_MAX_COMMAND_TIMEOUT = 20
|
DEFAULT_MAX_COMMAND_TIMEOUT = 20
|
||||||
DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT = True
|
DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT = True
|
||||||
DEBUG_TRANSCRIPT_DIR = Path(__file__).resolve().parents[1] / "logs" / "approval_agent"
|
DEBUG_TRANSCRIPT_DIR = Path(LOGS_DIR) / "approval_agent"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_env_token(value: str) -> str:
|
def _resolve_env_token(value: str) -> str:
|
||||||
|
|||||||
@ -23,16 +23,17 @@ from typing import Any, Callable, Dict, List, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from config import PROMPTS_DIR
|
from config import PROMPTS_DIR, resolve_deploy_config, LOGS_DIR
|
||||||
from modules.goal_state_manager import REVIEW_MODE_ACTIVE, REVIEW_MODE_READONLY
|
from modules.goal_state_manager import REVIEW_MODE_ACTIVE, REVIEW_MODE_READONLY
|
||||||
|
|
||||||
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "goal_review.json"
|
# 部署级配置(目标审核智能体 url/key/model,含密钥引用)→ 优先 ~/.agents/<mode>/config
|
||||||
|
CONFIG_PATH = Path(resolve_deploy_config("goal_review.json"))
|
||||||
PROMPT_NAME = "goal_review_agent.txt"
|
PROMPT_NAME = "goal_review_agent.txt"
|
||||||
DEFAULT_MAX_ROUNDS = 3
|
DEFAULT_MAX_ROUNDS = 3
|
||||||
DEFAULT_TIMEOUT_SECONDS = 60
|
DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
DEFAULT_MAX_COMMAND_TIMEOUT = 60
|
DEFAULT_MAX_COMMAND_TIMEOUT = 60
|
||||||
DEBUG_SAVE_GOAL_REVIEW_TRANSCRIPT = True
|
DEBUG_SAVE_GOAL_REVIEW_TRANSCRIPT = True
|
||||||
DEBUG_TRANSCRIPT_DIR = Path(__file__).resolve().parents[1] / "logs" / "goal_review_agent"
|
DEBUG_TRANSCRIPT_DIR = Path(LOGS_DIR) / "goal_review_agent"
|
||||||
|
|
||||||
# 兜底续命消息(审核智能体未能产出明确结论时使用)
|
# 兜底续命消息(审核智能体未能产出明确结论时使用)
|
||||||
FALLBACK_CONTINUE_MESSAGE = "审核未能给出明确结论。请重新对照目标核查当前进度,找出尚未完成的部分并继续推进。"
|
FALLBACK_CONTINUE_MESSAGE = "审核未能给出明确结论。请重新对照目标核查当前进度,找出尚未完成的部分并继续推进。"
|
||||||
|
|||||||
@ -5,10 +5,11 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS
|
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
|
||||||
|
|
||||||
_LOCK = threading.Lock()
|
_LOCK = threading.Lock()
|
||||||
_POLICY_PATH = Path(__file__).resolve().parents[1] / "config" / "host_sandbox_policy.json"
|
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
||||||
|
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
|
||||||
|
|
||||||
|
|
||||||
def _ensure_file() -> None:
|
def _ensure_file() -> None:
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from config import (
|
|||||||
SUB_AGENT_STATE_FILE,
|
SUB_AGENT_STATE_FILE,
|
||||||
SUB_AGENT_STATUS_POLL_INTERVAL,
|
SUB_AGENT_STATUS_POLL_INTERVAL,
|
||||||
SUB_AGENT_TASKS_BASE_DIR,
|
SUB_AGENT_TASKS_BASE_DIR,
|
||||||
|
NODE_BIN,
|
||||||
)
|
)
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
import logging
|
import logging
|
||||||
@ -382,7 +383,7 @@ class SubAgentManager:
|
|||||||
thinking_mode: Optional[str],
|
thinking_mode: Optional[str],
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
cmd = [
|
cmd = [
|
||||||
"node",
|
NODE_BIN,
|
||||||
batch_path,
|
batch_path,
|
||||||
"--workspace", workspace_path,
|
"--workspace", workspace_path,
|
||||||
"--task-file", task_file,
|
"--task-file", task_file,
|
||||||
|
|||||||
1
scripts/__init__.py
Normal file
1
scripts/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""运维与初始化脚本包。"""
|
||||||
@ -14,6 +14,11 @@
|
|||||||
先打包备份到 ``<运行态根上级>/backup_<时间戳>.tar.gz``,
|
先打包备份到 ``<运行态根上级>/backup_<时间戳>.tar.gz``,
|
||||||
校验备份成功后再复制到目标位置;复制采用「不覆盖已存在目标」的
|
校验备份成功后再复制到目标位置;复制采用「不覆盖已存在目标」的
|
||||||
保守策略,全程复制而非移动,旧目录保留,便于回滚。
|
保守策略,全程复制而非移动,旧目录保留,便于回滚。
|
||||||
|
- 部署级配置文件(custom_models / host_workspaces / auto_approval / goal_review /
|
||||||
|
forbidden_commands / host_sandbox_policy)
|
||||||
|
从源码树 ``config/`` 复制到部署配置目录 ``~/.agents/<mode>/config/``,
|
||||||
|
同样先备份、不覆盖已存在目标。docker_risk_markers、skill_hints 属于
|
||||||
|
程序能力,保留源码树,不迁。
|
||||||
|
|
||||||
本脚本只做「复制 + 备份」,不删除任何源数据,确保完全可回滚。
|
本脚本只做「复制 + 备份」,不删除任何源数据,确保完全可回滚。
|
||||||
确认新目录工作正常后,可自行手动删除源码树内的旧目录。
|
确认新目录工作正常后,可自行手动删除源码树内的旧目录。
|
||||||
@ -52,6 +57,30 @@ def _migrate_targets() -> dict[str, Path]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 部署级配置文件:源码树 config/<name> -> 部署配置目录 ~/.agents/<mode>/config/<name>。
|
||||||
|
# 仅迁移部署者自定义类(含密钥/机器特定/可调策略);docker_risk_markers、skill_hints
|
||||||
|
# 属于程序能力,保留源码树,不迁。
|
||||||
|
_DEPLOY_CONFIG_FILES = (
|
||||||
|
"custom_models.json",
|
||||||
|
"host_workspaces.json",
|
||||||
|
"auto_approval.json",
|
||||||
|
"goal_review.json",
|
||||||
|
"forbidden_commands.json",
|
||||||
|
"host_sandbox_policy.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_config_files() -> list[tuple[Path, Path]]:
|
||||||
|
"""返回 (源码树配置文件, 部署目录目标) 列表,仅含实际存在的源文件。"""
|
||||||
|
deploy_dir = Path(config.DEPLOY_CONFIG_DIR)
|
||||||
|
pairs: list[tuple[Path, Path]] = []
|
||||||
|
for name in _DEPLOY_CONFIG_FILES:
|
||||||
|
src = REPO_ROOT / "config" / name
|
||||||
|
if src.is_file():
|
||||||
|
pairs.append((src, deploy_dir / name))
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
def _human_size(num: int) -> str:
|
def _human_size(num: int) -> str:
|
||||||
value = float(num)
|
value = float(num)
|
||||||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||||
@ -111,6 +140,17 @@ def _copy_tree(src: Path, dst: Path, *, dry_run: bool) -> None:
|
|||||||
shutil.copytree(src, dst)
|
shutil.copytree(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_file(src: Path, dst: Path, *, dry_run: bool) -> None:
|
||||||
|
if dst.exists():
|
||||||
|
print(f"[skip] 目标已存在,跳过以避免覆盖:{dst}")
|
||||||
|
return
|
||||||
|
print(f"[copy] {src} -> {dst} ({_human_size(src.stat().st_size)})")
|
||||||
|
if dry_run:
|
||||||
|
return
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="迁移运行态数据到运行态根目录(~/.agents/<mode>)")
|
parser = argparse.ArgumentParser(description="迁移运行态数据到运行态根目录(~/.agents/<mode>)")
|
||||||
parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不实际写入")
|
parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不实际写入")
|
||||||
@ -132,19 +172,27 @@ def main() -> int:
|
|||||||
else:
|
else:
|
||||||
print(f"[absent] 源目录不存在或为空,跳过:{src}")
|
print(f"[absent] 源目录不存在或为空,跳过:{src}")
|
||||||
|
|
||||||
if not present:
|
# 收集需迁移的部署级配置文件(源码树 config/*.json -> 部署目录)
|
||||||
|
config_files = _migrate_config_files()
|
||||||
|
for src, dst in config_files:
|
||||||
|
print(f"[config] 待迁移配置:{src.name} -> {dst}")
|
||||||
|
if not config_files:
|
||||||
|
print("[config] 无部署级配置文件需迁移。")
|
||||||
|
|
||||||
|
if not present and not config_files:
|
||||||
print("没有需要迁移的运行态数据。")
|
print("没有需要迁移的运行态数据。")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# 1) 备份(备份到运行态根的上级目录,与运行态根同级)
|
# 1) 备份(备份到运行态根的上级目录,与运行态根同级)
|
||||||
backup_root = runtime_root.parent
|
backup_root = runtime_root.parent
|
||||||
|
backup_sources = [s for s, _ in present] + [s for s, _ in config_files]
|
||||||
try:
|
try:
|
||||||
_backup([s for s, _ in present], backup_root, dry_run=args.dry_run)
|
_backup(backup_sources, backup_root, dry_run=args.dry_run)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"[error] 备份失败,已中止迁移(未改动任何数据):{exc}", file=sys.stderr)
|
print(f"[error] 备份失败,已中止迁移(未改动任何数据):{exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# 2) 复制(不覆盖已有目标,源目录保留)
|
# 2) 复制(不覆盖已有目标,源保留)
|
||||||
for src, dst in present:
|
for src, dst in present:
|
||||||
try:
|
try:
|
||||||
_copy_tree(src, dst, dry_run=args.dry_run)
|
_copy_tree(src, dst, dry_run=args.dry_run)
|
||||||
@ -152,6 +200,13 @@ def main() -> int:
|
|||||||
print(f"[error] 复制失败:{src} -> {dst}:{exc}", file=sys.stderr)
|
print(f"[error] 复制失败:{src} -> {dst}:{exc}", file=sys.stderr)
|
||||||
print(" 已迁移的部分保留,源数据未删除,可重跑或手动处理。", file=sys.stderr)
|
print(" 已迁移的部分保留,源数据未删除,可重跑或手动处理。", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
for src, dst in config_files:
|
||||||
|
try:
|
||||||
|
_copy_file(src, dst, dry_run=args.dry_run)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[error] 复制配置失败:{src} -> {dst}:{exc}", file=sys.stderr)
|
||||||
|
print(" 已迁移的部分保留,源文件未删除,可重跑或手动处理。", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|||||||
329
scripts/setup.py
Normal file
329
scripts/setup.py
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""首次启动初始化向导(终端交互)。
|
||||||
|
|
||||||
|
在启动 Web 服务之前,于终端一次性设置好"必须在程序启动前确定"的内容,
|
||||||
|
并写出项目根目录的 ``.env``:
|
||||||
|
|
||||||
|
- 运行模式 host / web(决定数据目录与子容器策略,在 import config 时即锁定)
|
||||||
|
- Web 监听端口 / 地址(服务 listen 前必须确定)
|
||||||
|
- 管理员账号(用户名 + 密码哈希,与 user_manager._ensure_admin_user 兼容)
|
||||||
|
- 会话与 Token 密钥(自动随机生成,不打扰用户)
|
||||||
|
- 基础 API 三件套(base_url / key / model_id)
|
||||||
|
- 可选的 thinking / title 模型
|
||||||
|
|
||||||
|
设计要点:
|
||||||
|
|
||||||
|
* **单一线性流程**:逐项询问,每项带默认值,回车即取默认。
|
||||||
|
* **不联网校验**:API 配置只写入,不当场测试连通性。
|
||||||
|
* **以 .env.example 为模板**:逐行读取模板,仅替换被本向导接管的键的值,
|
||||||
|
其余行(包括全部注释与未涉及的配置项)原样保留——保证生成的 .env
|
||||||
|
仍带完整文档,也不会丢任何配置开关。
|
||||||
|
* **不 import config**:向导在 .env 尚不存在时运行,且模式选择会影响路径解析,
|
||||||
|
因此刻意不依赖 config,避免过早锁定路径或触发 .env 加载。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python -m scripts.setup # 交互式向导
|
||||||
|
python scripts/setup.py # 同上
|
||||||
|
python -m scripts.setup --force # 跳过"已存在 .env"的确认(仍会备份)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from getpass import getpass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
except ImportError: # pragma: no cover - 缺依赖时给出可读提示
|
||||||
|
print("缺少依赖 werkzeug,请先安装:pip install -r requirements.txt")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ENV_PATH = REPO_ROOT / ".env"
|
||||||
|
ENV_EXAMPLE_PATH = REPO_ROOT / ".env.example"
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_config_dir(mode: str, custom_data: str) -> Path:
|
||||||
|
"""推导部署级配置目录(~/.agents/<mode>/config)。
|
||||||
|
|
||||||
|
必须与 config/paths.py 的解析保持一致:
|
||||||
|
- 自定义数据根(向导里填的)优先,作为运行态根目录;
|
||||||
|
- 否则默认 ~/.agents/<mode>;
|
||||||
|
- 配置目录 = 运行态根 / config。
|
||||||
|
|
||||||
|
setup 刻意不 import config(避免在 .env 写好前过早锁定路径),
|
||||||
|
因此这里用向导现场收集到的 mode / custom_data 自行推导。
|
||||||
|
"""
|
||||||
|
root = Path(custom_data).expanduser() if custom_data else (Path("~/.agents").expanduser() / mode)
|
||||||
|
return (root / "config").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
# === 终端交互辅助 ===========================================================
|
||||||
|
|
||||||
|
def _ask(prompt: str, default: str = "") -> str:
|
||||||
|
"""询问一项,回车取默认。default 非空时在提示里展示。"""
|
||||||
|
suffix = f" [{default}]" if default else ""
|
||||||
|
try:
|
||||||
|
raw = input(f"{prompt}{suffix}: ").strip()
|
||||||
|
except EOFError:
|
||||||
|
raw = ""
|
||||||
|
return raw or default
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_choice(prompt: str, choices: list[str], default: str) -> str:
|
||||||
|
"""从有限选项中选择,输入不在集合内则重问。"""
|
||||||
|
choices_lower = {c.lower(): c for c in choices}
|
||||||
|
hint = "/".join(choices)
|
||||||
|
while True:
|
||||||
|
raw = _ask(f"{prompt}({hint})", default).lower()
|
||||||
|
if raw in choices_lower:
|
||||||
|
return choices_lower[raw]
|
||||||
|
print(f" 请输入 {hint} 之一。")
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_required(prompt: str, default: str = "") -> str:
|
||||||
|
"""必填项,留空则重问。"""
|
||||||
|
while True:
|
||||||
|
value = _ask(prompt, default)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
print(" 此项必填,不能为空。")
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_password(prompt: str) -> str:
|
||||||
|
"""隐藏输入密码,二次确认一致。"""
|
||||||
|
while True:
|
||||||
|
first = getpass(f"{prompt}: ")
|
||||||
|
if not first:
|
||||||
|
print(" 密码不能为空。")
|
||||||
|
continue
|
||||||
|
second = getpass("再次输入确认: ")
|
||||||
|
if first != second:
|
||||||
|
print(" 两次输入不一致,请重试。")
|
||||||
|
continue
|
||||||
|
return first
|
||||||
|
|
||||||
|
|
||||||
|
# === .env 生成 =============================================================
|
||||||
|
|
||||||
|
def _render_env(values: dict[str, str]) -> str:
|
||||||
|
"""以 .env.example 为模板渲染 .env:仅替换被接管键的值,其余原样保留。
|
||||||
|
|
||||||
|
被接管的键写入用户配置值;模板里未被接管的键与全部注释、空行原样保留。
|
||||||
|
若某接管键在模板中不存在(理论上不会),追加到末尾。
|
||||||
|
"""
|
||||||
|
if not ENV_EXAMPLE_PATH.exists():
|
||||||
|
# 没有模板时退化为按 values 直接生成(无注释)
|
||||||
|
return "".join(f"{k}={v}\n" for k, v in values.items())
|
||||||
|
|
||||||
|
remaining = dict(values)
|
||||||
|
out_lines: list[str] = []
|
||||||
|
for raw in ENV_EXAMPLE_PATH.read_text(encoding="utf-8").splitlines():
|
||||||
|
stripped = raw.strip()
|
||||||
|
if stripped and not stripped.startswith("#") and "=" in stripped:
|
||||||
|
key = stripped.split("=", 1)[0].strip()
|
||||||
|
if key in remaining:
|
||||||
|
out_lines.append(f"{key}={remaining.pop(key)}")
|
||||||
|
continue
|
||||||
|
out_lines.append(raw)
|
||||||
|
|
||||||
|
# 模板里没有、但本向导要写的键(兜底,正常不会触发)
|
||||||
|
if remaining:
|
||||||
|
out_lines.append("")
|
||||||
|
out_lines.append("# === 由 setup 向导补充 ===")
|
||||||
|
for key, value in remaining.items():
|
||||||
|
out_lines.append(f"{key}={value}")
|
||||||
|
|
||||||
|
return "\n".join(out_lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_existing_env() -> Path | None:
|
||||||
|
if not ENV_PATH.exists():
|
||||||
|
return None
|
||||||
|
backup = ENV_PATH.with_name(f".env.bak_{time.strftime('%Y%m%d_%H%M%S')}")
|
||||||
|
shutil.copy2(ENV_PATH, backup)
|
||||||
|
return backup
|
||||||
|
|
||||||
|
|
||||||
|
# === custom_models.json 生成 ==============================================
|
||||||
|
|
||||||
|
def _build_custom_model_entry(
|
||||||
|
*, model_name: str, url: str, api_key: str, model_id: str, supports_thinking: bool
|
||||||
|
) -> dict:
|
||||||
|
"""构造一个最小可用的 custom_models 条目。
|
||||||
|
|
||||||
|
字段与 config/model_profiles.py 的解析规则对齐:
|
||||||
|
- url / apikey 直接写字面量值(解析器对非全大写字符串按字面量处理);
|
||||||
|
- param_toggle 类型用 thinkmode_status.model_id 承载真实模型 ID;
|
||||||
|
- reasoning_capability 决定是否暴露思考能力。
|
||||||
|
"""
|
||||||
|
capability = "fast,thinking" if supports_thinking else "fast"
|
||||||
|
entry: dict = {
|
||||||
|
"model_name": model_name,
|
||||||
|
"description": model_name,
|
||||||
|
"visible": True,
|
||||||
|
"url": url,
|
||||||
|
"apikey": api_key,
|
||||||
|
"multimodal": "none",
|
||||||
|
"reasoning_capability": capability,
|
||||||
|
"context_window": 128000,
|
||||||
|
"max_output_tokens": 32768,
|
||||||
|
"thinkmode_status": {
|
||||||
|
"type": "param_toggle",
|
||||||
|
"model_id": model_id,
|
||||||
|
"fast_extra_parameter": {},
|
||||||
|
"thinking_extra_parameter": {},
|
||||||
|
},
|
||||||
|
"extra_parameter": {},
|
||||||
|
"model_description": f"你的基础模型是 {model_name}。",
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _write_custom_models(target_path: Path, entry: dict) -> Path | None:
|
||||||
|
"""写出 custom_models.json 到部署配置目录。已存在则先备份再覆盖。"""
|
||||||
|
backup = None
|
||||||
|
if target_path.exists():
|
||||||
|
backup = target_path.with_name(
|
||||||
|
f"custom_models.json.bak_{time.strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
)
|
||||||
|
shutil.copy2(target_path, backup)
|
||||||
|
payload = {"models": [entry]}
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_path.write_text(
|
||||||
|
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
return backup
|
||||||
|
|
||||||
|
|
||||||
|
# === 主流程 ================================================================
|
||||||
|
|
||||||
|
def run_wizard(force: bool = False) -> int:
|
||||||
|
print("=" * 60)
|
||||||
|
print(" AI Agent 首次启动初始化向导")
|
||||||
|
print("=" * 60)
|
||||||
|
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if ENV_PATH.exists() and not force:
|
||||||
|
print(f"检测到已存在配置文件:{ENV_PATH}")
|
||||||
|
ans = _ask_choice("是否备份后重新配置", ["y", "n"], "n")
|
||||||
|
if ans == "n":
|
||||||
|
print("已取消,未做任何修改。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
|
||||||
|
# --- 1. 运行模式 ---
|
||||||
|
print("\n[1/5] 运行模式")
|
||||||
|
print(" host = 单机本地模式,命令直接在宿主机沙箱执行")
|
||||||
|
print(" web = 多用户模式,每个用户分配独立 Docker 子容器")
|
||||||
|
mode = _ask_choice("选择运行模式", ["host", "web"], "host")
|
||||||
|
values["TERMINAL_SANDBOX_MODE"] = mode
|
||||||
|
default_host = "127.0.0.1" if mode == "host" else "0.0.0.0"
|
||||||
|
|
||||||
|
# --- 2. Web 服务 ---
|
||||||
|
print("\n[2/5] Web 服务监听")
|
||||||
|
values["WEB_SERVER_PORT"] = _ask("监听端口", "8091")
|
||||||
|
print(" 监听地址:单机本地建议 127.0.0.1(只听本机);多用户/服务器用 0.0.0.0")
|
||||||
|
values["WEB_SERVER_HOST"] = _ask("监听地址", default_host)
|
||||||
|
|
||||||
|
# --- 3. 数据目录(可选覆盖)---
|
||||||
|
print("\n[3/5] 数据目录")
|
||||||
|
default_data = f"~/.agents/{mode}"
|
||||||
|
print(f" 运行态数据(对话/用户/记忆/日志)默认存放在 {default_data}")
|
||||||
|
custom_data = _ask("自定义数据根目录(留空用默认)", "")
|
||||||
|
if custom_data:
|
||||||
|
# 模式根变量:host -> AGENTS_HOST_HOME,web -> AGENTS_WEB_HOME
|
||||||
|
values["AGENTS_HOST_HOME" if mode == "host" else "AGENTS_WEB_HOME"] = custom_data
|
||||||
|
|
||||||
|
# --- 4. 管理员账户 ---
|
||||||
|
print("\n[4/5] 管理员账户")
|
||||||
|
admin_user = _ask("管理员用户名", "admin")
|
||||||
|
admin_pw = _ask_password("管理员密码")
|
||||||
|
values["AGENT_ADMIN_USERNAME"] = admin_user
|
||||||
|
values["AGENT_ADMIN_PASSWORD_HASH"] = generate_password_hash(admin_pw)
|
||||||
|
|
||||||
|
# --- 5. 模型配置(写入部署级配置目录 ~/.agents/<mode>/config/custom_models.json)---
|
||||||
|
print("\n[5/5] 模型配置")
|
||||||
|
print(" 配置一个可用模型(写入 ~/.agents/<mode>/config/custom_models.json)。")
|
||||||
|
print(" 启动后还可在后台页面继续增删模型。")
|
||||||
|
model_name = _ask_required("模型显示名(界面上看到的名字)", "默认模型")
|
||||||
|
model_url = _ask_required("API 地址(Base URL)", "")
|
||||||
|
model_apikey = _ask_required("API Key", "")
|
||||||
|
model_id = _ask_required("模型 ID(请求里实际发送的 model 字段)", "")
|
||||||
|
supports_thinking = _ask_choice(
|
||||||
|
"该模型是否支持思考模式", ["y", "n"], "n"
|
||||||
|
) == "y"
|
||||||
|
|
||||||
|
model_entry = _build_custom_model_entry(
|
||||||
|
model_name=model_name,
|
||||||
|
url=model_url,
|
||||||
|
api_key=model_apikey,
|
||||||
|
model_id=model_id,
|
||||||
|
supports_thinking=supports_thinking,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 自动生成密钥 ---
|
||||||
|
values["WEB_SECRET_KEY"] = secrets.token_hex(32)
|
||||||
|
values["API_TOKEN_SECRET"] = secrets.token_hex(32)
|
||||||
|
|
||||||
|
# --- 写出 ---
|
||||||
|
rendered = _render_env(values)
|
||||||
|
backup = _backup_existing_env()
|
||||||
|
ENV_PATH.write_text(rendered, encoding="utf-8")
|
||||||
|
try:
|
||||||
|
ENV_PATH.chmod(0o600) # .env 含密钥,收紧权限(Windows 上静默忽略)
|
||||||
|
except (OSError, NotImplementedError):
|
||||||
|
pass
|
||||||
|
# 模型配置写到部署级配置目录(~/.agents/<mode>/config),而非源码树
|
||||||
|
custom_models_path = _deploy_config_dir(mode, custom_data) / "custom_models.json"
|
||||||
|
models_backup = _write_custom_models(custom_models_path, model_entry)
|
||||||
|
|
||||||
|
# --- 摘要 ---
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" 配置完成")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" 配置文件:{ENV_PATH}")
|
||||||
|
if backup:
|
||||||
|
print(f" 原配置已备份:{backup}")
|
||||||
|
print(f" 模型配置:{custom_models_path}")
|
||||||
|
if models_backup:
|
||||||
|
print(f" 原模型配置已备份:{models_backup}")
|
||||||
|
print(f" 运行模式:{mode}")
|
||||||
|
print(f" 监听地址:{values['WEB_SERVER_HOST']}:{values['WEB_SERVER_PORT']}")
|
||||||
|
print(f" 数据目录:{custom_data or default_data}")
|
||||||
|
print(f" 管理员: {admin_user}")
|
||||||
|
print(f" 模型: {model_name}({model_id})@ {model_url}")
|
||||||
|
print(f" 密钥: 已自动生成 WEB_SECRET_KEY / API_TOKEN_SECRET")
|
||||||
|
print()
|
||||||
|
print(" 下一步启动服务:")
|
||||||
|
print(" python -m server.app")
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="AI Agent 首次启动初始化向导")
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="跳过『已存在 .env』的确认提示(仍会先备份再覆盖)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
return run_wizard(force=args.force)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n已中断,未写入配置。")
|
||||||
|
return 130
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@ -228,6 +228,8 @@ from config import (
|
|||||||
PROJECT_MAX_STORAGE_MB,
|
PROJECT_MAX_STORAGE_MB,
|
||||||
PROJECT_MAX_STORAGE_BYTES,
|
PROJECT_MAX_STORAGE_BYTES,
|
||||||
UPLOAD_SCAN_LOG_SUBDIR,
|
UPLOAD_SCAN_LOG_SUBDIR,
|
||||||
|
WEB_SERVER_PORT,
|
||||||
|
WEB_SERVER_HOST,
|
||||||
)
|
)
|
||||||
from modules.user_manager import UserManager, UserWorkspace
|
from modules.user_manager import UserManager, UserWorkspace
|
||||||
from modules.gui_file_manager import GuiFileManager
|
from modules.gui_file_manager import GuiFileManager
|
||||||
@ -325,7 +327,7 @@ ADMIN_CUSTOM_TOOLS_DIR = (Path(app.static_folder) / 'custom_tools').resolve()
|
|||||||
RECENT_UPLOAD_EVENT_LIMIT = 150
|
RECENT_UPLOAD_EVENT_LIMIT = 150
|
||||||
RECENT_UPLOAD_FEED_LIMIT = 60
|
RECENT_UPLOAD_FEED_LIMIT = 60
|
||||||
|
|
||||||
DEFAULT_PORT = 8091
|
DEFAULT_PORT = WEB_SERVER_PORT
|
||||||
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
||||||
CSRF_HEADER_NAME = state.CSRF_HEADER_NAME
|
CSRF_HEADER_NAME = state.CSRF_HEADER_NAME
|
||||||
CSRF_SESSION_KEY = state.CSRF_SESSION_KEY
|
CSRF_SESSION_KEY = state.CSRF_SESSION_KEY
|
||||||
@ -1343,7 +1345,7 @@ def run_server(path: str, thinking_mode: bool = False, port: int = DEFAULT_PORT,
|
|||||||
start_background_jobs()
|
start_background_jobs()
|
||||||
socketio.run(
|
socketio.run(
|
||||||
app,
|
app,
|
||||||
host='0.0.0.0',
|
host=WEB_SERVER_HOST,
|
||||||
port=port,
|
port=port,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
use_reloader=debug,
|
use_reloader=debug,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from config import TITLE_API_BASE_URL, TITLE_API_KEY, TITLE_MODEL_ID
|
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
from config import LOGS_DIR
|
from config import LOGS_DIR
|
||||||
from utils.api_client import DeepSeekClient
|
from utils.api_client import DeepSeekClient
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from collections import defaultdict, deque
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from config import LOGS_DIR, PROJECT_MAX_STORAGE_BYTES, PROJECT_MAX_STORAGE_MB
|
from config import LOGS_DIR, PROJECT_MAX_STORAGE_BYTES, PROJECT_MAX_STORAGE_MB, WEB_SERVER_PORT
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
from modules.custom_tool_registry import CustomToolRegistry
|
from modules.custom_tool_registry import CustomToolRegistry
|
||||||
from modules.mcp_server_registry import MCPServerRegistry
|
from modules.mcp_server_registry import MCPServerRegistry
|
||||||
@ -60,7 +60,7 @@ PROJECT_STORAGE_CACHE: Dict[str, Dict[str, Any]] = {}
|
|||||||
PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_TTL", "30"))
|
PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_TTL", "30"))
|
||||||
|
|
||||||
# 其他配置
|
# 其他配置
|
||||||
DEFAULT_PORT = 8091
|
DEFAULT_PORT = WEB_SERVER_PORT
|
||||||
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
||||||
CSRF_HEADER_NAME = "X-CSRF-Token"
|
CSRF_HEADER_NAME = "X-CSRF-Token"
|
||||||
CSRF_SESSION_KEY = "_csrf_token"
|
CSRF_SESSION_KEY = "_csrf_token"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ print(json.dumps({
|
|||||||
"LOGS_DIR": config.LOGS_DIR,
|
"LOGS_DIR": config.LOGS_DIR,
|
||||||
"USER_SPACE_DIR": config.USER_SPACE_DIR,
|
"USER_SPACE_DIR": config.USER_SPACE_DIR,
|
||||||
"API_USER_SPACE_DIR": config.API_USER_SPACE_DIR,
|
"API_USER_SPACE_DIR": config.API_USER_SPACE_DIR,
|
||||||
|
"DEPLOY_CONFIG_DIR": config.DEPLOY_CONFIG_DIR,
|
||||||
"SUB_AGENT_STATE_FILE": config.SUB_AGENT_STATE_FILE,
|
"SUB_AGENT_STATE_FILE": config.SUB_AGENT_STATE_FILE,
|
||||||
"SUB_AGENT_TASKS_BASE_DIR": config.SUB_AGENT_TASKS_BASE_DIR,
|
"SUB_AGENT_TASKS_BASE_DIR": config.SUB_AGENT_TASKS_BASE_DIR,
|
||||||
}, ensure_ascii=False))
|
}, ensure_ascii=False))
|
||||||
@ -37,6 +38,7 @@ print(json.dumps({
|
|||||||
"TERMINAL_SANDBOX_MODE", "AGENTS_HOST_HOME", "AGENTS_WEB_HOME",
|
"TERMINAL_SANDBOX_MODE", "AGENTS_HOST_HOME", "AGENTS_WEB_HOME",
|
||||||
"DATA_DIR", "LOGS_DIR", "USER_SPACE_DIR", "API_USER_SPACE_DIR",
|
"DATA_DIR", "LOGS_DIR", "USER_SPACE_DIR", "API_USER_SPACE_DIR",
|
||||||
"DEFAULT_PROJECT_PATH", "HOST_WORKSPACES_FILE", "PROMPTS_DIR",
|
"DEFAULT_PROJECT_PATH", "HOST_WORKSPACES_FILE", "PROMPTS_DIR",
|
||||||
|
"DEPLOY_CONFIG_DIR",
|
||||||
"SUB_AGENT_STATE_FILE", "SUB_AGENT_TASKS_BASE_DIR",
|
"SUB_AGENT_STATE_FILE", "SUB_AGENT_TASKS_BASE_DIR",
|
||||||
):
|
):
|
||||||
env.pop(key, None)
|
env.pop(key, None)
|
||||||
@ -80,11 +82,11 @@ print(json.dumps({
|
|||||||
self.assertEqual(data["LOGS_DIR"], str(host_home / "logs"))
|
self.assertEqual(data["LOGS_DIR"], str(host_home / "logs"))
|
||||||
|
|
||||||
def test_source_tree_paths_stay_anchored_to_repo_root(self):
|
def test_source_tree_paths_stay_anchored_to_repo_root(self):
|
||||||
"""配置类路径(提示词、host_workspaces.json、默认工作区)仍锚定源码树。"""
|
"""配置类路径(提示词、默认工作区)仍锚定源码树。"""
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "docker"})
|
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "docker"})
|
||||||
|
|
||||||
for key in ("DEFAULT_PROJECT_PATH", "HOST_WORKSPACES_FILE", "PROMPTS_DIR"):
|
for key in ("DEFAULT_PROJECT_PATH", "PROMPTS_DIR"):
|
||||||
value = Path(data[key])
|
value = Path(data[key])
|
||||||
self.assertTrue(value.is_absolute(), f"{key} not absolute: {data[key]}")
|
self.assertTrue(value.is_absolute(), f"{key} not absolute: {data[key]}")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
@ -92,6 +94,30 @@ print(json.dumps({
|
|||||||
f"{key} should anchor to repo root: {data[key]}",
|
f"{key} should anchor to repo root: {data[key]}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_deploy_config_anchors_to_runtime_root(self):
|
||||||
|
"""部署级配置目录与 host_workspaces.json 锚定到运行态根 ~/.agents/<mode>/config。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "web"})
|
||||||
|
|
||||||
|
web_config = Path("~/.agents/web/config").expanduser().resolve()
|
||||||
|
self.assertEqual(data["DEPLOY_CONFIG_DIR"], str(web_config))
|
||||||
|
self.assertEqual(
|
||||||
|
data["HOST_WORKSPACES_FILE"], str(web_config / "host_workspaces.json")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_deploy_config_follows_mode_home(self):
|
||||||
|
"""模式根变量覆盖时,部署配置目录跟随该根目录。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
custom = Path(td) / "web_root"
|
||||||
|
data = self._load_paths(
|
||||||
|
cwd=Path(td),
|
||||||
|
extra_env={
|
||||||
|
"TERMINAL_SANDBOX_MODE": "web",
|
||||||
|
"AGENTS_WEB_HOME": str(custom),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(data["DEPLOY_CONFIG_DIR"], str((custom / "config").resolve()))
|
||||||
|
|
||||||
def test_mode_home_env_overrides_root(self):
|
def test_mode_home_env_overrides_root(self):
|
||||||
"""模式根变量(AGENTS_HOST_HOME / AGENTS_WEB_HOME)整体搬迁运行态目录。"""
|
"""模式根变量(AGENTS_HOST_HOME / AGENTS_WEB_HOME)整体搬迁运行态目录。"""
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
|||||||
@ -14,14 +14,8 @@ from pathlib import Path
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
try:
|
try:
|
||||||
from config import (
|
from config import (
|
||||||
API_BASE_URL,
|
|
||||||
API_KEY,
|
|
||||||
MODEL_ID,
|
|
||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
DEFAULT_RESPONSE_MAX_TOKENS,
|
DEFAULT_RESPONSE_MAX_TOKENS,
|
||||||
THINKING_API_BASE_URL,
|
|
||||||
THINKING_API_KEY,
|
|
||||||
THINKING_MODEL_ID,
|
|
||||||
LOGS_DIR,
|
LOGS_DIR,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -31,14 +25,8 @@ except ImportError:
|
|||||||
if str(project_root) not in sys.path:
|
if str(project_root) not in sys.path:
|
||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
from config import (
|
from config import (
|
||||||
API_BASE_URL,
|
|
||||||
API_KEY,
|
|
||||||
MODEL_ID,
|
|
||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
DEFAULT_RESPONSE_MAX_TOKENS,
|
DEFAULT_RESPONSE_MAX_TOKENS,
|
||||||
THINKING_API_BASE_URL,
|
|
||||||
THINKING_API_KEY,
|
|
||||||
THINKING_MODEL_ID,
|
|
||||||
LOGS_DIR,
|
LOGS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,15 +39,17 @@ def _api_dump_enabled() -> bool:
|
|||||||
|
|
||||||
class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
||||||
def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
|
def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
|
||||||
|
# 模型 API 配置(端点 / 密钥 / 模型 ID)由 apply_profile 从 custom_models 注入;
|
||||||
|
# 这里仅初始化为空,未应用任何模型档案前不可直接发请求。
|
||||||
self.fast_api_config = {
|
self.fast_api_config = {
|
||||||
"base_url": API_BASE_URL,
|
"base_url": "",
|
||||||
"api_key": API_KEY,
|
"api_key": "",
|
||||||
"model_id": MODEL_ID
|
"model_id": ""
|
||||||
}
|
}
|
||||||
self.thinking_api_config = {
|
self.thinking_api_config = {
|
||||||
"base_url": THINKING_API_BASE_URL or API_BASE_URL,
|
"base_url": "",
|
||||||
"api_key": THINKING_API_KEY or API_KEY,
|
"api_key": "",
|
||||||
"model_id": THINKING_MODEL_ID or MODEL_ID
|
"model_id": ""
|
||||||
}
|
}
|
||||||
self.fast_max_tokens = None
|
self.fast_max_tokens = None
|
||||||
self.thinking_max_tokens = None
|
self.thinking_max_tokens = None
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user