From 79755bf9e8d57dfd321748f966f8b84b64d273f5 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 22 Jul 2026 10:12:03 +0800 Subject: [PATCH] fix: verify AtlasCloud frontend model requests --- components/canvas/CanvasChatPanel.tsx | 182 ++++++++++-- .../canvas/chat-panel/canvas-chat-session.ts | 4 + lib/ai/atlascloud-models.ts | 49 ++++ lib/ai/atlascloud-request.ts | 209 ++++++++++++++ lib/ai/providers/atlascloud.ts | 268 ++++-------------- scripts/sync-atlascloud-models.ts | 92 ++++++ scripts/verify-atlascloud-frontend.ts | 204 +++++++++++++ tests/routes/model-classification.test.ts | 68 +++++ 8 files changed, 855 insertions(+), 221 deletions(-) create mode 100644 lib/ai/atlascloud-request.ts create mode 100644 scripts/sync-atlascloud-models.ts create mode 100644 scripts/verify-atlascloud-frontend.ts diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index b021b4b2c..d04dab8b3 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -4335,6 +4335,7 @@ export function CanvasChatPanel({ () => String(siteSettings?.lovartComposerPrefix || "").trim() || CANVAS_COMPOSER_PREFIX_DEFAULT, + maxVideoReferences?: number; [siteSettings?.lovartComposerPrefix] ); const canvasComposerPreviewTexts = useMemo(() => { @@ -4363,6 +4364,7 @@ export function CanvasChatPanel({ }; } + maxVideoReferences?: number; let textIndex = 0; let charIndex = 0; @@ -4860,6 +4862,25 @@ export function CanvasChatPanel({ ); const selectableModels = useMemo(() => { if (activeComposerMode === "detail") { + const atlasVideoReferenceLimit = + currentModel?.providerType === "atlascloud" + ? Math.max( + 0, + Math.min( + 4, + Number( + currentModel.capabilities?.maxVideoReferences || + currentModel.imageGenConfig?.maxVideoReferences || + 0 + ) + ) + ) + : 0; + const supportsAtlasVideoReference = atlasVideoReferenceLimit > 0; + const requiresAtlasVideoReference = Boolean( + supportsAtlasVideoReference && + currentModel?.capabilities?.requiresVideoReference === true + ); return [SMART_AUTO_MODEL]; } if (activeComposerMode === "video") { @@ -11419,6 +11440,20 @@ export function CanvasChatPanel({ } if (isDetailGenerationMode && canUseDetailGeneration) { hiddenSegments.push( + if (supportsAtlasVideoReference) { + let keptVideoCount = 0; + effectiveAttachments = effectiveAttachments.filter((attachment) => { + if (!isLikelyVideoAttachment(attachment.url, attachment.contentType)) { + return true; + } + keptVideoCount += 1; + return keptVideoCount <= atlasVideoReferenceLimit; + }); + if (requiresAtlasVideoReference && keptVideoCount === 0) { + toast.error("当前模型需要先上传 1 个参考视频"); + return; + } + } [ "【AI详情生成模式】", "你正在处理电商详情页任务,请先规划分镜结构,再执行生图。", @@ -12068,6 +12103,53 @@ export function CanvasChatPanel({ clearComposerUploadProgress, hasAgentUploadSlots, isClawdMode, + const uploadVideoFileForComposerAttachment = useCallback(async ( + file: File, + progressOptions?: CanvasUploadProgressOptions + ): Promise => { + if (!file.type.startsWith("video/")) { + toast.error("请选择视频文件"); + return null; + } + if (file.size > 15 * 1024 * 1024) { + toast.error("参考视频大小不能超过 15MB"); + return null; + } + + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("purpose", "canvas-video-process"); + if (progressOptions) updateComposerUploadProgress(progressOptions); + const response = await fetchUploadWithRetry( + formData, + progressOptions + ? { + onProgress: (progress) => { + updateComposerUploadProgress(progressOptions, progress); + }, + } + : undefined + ); + if (!response.ok) throw new Error("上传失败"); + + const data = await response.json(); + clearComposerUploadProgress(250); + return { + imageUrl: String(data.url || ""), + name: file.name || `参考视频-${Date.now()}.mp4`, + contentType: file.type || "video/mp4", + assetId: + typeof data.assetId === "string" && data.assetId.trim() + ? data.assetId.trim() + : undefined, + }; + } catch (_error) { + toast.error("参考视频上传失败"); + return null; + } + }, [clearComposerUploadProgress, updateComposerUploadProgress]); + isVideoModel, maybeAutoSetCanvasTitle, nodes.length, @@ -12400,6 +12482,58 @@ export function CanvasChatPanel({ if (acceptedFiles.length === 0) return; + + if (supportsAtlasVideoReference) { + const selectedVideoFiles = selectedFiles.filter((file) => + file.type.startsWith("video/") + ); + const existingVideoCount = attachments.filter((attachment) => + isLikelyVideoAttachment(attachment.url, attachment.contentType) + ).length; + const availableVideoSlots = Math.max( + 0, + atlasVideoReferenceLimit - existingVideoCount + ); + const acceptedVideoFiles = selectedVideoFiles.slice(0, availableVideoSlots); + + if (selectedVideoFiles.length > acceptedVideoFiles.length) { + toast.info(`当前模型最多支持 ${atlasVideoReferenceLimit} 个参考视频`); + } + if (acceptedVideoFiles.length > 0) { + let uploadedCount = 0; + try { + for (const [index, file] of acceptedVideoFiles.entries()) { + const uploaded = await uploadVideoFileForComposerAttachment(file, { + title: "正在上传参考视频", + fileName: file.name, + fileIndex: index, + fileTotal: acceptedVideoFiles.length, + target: "composer-reference", + }); + if (!uploaded?.imageUrl) continue; + addUploadedImageAttachmentToComposer(uploaded); + uploadedCount += 1; + } + } finally { + clearComposerUploadProgress(450); + } + if (uploadedCount > 0) { + toast.success( + uploadedCount === 1 + ? "参考视频已添加到对话" + : `已添加 ${uploadedCount} 个参考视频` + ); + } + } + + selectedFiles = selectedFiles.filter( + (file) => !file.type.startsWith("video/") + ); + if (selectedFiles.length === 0) { + e.target.value = ""; + return; + } + } setClawdTableFiles((currentFiles) => { const existingSignatures = new Set( currentFiles.map( @@ -13881,19 +14015,25 @@ export function CanvasChatPanel({ : "border border-black/5 bg-white/90 text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100" )} > - { - if (event.currentTarget.src !== fileUrl) { - event.currentTarget.src = fileUrl; - } - }} - /> - {part.name || 'Image'} + {isVideoAttachment ? ( +