diff --git a/backend/internal/app/logical_models_test.go b/backend/internal/app/logical_models_test.go index a43483a78..fefd2bc59 100644 --- a/backend/internal/app/logical_models_test.go +++ b/backend/internal/app/logical_models_test.go @@ -112,6 +112,14 @@ func TestMatchCapabilityRoutesTextImagesOnlyToDeclaredProfiles(t *testing.T) { } } +func TestMatchCapabilityRoutesUndeclaredVideoToTextMultimodalProfile(t *testing.T) { + profile := CapabilitySpec{Version: 1, Capability: "text"} + intent := ModelRequestIntent{Capability: "text", Inputs: map[string]int{"video": 1}} + if match := MatchCapability(profile, intent); !match.Matched { + t.Fatalf("multimodal text profile should accept legacy undeclared video input: %#v", match) + } +} + func TestMatchCapabilityAcceptsWildcardOptionValues(t *testing.T) { spec := CapabilitySpec{ Version: 1, diff --git a/backend/internal/app/model_router.go b/backend/internal/app/model_router.go index 0e0e047e4..78a386e0c 100644 --- a/backend/internal/app/model_router.go +++ b/backend/internal/app/model_router.go @@ -274,9 +274,12 @@ func MatchCapability(spec CapabilitySpec, intent ModelRequestIntent) CapabilityM } constraint, declared := spec.Inputs[inputType] if !declared { - // Multimodal text models may accept reference images even when the - // provider catalog does not explicitly declare the image input. - if count > 0 && !(normalizeCapability(spec.Capability) == "text" && inputType == "image") { + // Multimodal text models may accept reference images or videos even + // when the provider catalog does not explicitly declare the input. + // The concrete model capability still controls the frontend limits; + // this fallback keeps older catalog entries routable for known + // multimodal text endpoints. + if count > 0 && !(normalizeCapability(spec.Capability) == "text" && (inputType == "image" || inputType == "video")) { reasons = append(reasons, "不支持 "+capabilityInputLabel(inputType)+"输入") } continue diff --git a/web/src/components/canvas/canvas-batch-table-node.tsx b/web/src/components/canvas/canvas-batch-table-node.tsx index a89700ea1..096e9e5dc 100644 --- a/web/src/components/canvas/canvas-batch-table-node.tsx +++ b/web/src/components/canvas/canvas-batch-table-node.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; import { Button, Input, Select, Switch, Tooltip } from "antd"; -import { GripVertical, Image as ImageIcon, LoaderCircle, Play, Plus, RefreshCw, Rows3, Trash2, Upload, X } from "lucide-react"; +import { Film, GripVertical, Image as ImageIcon, LoaderCircle, Play, Plus, RefreshCw, Rows3, Trash2, Upload, X } from "lucide-react"; import { CachedResourceImage } from "@/components/cached-resource-image"; import { BATCH_REFERENCE_HANDLE_GAP, BATCH_REFERENCE_HANDLE_TOP, MAX_BATCH_REFERENCE_COLUMNS, MAX_BATCH_TEXT_COLUMNS, batchGridTemplateColumns, batchPromptForRow, batchReferenceColumns, batchReferenceHandleId, batchTextColumns, batchTextHandleId, batchTextHandleTop } from "@/lib/canvas/canvas-batch-table"; @@ -19,6 +19,7 @@ type Props = { onUpdateRow: (rowId: string, patch: Partial) => void; onFillRows: () => void; onGenerate: (rowIds?: string[]) => void; + onCreateStoryboard?: () => void; onRetryItem: (batchId: string, itemId: string) => void; onAddReferenceColumn: () => void; onRemoveReferenceColumn?: () => void; @@ -41,7 +42,7 @@ const OPERATION_OPTIONS = [ type ReferenceCell = { rowId: string; columnIndex: number }; -export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, theme, onPatchTable, onAddRow, onRemoveRow, onUpdateRow, onFillRows, onGenerate, onRetryItem, onAddReferenceColumn, onRemoveReferenceColumn: _onRemoveReferenceColumn, onFocusOutput: _onFocusOutput, onAddTextColumn = () => {}, onReorderReferenceColumns, onMoveReferenceCell, onReplaceReference = () => {}, onUploadReference, onRemoveReference = () => {}, onConnectStart, onConnectDrop, readOnly = false }: Props) { +export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, theme, onPatchTable, onAddRow, onRemoveRow, onUpdateRow, onFillRows, onGenerate, onCreateStoryboard, onRetryItem, onAddReferenceColumn, onRemoveReferenceColumn: _onRemoveReferenceColumn, onFocusOutput: _onFocusOutput, onAddTextColumn = () => {}, onReorderReferenceColumns, onMoveReferenceCell, onReplaceReference = () => {}, onUploadReference, onRemoveReference = () => {}, onConnectStart, onConnectDrop, readOnly = false }: Props) { const table = node.metadata?.batchTable || { operation: "try_on" as const, concurrency: 10, rows: [] }; const referenceColumns = batchReferenceColumns(table); const textColumns = batchTextColumns(table); @@ -49,9 +50,10 @@ export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, t const hasGlobalPrompt = Boolean(globalPrompt.trim()); const nodeById = useMemo(() => new Map(nodes.map((item) => [item.id, item])), [nodes]); const batchItemByRowId = useMemo(() => new Map((batch?.items || []).map((item) => [item.rowId, item])), [batch?.items]); - const connectedImageCount = useMemo(() => new Set(connections.filter((connection) => connection.toNodeId === node.id).map((connection) => connection.fromNodeId)).size, [connections, node.id]); + const connectedMediaCount = useMemo(() => new Set(connections.filter((connection) => connection.toNodeId === node.id).map((connection) => connection.fromNodeId)).size, [connections, node.id]); const completed = table.rows.filter((row) => Boolean(row.outputNodeId && nodeById.get(row.outputNodeId)?.metadata?.content)).length; const isAiTable = Boolean(table.aiGenerated); + const isStoryboardTable = table.contentKind === "storyboard" && Boolean(table.storyboardRows?.length); const gridTemplateColumns = batchGridTemplateColumns(referenceColumns.length, textColumns.length); const [draggingColumnId, setDraggingColumnId] = useState(null); const [draggingCell, setDraggingCell] = useState(null); @@ -244,7 +246,7 @@ export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, t aria-label="并发数" /> - {referenceColumns.length} 组参考 · 已连 {connectedImageCount} 张图 · 完成 {completed}/{table.rows.length} + {referenceColumns.length} 组参考 · 已连 {connectedMediaCount} 个素材 · 完成 {completed}/{table.rows.length} {!readOnly ? (
@@ -268,6 +270,11 @@ export function CanvasBatchTableNodeContent({ node, nodes, connections, batch, t + {isStoryboardTable && onCreateStoryboard ? ( + + ) : null} @@ -590,7 +597,7 @@ function ReferenceThumbnail({ node, theme, readOnly, rowId, columnIndex, columnI onLostPointerCapture={onLostPointerCapture} onDragStart={(event) => event.preventDefault()} > - + {!readOnly ? 替换 : null} {!readOnly ? (