# 路径硬编码调研报告

> 调研时间：2026-06-06
> 源码位置：`~/.sanguo_projects/sanguo_moziplus_v2/src/`

## 调研结论

### 4 处硬编码精确位置

| # | 文件 | 行号 | 硬编码路径 | 建议环境变量 |
|---|------|------|-----------|-------------|
| 1 | `blackboard/registry.py` | 264 | `Path.home() / ".openclaw" / "sanguo_projects"` | `SANGUO_PROJECTS_DIR` |
| 2 | `daemon/spawner.py` | 1177 | `Path.home() / ".openclaw" / "agents" / agent_id / "sessions" / "sessions.json"` | `OPENCLAW_HOME` |
| 3 | `daemon/spawner.py` | 1261 | 同上 | `OPENCLAW_HOME` |
| 4 | `api/blackboard_routes.py` | 161 | `Path.home() / ".openclaw" / "openclaw.json"` | `OPENCLAW_HOME` |

### 修复优先级和风险评估

| 优先级 | 位置 | 风险 | 理由 |
|--------|------|------|------|
| **P1** | registry.py:264 | 低 | 只需改 1 行，方法已支持 `scan_dir` 参数，fallback 简单 |
| **P1** | blackboard_routes.py:161 | 低 | 只需改 1 行，纯读取操作 |
| **P2** | spawner.py:1177,1261 | 中 | 2 处相同路径，且有测试覆盖（通过 mock），需确保 mock 不依赖硬编码路径 |

**总体风险：低**。4 处修改均为简单路径替换，不影响逻辑，且设计文档已规划好环境变量名。

---

## 现有环境变量模式

源码中已有的环境变量用法：

| 环境变量 | 位置 | 用途 | 默认值 |
|---------|------|------|--------|
| `BLACKBOARD_ROOT` | `utils.py:19` | 项目数据根目录 | `{src}/../data/` 或 `config/default.yaml` 的 `data_root` |
| `MOZI_SKILL_PATH` | `daemon/bootstrap.py:41` | 技能文件搜索路径 | `~/.sanguo_projects/sanguo_mozi/skills` |

### 命名规范

```python
# utils.py — 优先级链：环境变量 > config 文件 > 默认值
root = os.environ.get("BLACKBOARD_ROOT")

# bootstrap.py — os.environ.get + fallback
SKILL_BASE_PATH = os.environ.get(
    "MOZI_SKILL_PATH",
    os.path.expanduser("~/.sanguo_projects/sanguo_mozi/skills")
)
```

**规范总结**：
- 全大写蛇形命名
- 使用 `os.environ.get("VAR_NAME", fallback)` 模式
- 测试中通过 `os.environ["VAR"] = ...` 设置，`del os.environ["VAR"]` 清理
- 已有 `BLACKBOARD_ROOT` 用于数据根目录，不应复用（语义不同）

---

## 逐项分析

### 1. registry.py:264 — SANGUO_PROJECTS_DIR

**精确位置**：`src/blackboard/registry.py` 第 264 行

**上下文代码**（方法：`discover_sanguo_projects`，行 262-296）：

```python
def discover_sanguo_projects(self, scan_dir: Optional[Path] = None) -> List[str]:
    """扫描 sanguo_projects 开发目录，自动注册正式项目"""
    scan_dir = scan_dir or Path.home() / ".openclaw" / "sanguo_projects"  # ← 第264行
    discovered = []

    if not scan_dir.exists():
        return discovered

    conn = self._connect()
    try:
        for child in sorted(scan_dir.iterdir()):
            if not child.is_dir():
                continue
            if not child.name.startswith("sanguo_"):
                continue
            # ... 自动注册到 blackboard.db
```

**使用场景**：
- **启动时**自动扫描 `~/.openclaw/sanguo_projects/` 目录，将 `sanguo_*` 子目录注册为正式项目
- 被 `main.py:118` 在启动时调用：`registry.discover_sanguo_projects()`
- 仅扫描一次，不涉及运行时高频访问

**默认值**：`~/.openclaw/sanguo_projects`
**Fallback**：`Path.home() / ".openclaw" / "sanguo_projects"`（保持现有行为）

**影响范围**：
- 直接引用：1 处（registry.py:264）
- 调用方：1 处（main.py:118），但调用时不传参数，走默认路径
- 数据库中注册的 `source="sanguo_projects_scan"` 标记会被使用（前端判断项目是否可删除）
- **测试覆盖**：无直接测试（`test_registry.py` 存在但需确认是否覆盖此方法）

**修复方案**：
```python
scan_dir = scan_dir or Path(os.environ.get(
    "SANGUO_PROJECTS_DIR",
    str(Path.home() / ".openclaw" / "sanguo_projects")
))
```

**需改文件**：`src/blackboard/registry.py`（1 处）

---

