feat: route canvas agent link operations

This commit is contained in:
134 2026-06-23 23:11:20 +08:00
parent f2ed508cb4
commit 3f8219f415
3 changed files with 121 additions and 0 deletions

View File

@ -11,6 +11,7 @@ type ExplicitOperationKind =
| "add_annotation"
| "create_agent_slot"
| "create_text_node"
| "link_nodes"
| "rename_node"
| "update_text"
| "update_note";
@ -254,6 +255,38 @@ function buildUpdateTextOperation(params: {
};
}
function buildLinkNodesOperation(params: {
context: CanvasAgentContext;
projectId: string;
messageId: string;
chatId: string;
userText: string;
requestedNodeIds?: string[];
}): CanvasAgentExplicitOperationPlan | null {
const text = params.userText.trim();
const hasLinkIntent = /连起来|连线|连接|关联|参考关系|版本关系|建立关系/.test(text);
if (!hasLinkIntent || hasMediaMutationIntent(text)) return null;
const targetNodeIds = getTargetNodeIds({
context: params.context,
requestedNodeIds: params.requestedNodeIds,
}).slice(0, 20);
if (targetNodeIds.length < 2) return null;
return {
operationPlan: {
...buildPlanBase({ ...params, operation: "link_nodes" }),
sourceNodeIds: targetNodeIds,
edgeType: "reference",
},
reply: "已为选中的画布节点建立参考连线。",
progressTitle: "正在连接画布节点",
progressMessage: "我会给选中的画布节点建立参考关系,不修改节点内容。",
completedTitle: "画布节点已连接",
failedTitle: "画布节点连接失败",
};
}
function getSlotTypeLabel(slotType: "image" | "video" | "ui-screen") {
if (slotType === "video") return "视频";
if (slotType === "ui-screen") return "UI";
@ -463,6 +496,7 @@ export function buildCanvasAgentExplicitOperationPlan(params: {
buildCreateAgentSlotOperation(commonParams) ||
buildCreateTextOperation(commonParams) ||
buildUpdateTextOperation(commonParams) ||
buildLinkNodesOperation(commonParams) ||
buildRenameOperation(commonParams) ||
buildUpdateNoteOperation(commonParams) ||
buildAddAnnotationOperation(commonParams)

View File

@ -818,6 +818,60 @@ test.describe("canvas agent operation mutation contract", () => {
expect(retryText.data.metadata.previousContent).toBe("旧文案");
});
test("links selected canvas nodes without duplicating existing edges", () => {
const source = buildImageNode("image-link-source");
const target = buildTextNode("text-link-target");
const plan: CanvasOperationPlan = {
version: 1,
operationId: "op-link-nodes",
operation: "link_nodes",
projectId,
sourceNodeIds: ["image-link-source", "text-link-target"],
edgeType: "reference",
metadata: {
messageId: "assistant-message-link",
chatId: "chat-1",
},
};
const first = applyCanvasOperationPlanToCanvasData({
canvasData: getCanvasData([source, target]),
plan,
});
expect(first.createdNodeIds).toEqual([]);
expect(first.updatedNodeIds).toEqual([
"image-link-source",
"text-link-target",
]);
expect(first.createdEdgeIds).toHaveLength(1);
expect(getResultEdges(first.canvasData)).toEqual([
expect.objectContaining({
source: "image-link-source",
target: "text-link-target",
type: "reference",
}),
]);
expect(getResultNodes(first.canvasData)).toEqual([
expect.objectContaining({
id: "image-link-source",
connections: ["text-link-target"],
}),
expect.objectContaining({
id: "text-link-target",
connections: ["image-link-source"],
}),
]);
const retry = applyCanvasOperationPlanToCanvasData({
canvasData: first.canvasData,
plan,
});
expect(retry.createdEdgeIds).toEqual([]);
expect(getResultEdges(retry.canvasData)).toHaveLength(1);
});
test("fills an agent slot and returns media item node mappings", () => {
const slot = buildAgentSlotNode();
const result = applyCanvasOperationPlanToCanvasData({

View File

@ -1455,6 +1455,29 @@ test.describe("canvas chat pure helpers", () => {
expect(outcome?.reply).toContain("文本");
});
test("builds deterministic link plans for selected canvas nodes", () => {
const outcome = buildCanvasAgentExplicitOperationPlan({
context: plannerContext,
projectId: plannerContext.projectId || "",
messageId: "message-1",
chatId: "chat-1",
userText: "把选中的两个节点连成参考关系",
});
expect(outcome?.operationPlan).toMatchObject({
operation: "link_nodes",
sourceNodeIds: ["image-1", "text-1"],
edgeType: "reference",
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,
@ -1634,6 +1657,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", () => {