feat(agent): 画布 Agent - 重构工作流上下文与技能交互

This commit is contained in:
ddCat committed 2026-08-24 12:09:15 +08:00
1 parent 919037e5d9
commit 326572ea03
33 files changed
+2230 -248

No files matched your search

+84 -2
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildCanvasContext, findCanvasNodes, getCanvasResources, validateCanvasOps } from "../src/canvas-context.js";
import { buildCanvasContext, findCanvasNodes, getCanvasConnection, getCanvasGenerationTasks, getCanvasNode, getCanvasResources, validateCanvasOps } from "../src/canvas-context.js";
import type { CanvasSnapshot } from "../src/types.js";
const state: CanvasSnapshot = {
@@ -12,7 +12,7 @@ const state: CanvasSnapshot = {
nodes: [
{ id: "prompt-1", type: "text", title: "提示词", position: { x: 0, y: 0 }, width: 340, height: 240, metadata: { content: "夜雨中的城市", status: "success" } },
{ id: "ref-1", type: "image", title: "角色参考", position: { x: 420, y: 0 }, width: 320, height: 320, metadata: { status: "success", storageKey: "resource:abc", mimeType: "image/png", assetId: "asset-1", assetCategory: "character", naturalWidth: 1024, naturalHeight: 1024 } },
{ id: "loading-1", type: "video", title: "待生成视频", position: { x: 840, y: 0 }, width: 480, height: 270, metadata: { status: "loading" } },
{ id: "loading-1", type: "video", title: "待生成视频", position: { x: 840, y: 0 }, width: 480, height: 270, metadata: { status: "loading", taskId: "task-1", taskStatus: "running", taskProgress: 42, taskStage: "rendering", taskProvider: "backend" } },
],
connections: [{ id: "c-1", fromNodeId: "prompt-1", toNodeId: "ref-1" }],
};
@@ -25,6 +25,9 @@ test("builds semantic canvas context without media URLs", () => {
assert.equal(context.resources[0]?.isReady, true);
assert.equal(context.resources[1]?.isReady, false);
assert.equal((context.nodes[1] as { resource?: { ready?: boolean } }).resource?.ready, true);
assert.equal((context.nodes[2] as { generation?: { taskId?: string; progress?: number } }).generation?.taskId, "task-1");
assert.equal((context.nodes[2] as { generation?: { taskId?: string; progress?: number; stage?: string } }).generation?.progress, 42);
assert.equal((context.nodes[2] as { generation?: { taskId?: string; progress?: number; stage?: string } }).generation?.stage, "rendering");
assert.equal("url" in context.nodes[1], false);
assert.match(context.stateHash, /^[a-f0-9]{16}$/);
});
@@ -34,6 +37,43 @@ test("finds real nodes and resources by semantic query", () => {
assert.equal(getCanvasResources(state, { status: "loading" }).resources[0]?.nodeId, "loading-1");
});
test("reads one node and its related connections without media URLs", () => {
const result = getCanvasNode(state, { id: "ref-1" });
assert.equal(result.found, true);
assert.equal(result.node?.id, "ref-1");
assert.equal(result.resource?.isReady, true);
assert.equal(result.connections[0]?.id, "c-1");
assert.equal("url" in (result.node || {}), false);
assert.equal(getCanvasNode(state, { id: "missing" }).found, false);
});
test("reads one connection with endpoint summaries", () => {
const result = getCanvasConnection(state, { id: "c-1" });
assert.equal(result.found, true);
assert.equal(result.connection?.fromTitle, "提示词");
assert.equal(result.toNode?.id, "ref-1");
assert.equal(getCanvasConnection(state, { id: "missing" }).found, false);
});
test("projects observable generation task state from canvas nodes", () => {
const result = getCanvasGenerationTasks(state, { status: "running" });
assert.equal(result.total, 1);
assert.deepEqual(result.tasks[0], {
taskId: "task-1",
nodeId: "loading-1",
nodeTitle: "待生成视频",
mode: undefined,
nodeStatus: "loading",
status: "running",
progress: 42,
stage: "rendering",
provider: "backend",
errorCode: undefined,
officialStatus: undefined,
resourceReady: false,
});
});
test("rejects stale or unsafe operations before dispatch", () => {
const result = validateCanvasOps(state, [
{ type: "update_node", id: "missing" },
@@ -42,3 +82,45 @@ test("rejects stale or unsafe operations before dispatch", () => {
assert.equal(result.ok, false);
assert.equal(result.issues.length, 2);
});
test("validates a batch transaction against its evolving node and connection state", () => {
const result = validateCanvasOps(state, [
{ type: "add_node", id: "new-1", nodeType: "image", position: { x: 10, y: 20 }, width: 320, height: 240 },
{ type: "connect_nodes", fromNodeId: "new-1", toNodeId: "ref-1", id: "new-connection" },
{ type: "delete_node", id: "new-1" },
{ type: "update_node", id: "new-1", patch: { title: "should fail" } },
]);
assert.equal(result.ok, false);
assert.match(result.issues.at(-1)?.message || "", /不在当前画布状态中/);
});
test("rejects duplicate, missing, and unsafe connection or viewport operations", () => {
const result = validateCanvasOps(state, [
{ type: "connect_nodes", fromNodeId: "prompt-1", toNodeId: "ref-1" },
{ type: "delete_connections", ids: ["missing-connection"] },
{ type: "set_viewport", viewport: { x: 0, y: 0, k: 99 } },
{ type: "add_node", nodeType: "image", width: -1, height: 0, position: { x: Number.NaN, y: 0 } },
]);
assert.equal(result.ok, false);
assert.ok(result.issues.some((issue) => issue.message.includes("相同端点")));
assert.ok(result.issues.some((issue) => issue.message.includes("不存在")));
assert.ok(result.issues.some((issue) => issue.message.includes("0.05 到 8")));
assert.ok(result.issues.some((issue) => issue.message.includes("正数")));
});
test("evolving connection state allows delete then recreate in one atomic batch", () => {
const result = validateCanvasOps(state, [
{ type: "delete_connections", id: "c-1" },
{ type: "connect_nodes", id: "c-2", fromNodeId: "prompt-1", toNodeId: "ref-1" },
]);
assert.equal(result.ok, true);
});
test("rejects direct media status forgery as a write error", () => {
const result = validateCanvasOps(state, [
{ type: "update_node", id: "ref-1", patch: { metadata: { status: "success" } } },
]);
assert.equal(result.ok, false);
assert.equal(result.issues[0]?.severity, "error");
});