153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
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[];
|
|
model?: 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 调用缺少图片输入");
|
|
}
|
|
|
|
const preferredModel = String(params.model || "").trim();
|
|
const models = Array.from(
|
|
new Set([
|
|
...(/^qwen.*(?:vl|vision)/i.test(preferredModel) ? [preferredModel] : []),
|
|
...QWEN3_VL_MODELS,
|
|
])
|
|
);
|
|
|
|
let lastError: Error | null = null;
|
|
|
|
for (const model of 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 调用失败");
|
|
}
|