13 KiB
路径硬编码调研报告
调研时间: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 |
命名规范
# 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):
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存在但需确认是否覆盖此方法)
修复方案:
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):
@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):
@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 直接替换了整个方法,不依赖路径硬编码,因此修复后测试无需改动
修复方案:
# 抽取为辅助函数或模块级常量
_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):
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_taskAPI 路由 - 测试覆盖:无直接测试(integration test 存在但未覆盖
_generate_title)
修复方案:
_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 — 已使用环境变量模式(可作为参考)
SKILL_BASE_PATH = os.environ.get(
"MOZI_SKILL_PATH",
os.path.expanduser("~/.sanguo_projects/sanguo_mozi/skills")
)
这是类属性级别的环境变量,但存在一个问题:它在类加载时就确定了值,运行时修改环境变量不会生效。建议 _OPENCLAW_HOME 等也采用相同方式(在模块加载时确定),保持一致性。
2. utils.py — BLACKBOARD_ROOT 已有完整优先级链
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(推荐):各文件内模块级常量
# spawner.py 顶部
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))
# 使用处
sessions_path = _OPENCLAW_HOME / "agents" / agent_id / "sessions" / "sessions.json"
# blackboard_routes.py 顶部
_OPENCLAW_HOME = Path(os.environ.get("OPENCLAW_HOME", str(Path.home() / ".openclaw")))
# 使用处
oc_cfg = _OPENCLAW_HOME / "openclaw.json"
# 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。不推荐,因为引入不必要的依赖关系。
需要改的文件清单
src/blackboard/registry.py— 第 264 行(1 处)src/daemon/spawner.py— 顶部加常量 + 第 1177 行 + 第 1261 行(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 处统一使用:
os.environ.get("VAR_NAME", str(Path.home() / ".openclaw" / "..."))
不设置环境变量时行为与现在完全一致,零破坏性。