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

96 lines
3.5 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
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: {
2026-06-21 16:28:26 +00:00
where: { OR: [{ qualityTier: null }, { qualityTier: { not: "EXCLUDE" } }] },
2026-06-21 12:50:31 +00:00
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 });
}
}