From 4d65ba0b79e83c95e550043b95e0d8e36ee9318f Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 19:34:43 +0800 Subject: [PATCH] fix: include canvas revisions in service saves --- lib/canvas/canvas-save-request.ts | 40 +++++++++++++++++ services/canvas/index.ts | 28 +++++++++--- tests/routes/canvas-chat-helpers.test.ts | 55 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 lib/canvas/canvas-save-request.ts diff --git a/lib/canvas/canvas-save-request.ts b/lib/canvas/canvas-save-request.ts new file mode 100644 index 000000000..4693d3110 --- /dev/null +++ b/lib/canvas/canvas-save-request.ts @@ -0,0 +1,40 @@ +import type { CanvasState, CanvasWorkspaceType } from "@/types/canvas"; + +export type CanvasSaveRevisionOptions = { + baseCanvasRevision?: string | null; + baseCanvasUpdatedAt?: string | null; +}; + +function normalizeRevisionValue(value: unknown) { + return String(value || "").trim() || undefined; +} + +export function mergeCanvasSaveRevisionOptions(params: { + options?: CanvasSaveRevisionOptions; + fallbackOptions?: CanvasSaveRevisionOptions; +}): CanvasSaveRevisionOptions { + return { + baseCanvasRevision: + normalizeRevisionValue(params.options?.baseCanvasRevision) ?? + normalizeRevisionValue(params.fallbackOptions?.baseCanvasRevision), + baseCanvasUpdatedAt: + normalizeRevisionValue(params.options?.baseCanvasUpdatedAt) ?? + normalizeRevisionValue(params.fallbackOptions?.baseCanvasUpdatedAt), + }; +} + +export function buildCanvasSaveRequestBody(params: { + canvasData: CanvasState; + workspaceType?: CanvasWorkspaceType; + options?: CanvasSaveRevisionOptions; + fallbackOptions?: CanvasSaveRevisionOptions; +}) { + const revisionOptions = mergeCanvasSaveRevisionOptions({ + options: params.options, + fallbackOptions: params.fallbackOptions, + }); + + return params.workspaceType + ? { canvasData: params.canvasData, workspaceType: params.workspaceType, ...revisionOptions } + : { canvasData: params.canvasData, ...revisionOptions }; +} diff --git a/services/canvas/index.ts b/services/canvas/index.ts index 6d5e0a20c..21bb0e72c 100644 --- a/services/canvas/index.ts +++ b/services/canvas/index.ts @@ -9,12 +9,17 @@ import type { import type { VideoGenerationMode, VideoRatio, VideoResolution } from '@/types/ai'; import { fetchUploadWithRetry } from '@/lib/client/upload-with-retry'; import { requestLongTaskResult } from '@/lib/client/long-task'; +import { + buildCanvasSaveRequestBody, + type CanvasSaveRevisionOptions, +} from '@/lib/canvas/canvas-save-request'; import type { EnhanceProKreaModel, EnhanceProOutputFormat, EnhanceProSubjectDetection, } from '@/lib/enhance-pro/config'; import { createLogger } from '@/lib/observability/logger'; +import { useCanvasStore } from '@/store/canvas'; const log = createLogger("Canvas"); @@ -27,6 +32,15 @@ function logCatch( log.error(`${context} failed`, error, extra); } +function getCanvasSaveRevisionFallback(): CanvasSaveRevisionOptions { + if (typeof window === "undefined") return {}; + const { canvasRevision, canvasUpdatedAt } = useCanvasStore.getState(); + return { + baseCanvasRevision: canvasRevision, + baseCanvasUpdatedAt: canvasUpdatedAt, + }; +} + // API 响应类型 interface ApiResponse { success: boolean; @@ -196,16 +210,16 @@ export const canvasService = { id: string, canvasData: CanvasState, workspaceType?: CanvasWorkspaceType, - options?: { - baseCanvasRevision?: string | null; - baseCanvasUpdatedAt?: string | null; - } + options?: CanvasSaveRevisionOptions ): Promise { try { const requestBody = JSON.stringify( - workspaceType - ? { canvasData, workspaceType, ...options } - : { canvasData, ...options } + buildCanvasSaveRequestBody({ + canvasData, + workspaceType, + options, + fallbackOptions: getCanvasSaveRevisionFallback(), + }) ); const doSave = async () => fetch(`/api/canvas/${id}`, { diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 817e41095..195a77702 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -110,6 +110,10 @@ import { isStaleCanvasSaveBaseRevision, parseCanvasRevisionDate, } from "@/lib/canvas/canvas-save-conflict"; +import { + buildCanvasSaveRequestBody, + mergeCanvasSaveRevisionOptions, +} from "@/lib/canvas/canvas-save-request"; import { applyCanvasOperationPlanToCanvasData, CanvasAgentMutationError, @@ -257,6 +261,57 @@ test.describe("canvas chat pure helpers", () => { }); }); + test("builds canvas save requests with fallback revision guards", () => { + const canvasData = { + nodes: [], + edges: [], + selectedNodeIds: [], + viewport: { x: 0, y: 0, zoom: 1 }, + tool: "select" as const, + }; + + expect( + mergeCanvasSaveRevisionOptions({ + fallbackOptions: { + baseCanvasRevision: "2026-06-22T00:00:00.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }, + }) + ).toEqual({ + baseCanvasRevision: "2026-06-22T00:00:00.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }); + expect( + mergeCanvasSaveRevisionOptions({ + options: { + baseCanvasRevision: "2026-06-22T00:00:02.000Z", + }, + fallbackOptions: { + baseCanvasRevision: "2026-06-22T00:00:00.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }, + }) + ).toEqual({ + baseCanvasRevision: "2026-06-22T00:00:02.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }); + expect( + buildCanvasSaveRequestBody({ + canvasData, + workspaceType: "video", + fallbackOptions: { + baseCanvasRevision: "2026-06-22T00:00:00.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }, + }) + ).toMatchObject({ + canvasData, + workspaceType: "video", + baseCanvasRevision: "2026-06-22T00:00:00.000Z", + baseCanvasUpdatedAt: "2026-06-22T00:00:00.000Z", + }); + }); + const plannerContext: CanvasAgentContext = { version: 1, source: "canvas",