### 2. spawner.py:1177 — OPENCLAW_HOME（第一处）

**精确位置**：`src/daemon/spawner.py` 第 1177 行

**上下文代码**（静态方法：`_revive_session`，行 1175-1200）：

```python
@staticmethod
def _revive_session(agent_id: str) -> bool:
    """假死复活术:修改 sessions.json status 从 running 改为 idle"""
    sessions_path = Path.home() / ".openclaw" / "agents" / agent_id / "sessions" / "sessions.json"  # ← 第1177行
    if not sessions_path.exists():
        return False
    try:
        with open(sessions_path) as f:
            sessions = json.load(f)
        main_key = f"agent:{agent_id}:main"
        main_session = sessions.get(main_key, {})
        if main_session.get("status") != "running":
            return False
        main_session["status"] = "idle"
        sessions[main_key] = main_session
        with open(sessions_path, "w") as f:
            json.dump(sessions, f, indent=2)
        logger.info("Revived %s: sessions.json status changed running→idle", agent_id)
        # 清理残留 lock 文件
        sf = main_session.get("sessionFile", "")
        if sf:
            lock_path = Path(sf + ".lock")
            if lock_path.exists():
                lock_path.unlink()
    except Exception:
        return False
    return True
```

**使用场景**：
- **Spawner Phase 0 健康检查**时，发现 agent 会话状态为 `running` 但实际已死，将其重置为 `idle`
- 运维级操作，非高频调用

---

### 3. spawner.py:1261 — OPENCLAW_HOME（第二处）

**精确位置**：`src/daemon/spawner.py` 第 1261 行

**上下文代码**（静态方法：`_check_session_state`，行 1254-1290）：

```python
@staticmethod
def _check_session_state(agent_id: str) -> dict:
    """检查 sessions.json 和 lock 状态"""
    result = {"status": "unknown", "lock_pid": None, "lock_pid_alive": False, "recent_compact": False}
    sessions_path = Path.home() / ".openclaw" / "agents" / agent_id / "sessions" / "sessions.json"  # ← 第1261行
    if not sessions_path.exists():
        return result
    try:
        with open(sessions_path) as f:
            sessions = json.load(f)
        main_key = f"agent:{agent_id}:main"
        main_session = sessions.get(main_key, {})
        result["status"] = main_session.get("status", "unknown")
        # 检查 lock ...
```

**使用场景**：
- **每个 tick** 检查所有 agent 的会话状态（status + lock）
- 高频调用（默认每 30 秒一次 tick）
- 返回值被 `_revive_session` 和其他调度逻辑使用

**两处 spawner.py 共享相同的路径模式**：`Path.home() / ".openclaw" / "agents" / agent_id / "sessions" / "sessions.json"`

**默认值**：`Path.home() / ".openclaw"`
**Fallback**：`Path.home() / ".openclaw"`

**影响范围**：
- 直接引用：2 处（spawner.py:1177, spawner.py:1261）
- 调用方：Spawner 主循环（`_phase0_health_check`）
- **测试覆盖**：有！`test_spawner.py` 大量 mock 了 `_check_session_state` 和 `_revive_session`，但这些 mock 直接替换了整个方法，**不依赖路径硬编码**，因此修复后测试无需改动

**修复方案**：
```python
# 抽取为辅助函数或模块级常量
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))

# 使用处
sessions_path = _OPENCLAW_HOME / "agents" / agent_id / "sessions" / "sessions.json"
```

**需改文件**：`src/daemon/spawner.py`（2 处 + 1 处常量定义）

---

### 4. blackboard_routes.py:161 — OPENCLAW_HOME

**精确位置**：`src/api/blackboard_routes.py` 第 161 行

**上下文代码**（函数：`_generate_title`，行 147-186）：

```python
async def _generate_title(description: str) -> str | None:
    """调用 LLM 生成简短标题"""
    if not description or len(description) < 5:
        return None
    try:
        from openai import OpenAI
        import json as _json
        base_url = "https://open.bigmodel.cn/api/paas/v4"
        api_key = ""
        model = "glm-4-flash"
        oc_cfg = Path.home() / ".openclaw" / "openclaw.json"  # ← 第161行
        if oc_cfg.exists():
            with open(oc_cfg) as f:
                cfg = _json.load(f)
            zhipu = cfg.get("models", {}).get("providers", {}).get("zhipu", {})
            if zhipu.get("baseUrl"):
                base_url = zhipu["baseUrl"]
            if zhipu.get("apiKey"):
                api_key = zhipu["apiKey"]
        if not api_key:
            return None  # 没配 API key 就跳过
        # ... 调用 OpenAI API 生成标题
```

**使用场景**：
- 创建任务时，如果未提供 title，调用 LLM 自动生成简短标题
- 读取 `~/.openclaw/openclaw.json` 获取 zhipu API 配置
- 非关键路径：即使文件不存在或读取失败，也会 graceful fallback（返回 None，使用默认标题）

