129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
import { generateText } from "ai";
|
||
import { NextRequest, NextResponse } from "next/server";
|
||
import { z } from "zod";
|
||
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
||
import { createLogger } from "@/lib/observability/logger";
|
||
import {
|
||
PRODUCT_PLANNING_ADVANCED_DISABLED,
|
||
PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE,
|
||
} from "@/lib/product-planning/advanced-disabled";
|
||
import { resolveProductPlanningVisionModel } from "@/lib/product-planning/model-runtime";
|
||
import {
|
||
isProductPlanningEnabled,
|
||
PRODUCT_PLANNING_DISABLED_MESSAGE,
|
||
} from "@/lib/product-planning/server-feature";
|
||
import { getProductPlanningToken } from "@/lib/product-planning/server-auth";
|
||
import type { ProductPlanningAnalyzeResponse } from "@/lib/product-planning/types";
|
||
|
||
export const runtime = "nodejs";
|
||
export const maxDuration = 120;
|
||
|
||
const logger = createLogger("product-planning-analyze-image-api");
|
||
|
||
const requestSchema = z.object({
|
||
itemId: z.string().trim().min(1),
|
||
imageUrl: z.string().trim().url(),
|
||
title: z.string().trim().optional(),
|
||
});
|
||
|
||
function jsonError(status: number, error: string) {
|
||
const payload: ProductPlanningAnalyzeResponse = {
|
||
success: false,
|
||
error,
|
||
};
|
||
return NextResponse.json(payload, { status });
|
||
}
|
||
|
||
function buildImageAnalyzePrompt(params: { itemId: string; title?: string }) {
|
||
return [
|
||
"你是一名资深电商视觉策划顾问,请分析这张竞品主图,并输出结构化 markdown。",
|
||
`商品ID:${params.itemId}`,
|
||
params.title ? `商品标题:${params.title}` : "",
|
||
"",
|
||
"请至少包含以下部分:",
|
||
"1. 画面场景与拍摄方式",
|
||
"2. 产品外观与结构要点",
|
||
"3. 当前主图正在强调的核心卖点",
|
||
"4. 用户感知风险或信息缺口",
|
||
"5. 给我们做同类商品主图时可复用的优化建议",
|
||
"",
|
||
"要求:",
|
||
"- 用中文输出",
|
||
"- 结论明确,避免空泛形容词",
|
||
"- 结合电商转化场景给出建议",
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n");
|
||
}
|
||
|
||
export async function POST(request: NextRequest) {
|
||
if (PRODUCT_PLANNING_ADVANCED_DISABLED) {
|
||
return jsonError(503, PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE);
|
||
}
|
||
|
||
const token = await getProductPlanningToken(request);
|
||
if (!token?.sub) {
|
||
return jsonError(401, "未登录");
|
||
}
|
||
if (!(await isProductPlanningEnabled())) {
|
||
return jsonError(403, PRODUCT_PLANNING_DISABLED_MESSAGE);
|
||
}
|
||
|
||
let body: unknown;
|
||
try {
|
||
body = await request.json();
|
||
} catch {
|
||
return jsonError(400, "请求体不是合法 JSON");
|
||
}
|
||
|
||
const parsed = requestSchema.safeParse(body);
|
||
if (!parsed.success) {
|
||
return jsonError(400, "请求参数不合法");
|
||
}
|
||
|
||
const resolvedModel = await resolveProductPlanningVisionModel();
|
||
if (!resolvedModel.model) {
|
||
return jsonError(500, resolvedModel.error || "当前站点没有可用的图片分析模型");
|
||
}
|
||
|
||
try {
|
||
const { inlineDataUrls } = await inlineVisionImages([parsed.data.imageUrl], {
|
||
maxImages: 1,
|
||
maxSide: 900,
|
||
timeoutMs: 12000,
|
||
retries: 1,
|
||
loggerPrefix: "[ProductPlanning][AnalyzeImage]",
|
||
});
|
||
const effectiveImage = inlineDataUrls[0] || parsed.data.imageUrl;
|
||
|
||
const { text } = await generateText({
|
||
model: resolvedModel.model,
|
||
messages: [
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{
|
||
type: "text",
|
||
text: buildImageAnalyzePrompt(parsed.data),
|
||
},
|
||
{
|
||
type: "image",
|
||
image: effectiveImage,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
temperature: 0.2,
|
||
maxTokens: 1200,
|
||
} as any);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
markdown: text,
|
||
} satisfies ProductPlanningAnalyzeResponse);
|
||
} catch (error) {
|
||
logger.error("request_failed", error);
|
||
return jsonError(502, "图片分析失败");
|
||
}
|
||
}
|