- 完整的知识库框架(P0-P5 路线图) - 12篇治理文档 - 工具脚本(kb-init.sh, kb-lint-fm.py, feishu-sync.py, kb-bot) - 7个 Claude 命令 - 示例项目和测试项目(wanniu-l1) - 契约文件(meta/kb-contract.yaml) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.1 KiB
Python
Executable File
115 lines
4.1 KiB
Python
Executable File
#!/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()
|