panatgithub_AnkiHeadingSync/docs/2026-04-18PLAN4.md

13 KiB
Raw Blame History

模块 3全新手动同步型插件的高性能架构开发计划

Summary

  • 本模块不再基于当前插件做兼容式演进,而是按“全新插件”设计。
  • 核心交互改为手动触发,不做默认自动同步。
  • 核心目标不是最少开发量,而是“手动同步体验稳定 + 全库同步性能尽可能高 + 后续可扩展”。
  • 不考虑老卡兼容、不考虑迁移、不考虑复用旧的 legacy cardKey 身份体系。
  • 允许插件在“用户主动点击同步”时回写 Markdown 标记,但禁止在日常编辑过程中自动改正文。

Product Goals

  • 支持两个主命令:
    • 同步当前文件到 Anki
    • 同步全库到 Anki
  • 支持一个维护命令:
    • 重建卡片索引
  • 支持标题块卡片模型:
    • 某一级标题 = Basic 卡
    • 某一级标题 = Cloze 卡
  • 同步身份稳定:
    • Markdown 侧身份使用 cardId
    • Anki 侧身份使用 noteId
  • 全库同步性能最优先:
    • 先过滤文件路径,再读文件内容
    • 文件没变则跳过
    • 文件变了也只更新变动卡片块
    • Anki 请求按批次执行,不按单卡逐条发

Non Goals

  • 不做自动实时同步
  • 不做旧插件数据迁移
  • 不做旧卡兼容逻辑
  • 不做双向同步
  • 不做 Anki 卡片模板 HTML/CSS 编辑器
  • 不支持无 marker 的长期身份推断策略

User Interaction Model

1. 默认模式:手动同步

  • 插件默认不监听文件改动后自动同步。
  • 只有用户主动点击命令时,才允许:
    • 扫描文件
    • 规划同步
    • 调用 Anki
    • 回写 Markdown 标记

2. 命令设计

  • 同步当前文件到 Anki
    • 读取当前激活 Markdown 文件
    • 仅处理该文件内的卡片块
  • 同步全库到 Anki
    • 处理配置范围内全部 Markdown 文件
    • 使用文件级和卡片级缓存跳过未变内容
  • 重建卡片索引
    • 重建本地状态
    • 不默认执行全量 Anki 字段更新

3. Notice / Result 输出

  • 每次同步结束后展示:
    • 扫描文件数
    • 扫描卡片数
    • 新增卡数
    • 更新卡数
    • orphan / 删除数
    • 上传 media 数
    • 跳过未变化卡数
  • 若发生 Markdown 回写冲突:
    • 明确提示哪些文件未完成 marker 写回
    • 不使用泛化报错文案

Identity Model

1. 双身份设计

  • cardId
    • Markdown 侧身份
    • 代表某个标题块本身
    • 一旦生成,不再修改
  • noteId
    • Anki 侧身份
    • 代表 Anki 中的 note

2. 标记格式

  • 块尾机器标记固定为:
    • <!-- AHS:card=<cardId> note=<noteId> -->
  • 在首次发现卡片但尚未创建 Anki note 时,允许写成:
    • <!-- AHS:card=<cardId> -->

3. 标记位置

  • 统一放在标题块末尾:
    • 当前标题块最后一个非空内容行之后
    • 下一个同级或更高层级标题之前
  • 目标示例:
    #### 标题
    正文内容
    <!-- AHS:card=ahs_k83jf29 note=1776442768197 -->
    

4. 身份规则

  • 插件长期只认 cardId
  • noteIdcardId 指向的远端目标
  • 不再使用 标题 + 行号 + 路径 作为正式身份
  • 不做无 marker 时的长期启发式身份匹配

Card Model

1. 卡片边界

  • 从当前目标标题开始
  • 到下一个同级或更高层级标题前结束

2. 卡片类型

  • basic
  • cloze

3. 每张卡的最小索引字段

  • cardId
  • noteId?
  • filePath
  • heading
  • headingLevel
  • cardType
  • blockStartOffset
  • blockEndOffset
  • rawBlockText
  • rawBlockHash
  • markerLine?
  • deckHint?
  • tagsHint[]

4. 每张卡的渲染字段

  • renderedTitle
  • renderedBody
  • renderedFields
  • mediaManifest
  • renderConfigHash

Architecture

1. Card Marker Service

负责:

  • 解析 AHS 标记
  • 生成新的 cardId
  • 生成标准 marker 文本
  • 在块尾插入或替换 marker

必须保证:

  • 一个标题块最多一个合法 marker
  • marker 不进入正文内容
  • 块尾写回顺序稳定

2. File Indexer

负责:

  • 列出候选 Markdown 文件
  • 先按 include/exclude 过滤路径
  • 读取变更文件内容
  • 提取卡片块
  • 生成文件级 hash 与卡片级 hash

必须保证:

  • 不在扫描阶段做完整 HTML render
  • 不在这里调用 Anki

3. Diff Planner

