26 KiB
Obsidian Anki 新插件设计备忘(DDD 讨论稿)
文档目的
这不是一个立即开工的实现文档,而是一份面向未来的设计备忘。
目标是把当前已经讨论清楚的方向、边界、约束、DDD 拆分方式、领域模型、同步策略、风险点与后续演进路径完整记录下来,避免之后重新开始时又回到模糊状态。
本文档面向的插件目标是:
- 重新做一个新的 Obsidian -> Anki 插件
- 吸收
AnkiHelperForCoding与obsidian-to-anki-plugin-forCoding的优点 - 保留“好用的做卡与同步能力”
- 去掉过重、过泛化、过难维护的部分
- 明确以 Obsidian 为唯一真源
- 明确只做单向同步:
Obsidian -> Anki
库一:/Users/panxiaorong/Documents/ObsidianPluginCode/AnkiHelperForCoding 库二:/Users/panxiaorong/Documents/ObsidianPluginCode/obsidian-to-anki-plugin-forCoding
背景与总体判断
现有两个仓库各有优点,但都不适合作为新插件的直接基础。
1. AnkiHelperForCoding 的优点
- 工作流聚焦
- 功能边界相对清楚
- 对标题式做卡思路明确
- 有轻量的批处理和增量处理思路
- 贴近 Obsidian 内的实际使用方式
2. obsidian-to-anki-plugin-forCoding 的优点
- 真正具备较完整的 AnkiConnect 同步链路
- 能创建牌组、添加卡片、更新卡片、处理媒体
- 有较成熟的 Markdown/媒体/公式/链接转 Anki 可显示内容的能力
- 有全库扫描、同步状态、hash、批量处理等工程经验
3. 为什么不能简单合并
obsidian-to-anki-plugin-forCoding 的问题不是“代码不好”,而是产品定位已经变成“通用同步平台”,而不是“聚焦标题式做卡的新插件”。
它变重主要来自以下六层:
- 输入语法越来越多
- 配置系统越来越像框架
- 同步系统承担了太多职责
- 格式转换链较长
- 全库扫描和过滤系统复杂化
- TypeScript 之外还有 Python 旁路与更重的工程负担
新插件的方向不是复制它,而是:
- 保留核心能力
- 重写核心结构
- 主动缩掉产品边界
当前已确定的产品方向
下面是当前讨论中已经较明确的决定。
1. 输入语法只做“标题段落式”
只支持标题段落式卡片,不做旧插件里的通用语法平台。
规则初步确定为:
- 问答题默认使用四级标题
H4 - 填空题默认使用五级标题
H5 - 两者都允许用户修改级别
- 一个标题对应一个卡片块
- 卡片内容范围是:
- 从该标题下一行开始
- 到下一个“同级或更高标题”之前结束
这意味着第一版明确不做:
- 自定义正则语法
- Inline Note
- Begin/End Note 块语法
- 任意用户定义的卡片语法体系
2. 配置系统不做大平台
配置系统要保留必要能力,但不再做成旧插件那种开放式框架。
当前已保留的配置方向:
- 问答题标题级别
- 填空题标题级别
TARGET DECKqa note typecloze note type- 默认 deck
- 扫描范围(包含/排除文件夹)
- 是否添加 Obsidian 文件回链
- 是否为 Cloze 启用高亮转空
明确不做:
- folder -> deck 映射表
- folder -> tag 映射表
- 通用 syntax setting 集合
- 大量用户可编程语法选项
- 复杂 glob 平台
3. 同步系统保留核心能力
同步系统虽然重,但其中很多能力是值得保留的。
建议保留的能力:
- 创建 deck
- 新增 note
- 更新已有 note
- 媒体上传
- 批量扫描
- 同步状态持久化
- 增量同步
- orphan 检测
暂不直接做:
- Anki -> Obsidian 反向回写
- 双向同步
- 高复杂度冲突解决
- 第一版直接自动删除 orphan 对应的 Anki 卡片
4. 格式保真是关键目标
新插件的一个核心目标是:
在 Obsidian 里看到的内容,进入 Anki 后也尽量保持可用、可读、可复习。
因此需要保留并重构以下能力:
- Markdown -> HTML
- 数学公式
- 代码块与行内代码尽量不被破坏
- 高亮
- Wikilink
- 图片 embed
- 音频 embed
- Obsidian 回链
5. 需要做全局扫描,但边界要收紧
当前讨论结果是:
- 需要全局扫描
- 但只支持用户指定包含文件夹和排除文件夹
- 不做复杂 glob 规则平台
推荐规则:
includeFolders为空时:扫描整个 vaultincludeFolders非空时:只扫描指定文件夹excludeFolders始终生效- 仅扫描 Markdown 文件
6. 工程上不保留 Python 旁路
新插件只保留 TypeScript / Obsidian 插件实现。
明确不做:
- Python CLI
- Python 安装与依赖管理
- 双实现栈同步维护
核心产品原则
这是新插件最关键的一条原则:
Obsidian 是唯一真源,Anki 是同步目标。
这条原则一旦确定,后续很多设计就会变得稳定:
- Obsidian 中的内容决定 Anki 中的内容
- Anki 中手改正文不会回写
- 下次同步时,Anki 中相关字段会被 Obsidian 内容覆盖
- 不需要做双向冲突合并
- 不需要把 Anki HTML 反解析成 Markdown
- 不需要做“Anki 改动写回标题块”的定位逻辑
为什么不做双向同步
虽然理论上可以做 Anki -> Obsidian 回写,但这不适合第一版,也可能永远不值得做。
原因包括:
- 无法轻易决定谁是最终真源
- Anki 中修改的可能是字段、标签、牌组、模型,而不只是正文
- Obsidian 的标题段落结构并不是数据库记录
- 回写时必须重新定位标题块
- 标题改名、移动、层级变化都会让回写不稳定
- HTML/媒体/链接从 Anki 回写为 Markdown 时存在信息损失
因此当前明确决定:
- 只做
Obsidian -> Anki - 不做双向同步
- 不做 Anki 正文回写
DDD 总体视角
这个插件如果用 DDD 来看,核心不是 UI,也不是 AnkiConnect API,而是:
把 Obsidian 中的标题段落块建模为“可同步卡片”,并稳定地同步到 Anki。
因此真正的核心领域不是:
- 设置页
- 命令面板
- HTTP 请求
而是:
- 卡片识别
- 卡片身份稳定性
- 卡片渲染
- 同步计划
- 同步状态演进
统一语言(Ubiquitous Language)
下面这些术语建议在整个项目里统一使用,避免后续沟通与实现混乱。
Source Note
Obsidian 中的一个 Markdown 文件。
Heading Block
一个标题以及它所管辖的正文范围。
Card Draft
从标题块中解析出来、但还没有完成渲染和同步决策的卡片草稿。
Card
领域中的标准化卡片对象,是同步的基本单位。
Card Type
卡片类型,目前只包括:
basiccloze
Deck Target
卡片最终应进入的 Anki 牌组。
Note Model
Anki 中的 note type / model,例如:
BasicCloze
Rendered Fields
已经转换成可发送给 Anki 的字段内容。
Sync Record
本地保存的同步记录,用于维护 cardKey -> noteId 映射及同步状态。
Sync Scope
本次扫描或同步覆盖的文件范围。
Orphan Card
本地同步记录里曾存在,但本次扫描中已经找不到来源卡片的记录。
Source Hash
源卡片内容计算得到的 hash,用于判断是否变化。
Bounded Context 拆分
建议把系统拆成四个主要上下文。
1. Card Authoring Context
负责从 Obsidian 内容里识别卡片。
职责
- 判断哪些标题会被视为卡片
- 判断这是问答题还是填空题
- 提取标题文本
- 提取正文块范围
- 解析文件级
TARGET DECK - 产出
CardDraft
不负责
- AnkiConnect 通信
- noteId 管理
- orphan 检测
- HTTP
- UI
这里的核心问题
- 标题块切分是否准确
- 标题级别策略是否稳定
- 内容范围是否与用户认知一致
2. Rendering Context
负责把卡片内容转成 Anki 可接收的字段内容。
职责
- Markdown -> HTML
- 数学公式处理
- 图片与音频处理
- Wikilink 处理
- Obsidian 回链处理
- 高亮与 Cloze 转换
- 生成最终字段值
不负责
- 扫描哪些文件
- 判断新增/更新/删除
- 生成 noteId
这里的核心问题
- 内容是否保真
- 代码块、公式、高亮等边界是否被破坏
- 媒体是否可上传并正确显示
3. Sync Context
负责决定“该如何把卡片同步到 Anki”。
职责
- 读取历史同步记录
- 判断新增与更新
- 检测 orphan
- 生成同步计划
- 执行同步动作
- 更新本地同步记录
不负责
- 标题块如何识别
- markdown 如何渲染
- 设置 UI
这里的核心问题
- 同一张卡如何稳定识别
- 什么情况下是更新
- 什么情况下视为 orphan
- 删除是否自动进行
4. Integration Context
负责和外部系统打交道。
对 Obsidian
- Vault 文件访问
- MetadataCache
- 当前活动文件
- 命令注册
- 设置页
对 Anki
- AnkiConnect 调用
- 创建 deck
- 新增 note
- 更新 note
- 删除 note
- 上传媒体
角色定位
这是防腐层,不应该污染领域模型。
Core Domain / Supporting / Generic
为了防止后续又掉回“大而杂”,建议从战略设计上明确区分:
Core Domain
- 标题块识别
- 卡片身份生成
- 同步计划生成
Supporting Subdomains
- Markdown/媒体/链接渲染
- 扫描范围过滤
- orphan 检测
- 同步状态存储
Generic Subdomains
- 设置页
- 命令注册
- Notice 提示
- JSON 持久化
- HTTP 请求
聚合设计建议
不建议在这个项目里设计过多聚合。两个主要聚合已经足够。
聚合一:SourceFile Aggregate
聚合根:SourceFile
包含内容
- 文件路径
- 文件内容
- metadata 快照
- 文件级 deck 信息
- 解析出的标题块
- 解析出的卡片草稿
职责
- 在单文件内完成标题块识别
- 保证一个文件中的卡片识别逻辑一致
- 处理文件级 deck 覆盖
不应该负责
- Anki noteId
- 全局同步状态
- orphan 判断
聚合二:SyncRegistry Aggregate
聚合根:SyncRegistry
包含内容
cardKey -> SyncRecord映射- 历史 noteId
- source hash
- 上次同步时间
- orphan 标记
职责
- 管理卡片同步状态生命周期
- 帮助判断新增、更新、orphan
- 与数据持久化边界配合
不应该负责
- markdown 渲染
- 标题块切分
实体与值对象建议
这里要严格区分 Entity 与 Value Object。
Entity
SourceFile
身份由 filePath 决定。
Card
身份由 cardKey 决定。
SyncRecord
身份由 cardKey 决定,也可内部持有 noteId。
Value Object
建议作为值对象的内容:
CardKeyDeckNameNoteModelNameHeadingLevelHeadingTextHeadingBlockRangeCardTypeRenderedFieldsSourceLocationContentHashSyncScope
Card 领域对象建议
建议把领域中的 Card 明确定义为“标准化同步对象”,而不是随便几段文本。
推荐形状如下:
type Card = {
key: CardKey;
source: SourceLocation;
type: CardType;
heading: string;
bodyMarkdown: string;
deck: DeckName;
noteModel: NoteModelName;
tags: string[];
fields: Record<string, string>;
contentHash: ContentHash;
};
字段意义
key- 这张卡在领域中的稳定身份
source- 它来自哪个文件、哪个标题位置
typebasic或cloze
heading- 标题文本
bodyMarkdown- 原始正文
deck- 最终牌组
noteModel- 最终 note type
fields- 已经渲染好的 Anki 字段
contentHash- 用于判断内容是否变化
一个重要原则
bodyMarkdown 与 fields 不应混为一谈。
前者是原始领域内容,后者是渲染后的派生内容。二者都应有清晰位置。
Card Draft 建议
为了让解析与渲染解耦,建议先有 CardDraft。
type CardDraft = {
source: SourceLocation;
heading: string;
headingLevel: number;
type: "basic" | "cloze";
bodyMarkdown: string;
deckHint?: string;
};
然后由渲染层把 CardDraft 转成正式 Card。
这样做的好处是:
- 标题识别逻辑和渲染逻辑解耦
- deck 解析、字段映射可以单独演进
- 未来更容易测试
Card Type 语义建议
当前只保留两类卡。
Basic
语义建议:
- 标题 -> Front
- 正文 -> Back
Cloze
语义建议:
- 正文 -> Cloze 主字段
- 标题不进入主 cloze 字段
- 标题可选进入额外字段,比如
Extra或上下文字段
原因:
- 如果标题也拼进 Cloze 主字段,会干扰 Cloze 语义
- 标题更适合做上下文或辅助定位信息
Note Model 与 Deck 的区别
必须明确区分:
Note Model
决定 Anki 里字段结构,例如:
BasicBasic (and reversed card)Cloze
Deck
决定卡片进入哪个牌组,例如:
EnglishEnglish::WordsHistory::Chapter1
它们是两个完全不同的维度。
因此配置上建议分别建模,而不是混淆。
Card Identity(cardKey)策略
这是整个系统最敏感的问题之一。
目标
cardKey 需要尽量满足:
- 同一文件内唯一
- 同一轮扫描内唯一
- 在小幅编辑下尽量稳定
- 不依赖 Anki noteId
第一版推荐策略
第一版可以先用较保守的组合:
cardKey = hash(
filePath +
headingLevel +
headingText +
blockStartLine +
cardType
)
优点
- 实现简单
- 同文件内冲突概率低
- 足够支撑第一版
缺点
- 标题改名会影响 key
- 前面插入内容导致行号变化会影响 key
- 文件移动会影响 key
未来可升级方向
未来如果需要更稳定,可以考虑:
- 持久化块级 anchor
- 在标题块内植入不可见标识
- 使用内部 source id 映射
DDD 视角下的建议
不要把 key 生成逻辑散落在代码中,而应抽成一个领域策略:
CardIdentityPolicy
它属于业务规则,不只是技术细节。
Deck 解析策略
当前建议的 deck 决策顺序:
- 文件中显式配置的
TARGET DECK - 插件默认 deck
第一版不做:
- folder -> deck 映射
- 基于路径的 deck 自动推导
- 标题级单独 deck 覆盖
TARGET DECK 的定位
作为保留下来的最小 syntax setting:
- 只保留
Target Deck Line - 不保留 Begin/End Note 等其它语法开关
标题级别策略
当前建议默认值:
qaHeadingLevel = 4clozeHeadingLevel = 5
需要保证的业务规则
- 两个级别都允许用户修改
- 两个级别不应相同
- 如果相同,应在配置层校验并阻止
扫描范围模型
虽然保留全局扫描,但需要主动收缩边界。
推荐配置
type ScanScopeConfig = {
includeFolders: string[];
excludeFolders: string[];
};
推荐行为
includeFolders为空:扫描全部 Markdown 文件includeFolders非空:只扫描这些文件夹下的 Markdown 文件excludeFolders永远对候选集做排除
第一版不做
- 任意 glob
- 单文件模式表达式
- ignore 语法 DSL
- 复杂优先级覆盖规则
Sync Record 建议
本地同步状态建议保存为独立模型。
type SyncRecord = {
cardKey: string;
noteId: number;
filePath: string;
sourceHash: string;
lastSyncedAt: number;
orphan: boolean;
};
字段说明
cardKey- 领域卡片身份
noteId- Anki 中对应 note 的 id
filePath- 便于追踪来源与调试
sourceHash- 判断是否发生变化
lastSyncedAt- 便于调试和排错
orphan- 表示当前扫描未再找到对应来源
为什么建议先不做自动删除
虽然同步系统中“删除失效卡片”看起来合理,但第一版不建议直接做自动删除。
原因
- 标题改名会被误判为删除旧卡、创建新卡
- 文件移动可能导致 key 变化
- 扫描范围变窄时可能导致误判 orphan
- 用户切换设置后,候选卡集合可能临时变化
- 删除是破坏性动作,风险高
第一版建议
- 支持新增
- 支持更新
- 支持 orphan 检测
- 支持手动清理 orphan 的未来扩展点
- 默认不自动删 Anki 卡
建议的领域服务
以下领域服务是比较关键的。
1. CardExtractionService
输入
SourceFileHeadingPolicy
输出
CardDraft[]
负责
- 找到有效标题
- 切分正文块
- 判断
basic/cloze
2. DeckResolutionService
输入
- 文件级
TARGET DECK - 插件默认 deck
- 卡片配置
输出
DeckName
负责
- 决定卡片最终牌组
3. CardIdentityService
输入
SourceLocation- 标题信息
- 卡片类型
输出
CardKey
负责
- 生成稳定 identity
4. CardRenderingService
输入
CardDraft
输出
Card
负责
- 生成最终字段
- 处理 Markdown/媒体/链接/公式
5. SyncPlanningService
输入
- 当前扫描得到的
Card[] - 历史
SyncRecord[]
输出
SyncPlan
负责
- 判断 add
- 判断 update
- 判断 orphan
这个服务是整个同步系统的关键,必须单独存在,不应散落在命令或 UI 层中。
Sync Plan 建议
推荐把同步规划显式建模出来。
type SyncPlan = {
toCreateDecks: string[];
toAdd: Card[];
toUpdate: Array<{ card: Card; noteId: number }>;
toMarkOrphan: SyncRecord[];
};
第一版动作集合建议
第一版先只做:
createDeckaddupdatemarkOrphan
不做:
delete
Application Service / Use Case 建议
DDD 中应用服务负责“编排用例”,不承担核心业务规则。
1. ScanAndPlanSyncUseCase
流程
- 读取扫描范围
- 收集候选文件
- 从文件解析
CardDraft - 渲染为
Card - 读取历史
SyncRecord - 生成
SyncPlan
2. ExecuteSyncPlanUseCase
流程
- 创建缺失 deck
- 上传媒体
- 新增 cards
- 更新 cards
- 更新同步状态
- 标记 orphan
3. SyncCurrentFileUseCase
流程
只对当前文件执行完整扫描与同步流程。
4. SyncVaultUseCase
流程
对扫描范围内的文件执行完整扫描与同步流程。
5. PreviewSyncUseCase(未来可选)
流程
只生成同步计划,不真正执行。
价值
- 调试
- 风险确认
- 后续做 UI 预览
Repository 建议
Repository 主要是持久化边界,不是业务逻辑本身。
1. SyncRegistryRepository
负责
- 读取同步记录
- 保存同步记录
- upsert 卡片同步状态
- 标记 orphan
可能的存储位置
- 插件
data.json
2. PluginConfigRepository
负责
- 读取插件配置
- 输出面向领域的配置对象
注意
不要让领域层直接消费 Obsidian 原始 Setting UI 数据结构。
3. ScanScopeRepository(可选)
如果未来扫描配置较复杂,可以单独抽象。
Anti-Corruption Layer(防腐层)建议
这个项目非常需要防腐层,否则外部 API 会把领域污染掉。
对 Obsidian 的防腐层
不要让领域层直接依赖:
AppTFileCachedMetadata
建议由基础设施层封装为:
ObsidianVaultGatewayObsidianMetadataGatewayObsidianWorkspaceGateway
然后向应用层提供干净 DTO。
对 AnkiConnect 的防腐层
不要让领域层直接拼 action JSON。
建议封装为:
AnkiGateway
对外提供的方法应是业务语义,而不是裸 invoke("addNote"):
ensureDeckExistsaddNoteupdateNotedeleteNotesstoreMedia
领域不变量建议
这些不变量建议尽早确立。
卡片识别相关
- 一个标题块最多生成一张卡
- 一张卡必须属于且只属于一种
CardType - 同一次扫描内
cardKey不可重复
配置相关
- 问答题级别和填空题级别不能相同
- 每张卡必须解析出 deck
- 每张卡必须解析出 note model
Basic / Cloze 相关
basic必须有 front/back 字段映射cloze必须映射到支持 cloze 的 note model
同步相关
- 一个
cardKey只能对应一个活动同步记录 - 一个
noteId不应对应多个不同cardKey - orphan 默认不能自动删除
建议的目录结构
如果未来真的开工,建议从一开始就按边界建目录,而不是继续堆在 main.ts 和工具函数里。
src/
domain/
card/
entities/
value-objects/
services/
policies/
sync/
entities/
value-objects/
services/
shared/
value-objects/
application/
use-cases/
dto/
infrastructure/
obsidian/
anki/
persistence/
presentation/
commands/
settings/
notices/
可以进一步细化为:
src/domain/card/entities/Card.ts
src/domain/card/entities/SourceFile.ts
src/domain/card/value-objects/CardKey.ts
src/domain/card/value-objects/DeckName.ts
src/domain/card/value-objects/SourceLocation.ts
src/domain/card/services/CardExtractionService.ts
src/domain/card/services/CardRenderingService.ts
src/domain/card/policies/CardIdentityPolicy.ts
src/domain/sync/entities/SyncRecord.ts
src/domain/sync/entities/SyncRegistry.ts
src/domain/sync/services/SyncPlanningService.ts
src/application/use-cases/SyncCurrentFileUseCase.ts
src/application/use-cases/SyncVaultUseCase.ts
src/application/use-cases/ScanAndPlanSyncUseCase.ts
src/application/use-cases/ExecuteSyncPlanUseCase.ts
src/infrastructure/obsidian/ObsidianVaultGateway.ts
src/infrastructure/obsidian/ObsidianMetadataGateway.ts
src/infrastructure/anki/AnkiConnectGateway.ts
src/infrastructure/persistence/DataJsonSyncRegistryRepository.ts
src/presentation/commands/registerCommands.ts
src/presentation/settings/PluginSettingTab.ts
src/presentation/notices/NoticeService.ts
需要保留和重写的部分
这是从两个旧仓库中提炼出的结论。
可以借鉴的方向
来自 AnkiHelperForCoding
- 聚焦标题级工作流
- 批处理与增量处理思路
- 与 Obsidian 工作流贴近的命令设计
来自 obsidian-to-anki-plugin-forCoding
- AnkiConnect 通信层思路
- Markdown/链接/媒体/公式转 Anki 的处理经验
- 媒体上传与 deck 创建
- 同步状态管理经验
必须重写的部分
- 领域模型
- 卡片解析流程
- 同步计划生成方式
- 配置模型
- 扫描范围模型
- 旧仓库里与通用语法平台强耦合的部分
第一版建议做什么
虽然当前阶段不是立即开发,但为了将来回头时不迷失,仍建议记录“第一版的可交付边界”。
第一版建议包含
- 标题段落式卡片解析
H4问答题、H5填空题默认配置- 标题级别可配置
Basic/Cloze两类卡片qaNoteType/clozeNoteType可配置- 默认 deck
- 文件级
TARGET DECK - 全局扫描
- include/exclude 文件夹
- 增量同步
- deck 创建
- note 新增
- note 更新
- 媒体上传
- markdown/html 转换
- Obsidian 回链
- orphan 检测(不自动删)
第一版明确不做什么
- 自定义正则系统
- inline note
- begin/end note 语法体系
- folder -> deck 映射
- folder -> tag 映射
- 双向同步
- Anki -> Obsidian 回写
- Python CLI
- 自动删除 orphan 对应 Anki 卡片
- 复杂 glob 系统
风险点与尚未完全拍板的问题
虽然方向已经清楚,但仍有一些点需要在未来真正开工前再确认。
1. cardKey 稳定性
当前推荐方案足以起步,但并不完美。
需要后续根据实际使用情况决定:
- 是否接受标题改名导致新卡
- 是否接受文件移动导致重建
- 是否需要更稳定的内部 source id
2. Cloze 标题是否写入字段
当前倾向是:
- Cloze 主字段只放正文
- 标题作为上下文信息另放
但这一点未来仍可在使用体验上再验证。
3. orphan 的后续处理策略
当前明确是:
- 先检测
- 先标记
- 不自动删
未来是否要加“手动清理 orphan”,可以再评估。
4. 媒体处理边界
第一版建议保留图片和音频支持,但需要注意:
- 不同格式是否都支持
- 文件定位是否稳定
- 上传后是否做重复检测
5. 渲染保真度边界
需要明确接受一个现实:
- 不可能一开始就 100% 复刻 Obsidian 显示效果
更现实的目标是:
- 保证常用内容结构可读、可复习
- 保证不轻易破坏数学、代码和媒体
未来扩展方向
以下方向可以保留想法,但不应现在就进第一版边界。
1. 预览同步计划
在真正同步前,让用户看到:
- 将新增几张
- 将更新几张
- 哪些是 orphan
2. 当前标题同步
相较于当前文件同步,进一步细化到只同步当前标题块。
3. 手动 orphan 清理
用户明确确认后,删除失效 Anki 卡片。
4. 更稳定的 card identity
引入块级 anchor 或内部 source id。
5. 更多上下文字段
例如:
- 文件名
- 标题路径
- Obsidian 链接
- 上级标题
结论
当前讨论已经形成一个较稳定的产品方向:
- 这是一个新的、聚焦“标题段落式做卡”的 Obsidian -> Anki 插件
- 它借鉴旧项目经验,但不继承旧项目的通用框架定位
- 它明确以 Obsidian 为唯一真源
- 它只做单向同步
- 它保留强同步能力、保留较高内容保真度、保留全局扫描
- 它主动砍掉通用语法平台、folder 映射、Python 旁路、双向同步与自动删除等高复杂度设计
从 DDD 视角看,真正应被认真设计的核心只有三件事:
- 如何把标题块稳定识别为领域卡片
- 如何生成尽量稳定的卡片身份
- 如何在不失控的前提下生成和执行同步计划
只要这三点做对,未来无论是否真的开工,这个插件的方向都不会再轻易漂移。
建议的下一步(未来恢复此 idea 时)
当未来决定真正开始做这个插件时,建议按下面顺序继续:
- 先把
Card、CardDraft、SyncRecord的精确定义拍板 - 再拍板
cardKey策略 - 再定义
Basic/Cloze的字段映射策略 - 再实现
CardExtractionService - 再实现
CardRenderingService - 再实现
SyncPlanningService - 最后接 Obsidian UI 与 AnkiConnect 集成
在真正动手写代码之前,都不要跳过前面三个建模步骤。