111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
import { generateText } from "ai";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { createLogger } from "@/lib/observability/logger";
|
|
import { buildProductPlanningAnalyzePrompt } from "@/lib/product-planning/analyze";
|
|
import { resolveProductPlanningTextModel } 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 {
|
|
ProductPlanningAnalyzeRequest,
|
|
ProductPlanningAnalyzeResponse,
|
|
} from "@/lib/product-planning/types";
|
|
|
|
export const runtime = "nodejs";
|
|
export const maxDuration = 120;
|
|
|
|
const logger = createLogger("product-planning-analyze-api");
|
|
|
|
const requestSchema = z.union([
|
|
z.object({
|
|
kind: z.literal("review"),
|
|
rows: z
|
|
.array(
|
|
z.object({
|
|
itemId: z.string(),
|
|
date: z.string(),
|
|
content: z.string(),
|
|
appendComment: z.string().optional(),
|
|
sku: z.string().optional(),
|
|
userId: z.string().optional(),
|
|
images: z.array(z.string()),
|
|
videoUrls: z.array(z.string()),
|
|
})
|
|
)
|
|
.min(1),
|
|
}),
|
|
z.object({
|
|
kind: z.literal("qa"),
|
|
rows: z
|
|
.array(
|
|
z.object({
|
|
itemId: z.string(),
|
|
question: z.string(),
|
|
questionDetail: z.string().optional(),
|
|
answer: z.string(),
|
|
answerUser: z.string().optional(),
|
|
answerDate: z.string().optional(),
|
|
isOfficial: z.boolean(),
|
|
})
|
|
)
|
|
.min(1),
|
|
}),
|
|
]);
|
|
|
|
function jsonError(status: number, error: string) {
|
|
const payload: ProductPlanningAnalyzeResponse = {
|
|
success: false,
|
|
error,
|
|
};
|
|
return NextResponse.json(payload, { status });
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
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, "请求参数不合法或 rows 为空");
|
|
}
|
|
|
|
const resolvedModel = await resolveProductPlanningTextModel();
|
|
if (!resolvedModel.model) {
|
|
return jsonError(500, resolvedModel.error || "当前站点没有可用的文本分析模型");
|
|
}
|
|
|
|
try {
|
|
const analyzeRequest = parsed.data as ProductPlanningAnalyzeRequest;
|
|
const prompt = buildProductPlanningAnalyzePrompt(analyzeRequest);
|
|
const { text } = await generateText({
|
|
model: resolvedModel.model,
|
|
prompt,
|
|
temperature: 0.3,
|
|
});
|
|
|
|
const payload: ProductPlanningAnalyzeResponse = {
|
|
success: true,
|
|
markdown: text,
|
|
};
|
|
return NextResponse.json(payload);
|
|
} catch (error) {
|
|
logger.error("request_failed", error);
|
|
return jsonError(502, "AI 分析失败");
|
|
}
|
|
}
|