feat(canvas): 云端 Agent - 完成策略分层、节点能力注册与可解释审批改造

This commit is contained in:
ddCat
2026-09-14 17:58:21 +08:00
parent 4b0e9d8965
commit 01486872c4
65 changed files with 5347 additions and 918 deletions

View File

@@ -4,26 +4,54 @@ import { agentApprovalPresentation } from "@/lib/canvas/agent-approval-presentat
describe("Agent approval presentation", () => {
const args = JSON.stringify({ snapshotHash: "private-hash", ops: [
{ type: "add_node", title: "开场分镜", content: "private script" },
{ type: "update_node", title: "结尾台词", content: "private dialogue" },
{ type: "add_node", id: "image-node", nodeType: "image", title: "开场分镜", content: "private script" },
{ type: "update_node", id: "video-node-1789310237935", patch: { title: "结尾台词", content: "private dialogue" } },
] });
it("summarizes a persisted approval without exposing tool names or content", () => {
it("prefers the server-authored preview and exposes the target and changed fields", () => {
const view = agentApprovalPresentation({
call: { function: { name: "canvas_apply_ops", arguments: args } },
preview: {
kind: "canvas_mutation",
title: "确认画布修改",
description: "请确认目标节点和修改字段;批准后才会写入画布。",
items: [{ operation: "update_node", nodeId: "video-node-1789310237935", nodeTitle: "满月庆祝视频草稿", nodeType: "video", nodeTypeLabel: "视频", fields: ["节点名称", "下一版提示词"], resultTitle: "满月庆祝视频草稿(舒缓呼吸感)", summary: "修改视频《满月庆祝视频草稿》的节点名称、下一版提示词" }],
},
});
expect(view.source).toBe("server");
expect(view.items[0]).toMatchObject({ nodeTitle: "满月庆祝视频草稿", nodeTypeLabel: "视频", fields: ["节点名称", "下一版提示词"] });
expect(view.items[0].summary).toContain("满月庆祝视频草稿");
expect(JSON.stringify(view)).not.toContain("private");
});
it("summarizes a legacy persisted approval without exposing content or tool names", () => {
const view = agentApprovalPresentation({ call: { function: { name: "canvas_apply_ops", arguments: args } } });
expect(view.title).toBe("确认画布修改");
expect(view.description).toContain("新增 1 个节点,修改 1 个节点");
expect(view.items).toEqual(["开场分镜", "结尾台词"]);
expect(view.items.map((item) => item.summary).join(" ")).toContain("目标节点");
expect(view.items.find((item) => item.operation === "update_node")?.fields).toEqual(["节点名称", "正文"]);
expect(JSON.stringify(view)).not.toContain("private");
expect(JSON.stringify(view)).not.toContain("canvas_apply_ops");
expect(JSON.stringify(view)).not.toContain("video-node-1789310237935");
});
it("shows both endpoints for legacy reference connections", () => {
const view = agentApprovalPresentation({ toolName: "canvas_apply_ops", arguments: { ops: [
{ type: "connect_nodes", fromNodeId: "reference-image-node", toNodeId: "video-node" },
] } });
expect(view.items[0].summary).toContain("来源");
expect(view.items[0].summary).toContain("目标");
expect(view.description).toContain("建立 1 条引用连线");
});
it("summarizes an event approval and media request", () => {
expect(agentApprovalPresentation({ toolName: "canvas_apply_ops", arguments: JSON.parse(args) }).items).toEqual(["开场分镜", "结尾台词"]);
expect(agentApprovalPresentation({ toolName: "generate_media", arguments: { mode: "video" } }).title).toBe("确认生成视频");
});
it("uses a safe fallback for malformed arguments", () => {
expect(agentApprovalPresentation({ toolName: "canvas_apply_ops", arguments: "{" }).description).toContain("修改当前画布");
it("uses an explicit safe state when arguments are malformed", () => {
const view = agentApprovalPresentation({ toolName: "canvas_apply_ops", arguments: "{" });
expect(view.description).toContain("无法确认具体修改目标");
expect(view.description).not.toContain("修改当前画布");
});
it("shows approved media specifications and references without exposing prompts or IDs", () => {
@@ -32,23 +60,13 @@ describe("Agent approval presentation", () => {
videoGenerateAudio: false, referenceNodeIds: ["private-cat", "private-hero"], prompt: "private prompt",
} });
expect(view.description).toContain("结果自动回写画布");
expect(view.items).toEqual(["节点:镜头1", "引用 2 个画布资产,并建立连线", "时长:12 秒", "画幅:9:16", "质量:720p", "音频:关闭"]);
expect(view.items[0].details).toEqual(["引用 2 个画布资产,并建立连线", "时长:12 秒", "画幅:9:16", "质量:720p", "音频:关闭"]);
expect(JSON.stringify(view)).not.toContain("private");
});
it("summarizes reference connections as canvas edits without generation", () => {
const view = agentApprovalPresentation({ toolName: "canvas_apply_ops", arguments: { ops: [
{ type: "add_node", title: "视频节点" }, { type: "connect_nodes" }, { type: "connect_nodes" },
] } });
expect(view.description).toContain("新增 1 个节点,建立 2 条引用连线");
expect(view.description).not.toContain("提交任务");
it("states when an unknown approval cannot identify a target", () => {
const view = agentApprovalPresentation({ toolName: "unknown_tool", arguments: {} });
expect(view.items).toHaveLength(0);
expect(view.description).toContain("无法识别");
});
it("shows the real catalog model and states that a draft is not a submitted task", () => {
const view = agentApprovalPresentation({ modelName: "用户选择的模型", toolName: "generate_media", arguments: { mode: "video", channelModelKey: "internal-key", size: "16:9" } });
expect(view.items).toContain("模型:用户选择的模型");
expect(view.items).toContain("画幅:16:9");
expect(view.description).toContain("尚未提交生成");
expect(view.description).toContain("拒绝则保留草稿");
});
});
});

View File

@@ -9,6 +9,32 @@ test("persisted node actions retain human-readable titles without a live canvas"
expect(agentCanvasActions("canvas_apply_ops", detail).map(agentCanvasActionLabel)).toEqual(["创建了视频节点《满月动画》", "引用了图片节点《满月照片》"]);
});
test("server-authored canvas actions explain updated fields, renamed nodes, and both connection endpoints", () => {
const detail = JSON.parse(JSON.stringify({ actions: [
{
action: "updated",
nodeId: "video",
title: "旧名",
nodeType: "video",
fields: ["节点名称", "下一版提示词"],
resultTitle: "新名",
},
{
action: "referenced",
nodeId: "image",
title: "参考图",
nodeType: "image",
targetNodeId: "video",
targetTitle: "目标视频",
targetNodeType: "video",
},
] }));
expect(agentCanvasActions("canvas_apply_ops", detail).map(agentCanvasActionLabel)).toEqual([
"更新了视频节点《旧名》(修改:节点名称、下一版提示词),名称改为《新名》",
"建立引用:图片节点《参考图》 → 视频节点《目标视频》",
]);
});
test("legacy generation failures retain node name and never claim creation or reference success", () => {
const detail = { eventType: "tool_failed", arguments: JSON.stringify({ nodeId: "video", title: "满月动画", mode: "video", referenceNodeIds: ["image"] }), result: { phase: "admission", taskSubmitted: false } };
expect(agentCanvasActions("generate_media", detail).map(agentCanvasActionLabel)).toEqual(["生成未完成:视频节点《满月动画》"]);

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test";
import { changeAgentPanelLayout, clampAgentPanelLayout, restoreAgentPanelLayout } from "@/lib/canvas/agent-panel-layout";
import { agentErrorPresentation } from "@/lib/canvas/agent-error-presentation";
import { agentErrorPresentation, agentSubmissionErrorTitle } from "@/lib/canvas/agent-error-presentation";
import { ApiError } from "@/services/api/request";
const viewport = { width: 1280, height: 800 };
@@ -64,4 +64,10 @@ describe("Agent error semantics", () => {
it("reports unimplemented endpoints without retry promises", () => {
expect(agentErrorPresentation(new ApiError("Not implemented", { status: 501 })).title).toBe("当前 Agent 能力尚未开放");
});
it("distinguishes confirmed server failures from missing responses", () => {
expect(agentSubmissionErrorTitle(new ApiError("系统处理失败", { status: 500 }), false)).toBe("服务端已返回错误;重试将核对原请求,不重复创建");
expect(agentSubmissionErrorTitle(new ApiError("参数错误", { status: 400 }), false)).toBe("请求已被服务端拒绝");
expect(agentSubmissionErrorTitle(new TypeError("network failed"), false)).toBe("未收到服务端确认;重试将核对原请求,不重复创建");
expect(agentSubmissionErrorTitle(undefined, true)).toBe("运行已接收,但本地提交记录清理失败");
});
});