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

657 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 模块 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. 标记位置
- 统一放在标题块末尾:
- 当前标题块最后一个非空内容行之后
- 下一个同级或更高层级标题之前
- 目标示例:
```md
#### 标题
正文内容
<!-- AHS:card=ahs_k83jf29 note=1776442768197 -->
```
### 4. 身份规则
- 插件长期只认 `cardId`
- `noteId``cardId` 指向的远端目标
- 不再使用 `标题 + 行号 + 路径` 作为正式身份
- 不做无 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`
条件满足任一:
- 本地有 `noteId``rawBlockHash` 变化
- 本地有 `noteId``renderConfigHash` 变化
- 本地记录为 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 的一致性
## Recommended Implementation Order
### 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 能正确解析 `cardId``noteId`
- 非法 marker 报错
- 同一块多个 marker 报错
- marker 不进入正文
### 2. File Index
- include/exclude 过滤先于文件读取
- 文件未变化时整文件跳过
- 文件变化但卡片块未变化时,卡片跳过
- 标题块边界提取正确
### 3. Planner
-`noteId``cardId` 卡进入 `toCreate`
- `rawBlockHash` 变化进入 `toUpdate`
- `renderConfigHash` 变化进入 `toUpdate`
- 已消失卡进入 `toOrphan`
- pending write-back 卡进入 `toRewriteMarker``toUpdate`
### 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 执行层必须按类型批处理