Your Name 7141704f65
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: align canvas auto-title model routing with agent multimodal
2026-02-20 21:58:57 +08:00

140 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import { generateText } from "ai";
import {
getAgentAiModelWithProvider,
getEnabledAiModelsWithProvider,
} from "@/lib/db/queries";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
const BLOCKED_PROVIDER_TYPES = new Set([
"fal-video",
"qianwen-video",
"volcano-video",
"doubao-video",
"alibaba-image",
"ruxa-image",
"fal-image",
"volcano-vision",
]);
type VisionModelItem = Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>[number];
function isVisionCandidate(item: VisionModelItem) {
const providerType = String(item.provider?.type || "");
if (BLOCKED_PROVIDER_TYPES.has(providerType)) return false;
if (item.model.supportsVision) return true;
if (isAiModelMultimodal(item.model)) return true;
const modelText = `${item.model.modelId || ""} ${item.model.displayName || ""}`.toLowerCase();
if (/vl|vision|多模态|glm-4v|gpt-4o|doubao-seed-2-0|seed-2-0/.test(modelText)) {
return true;
}
return false;
}
function scoreVisionCandidate(item: VisionModelItem) {
let score = 0;
const providerType = String(item.provider?.type || "");
if (isAiModelMultimodal(item.model)) score += 220;
if (item.model.supportsVision) score += 160;
if (providerType === "volcano-multimodal") score += 80;
if (providerType === "zhipu") score += 50;
if (providerType === "alibaba") score += 40;
if (providerType === "custom") score += 30;
if (item.model.isDefault) score += 20;
score -= Math.max(0, item.model.sortOrder ?? 0);
return score;
}
function pickAutoTitleModel(allModels: VisionModelItem[], agentModel: VisionModelItem | undefined) {
if (agentModel && isVisionCandidate(agentModel)) {
return agentModel;
}
const candidates = allModels.filter((item) => isVisionCandidate(item));
if (candidates.length === 0) return null;
return [...candidates].sort((a, b) => scoreVisionCandidate(b) - scoreVisionCandidate(a))[0];
}
export async function POST(request: NextRequest) {
console.log("[Auto-title] API called");
try {
const session = await auth();
if (!session?.user?.id) {
console.log("[Auto-title] Unauthorized");
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { imageUrl } = await request.json();
console.log("[Auto-title] Image URL:", imageUrl);
if (!imageUrl) {
return NextResponse.json({ error: "Image URL is required" }, { status: 400 });
}
// 从数据库获取视觉模型,优先使用 AGENT 多模态模型
const modelsData = await getEnabledAiModelsWithProvider();
const agentModel = await getAgentAiModelWithProvider();
console.log("[Auto-title] Available models count:", modelsData.length);
console.log("[Auto-title] All model IDs:", modelsData.map(m => m.model.modelId));
const visionModelData = pickAutoTitleModel(modelsData, agentModel);
console.log("[Auto-title] Selected vision model:", {
modelId: visionModelData?.model.modelId,
displayName: visionModelData?.model.displayName,
providerType: visionModelData?.provider?.type,
usedAgentModel: Boolean(agentModel && visionModelData && agentModel.model.id === visionModelData.model.id),
isMultimodal: Boolean(visionModelData && isAiModelMultimodal(visionModelData.model)),
});
if (!visionModelData) {
console.log("[Auto-title] No compatible vision model found, using default title");
return NextResponse.json({ title: "未命名画布" });
}
const visionModel = visionModelData.model;
const model = await getLanguageModelById(visionModel.id);
if (!model) {
console.log("[Auto-title] Failed to load vision model");
return NextResponse.json({ title: "未命名画布" });
}
// 构造 Prompt
const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。";
const { text } = await generateText({
model,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{ type: 'image', image: imageUrl },
],
},
],
temperature: 0.2,
maxOutputTokens: 50,
});
let title = (text || "").trim();
title = title.replace(/[\s\p{P}\p{S}]+/gu, "");
if (title.length > 4) title = title.slice(0, 4);
if (title.length < 2) title = "新画布";
console.log("[Auto-title] Generated title:", title);
return NextResponse.json({ title });
} catch (error) {
console.error("[Auto-title] Error:", error);
// 失败时返回默认标题,不报错中断流程
return NextResponse.json({ title: "新画布" });
}
}