From 5023e8a4bb4e67547fb8039b9f0c391aa36af5ce Mon Sep 17 00:00:00 2001 From: Dusk Date: Tue, 21 Apr 2026 10:52:44 +0800 Subject: [PATCH] fix(qa-group): recreate missing notes on unchanged sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文: 修复 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. --- .../services/QaGroupSyncService.test.ts | 42 +++++++++++++++++++ .../services/QaGroupSyncService.ts | 28 ++++++------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/application/services/QaGroupSyncService.test.ts b/src/application/services/QaGroupSyncService.test.ts index a9eb87d..5f2d962 100644 --- a/src/application/services/QaGroupSyncService.test.ts +++ b/src/application/services/QaGroupSyncService.test.ts @@ -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 { diff --git a/src/application/services/QaGroupSyncService.ts b/src/application/services/QaGroupSyncService.ts index f36df06..7614dd2 100644 --- a/src/application/services/QaGroupSyncService.ts +++ b/src/application/services/QaGroupSyncService.ts @@ -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)) {