#!/usr/bin/env python3 """feishu-render.py —— 知识库 → 飞书呈现(单向 md→飞书,不回写) 两种载体: --bitable 扫 projects 下页的 frontmatter,每页一条记录 upsert 到多维表格(结构型呈现,可筛选/看板) --docs 代号 把某项目的 _index + 决策同步为飞书云文档(叙事型呈现) 飞书只读呈现,写路径仍是 Obsidian/Git,防双写冲突。 凭据由 lark-cli 自管,不入库。字段严格对齐 meta/kb-contract.yaml。 用法: python3 tools/feishu-render.py --bitable [--dry-run] python3 tools/feishu-render.py --docs <代号> [--dry-run] 注意:本文件是接口骨架。bitable app/table 的创建与 record 写入留 TODO, 接入时填 _bitable_upsert();docs 同步复用 lark-doc-manager。 """ import sys import os import re import glob import argparse try: import yaml except ImportError: print("ERROR: 需要 PyYAML。请运行: pip install pyyaml", file=sys.stderr) sys.exit(2) ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) FM_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL) # 呈现给飞书多维表格的字段(对齐 kb-contract.yaml) BITABLE_FIELDS = ["type", "title", "status", "source", "created", "source_link"] def parse_fm(fp): text = open(fp, encoding="utf-8").read() m = FM_RE.match(text) if not m: return None try: data = yaml.safe_load(m.group(1)) return data if isinstance(data, dict) else None except yaml.YAMLError: return None def collect_pages(): """扫 projects 下所有知识页,返回 (相对路径, frontmatter) 列表。""" rows = [] for fp in glob.glob(os.path.join(ROOT, "projects", "**", "*.md"), recursive=True): fm = parse_fm(fp) if fm and fm.get("type"): rel = os.path.relpath(fp, ROOT) project = rel.split(os.sep)[1] if os.sep in rel else "" rows.append((rel, project, fm)) return rows def _bitable_upsert(rows, dry_run): """接入时实现:通用 api 写多维表格记录。 唯一键 = 页相对路径(防重复插入,做 upsert)。 命令参考: lark-cli api POST /open-apis/bitable/v1/apps/{app}/tables/{table}/records \ --data '{"fields": {...}}' """ print(f"[bitable] 采集到 {len(rows)} 页知识:") for rel, project, fm in rows: fields = {k: fm.get(k, "") for k in BITABLE_FIELDS} fields["项目"] = project fields["页路径"] = rel if dry_run: print(f" - {project}/{fm.get('type')}: {fm.get('title', rel)}") else: # TODO: lark-cli api POST .../records(含 app_token/table_id 配置) pass if not dry_run: raise NotImplementedError( "Bitable 写入尚未接入(P2-d)。需先创建多维表并配置 app_token/table_id。" "当前 --dry-run 可预览将同步的记录。" ) def _docs_sync(code, dry_run): idx = os.path.join(ROOT, "projects", code, "_index.md") if not os.path.isfile(idx): print(f"ERROR: 找不到 projects/{code}/_index.md", file=sys.stderr) sys.exit(1) print(f"[docs] 将同步 projects/{code}/ 的 _index + 决策为飞书云文档") if dry_run: print(" (dry-run:实际同步走 lark-doc-manager / lark-cli docs +create/+update)") return raise NotImplementedError( "云文档同步走 lark-doc-manager skill(已激活)。接入时在此调用其创建/更新流程。" ) def main(): ap = argparse.ArgumentParser(description="知识库 → 飞书呈现") g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--bitable", action="store_true", help="同步全库到多维表格") g.add_argument("--docs", metavar="代号", help="同步某项目为云文档") ap.add_argument("--dry-run", action="store_true", help="只预览不写飞书") args = ap.parse_args() if args.bitable: _bitable_upsert(collect_pages(), args.dry_run) else: _docs_sync(args.docs, args.dry_run) if __name__ == "__main__": main()