**默认值**：`Path.home() / ".openclaw"`
**Fallback**：`Path.home() / ".openclaw"`

**影响范围**：
- 直接引用：1 处（blackboard_routes.py:161）
- 调用方：`create_task` API 路由
- **测试覆盖**：无直接测试（integration test 存在但未覆盖 `_generate_title`）

**修复方案**：
```python
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))
oc_cfg = _OPENCLAW_HOME / "openclaw.json"
```

**需改文件**：`src/api/blackboard_routes.py`（1 处 + 1 处常量定义）

---

## 额外发现

### 1. bootstrap.py:42 — 已使用环境变量模式（可作为参考）

```python
SKILL_BASE_PATH = os.environ.get(
    "MOZI_SKILL_PATH",
    os.path.expanduser("~/.sanguo_projects/sanguo_mozi/skills")
)
```

这是**类属性级别的环境变量**，但存在一个问题：它在类加载时就确定了值，运行时修改环境变量不会生效。建议 `_OPENCLAW_HOME` 等也采用相同方式（在模块加载时确定），保持一致性。

### 2. utils.py — BLACKBOARD_ROOT 已有完整优先级链

```python
def get_data_root() -> Path:
    root = os.environ.get("BLACKBOARD_ROOT")
    if root:
        return Path(root)
    # ... config/default.yaml → 相对路径 fallback
```

这是最完善的实现，有 3 级 fallback。本次 4 处硬编码不需要这么复杂，简单的 `os.environ.get(var, default)` 即可。

### 3. 无其他路径硬编码

除了已知的 4 处，源码中 `.openclaw` 路径的引用仅出现在：
- 前端代码（`frontend/dist/` 和 `frontend/src/`）— 无需修改
- 测试代码 — 使用 `BLACKBOARD_ROOT` 临时目录隔离
- 文档 — 描述性引用

### 4. 无 `OPENCLAW_HOME` 或 `SANGUO_PROJECTS_DIR` 的现有定义

源码中未定义这两个环境变量，不会冲突。

---

## 统一修复方案

### 环境变量设计

| 环境变量 | 默认值 | 影响文件 | 说明 |
|---------|--------|---------|------|
| `OPENCLAW_HOME` | `~/.openclaw` | spawner.py (×2), blackboard_routes.py (×1) | OpenClaw 配置根目录 |
| `SANGUO_PROJECTS_DIR` | `~/.openclaw/sanguo_projects` | registry.py (×1) | 三国项目开发目录 |

### 实现方式

**方案 A（推荐）：各文件内模块级常量**

```python
# spawner.py 顶部
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))

# 使用处
sessions_path = _OPENCLAW_HOME / "agents" / agent_id / "sessions" / "sessions.json"
```

```python
# blackboard_routes.py 顶部
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))

# 使用处
oc_cfg = _OPENCLAW_HOME / "openclaw.json"
```

```python
# registry.py 内方法
scan_dir = scan_dir or Path(os.environ.get(
    "SANGUO_PROJECTS_DIR",
    str(Path.home() / ".openclaw" / "sanguo_projects")
))
```

**优点**：
- 与 `MOZI_SKILL_PATH`（bootstrap.py）模式一致
- 模块加载时确定，性能无开销
- 改动最小，每个文件加 1 行常量 + 替换路径

**方案 B（过度设计）：集中到 utils.py**

将 `OPENCLAW_HOME` 和 `SANGUO_PROJECTS_DIR` 都定义在 `utils.py`，其他文件 import。不推荐，因为引入不必要的依赖关系。

### 需要改的文件清单

1. `src/blackboard/registry.py` — 第 264 行（1 处）
2. `src/daemon/spawner.py` — 顶部加常量 + 第 1177 行 + 第 1261 行（3 处）
3. `src/api/blackboard_routes.py` — 顶部加常量 + 第 161 行（2 处）

### 测试适配

- **test_spawner.py**：不需要改动。测试通过 lambda/mock 替换整个 `_check_session_state` 和 `_revive_session`，不依赖实际路径。
- **test_registry.py**：如需测试 `SANGUO_PROJECTS_DIR` 环境变量，可添加新测试用例，通过 `os.environ` 设置临时目录验证。
- **integration tests**：已通过 `BLACKBOARD_ROOT` 隔离，不受影响。
- **建议新增**：为 `OPENCLAW_HOME` 和 `SANGUO_PROJECTS_DIR` 各添加一个单元测试，验证环境变量覆盖生效。

### Fallback 逻辑

所有 4 处统一使用：
```python
os.environ.get("VAR_NAME", str(Path.home() / ".openclaw" / "..."))
```

不设置环境变量时行为与现在完全一致，零破坏性。
