haixunMaster/app/api/outreach/generate/route.ts

96 lines
3.4 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 { 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 });
}
}