mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 17:10:28 +00:00
- 将 marker、state、writeback 的 durable identity 改为 noteId - Switch marker, state, and writeback durable identity to noteId - 支持满足标题规则的旧 ID 卡片重接进并按当前 renderer 重渲染 - Reconnect legacy ID cards that match heading rules and rerender them with the current renderer - 补充 schema migration、测试、构建验证与插件同步 - Add schema migration, tests, build verification, and plugin sync
99 lines
6 KiB
Markdown
99 lines
6 KiB
Markdown
# 兼容性触点报告
|
||
|
||
## 分析边界
|
||
|
||
- 只评估当前 manual-sync 主链,不把旧 syncRegistry 主链当成最终方案。
|
||
- 证据来源限定为四类真值:仓库代码、用户提供的旧 Markdown 样本、用户提供的新 Markdown 样本、实时 AnkiConnect 返回。
|
||
- 目标问题只有一个:当前主链能否安全接管由 Obsidian to Anki 生成的旧卡,并继续后续同步。
|
||
|
||
## 结论先行
|
||
|
||
当前主链不能直接接管旧卡。核心阻塞点不是 note model,也不是 deck,而是 identity bootstrap。
|
||
|
||
- 旧样本只提供 legacy noteId marker:`<!--ID: 1760624596099-->`
|
||
- 新主链只识别 AHS marker:`<!-- AHS:card=ahs_xxx note=1776515511477 -->`
|
||
- 当前状态层以 `cardId -> noteId` 为主键,而不是以 `noteId` 为主键
|
||
- 因此,旧文件在首次进入当前主链时,既拿不到 `marker.cardId`,也拿不到 `pluginState.cards[cardId]`,最后只能被当成新卡创建
|
||
|
||
这意味着“直接接管”会造成重复建卡,而不是认领旧卡。
|
||
|
||
## 关键触点一览
|
||
|
||
| 触点 | 当前行为 | 兼容性影响 | 结论 |
|
||
| --- | --- | --- | --- |
|
||
| `src/domain/manual-sync/services/CardMarkerService.ts` | `isCandidate()` 和 `parse()` 只认 `<!-- AHS:card=... note=... -->` | 旧样本 `<!--ID: ...-->` 对主链是不可见 marker | 不兼容 |
|
||
| `src/domain/manual-sync/services/CardIndexingService.ts` 的 `extractMarker()` | 只会剥离 AHS marker;没识别出的 legacy marker 会被保留在正文里 | 旧 marker 不会转成 `noteId`,还会污染 `bodyMarkdown` 与 `rawBlockHash` | 不兼容 |
|
||
| `src/domain/manual-sync/services/CardIndexingService.ts` 的 `resolveIdentity()` | 解析顺序是 `marker.cardId -> pendingWriteBack -> knownCards(filePath, rawBlockHash) -> 生成新 cardId` | 旧文件首次扫描时三条已有身份路径都不存在,必然生成新 `cardId` | 不兼容 |
|
||
| `src/domain/manual-sync/services/DiffPlannerService.ts` | `resolvedNoteId` 为空就进入 `toCreate` | 旧卡不会被“接管更新”,而会被“新建重复卡” | 不兼容 |
|
||
| `src/domain/manual-sync/entities/PluginState.ts` | `cards` 以 `cardId` 为 key,状态里没有 legacy marker 元数据位 | 当前状态容器无法直接承接“只有 noteId、没有 cardId”的旧卡 | 不兼容 |
|
||
| `src/infrastructure/persistence/DataJsonPluginStateRepository.ts` | 只读取 `snapshot.pluginState`,不会消费 `syncRegistry` | 即便 data.json 里还有旧 `syncRegistry` 记录,manual-sync 也不会拿来认领旧卡 | 不兼容 |
|
||
| `src/application/services/FileIndexerService.ts` | 只把 `pluginState.cards` 和 `pendingWriteBack` 喂给 indexer | 旧卡若没先迁入 `pluginState`,主链没有任何第二身份来源 | 不兼容 |
|
||
| `src/application/services/MarkdownWriteBackService.ts` | 只会写入新的 AHS marker | 如果直接跑在旧文件上,会在 legacy marker 后再插入一个 AHS marker,而不是替换旧 marker | 有风险 |
|
||
| `src/domain/manual-sync/services/DeckExtractionService.ts` + `DeckNormalizationService.ts` | 会提取 `TARGET DECK`,并保留 deck segment 原文,只做分隔符规范化 | 旧样本里的 `[[anki背诵]]::[[《城市居住区规划设计标准》图解2021年]]` 能被原样保留 | 兼容 |
|
||
| `src/domain/manual-sync/services/ManualCardRenderer.ts` | 当前渲染会在背面追加 `Open in Obsidian` backlink | 即使完成接管,后续同步也会把旧卡字段逐步归一到新渲染样式 | 条件兼容 |
|
||
| `src/application/services/HeadingSyncMarkerService.ts` + `src/domain/card/services/CardExtractionService.ts` | 旧链路支持的是 `<!-- AHS:123 -->` 这种 noteId-only marker | 这和本次真实旧样本的 `<!--ID: ...-->` 不是同一种格式,也不在 current manual-sync 主链里 | 不能直接复用 |
|
||
|
||
## 代码触点拆解
|
||
|
||
### 1. Marker 语义已经换代
|
||
|
||
当前 manual-sync 的 marker 不是“把 noteId 嵌进去就行”,而是显式要求:
|
||
|
||
- 一个稳定的 `cardId`
|
||
- 一个可选的 `noteId`
|
||
- 一个确定的 AHS marker 字面量
|
||
|
||
旧样本只有 `noteId`,没有 `cardId`。这不是缺一个字段那么简单,而是整个主链的身份主键已经从 `noteId` 切成了 `cardId`。
|
||
|
||
### 2. 当前主链不会利用旧 `syncRegistry`
|
||
|
||
实时 data.json 里仍然同时存在:
|
||
|
||
- `syncRegistry.records[].identityMode = "embedded-note-id"`
|
||
- `pluginState.files / pluginState.cards`
|
||
|
||
但 `DataJsonPluginStateRepository` 只加载 `pluginState`,manual-sync 完全不看 `syncRegistry`。所以旧状态即便还在 data.json 里,也不会自动帮助旧卡进入新主链。
|
||
|
||
### 3. Deck 不是阻塞点
|
||
|
||
这是这次分析里最重要的反向结论之一。
|
||
|
||
- 旧样本 Markdown 明确声明了 `TARGET DECK: [[anki背诵]]::[[《城市居住区规划设计标准》图解2021年]]`
|
||
- 旧样本 live card 的 `cardsInfo.deckName` 也正是这个 wikilink deck 字符串
|
||
- 当前 `DeckNormalizationService` 不会剥掉 `[[...]]`,只会规范 `/`、`\` 和 `::`
|
||
|
||
因此,旧 deck 的字面值兼容是成立的。真正不兼容的是 identity,不是 deck。
|
||
|
||
### 4. Note model 与字段映射也不是阻塞点
|
||
|
||
实时样本显示:
|
||
|
||
- 旧样本 live note:`modelName = 问答题`,字段为 `正面 / 背面`
|
||
- 新样本 live note:`modelName = 问答题`,字段为 `正面 / 背面`
|
||
- 当前插件设置:`basic:问答题 -> titleField=正面, bodyField=背面`
|
||
|
||
所以旧卡与新卡在 Anki 侧的数据模型是兼容的,后续接管不需要做 note type 迁移。
|
||
|
||
## 最终触点判断
|
||
|
||
### 已兼容
|
||
|
||
- 旧 deck 字符串可继续沿用
|
||
- 旧 note model / 字段映射与当前设置兼容
|
||
- 旧 noteId 在 Anki 中真实存在,具备可迁移基础
|
||
|
||
### 未兼容
|
||
|
||
- legacy marker 识别
|
||
- 首次 identity 认领
|
||
- 旧状态向 `pluginState` 的引导
|
||
- 直接运行时的防重复保障
|
||
|
||
### 因此得到的主结论
|
||
|
||
当前主链不属于“可直接接管”。
|
||
|
||
它属于“可以接管,但必须先做一次迁移写回”:
|
||
|
||
- 先把旧 `noteId` 身份翻译成新 `cardId + noteId` 身份
|
||
- 再把 Markdown marker 和 `pluginState` 一次性对齐到 current manual-sync 语义
|