auto-sync: 2026-06-06 11:24:16

This commit is contained in:
cfdaily
2026-06-06 11:24:16 +08:00
parent 3c8f77d914
commit b8c4f9f5b8
+363
View File
@@ -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" / "..."))
```
不设置环境变量时行为与现在完全一致,零破坏性。