负责:

  • 比较“当前索引”和“本地状态”
  • 输出:
    • toCreate
    • toUpdate
    • toOrphan
    • toDelete(如果启用)
    • toRewriteMarker

4. Renderer

负责:

  • 只渲染待同步卡片
  • 解析 markdown / cloze / embeds / wikilinks
  • 生成最终 Anki 字段

必须保证:

  • 未变化卡片不进入 renderer

5. Anki Batch Executor

负责:

  • 批量 createDeck
  • 批量 notesInfo
  • 批量 addNote
  • 批量 updateNoteFields
  • 按 deck 分组批量 changeDeck
  • 批量 tag 操作
  • 批量 storeMedia

6. Markdown Write Back Service

负责:

  • 按文件聚合 marker 写回
  • 同一文件只写一次
  • optimistic concurrency 校验
  • 失败时记录待补写状态

7. Local State Store

负责:

  • 持久化文件索引
  • 持久化卡片索引
  • 持久化 cardId -> noteId
  • 持久化待补写 marker 状态

File Discovery And Scan Strategy

1. 文件发现顺序

  • 先列出全部 Markdown 文件路径
  • 再做 include/exclude 过滤
  • 只读取命中的文件

2. 文件级缓存

本地状态必须保存:

  • filePath
  • fileHash
  • lastIndexedAt
  • cardIds[]

3. 文件级跳过策略

  • 文件 fileHash 未变化:
    • 全文件跳过
    • 不提取卡片
    • 不 render
    • 不发 Anki 请求

4. 卡片级提取

对变更文件:

  • 提取标题块
  • 提取 marker
  • 计算 rawBlockHash
  • 与本地卡片状态对比

5. 卡片级跳过策略

  • rawBlockHash 未变化:
    • 卡片跳过
    • 不 render
    • 不 update Anki

Rendering Strategy

1. 渲染延后

  • 扫描阶段只做轻量提取
  • 只有 toCreate / toUpdate 才进入渲染阶段

2. 渲染配置哈希

除了 rawBlockHash,还必须保存:

  • renderConfigHash

它至少包含:

  • note type
  • 字段映射
  • deck 规则
  • backlink 开关
  • cloze 转换开关

这样即使 Markdown 没变,但配置变了,也会触发 update。

3. 渲染产物

  • title
  • body
  • fields
  • mediaManifest

Sync Planning Rules

1. toCreate

条件:

  • 卡片有 cardId
  • 本地状态中没有 noteId

2. toUpdate

条件满足任一:

  • 本地有 noteIdrawBlockHash 变化
  • 本地有 noteIdrenderConfigHash 变化
  • 本地记录为 pending write-back

3. toOrphan

条件:

  • 本地状态里存在 cardId
  • 当前索引中该 cardId 已消失

第一版默认:

  • 仅标记 orphan
  • 不自动删 Anki note

4. toRewriteMarker

条件:

  • 缺失 cardId
  • 已有 cardId 但缺失 noteId
  • marker 格式可恢复但内容过时

Anki Execution Strategy

1. 请求批处理原则

  • 所有 Anki 操作按“操作类型”分批
  • 禁止按单卡逐条独立请求
  • 支持受控 batch size
  • 支持受控并发

2. 推荐执行顺序

  1. createDeck
  2. notesInfo(existing noteIds)
  3. addNote
  4. updateNoteFields
  5. changeDeck 按目标 deck 分组
  6. removeTags / addTags
  7. storeMedia
  8. Markdown marker 写回

3. notesInfo

  • 对全部待更新 noteId 一次拉取
  • 不能在 updateNote 内部每张卡再补一轮 notesInfo

4. changeDeck

  • 先拿 note 对应 cardIds
  • 再按 deck 分组聚合
  • 同目标 deck 的卡一次 changeDeck

5. Media 上传

  • 先全局去重
  • key 至少包含:
    • absolutePath
    • targetFileName
    • mtime 或 content hash
  • 用 3 到 5 并发上传
  • 避免无脑全并发

6. 批处理调度器

建议新增 BatchScheduler

负责:

  • 切分 batch
  • 控制并发数
  • 失败收集
  • 对可重试错误做有限重试

Markdown Write Back Strategy

1. 写回时机

  • 仅在用户主动点击同步命令时允许写回
  • 平时编辑过程中绝不自动改正文

2. 写回内容

  • cardId
  • noteId
  • 修复过时 marker

3. 写回粒度

  • 同一文件一次写回
  • 同步结束后按文件聚合执行

4. 写回顺序

  • 同一文件多个块从下往上处理
  • 避免上方插入影响下方定位

5. 冲突控制

  • 写回前比较当前文件 hash / expected content
  • 若文件已变化:
    • 不覆盖
    • 记录 pending write-back
    • 在结果 notice 中明确提示

Local State Design

1. 文件层状态

  • filePath
  • fileHash
  • lastIndexedAt
  • cardIds[]

2. 卡片层状态

  • cardId
  • noteId?
  • filePath
  • rawBlockHash
  • renderConfigHash
  • lastSyncedAt
  • cardType
  • deck
  • orphan

