--- name: knowledge-sync description: 把原始库的变化同步到 AI 库:扫描或指定文档、生成知识卡,整批 dry-run 清单确认后替换卡片并更新 SyncState。 --- # knowledge-sync 把原始库(飞书 Wiki)中的知识文档归纳成结构化知识卡,写入 AI 库(飞书 Base) 的 `KnowledgeCards` 表,并把每个文档的同步状态写入 `SyncState` 表。原始库全程 只读,写入只发生在 AI 库。 ## 适用时机 - 原始库中的文档新增、修改、移动或改名、删除后,需要把这些变化同步到 AI 库。 - 首次同步:AI 库的 `KnowledgeCards`、`SyncState` 两张表已由 `setup` 建好, 并把 `table_id` 写回 `~/.codex-script2shot/config.toml`。 - 只改动了一个文档,或某个文档的 `obj_edit_time` 没有变化但需要强制重新归纳时, 使用指定文档模式。 ## 输入模式 **扫描模式(缺省)**:用户只说 `$knowledge-sync`。扫描原始库的节点元数据,与 AI 库的 SyncState 对比,生成整批变化清单,确认一次后逐文档执行。 **指定文档模式**:`$knowledge-sync 只同步这个文档:<飞书文档 URL 或 token>`。 跳过扫描与整批确认,直接对给定文档强制重新归纳;仍要更新 SyncState。前置条件: 该文档必须已有 SyncState 记录(此前至少同步过一次);否则先用扫描模式同步一次。 ## 流程 ### 扫描模式 1. **`health` 预检**:调用 MCP 工具 `health`。存在 `status: "fail"` 的检查项就停在 预检,把失败明细报告给用户,不要继续同步。 2. **读取状态并生成计划**:调用 `read_sync_state` 和 `scan_knowledge_a`,再把 `scan_knowledge_a` 返回的 `nodes[]` 作为 `nodes`、`read_sync_state` 返回的 `records[]` 作为 `sync_records`,调用 `build_sync_plan`,得到 `to_add` / `to_update` / `to_move` / `to_delete` / `unchanged` / `warnings`。 `unchanged` 文档不读正文、不写任何东西。 3. **输出整批 dry-run 清单,请用户确认一次**:把 `to_add`、`to_update`、`to_move`、 `to_delete` 逐条列出 `title` 与 `path`(`unchanged` 只报数量),原样带上 `warnings`,并注明 `to_delete` 是不可逆的删除操作。得到用户明确确认后才进入执行; 用户不确认就停在这里。 4. **按文档处理**(对确认清单内的每个文档;单个文档失败不阻塞其他文档): - 新增 / 修改:`fetch_knowledge_a_doc`(`expand_references: true`)读取正文、引用 目标正文与 `revision_id` → 按 `references/card-authoring.md` 生成该文档的完整新卡集合 → `replace_doc_cards` (`cards` + `dry_run: false`)替换旧卡 → 成功后再调用 `update_doc_sync_state`。 - 引用展开的三种状态:`ok` 直接并入归纳;`needs_render`(网页是 JS 渲染、静态 HTML 没有正文)时用会话内的网页/浏览器工具读取该 URL 全文,读取失败就在汇总 报告里写明并跳过该引用;`failed` 保留 warning,按正文已有内容归纳,不编造引用内容。 - 移动 / 改名:不读正文、不重新生成卡片,只调用 `replace_doc_cards` 的 `metadata_patch` 模式(+ `dry_run: false`)更新来源元数据,成功后再调用 `update_doc_sync_state`;`source_revision`、`card_count` 沿用现有 SyncState 记录。 - `update_doc_sync_state` 的 `record` 来源元数据(`source_node_token`、 `source_path`、`scope`、`project_id`、`obj_edit_time`)取本次扫描结果中同一 `source_doc_token` 的节点;`source_revision` 取本次 fetch 的 `revision_id` (移动 / 改名除外);`card_count` 取本次写入的卡片数;`last_synced_at` 用当前 时间(带时区偏移的 ISO 8601)。 5. **删除类**:对 `to_delete` 逐条调用 `delete_doc_cards`(`dry_run: false`), 删除该文档的知识卡与 SyncState 记录。 6. **汇总报告**:报告每个文档的处理状态、失败明细与 `warnings`;失败文档明确写 「未更新 SyncState,可重试」。 ### 指定文档模式 跳过扫描模式的第 2–3 步:不调用 `build_sync_plan`,不输出整批清单,也不做整批确认。 直接对给定文档执行第 4 步,强制重新归纳(即使 `obj_edit_time` 没有变化),状态更新 照旧。用户提出指定文档请求即视为对该文档执行替换的确认。 指定文档模式的状态元数据来源: - 先用 `read_sync_state` 查出该文档的现有记录(只取该文档的既有状态,不生成整批 计划):`source_node_token`、`source_path`、`scope`、`project_id`、`obj_edit_time` 沿用记录中的值。 - `source_title` 按下面的「标题回退」处理;`source_doc_token`、`source_revision` 取本次 `fetch_knowledge_a_doc` 的 `doc_token`、`revision_id`。 - 该文档在 SyncState 中没有记录时不要猜元数据:返回结构化失败并保持 SyncState 不变,提示用户核对文档地址或改用扫描模式。 ### 标题回退 `fetch_knowledge_a_doc` 的标题提取顺序固定为:正文 `