feat(canvas): 云端 Agent - 完成策略分层、节点能力注册与可解释审批改造
This commit is contained in:
@@ -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("拒绝则保留草稿");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(["生成未完成:视频节点《满月动画》"]);
|
||||
|
||||
@@ -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("运行已接收,但本地提交记录清理失败");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user