import "server-only"; import { getEnabledAiProviders } from "@/lib/db/queries"; const QWEN3_VL_MODELS = [ "qwen3-vl-plus", "qwen-vl-plus", ] as const; const QWEN_COMPAT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; function parseAssistantText(payload: any): string { const choices = Array.isArray(payload?.choices) ? payload.choices : []; const content = choices[0]?.message?.content; if (typeof content === "string") { return content.trim(); } if (Array.isArray(content)) { const chunks = content .map((part: any) => (typeof part?.text === "string" ? part.text.trim() : "")) .filter(Boolean); return chunks.join("\n").trim(); } return ""; } async function getQwenImageProviderApiKey() { const providers = await getEnabledAiProviders(); const provider = providers.find( (item) => item.type === "alibaba-image" && typeof item.apiKey === "string" && item.apiKey.trim(), ); if (!provider?.apiKey) { throw new Error("未配置可用的千问生图 Provider API Key"); } return provider.apiKey.trim(); } export async function callQwen3Vl(params: { imageUrl?: string; imageUrls?: string[]; prompt: string; temperature?: number; maxTokens?: number; timeoutMs?: number; retries?: number; }) { const apiKey = await getQwenImageProviderApiKey(); const endpoint = `${QWEN_COMPAT_BASE_URL}/chat/completions`; const timeoutMs = Math.max(10000, Math.min(params.timeoutMs ?? 45000, 120000)); const retries = Math.max(0, Math.min(params.retries ?? 1, 2)); const imageUrls = [ ...(Array.isArray(params.imageUrls) ? params.imageUrls : []), params.imageUrl || "", ] .map((item) => String(item || "").trim()) .filter(Boolean); if (imageUrls.length === 0) { throw new Error("Qwen3-VL 调用缺少图片输入"); } let lastError: Error | null = null; for (const model of QWEN3_VL_MODELS) { for (let attempt = 0; attempt <= retries; attempt += 1) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(endpoint, { method: "POST", signal: controller.signal, headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model, temperature: params.temperature ?? 0.1, max_tokens: params.maxTokens ?? 300, messages: [ { role: "user", content: [ { type: "text", text: params.prompt }, ...imageUrls.map((url) => ({ type: "image_url", image_url: { url, }, })), ], }, ], }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { const message = payload?.error?.message || payload?.message || `HTTP ${response.status}`; throw new Error(message); } const text = parseAssistantText(payload); if (!text) { throw new Error("Qwen3-VL 未返回文本内容"); } return { text, model }; } finally { clearTimeout(timeout); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); const errorText = String(lastError.message || "").toLowerCase(); const isModelNotFound = /model.*not.*found|invalid model|not support/i.test(errorText); const isRetriable = /abort|timeout|timed out|ecconnreset|econnreset|429|rate limit|503/.test(errorText); if (isModelNotFound) { break; } if (isRetriable && attempt < retries) { await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1))); continue; } break; } } } throw lastError || new Error("Qwen3-VL 调用失败"); }