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