fix(ecommerce): 502 bug - vision model upgrade + MIME normalization + fallback

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 15:20:23 +08:00
parent 16bf7bbdad
commit 160552b45e
2 changed files with 38 additions and 26 deletions
+35 -25
View File
@@ -1,7 +1,8 @@
import { buildApiUrl, buildAuthHeaders } from "./serverConnection"; import { buildApiUrl, buildAuthHeaders } from "./serverConnection";
const TEXT_MODEL = "qwen-max"; 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 { export interface AdVideoUserConfig {
platform: string; platform: string;
@@ -149,30 +150,39 @@ async function visionChat(
...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })), ...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })),
{ type: "text", text }, { type: "text", text },
]; ];
const timeoutSignal = AbortSignal.timeout(60000); const messages = [
const combinedSignal = signal { role: "system", content: systemPrompt },
? AbortSignal.any([signal, timeoutSignal]) { role: "user", content },
: timeoutSignal; ];
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST", for (const model of [VISION_MODEL, VISION_FALLBACK_MODEL]) {
headers: buildAuthHeaders(), const timeoutSignal = AbortSignal.timeout(60000);
body: JSON.stringify({ const combinedSignal = signal
model: VISION_MODEL, ? AbortSignal.any([signal, timeoutSignal])
messages: [ : timeoutSignal;
{ role: "system", content: systemPrompt }, try {
{ role: "user", content }, const res = await fetch(buildApiUrl("ai/chat"), {
], method: "POST",
stream: false, headers: buildAuthHeaders(),
temperature: 0.3, body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }),
}), signal: combinedSignal,
signal: combinedSignal, });
}); if (!res.ok) {
if (!res.ok) throw new Error(`图片理解调用失败 (${res.status})`); const errBody = await res.text().catch(() => "");
const payload = await res.json(); if (model === VISION_MODEL && errBody.includes("image format")) continue;
const out: string = throw new Error(`图片理解调用失败 (${res.status})`);
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; }
if (!out) throw new Error("图片理解未返回有效内容"); const payload = await res.json();
return out; 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 = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`; const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`;
+3 -1
View File
@@ -1321,18 +1321,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
}); });
const uploadProductImages = async (): Promise<string[]> => { const uploadProductImages = async (): Promise<string[]> => {
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
const urls: string[] = []; const urls: string[] = [];
for (const item of productImages) { for (const item of productImages) {
try { try {
const resp = await fetch(item.src); const resp = await fetch(item.src);
const blob = await resp.blob(); const blob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(blob.type) ? blob.type : "image/png";
const dataUrl = await new Promise<string>((resolve, reject) => { const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => resolve(String(reader.result)); reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error); reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob); 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); urls.push(url);
} catch { } catch {
// skip images that fail to upload // skip images that fail to upload