96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { ZodError } from "zod";
|
||
import { prisma } from "@/lib/db";
|
||
import { getActiveAccountId } from "@/lib/account-context";
|
||
import { generateOutreachForScanItem } from "@/lib/services/outreach";
|
||
import { trackAiTask } from "@/lib/jobs/track";
|
||
import { isPlacementGoal } from "@/lib/types/topic-goal";
|
||
|
||
export const maxDuration = 120;
|
||
|
||
export async function POST(request: Request) {
|
||
try {
|
||
const body = (await request.json()) as {
|
||
scanItemId?: string;
|
||
scanId?: string;
|
||
limit?: number;
|
||
productBrief?: string | null;
|
||
productContext?: string | null;
|
||
topicId?: string;
|
||
};
|
||
|
||
if (body.scanItemId) {
|
||
const accountId = await getActiveAccountId();
|
||
const item = await prisma.scanItem.findUnique({
|
||
where: { id: body.scanItemId },
|
||
include: { scan: { include: { topic: true } } },
|
||
});
|
||
if (!item || (accountId && item.scan.accountId !== accountId)) {
|
||
return NextResponse.json({ error: "找不到貼文" }, { status: 404 });
|
||
}
|
||
if (!isPlacementGoal(item.scan.scanGoal ?? item.scan.topic.topicGoal)) {
|
||
return NextResponse.json({ error: "這篇來自拷貝忍者,不會送到找 TA" }, { status: 400 });
|
||
}
|
||
|
||
if (body.productContext !== undefined && body.productContext !== null) {
|
||
await prisma.topic.update({
|
||
where: { id: item.scan.topicId },
|
||
data: { productContext: body.productContext.trim() || null },
|
||
});
|
||
}
|
||
|
||
// 傳「原始」品牌資料(JSON),讓 service 自行格式化並取出 CTA/網址
|
||
const rawBrief = body.productBrief?.trim() || body.productContext?.trim() || undefined;
|
||
|
||
const target = await trackAiTask("生成找 TA 話術", () =>
|
||
generateOutreachForScanItem(body.scanItemId!, { productBrief: rawBrief })
|
||
);
|
||
return NextResponse.json({ target });
|
||
}
|
||
|
||
if (!body.scanId) {
|
||
return NextResponse.json({ error: "缺少 scanItemId 或 scanId" }, { status: 400 });
|
||
}
|
||
|
||
const accountId = await getActiveAccountId();
|
||
|
||
const scan = await prisma.scan.findUnique({
|
||
where: { id: body.scanId },
|
||
include: {
|
||
topic: true,
|
||
items: {
|
||
where: { qualityTier: { not: "EXCLUDE" } },
|
||
orderBy: [{ combinedScore: "desc" }, { score: "desc" }],
|
||
take: body.limit ?? 8,
|
||
include: { replies: { orderBy: { likeCount: "desc" }, take: 5 } },
|
||
},
|
||
},
|
||
});
|
||
if (!scan || (accountId && scan.accountId !== accountId)) {
|
||
return NextResponse.json({ error: "找不到海巡紀錄" }, { status: 404 });
|
||
}
|
||
if (!isPlacementGoal(scan.scanGoal ?? scan.topic.topicGoal)) {
|
||
return NextResponse.json({ error: "只有找 TA 任務可以批次生成留言" }, { status: 400 });
|
||
}
|
||
|
||
const created = await trackAiTask("批次生成找 TA 話術", async () => {
|
||
const targets = [];
|
||
for (const item of scan.items) {
|
||
targets.push(await generateOutreachForScanItem(item.id));
|
||
}
|
||
return targets;
|
||
});
|
||
|
||
return NextResponse.json({ targets: created });
|
||
} catch (error) {
|
||
if (error instanceof ZodError) {
|
||
return NextResponse.json(
|
||
{ error: "AI 回傳格式不完整(缺少留言正文),請再試一次" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
const message = error instanceof Error ? error.message : "生成獲客留言失敗";
|
||
return NextResponse.json({ error: message }, { status: 500 });
|
||
}
|
||
}
|