140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
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: "新画布" });
|
||
}
|
||
}
|