From 5ad05efb58976f48deb7061ff260b39a96e7a891 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sat, 19 Sep 2026 08:43:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E7=94=BB=E5=B8=83=E5=88=A0?= =?UTF-8?q?=E9=99=A4=20-=20=E8=B7=B3=E8=BF=87=E5=85=B3=E8=81=94=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E9=A2=84=E5=8A=A0=E8=BD=BD=E4=B8=8E=E5=90=88=E5=90=8C?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/progress/pending-test.mdx | 7 +++ web/src/services/user-data-sync.ts | 4 +- web/test/canvas-remote-revision.test.ts | 51 ++++++++++++++++++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 9e3babab6..8d255be29 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,13 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 画布删除不再预加载关联素材 + +登录后的画布删除直接按 ID 请求服务端,不再先进入编辑加载流程,避免历史素材的 MIME 等字段校验阻塞删除。保留账号会话检查、服务端删除失败上抛及成功后清理本地缓存的顺序。 + +- [x] 26 项画布同步与素材合同专项测试通过;新增回归覆盖关联素材为 `image/*`、未缓存画布、重复 ID,以及批量删除部分失败后缓存与模拟服务端状态一致。 +- [ ] 在真实账号下从画布列表和编辑页删除包含异常 MIME 素材的画布,刷新确认已删除;自动化验证使用模拟 API,未删除实际账号数据。 + ## 站点皮肤、控件焦点与外观分类 主按钮填充改为主题参数统一管理,经典黑白采用黑白基础色与 115° 紫蓝渐变;复制主题后可分别编辑明暗模式的纯色/渐变、角度、常态/悬停/按下双色和文字色。创作页“开始创作”与前后台主按钮使用相同参数,危险、幽灵和禁用按钮不套用渐变。后台隔离用户工作台外壳与浮层样式。输入、选择、日期和搜索控件移除重复焦点阴影,保留键盘焦点与错误状态。站点及外观按品牌、登录页、欢迎页、SEO、页尾备案、皮肤六个 Tab 展示,页面草稿统一保存,欢迎页开关继续独立即时保存。 diff --git a/web/src/services/user-data-sync.ts b/web/src/services/user-data-sync.ts index ced9aaed0..a148f81d8 100644 --- a/web/src/services/user-data-sync.ts +++ b/web/src/services/user-data-sync.ts @@ -457,9 +457,7 @@ export async function deleteCanvasProjectsWithRemoteSync(ids: string[]) { const epoch = sessionEpoch; const projectIds = [...new Set(ids.map((id) => id.trim()).filter(Boolean))]; if (!projectIds.length) return; - if (incrementalSession) { - for (const id of projectIds) await loadCanvasProjectForEditing(id); - } + // 删除只依赖画布 ID,跳过编辑加载,避免关联素材的合同校验阻塞删除。 await withRemoteUserDataSyncExclusive(async () => { if (epoch !== sessionEpoch) throw new Error("账号已切换,请重新选择要删除的画布"); if (activeRemoteUserId) requireRemoteUserDataBaseline(); diff --git a/web/test/canvas-remote-revision.test.ts b/web/test/canvas-remote-revision.test.ts index faccce743..1dd63916d 100644 --- a/web/test/canvas-remote-revision.test.ts +++ b/web/test/canvas-remote-revision.test.ts @@ -4,7 +4,7 @@ import { apiClient } from "../src/services/api/request"; import { canvasContentHash } from "../src/lib/canvas/canvas-content"; import { rebaseCanvasProjects, parseCanvasStorageDocument } from "../src/lib/canvas/canvas-storage-revision"; import { readCanvasSyncDrafts } from "../src/services/canvas-sync-drafts"; -import { applyAgentCanvasPatches, refreshCanvasAfterAgent, initializeRemoteUserDataSession, installRemoteUserDataAutoSync, loadCanvasProjectForEditing, resetRemoteUserDataSync, saveRemoteUserDataNow, syncRemoteUserData } from "../src/services/user-data-sync"; +import { applyAgentCanvasPatches, deleteCanvasProjectsWithRemoteSync, refreshCanvasAfterAgent, initializeRemoteUserDataSession, installRemoteUserDataAutoSync, loadCanvasProjectForEditing, resetRemoteUserDataSync, saveRemoteUserDataNow, syncRemoteUserData } from "../src/services/user-data-sync"; import { createAgentCanvasSync } from "../src/services/agent-canvas-sync"; import { flushCanvasStorePersistence, useCanvasStore, type CanvasProject } from "../src/stores/canvas/use-canvas-store"; import { flushAssetStorePersistence, useAssetStore } from "../src/stores/use-asset-store"; @@ -22,6 +22,7 @@ let autoSave: (() => void) | undefined; let remote = new Map(); let requests: Array<{ method: string; id: string; project?: CanvasProject }> = []; let beforePut: (() => Promise) | undefined; +let deleteFailureId: string | undefined; function canvas(id = "canvas"): CanvasProject { return { @@ -52,6 +53,7 @@ beforeEach(async () => { requests = []; autoSave = undefined; beforePut = undefined; + deleteFailureId = undefined; remote = new Map([ ["canvas", canvas()], ["other", canvas("other")], @@ -82,7 +84,19 @@ beforeEach(async () => { let data: unknown; let status = 200; if (id === "snapshot") data = { projects: structuredClone([...remote.values()]), assets: [] }; - else if (id === "restore" && method === "post") { + else if (method === "delete") { + if (id === deleteFailureId) status = 403; + else { + remote.delete(id); + data = { id }; + } + } else if (id === "batch" && method === "post") { + data = { assets: [{ + id: "invalid-mime-asset", kind: "image", title: "历史素材", coverUrl: "", tags: [], + createdAt: "2026-09-01", updatedAt: "2026-09-01", + data: { dataUrl: "https://example.com/image.png", width: 100, height: 100, bytes: 1, mimeType: "image/*" }, + }] }; + } else if (id === "restore" && method === "post") { const current = remote.get("canvas")!; if (body.revision !== current.revision) status = 409; else { @@ -119,6 +133,39 @@ afterEach(async () => { else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow }); }); +test("deletion skips editing and invalid MIME assets, including an uncached canvas", async () => { + const project = canvas(); + project.nodes[0].metadata = { assetId: "invalid-mime-asset" }; + remote.set(project.id, project); + await initializeRemoteUserDataSession(scope); + await expect(loadCanvasProjectForEditing(project.id)).rejects.toThrow("具体 MIME"); + useCanvasStore.getState().deleteProjects(["other"]); + await flushCanvasStorePersistence(); + requests = []; + + await deleteCanvasProjectsWithRemoteSync([" canvas ", "canvas", "other"]); + + expect(requests.map(({ method, id }) => ({ method, id }))).toEqual([ + { method: "delete", id: "canvas" }, { method: "delete", id: "other" }, + ]); + expect(remote.size).toBe(0); + expect(useCanvasStore.getState().projects).toEqual([]); + await useCanvasStore.persist.rehydrate(); + expect(useCanvasStore.getState().projects).toEqual([]); +}); + +test("batch deletion retains failed canvases and persists successful deletions", async () => { + await initializeRemoteUserDataSession(scope); + deleteFailureId = "other"; + + await expect(deleteCanvasProjectsWithRemoteSync(["canvas", "other"])).rejects.toThrow(); + + expect([...remote.keys()]).toEqual(["other"]); + expect(useCanvasStore.getState().projects.map((project) => project.id)).toEqual(["other"]); + await useCanvasStore.persist.rehydrate(); + expect(useCanvasStore.getState().projects.map((project) => project.id)).toEqual(["other"]); +}); + test("stale viewport, no-op restore and open do not submit old content", async () => { const old = useCanvasStore.getState().openProject("canvas")!; remote.set("canvas", { ...addNode(canvas(), "afternoon-video"), revision: 2 });