fix(qa-group): recreate missing notes on unchanged sync

中文: 修复 QA Group 在本地 state 与正文 hash 看起来未变化时,如果 Anki 中对应 note 已丢失却不会自动补建的问题。现在同步会先校验 note 是否仍存在,缺失时自动重建并重写 GI。

English: Fix QA Group sync so missing Anki notes are recreated even when local state and block hashes look unchanged. Sync now verifies note existence first and self-heals by rebuilding the note and rewriting GI when needed.
This commit is contained in:
Dusk 2026-04-21 10:52:44 +08:00
parent c8b7915c40
commit 5023e8a4bb
2 changed files with 55 additions and 15 deletions

View file

@ -141,6 +141,48 @@ describe("QaGroupSyncService", () => {
expect(result.syncedGroupBlocks[0]?.groupId).toBe("group-1");
expect(result.syncedGroupBlocks[0]?.noteId).toBeDefined();
});
it("recreates a missing QA Group note even when the local state looks unchanged", async () => {
const ankiGateway = new FakeManualSyncAnkiGateway();
const vaultGateway = new FakeManualSyncVaultGateway();
const existingState = createStoredGroupBlockState();
const service = new QaGroupSyncService(
ankiGateway,
undefined,
undefined,
undefined,
() => 1234,
() => "group-x",
() => "item-new",
vaultGateway.createBacklink.bind(vaultGateway),
);
const result = await service.sync([
createIndexedGroupBlock({
noteId: 42,
groupId: "group-1",
rawBlockHash: existingState.rawBlockHash,
items: existingState.items.map(({ title, answer, ordinalInMarkdown }) => ({ title, answer, ordinalInMarkdown })),
}),
], {
...createEmptyPluginState(),
groupBlocks: {
"group-1": existingState,
},
}, createModule3Settings());
expect(result.created).toBe(1);
expect(result.updated).toBe(0);
expect(result.touchedSyncKeys).toContain("notes/example.md\u0000group\u00001\u0000hash-1");
expect(ankiGateway.addedNotes).toHaveLength(1);
expect(ankiGateway.addedNotes[0]?.fields).toMatchObject({
GroupId: "group-1",
S01_Q: "Alpha",
S03_Q: "Beta",
});
expect(result.markerWrites[0]?.noteId).toBe(result.syncedGroupBlocks[0]?.noteId);
expect(result.syncedGroupBlocks[0]?.noteId).not.toBe(42);
});
});
function createIndexedGroupBlock(overrides: Partial<IndexedGroupCardBlock> = {}): IndexedGroupCardBlock {

View file

@ -132,21 +132,19 @@ export class QaGroupSyncService {
let fieldsChanged = !stateUnchanged;
let deckChanged = recovered.stateRecord ? recovered.stateRecord.deck !== deck : false;
if (!stateUnchanged) {
existingNote ??= await this.tryLoadQaGroupNote(noteId, block);
if (!existingNote) {
noteId = await this.ankiGateway.addNote({
deckName: deck,
modelName: QA_GROUP_MODEL_NAME,
fields,
tags: [],
});
created += 1;
touchedSyncKeys.add(block.syncKey);
} else {
fieldsChanged = !haveEqualFields(existingNote.fields, fields);
deckChanged = !(existingNote.deckNames ?? []).includes(deck);
}
existingNote ??= await this.tryLoadQaGroupNote(noteId, block);
if (!existingNote) {
noteId = await this.ankiGateway.addNote({
deckName: deck,
modelName: QA_GROUP_MODEL_NAME,
fields,
tags: [],
});
created += 1;
touchedSyncKeys.add(block.syncKey);
} else if (!stateUnchanged) {
fieldsChanged = !haveEqualFields(existingNote.fields, fields);
deckChanged = !(existingNote.deckNames ?? []).includes(deck);
}
if (existingNote && (fieldsChanged || deckChanged)) {