# 配置系统改造计划

> 版本：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：分层配置优先级**

```
优先级（高→低）：
  运行时 Set（config_manager.set()）
→ 环境变量覆盖
→ 项目级配置（<workspace>/.astrion/config.json）
→ 全局配置（~/.astrion/astrion/settings.json）
→ 默认值
```

**模式 3：统一 ConfigManager 单例**

```
ConfigManager
  ├── load()           从 JSON 文件加载
  ├── get(key)         获取配置值
  ├── set(key, value)  运行时修改 + 持久化
  ├── reload()         重新加载（配置热更新）
  ├── subscribe()      变更监听
  └── _resolve_path()  路径解析
```

**模式 4：Flag 系统**

```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. 运行时 Set（config_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()` → 服务启动 |
