From 160552b45e96db60a69f80eaf10b2fa6bebed408 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Tue, 2 Jun 2026 15:20:23 +0800 Subject: [PATCH] fix(ecommerce): 502 bug - vision model upgrade + MIME normalization + fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade VISION_MODEL to qwen3.7-plus (latest, confirmed working with image_url) - Add VISION_FALLBACK_MODEL = qwen-vl-plus for retry on "image format" errors - Normalize upload MIME types: unsupported formats (HEIC/AVIF) fall back to image/png to prevent server saving as .bin which DashScope can't read - Server-side: add image/avif, image/heic, image/heif to MIME_EXTENSIONS Root cause: DashScope returned "image format is illegal" when uploaded images had unrecognized MIME types → saved as .bin → DashScope couldn't decode. Co-Authored-By: Claude Opus 4.7 --- src/api/adVideoPlanClient.ts | 60 ++++++++++++++---------- src/features/ecommerce/EcommercePage.tsx | 4 +- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/api/adVideoPlanClient.ts b/src/api/adVideoPlanClient.ts index ae700ea..fc29983 100644 --- a/src/api/adVideoPlanClient.ts +++ b/src/api/adVideoPlanClient.ts @@ -1,7 +1,8 @@ import { buildApiUrl, buildAuthHeaders } from "./serverConnection"; const TEXT_MODEL = "qwen-max"; -const VISION_MODEL = "qwen3.6-plus"; +const VISION_MODEL = "qwen3.7-plus"; +const VISION_FALLBACK_MODEL = "qwen-vl-plus"; export interface AdVideoUserConfig { platform: string; @@ -149,30 +150,39 @@ async function visionChat( ...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })), { type: "text", text }, ]; - const timeoutSignal = AbortSignal.timeout(60000); - const combinedSignal = signal - ? AbortSignal.any([signal, timeoutSignal]) - : timeoutSignal; - const res = await fetch(buildApiUrl("ai/chat"), { - method: "POST", - headers: buildAuthHeaders(), - body: JSON.stringify({ - model: VISION_MODEL, - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content }, - ], - stream: false, - temperature: 0.3, - }), - signal: combinedSignal, - }); - if (!res.ok) throw new Error(`图片理解调用失败 (${res.status})`); - const payload = await res.json(); - const out: string = - payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; - if (!out) throw new Error("图片理解未返回有效内容"); - return out; + const messages = [ + { role: "system", content: systemPrompt }, + { role: "user", content }, + ]; + + for (const model of [VISION_MODEL, VISION_FALLBACK_MODEL]) { + const timeoutSignal = AbortSignal.timeout(60000); + const combinedSignal = signal + ? AbortSignal.any([signal, timeoutSignal]) + : timeoutSignal; + try { + const res = await fetch(buildApiUrl("ai/chat"), { + method: "POST", + headers: buildAuthHeaders(), + body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }), + signal: combinedSignal, + }); + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + if (model === VISION_MODEL && errBody.includes("image format")) continue; + throw new Error(`图片理解调用失败 (${res.status})`); + } + const payload = await res.json(); + const out: string = + payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; + if (!out) throw new Error("图片理解未返回有效内容"); + return out; + } catch (err) { + if (model === VISION_MODEL && err instanceof Error && err.message?.includes("图片理解调用失败")) continue; + throw err; + } + } + throw new Error("图片理解调用失败,所有模型均不可用"); } const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`; diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 1f561ba..9774b8a 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -1321,18 +1321,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); const uploadProductImages = async (): Promise => { + const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]); const urls: string[] = []; for (const item of productImages) { try { const resp = await fetch(item.src); const blob = await resp.blob(); + const mimeType = SUPPORTED_IMAGE_TYPES.has(blob.type) ? blob.type : "image/png"; const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result)); reader.onerror = () => reject(reader.error); reader.readAsDataURL(blob); }); - const { url } = await aiGenerationClient.uploadAsset({ dataUrl, name: item.name, mimeType: blob.type }); + const { url } = await aiGenerationClient.uploadAsset({ dataUrl, name: item.name, mimeType }); urls.push(url); } catch { // skip images that fail to upload