跳转到内容

用稳定笔记 ID 更新 Anki 牌组

为已经分发的 Anki 牌组发布修正或新增卡片。稳定笔记 ID 帮助 Anki Forge 和 Anki 识别已有笔记,而不是把它们当成新笔记;Anki 导入设置与较新的本地编辑仍会影响最终变化。

保持项目命名空间和笔记、模型、字段、模板键稳定。每个新版本都以上一版分发时的原始 APKG 为基准构建。包内包含完整的标识映射和修订信息,且这些信息采用带版本号的格式。请将原始包与对应的源码版本一起保存。

运行两个版本并比较

章节:运行两个版本并比较
终端窗口
cargo run --locked -p ankiforge --example docs_workflow -- target/docs-examples

这会写出 spanish-v1.apkg 和 spanish-v2.apkg,比较修改后的答案,并检查更新包的数量。可执行源码如下:

fn vocabulary(answer: &str) -> anyhow::Result<Project> {
let mut project = Project::new("docs-spanish-updates")?.default_deck("Spanish");
project.add("hola", Note::basic("hola", answer))?;
Ok(project)
}
fn updates(output: &Path) -> anyhow::Result<()> {
let previous = output.join("spanish-v1.apkg");
let next = output.join("spanish-v2.apkg");
vocabulary("hello")?.build(BuildOptions::to(&previous))?;
let project = vocabulary("hello; hi")?;
let comparison = project.compare(CompareOptions::against(&previous))?;
ensure!(comparison.policy().allows_publication());
let built = project.build(BuildOptions::to(&next).update_from(&previous))?;
verify(&built, 1, 1, 0)?;
ensure!(built.report().comparison().is_some());
Ok(())
}

即使发现项会阻止发布,compare 也可以成功完成。请分别读取 comparison.policy().allows_publication()、原始风险级别和证据。build(...update_from(...)) 执行相同分析;若策略阻止候选包,则在发布前返回 BuildError。

策略与检查限制

章节:策略与检查限制

这份完整程序选择更严格的阈值,并对比较和更新使用相同配置:

use ankiforge::{BuildOptions, Note, Project};
use ankiforge::build::InspectLimits;
use ankiforge::update::{CompareOptions, RiskLevel, UpdatePolicy};
fn main() -> anyhow::Result<()> {
let mut first = Project::new("policy-example")?;
first.add("cell", Note::basic("Cell?", "Unit of life"))?;
first.build(BuildOptions::to("policy-v1.apkg"))?;
let mut next = Project::new("policy-example")?;
next.add("cell", Note::basic("Cell?", "The basic unit of life"))?;
let mut limits = InspectLimits::default();
limits.max_collection_bytes = 1 << 30;
let policy = UpdatePolicy::default().fail_on(RiskLevel::Medium);
let comparison = next.compare(CompareOptions::against("policy-v1.apkg")
.inspect_limits(limits.clone()).update_policy(policy.clone()))?;
assert!(comparison.policy().allows_publication());
next.build(BuildOptions::to("policy-v2.apkg").update_from("policy-v1.apkg")
.inspect_limits(limits).update_policy(policy))?;
Ok(())
}

默认策略阻止 High 和 Critical 发现项。审阅特定类别后,调用方可有意选择 UpdatePolicy::default().allow(RiskCode::NoteRemoved)。它放行该类别中的所有发现项,同时保留原级别和证据;并不表示逐条批准指定笔记。没有匹配发现项的放行会产生警告;未知风险码被拒绝。不要自动放行比较返回的每一个码。

缺失或损坏的证据、命名空间不匹配、无效候选数据和检查限制都是硬错误,不是可放行的风险类别。分析无法完成时,比较返回错误及已完成的观察结果。对没有 update_from 的新建请求显式设置策略属于配置错误,与 setter 调用顺序无关。

包内包含 ankiforge-identity.json,格式为 ankiforge-identity-v1。它将模型或配置 ID、笔记 GUID、历史字段或模板槽位、卡片或遮罩序号、修订和媒体绑定到实际集合与载荷。Anki 重新导出不保留该证据,不能作为更新基线。缺失证据绝不会被静默重建。

显示名称和内容编辑保留身份键。内容未变时保留基线修订;内容改变时推进修订。字段或模板重排保留历史映射。退役身份继续保留,恢复相同键时可复用。笔记键不能移到另一模型,模型键也不能在普通与 Cloze 类型间改变。

客户端导入行为与发布检查

章节:客户端导入行为与发布检查

APKG 是分发包,不是删除同步。省略笔记、字段、模板或遮罩,不会删除学习者已有数据。字段或模板 schema 和排序字段变化属于高风险,因为 Anki 合并与导入设置会影响行为。Schema 变化可能复制模型,或在笔记内容比较前更新目标笔记的时间戳。

在支持的客户端测试首次导入以及导入已有集合的更新。使用目标合并与笔记更新设置,检查字段、GUID、卡片集合、本地编辑和已有调度。仅稳定身份不保证较新本地编辑被覆盖,也不保证已删除的历史被恢复。

将候选包写到与归档基线不同的路径。审阅后分发被接受的包,并将它保留为下一次发布的证据。所有权和发布事实见构建保证。

构建一次,审阅后发布

章节:构建一次,审阅后发布

此 API 已包含在公开 0.3.1 包中。

构建一次,检查报告,然后发布同一个候选文件。审阅后再运行一次普通构建会创建另一个候选包;预备发布使实际 APKG 始终与已审阅报告对应。

use ankiforge::{BuildOptions, Note, Project};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut project = Project::new("spanish")?.default_deck("Spanish");
project.add("es:hola", Note::basic("hola", "hello"))?;
project.build(BuildOptions::to("spanish-original.apkg"))?;
let mut next = Project::new("spanish")?.default_deck("Spanish");
next.add("es:hola", Note::basic("hola", "hello; hi"))?;
let prepared = next.prepare_publication(
BuildOptions::to("spanish-reviewed.apkg").update_from("spanish-original.apkg"),
)?;
println!("{:?}", prepared.report().comparison());
if prepared
.report()
.comparison()
.is_some_and(|report| report.policy().allows_publication())
{
let output = prepared.publish()?;
println!("{}", output.artifact().path().display());
}
Ok(())
}

在固定修订的源码目录中,把上面的完整程序保存为 anki_forge/examples/docs_publication.rs(本指南新增该示例;1199196 中未包含它),然后运行:

终端窗口
cargo run --locked -p ankiforge --example docs_publication

示例在仓库根目录写出 spanish-original.apkg,并在策略允许发布时写出 spanish-reviewed.apkg。预备阶段不发布到目标。批准分发前,应审阅发现项并测试客户端导入;此示例只在策略允许时自动发布这次简单内容编辑。基准测试未测量该流程。

参见构建生命周期、Rust API、Node API和 Python API。

查看 Markdown