# Soia Cwork Feishu Doc Git Sync

> 将飞书知识库或云文档以应用身份只读同步为本地 Markdown，保留目录、来源和同步元数据，并可接入 Git、Obsidian 与 VitePress；当用户要求同步飞书知识库、备份到 Git、在本地查看或规划双向同步时使用。

- Skill: `soia-team/soia-cwork-feishu-doc-git-sync` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add soia-team/soia-cwork-feishu-doc-git-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/soia-team/soia-cwork-feishu-doc-git-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: soia-team (https://skillmd.com/u/soia-team)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/soia-team/soia-cwork-feishu-doc-git-sync

---


# soia-cwork-feishu-doc-git-sync

把飞书知识库的内容镜像到一个本地 Markdown 知识库。默认方向是 Feishu → local/Git/Obsidian/VitePress；本技能不默认向飞书写入，双向同步必须先建立文档归属、冲突策略和写权限。

## 客户可读说明

### 这个技能可以做什么

| 客户想要 | 技能会做 | 客户能看到 |
|---|---|---|
| 同步飞书知识库到本地 | 遍历知识空间节点，读取可读文档并生成 Markdown | 本地镜像目录、目录层级、来源链接、同步清单 |
| 排除指定知识库子树 | 按稳定节点 ID 或精确标题跳过根节点及全部后代 | 不读取该子树正文、表格、图片或附件，也不加入侧边栏和重试队列 |
| 备份到 Git | 将生成内容放入客户指定的 Git 仓库并检查差异 | commit/push 回执、文件变更和失败清单 |
| 用 Obsidian 查看 | 在独立 vault 中保存规则、镜像和本地补录 | 可直接用 Obsidian 打开的 vault |
| 用 VitePress 展示 | 生成站点侧边栏并构建静态站点 | 本地开发服务或构建产物 |
| 检查表格/多维表格导出能力 | 解析真实资源类型、权限和可用导出格式 | 只读探查结果；不会默认生成 Excel 文件 |
| 同步独立或内嵌 Sheet | 对私有配置明确授权的范围读取 Sheet；可自动覆盖全部独立 Sheet 节点，内嵌 Sheet 从文档 XML 中识别后再读取 | 有边界的 Markdown 表格快照；不再把已授权的 Sheet 节点写成正文占位 |
| 保留 Sheet 公式、样式、批注与图表信息 | 对已选范围保存单元格、布局、图表和浮动图片元数据快照 | Markdown 表格旁的本地保真 JSON；不伪装为可编辑工作簿 |
| 初始化完整 Sheet 与报表 | 经确认后分批导出整个 Sheet 工作簿 | `.xlsx` 保真副本，保留公式、样式、批注、图表、透视和单元格图片 |
| 镜像指定多维表格 | 读取指定 Base 表的字段、限量记录和可选视图，生成 Markdown 与快照 | 有上限的表格内容、schema/记录快照；默认不读取任何 Base 数据 |
| 初始化多维表、多人报表 | 经确认后分批导出完整 Base；选定表也可读取仪表盘与报表块元数据 | `.base` 保真副本；仪表盘快照写入 JSON，不伪装成交互式网页 |
| 本地化资源与导航 | 经确认后下载文档图片/附件，或下载所选多维表格记录附件，并把文档内部链接和子页面列表改为本地导航 | 本地资源、相对链接和可选子页面导航 |
| 初始化知识库文件 | 经确认后分批下载 Wiki `file` 节点的原始二进制 | 本地链接；ZIP、DMG、EXE 等只保存，绝不执行、挂载或解压 |
| 查看同步变更 | 经配置后生成新增、修改、移动和远端删除的本地变更台账与受限 diff | 本次同步的统计、变更清单和差异详情 |
| 规划双向同步 | 区分只读镜像、托管文档和本地补录 | 冲突/权限风险说明，不自动覆盖飞书 |

### 客户如何使用

1. 确认 `soia-cwork-feishu-cli` 已完成飞书应用凭证登录，并且机器人可以读取目标知识空间。
2. 在本机私有配置中填写知识空间 ID、输出目录和来源 URL 模板；不要把 App Secret、token 或企业私有路径提交到公开技能仓库。
3. 首次使用先执行 dry-run，核对空间、节点数量和目标目录。
4. 先用单节点隔离试点核对表格、资源和样式快照；`--pilot-node-token` 只写明确选择的节点到单独试点目录，不会给空目录补齐其他节点占位文件。
5. 执行镜像同步。默认只写本地文件和同步元数据，不修改飞书内容，也不删除本地历史文件。
6. 如需排除完整目录树，在私有配置设置 `sync.exclude_subtrees.enabled: true`，并在 `roots` 中优先填写稳定 `node_token`；首次清理既有 Markdown、完整导出和已下载附件时显式运行一次 `--rebuild-tree`，本地补录目录永不受影响。
7. 同步写入后会自动校验 manifest、文件存在性、frontmatter、失败占位、侧边栏覆盖范围、排除子树残留、资源引用、未归档的嵌入式 Sheet、未归档的 Sheet 内嵌 Base、`all_docx` 模式下尚未完成 XML 语义扫描的历史文档，以及 `all_nodes` 模式下仍未生成真实表格的独立 Sheet；发现 `failed`/`stale` 或语义缺口时返回非零结果，不能把空白占位或局部内容当作完整成功。
8. 如需检查表格导出，先做 `drive +inspect`/帮助/schema 探查；能力探查不等于授权导出。
9. 只有客户明确确认导出范围、格式、文件数和本地目录后，才调用 `drive +export` 或 `drive +export-download`。
10. 如需镜像 Sheet，先在私有配置明确范围：全部独立 Sheet 使用 `sync.sheets.enabled: true` 与 `all_nodes: true` 自动发现每个网格子表，或在 `selections` 中逐项指定 `node_token`、稳定 `sheet_id` 和有界 A1 `range`；两种方式都必须设置行列、单元格和返回字符上限。响应达到 `max_chars` 时必须按 `actual_range` 下一行续读，不能把截断当成整表失败。混合工作簿还需显式设置 `include_bitable_tabs: true` 与记录上限，随后把 Base 子表路由到多维表格读取。文档内嵌 Sheet 启用 `sync.embedded_sheets.enabled` 后，还须选择 `all_docx: true` 或 `node_tokens`。按需开启 `sync.sheets.preserve` 保存公式、样式、批注、布局和图表等元数据。未启用时必须显示并校验语义缺口，不得静默删除 `<sheet>` 或把独立 Sheet 占位当成完整归档。
11. 如需镜像多维表格，逐项指定 `sync.bitables.selections` 的 `node_token`、`table_id` 和 `max_records`，再开启 `sync.bitables.enabled` 或传入 `--sync-bitables`；附件二进制还需要单独开启 `download_attachments`，仪表盘/报表元数据需要 `include_dashboards`。
12. 只有用户明确确认来源、格式、文件数、输出目录和 Git 策略后，才能执行完整初始化：Sheet 设置 `sync.sheets.workbook_exports.enabled=true` 与 `all_nodes=true`，Base 设置 `sync.bitables.base_exports.enabled=true` 与 `all_nodes=true`，Wiki 文件设置 `sync.files.downloads.enabled=true` 与 `all_nodes=true`。每项必须配置 `batch_size`，重复执行至 deferred 为零。
13. 如需离线资源、文档间本地跳转、子页面导航或变更台账，先在私有配置中逐项启用 `download_assets`、`localize_internal_links`、`render_sub_page_navigation`、`change_ledger`；它们默认关闭以兼容已有镜像。
14. 批量初始化前先以一份代表性范围试点，确认资源数量、失败类别和本地渲染；同步完成后再运行 Git diff、站点构建和必要的人工抽查。

推荐命令：

```bash
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --dry-run
# 试点只写所选节点到独立目录；仍会核验它是否属于当前知识库
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --output-dir <pilot-output-dir> \
  --pilot-node-token <node_token> --incremental
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental
# 没有事件订阅时，按 wiki +node-get 的远端更新时间判断正文是否变化
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental --probe-remote-metadata
# 复用已完整正文，优先补独立 Sheet stub、未扫描的内嵌 Sheet 文档，再退避重试失败项
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --retry-failed
# 事件适配器已经拿到变动 ID 时，只拉对应节点；可重复传入多个 ID
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --changed-node-token <node_token> --changed-obj-token <obj_token>
# 只修复指定节点的本地格式，复用 manifest 中的其他文档，不重试历史失败项
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --rebuild-tree --rebuild-tree-only --only-node-token <node_token> --skip-assets
# 官方 webhook/长连接适配器写入 JSON/NDJSON 后，按事件目标增量拉取
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --event-file <events.ndjson>
# 仅在确认历史生成目录曾经扁平化时执行一次结构迁移
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --retry-failed --rebuild-tree
# 大型知识库遇到限流时分批补偿；重复执行直到 --validate-only 通过
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> \
  --retry-failed --retry-batch-size 100 --skip-assets
# 如果只需要修复本地目录层级、暂时不请求飞书
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --rebuild-tree --rebuild-tree-only
# 从飞书刷新最新目录层级和兄弟节点顺序，但复用现有本地正文
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --rebuild-tree --refresh-tree-only
# 应用私有配置中的整棵子树排除，并清理该子树既有 Markdown、完整导出和已下载附件
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --rebuild-tree --refresh-tree-only --skip-assets
# 下载图片到本地镜像并把正文中的远程 URL 改成相对路径
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --download-assets
# 将私有配置中明确选择的 Sheet 范围渲染为 Markdown 表格
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --sync-sheets
# 将已授权文档中的嵌入式 Sheet 追加为有界 Markdown 表格快照
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --sync-embedded-sheets
# 将私有配置中明确选择的多维表格镜像为 Markdown 与 JSON 快照
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --sync-bitables
# 完整初始化仍使用同一同步命令；私有配置中 workbook_exports/base_exports/files.downloads
# 的 enabled 与 all_nodes 必须都为 true，并按 batch_size 分批重复执行
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --incremental \
  --sync-sheets --sync-bitables
# 只校验最近一次同步生成的本地镜像，不访问飞书
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --validate-only
```

### 依赖与安装

装整个域（Claude Code 与 Codex 共用同一份域插件）：

```bash
claude plugin marketplace add soia-team/soia-open-skills
claude plugin install soia-cwork-office@soia
```

只装这一个技能：

```bash
npx skills add soia-team/soia-open-cwork-office-skills -g -a '*' -s soia-cwork-feishu-doc-git-sync -y
```

**WorkBuddy** 的装载单位是角色化专家而不是插件，`npx skills add -a '*'` 覆盖不到它，需要单独安装，见 [docs/install/workbuddy.md](https://github.com/soia-team/soia-open-skills/blob/main/docs/install/workbuddy.md)。

### 三种工作模式

- `mirror`：默认模式。知识库是来源，本地生成的 `10_knowledge-base/` 不应手工编辑。
- `local`：只维护本地 `20_本地补录/`，不会被镜像同步覆盖，也不会自动上传飞书。
- `managed`：未来用于明确指定的双向托管文档。必须逐文档确认写入权限、冲突规则和发布动作；当前脚本只提供只读镜像基础，不把它伪装成已经完成的双向同步。

### ID 增量同步与事件推送

- `node_token` 是同步主键，`obj_token` 是正文读取和事件映射的对象键；标题变化、移动和重名都不应改变这两个 ID。
- `sync.exclude_subtrees` 必须在私有配置中显式启用；优先按稳定 `node_token` 排除，`exact_title` 会排除所有精确同名根节点。同步器只在根节点的父级列表中识别它，不再枚举后代，并从正文、Sheet、Base、图片、附件、重试、活跃 manifest 和侧边栏中同时排除整棵子树。
- 已生成的排除子树内容不会被普通增量同步静默删除；用户明确要求清理后运行 `--rebuild-tree`，同步器把已知成员记录为 `excluded` 而不是误报为远端删除，并清理、校验对应 Markdown、目录、`_exports/` 完整导出和 `_assets/` 已下载附件均不残留；`20_本地补录/` 永不删除。
- `--only-node-token` 是单文档修复开关；与 `--rebuild-tree-only` 一起使用时只从已有 manifest 定位节点，不重新遍历飞书树，也不会因为其他节点历史失败而重试它们。
- 首次同步建立完整基线，记录 `obj_edit_time`/`remote_updated_at` 和 `docs +fetch` 返回的 `revision_id`。
- 后续 `--incremental` 仍会先按 `parent_node_token` 重建树，但只读取新增、失败、事件命中或远端编辑时间变化的文档正文；未变化节点复用本地 Markdown。
- 兄弟节点顺序直接保留 `wiki +node-list` 返回的飞书顺序，不按标题重新排序；因此 VitePress/Obsidian 目录应与飞书知识库的手工排序一致。
- 没有事件目标时，默认用 `wiki +node-get` 做元数据探测；这会产生较多轻量元数据请求，但避免重复下载正文。大型空间可改用官方事件订阅并传 `--event-file`。
- 事件只提供“哪个对象可能变了”的提示，不能替代 Wiki 树对账；创建、删除、标题变化和未识别事件仍需重新对账节点树。
- 官方事件订阅、权限和 `drive.file.*` 覆盖边界见 [references/events.yml](references/events.yml)。当前 `lark-cli event list` 未暴露云文档 `drive.file.*` 事件，因此本脚本不声称已经在 CLI 内常驻监听；外部长连接/webhook 适配器可以把 JSON/NDJSON 交给 `--event-file`。

## 依赖与安装

安装本技能及其同仓依赖：

```bash
claude plugin marketplace add soia-team/soia-open-skills
```

```bash
claude plugin install soia-cwork-office@soia
```

只要这一个技能时，可用 npx 路线。注意技能会落进共享真源 `~/.agents/skills`；若同时装了插件，同一技能会出现两份索引且各自漂移，建议二选一：

```bash
npx skills add soia-team/soia-open-cwork-office-skills -g -a '*' -s soia-cwork-feishu-doc-git-sync -y
```

| 依赖 | 类型 | 安装 / 配置 | 缺失时怎么处理 |
|---|---|---|---|
| `soia-cwork-feishu-cli` | 强依赖 | 安装并配置飞书官方 `lark-cli` 应用凭证 | 停止，先完成 bot 登录和权限检查 |
| `lark-cli` | 强依赖 | 参见 `soia-cwork-feishu-cli` 的安装说明 | 停止并报告安装命令 |
| Python 3.10+ | 强依赖 | 使用系统 Python 或项目 Python | 停止 |
| PyYAML | 强依赖 | `python3 -m pip install pyyaml` | 停止并报告依赖缺失 |
| Git | 可选增强 | 安装 Git | 仍可生成本地镜像，但不能提交/推送 |
| VitePress | 可选增强 | 由目标文档仓库提供 | 仍可同步到 Obsidian |
| Obsidian | 可选增强 | 用户本机安装 | 仍可生成普通 Markdown |

私有配置默认位置：

```text
~/.config/soia-skills/soia-cwork-feishu-doc-git-sync/config.yml
```

也可以使用 `SOIA_CWORK_FEISHU_DOC_GIT_SYNC_CONFIG_FILE` 指定配置文件。参考 [assets/config.example.yml](assets/config.example.yml)。

最小配置示例：

```yaml
version: 1
provider:
  cli: lark-cli
  profile: <configured-cli-profile>
  brand: feishu
  identity: bot
space:
  id: <wiki-space-id>
  source_url_template: https://<tenant>.feishu.cn/wiki/{node_token}
paths:
  output_dir: <git-repository>/docs/feishu-knowledge
  generated_dir: 10_<knowledge-base-name>
sync:
  mode: mirror
  prune: false
```

权限建议：首轮只申请知识库、文档与 Sheet 只读权限。图片和附件下载是可选增强，涉及云盘/导出权限时单独申请；双向写入权限永不作为默认权限。

## 同步规则

- 只使用 `--as bot` 的应用身份读取，默认不需要用户身份 token。
- 通过 node token 遍历知识空间，使用文档 token 读取 `docx` 内容。
- `paths.generated_dir` 必须使用目标知识库的真实名称或稳定英文名称；例如 `10_后端技术支持库`，不要再嵌套一个泛化的 `feishu-knowledge` 目录。
- 如果已有输出目录中只有一个 `10_*` 生成目录而配置未填写 `paths.generated_dir`，技能会复用它；如果发现多个候选目录，会停止并要求先明确配置，避免自动制造重复目录。
- 每个生成 Markdown 写入来源 URL、space ID、node token、object token、父节点和内容 hash。
- 使用 `sync-state.json` 保留 node token 到本地路径的映射；标题变化时尽量保持稳定路径，树位置由最新 `parent_node_token` 重新计算。
- `manifest.json` 和 `sync-state.json` 记录 `obj_edit_time`、`remote_updated_at`、`revision_id`，用于增量选择和审计。
- 本地单个路径组件最多 48 个字符；超长标题会保留完整标题在 frontmatter/侧边栏，并在文件夹或文件名中追加 node ID 短后缀，避免 Obsidian、macOS 和 VitePress/Rollup 路径过长。
- 同步器会把飞书导出的自定义 `figure/source/grid/callout`、媒体 token 和 XML 片段转换为可被 Markdown/VitePress 解析的形式；这只改变本地渲染，不写回飞书。
- 飞书文档引用会按 `node_token` 优先、`obj_token` 兜底解析为可点击的飞书 Wiki 链接；用户引用会保留为 `@显示名`。静态 Markdown 不复制飞书的悬浮卡片和成员头像交互，但不再错误降级为代码样式。
- 有子节点的飞书节点必须生成一个同名目录，并把正文放在目录内的同名 index Markdown：`父目录/节点名/节点名.md`；叶子节点才直接生成 `节点名.md`。不要生成同级的“同名文件 + 同名文件夹”。
- 如果飞书本身存在同名叶子与可展开节点、父子同名或同级重复可展开节点，目录/文件会追加稳定的 node ID 短后缀；这是为了避免本地文件系统发生同级冲突，manifest 仍以 `node_token` 区分真实节点。
- `--retry-failed` 会复用已有完整正文，并把失败节点、独立 Sheet stub、未完成 XML 语义扫描的历史文档加入补偿队列；适合遇到飞书接口限流或启用新的 Sheet 策略后继续补齐。
- 历史镜像中如果保留“当前同步器只读取文档正文”一类旧占位，即使飞书节点后来从 Sheet 变成 Docx、manifest 状态仍是 `ok`，验收也会判定失败，并在 `--retry-failed` 队列中最高优先级重拉；不能把类型漂移后的旧占位当成成功正文。
- `--retry-batch-size N` 仅与 `--retry-failed` 配合使用，每次最多补偿 N 个失败或语义缺口节点；大型空间应重复执行，并以 `--validate-only` 的 `failed_records=0`、`unmirrored_sheet_nodes=0`、`unscanned_embedded_sheet_documents=0` 为结束条件。
- `--retry-failed` 使用上一次 manifest 作为节点清单，不再先对全量文档做元数据探测；启用 Sheet 全量策略时，它会优先补独立 Sheet stub 与缺少语义扫描标记的历史 Docx，再重试普通失败项，并继续受全局节流保护。大型空间配合 `--retry-batch-size` 重复执行，直到 `unmirrored_sheet_nodes` 与 `unscanned_embedded_sheet_documents` 都为 0。
- 同一输出目录同时只允许一个同步进程；如果上一轮仍在退出或用户重复启动，后续进程会停止并报告，不会并发覆盖 manifest。
- 全局请求节流和指数退避会跨同步 worker 生效；`sync.min_request_interval_seconds` 默认 0.5 秒，避免大知识库并发触发 `99991400` 限流。
- 正文读取失败但本地已有旧正文时，节点会标记为 `stale` 并保留旧正文；下一次增量同步会继续重试，校验不会将其视为成功。
- 每次非 dry-run 同步结束都会运行本地验收；`--validate-only` 可单独复核最近一次结果。验收失败时退出码为 2，并在 manifest 的 `validation` 节点保留机器可读摘要。
- `prune: false` 时不删除已消失节点对应的本地文件；节点会在 manifest 中标记为 deleted，避免一次权限或网络异常造成数据丢失。
- `20_本地补录/` 与 `90_同步元数据/` 不会被知识库同步覆盖。
- `--rebuild-tree` 只处理 `paths.generated_dir` 内由同步器生成的旧扁平文件与已排除节点的已知资源，不触碰 `20_本地补录/`。
- `--rebuild-tree-only` 仅复用已有 manifest 和生成文件做目录迁移，不发起飞书正文请求；如果同时启用资源本地化，仍可能只为刷新过期媒体 URL 读取含资源的文档。
- `--refresh-tree-only` 会重新读取飞书节点树和兄弟顺序，按最新 `parent_node_token` 重建本地目录和侧边栏，但复用已有本地正文；启用资源本地化时，会额外刷新仍含未本地化资源的文档；必须与 `--rebuild-tree` 一起使用。
- `manifest.json`/`sync-state.json` 的 `tree_order: feishu_node_list` 表示目录顺序来源于飞书节点列表，不是标题排序。
- 图片默认保留远程 URL；设置 `sync.download_assets: true` 或传入 `--download-assets` 后，技能会把正文中的远程图片及 `<source token="...">` 媒体块下载到 `paths.generated_dir/_assets/`，并把 Markdown/HTML 引用改写为相对路径。已下载的飞书附件卡片必须进一步改写为标准 Markdown 本地链接；若该链接被单独的 `<p>` 包裹，必须同时移除该 HTML 容器，确保 Obsidian 和 VitePress 都能点击打开。未下载的卡片仍保留原远程引用并报告失败。
- 下载资源时，优先按飞书媒体 token 去重；同一附件或图片即使带有不同的短期签名 URL，也只保留一份本地资源。无 token 的资源仍按 URL 内容寻址。
- `sync.localize_internal_links: true` 时，已同步的 Wiki/文档引用会改为相对本地 Markdown 链接；`sync.render_sub_page_navigation: true` 时，飞书导出的 `<sub-page-list>` 会改为本地 Markdown 子页面导航。两项均默认关闭，不影响已有外链行为。
- `sync.change_ledger: true` 时，会在同步元数据下按运行生成新增、修改、移动和远端删除的变更台账；修改项只保留受 `change_ledger_max_diff_lines` 限制的 diff，不复制文档全文，也不改变生成镜像或本地补录目录。
- `sheet` 默认只生成元数据 stub。经用户明确授权全库有界快照后，设置 `sync.sheets.enabled: true` 与 `all_nodes: true`，同步器会用 `sheets +workbook-info` 自动发现每个独立 Sheet 的网格子表，再按 `max_rows`、`max_columns`、`max_cells`、`max_chars` 上限调用 `sheets +csv-get`；响应截断时按 `actual_range` 分行续读。也可继续用 `selections` 精确声明单个范围。混合工作簿中的 Base 子表只有在 `include_bitable_tabs: true` 时才按 `max_bitable_records` 有界读取，否则节点失败，不能静默跳过；若 Base 权限不足，已读出的普通 Sheet 仍落盘，但节点保持 `failed` 并写入明确缺口标记。`all_nodes` 验收会拒绝任何 `metadata_stub`、缺少真实表格标记的 Sheet 节点或未归档的 Base 子表。完整契约见 [references/sheet-mirroring.yml](references/sheet-mirroring.yml)。
- 飞书 Markdown 可能只返回 `<sheet>` 引用而不返回任何单元格。同步器不得删除该引用后仍报告完整成功：未授权下钻时保留可见缺口并使验收失败；仅当 `sync.embedded_sheets.enabled` 且明确选择 `all_docx: true` 或 `node_tokens` 时，才读取文档 XML 并在配置上限内生成“嵌入式表格”快照。首次启用会补扫旧文档；快照不会伪装成交互式原表。完整范围契约见 [references/sheet-mirroring.yml](references/sheet-mirroring.yml)。
- `sync.sheets.preserve.enabled: true` 会在同一选定范围另存单元格值、公式、样式、批注，以及工作表布局、图表和浮动图片元数据的 JSON 快照；可额外开启透视、筛选、条件格式和迷你图元数据。它不把图表或图片伪装成原生 Markdown。完整工作簿需在用户明确批准后同时设置 `sync.sheets.workbook_exports.enabled: true` 和 `all_nodes: true`，同步器会用 `drive +export` 分批生成 `.xlsx` 并在 Sheet 索引中链接。
- 异步 Sheet 导出未在首轮轮询完成时，会把导出任务票据保存在生成目录的私有快照中；后续批次只轮询同一任务，并在就绪后使用 `drive +export-download` 下载，不会反复创建相同工作簿导出任务。`manifest.stats.sheet_workbooks_pending` 表示仍在飞书端处理的数量。
- 图片/附件本地化是显式 opt-in 的本地数据下载；不能因为用户只要求“检查图片”就下载全部素材。持久化配置中的 `sync.download_assets: true` 只能视为用户此前对该资源范围的明确授权，不得扩展为表格或多维表格导出授权。若先执行 Sheet/Base/文件的结构初始化并使用 `--skip-assets` 避免输出锁冲突，必须在初始化结束后单独执行资源本地化批次；`--skip-assets` 不会把图片或附件标记为已下载。
- `bitable` 与未授权的 `sheet` 默认只生成元数据 stub，不读取表内数据；已启用 `sync.sheets.all_nodes` 的独立 Sheet 不允许继续保留 stub。设置 `sync.bitables.enabled: true` 后，必须逐项声明 `node_token`、`table_id` 与 `max_records`；同步器以 `base +field-list`、`base +record-list` 和可选的 `base +view-list`/`base +dashboard-list`/`base +dashboard-block-list` 生成 Markdown 与 JSON 快照。`sync.bitables.download_attachments: true` 是独立的二进制下载授权，使用 `base +record-download-attachment` 并只处理已选表的已读记录附件。完整 Base 导出仍须在用户明确批准后同时设置 `sync.bitables.base_exports.enabled: true` 和 `all_nodes: true`，同步器用 `drive +export` 分批生成 `.base`。
- Wiki `file` 节点默认只生成元数据 stub；用户明确确认完整初始化后，同时设置 `sync.files.downloads.enabled: true` 与 `all_nodes: true`，同步器才会分批调用 `drive +download`。文件以原始二进制保存，不会执行、挂载、解压或解析 ZIP、DMG、EXE 等格式。
- 飞书图片 URL 可能很快过期。启用本地化并使用 `--refresh-asset-urls` 时，会先刷新关联文档的 URL 再下载，不会无条件重拉所有正文；下载后统一写为转义 `alt` 的本地 HTML `<img>`，并按文件二进制识别 PNG/JPEG/GIF/WebP/SVG、修正错误缓存后缀。
- 事件目标、`--changed-*-token`、`--only-node-token` 或隔离试点只处理本轮重读文档的资源，不会把整库历史资源重新加入下载队列；完整资源回填则不传目标并按 `asset_batch_size` 分批执行。
- 图片下载使用 URL 内容寻址文件名，重复同步会复用已有资源；Markdown 的普通 URL 与 `(<https://…>)` 尖括号 URL 都会进入同一下载队列。可通过 `asset_workers`、`asset_timeout_seconds`、`asset_download_attempts`、`max_asset_bytes` 限制并发、超时、媒体重试和单文件大小；单资源默认上限为 50 MiB，可在私有配置中按已确认的下载范围调低或调高。携带飞书媒体 token 的附件遇到短暂或未归类 CLI 失败时会在上限内重试；明确的权限不足、资源不存在或 `asset_too_large` 不会盲目重试。大型存量镜像可设置正整数 `asset_batch_size`：每次只请求该数量的尚未落盘资源，已下载资源仍会被本地化改写；`asset_refreshed_batch_size` 可限制同一批已刷新文档的新链接下载量。重复执行并以 `--validate-only` 确认完成。manifest 会报告本轮 `assets_deferred`。下载失败只保留原 URL，并在 manifest 的 `assets_failed` 计数中报告，不把鉴权 URL 写入日志或清单。
- `<source token="...">` 或无 URL 的 `<img token="...">` 会调用官方 `docs +media-download`；远程 URL 不可直接读取时，需要按权限清单补充 `docs:document.media:download` 或 `drive:file:download`，并在私有配置中启用本地资源下载。没有下载权限时不得猜测本地资源已经完整。

## 安全规则

- 不在公开 skill、Obsidian vault、Markdown、Git 提交、终端输出或最终回复中写入 App Secret、access token、cookie。
- 终端日志、进度回执和最终回复不得输出本地绝对路径、具体本地文件名、操作系统用户名、用户名、密码、App Secret、access token 或私有下载 URL；统一使用脱敏占位符，只报告状态、数量和错误类别。详见 [references/output-redaction.yml](references/output-redaction.yml)。
- 不默认调用飞书创建、更新、删除接口。
- `drive +export`、`drive +export-download`、`docs +media-download` 和附件下载均属于数据导出/下载动作；用户说“看下能否导出”时只做 inspect、help、schema 或 dry-run，不得直接创建本地文件。
- Sheet 与 Base 值镜像都属于敏感数据落盘：只有用户明确确认需要将选定范围/表同步到本地工作区，并在私有配置中写入有界 `sync.sheets.selections` 或受限 `sync.bitables.selections` 后才可启用；该确认不授权任何工作簿导出、未选附件下载或自动 Git 提交。
- 真实导出前必须明确回执来源、类型、格式、预计文件数、输出目录和 Git 追踪策略；“检查能力”不等于“授权导出”。
- 导出文件默认放在临时目录或用户明确指定的目录；只有经用户明确确认的完整镜像初始化才能写入生成目录下的 `_exports/` 或 `_assets/`，并且不得自动提交 Git、自动推送远程或写回飞书。
- bot 无权访问的个人云盘或私有资源必须报告为不可见，不得切换 user OAuth 代为读取。
- 不默认覆盖本地补录、删除历史文件或推送远程 Git；这些属于需要明确确认的写入/发布动作。
- 执行前检查目标仓库、当前分支和远程地址；发现与预期不符时停止并报告。

## 私密信息与中间数据

- 把排除节点 ID、知识空间 ID、凭据别名和本地输出位置只写入技能私有配置，不提交公共仓库。
- 把 active/excluded 节点清单、同步状态、侧边栏和变更台账保存在用户指定镜像的同步元数据目录；排除记录只用于证明未抓取范围，不包含正文。
- 把临时下载和接口中间结果交给系统临时目录；只把用户授权的 Markdown、表格快照和资源文件写入交付目录。

## 日志与完成回执

终端和最终回复至少报告：

- `started`：空间、身份、配置来源和目标目录（不打印秘密）。
- `processed`：节点、文档、跳过和失败数量。
- `created/updated`：生成或更新的 Markdown、manifest、sidebar 数量。
- `skipped/failed`：失败节点、原因和是否可重试。
- `verification`：Git diff、VitePress build、抽样文档和源链接检查结果。
- `next_step`：是否需要补权限、确认 Git push 或规划双向同步。
- 文件变化只报告数量和类别，不列本地路径或文件名；身份只报告 `bot identity` / `user identity` 等非敏感状态。

## Resources

- 权限与权限申请分层：[references/permissions.yml](references/permissions.yml)
- 飞书 CLI 命令与权限申请流程：同仓库 `soia-cwork-feishu-cli/references/cli-workflows.md` 和 `soia-cwork-feishu-cli/references/permissions.md`。
- 事件订阅与增量目标：[references/events.yml](references/events.yml)
- 同步策略：[references/sync-policy.yml](references/sync-policy.yml)
- 文档格式转换：[references/block-mapping.yml](references/block-mapping.yml)
- Sheet 范围镜像：[references/sheet-mirroring.yml](references/sheet-mirroring.yml)
- 富资源镜像能力与边界：[references/rich-resource-mirroring.yml](references/rich-resource-mirroring.yml)
- 表格/多维表格导出安全策略：[references/export-policy.yml](references/export-policy.yml)
- 日志与回复脱敏策略：[references/output-redaction.yml](references/output-redaction.yml)
- Git 与 VitePress 接入：[references/git-vitepress.yml](references/git-vitepress.yml)
- 私有配置模板：[assets/config.example.yml](assets/config.example.yml)

## Validation

```bash
python3 scripts/sync_feishu_wiki.py --help
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --dry-run
python3 scripts/sync_feishu_wiki.py --config <private-config.yml> --validate-only
git diff --check
```

### Forward test

Before a real sync, run a dry-run or a small authorized representative scope and verify the tree, stable node-ID mapping, ordering, incremental selection, asset references, and failure receipt. After every write, require the built-in validation gate to pass; a zero exit code alone is not evidence that the mirror is complete.

