diff --git a/apps/web/src/lib/desktop-repository.test.mjs b/apps/web/src/lib/desktop-repository.test.mjs index c7ff3984..72800660 100644 --- a/apps/web/src/lib/desktop-repository.test.mjs +++ b/apps/web/src/lib/desktop-repository.test.mjs @@ -13,6 +13,7 @@ globalThis.window = { sidecarRequest: async (method, params) => { lastRequest = { method, params }; if (method === "memo.get") { + const filename = params.memoId === "memo_wechat" ? "微信图片_202609231723_1.jpg" : "screenshot.webp"; return { memo: { id: params.memoId, @@ -20,10 +21,10 @@ globalThis.window = { type: "doc", content: [{ type: "image", - attrs: { alt: "screenshot.webp", src: "edgeever-staged://stage_dead" }, + attrs: { alt: filename, src: "edgeever-staged://stage_dead" }, }], }, - contentMarkdown: "![screenshot.webp](edgeever-staged://stage_dead)\n\n", + contentMarkdown: `![${filename}](edgeever-staged://stage_dead)\n\n`, }, }; } @@ -35,8 +36,14 @@ globalThis.window = { kind: "image", filename: "screenshot.webp", url: "/api/v1/resources/res_shot/blob", + }, { + id: "res_wechat", + memoId: "memo_wechat", + kind: "image", + filename: "微信图片_202609231723_1.webp", + url: "/api/v1/resources/res_wechat/blob", }], - summary: { totalCount: 1, totalBytes: 1, imageCount: 1, attachmentCount: 0 }, + summary: { totalCount: 2, totalBytes: 2, imageCount: 2, attachmentCount: 0 }, }; } if (method === "memo.update") { @@ -132,4 +139,10 @@ describe("desktop repository memo reads", () => { expect(result.memo.contentMarkdown).not.toContain("edgeever-staged://"); expect(result.memo.contentJson.content[0].attrs.src).toBe("edgeever-resource://resource/res_shot"); }); + + test("recovers an imported WeChat image after jpg compression changed its extension", async () => { + const result = await createDesktopRepository().getMemo("memo_wechat"); + expect(result.memo.contentJson.content[0].attrs.src).toBe("edgeever-resource://resource/res_wechat"); + expect(result.memo.contentMarkdown).toContain("edgeever-resource://resource/res_wechat"); + }); }); diff --git a/apps/web/src/lib/staged-resource-repair.test.mjs b/apps/web/src/lib/staged-resource-repair.test.mjs index caabbc3c..db62c7be 100644 --- a/apps/web/src/lib/staged-resource-repair.test.mjs +++ b/apps/web/src/lib/staged-resource-repair.test.mjs @@ -81,6 +81,36 @@ describe("staged resource repair", () => { .toEqual([]); }); + test("recovers a WeChat image whose uploaded filename changed from jpg to webp", () => { + const memo = { + contentJson: { type: "doc", content: [{ type: "image", attrs: { + alt: "微信图片_202609231723_1.jpg", + src: "edgeever-staged://stage_missing", + width: 35, + } }] }, + contentMarkdown: "![微信图片_202609231723_1.jpg](edgeever-staged://stage_missing)", + }; + const resourceUrl = "/api/v1/resources/res_wechat/blob"; + const repaired = repairMemoStagedResourceUrls(memo, [{ + url: resourceUrl, + filename: "微信图片_202609231723_1.webp", + kind: "image", + }]); + expect(repaired.contentJson.content[0].attrs).toEqual({ + alt: "微信图片_202609231723_1.jpg", + src: resourceUrl, + width: 35, + }); + expect(repaired.contentMarkdown).toBe(`![微信图片_202609231723_1.jpg](${resourceUrl})`); + }); + + test("does not guess between resources with the same filename stem", () => { + expect(findMatchingMemoResource([ + { url: "/api/v1/resources/res_a/blob", filename: "image.jpg", kind: "image" }, + { url: "/api/v1/resources/res_b/blob", filename: "image.webp", kind: "image" }, + ], "image.png", "image")).toBeNull(); + }); + test("does not guess when a memo has several images and the placeholder has no filename", () => { expect(findMatchingMemoResource([ screenshotResource, diff --git a/apps/web/src/lib/staged-resource-repair.ts b/apps/web/src/lib/staged-resource-repair.ts index d06cbdf3..8ea79892 100644 --- a/apps/web/src/lib/staged-resource-repair.ts +++ b/apps/web/src/lib/staged-resource-repair.ts @@ -106,7 +106,13 @@ export const findMatchingMemoResource = ( const normalizedFilename = filename?.trim(); if (normalizedFilename) { const named = pool.filter((resource) => resource.filename === normalizedFilename); - return named[0] ?? null; + if (named.length === 1) return named[0]; + if (named.length > 1) return null; + // Image compression may change only the extension (for example, .jpg to + // .webp) while the editor keeps the original filename as the image alt. + const stem = normalizedFilename.replace(/\.[^.]+$/, ""); + const sameStem = pool.filter((resource) => resource.filename?.replace(/\.[^.]+$/, "") === stem); + return sameStem.length === 1 ? sameStem[0] : null; } if (pool.length === 1) return pool[0]; return null;