# 路径硬编码调研报告 > 调研时间: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" / "...")) ``` 不设置环境变量时行为与现在完全一致,零破坏性。