Files
sanguo_moziplus_v2/docs/research/path-hardcode-research.md
T
2026-06-06 11:24:16 +08:00

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