Compare commits
3 Commits
codex/canv
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 57e94b797c | |||
| ca6b27624c | |||
| fa3210a367 |
@ -329,6 +329,7 @@ export async function POST(request: Request) {
|
||||
withModel,
|
||||
imageSize: snapshot.imagePreferences?.parameters?.imageSize,
|
||||
imagePreferences: snapshot.imagePreferences,
|
||||
priorCalls: snapshot.calls,
|
||||
preferredModelId: snapshot.imagePreferences?.modelId || (preset?.type === "image" ? preset.defaultModelId || undefined : undefined),
|
||||
operationSelectedNodeIds: selectedNodeIds,
|
||||
objective: [snapshot.objective, ...snapshot.inputs.map((input) => input.text)].join(
|
||||
|
||||
@ -13,13 +13,18 @@ export type AgentImageParameters = {
|
||||
};
|
||||
export type AgentImagePreferences = { modelId?: string; parameters?: AgentImageParameters };
|
||||
|
||||
export function selectedAgentImageCount(preferences?: AgentImagePreferences) {
|
||||
const count = preferences?.parameters?.imageCount ?? preferences?.parameters?.n;
|
||||
return count === undefined ? undefined : Math.floor(count);
|
||||
}
|
||||
|
||||
export function applyAgentImagePreferences<T extends Record<string, unknown>>(
|
||||
input: T,
|
||||
preferences?: AgentImagePreferences,
|
||||
): T & AgentImageParameters & { modelId?: string } {
|
||||
const parameters = Object.fromEntries(Object.entries(preferences?.parameters || {})
|
||||
.filter(([, value]) => value !== undefined));
|
||||
const count = preferences?.parameters?.imageCount ?? preferences?.parameters?.n;
|
||||
const count = selectedAgentImageCount(preferences);
|
||||
return {
|
||||
...input,
|
||||
...parameters,
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import type { AgentCallRecord, AgentRunSnapshot } from "./protocol";
|
||||
|
||||
export function countAgentGeneratedImages(calls: AgentCallRecord[]) {
|
||||
return calls.reduce((count, call) => {
|
||||
const output = call.output;
|
||||
if (call.status !== "succeeded" || !output?.appliedImagePreferences ||
|
||||
!Array.isArray(output.createdNodeIds)) return count;
|
||||
return count + output.createdNodeIds.length;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// A spontaneous download command is not an unfinished image edit. Keep genuine
|
||||
// download requests and all editor panels waiting for their actual interaction.
|
||||
export function isAncillaryDownload(snapshot: AgentRunSnapshot, call: AgentCallRecord) {
|
||||
|
||||
@ -5,7 +5,8 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { readCanvasImages, readOwnedCanvas, type RuntimeImageNode } from "./canvas";
|
||||
import type { AgentRunSnapshot } from "./protocol";
|
||||
import { finalResultNodeIds, originalSourceNodeIds, requiresCanvasInteraction } from "./results";
|
||||
import { countAgentGeneratedImages, finalResultNodeIds, originalSourceNodeIds, requiresCanvasInteraction } from "./results";
|
||||
import { selectedAgentImageCount } from "./image-preferences";
|
||||
import { isCompletedSimpleCutout } from "./cutout-completion";
|
||||
|
||||
export async function inspectImageAlpha(source: Buffer) {
|
||||
@ -124,14 +125,16 @@ export async function verifyCanvasAgentResults(params: {
|
||||
(call) =>
|
||||
call.status !== "succeeded" ||
|
||||
requiresCanvasInteraction(snapshot, call) ||
|
||||
(typeof call.output?.status === "string" && call.output.status !== "succeeded"),
|
||||
(typeof call.output?.status === "string" && call.output.status !== "succeeded" &&
|
||||
!(call.output.status === "partial" && call.output.appliedImagePreferences)),
|
||||
);
|
||||
if (incomplete.length) return { passed: false, issues: ["部分操作仍在等待交互或未完成"] };
|
||||
const plan = calls.filter((call) => call.name === "planCanvasTask").at(-1)?.output;
|
||||
if (!plan) return { passed: false, issues: ["缺少明确的任务验收计划"] };
|
||||
// Intermediate images consumed by later write tools do not count as final deliverables.
|
||||
const finalIds = finalResultNodeIds(snapshot);
|
||||
const expectedCount = snapshot.requirements?.expectedImageCount ?? plan.expectedImageCount;
|
||||
const expectedCount = snapshot.requirements?.expectedImageCount ?? plan.expectedImageCount ??
|
||||
(countAgentGeneratedImages(calls) > 0 ? selectedAgentImageCount(snapshot.imagePreferences) : undefined);
|
||||
if (expectedCount && finalIds.length !== expectedCount) {
|
||||
return {
|
||||
passed: false,
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import type { AgentVisionExecutor } from "../runtime/agent-vision-failover";
|
||||
import { resolveUserImageModel, applyAgentImagePreferences, agentProviderImageParameters, type AgentImagePreferences } from "./image-preferences";
|
||||
import {
|
||||
agentProviderImageParameters,
|
||||
applyAgentImagePreferences,
|
||||
resolveUserImageModel,
|
||||
selectedAgentImageCount,
|
||||
type AgentImagePreferences,
|
||||
} from "./image-preferences";
|
||||
import { tool, type LanguageModel, type ToolSet } from "ai";
|
||||
import { z } from "zod";
|
||||
import { createCanvasCapabilityTools } from "@/lib/canvas/capabilities/ai-sdk-tools";
|
||||
@ -18,6 +24,8 @@ import {
|
||||
sortStyleProImageModelEntries,
|
||||
} from "@/lib/style-pro/model-options";
|
||||
import type { CanvasAgentLoopPorts, CanvasAgentTool } from "./loop";
|
||||
import { AgentToolRejectedError, type AgentCallRecord } from "./protocol";
|
||||
import { countAgentGeneratedImages } from "./results";
|
||||
import { readCanvasImages, readOwnedCanvas } from "./canvas";
|
||||
import { analyzeRuntimeImages } from "./review";
|
||||
import { isExplicitDestructiveIntent } from "./capability-access";
|
||||
@ -37,11 +45,13 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
imageSize?: string;
|
||||
preferredModelId?: string;
|
||||
imagePreferences?: AgentImagePreferences;
|
||||
priorCalls?: AgentCallRecord[];
|
||||
emit: (type: string, data: unknown) => void;
|
||||
objective?: string;
|
||||
operationSelectedNodeIds?: string[];
|
||||
}) {
|
||||
const { nodes } = await readOwnedCanvas(params.userId, params.projectId);
|
||||
const selectedImageCount = selectedAgentImageCount(params.imagePreferences);
|
||||
const imageNodeIds = nodes.filter((node) => node.type === "image").map((node) => node.id);
|
||||
const defaults = params.selectedNodeIds.filter((id) => imageNodeIds.includes(id));
|
||||
const sdk = await createCanvasCapabilityTools({
|
||||
@ -128,8 +138,8 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
visualCriteria: z.array(z.string().max(200)).max(10),
|
||||
}),
|
||||
async (input) => ({ ...input,
|
||||
...(input.expectedImageCount !== undefined && params.imagePreferences?.parameters?.imageCount !== undefined
|
||||
? { expectedImageCount: params.imagePreferences.parameters.imageCount } : {}),
|
||||
...(input.expectedImageCount !== undefined && selectedImageCount !== undefined
|
||||
? { expectedImageCount: selectedImageCount } : {}),
|
||||
...(params.imagePreferences?.parameters?.aspectRatio && params.imagePreferences.parameters.aspectRatio !== "auto"
|
||||
? { aspectRatio: params.imagePreferences.parameters.aspectRatio } : {}),
|
||||
}),
|
||||
@ -174,7 +184,9 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
async (input) => analyzeRuntimeImages({ ...params, ...input }),
|
||||
);
|
||||
|
||||
let generatedCount = 0;
|
||||
let submittedCount = 0;
|
||||
let occupiedImageSlots = countAgentGeneratedImages(params.priorCalls ?? []);
|
||||
const countLimitMessage = "本次设定的生成张数已占满,请先查看正在生成或已经生成的结果。";
|
||||
register(
|
||||
"generateCanvasImages",
|
||||
"生成图片",
|
||||
@ -189,9 +201,8 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
}),
|
||||
async (toolInput, callId) => {
|
||||
const input = applyAgentImagePreferences(toolInput, params.imagePreferences);
|
||||
// Bound per-task spend without introducing any global/model concurrency queue.
|
||||
if (generatedCount + input.imageCount > 24)
|
||||
throw new Error("本次任务已达到 24 张生成预算,请检查结果后继续。");
|
||||
if (selectedImageCount !== undefined && occupiedImageSlots >= selectedImageCount)
|
||||
throw new AgentToolRejectedError(countLimitMessage);
|
||||
const sources = await readCanvasImages(
|
||||
params.userId,
|
||||
params.projectId,
|
||||
@ -214,10 +225,17 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
if (!chosen) throw new Error("系统未配置可用的图片生成模型");
|
||||
const imageSize = input.imageSize || params.imageSize || "2K";
|
||||
const quota = await checkUserImageQuota(params.userId);
|
||||
if (!quota.allowed || (quota.remaining !== null && input.imageCount > quota.remaining))
|
||||
throw new Error("本月图片生成配额不足。");
|
||||
const points = await getPointsSystemConfig();
|
||||
const user = await getUserById({ id: params.userId });
|
||||
// Reserve the remaining slots before dispatch, including calls still in flight.
|
||||
if (selectedImageCount !== undefined) {
|
||||
input.imageCount = Math.min(input.imageCount, selectedImageCount - occupiedImageSlots);
|
||||
if (input.imageCount < 1) throw new AgentToolRejectedError(countLimitMessage);
|
||||
}
|
||||
if (submittedCount + input.imageCount > 24)
|
||||
throw new AgentToolRejectedError("本次任务已达到 24 张生成预算,请检查结果后继续。");
|
||||
if (!quota.allowed || (quota.remaining !== null && input.imageCount > quota.remaining))
|
||||
throw new Error("本月图片生成配额不足。");
|
||||
const cost =
|
||||
getModelGenerationCreditCost(
|
||||
{ imageGenConfig: chosen.model.imageGenConfig as DiscountableImageGenConfigLike | null },
|
||||
@ -231,7 +249,8 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
(user?.pointsBalance ?? 0) < cost
|
||||
)
|
||||
throw new Error("积分不足,请充值或联系管理员。");
|
||||
generatedCount += input.imageCount;
|
||||
submittedCount += input.imageCount;
|
||||
occupiedImageSlots += input.imageCount;
|
||||
const result = await generateCanvasImage({
|
||||
prompt: input.prompt,
|
||||
model: chosen.model.id,
|
||||
@ -311,6 +330,7 @@ export async function buildCanvasRuntimeTools(params: {
|
||||
},
|
||||
);
|
||||
}
|
||||
occupiedImageSlots += result.images.length - input.imageCount;
|
||||
return {
|
||||
status: result.images.length < input.imageCount ? "partial" : "succeeded",
|
||||
appliedImagePreferences: { modelId: result.modelRef, imageSize, aspectRatio: input.aspectRatio, imageCount: input.imageCount },
|
||||
|
||||
@ -7,7 +7,8 @@ import type { CanvasAgentTool } from "@/lib/ai/canvas-agent-runtime/loop";
|
||||
import { AgentToolRejectedError, publicAgentError, type AgentCallRecord, type AgentRunSnapshot, type CanvasAgentRunStatus } from "@/lib/ai/canvas-agent-runtime/protocol";
|
||||
import { formatRequestFailure, getRequestFailureDetails } from "@/lib/request-error";
|
||||
import { isCompletedSimpleCutout } from "@/lib/ai/canvas-agent-runtime/cutout-completion";
|
||||
import { requiresCanvasInteraction } from "@/lib/ai/canvas-agent-runtime/results";
|
||||
import { countAgentGeneratedImages, requiresCanvasInteraction } from "@/lib/ai/canvas-agent-runtime/results";
|
||||
import { selectedAgentImageCount } from "@/lib/ai/canvas-agent-runtime/image-preferences";
|
||||
import { AgentImageGenerationError } from "@/lib/ai/runtime/image-generation-error";
|
||||
import { publicAssistantText } from "./assistant-output";
|
||||
|
||||
@ -49,6 +50,16 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
if (!details.recommendation) details.recommendation = "请联系管理员并提供请求编号,核对该步骤的错误。";
|
||||
return `${call.title || "创作工具"}:${formatRequestFailure(details, ports.runId)}`;
|
||||
}
|
||||
function imageCountReached() {
|
||||
const count = snapshot.workspace === "video" ? undefined : selectedAgentImageCount(snapshot.imagePreferences);
|
||||
return count !== undefined && countAgentGeneratedImages(snapshot.calls) >= count;
|
||||
}
|
||||
function reviewMessage(issues: string[]) {
|
||||
const summary = `结果待核对:${issues.join(";")}`;
|
||||
return imageCountReached()
|
||||
? `${summary}\n\n已保留本次生成的图片。如需再生成,请发送新指令。`
|
||||
: summary;
|
||||
}
|
||||
let modelError = "";
|
||||
let toolFailure = "";
|
||||
let nativeFailed = false;
|
||||
@ -84,6 +95,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
`可用业务工具:${tools.join("、")}。工具名称带 mcp__canvas__ 前缀。`,
|
||||
"原生文件、Bash、记忆及子任务能力仅作用于此任务的私有工作区,不能通过 shell 直接访问模型服务商或本站后台;业务操作使用 MCP。",
|
||||
"用户只要求生图或改图时,把结果写入画布并验收即可。除非用户明确要求下载或导出,不要主动调用 image_download,也不要为收尾打开交互面板。",
|
||||
"用户设置的生成张数是本次任务的总数,包括验收未通过的已生成图片。达到张数后,保留结果并说明问题;如需再次生成,等待用户发送新指令。",
|
||||
"用户指定的生图模型被明确拒绝时,停止本轮收费操作,询问用户希望切换生图模型还是稍后重试。切换需用户确认并更新模型选择;结果未知的超时必须先核对原任务,不可直接换模型重发。不要为普通图片编辑调用 appearance.post 或其他无关业务接口。",
|
||||
"imagePreferences 是用户在输入框选择的生图模型和图片参数,与当前规划模型不同。生成和编辑图片优先用 generateCanvasImages;用户选定的模型不得被技能默认值、你选的其他模型或重试覆盖。自动选择生图模型时,图片工具内部会在明确失败后切换备用模型并补齐结果,不要自行重放付费操作;用户明确指定模型时禁止故障切换。比例、尺寸、数量、反向提示词等控件必须传入;不要通过其他生成接口绕过用户选择。分析、问答、排版等不需要出图的任务无需生成图片。",
|
||||
"转正面、换角度、旋转主体视角时,直接用 generateCanvasImages,referenceNodeIds 指向要编辑的真实图片,提示词描述目标视角并保留产品结构和未要求修改的细节。旧多视角功能、angleEditImage 和 image.multi_angle 已废弃,即使历史对话用过也不得再调用或打开旧面板。",
|
||||
@ -277,12 +289,12 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
const verification = await ports.verify(reviewSnapshot); snapshot.verification = verification;
|
||||
snapshot.inputTokens = reviewSnapshot.inputTokens; snapshot.outputTokens = reviewSnapshot.outputTokens;
|
||||
if (verification.passed) return await save(result.response || "任务完成,结果已写入画布。", "completed");
|
||||
if (verification.retryable && snapshot.repairs < 2) {
|
||||
if (verification.retryable && snapshot.repairs < 2 && !imageCountReached()) {
|
||||
snapshot.repairs++;
|
||||
prompt = `验收需要定点修复:${JSON.stringify(verification)}。只处理失败结果,禁止整批重新生成;保留所有通过结果。`;
|
||||
continue;
|
||||
}
|
||||
return await save(`结果待核对:${verification.issues.join(";")}`, "needs_review");
|
||||
return await save(reviewMessage(verification.issues), "needs_review");
|
||||
}
|
||||
} catch (error) {
|
||||
if (snapshot.nativeRuntime) snapshot.nativeRuntime.state = "stopped";
|
||||
@ -306,7 +318,7 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
||||
if (control.cancelled || ports.signal.aborted) return await save("已停止后续步骤,完成的结果保留在画布。", "cancelled");
|
||||
if (inputs !== JSON.stringify(snapshot.inputs)) return await save("已收到新的补充要求,图片已保留,请继续处理。", "needs_review");
|
||||
return await save(verification.passed ? "图片已完成并通过检查,结果已保留在画布。"
|
||||
: `图片已返回画布,结果待核对:${verification.issues.join(";")}`, verification.passed ? "completed" : "needs_review");
|
||||
: `图片已返回画布,${reviewMessage(verification.issues)}`, verification.passed ? "completed" : "needs_review");
|
||||
} catch { /* Keep the original interruption if independent inspection also fails. */ }
|
||||
}
|
||||
return await save(cancelled ? "已停止后续步骤,完成的结果保留在画布。" : toolFailure || modelError || publicAgentError(error), cancelled ? "cancelled" : "needs_review");
|
||||
|
||||
@ -1,21 +1,36 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||||
import { applyAgentImagePreferences, agentProviderImageParameters } from "@/lib/ai/canvas-agent-runtime/image-preferences";
|
||||
import type { AgentCallRecord } from "@/lib/ai/canvas-agent-runtime/protocol";
|
||||
|
||||
const preferences = {
|
||||
modelId: "user-image-model",
|
||||
parameters: { imageCount: 3, aspectRatio: "3:4", imageSize: "4K", negativePrompt: "不要文字",
|
||||
quality: "high" as const, size: "1536*2048", promptExtend: false, watermark: false, seed: 0 },
|
||||
};
|
||||
function harness(results?: any[]) {
|
||||
function harness(results?: any[] | ((count: number) => any[])) {
|
||||
const saved: any[] = [];
|
||||
const generated: any[] = [];
|
||||
const operations: any[] = [];
|
||||
const mock = serverLoader({
|
||||
"@/lib/canvas/capabilities/ai-sdk-tools": { createCanvasCapabilityTools: async () => ({ editTargetImage: {} }) },
|
||||
"@/lib/canvas/capabilities/registry": { CANVAS_CAPABILITY_REGISTRY: [{ id: "image.prompt_regenerate", title: "重绘", risk: "external", agent: { toolName: "editTargetImage" } }] },
|
||||
"@/lib/canvas/agent-mutations": { applyCanvasOperationPlan: async (value: any) => { operations.push(value); return { createdNodeIds: ["new-image"] }; } },
|
||||
"@/lib/canvas/generate-image": { generateCanvasImage: async (value: any) => { generated.push(value); return { modelRef: results ? "backup" : value.model, images: results || [{ originalUrl: "https://test.example/result.png", thumbnailUrl: "https://test.example/thumb.png" }] }; } },
|
||||
"@/lib/canvas/agent-mutations": {
|
||||
applyCanvasOperationPlan: async (value: any) => {
|
||||
operations.push(value);
|
||||
return { createdNodeIds: value.plan.mediaItems.map((item: { mediaItemId: string }) => item.mediaItemId) };
|
||||
},
|
||||
},
|
||||
"@/lib/canvas/generate-image": {
|
||||
generateCanvasImage: async (value: any) => {
|
||||
generated.push(value);
|
||||
const images = typeof results === "function" ? results(value.imageCount) : results;
|
||||
return {
|
||||
modelRef: Array.isArray(results) ? "backup" : value.model,
|
||||
images: images ?? [{ originalUrl: "https://test.example/result.png", thumbnailUrl: "https://test.example/thumb.png" }],
|
||||
};
|
||||
},
|
||||
},
|
||||
"@/lib/ai/runtime/generated-image-persistence": { saveGeneratedImages: async (...args: any[]) => { saved.push(args); } },
|
||||
"@/lib/ai/model-router": { routeToModel: async () => ({ id: "system-default" }) },
|
||||
"@/lib/db/queries": { checkUserImageQuota: async () => ({ allowed: true, remaining: 10 }), getPointsSystemConfig: async () => ({ enabled: false }), getUserById: async () => ({}) },
|
||||
@ -27,10 +42,10 @@ function harness(results?: any[]) {
|
||||
"./review": {},
|
||||
"./image-access-tools": { registerImageCanvasAccessTools() {} },
|
||||
});
|
||||
const build = async (imagePreferences: any = preferences) => mock("lib/ai/canvas-agent-runtime/tools.ts").buildCanvasRuntimeTools({
|
||||
const build = async (imagePreferences: any = preferences, priorCalls?: AgentCallRecord[]) => mock("lib/ai/canvas-agent-runtime/tools.ts").buildCanvasRuntimeTools({
|
||||
userId: "u", chatId: "c", projectId: "p", runId: "r", assistantMessageId: "a", selectedNodeIds: ["reference"],
|
||||
request: new Request("https://test.example/api/canvas/agent-chat"), model: {}, supportsVision: true,
|
||||
preferredModelId: "system-default", imagePreferences, emit() {},
|
||||
preferredModelId: "system-default", imagePreferences, priorCalls, emit() {},
|
||||
});
|
||||
return { build, generated, operations, saved };
|
||||
}
|
||||
@ -47,12 +62,35 @@ test("real generation tool honors the chosen image model and all parameters desp
|
||||
providerImageParams: { quality: "high", size: "1536*2048", watermark: false, seed: 0 } });
|
||||
expect(output.appliedImagePreferences).toEqual({ modelId: "user-image-model", imageCount: 3, imageSize: "4K", aspectRatio: "3:4" });
|
||||
});
|
||||
test("redraw and a corrective retry inherit the same user model, batch and image controls", async () => {
|
||||
const h = harness(); const tools = await h.build();
|
||||
await tools.executable.editTargetImage.execute({ nodeIds: ["reference"], args: { prompt: "换背景", modelId: "system-default" } }, "edit-1");
|
||||
await tools.executable.generateCanvasImages.execute({ prompt: "修正背景", modelId: "system-default" }, "retry-1");
|
||||
test("redraw and generation share the task limit and only fill the missing images", async () => {
|
||||
const image = { originalUrl: "https://test.example/result.png", thumbnailUrl: "https://test.example/thumb.png" };
|
||||
const h = harness(count => Array.from({ length: count === 3 ? 1 : count }, () => image));
|
||||
const tools = await h.build();
|
||||
const first = await tools.executable.editTargetImage.execute({ nodeIds: ["reference"], args: { prompt: "换背景", modelId: "system-default" } }, "edit-1");
|
||||
expect(first.status).toBe("partial");
|
||||
const filled = await tools.executable.generateCanvasImages.execute({ prompt: "补齐图片", modelId: "system-default" }, "fill-1");
|
||||
expect(filled.status).toBe("succeeded");
|
||||
await expect(tools.executable.generateCanvasImages.execute({ prompt: "再次修正" }, "retry-1")).rejects.toThrow("张数已占满");
|
||||
expect(h.generated).toHaveLength(2);
|
||||
expect(h.generated.map(call => call.imageCount)).toEqual([3, 2]);
|
||||
for (const invocation of h.generated) expect(invocation).toMatchObject({ model: preferences.modelId, imageSize: "4K", aspectRatio: "3:4", allowFailover: false });
|
||||
});
|
||||
|
||||
test("one selected image allows only one submission even when calls overlap or the task resumes", async () => {
|
||||
const h = harness();
|
||||
const selection = { parameters: { imageCount: 1 } };
|
||||
const tools = await h.build(selection);
|
||||
const generate = (callId: string) => tools.executable.generateCanvasImages.execute({ prompt: "修改图片" }, callId);
|
||||
const [first, duplicate] = await Promise.allSettled([generate("original"), generate("parallel")]);
|
||||
expect(first.status).toBe("fulfilled");
|
||||
expect(duplicate.status).toBe("rejected");
|
||||
if (first.status !== "fulfilled") throw first.reason;
|
||||
const resumed = await h.build(selection, [{ id: "original", name: "generateCanvasImages", input: {}, status: "succeeded", output: first.value }]);
|
||||
await expect(resumed.executable.generateCanvasImages.execute({ prompt: "继续" }, "resume")).rejects.toThrow("张数已占满");
|
||||
expect(h.generated).toHaveLength(1);
|
||||
const next = await h.build(selection);
|
||||
await next.executable.generateCanvasImages.execute({ prompt: "用户新指令" }, "next");
|
||||
expect(h.generated).toHaveLength(2);
|
||||
for (const invocation of h.generated) expect(invocation).toMatchObject({ model: preferences.modelId, imageSize: "4K", imageCount: 3, aspectRatio: "3:4", allowFailover: false });
|
||||
});
|
||||
test("an unavailable explicit choice never silently generates or charges through the default", async () => {
|
||||
const h = harness(); const tools = await h.build({ ...preferences, modelId: "disabled-or-missing" });
|
||||
|
||||
@ -94,6 +94,17 @@ test("checks real image bytes and compares results to their source images", asyn
|
||||
expect(result.issues).toEqual([]);
|
||||
});
|
||||
|
||||
test("a partial image batch can be filled and verified without discarding the first result", async () => {
|
||||
const f = await fixture('{"passed":true,"issues":[],"failedNodeIds":[]}');
|
||||
const { snapshot } = f.params;
|
||||
snapshot.requirements = { expectedImageCount: 2 };
|
||||
snapshot.calls[1].output = { status: "partial", createdNodeIds: ["result"], appliedImagePreferences: { imageCount: 2 } };
|
||||
await expect(verifyCanvasAgentResults(f.params)).resolves.toMatchObject({ passed: false, retryable: true });
|
||||
snapshot.resultNodeIds.push("remaining-result");
|
||||
snapshot.calls.push({ id: "fill", name: "generateCanvasImages", input: {}, status: "succeeded", output: { status: "succeeded", createdNodeIds: ["remaining-result"] } });
|
||||
await expect(verifyCanvasAgentResults(f.params)).resolves.toMatchObject({ passed: true });
|
||||
});
|
||||
|
||||
test("malformed vision JSON switches models, while a valid failed review never triggers failover", async () => {
|
||||
const bad = await fixture("not JSON");
|
||||
const validNegative = await fixture('{"passed":false,"issues":["主体不一致"],"failedNodeIds":["result"]}');
|
||||
|
||||
@ -30,6 +30,26 @@ function fixture() {
|
||||
|
||||
const result = { type: "result", sessionId: "test-session", response: "已完成" };
|
||||
|
||||
test("a failed quality check keeps the single generated image without opening a repair round", async () => {
|
||||
const { ports, run, snapshot } = fixture();
|
||||
snapshot.imagePreferences = { parameters: { imageCount: 1 } };
|
||||
let attempts = 0;
|
||||
ports.tools.generateCanvasImages.execute = async () => ({
|
||||
createdNodeIds: ["generated-image"], appliedImagePreferences: { imageCount: 1 },
|
||||
});
|
||||
ports.native = async options => {
|
||||
attempts++;
|
||||
await options.execute("planCanvasTask", {}, "plan", options.signal!);
|
||||
await options.execute("generateCanvasImages", { referenceNodeIds: ["original"] }, "gen", options.signal!);
|
||||
return { ...result, response: "图片已生成,正在检查" };
|
||||
};
|
||||
ports.verify = async () => ({ passed: false, issues: ["主体细节不符"], retryable: true, failedNodeIds: ["generated-image"] });
|
||||
expect(await run(ports)).toBe("needs_review");
|
||||
expect(attempts).toBe(1);
|
||||
expect(snapshot.resultNodeIds).toEqual(["generated-image"]);
|
||||
expect(snapshot.message).toMatch(/主体细节不符[\s\S]*如需再生成,请发送新指令/);
|
||||
});
|
||||
|
||||
test("an unconfirmed paid tool stops the planner and immediately preserves the reason and original request ID", async () => {
|
||||
const { ports, run, snapshot } = fixture();
|
||||
const emitted: string[] = [], saved: string[] = [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user