#!/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 --write AI 自动补齐 frontmatter:title(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()