Initial commit: company-kb framework
- 完整的知识库框架(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>
This commit is contained in:
114
tools/feishu-render.py
Executable file
114
tools/feishu-render.py
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user