agent-Specialization/docs/config_refactor_plan.md
JOJO e3c897947f refactor(paths): 统一迁移运行态数据路径到 Astrion 命名空间
- 工作区内部路径 .agents/ -> .astrion/
- 默认运行态数据根 ~/.agents/agents/ -> ~/.astrion/astrion/
- 环境变量 AGENTS_DATA_ROOT -> ASTRION_DATA_ROOT(无兼容)
- 同步更新代码、测试、文档与脚本中的路径引用
- 新增 .gitignore 忽略 .astrion/ 与 .agents/ 运行态目录
2026-07-10 00:35:15 +08:00

523 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 配置系统改造计划
> 版本v2.0 | 日期2026-06-13 | 基于 OpenCode 架构分析 + 当前代码现状
---
## 〇、最终目录结构(已确认)
### 目录树
```
~/.astrion/astrion/ ← 数据根目录data_root
├── settings.json ← 唯一配置文件
├── config/ ← 部署级 JSON两个模式共享
│ ├── custom_models.json
│ ├── host_workspaces.json
│ ├── host_sandbox_policy.json
│ ├── forbidden_commands.json
│ └── ...
├── host/ ← host 模式(单用户)
│ ├── data/ ← 对话/记忆/用户数据库
│ └── logs/
└── web/ ← web 模式(多用户)
├── users/ ← 每个用户独立工作区
│ ├── alice/
│ │ └── data/ ← alice 的对话/记忆
│ └── bob/
│ └── data/
└── logs/ ← web 服务级日志
```
> **注意**host 模式没有 `users/`(单用户直接走 `data/`web 模式根下没有 `data/`(数据分散在各 `users/<name>/` 下)。
### 元配置 vs 普通配置
**元配置**(启动前必须知道,不在 `settings.json` 里):
| 元配置项 | 设定方式 | 默认值 |
|---------|---------|--------|
| `data_root` | CLI `--data-root` / 环境变量 `ASTRION_DATA_ROOT` | `~/.astrion/astrion` |
**普通配置**(启动后加载,在 `settings.json` 里):
| 分类 | 示例 |
|------|------|
| 运行模式 | `terminal.sandbox_mode`host/web |
| 服务 | `server.port`、`server.host` |
| 模型 | `models.default_model_key` |
| 终端 | `terminal.execution_mode_default`、沙箱参数 |
| 管理员 | `admin.username`、`admin.password_hash` |
| Flags | `flags.api_dump_enabled` 等 |
### 启动流程
```
1. 解析 CLI args / 环境变量 → 确定 data_root默认 ~/.astrion/astrion
2. 在 {data_root}/ 下创建目录结构(若不存在)
3. 加载 {data_root}/settings.json
4. 根据 settings.json 中的 sandbox_mode 确定数据子目录host/ 或 web/
5. 初始化子目录下的 data/ users/ logs/
6. 加载 {data_root}/config/ 下的部署级 JSON
7. 启动 Web 服务
```
关键约束:
- `data_root` 是元配置,**不在** `settings.json` 里(否则鸡生蛋)
- `config/` 在根目录,两个模式**共享**custom_models 不应该切换模式就丢失)
- 首次启动时若 `settings.json` 不存在,使用硬编码默认值 + 自动弹出初始化向导
与 OpenCode 对照:
```
OpenCode: ~/.config/opencode/config.json + ~/.local/share/opencode/
我们: {data_root}/settings.json + {data_root}/{mode}/
其中 data_root 默认 ~/.astrion/astrion
```
---
## 一、现状诊断
### 1.1 架构问题全景图
```
当前启动流程:
./start.sh
→ _bootstrap.sh创建 venv、pip install、npm ci & build
→ 若 .env 不存在 → python -m scripts.setup交互式向导写出 .env
→ exec python -m server.app
→ import config触达 _load_dotenv(),锁定一切 ↓)
┌──────────────────────────────────────────────────────────┐
│ import config 瞬间,以下全部固化为模块级常量: │
│ │
│ config/paths.py RUNTIME_ROOT, DATA_DIR, LOGS_DIR, │
│ USER_SPACE_DIR, DEFAULT_PROJECT_PATH │
│ config/server.py WEB_SERVER_PORT, WEB_SERVER_HOST │
│ config/terminal.py TERMINAL_SANDBOX_MODE, ... │
│ config/auth.py ADMIN_USERNAME, ADMIN_PASSWORD_HASH │
│ config/model_profiles.py 模型列表 │
└──────────────────────────────────────────────────────────┘
当前目录结构问题web 模式数据也落在 host/ 下,分流名存实亡):
~/.astrion/
├── host/ ← 实际数据全在这里(因为 .env 写死 host
│ ├── .env ← ❌ 配置居然放在数据目录里
│ ├── config/
│ ├── data/
│ ├── users/
│ └── logs/
└── web/ ← 几乎空,从未被真正使用
```
### 1.2 .env 被 6 处独立读取
| # | 文件 | 读取方式 | 问题 |
|---|------|---------|------|
| 1 | `config/__init__.py:_load_dotenv()` | 主加载器,读入 `os.environ` | 唯一的"正规"入口,但逻辑是 .env 不覆盖已有环境变量 |
| 2 | `config/auth.py:_dotenv_cache()` | 直接读 `.env` 文件,有独立缓存 | **优先级反转**:优先 .env 再回退 os.environ |
| 3 | `config/model_profiles.py:_env_optional()` | 先 os.environ回退读 `.env` | 与主加载器逻辑重复 |
| 4 | `utils/api_client.py:_resolve_env_value()` | 同上 | 同上 |
| 5 | `server/chat_flow_helpers.py:_env_optional()` | 同上 | 同上 |
| 6 | `modules/balance_client.py:_read_dotenv()` | 直接读 `.env`,有独立缓存 | 第 3 份 .env 解析实现 |
**核心问题**没有统一的配置来源6 个模块各有一套 .env 解析逻辑,优先级不一致。
### 1.3 历史遗留代码
| 文件 | 问题 |
|------|------|
| `web_server.py` | 已标记 deprecated但仍存在且是 `main.py` 的实际调用路径 |
| `main.py` | CLI 模式代码是死代码(`setup_run_mode()` 硬编码 `self.web_mode = True` |
| `server/app.py` | 完全转发 `app_legacy.py`"逐步替换"从未实现 |
| `server/app_legacy.py` | 1409 行巨型文件,包含了从路由到工具执行的几乎所有逻辑 |
### 1.4 初始化脚本
```
setup.sh → _bootstrap.sh共享引导→ scripts/setup.py交互向导
start.sh → _bootstrap.sh共享引导→ python -m server.app
```
- `scripts/setup.py` 刻意不 `import config`(怕过早锁定路径),说明架构本身有问题
- 每次启动都走 `ensure_python_env` / `ensure_node_env`
---
## 二、OpenCode 架构参考
### 2.1 核心设计原则
| 原则 | OpenCode 做法 |
|------|-------------|
| **配置存储** | JSON(C) 文件 + 环境变量,**无 .env 文件** |
| **加载时机** | 运行时 Effect Service惰性按需加载 |
| **路径标准** | XDG 标准(`~/.config/opencode`、`~/.local/share/opencode` |
| **配置层级** | 9 层优先级叠加全局→项目→远程→托管→MDM |
| **变量引用** | `{env:VAR}` / `{file:path}` 语法 |
| **运行时修改** | `Config.update()` / `Config.updateGlobal()` 写回 JSON |
| **依赖管理** | Effect-TS Layer 依赖注入,服务间松耦合 |
### 2.2 可借鉴的关键模式
**模式 1运行时 Service 而非模块常量**
```python
# ❌ 当前做法
from config import DATA_DIR # import 时固化
# ✅ 目标做法
from config.manager import config_manager
data_dir = config_manager.get("DATA_DIR") # 运行时获取,支持热更新
```
**模式 2分层配置优先级**
```
优先级(高→低):
运行时 Setconfig_manager.set()
→ 环境变量覆盖
→ 项目级配置(<workspace>/.astrion/config.json
→ 全局配置(~/.astrion/astrion/settings.json
→ 默认值
```
**模式 3统一 ConfigManager 单例**
```
ConfigManager
├── load() 从 JSON 文件加载
├── get(key) 获取配置值
├── set(key, value) 运行时修改 + 持久化
├── reload() 重新加载(配置热更新)
├── subscribe() 变更监听
└── _resolve_path() 路径解析
```
**模式 4Flag 系统**
```python
# 所有 feature flag 集中定义
class Flags:
DISABLE_AUTOCOMPACT = _flag("AGENT_DISABLE_AUTOCOMPACT")
ENABLE_API_DUMP = _flag("AGENT_API_DUMP_ENABLED")
# ...
```
---
## 三、改造目标
### 3.1 最终状态
```
用户启动
→ python -m server.app [--data-root ~/.astrion/astrion]
→ 解析 data_root元配置CLI > 环境变量 > 默认值)
→ ConfigManager.initialize(data_root)
→ 加载 {data_root}/settings.json
→ 解析 sandbox_mode → 确定数据子目录 {data_root}/{mode}/
→ 加载 {data_root}/config/ 下的部署级 JSON
→ Web 服务启动
→ 管理后台 UI 可实时修改 settings.json 中的配置
→ 配置变更自动通知相关组件
```
### 3.2 核心原则
1. **启动后再配置**:除最低限度启动参数(端口等)外,所有配置可在运行时修改
2. **单一配置源**:所有模块统一通过 `ConfigManager` 获取配置
3. **JSON 为主、.env 退役**:配置文件统一为 `{data_root}/settings.json`。.env 仅作为开发环境快捷方式(可选),不再参与正式配置流程
4. **元配置与普通配置分离**`data_root` 等必须在加载配置前确定的参数走 CLI/环境变量,其余走 `settings.json`
4. **向后兼容**:分阶段改造,每阶段可独立上线
5. **最小改动**:先统一读取入口,再逐步改造消费端
---
## 四、分阶段改造计划
### 阶段 1统一配置源预计改动文件数~8
**目标**:消除 6 处独立 .env 读取,所有配置从单一入口获取。
#### 1.1 新建 `config/manager.py`
```python
"""
ConfigManager — 统一配置管理器(单例)
职责:
- 从 JSON 配置文件加载配置
- .env 仅作为"种子"ConfigManager 加载后统一接管
- 所有模块通过 ConfigManager 获取配置,不再直接读 .env 或 os.environ
"""
class ConfigManager:
_instance = None
def __init__(self):
self._data: dict = {}
self._seeds: dict = {} # 从 .env 一次性读入的种子值
self._callbacks: list = []
# ── 初始化 ──
def initialize(self, mode: str | None = None):
"""启动时调用一次。加载 .env 种子 → 合并 JSON 配置 → 应用默认值。"""
self._load_env_seeds()
self._load_json_config(mode)
self._apply_defaults()
# ── 读取 ──
def get(self, key: str, default=None):
"""获取配置值。优先级:运行时 Set > 环境变量 > JSON 配置 > 默认值。"""
def get_path(self, key: str) -> str:
"""获取路径类配置(自动展开 ~、转绝对路径)。"""
# ── 写入 ──
def set(self, key: str, value):
"""运行时修改配置(持久化到 JSON + 通知订阅者)。"""
# ── 热更新 ──
def reload(self):
"""重新加载 JSON 配置(保留运行时 Set 的覆盖)。"""
def on_change(self, callback):
"""注册配置变更回调。"""
```
#### 1.2 统一改造点
| 当前位置 | 改造方式 |
|---------|---------|
| `config/__init__.py:_load_dotenv()` | 替换为 `config_manager.initialize()` |
| `config/auth.py:_dotenv_cache()` | 改为 `config_manager.get("AGENT_ADMIN_USERNAME")` |
| `config/model_profiles.py:_env_optional()` | 改为 `config_manager.get(...)`,移除 .env 回退 |
| `utils/api_client.py:_resolve_env_value()` | 同上 |
| `server/chat_flow_helpers.py:_env_optional()` | 同上 |
| `modules/balance_client.py:_read_dotenv()` | 同上 |
| `config/terminal.py` | 常量改为从 `config_manager` 获取 |
| `config/server.py` | 同上(端口等可保留环境变量覆盖,但统一入口) |
#### 1.3 兼容性策略
```python
# config/__init__.py 保持向后兼容
from config.manager import config_manager
# 旧的 from config import DATA_DIR 仍然可用(通过模块级 __getattr__ 代理)
def __getattr__(name):
return config_manager.get(name)
```
#### 1.4 配置文件格式
```jsonc
// ~/.astrion/astrion/settings.json唯一配置不分 host/web
{
"terminal": {
"sandbox_mode": "host", // "host" | "web"
"execution_mode_default": "sandbox",
"direct_ttl_seconds": 600
},
"server": {
"port": 8091,
"host": "127.0.0.1"
},
"paths": {
"project_path": null // null = ./project相对于仓库根目录
},
"models": {
"default_model_key": null
},
"flags": {
"api_dump_enabled": false
}
}
```
路径解析规则:
- `data_root` 未设置时,默认 `~/.astrion/astrion`
- `data_root` 设置后host/web 子目录在其下创建
- 模式子目录:`{data_root}/{sandbox_mode}/`,如 `~/.astrion/astrion/host/data/`
#### 1.5 验证标准
- [ ] 删除项目根目录 `.env` 后,程序仍能正常启动(使用默认值)
- [ ] 所有现有功能不受影响
- [ ] `grep -rn 'parents\[1\].*\.env'` 结果中只剩 `config/manager.py``scripts/setup.py`
---
### 阶段 2运行时配置修改预计改动文件数~10
**目标**:管理后台 UI 可修改配置,无需重启。
#### 2.1 ConfigManager 增强
```python
class ConfigManager:
def set(self, key: str, value):
"""运行时修改配置"""
self._data[key] = value
self._persist() # 写回 JSON
self._notify(key, value) # 通知订阅者
def subscribe(self, key: str, callback):
"""订阅特定配置项的变更"""
def get_all(self) -> dict:
"""获取完整配置(供管理后台展示)"""
```
#### 2.2 前端管理后台新增"系统设置"页面
```
/ 管理后台
├── 用户管理
├── 模型配置 (已有)
├── 系统设置 (新增)
│ ├── 运行模式host/web
│ ├── 数据目录
│ ├── Web 服务(端口/地址)
│ ├── 终端沙箱配置
│ └── 管理员账户
└── ...
```
#### 2.3 不可运行时修改的配置
以下配置修改后需要手动重启才能生效,管理后台应有明确提示:
| 配置项 | 原因 |
|--------|------|
| `server.port` | Flask/SocketIO 已在监听端口 |
| `server.host` | 同上 |
| `terminal.sandbox_mode` | 影响终端管理器初始化 |
| `data_root` | 元配置,启动后数据目录已锁定 |
#### 2.4 验证标准
- [ ] 在管理后台修改模型配置后,新对话立即生效(无需重启)
- [ ] 修改数据目录路径后,新数据写入新位置
- [ ] 修改 Web 端口时,前端提示"需要重启"
---
### 阶段 3分层配置 + 项目级覆盖(预计改动文件数:~6
**目标**:支持全局配置 + 项目级配置 + 环境变量覆盖。
#### 3.1 配置优先级
```
优先级(高→低):
1. 运行时 Setconfig_manager.set(),内存中)
2. 环境变量覆盖os.environ
3. 项目级配置(<workspace>/.astrion/config.json
4. 全局配置(~/.astrion/astrion/<mode>/config/settings.json
5. 默认值(硬编码)
```
#### 3.2 变量引用语法
```jsonc
// 配置文件中支持变量引用
{
"model": {
"base_url": "{env:AGENT_API_BASE_URL}",
"api_key": "{env:AGENT_API_KEY}"
},
"prompt": {
"custom": "{file:~/my_prompt.txt}"
}
}
```
- `{env:VAR}` — 引用环境变量
- `{file:path}` — 引用文件内容(`~/` 展开为用户主目录)
#### 3.3 项目级配置
```python
# 当用户选择了工作区后,自动加载项目级配置
config_manager.load_project_config(workspace_path)
# → 读取 <workspace_path>/.astrion/config.json
# → 浅层覆盖全局配置
```
#### 3.4 验证标准
- [ ] 不同工作区可以有不同配置(如不同默认模型)
- [ ] `{env:VAR}` 语法正常工作
- [ ] 配置文件缺失时回退到上一级,不报错
---
### 阶段 4清理历史遗留预计改动文件数~5
**目标**:移除屎山代码,精简入口。
#### 4.1 清理清单
| 文件 | 操作 |
|------|------|
| `web_server.py` | 删除(已是兼容入口,无实际逻辑) |
| `main.py` | 删除 CLI 模式死代码(`setup_project_path`、`setup_run_mode` 等交互式部分),精简为纯 Web 启动 |
| `server/app.py` | 合并 `app_legacy.py` 的核心逻辑,"转发"改为"所有权" |
| `server/app_legacy.py` | 拆分为多个模块chat/、tasks/、conversation/ 已有蓝图,继续拆分) |
#### 4.2 标准化启动
```bash
# 唯一的启动方式
python -m server.app [--port 8091] [--host 127.0.0.1]
# 或保留 start.sh 作为便利脚本
./start.sh
```
移除 `setup.sh``start.sh` 的双脚本依赖,合并为一个入口。
#### 4.3 验证标准
- [ ] `python -m server.app` 正常工作
- [ ] 旧的 `python web_server.py` 报错提示使用新入口
- [ ] `python main.py` 仍可工作但输出 deprecation warning
---
## 五、文件变更清单总览
| 阶段 | 新增文件 | 修改文件 | 删除文件 |
|------|---------|---------|---------|
| 1 | `config/manager.py` | `config/__init__.py`, `config/auth.py`, `config/model_profiles.py`, `config/paths.py`, `config/terminal.py`, `config/server.py`, `utils/api_client.py`, `server/chat_flow_helpers.py`, `modules/balance_client.py` | — |
| 2 | `static/src/components/admin/SystemSettings.vue`(前端) | `config/manager.py`(增强), `server/` 新增 `/api/admin/settings` 路由 | — |
| 3 | — | `config/manager.py`(增强分层加载、变量解析) | — |
| 4 | — | `main.py`(精简), `server/app.py`(接管) | `web_server.py` |
---
## 六、风险控制
1. **每阶段独立可上线**:阶段 1 完成后即可合并,不影响阶段 2-4 的进度
2. **向后兼容**:阶段 1 保留 `from config import DATA_DIR` 语法,通过 `__getattr__` 代理
3. **回滚安全**:配置文件备份机制(`settings.json.bak_<timestamp>`
4. **渐进式清理**:先统一入口,再逐个模块迁移,不搞大爆炸重构
---
## 七、OpenCode vs 我们的对照速查表
| 概念 | OpenCode | 我们的目标 |
|------|---------|-----------|
| 全局配置路径 | `~/.config/opencode/config.json` | `{data_root}/settings.json`(默认 `~/.astrion/astrion/settings.json` |
| 项目配置 | `<cwd>/opencode.json(c)` 向上查找 | `<workspace>/.astrion/config.json` |
| 配置 Service | `Config.Service` (Effect-TS) | `ConfigManager` (Python 单例) |
| 变量引用 | `{env:VAR}` / `{file:path}` | 同 |
| 运行时修改 | `Config.update()` / `Config.updateGlobal()` | `config_manager.set()` |
| Flag 系统 | `Flag.OPENCODE_*` 集中定义 | `config/flags.py` 集中定义 |
| 分层加载 | 9 层 mergeDeep | 4 层优先级 |
| 路径标准 | XDG (`xdg-basedir`) | `{data_root}/` + `{mode}/`data_root 由 CLI/环境变量指定 |
| 初始化入口 | CLI handler → Runtime → Bootstrap → Config.get() | `server.app``config_manager.initialize()` → 服务启动 |