#!/usr/bin/env python3 """智能体/技能登记脚本——把 Git 侧的 agent/skill 元数据影子进库,供看板只读。 落库设计 §2.10 的 A 方案: - Git 侧(.claude/agents/*.md、.claude/skills/*/SKILL.md)仍是配置权威; - 本脚本抽取 frontmatter(name/description/model/tools)+ 尽力抽取涉及的表名, upsert 进 example_agent_role / example_skill; - 看板只读登记表、不读 Git。幂等可重跑(upsert),配置变更后重跑即同步。 跑法(仓库根目录):.venv/bin/python .claude/skills/db/scripts/sync_agent_registry.py """ import json import re from pathlib import Path from db import connect # 复用锁死的 DSN(与 db.py 同目录,脚本目录自动在 sys.path) ROOT = Path(__file__).resolve().parents[4] # .claude/skills/db/scripts → 仓库根 AGENTS_DIR = ROOT / ".claude" / "agents" SKILLS_DIR = ROOT / ".claude" / "skills" TABLE_RE = re.compile(r"\b(muse_[a-z_]+|example_[a-z_]+)\b") def parse_frontmatter(text: str) -> dict: """解析开头 --- 围栏内的 frontmatter 为 key->字符串(每行按第一个冒号切,值可含冒号/逗号)。""" lines = text.splitlines() if not lines or lines[0].strip() != "---": return {} fm = {} for line in lines[1:]: if line.strip() == "---": break if ":" in line: k, _, v = line.partition(":") fm[k.strip()] = v.strip() return fm def sync_roles(conn) -> int: n = 0 for md in sorted(AGENTS_DIR.glob("*.md")): fm = parse_frontmatter(md.read_text(encoding="utf-8")) role = fm.get("name") or md.stem desc = fm.get("description", "") display = desc.split("——", 1)[0].strip() or None # "网文写手——…" 取破折号前作显示名 tools_raw = fm.get("tools") tools = json.dumps([t.strip() for t in tools_raw.split(",") if t.strip()], ensure_ascii=False) if tools_raw else None conn.execute( """INSERT INTO example_agent_role (role, display_name, model, tools, responsibility, source_ref, synced_at, creator, updater) VALUES (%s,%s,%s,%s::jsonb,%s,%s,CURRENT_TIMESTAMP,'sync_agent_registry','sync_agent_registry') ON CONFLICT (tenant_id, role) DO UPDATE SET display_name=EXCLUDED.display_name, model=EXCLUDED.model, tools=EXCLUDED.tools, responsibility=EXCLUDED.responsibility, source_ref=EXCLUDED.source_ref, synced_at=CURRENT_TIMESTAMP, updater='sync_agent_registry'""", (role, display, fm.get("model"), tools, desc or None, str(md.relative_to(ROOT)))) n += 1 return n def sync_skills(conn) -> int: n = 0 for sk in sorted(SKILLS_DIR.glob("*/SKILL.md")): text = sk.read_text(encoding="utf-8") fm = parse_frontmatter(text) name = fm.get("name") or sk.parent.name tables = sorted(set(TABLE_RE.findall(text))) # 尽力抽取涉及的表名(不区分读写,待人工核) model_used = None if "MiniMax" in text: model_used = "MiniMax-M3" elif re.search(r"\bopus\b", text): model_used = "opus" conn.execute( """INSERT INTO example_skill (skill_name, purpose, reads, model_used, source_ref, synced_at, creator, updater) VALUES (%s,%s,%s::jsonb,%s,%s,CURRENT_TIMESTAMP,'sync_agent_registry','sync_agent_registry') ON CONFLICT (tenant_id, skill_name) DO UPDATE SET purpose=EXCLUDED.purpose, reads=EXCLUDED.reads, model_used=EXCLUDED.model_used, source_ref=EXCLUDED.source_ref, synced_at=CURRENT_TIMESTAMP, updater='sync_agent_registry'""", (name, fm.get("description"), json.dumps(tables, ensure_ascii=False) if tables else None, model_used, str(sk.relative_to(ROOT)))) n += 1 return n def main(): with connect() as conn: nr = sync_roles(conn) ns = sync_skills(conn) conn.commit() print(f"已登记 {nr} 个角色、{ns} 个技能 → example_agent_role / example_skill") if __name__ == "__main__": main()