#!/usr/bin/env python3 """从 Markdown 标签知识库生成机器可读索引。 这个脚本只做确定性整理,不做语义判断: - 人继续维护 knowledge/ 下的中文 Markdown。 - 脚本把标签、intent、function、object、边界文件整理成 JSON manifest。 - Agent 和校验脚本优先读取 manifest,避免每次全量扫描知识库。 """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any SCRIPT_PATH = Path(__file__).resolve() SKILL_ROOT = SCRIPT_PATH.parents[1] KNOWLEDGE_ROOT = SKILL_ROOT / "knowledge" DEFAULT_OUTPUT = KNOWLEDGE_ROOT / "索引" / "label_manifest.json" def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def rel(path: Path) -> str: return path.relative_to(SKILL_ROOT).as_posix() def first_heading(text: str, fallback: str) -> str: match = re.search(r"^#\s+(.+?)\s*$", text, re.MULTILINE) return match.group(1).strip() if match else fallback def extract_section(text: str, heading: str) -> str: pattern = rf"^##\s+{re.escape(heading)}\s*$" match = re.search(pattern, text, re.MULTILINE) if not match: return "" start = match.end() next_match = re.search(r"^##\s+", text[start:], re.MULTILINE) end = start + next_match.start() if next_match else len(text) return text[start:end].strip() def extract_bullet_value(text: str, label: str) -> str: match = re.search(rf"^-\s*{re.escape(label)}[::]\s*(.+?)\s*$", text, re.MULTILINE) return match.group(1).strip() if match else "" def parse_markdown_table(text: str) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] table_lines = [line.strip() for line in text.splitlines() if line.strip().startswith("|")] if len(table_lines) < 2: return rows header = [cell.strip() for cell in table_lines[0].strip("|").split("|")] for line in table_lines[2:]: cells = [cell.strip() for cell in line.strip("|").split("|")] if len(cells) != len(header): continue rows.append(dict(zip(header, cells))) return rows def parse_enum_values(description: str) -> list[str]: if "枚举" not in description: return [] desc = re.sub(r"[,。;;].*$", "", description.split("枚举", 1)[-1]) desc = desc.replace(":", ":") if ":" in desc: desc = desc.split(":", 1)[1] parts = re.split(r"[、,,/| 或 和]+", desc) values: list[str] = [] for part in parts: cleaned = part.strip().strip("`").strip() cleaned = re.sub(r"[((].*?[))]", "", cleaned).strip() if cleaned and re.fullmatch(r"[A-Za-z0-9_]+", cleaned): values.append(cleaned) return values def parse_label_cards() -> list[dict[str, Any]]: labels_root = KNOWLEDGE_ROOT / "标签" labels: list[dict[str, Any]] = [] if not labels_root.exists(): return labels for path in sorted(labels_root.rglob("*.md")): text = read_text(path) name = first_heading(text, path.stem) domain = path.parent.name label = { "name": name, "domain": domain, "path": rel(path), "old_tag": extract_bullet_value(text, "旧 tag 标签"), "agent_candidate": extract_bullet_value(text, "Agent 包装候选(不代表最终)"), "function_candidate": extract_bullet_value(text, "Function 输出候选"), "recommended_output_shape": extract_bullet_value(text, "推荐输出形态"), "scope": extract_section(text, "适用范围"), "examples": extract_section(text, "典型 Query"), "confusing_labels": extract_section(text, "易混淆标签"), "principles": extract_section(text, "划分原则"), } labels.append(label) return labels def parse_function_catalog() -> list[dict[str, Any]]: path = KNOWLEDGE_ROOT / "输出能力" / "函数目录.md" if not path.exists(): return [] text = read_text(path) functions: list[dict[str, Any]] = [] sections = list(re.finditer(r"^##\s+(.+?)\s*$", text, re.MULTILINE)) for index, match in enumerate(sections): name = match.group(1).strip() start = match.end() end = sections[index + 1].start() if index + 1 < len(sections) else len(text) body = text[start:end].strip() params: dict[str, dict[str, Any]] = {} param_block = "" if "参数:" in body: param_block = body.split("参数:", 1)[1] if "示例:" in param_block: param_block = param_block.split("示例:", 1)[0] for row in parse_markdown_table(param_block): param_name = row.get("参数", "").strip() description = row.get("说明", "").strip() if not param_name: continue params[param_name] = { "description": description, "enum_values": parse_enum_values(description), } functions.append( { "name": name, "path": rel(path), "params": params, "allow_unknown_params": False, "summary": re.sub(r"\s+", " ", extract_summary(body)).strip(), } ) return functions def parse_object_catalog() -> list[dict[str, Any]]: path = KNOWLEDGE_ROOT / "输出能力" / "对象目录.md" if not path.exists(): return [] text = read_text(path) objects: list[dict[str, Any]] = [] sections = list(re.finditer(r"^##\s+(.+?)\s*$", text, re.MULTILINE)) for index, match in enumerate(sections): name = match.group(1).strip() start = match.end() end = sections[index + 1].start() if index + 1 < len(sections) else len(text) body = text[start:end].strip() if name in {"VisionQA 辅助对象", "生成类辅助对象"}: for row in parse_markdown_table(body): object_name = row.get("对象", "").strip() if object_name: objects.append( { "name": object_name, "path": rel(path), "params": {}, "allow_unknown_params": True, "summary": row.get("说明", "").strip(), } ) continue params: dict[str, dict[str, Any]] = {} param_block = body.split("参数:", 1)[1] if "参数:" in body else "" if "示例:" in param_block: param_block = param_block.split("示例:", 1)[0] for row in parse_markdown_table(param_block): param_name = row.get("参数", "").strip() description = row.get("说明", "").strip() if not param_name: continue params[param_name] = { "description": description, "enum_values": parse_enum_values(description), } objects.append( { "name": name, "path": rel(path), "params": params, "allow_unknown_params": False, "summary": re.sub(r"\s+", " ", extract_summary(body)).strip(), } ) return objects def parse_intent_catalog() -> list[dict[str, Any]]: path = KNOWLEDGE_ROOT / "输出能力" / "意图目录.md" if not path.exists(): return [] text = read_text(path) intents: list[dict[str, Any]] = [] for row in parse_markdown_table(text): name = row.get("intent", "").replace("\\|", "|").strip() if not name: continue intents.append( { "name": name, "agent": row.get("Agent", "").strip(), "instruction_type": row.get("指令类型", "").strip(), "scope": row.get("简要范围", "").strip(), "path": rel(path), } ) return intents def extract_summary(body: str) -> str: match = re.search(r"功能范围[::]\s*(.+?)(?:\n\n|$)", body, re.DOTALL) return match.group(1).strip() if match else "" def parse_boundary_files() -> list[dict[str, str]]: boundary_root = KNOWLEDGE_ROOT / "边界" if not boundary_root.exists(): return [] files: list[dict[str, str]] = [] for path in sorted(boundary_root.rglob("*.md")): if path.name == "README.md": continue text = read_text(path) parts = path.relative_to(boundary_root).parts category = parts[0] if len(parts) > 1 else "root" files.append({"name": first_heading(text, path.stem), "path": rel(path), "category": category}) return files def parse_dimension_files() -> list[dict[str, str]]: root = KNOWLEDGE_ROOT / "判断维度" if not root.exists(): return [] files: list[dict[str, str]] = [] for path in sorted(root.rglob("*.md")): if path == root / "README.md": continue text = read_text(path) item = {"name": first_heading(text, path.stem), "path": rel(path)} parent_dimension = extract_bullet_value(text, "父维度") output_field = extract_bullet_value(text, "输出字段") if parent_dimension: item["parent_dimension"] = parent_dimension if output_field: item["output_field"] = output_field files.append(item) return files def build_manifest() -> dict[str, Any]: labels = parse_label_cards() functions = parse_function_catalog() objects = parse_object_catalog() intents = parse_intent_catalog() old_tags = sorted({item["old_tag"] for item in labels if item.get("old_tag") and item["old_tag"] != "待确认。"}) label_names = sorted({item["name"] for item in labels}) agent_tags = sorted(set(old_tags) | set(label_names)) return { "schema_version": 1, "skill": "label-master", "display_name": "标签大师", "description": "标签大师的中控标签知识库机器索引;由 scripts/build_label_manifest.py 从 Markdown 生成。", "counts": { "labels": len(labels), "agent_tags": len(agent_tags), "functions": len(functions), "objects": len(objects), "intents": len(intents), }, "agent_tags": agent_tags, "labels": labels, "functions": functions, "objects": objects, "intents": intents, "boundaries": parse_boundary_files(), "dimensions": parse_dimension_files(), } def write_manifest(path: Path, manifest: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) text = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n" path.write_text(text, encoding="utf-8") def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="生成 label-master 标签大师机器索引") parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="输出 JSON 路径") parser.add_argument("--check", action="store_true", help="只检查输出文件是否为最新,不写入") args = parser.parse_args(argv) output = Path(args.output) if not output.is_absolute(): output = SKILL_ROOT / output manifest = build_manifest() new_text = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n" if args.check: if not output.exists(): print(f"ERROR: manifest 不存在:{output}", file=sys.stderr) return 1 old_text = output.read_text(encoding="utf-8") if old_text != new_text: print(f"ERROR: manifest 不是最新:{output}", file=sys.stderr) return 1 print(f"OK: manifest 已是最新:{output}") return 0 output.parent.mkdir(parents=True, exist_ok=True) output.write_text(new_text, encoding="utf-8") print(f"Wrote {output}") print(json.dumps(manifest["counts"], ensure_ascii=False, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())