From b8c4f9f5b8b05a3f7f7d24fa76a0684c08201f8e Mon Sep 17 00:00:00 2001 From: cfdaily Date: Sat, 6 Jun 2026 11:24:16 +0800 Subject: [PATCH] auto-sync: 2026-06-06 11:24:16 --- docs/research/path-hardcode-research.md | 363 ++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 docs/research/path-hardcode-research.md diff --git a/docs/research/path-hardcode-research.md b/docs/research/path-hardcode-research.md new file mode 100644 index 0000000..d32e477 --- /dev/null +++ b/docs/research/path-hardcode-research.md @@ -0,0 +1,363 @@ +# 路径硬编码调研报告 + +> 调研时间: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" / "...")) +``` + +不设置环境变量时行为与现在完全一致,零破坏性。