3. 待补写状态

  • filePath
  • cardId
  • noteId
  • expectedFileHash
  • targetMarker

4. 存储要求

  • 单次同步结束统一保存
  • 必须支持局部覆盖和完整重建

Commands And UX

1. 同步当前文件

流程:

  1. 获取当前 Markdown 文件
  2. 提取卡片块
  3. 补齐缺失 cardId
  4. 生成计划
  5. 执行 Anki 批同步
  6. 按文件回写 marker
  7. 更新本地状态
  8. 展示结果 notice

2. 同步全库

流程:

  1. 列出全部 Markdown 文件路径
  2. include/exclude 过滤
  3. 文件 hash 对比,跳过未变化文件
  4. 对变更文件做块级 diff
  5. 渲染待同步卡
  6. 批量执行 Anki 请求
  7. 批量写回 marker
  8. 保存本地状态
  9. 展示结果 notice

3. 重建卡片索引

流程:

  1. 全库重建文件索引与卡片索引
  2. 不默认发送所有更新到 Anki
  3. 修复本地状态与文件 marker 的一致性

Phase 1Identity And Marker

  • 实现 cardId 生成规则
  • 实现 AHS marker 解析与块尾写回
  • 为卡片块补写 cardId

Phase 2File And Card Index

  • 文件路径过滤
  • 文件级 hash
  • 卡片块提取
  • 卡片级 rawBlockHash

Phase 3Local State

  • 文件层状态持久化
  • 卡片层状态持久化
  • pending write-back 状态持久化

Phase 4Planner

  • toCreate
  • toUpdate
  • toOrphan
  • toRewriteMarker

Phase 5Renderer

  • 只为待同步卡渲染字段
  • 解析 media / wikilink / cloze

Phase 6Anki Batch Executor

  • 批量 notesInfo
  • 批量 addNote
  • 批量 updateNoteFields
  • deck 分组 changeDeck
  • media 批量上传

Phase 7Commands And Notice

  • 当前文件同步命令
  • 全库同步命令
  • 重建索引命令
  • 结果 notice

Public Interfaces / Types

Marker

  • AhsMarker
    • cardId: string
    • noteId?: number
    • raw: string
    • lineIndex: number

File Index

  • IndexedFile
    • filePath: string
    • fileHash: string
    • cards: IndexedCard[]

Card Index

  • IndexedCard
    • cardId: string
    • noteId?: number
    • filePath: string
    • cardType: "basic" | "cloze"
    • heading: string
    • rawBlockHash: string
    • markerLine?: number

Planner

  • SyncPlan
    • toCreate: IndexedCard[]
    • toUpdate: IndexedCard[]
    • toOrphan: string[]
    • toRewriteMarker: IndexedCard[]

Local State

  • PluginState
    • files: Record<string, FileState>
    • cards: Record<string, CardState>
    • pendingWriteBack: PendingWriteBackState[]

Test Plan

1. Marker / Identity

  • 缺失 marker 的卡片块会生成 cardId
  • 合法 AHS marker 能正确解析 cardIdnoteId
  • 非法 marker 报错
  • 同一块多个 marker 报错
  • marker 不进入正文

2. File Index

  • include/exclude 过滤先于文件读取
  • 文件未变化时整文件跳过
  • 文件变化但卡片块未变化时,卡片跳过
  • 标题块边界提取正确

3. Planner

  • noteIdcardId 卡进入 toCreate
  • rawBlockHash 变化进入 toUpdate
  • renderConfigHash 变化进入 toUpdate
  • 已消失卡进入 toOrphan
  • pending write-back 卡进入 toRewriteMarkertoUpdate

4. Renderer

  • 只渲染 toCreate/toUpdate
  • cloze 渲染正确
  • media manifest 正确
  • wikilink / backlink 正确

5. Anki Batch Executor

  • notesInfo 对全部待更新 noteId 批量请求
  • updateNoteFields 按 batch 执行
  • changeDeck 按目标 deck 分组
  • media 上传去重
  • 失败时能返回批次级错误信息

6. Markdown Write Back

  • 同一文件多个 marker 一次写回
  • 写回顺序自底向上
  • 文件变化时触发冲突,不覆盖
  • 冲突后生成 pending write-back

7. Commands

  • 同步当前文件 只影响当前文件
  • 同步全库 会跳过未变化文件
  • 重建卡片索引 不默认触发全量远端更新

Risks

  • cardId 设计一旦上线不宜再频繁变更
  • 文件级和卡片级缓存同时存在,状态结构会比当前插件复杂
  • Anki 批处理若 batch 过大,可能触发单次请求过重的问题
  • Markdown 块尾写回必须非常稳定,否则会直接影响用户信任

Decisions Locked For Module 3

  • 新插件默认只支持手动同步
  • 不做旧插件迁移
  • 不继续使用 legacy cardKey
  • 正式身份采用 cardId + noteId
  • 块尾 AHS marker 是唯一支持的机器标记形式
  • 全库同步必须实现文件级和卡片级跳过
  • Anki 执行层必须按类型批处理