Files
2026-06-06 11:24:16 +08:00

13 KiB
Raw Permalink Blame History

路径硬编码调研报告

调研时间: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.yamldata_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 FallbackPath.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.py1 处)


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" FallbackPath.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.py2 处 + 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" FallbackPath.home() / ".openclaw"

影响范围

  • 直接引用:1 处(blackboard_routes.py:161
  • 调用方:create_task API 路由
  • 测试覆盖:无直接测试(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.py1 处 + 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_HOMESANGUO_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_PATHbootstrap.py)模式一致
  • 模块加载时确定,性能无开销
  • 改动最小,每个文件加 1 行常量 + 替换路径

方案 B(过度设计):集中到 utils.py

OPENCLAW_HOMESANGUO_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_HOMESANGUO_PROJECTS_DIR 各添加一个单元测试,验证环境变量覆盖生效。

Fallback 逻辑

所有 4 处统一使用:

os.environ.get("VAR_NAME", str(Path.home() / ".openclaw" / "..."))

不设置环境变量时行为与现在完全一致,零破坏性。