fix: keep canvas annotation operations idempotent

This commit is contained in:
134 2026-06-23 14:49:37 +08:00
parent 909f1c506c
commit c0550988e0
2 changed files with 71 additions and 0 deletions

View File

@ -786,6 +786,13 @@ function applyAddAnnotationOperation(params: {
const existingAnnotations = Array.isArray(metadata.annotations)
? metadata.annotations.filter((item) => item && typeof item === "object")
: [];
const hasExistingOperationAnnotation = existingAnnotations.some(
(item) =>
(item as Record<string, unknown>).canvasAgentOperationId ===
params.plan.operationId
);
if (hasExistingOperationAnnotation) return;
const nextAnnotations = annotations.map((annotation, index) => ({
...annotation,
id:

View File

@ -688,6 +688,70 @@ test.describe("canvas agent operation mutation contract", () => {
]);
});
test("adds annotation metadata to media nodes without duplicating retries", () => {
const target = buildImageNode("image-annotated");
const plan: CanvasOperationPlan = {
version: 1,
operationId: "op-add-annotation",
operation: "add_annotation",
projectId,
targetNodeIds: ["image-annotated"],
textContent: "背景换浅灰,主体保持不变",
annotations: [
{
type: "box",
text: "处理这块背景",
bounds: { x: 0.12, y: 0.2, width: 0.5, height: 0.32 },
color: "#f59e0b",
},
],
metadata: {
messageId: "assistant-message-annotation",
chatId: "chat-1",
},
};
const first = applyCanvasOperationPlanToCanvasData({
canvasData: getCanvasData([target]),
plan,
});
const annotatedNode = getResultNodes(first.canvasData).find(
(node) => node.id === "image-annotated"
);
expect(first.createdNodeIds).toEqual([]);
expect(first.updatedNodeIds).toEqual(["image-annotated"]);
expect(annotatedNode).toMatchObject({
data: {
hasAnnotations: true,
metadata: {
annotations: [
expect.objectContaining({
id: "op-add-annotation:annotation-image-annotated-1",
sourceNodeId: "image-annotated",
type: "box",
text: "处理这块背景",
bounds: { x: 0.12, y: 0.2, width: 0.5, height: 0.32 },
createdByAgent: true,
canvasAgentOperationId: "op-add-annotation",
}),
],
},
},
});
const retry = applyCanvasOperationPlanToCanvasData({
canvasData: first.canvasData,
plan,
});
const retryNode = getResultNodes(retry.canvasData).find(
(node) => node.id === "image-annotated"
);
expect(retry.updatedNodeIds).toEqual([]);
expect(retryNode.data.metadata.annotations).toHaveLength(1);
});
test("keeps batch media writes idempotent by operation id", () => {
const anchor = buildImageNode("image-anchor");
const plan = {