From 60bad10fc927a4067f0f5aba2ba23eae76522e19 Mon Sep 17 00:00:00 2001 From: dyhhhhh <63825522+dyhhhhhh@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:54:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E7=94=BB=E5=B8=83=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=94=9F=E6=88=90=20-=20=E5=8F=82=E8=80=83=E5=9B=BE?= =?UTF-8?q?=E8=B6=85=E9=99=90=E6=97=B6=E5=81=9C=E6=AD=A2=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=B9=B6=E6=8F=90=E7=A4=BA=E6=B8=85=E7=90=86=E8=BF=9E=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留全部已连接参考图,在首次生成、重试和任务入口统一校验模型上限。 --- .../lib/canvas/canvas-project-generation.ts | 17 ++++++++++------- .../canvas-image-generation-executor.ts | 9 +++++++-- .../canvas/use-canvas-generation-retry.ts | 16 +++++++++++++--- web/test/canvas-model-policy.test.ts | 19 ++++++++++++++++++- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/web/src/lib/canvas/canvas-project-generation.ts b/web/src/lib/canvas/canvas-project-generation.ts index 822ab35f2..0ed765e0f 100644 --- a/web/src/lib/canvas/canvas-project-generation.ts +++ b/web/src/lib/canvas/canvas-project-generation.ts @@ -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 }; diff --git a/web/src/pages/canvas/canvas-image-generation-executor.ts b/web/src/pages/canvas/canvas-image-generation-executor.ts index 33393cca7..5392122a7 100644 --- a/web/src/pages/canvas/canvas-image-generation-executor.ts +++ b/web/src/pages/canvas/canvas-image-generation-executor.ts @@ -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]; diff --git a/web/src/pages/canvas/use-canvas-generation-retry.ts b/web/src/pages/canvas/use-canvas-generation-retry.ts index 59934190d..7cbcccffb 100644 --- a/web/src/pages/canvas/use-canvas-generation-retry.ts +++ b/web/src/pages/canvas/use-canvas-generation-retry.ts @@ -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); diff --git a/web/test/canvas-model-policy.test.ts b/web/test/canvas-model-policy.test.ts index 84e1557a8..8faa96862 100644 --- a/web/test/canvas-model-policy.test.ts +++ b/web/test/canvas-model-policy.test.ts @@ -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); + }); +});