2026-05-27 13:16:37 +08:00

129 lines
3.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 { 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, "图片分析失败");
}
}