把 Anki 26.05 Rust Core 接到 HarmonyOS:记得闪卡 jidecards 全部源码架构详解(欢迎共建)(鸿蒙端闪卡软件开发)(软件开发至落地的思路分享)
把 Anki 26.05 Rust Core 接到 HarmonyOS:记得闪卡 jidecards 全部源码架构详解(欢迎共建)(鸿蒙端闪卡软件开发)(软件开发实现思路分享)

一、它已经不只是“能复习”的最小客户端
1. 学习与调度
- 从 Anki 队列获取队首卡片,展示问题与答案,按 Again / Hard / Good / Easy 四档评分。
- 支持 FSRS 开关、目标记忆保持率、牌组选项和重新调度。
- 支持撤销、埋藏、暂停、恢复、删除当前卡片和学习完成页。
- 支持普通问答、完形填空、输入答案和图片遮罩。
[sound:...]由 HarmonyOS 原生播放器播放;[anki:tts]由 CoreSpeechKit 朗读。- 支持底部工具栏、可拖拽吸附的浮动工具栏、物理键盘快捷键,以及实验模式下的 Tap Zones 快速答题。
2. 牌组、制卡与浏览器
- 层级牌组的新建、重命名、删除、排序、隐藏、别名和背景图裁剪。
- 新建普通、填空、输入答案、图片遮罩等笔记;字段数量与顺序来自真实笔记类型。
- Cards / Notes 两种浏览模式,Anki 搜索语法、牌组树、标签树、已保存搜索和 AND/OR 组合。
- 编辑字段与标签,并实时预览卡片正反面。
- 批量改牌组、设置标志、暂停、恢复、删除、设置到期日、重新定位新卡、更改笔记类型、查找替换。
- 标签的重命名、删除、补全和搜索。
浏览页约 2450 行,同时调用搜索、笔记、笔记类型、卡片、牌组、调度、统计、标签和配置服务,复杂度主要来自 Anki 业务语义。
3. 统计与桌面服务卡片
统计页实际挂载了 13 类区块:今日计数、未来预测、年历热力图、复习量、卡片状态、小时分布、难度、间隔、稳定度、真实保持率、记忆率、回答按钮和新增卡片。
其中稳定度、可提取性等数据依赖 FSRS;页面支持按牌组筛选、近一年/全部范围以及图表偏好。项目还实现了一个 2×4 桌面服务卡片,把首页的 8 页学习摘要放到桌面,点击后通过 jidecards://stats DeepLink 直接进入统计页。
4. 同步、迁移与维护
- AnkiWeb 登录、同步状态检查、普通集合同步、媒体同步、全量上传/下载和中止同步。
- 自定义同步端点与自定义 CA 证书。
.apkg牌组合并导入/导出。.colpkg全量个人数据备份与恢复。- 媒体检查、缺失/未引用文件定位、回收站恢复与永久清理。
- 数据库检查、空卡、重复笔记、未使用标签、笔记类型管理。
默认云端目录目前配置了 6 组公开 APKG:四六级、高考、中考、AI 机器学习、IT 计算机和中国法律。大文件由 HarmonyOS 文件传输代理下载,包含进度、无进度超时、临时文件和 ZIP 签名校验,完成后进入正常 APKG 导入链路。
5. HarmonyOS 侧的原生体验
- 浅色、深色、跟随系统三种外观模式,蓝、绿、紫、青、橙、金、灰 7 套颜色主题。
- 中文和英文两套资源;当前两边各 1327 个字符串 key,名称完全对齐。
- 多条定时学习提醒,使用 HarmonyOS 代理提醒能力。
- 沉浸式窗口与动态安全区适配,覆盖旋转、折叠屏展开/折叠和分屏场景。
- 前后台切换时刷新桌面卡片和学习提醒;桌面数据刷新使用短时任务,避免应用进入后台后异步任务被立即挂起。
二、真正的核心:ArkUI 不重新实现 Anki
主调用链如下:
ArkUI 页面/组件
→ backend/* 领域 Service
→ 后端会话
→ 后端客户端
→ libjidecards.so(Node-API C++)
→ rsharmony(Rust C ABI)
→ Anki 26.05 Rust Core
1. ArkTS Service 只做领域调用和 protobuf 编解码
例如 调度器服务.ts 暴露的是“获取队首卡片”“提交评分”“埋藏或暂停”“设置到期日”,而不是一个任意 runRpc()。UI 不需要知道服务号和 protobuf 字段,Agent 更拿不到裸 RPC。
服务号和方法号集中在 backend/服务索引.ts。Anki 26.05 的后端分派使用固定编号,例如:
export const 服务号 = {
后端同步: 1,
后端集合: 3,
后端卡片: 5,
后端牌组: 7,
后端调度器: 13,
后端卡片渲染: 27,
后端搜索: 29,
后端导入导出: 39,
后端媒体: 41,
后端统计: 43
} as const;
这意味着升级 Anki 上游不能只改版本号。服务编号、方法编号、proto 字段和生成代码都要重新核对,否则最危险的结果不是“编译失败”,而是请求被送到错误的方法。
2. C++ Node-API 层只做异步桥接
native_module.cpp 对 ArkTS 只导出三个函数:
openBackend(init: Uint8Array): number
runMethodRaw(handle, service, method, input): Promise<Uint8Array>
closeBackend(handle): void
runMethodRaw() 把 ArkTS 的 Uint8Array 复制到原生任务,放入 napi_async_work,完成后再解析 Promise。C++ 层不理解卡片、牌组、FSRS,也不拼业务错误文案。
3. Rust FFI 负责句柄、内存和 panic 边界
rsharmony 用全局注册表为每个 Anki Backend 分配非零句柄;每个实例放在 Arc<Mutex<...>> 中,因此同一 Backend 的调用会串行化,不同句柄彼此独立。
FFI 返回的是显式状态码和拥有所有权的 AnkiBuffer:
0 OK
1 INVALID_ARGUMENT
2 HANDLE_NOT_FOUND
3 BACKEND_ERROR
4 NATIVE_FATAL
所有进入 Anki Backend 的调用都包在 catch_unwind() 中。Rust panic、锁中毒、后端 protobuf 错误分别映射到不同状态,panic 不会穿过 C ABI 把 ArkTS 进程带走。C++ 拿到字节后复制,再调用 anki_buffer_free() 归还 Rust 分配的内存。
三、为什么学习页不自己计算 FSRS
Anki 的 SchedulingState 是包含 New、Learning、Review、Relearning、Filtered 等分支的深层 protobuf oneof。前端如果把它完整解码,再按自己的类型重编码,很容易在 Anki 升级时丢掉不认识的字段或 oneof 分支。
jidecards 的做法是把五个状态当作不透明字节:
export interface SchedulingStatesRaw {
current: Uint8Array;
again: Uint8Array;
hard: Uint8Array;
good: Uint8Array;
easy: Uint8Array;
}
评分时只选择对应的一份 newState,与当前状态一起原样回传:
const states = this.当前卡片.states;
const newStates = [states.again, states.hard, states.good, states.easy];
await this.调度器服务实例.提交评分({
cardId: this.当前卡片.cardId,
currentState: states.current,
newState: newStates[rating],
rating,
answeredAtMillis: Date.now(),
millisecondsTaken: Date.now() - this.展示时刻毫秒
});
连四个按钮上显示的“10 分钟”“3 天”也不是前端估算,而是把同一组状态交给后端的 DescribeNextStates。
结果是:
- FSRS / 传统调度器都以 Anki Core 为唯一事实来源,前端不复制调度规则。
- 评分记录包含真实作答时间和耗时。
- 上游新增状态字段时,旧前端仍能保持字节级透传。
四、卡片不是纯文本:渲染、媒体、TTS 与输入答案
Anki Core 先渲染 question / answer 节点流和模板 CSS,学习卡片HTML构建器.ets 再组装完整 HTML,交给 HarmonyOS Web 组件显示。
本地媒体没有开放 file://。构建器把相对资源改写到一个不存在于公网的锚点:
export const 媒体基地址 = 'https://jidecards-media.local/';
Web 请求这个地址时,学习页的 onInterceptRequest 再从应用沙箱的 collection.media/ 读取文件,按扩展名设置 MIME。这样既能让 HTML 正常引用图片,又不用给 Web 组件裸露整个沙箱路径。
音频也没有留给 Web 组件自行处理:
- 后端从卡片 HTML 中提取
[sound:...]和[anki:tts]。 - 本地音频进入串行原生播放队列。
- TTS 项交给 CoreSpeechKit。
- 音频或 TTS 提取失败时降级为静音,不阻断复习。
输入答案则走另一条兼容链路:从后端渲染结果识别 [[type:...]] 标记,翻面时读取真实字段;如果是 Cloze,先按当前模板序号提取对应填空,再由纯函数拼写比对器生成字符级 diff HTML。
图片遮罩使用 canvas 绘制。目前“切换遮罩”按钮因 Web 事件转发稳定性问题被隐藏,但底层 toggle 和重绘逻辑仍保留。
五、集合生命周期:导出一次,为什么会影响后续调用
Anki Backend 不是无状态 RPC 服务。它内部持有已打开的 collection,所以 ArkTS 侧专门做了一个单例 后端会话,状态只有三种:
closed → ready → collectionClosed → ready
并发调用 确保已打开() 会共享同一个 opening Promise,避免同时创建多个原生句柄。打开失败会关闭半初始化句柄,允许下次重试。
Anki 的 export_collection_package 会通过 guard.take() 消费当前 collection。导出文件成功后,Backend 内部已经不再持有它。如果 ArkTS 还认为状态是 ready,下一次调用就会得到 CollectionNotOpen。
所以导出完整集合后,代码会:
- 把本地状态标记为
collectionClosed,不再多调一次 Close。 - 重新打开 collection。
- 即使重开失败,也不覆盖已经成功生成的导出结果。
恢复 COLPKG 的流程:
- 先要求 UI 二次确认。
- 把传入 URI 流式复制到沙箱临时文件。
- 关闭 collection。
- 复制
collection.anki2、collection.mdb和整个collection.media作为安全副本。 - 调 Anki Core 导入新集合。
- 成功后重开并删除副本;失败则恢复副本、重开旧集合,再抛出原始错误。
六、同步:让 Anki Core 处理协议,UI 只编排状态
AnkiWeb 的网络请求由 Anki Rust Core 发出,ArkTS 侧没有另写一套同步协议。同步服务.ts 只包裹登录、状态、集合、媒体、全量上传/下载和中止等方法。
真正的流程判断被放在无平台依赖的纯函数模块 同步流程.ts:
SyncStatus
├─ NO_CHANGES → 结束
├─ NORMAL_SYNC → 普通集合同步
└─ FULL_SYNC → 进入全量同步确认
SyncCollection
├─ NO_CHANGES / NORMAL_SYNC → 完成
├─ FULL_DOWNLOAD → 确认全量下载
├─ FULL_UPLOAD → 确认全量上传
└─ 未知值 → 保守按 FULL_SYNC 处理
集合与媒体状态分开管理,服务端下发 newEndpoint 时会保留原 hkey 和超时设置,只替换端点。错误优先依据后端 BackendError.kind 分类为鉴权、网络或其他,不依赖可能已经本地化的错误字符串。
七、源码里的 AI Agent,为什么不是“让模型直接改数据库”
当前 2.3.3 已把所有可见 Agent 入口统一关在 SHOW_AI_AGENT_CHANNELS=false 后面,所以用户安装当前发布包时看不到 AI 制卡、学习页 AI 改卡、浏览器批量 AI 改卡和提供商设置。
Agent 使用受控执行边界:
用户请求
→ Provider(DeepSeek / OpenAI / 自定义 Responses 接口)
→ 有界 AgentRunner
→ 语义工具注册表
→ 只读结果或 ChangeDraft
→ 用户确认
→ AgentDraftExecutor
→ 既有牌组/笔记/卡片/媒体 Service
模型可见的是 search_cards、get_note_context、create_flashcards、propose_update_notes 这类语义工具,不存在 SQLite、Shell、任意文件或裸后端 RPC。
工具风险分三层:
| 风险 | 例子 | 实际行为 |
|---|---|---|
| 只读 | 搜索卡片、读取笔记上下文、读取统计、搜索 Wikimedia 图片 | 可执行,但只能扩大“可读范围” |
| 普通写 | 新建闪卡、修改字段/标签、移动卡片 | 只生成 ChangeDraft,不立即写入 |
| 高风险 | 删除卡片/笔记/牌组/笔记类型、修改模板 CSS、更换笔记类型 | 只生成高风险草稿,要求两级确认 |
安全机制不是只靠提示词:
AgentScope记录本轮真实发现的卡片、笔记、牌组和笔记类型 ID,模型不能凭空构造一个 ID 去修改。AgentRunner默认最多请求 Provider 8 次、调用工具 16 次;重复 tool call id 和连续失败会被截断。- 模型生成的写操作只能到达草稿注册表,注册表没有提交方法。
- 普通写入需要绑定草稿的一次性确认令牌;高风险写入需要两个不同级别的令牌。
- 确认令牌默认 120 秒过期,只能使用一次。
- 草稿准备时检查一次
before基线,真正执行前再检查一次;确认期间数据被其他操作改过,就把草稿标记为conflict并停止写入。 - 图片候选只接受当前 Scope 内由 Wikimedia 搜索返回的 candidateId;媒体写入失败会把已写图片移入回收站做补偿。
- Provider API Key 存在 HarmonyOS Asset Store,而不是明文 Preferences。

重新开放 Agent 不能只改布尔值,还要针对发布版本回归 Provider 请求、流式响应、图片、取消、高风险写入和商店合规;已通过的主流程必须保持稳定,边界与组合场景继续补齐。
八、项目结构:第一次贡献从哪里进
jidecards/
├─ AppScope/ # 包名、版本、应用级资源
├─ entry/src/main/ets/
│ ├─ pages/ # 首页、学习、浏览、统计、设置、制卡
│ ├─ components/ # 通用组件、浏览器、统计、设置、Agent UI
│ ├─ stores/ # 页面/面板状态机
│ ├─ model/ # 纯逻辑、偏好、主题、同步决策、Agent 契约
│ ├─ backend/ # Anki 领域 Service 与会话
│ ├─ proto/ # 项目自有 protobuf 读写与消息编解码
│ ├─ formability/ # 桌面服务卡片 Ability
│ └─ widget/ # 桌面卡片页面
├─ native/
│ ├─ napi_bridge/ # C++ Node-API 桥
│ └─ rsharmony/ # Rust C ABI、句柄注册表与 Anki Core 适配
├─ tools/ # 构建、诊断、补丁与测试脚本
├─ tools/tests/ # Node 契约/纯逻辑/protobuf 测试
├─ hosting/ # 云端牌组与公告目录
└─ third_party/anki/ # 本地准备的锁定 Anki 源码,不进 Git
定位问题可按这个顺序:
界面现象
→ 对应 page/component
→ 找它调用的 backend Service
→ 查看服务号与消息 codec
→ 必要时再进入 Node-API / Rust / Anki 上游
页面持有 UI 状态,Service 不持有 UI 状态,原生桥不持有业务逻辑;定位问题时不必先改 Rust Core 或拆新接口。
九、如何本地构建
环境基线来自构建脚本,而不是旧计划文档:
- Node.js 18+
- DevEco Studio 6.1.0.860
- HarmonyOS SDK 6.1.0.105,Compatible API 21、Compile/Target API 23
- Rust 1.92.0
protoc、cargo-zigbuild、zig
克隆主仓库并准备锁定的 Anki 26.05:
git clone https://github.com/wuweiyouzuoju/jidecards-anki-harmonyos.git
cd jidecards-anki-harmonyos
git clone https://github.com/ankitects/anki.git third_party/anki
git -C third_party/anki checkout --detach e64c6b1
git -C third_party/anki rev-parse --short=7 HEAD
ohpm install
npm run doctor
npm test
npm run build:app
build:app 先编译 Rust 的 aarch64-unknown-linux-ohos 与 x86_64-unknown-linux-ohos 产物,再由 Hvigor 组装 HAP。脚本会幂等应用 tools/patches/anki-compact-import-log.patch,避免大型 APKG 的导入日志额外保留全部字段。
真机调试请始终覆盖安装:
hdc -t <connect-key> install -r <signed-hap>
不要为了省事执行 uninstall。应用数据库和媒体都在沙箱目录,卸载会把真实学习数据一起清掉。
十、验证现状:主链路已上真机,边界覆盖仍要继续补齐
项目已完成核心业务全链路真机验证并通过华为应用市场上架审核。不同设备型号、系统小版本、折叠与分屏、网络中断时机、超大牌组、异常数据、复杂模板和快速操作仍会形成大量边界组合:
核心业务主路径已经完成真机验证并成功上架;当前测试覆盖能够支撑正式使用,但不代表所有边界条件均已穷尽,后续版本仍需持续增加异常路径、设备组合和回归样本。
本次代码阅读另执行了三组检查:
npm run doctor
→ 当前机器上的 Node / Git / DevEco / Java / Rust / Cargo / Clang / CMake / Ninja / Hvigor 通过
npm test
→ tests 788, pass 788, fail 0
cargo test -p jidecards_core --no-default-features
→ Rust FFI 单元测试 4 条 + registry 集成测试 3 条,全部通过
Node 用例覆盖 protobuf、纯函数、源码契约和 Agent 安全边界,是既有真机验证之外的自动化防回归网,不是真机端到端测试。
tools\build-native.ps1 -Target host-test
该入口在本次审读机器上因缺少 cargo-zigbuild 未启动,但 npm run doctor 仍返回“toolchain is ready”,说明诊断脚本没有覆盖构建脚本的全部依赖。这不否定开发与上架阶段已经完成的真机构建和验收,也是一个边界明确的待改进项。
十一、现在最需要哪些贡献
当前主分支提交主要来自同一位作者,下一步需要更多人共同维护。
1. 构建与 CI
- 给
doctor.mjs补齐protoc、cargo-zigbuild、zig检查,让“诊断通过”真正意味着构建入口可执行。 - 增加带
anki-core的主机测试或可复现缓存,缩短第一次构建时间。 - 完善 arm64 真机与 x86_64 模拟器的发布前验证清单。
2. Anki 上游兼容
- 为服务号、方法号和 protobuf codec 增加自动核对工具,降低升级 Anki 时错配的风险。
- 补充真实 APKG/COLPKG 样本的导入导出回归。
- 验证复杂模板、Cloze、输入答案、图片遮罩、音视频和 MathJax 的兼容性。
3. HarmonyOS 真机体验
- 折叠屏、平板、2in1、分屏和物理键盘回归。
- Web 组件里的图片遮罩交互、手势冲突和焦点问题。
- TTS、AVPlayer、后台短时任务、提醒和桌面服务卡片在不同设备上的稳定性。
- 无障碍、字体缩放、对比度和大数据量列表性能。
4. 浏览器与统计
- 对照 Anki 桌面端补齐搜索、列配置、批量操作和卡片预览边界。
- 为大牌组、深层牌组树、海量标签和长字段做性能压测。
- 继续校对统计口径,避免图表“看起来合理”但与 Anki Core 数据含义不一致。
5. Agent 的可重复版本回归
- 把实体手机上的 DeepSeek / OpenAI / 自定义 Responses 接口验收固化成可重复的版本回归流程。
- 流式 UTF-8、取消、重试、截断续写和来源展示。
- 普通草稿、高风险双确认、冲突检测、部分成功重试与媒体补偿。
- 在不绕过
AgentScope、ChangeDraft和AgentDraftExecutor的前提下扩展工具。 - (Agent能力为了过审暂时隐藏了入口,后续会设置开发者调试入口)
6. 低门槛贡献
- 补测试样本、复现步骤和真机日志。
- 修正文案与中英资源,但必须保持两套 key 一致。
- 改进构建错误提示和新贡献者上手流程。
- 对截图、CSDN 教程、视频演示和使用反馈做整理。
(以上建议来自codex,实际情况需实际判断)
(AI友好项目,欢迎使用AI参与)
十二、参与方式
仓库地址:https://github.com/wuweiyouzuoju/jidecards-anki-harmonyos
https://gitcode.com/cheng_rui/jidecards-anki-harmonyos
你可以直接:
- Fork 仓库,先跑
npm test。 - 从一个边界明确的问题开始,提交 Issue 或 Pull Request。
- PR 中说明改了哪条数据流、验证了什么、哪些真机场景尚未验证。
- 想先讨论需求或真机问题,也可以加入项目 Q 群
项目采用 AGPL-3.0-or-later。jidecards 是独立第三方项目,与 Ankitects、AnkiWeb、AnkiDroid 无隶属或背书关系;Anki Rust Core 的版权仍归其原作者与贡献者所有。
熟悉 ArkTS、ArkUI、Rust、FFI、protobuf、Anki/FSRS,或能提供 HarmonyOS 真机测试,都可以直接参与。目标是在不重写成熟调度内核的前提下,继续完善这个原生、可维护、兼容 Anki 数据生态的 HarmonyOS 客户端。
更多推荐




所有评论(0)