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:
yangqianqian
2026-07-16 18:24:39 +08:00
commit 84f7950589
60 changed files with 4410 additions and 0 deletions

131
tools/feishu-pull.py Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""feishu-pull.py —— 飞书半自动单向导出(飞书 → docs/
MVP 形态:半自动单向。飞书只作「文档来源」,不直接写 Git。
凭据走环境变量(绝不入库):
FEISHU_APP_ID, FEISHU_APP_SECRET
用法:
# 干跑(默认):只打印将要导出什么,不落盘
python3 tools/feishu-pull.py <代号> --doc <飞书文档token或url>
# 实际写入 projects/<代号>/docs/
python3 tools/feishu-pull.py <代号> --doc <token> --write
AI 自动补齐 frontmattertitle(H1回填) / ingested(今天) / source(飞书文档) / source_link。
去重:写前查 meta/ingest-manifest.json 的内容 hash。
注意:本文件是接口骨架。飞书 SDK 调用留 TODO接入时填 _fetch_doc()。
凭据缺失时优雅报错,不泄露。
"""
import sys
import os
import json
import hashlib
import argparse
import datetime
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MANIFEST = os.path.join(ROOT, "meta", "ingest-manifest.json")
def load_manifest():
if os.path.isfile(MANIFEST):
with open(MANIFEST, encoding="utf-8") as f:
return json.load(f)
return {"_comment": "", "entries": {}}
def save_manifest(m):
with open(MANIFEST, "w", encoding="utf-8") as f:
json.dump(m, f, ensure_ascii=False, indent=2)
def content_hash(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def _fetch_doc(token):
"""接入飞书时实现:用 tenant_access_token 拉文档,返回 (标题, markdown 正文, 原始链接)。
现为占位,避免误导:明确报未接入。"""
app_id = os.environ.get("FEISHU_APP_ID")
app_secret = os.environ.get("FEISHU_APP_SECRET")
if not app_id or not app_secret:
raise RuntimeError(
"缺凭据:请设置环境变量 FEISHU_APP_ID / FEISHU_APP_SECRET不要写进代码或 .env 入库)"
)
# TODO: 接入 lark-oapi SDK
# 1) POST /open-apis/auth/v3/tenant_access_token/internal
# 2) GET /open-apis/docx/v1/documents/{token}/raw_content
raise NotImplementedError(
"飞书 SDK 调用尚未接入P2 阶段实现)。当前仅提供 frontmatter 自动补齐与去重骨架。"
)
def build_page(title, body, source_link):
today = datetime.date.today().isoformat()
fm = (
"---\n"
"type: doc\n"
f'title: "{title}"\n'
"status: seed\n"
f"created: {today}\n"
f"ingested: {today}\n"
"source: 飞书文档\n"
f"source_link: {source_link}\n"
"tags: []\n"
"related: []\n"
"---\n\n"
f"# {title}\n\n"
f"{body}\n"
)
return fm
def slugify(title):
keep = "".join(c if c.isalnum() or c in " -_" else "" for c in title)
return "-".join(keep.split())[:60] or "untitled"
def main():
ap = argparse.ArgumentParser(description="飞书半自动单向导出")
ap.add_argument("code", help="项目代号(英文/拼音)")
ap.add_argument("--doc", required=True, help="飞书文档 token 或 URL")
ap.add_argument("--write", action="store_true", help="实际写盘(默认干跑)")
args = ap.parse_args()
outdir = os.path.join(ROOT, "projects", args.code, "docs")
try:
title, body, link = _fetch_doc(args.doc)
except (RuntimeError, NotImplementedError) as e:
print(f"[飞书导出未就绪] {e}", file=sys.stderr)
sys.exit(2)
page = build_page(title, body, link)
h = content_hash(body)
manifest = load_manifest()
if h in manifest.get("entries", {}):
print(f"[去重] 内容已存在于 {manifest['entries'][h]},跳过。")
return
fname = slugify(title) + ".md"
dest = os.path.join(outdir, fname)
rel = os.path.relpath(dest, ROOT)
if not args.write:
print(f"[干跑] 将写入: {rel}\n--- frontmatter 预览 ---\n{page[:400]}...")
print("加 --write 实际落盘。")
return
os.makedirs(outdir, exist_ok=True)
with open(dest, "w", encoding="utf-8") as f:
f.write(page)
manifest.setdefault("entries", {})[h] = rel
save_manifest(manifest)
print(f"[已导出] {rel}")
if __name__ == "__main__":
main()