#!/usr/bin/env python3 """ capability_doctor.py — 能力体检(一条命令验证「清单说的 ready 是不是真能跑」) 背景:capabilities.yaml 是能力唯一真相源,但清单可能与实际脱节 (实测曾发现:frame_chain 代码早已实现却报 missing; lipsync 实际用百炼却登记成 fal)。 本工具做静态体检,把「名不副实」的能力揪出来。 用法: python capability_doctor.py # 全部能力体检 python capability_doctor.py --capability lipsync python capability_doctor.py --json # 机器可读(供 Dashboard) python capability_doctor.py --only-issues # 只看有问题的 检查项(全部零成本,不调用任何付费接口): 1. impl 文件是否存在(清单 impl 字段指向的代码) 2. provider 的 api_key 是否已配置(api_key_ref → config.yaml + local.yaml) 3. 状态一致性:status=ready 但没有任何可用 provider → ✗ 名不副实 4. 结构完整性:是否声明了 providers / status 退出码:0 = 无问题;1 = 存在 ⚠️ / ✗ """ import argparse import json import re import sys from pathlib import Path # ── 定位 ── _SCRIPTS = Path(__file__).resolve().parent _AVE = _SCRIPTS.parent # 05_tools/09_ave if str(_SCRIPTS) not in sys.path: sys.path.insert(0, str(_SCRIPTS)) try: import yaml except ImportError: print("需要 pyyaml:pip install pyyaml") sys.exit(2) CAP_FILE = _AVE / "config" / "capabilities.yaml" PLACEHOLDERS = {"", "sk-xxx", "xxx", "your_key", "your_openai_api_key", "omlx", "changeme", "todo"} def load_caps() -> list: d = yaml.safe_load(CAP_FILE.read_text(encoding="utf-8")) or {} return d.get("capabilities") or [] def load_config_merged() -> dict: """合并 config.yaml(模板)与 local.yaml(本机真实密钥,优先级更高)""" merged: dict = {} def _merge(src: dict): for k, v in (src or {}).items(): if isinstance(v, dict) and isinstance(merged.get(k), dict): merged[k].update(v) else: merged[k] = v tmpl = _AVE / "config.yaml" if tmpl.exists(): try: _merge(yaml.safe_load(tmpl.read_text(encoding="utf-8")) or {}) except Exception: pass local = Path.home() / "workbuddy-agent-os/agent-local/tools/ave/config/local.yaml" if local.exists(): try: _merge(yaml.safe_load(local.read_text(encoding="utf-8")) or {}) except Exception: pass return merged def _get_by_path(cfg: dict, dotted: str): cur = cfg for part in (dotted or "").split("."): if not isinstance(cur, dict) or part not in cur: return None cur = cur[part] return cur def _key_ok(val) -> bool: if val is None: return False s = str(val).strip() return bool(s) and s.lower() not in PLACEHOLDERS and not s.startswith("${") def _impl_targets(impl: str) -> list: """从 impl 描述提取可核对的路径(文件或目录都可) · 'composer/lipsync.py(lipsync_videoretalk)' → ['composer/lipsync.py'] · 'music_selector/' → ['music_selector'] · 'assets_manager/ + character_generator/' → ['assets_manager', 'character_generator'] """ if not impl: return [] files = re.findall(r"([\w./-]+\.py)", impl) if files: return files dirs = re.findall(r"([\w][\w./-]*)/", impl) seen, out = set(), [] for d in dirs: d = d.rstrip('/') if d and d not in seen: seen.add(d) out.append(d) return out or [impl.strip()] def check_capability(cap: dict, cfg: dict) -> dict: """单个能力的静态体检结果""" cid = cap.get("id", "?") status = (cap.get("status") or "").strip() or "(未声明)" providers = cap.get("providers") or [] issues: list[str] = [] warns: list[str] = [] # ① impl 实现(文件或目录) impl = cap.get("impl") or "" targets = _impl_targets(impl) impl_ok = None if targets: found = [] missing = [] for rel in targets: cands = [_SCRIPTS / rel, _SCRIPTS / "capabilities" / rel, _AVE / rel] (found if any(c.exists() for c in cands) else missing).append(rel) impl_ok = bool(found) # planned / missing 能力允许尚无实现;ready/partial 缺实现才是问题 if missing and status in ("ready", "partial"): warns.append(f"impl 未找到: {', '.join(missing[:3])}") elif status in ("ready", "partial"): warns.append("未声明 impl(无法核对实现)") # ② provider 的 key 配置情况 prov_ok: list[str] = [] prov_missing: list[str] = [] for p in providers: ref = p.get("api_key_ref") or "" pid = p.get("id") or "?" if not ref: prov_ok.append(f"{pid}(无需key)") # 本地实现(ffmpeg 等) continue val = _get_by_path(cfg, ref) if _key_ok(val): prov_ok.append(pid) else: prov_missing.append(f"{pid}({ref})") # ③ 状态一致性 if status == "ready": if providers and not prov_ok: issues.append(f"status=ready 但没有任何可用 provider(缺: {', '.join(prov_missing)})") if impl_ok is False: issues.append("status=ready 但 impl 文件不存在") elif status == "partial": if providers and not prov_ok: warns.append(f"partial 且全部 provider 不可用(缺: {', '.join(prov_missing)})") elif status in ("planned", "missing", ""): pass # ④ 结构完整性 if not providers: warns.append("未声明 providers") verdict = "✗" if issues else ("⚠️" if warns else "✓") return { "id": cid, "name": cap.get("name", ""), "status": status, "impl": impl, "impl_ok": impl_ok, "providers_ok": prov_ok, "providers_missing": prov_missing, "issues": issues, "warns": warns, "verdict": verdict, } def main(): ap = argparse.ArgumentParser(description="能力体检(静态,零成本)") ap.add_argument("--capability", help="只查某个能力 id") ap.add_argument("--json", action="store_true", help="输出 JSON") ap.add_argument("--only-issues", action="store_true", help="只显示 ⚠️/✗") args = ap.parse_args() caps = load_caps() cfg = load_config_merged() if args.capability: caps = [c for c in caps if c.get("id") == args.capability] if not caps: print(f"未找到能力: {args.capability}") return 2 results = [check_capability(c, cfg) for c in caps] if args.only_issues: results = [r for r in results if r["verdict"] != "✓"] if args.json: print(json.dumps({"total": len(results), "results": results}, ensure_ascii=False, indent=1)) else: bad = [r for r in results if r["verdict"] == "✗"] warn = [r for r in results if r["verdict"] == "⚠️"] good = [r for r in results if r["verdict"] == "✓"] print(f"\n能力体检(共 {len(results)} 项) ✓ {len(good)} · ⚠️ {len(warn)} · ✗ {len(bad)}\n") for r in results: if args.only_issues and r["verdict"] == "✓": continue line = f" {r['verdict']} {r['id']:26s} [{r['status']:8s}]" if r["providers_ok"]: line += f" ✓{','.join(r['providers_ok'])[:36]}" if r["providers_missing"]: line += f" ✗{','.join(r['providers_missing'])[:36]}" print(line) for m in r["issues"]: print(f" ✗ {m}") for m in r["warns"]: print(f" · {m}") print() return 1 if any(r["verdict"] == "✗" for r in results) else 0 if __name__ == "__main__": sys.exit(main())