# 模块 2:将 Anki `noteId` 回写到标题块末尾,使用短标记 `` > 归档说明(2026-04-21):本文描述的是已移除的 legacy syncRegistry 主链方案。 > 当前保留的生产同步链路是 `ManualSyncService` 所在的 manual-sync 体系。 ## Summary - 新增卡片同步成功后,把返回的 `Anki noteId` 写回对应标题块的块尾,格式固定为 ``。 - 以后该标题块再次同步时,优先按这个 `noteId` 更新原卡,不再依赖 `标题 + 行号 + 路径`。 - 标记放在块尾而不是标题下方,用来明确卡片边界。 - 老卡不迁移,只对以后新增的卡生效;老卡继续走当前 `cardKey` 逻辑。 - 如果块尾 `noteId` 在 Anki 中不存在,自动重建新卡并替换为新的 `noteId`。 - 如果同一标题块从 `basic` 切到 `cloze` 或反过来,直接报错并要求用户重建。 ## Key Changes ### 1. 标记语法与块尾规则 - 唯一支持的标记格式: - `` - 标记位置固定: - 当前标题块最后一个非空内容行之后 - 下一个同级或更高层级标题之前 - 写回规则固定: - 有正文时:写在正文末尾 - 无正文时:写在标题下一行 - 标记本身视为块尾终止标记 - 读取规则固定: - 只识别当前标题块块尾的最后一个合法 `AHS` 标记 - 标记不进入 `bodyMarkdown` - 同一标题块多个合法 `AHS` 标记时报错 - 非法 `AHS` 格式时报错,不静默忽略 ### 2. 提取与渲染 - `CardDraft` 增加 `embeddedNoteId?: number` - `SourceLocation` 增加块尾定位信息,至少包括: - `blockEndLine` - `contentEndLine` - `markerLine?: number` - `CardExtractionService` 负责: - 识别块尾 `` - 从正文中剔除该标记 - 将 `embeddedNoteId` 放入 `CardDraft` - `CardRenderingService` 不处理身份标记,继续只处理标题和正文渲染 ### 3. 同步身份与本地状态 - 同步身份模式固定为三种: - `embedded-note-id` - `legacy-card-key` - `pending-note-id-write` - `SyncRecord` 增加: - `identityMode: "embedded-note-id" | "legacy-card-key" | "pending-note-id-write"` - `legacyCardKey?: string` - `noteId: number` - `filePath` - `sourceHash` - `lastSyncedAt` - `orphan` - `SyncRegistry` 必须支持: - `findByNoteId(noteId)` - `get(cardKey)` 保留兼容 - 新卡首次成功写回 `AHS` 标记后,记录保存为 `embedded-note-id` - 老卡无标记时,继续保存为 `legacy-card-key` - 建卡成功但标记写回失败时,保存为 `pending-note-id-write` 并保留 `legacyCardKey` ### 4. 规划逻辑 - 匹配优先级固定: 1. 有 `embeddedNoteId` 时,按 `noteId` 2. 无 `embeddedNoteId` 时,按当前 `legacy cardKey` - 对 `embeddedNoteId` 卡片: - registry 有同 `noteId` 记录:按 `sourceHash` 判断是否更新 - registry 无记录:仍视为已有卡,可直接更新 - orphan 判定拆成两套: - `embedded-note-id` 按本轮看到的 `noteId` - `legacy-card-key` 按本轮看到的 `cardKey` - 老卡不自动补写 `AHS`,只保留当前行为 ### 5. 执行逻辑 - `AnkiGateway` 增加: - `getNoteSummaries(noteIds: number[]): Promise>` - 执行前先校验所有 `embeddedNoteId`: - note 存在且模型一致:`updateNote` - note 不存在:自动重建 - note 存在但模型不一致:报错并停止该卡同步 - 自动重建流程固定: 1. `addNote` 2. 用新的 `noteId` 替换块尾旧 `AHS` 标记 3. registry 更新为新的 `embedded-note-id` - 新卡首次同步流程固定: 1. 无 `AHS` 标记,先 `addNote` 2. 把返回的 `noteId` 写到块尾 3. registry 保存为 `embedded-note-id` ### 6. Markdown 写回 - `VaultGateway` 增加: - `replaceMarkdownFile(path: string, expectedContent: string, nextContent: string): Promise` - 写回采用 optimistic concurrency: - 文件内容仍等于扫描时内容才允许写回 - 块尾写回算法固定: - 定位当前标题块结束边界 - 移除旧 `AHS` 标记(若存在) - 将新标记写到块内最后一个非空内容行之后 - 不允许落到下一个标题之后 - 若 `addNote` 成功但写回失败: - 不删除新建 note - registry 记为 `pending-note-id-write` - 以后只要还能按 legacy key 命中,就继续尝试补写 `AHS` - 补写成功后,记录转为 `embedded-note-id` ## Public Interfaces / Types - `CardDraft.embeddedNoteId?: number` - `SourceLocation.contentEndLine: number` - `SourceLocation.markerLine?: number` - `SyncRecord.identityMode` - `SyncRecord.legacyCardKey?: string` - `VaultGateway.replaceMarkdownFile(...)` - `AnkiGateway.getNoteSummaries(...)` - `HeadingSyncMarker` - `noteId: number` - `raw: string` - `lineIndex: number` ## Test Plan - 块尾 `` 能被正确识别 - `AHS` 标记不会进入 `bodyMarkdown` - 同一标题块多个 `AHS` 标记时报错 - 非法 `AHS` 格式时报错 - 新卡创建后,`AHS` 被写到块尾 - 空正文标题块时,`AHS` 写在标题下一行 - 带 `AHS` 的卡在改标题、改正文、块内移动、文件移动后仍更新原 note - registry 丢失时,带 `AHS` 的卡仍能按 `noteId` 更新 - `noteId` 在 Anki 不存在时会自动重建,并替换块尾 `AHS` - `basic/cloze` 类型切换时报错 - 老卡无 `AHS` 时继续走 legacy `cardKey` - `addNote` 成功但写回失败时,生成 `pending-note-id-write` - 下次命中后可补写 `AHS` 并转正 - orphan 检测对 `embedded-note-id` 和 `legacy-card-key` 都正确 ## Assumptions - 第一版只支持块尾短标记 `` - 不兼容旧的 `ID:` 行、长注释格式、frontmatter、block ID - 老卡不迁移;只有以后新增的卡获得稳定 `noteId` 身份 - `basic` 与 `cloze` 的跨类型切换第一版不自动处理 - 允许插件改写 Markdown 文件,在标题块末尾插入或替换 `AHS` 标记