fix: tighten canvas operation protocol

This commit is contained in:
134 2026-06-23 22:49:06 +08:00
parent 64ba6a6f29
commit d5ac630d01
3 changed files with 64 additions and 3 deletions

View File

@ -1,6 +1,13 @@
import { z } from "zod";
import type { CanvasOperationPlan } from "@/types/canvas/agent-protocol";
const httpMediaUrlSchema = z
.string()
.url()
.refine((url) => /^https?:\/\//i.test(url), {
message: "仅支持 http/https 媒体 URL",
});
export const canvasAgentOperationPlanSchema = z
.object({
version: z.literal(1),
@ -89,7 +96,7 @@ export const canvasAgentOperationPlanSchema = z
media: z
.object({
type: z.enum(["image", "video"]),
url: z.string().url(),
url: httpMediaUrlSchema,
assetId: z.string().uuid().optional(),
width: z.number().positive().optional(),
height: z.number().positive().optional(),
@ -104,7 +111,7 @@ export const canvasAgentOperationPlanSchema = z
z.object({
mediaItemId: z.string().min(1).max(160),
type: z.enum(["image", "video"]),
url: z.string().url(),
url: httpMediaUrlSchema,
assetId: z.string().uuid().optional(),
width: z.number().positive().optional(),
height: z.number().positive().optional(),

View File

@ -75,10 +75,14 @@ export async function handleCanvasAgentOperationRouteRequest(params: {
},
};
}
const operationPlan: CanvasOperationPlan = {
...body.operationPlan,
projectId: planProjectId || params.projectId,
};
const result = await params.applyOperation({
userId,
projectId: params.projectId,
plan: body.operationPlan,
plan: operationPlan,
});
return {

View File

@ -324,6 +324,30 @@ test.describe("canvas agent operation request schema", () => {
],
})
).toContain("operationPlan.mediaItems.0.url");
expect(
issuePathsFor({
operation: "insert_beside",
anchorNodeId: "image-1",
mediaItems: [
{
...buildMediaItem(),
url: "ftp://example.com/generated.png",
},
],
})
).toContain("operationPlan.mediaItems.0.url");
expect(
issuePathsFor({
operation: "append_revision",
sourceNodeIds: ["image-1"],
media: {
type: "image",
url: "file:///tmp/generated.png",
},
})
).toContain("operationPlan.media.url");
});
});
@ -391,6 +415,7 @@ test.describe("canvas agent operation route handler", () => {
const calls: Array<{
userId: string;
projectId: string;
planProjectId?: string;
operationId: string;
}> = [];
const expectedResult = buildRouteSuccessResult();
@ -408,6 +433,7 @@ test.describe("canvas agent operation route handler", () => {
calls.push({
userId: input.userId,
projectId: input.projectId,
planProjectId: input.plan.projectId,
operationId: input.plan.operationId,
});
return expectedResult;
@ -423,11 +449,35 @@ test.describe("canvas agent operation route handler", () => {
{
userId: "user-1",
projectId,
planProjectId: projectId,
operationId: "message-1:canvas-agent:test",
},
]);
});
test("fills missing operation plan project id from route project id", async () => {
let appliedPlanProjectId = "";
const response = await handleCanvasAgentOperationRouteRequest({
userId: "user-1",
projectId,
rawBody: buildRouteRequestBody({
projectId: undefined,
operation: "insert_beside",
anchorNodeId: "image-1",
mediaItems: [buildMediaItem(1)],
}),
features: enabledRouteFeatures,
applyOperation: async (input) => {
appliedPlanProjectId = String(input.plan.projectId || "");
return buildRouteSuccessResult();
},
});
expect(response.status).toBe(200);
expect(appliedPlanProjectId).toBe(projectId);
});
test("rejects operation plans for a different project before applying", async () => {
let operationApplied = false;