fix(canvas): 画布图片生成 - 参考图超限时停止生成并提示清理连线

保留全部已连接参考图,在首次生成、重试和任务入口统一校验模型上限。
This commit is contained in:
dyhhhhh
2026-08-17 16:54:56 +08:00
committed by GitHub
parent fddda497d0
commit 60bad10fc9
4 changed files with 48 additions and 13 deletions

View File

@@ -52,14 +52,14 @@ export async function runBackendCanvasGenerationTask(
},
dependencies?: GenerationTaskDependencies,
) {
const normalizedReferenceImages = mode === "image" ? limitCanvasImageReferences(config, referenceImages) : referenceImages;
if (mode === "image") assertCanvasImageReferenceLimit(config, referenceImages);
return runBackendGenerationTask(
{
projectId,
mode,
prompt,
config,
referenceImages: normalizedReferenceImages,
referenceImages,
referenceVideos,
referenceAudios,
mask,
@@ -74,12 +74,15 @@ export async function runBackendCanvasGenerationTask(
);
}
// Canvas references can include a scene frame plus multiple character turnarounds. Keep
// the earliest connected scene image when a provider accepts fewer images than the graph.
export function limitCanvasImageReferences(config: AiConfig, referenceImages: ReferenceImage[]) {
export function canvasImageReferenceLimitError(config: AiConfig, referenceImages: ReferenceImage[]) {
const maxImages = modelCapabilityConfigFor(config, config.model).image?.references.maxImages;
if (maxImages === undefined || maxImages < 1 || referenceImages.length <= maxImages) return referenceImages;
return referenceImages.slice(0, maxImages);
if (maxImages === undefined || referenceImages.length <= maxImages) return "";
return `当前图片模型最多支持 ${maxImages} 张参考图,当前已连接 ${referenceImages.length} 张。请移除多余连线后重试`;
}
export function assertCanvasImageReferenceLimit(config: AiConfig, referenceImages: ReferenceImage[]) {
const error = canvasImageReferenceLimitError(config, referenceImages);
if (error) throw new Error(error);
}
export { backendProviderConfig };

View File

@@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { canGenerateImageInPlace, findAvailableGenerationGroupPosition, imageGenerationChildPosition, imageGenerationGroupSize } from "@/lib/canvas/canvas-generation-layout";
import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
import { buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, limitCanvasImageReferences, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
import { canvasImageReferenceLimitError, buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
import { CONTENT_MODERATION_ERROR_CODE, generationFailureMetadata, type GenerationFailureMetadata } from "@/lib/generation-error";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
@@ -39,13 +39,18 @@ export async function executeImageGeneration({
showError,
registerPendingNodeIds,
}: CanvasGenerationExecution) {
const referenceLimitError = canvasImageReferenceLimitError(generationConfig, generationContext.referenceImages);
if (referenceLimitError) {
showError(referenceLimitError);
return;
}
const count = getGenerationCount(generationConfig.count);
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
const isImageNode = sourceNode?.type === CanvasNodeType.Image;
const reuseSourceNode = canGenerateImageInPlace(sourceNode);
const directCopiedBatch = count > 1 && isImageNode && Boolean(sourceNode?.metadata?.content) && (Boolean(sourceNode?.metadata?.copiedFromNodeId) || sourceNode?.title.endsWith(" Copy"));
// 已有图片生成新结果并保留旧版本;参考图只来自入边,避免把旧结果误当成自身输入。
const referenceImages = limitCanvasImageReferences(generationConfig, generationContext.referenceImages);
const referenceImages = generationContext.referenceImages;
const generationType = referenceImages.length ? ("edit" as const) : ("generation" as const);
const generationMetadata = buildImageGenerationMetadata(generationType, generationConfig, count, referenceImages);
const parentConfig = NODE_DEFAULT_SIZE[isConfigNode ? CanvasNodeType.Config : isImageNode ? CanvasNodeType.Image : CanvasNodeType.Text];

View File

@@ -13,7 +13,7 @@ import {
findRetrySourceNode,
generationReferenceUrls,
isGenerationCanceled,
limitCanvasImageReferences,
canvasImageReferenceLimitError,
resolveMetadataReferences,
resolveStoredReferenceImages,
runBackendCanvasGenerationTask,
@@ -108,7 +108,10 @@ export function useCanvasGenerationRetry({
try {
const promptOnly = retryMode === "video";
const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt, assets, promptOnly);
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly);
rawContext =
hasSavedImageMetadata && !baseContext.characterReferences.length
? null
: await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly);
} catch (error) {
const failure = generationFailureMetadata(error, retryPromptSource);
message.error(failure.errorDetails);
@@ -168,7 +171,14 @@ export function useCanvasGenerationRetry({
message.error("参考图片已丢失,无法继续重试");
return;
}
const retryImages = retryMode === "image" ? limitCanvasImageReferences(generationConfig, retryReferenceImages || []) : retryReferenceImages || [];
const retryImages = retryReferenceImages || [];
if (retryMode === "image") {
const referenceLimitError = canvasImageReferenceLimitError(generationConfig, retryImages);
if (referenceLimitError) {
message.error(referenceLimitError);
return;
}
}
const storedVideoImages = node.type === CanvasNodeType.Video && !context?.referenceImages.length ? await resolveStoredReferenceImages(node.metadata?.references) : [];
if (storedVideoImages === null) {
markMissingReferences(node.id, setNodes);

View File

@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
import { buildGenerationConfig, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
import { assertCanvasImageReferenceLimit, buildGenerationConfig, canvasImageReferenceLimitError, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel } from "../src/lib/model-selection";
import { defaultConfig, type AiConfig, type ModelChannel } from "../src/stores/use-config-store";
@@ -124,3 +124,20 @@ describe("画布连线能力", () => {
expect(canvasConnectionError(config, nodes, [], { fromNodeId: "character", toNodeId: "target" })).toBe("");
});
});
describe("图片参考图上限", () => {
test("超出当前模型上限时保留全部输入并返回明确错误", () => {
const config = policyConfig();
const modelCost = config.channels[0]?.modelCosts?.[0];
if (!modelCost?.capabilityConfig?.image) throw new Error("缺少图片能力配置");
modelCost.capabilityConfig.image.references.maxImages = 1;
const references = [
{ id: "image-a", name: "image-a.png", type: "image/png", dataUrl: "data:image/png;base64,YQ==" },
{ id: "image-b", name: "image-b.png", type: "image/png", dataUrl: "data:image/png;base64,Yg==" },
];
expect(canvasImageReferenceLimitError(config, references)).toContain("最多支持 1 张参考图,当前已连接 2 张");
expect(() => assertCanvasImageReferenceLimit(config, references)).toThrow("请移除多余连线后重试");
expect(references).toHaveLength(2);
});
});