fix: include canvas revisions in service saves

This commit is contained in:
134 2026-06-23 19:34:43 +08:00
parent 733d6338c2
commit 4d65ba0b79
3 changed files with 116 additions and 7 deletions

View File

@ -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 };
}

View File

@ -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<T = any> {
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<ApiResponse> {
try {
const requestBody = JSON.stringify(
workspaceType
? { canvasData, workspaceType, ...options }
: { canvasData, ...options }
buildCanvasSaveRequestBody({
canvasData,
workspaceType,
options,
fallbackOptions: getCanvasSaveRevisionFallback(),
})
);
const doSave = async () =>
fetch(`/api/canvas/${id}`, {

View File

@ -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",