feat: route canvas agent text updates
This commit is contained in:
parent
67dbeeab1a
commit
8a2483ad7d
@ -12,6 +12,7 @@ type ExplicitOperationKind =
|
||||
| "create_agent_slot"
|
||||
| "create_text_node"
|
||||
| "rename_node"
|
||||
| "update_text"
|
||||
| "update_note";
|
||||
|
||||
export type CanvasAgentExplicitOperationPlan = {
|
||||
@ -199,6 +200,60 @@ function buildCreateTextOperation(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function hasImageTextEditIntent(userText: string) {
|
||||
return /(?:图片|图像|照片|主图|海报|画面|图中|图里|图片里|图片上|海报上).*(?:文字|文本|文案|字).*(?:改|换|替换|编辑|修改|去掉|删除)|(?:文字|文本|文案|字).*(?:图片|图像|照片|主图|海报|画面|图中|图里|图片里|图片上|海报上).*(?:改|换|替换|编辑|修改|去掉|删除)/.test(
|
||||
userText
|
||||
);
|
||||
}
|
||||
|
||||
function buildUpdateTextOperation(params: {
|
||||
context: CanvasAgentContext;
|
||||
projectId: string;
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
userText: string;
|
||||
requestedNodeIds?: string[];
|
||||
}): CanvasAgentExplicitOperationPlan | null {
|
||||
const text = params.userText.trim();
|
||||
const hasUpdateTextIntent =
|
||||
/(?:选中|当前|这个|这段|该).*(?:文本|文字|文案|标语|说明|copy|text).*(?:改成|改为|修改为|更新为|设为|设置为|换成)|(?:文本|文字|文案|标语|说明|copy|text).*(?:改成|改为|修改为|更新为|设为|设置为|换成)/i.test(
|
||||
text
|
||||
);
|
||||
if (!hasUpdateTextIntent || hasMediaMutationIntent(text)) return null;
|
||||
if (hasImageTextEditIntent(text)) return null;
|
||||
|
||||
const textContent = extractContentFromPatterns(
|
||||
text,
|
||||
[
|
||||
/(?:改成|改为|修改为|更新为|设为|设置为|换成)\s*[::]?\s*([\s\S]+)$/i,
|
||||
/(?:文本|文字|文案|标语|说明|copy|text)[^::]*(?:改成|改为|修改为|更新为|设为|设置为|换成)\s*[::]?\s*([\s\S]+)$/i,
|
||||
/[::]\s*([\s\S]+)$/i,
|
||||
],
|
||||
4000
|
||||
);
|
||||
if (!textContent) return null;
|
||||
|
||||
const targetNodeIds = getTargetNodeIds({
|
||||
context: params.context,
|
||||
requestedNodeIds: params.requestedNodeIds,
|
||||
allowedTypes: ["text"],
|
||||
}).slice(0, 10);
|
||||
if (targetNodeIds.length === 0) return null;
|
||||
|
||||
return {
|
||||
operationPlan: {
|
||||
...buildPlanBase({ ...params, operation: "update_text" }),
|
||||
targetNodeIds,
|
||||
textContent,
|
||||
},
|
||||
reply: "已更新选中文本内容。",
|
||||
progressTitle: "正在更新画布文本",
|
||||
progressMessage: "我会只修改选中文本节点的内容,不移动画布节点。",
|
||||
completedTitle: "画布文本已更新",
|
||||
failedTitle: "画布文本更新失败",
|
||||
};
|
||||
}
|
||||
|
||||
function getSlotTypeLabel(slotType: "image" | "video" | "ui-screen") {
|
||||
if (slotType === "video") return "视频";
|
||||
if (slotType === "ui-screen") return "UI";
|
||||
@ -407,6 +462,7 @@ export function buildCanvasAgentExplicitOperationPlan(params: {
|
||||
return (
|
||||
buildCreateAgentSlotOperation(commonParams) ||
|
||||
buildCreateTextOperation(commonParams) ||
|
||||
buildUpdateTextOperation(commonParams) ||
|
||||
buildRenameOperation(commonParams) ||
|
||||
buildUpdateNoteOperation(commonParams) ||
|
||||
buildAddAnnotationOperation(commonParams)
|
||||
|
||||
@ -1132,7 +1132,10 @@ function applyOperationToCanvasData(params: {
|
||||
if (
|
||||
existingOperationNodes.length > 0 &&
|
||||
(params.plan.operation === "replace_node_media" ||
|
||||
params.plan.operation === "move_nodes")
|
||||
params.plan.operation === "move_nodes" ||
|
||||
params.plan.operation === "rename_node" ||
|
||||
params.plan.operation === "update_note" ||
|
||||
params.plan.operation === "update_text")
|
||||
) {
|
||||
return {
|
||||
canvasData: params.canvasData,
|
||||
@ -1385,6 +1388,7 @@ function applyOperationToCanvasData(params: {
|
||||
content: textContent,
|
||||
metadata: {
|
||||
...(data.metadata || {}),
|
||||
...(params.plan.metadata || {}),
|
||||
updatedByAgent: true,
|
||||
canvasAgentOperationId: params.plan.operationId,
|
||||
canvasAgentOperation: params.plan.operation,
|
||||
|
||||
@ -83,6 +83,23 @@ function buildImageNode(id = "image-1") {
|
||||
};
|
||||
}
|
||||
|
||||
function buildTextNode(id = "text-1") {
|
||||
return {
|
||||
id,
|
||||
type: "text",
|
||||
position: { x: 160, y: 180 },
|
||||
size: { width: 280, height: 120 },
|
||||
data: {
|
||||
content: "旧文案",
|
||||
fontSize: 18,
|
||||
color: "#111827",
|
||||
},
|
||||
connections: [],
|
||||
createdAt: "2026-06-22T00:00:00.000Z",
|
||||
updatedAt: "2026-06-22T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentSlotNode(id = "slot-1", slotType: "image" | "video" = "image") {
|
||||
return {
|
||||
id,
|
||||
@ -751,6 +768,56 @@ test.describe("canvas agent operation mutation contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("updates selected text nodes with trace metadata and idempotency", () => {
|
||||
const plan: CanvasOperationPlan = {
|
||||
version: 1,
|
||||
operationId: "op-update-text-node",
|
||||
operation: "update_text",
|
||||
projectId,
|
||||
targetNodeIds: ["text-update"],
|
||||
textContent: "轻盈强吸力,家务更省心",
|
||||
metadata: {
|
||||
messageId: "assistant-message-update-text",
|
||||
chatId: "chat-1",
|
||||
},
|
||||
};
|
||||
|
||||
const first = applyCanvasOperationPlanToCanvasData({
|
||||
canvasData: getCanvasData([buildTextNode("text-update")]),
|
||||
plan,
|
||||
});
|
||||
const updatedText = getResultNodes(first.canvasData).find(
|
||||
(node) => node.id === "text-update"
|
||||
);
|
||||
|
||||
expect(first.createdNodeIds).toEqual([]);
|
||||
expect(first.updatedNodeIds).toEqual(["text-update"]);
|
||||
expect(updatedText).toMatchObject({
|
||||
data: {
|
||||
content: "轻盈强吸力,家务更省心",
|
||||
metadata: {
|
||||
updatedByAgent: true,
|
||||
canvasAgentOperationId: "op-update-text-node",
|
||||
canvasAgentOperation: "update_text",
|
||||
previousContent: "旧文案",
|
||||
messageId: "assistant-message-update-text",
|
||||
chatId: "chat-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const retry = applyCanvasOperationPlanToCanvasData({
|
||||
canvasData: first.canvasData,
|
||||
plan,
|
||||
});
|
||||
const retryText = getResultNodes(retry.canvasData).find(
|
||||
(node) => node.id === "text-update"
|
||||
);
|
||||
|
||||
expect(retry.updatedNodeIds).toEqual(["text-update"]);
|
||||
expect(retryText.data.metadata.previousContent).toBe("旧文案");
|
||||
});
|
||||
|
||||
test("fills an agent slot and returns media item node mappings", () => {
|
||||
const slot = buildAgentSlotNode();
|
||||
const result = applyCanvasOperationPlanToCanvasData({
|
||||
|
||||
@ -1432,6 +1432,29 @@ test.describe("canvas chat pure helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("builds deterministic text update plans for selected text nodes", () => {
|
||||
const outcome = buildCanvasAgentExplicitOperationPlan({
|
||||
context: plannerContext,
|
||||
projectId: plannerContext.projectId || "",
|
||||
messageId: "message-1",
|
||||
chatId: "chat-1",
|
||||
userText: "把选中文案改成:轻盈强吸力,家务更省心",
|
||||
});
|
||||
|
||||
expect(outcome?.operationPlan).toMatchObject({
|
||||
operation: "update_text",
|
||||
targetNodeIds: ["text-1"],
|
||||
textContent: "轻盈强吸力,家务更省心",
|
||||
preserveOriginal: true,
|
||||
metadata: {
|
||||
messageId: "message-1",
|
||||
chatId: "chat-1",
|
||||
planner: "deterministic",
|
||||
},
|
||||
});
|
||||
expect(outcome?.reply).toContain("文本");
|
||||
});
|
||||
|
||||
test("builds deterministic agent slot creation plans from explicit canvas prompts", () => {
|
||||
const context: CanvasAgentContext = {
|
||||
...plannerContext,
|
||||
@ -1601,6 +1624,16 @@ test.describe("canvas chat pure helpers", () => {
|
||||
userText: "按这张图上的批注修改",
|
||||
})
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
buildCanvasAgentExplicitOperationPlan({
|
||||
context: plannerContext,
|
||||
projectId: plannerContext.projectId || "",
|
||||
messageId: "message-1",
|
||||
chatId: "chat-1",
|
||||
userText: "把图片里的文字改成新品上市",
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("normalizes model-planned annotations to selected media nodes", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user