183 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { asSchema, type ToolSet } from "ai";
import { join } from "node:path";
import { runNativeZcode, type NativeEvent, type NativeResult } from "@/services/zcode-agent/runner.mjs";
import type { CanvasAgentTool } from "@/lib/ai/canvas-agent-runtime/loop";
import { publicAgentError, type AgentRunSnapshot, type CanvasAgentRunStatus } from "@/lib/ai/canvas-agent-runtime/protocol";
type Ports = {
native?: typeof runNativeZcode;
runId: string;
snapshot: AgentRunSnapshot;
definitions: ToolSet;
tools: Record<string, CanvasAgentTool>;
instructions: string;
context: unknown;
gateway: { modelId: string; apiType: string; forward: (body: Record<string, unknown>, path: string, signal: AbortSignal) => Promise<Response> };
signal: AbortSignal;
checkpoint: (snapshot: AgentRunSnapshot, status: CanvasAgentRunStatus) => Promise<void>;
consumeInputs: (snapshot: AgentRunSnapshot) => Promise<{ cancelled: boolean }>;
isCancelled: () => Promise<boolean>;
verify: (snapshot: AgentRunSnapshot) => Promise<NonNullable<AgentRunSnapshot["verification"]>>;
};
export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunStatus> {
const { snapshot } = ports;
let serial: Promise<unknown> = Promise.resolve();
function exclusive<T>(action: () => Promise<T>): Promise<T> {
const next = serial.then(action); serial = next.catch(() => undefined); return next;
}
async function save(message: string, status: CanvasAgentRunStatus = "running") {
snapshot.message = message;
await ports.checkpoint(structuredClone(snapshot), status); return status;
}
let modelError = "";
try {
if (snapshot.calls.some(call => call.status === "started" || call.uncertain))
return await save("存在结果未确认的工具调用,请先核对画布,避免重复生成。", "needs_review");
snapshot.nativeRuntime = { ...snapshot.nativeRuntime, engine: "zcode", state: "starting" };
const schemas = new Map(Object.entries(ports.definitions).map(([name, definition]) => [name, asSchema(definition.inputSchema!)]));
const toolDescriptors = Object.entries(ports.tools).map(([name, entry]) => ({ name,
description: ports.definitions[name]?.description || entry.title,
inputSchema: schemas.get(name)!.jsonSchema, readOnly: entry.readOnly }));
toolDescriptors.push({ name: "requestCanvasUserInput", description: "需要用户补充必要信息时提出问题,保存任务并等待用户回答。不要用文件、shell 或虚构数据代替用户输入。",
inputSchema: { type: "object", properties: { question: { type: "string", minLength: 1, maxLength: 1000 } }, required: ["question"], additionalProperties: false }, readOnly: true });
let question = "";
const tools = Object.keys(ports.tools);
const instructions = [ports.instructions,
"你正在独立沙箱内运行原生 ZCode 核心。本站图片/视频/节点/界面能力已通过 canvas MCP 全部授权;先读实际画布,不猜测节点 ID。",
`可用业务工具:${tools.join("、")}。工具名称带 mcp__canvas__ 前缀。`,
"原生文件、Bash、记忆及子任务能力仅作用于此任务的私有工作区,不能通过 shell 直接访问模型服务商或本站后台;业务操作使用 MCP。",
"需要澄清时调用 requestCanvasUserInput;不要使用终端交互。工具返回的 userUpdates 是用户的新补充,必须从下一步纳入。",
"工具错误涉及结果未确认时停止收费动作,不通过更换调用 ID 或另一个工具重复生成。用户明确要求修改的目标才能删除或覆盖。",
"只有真实工具返回成功才可声称完成;打开面板、等待用户选文件、后台缺少处理服务都不能声称媒体已处理。",
].join("\n");
let prompt = snapshot.nativeRuntime.sessionId
? ["继续当前任务,保留所有已完成结果。", ...snapshot.inputs.filter(input => input.applied).slice(-4).map(input => input.text)].join("\n")
: `${snapshot.objective}\n\n当前画布与历史对话上下文:${JSON.stringify(ports.context)}`;
while (true) {
const control = await exclusive(() => ports.consumeInputs(snapshot));
if (control.cancelled || ports.signal.aborted) return await save("已停止后续处理,完成的结果保留在画布。", "cancelled");
await save("正在启动 ZCode 独立沙箱");
const startedInputs = new Set(snapshot.inputs.map(input => input.id));
const result = await (ports.native || runNativeZcode)({
runId: ports.runId, attemptId: crypto.randomUUID(),
runtimeDir: process.env.CANVAS_ZCODE_RUNTIME_DIR || "/opt/yedir-canvas-zcode/current",
stateDir: process.env.CANVAS_ZCODE_STATE_DIR || "/var/lib/yedir-canvas-agent",
nodePath: process.env.CANVAS_ZCODE_NODE_PATH || join(process.env.CANVAS_ZCODE_RUNTIME_DIR || "/opt/yedir-canvas-zcode/current", "bin/node"),
modelId: ports.gateway.modelId, apiType: ports.gateway.apiType,
instructions, prompt, context: ports.context, sessionId: snapshot.nativeRuntime.sessionId,
signal: ports.signal, tools: toolDescriptors, formatToolError: publicAgentError,
model: async (body, path, signal) => {
const response = await ports.gateway.forward(body, path, signal);
if (!response.ok) modelError = response.status === 401 ? "当前 Agent 模型服务商鉴权失败,请检查模型配置"
: response.status === 429 ? "Agent 模型通道额度不足或请求过多" : "Agent 模型连接中断,已保留执行记录";
return response;
},
onSandbox: async metadata => exclusive(async () => {
snapshot.nativeRuntime = { ...snapshot.nativeRuntime!, sandbox: metadata.kind, commit: metadata.commit, state: "starting" };
await save("正在启动独立沙箱中的 ZCode 核心");
}),
onEvent: async (event: NativeEvent | NativeResult) => {
if (!["turn.started", "result", "session.created", "session.resumed"].includes(event.type)) return;
await exclusive(async () => {
if (event.sessionId) snapshot.nativeRuntime!.sessionId = event.sessionId;
if (event.traceId) snapshot.nativeRuntime!.traceId = event.traceId;
snapshot.nativeRuntime!.state = "running";
if (event.type === "turn.started") snapshot.steps++;
await save("ZCode 正在规划并执行画布任务");
});
},
execute: async (name, input, callId) => {
if (name === "requestCanvasUserInput") {
if (typeof input.question !== "string" || !input.question.trim() || input.question.length > 1000) throw new Error("请提供明确的问题");
question = input.question; return { status: "waiting_for_user", question };
}
const entry = ports.tools[name]; if (!entry) throw new Error("此画布工具未授权");
const schema = schemas.get(name)!;
const validated = schema.validate ? await schema.validate(input) : { success: true as const, value: input };
if (!validated.success) throw new Error("画布工具参数无效,未提交业务操作");
const previous = await exclusive(async () => {
if ((await ports.consumeInputs(snapshot)).cancelled || ports.signal.aborted) throw new Error("任务已停止,未提交新操作");
const old = snapshot.calls.find(call => call.id === callId);
if (old) {
if (old.name !== name || JSON.stringify(old.input) !== JSON.stringify(input)) throw new Error("工具调用标识与参数不一致");
if (old.status === "succeeded") return old.output!;
throw new Error("此工具结果待确认,禁止自动重复执行");
}
if (!entry.readOnly && snapshot.calls.some(call => call.uncertain)) throw new Error("先核对未确认的操作,禁止重复提交收费任务");
if (!entry.readOnly && !snapshot.calls.some(call => call.name === "planCanvasTask" && call.status === "succeeded"))
throw new Error("请先制定任务计划再执行画布修改");
if (!entry.readOnly && snapshot.verification?.retryable && snapshot.verification.failedNodeIds?.length) {
const targets = [input.nodeId, ...(Array.isArray(input.nodeIds) ? input.nodeIds : []),
...(Array.isArray(input.referenceNodeIds) ? input.referenceNodeIds : [])].filter((id): id is string => typeof id === "string");
if (!targets.length || targets.some(id => !snapshot.verification!.failedNodeIds!.includes(id)))
throw new Error("修复只能处理未通过验收的节点,禁止重复生成已通过的结果");
}
snapshot.calls.push({ id: callId, name, input, status: "started" });
await save(`正在执行:${entry.title}`); return undefined;
});
if (previous) return previous;
try {
const output = await entry.execute(validated.value as Record<string, unknown>, callId);
await exclusive(async () => {
const call = snapshot.calls.find(call => call.id === callId)!;
call.status = "succeeded"; call.output = output;
if (name === "planCanvasTask") snapshot.requirements = { ...snapshot.requirements,
...(typeof output.expectedImageCount === "number" ? { expectedImageCount: output.expectedImageCount } : {}),
...(typeof output.aspectRatio === "string" ? { aspectRatio: output.aspectRatio } : {}) };
const ids = Array.isArray(output.createdNodeIds) ? output.createdNodeIds.filter((id): id is string => typeof id === "string") : [];
snapshot.resultNodeIds = [...new Set([...snapshot.resultNodeIds, ...ids])];
await save(`已完成:${entry.title}`);
});
return { ...output, userUpdates: snapshot.inputs.filter(input => !startedInputs.has(input.id)).map(input => input.text) };
} catch (error) {
await exclusive(async () => {
const call = snapshot.calls.find(call => call.id === callId)!;
call.status = "failed"; call.error = publicAgentError(error); call.uncertain = !entry.readOnly;
await save(call.error);
});
throw error;
}
},
});
await exclusive(async () => {
snapshot.nativeRuntime!.sessionId = result.sessionId;
snapshot.nativeRuntime!.state = "stopped";
snapshot.inputTokens += Number(result.usage?.inputTokens || 0);
snapshot.outputTokens += Number(result.usage?.outputTokens || 0);
snapshot.messages.push({ role: "assistant", content: result.response || "" });
await ports.consumeInputs(snapshot);
});
if (await ports.isCancelled()) return await save("已停止后续处理,完成的结果保留在画布。", "cancelled");
if (snapshot.calls.some(call => call.status === "failed" || call.uncertain))
return await save("部分工具结果待确认,已保留执行记录,请核对后继续。", "needs_review");
if (question || snapshot.calls.some(call => call.output?.mode === "interactive"))
return await save(question || "请在已打开的工具面板中完成操作,然后继续任务。", "needs_review");
const supplements = snapshot.inputs.filter(input => !startedInputs.has(input.id));
if (supplements.length) { prompt = supplements.map(input => input.text).join("\n"); continue; }
// Text/analysis tasks have no generated media to inspect. Mutating tasks
// still use the existing independent checks against actual saved nodes.
const mutated = snapshot.calls.some(call => !ports.tools[call.name]?.readOnly);
if (!mutated && !snapshot.requirements?.expectedImageCount) {
const expectsChange = /生成|抠图|去背景|换背景|删除|移除|替换|改名|移动|放大|裁剪|旋转|导出|下载|制作|添加|上传/.test(snapshot.objective)
&& !/只(?:要|需)?(?:分析|讨论|建议|提示词)|不要(?:执行|生成|修改)/.test(snapshot.objective);
return await save(expectsChange ? "尚未执行画布操作,请核对任务计划后继续。" : result.response || "已完成画布分析。", expectsChange ? "needs_review" : "completed");
}
await save("正在核对画布中的实际结果");
const verification = await ports.verify(snapshot); snapshot.verification = verification;
if (verification.passed) return await save(result.response || "任务完成,结果已写入画布。", "completed");
if (verification.retryable && snapshot.repairs < 2) {
snapshot.repairs++;
prompt = `验收需要定点修复:${JSON.stringify(verification)}。只处理失败结果,禁止整批重新生成;保留所有通过结果。`;
continue;
}
return await save(`结果待核对:${verification.issues.join(";")}`, "needs_review");
}
} catch (error) {
if (snapshot.nativeRuntime) snapshot.nativeRuntime.state = "stopped";
const cancelled = await ports.isCancelled().catch(() => false);
return await save(cancelled ? "已停止后续步骤,完成的结果保留在画布。" : modelError || publicAgentError(error), cancelled ? "cancelled" : "needs_review");
}
}