haixunMaster/lib/services/outreach.ts

111 lines
3.6 KiB
TypeScript

import { prisma, resolvePersona } from "@/lib/db";
import { getOrCreateSettings } from "@/lib/user-settings";
import { getActiveAccountProfile } from "@/lib/account-context";
import { generateOutreachCommentDrafts } from "@/lib/ai/generate-replies";
import { parseProviderApiKeys } from "@/lib/ai/keys";
import { resolveProductContextForPlacement } from "@/lib/services/product-catalog";
import { formatProductContextForPrompt, getProductCta } from "@/lib/types/product-context";
import { isPlacementGoal } from "@/lib/types/topic-goal";
export async function generateOutreachForScanItem(
scanItemId: string,
options?: { productBrief?: string | null }
) {
const item = await prisma.scanItem.findUnique({
where: { id: scanItemId },
include: {
replies: { orderBy: { likeCount: "desc" }, take: 5 },
outreachTargets: { include: { drafts: true } },
scan: { include: { topic: true } },
},
});
if (!item) throw new Error("找不到貼文");
const topic = item.scan.topic;
if (!isPlacementGoal(item.scan.scanGoal ?? topic.topicGoal)) {
throw new Error("只有找 TA 任務可以生成主動互動留言");
}
const settings = await getOrCreateSettings();
const account = await getActiveAccountProfile();
const apiKeys = parseProviderApiKeys(settings.providerApiKeys);
const resolvedContext =
options?.productBrief?.trim() ||
(await resolveProductContextForPlacement({
brandProfileId: topic.brandProfileId,
productProfileId: topic.productProfileId,
fallbackContext: topic.productContext,
searchTag: item.searchTag,
})) ||
topic.productContext?.trim() ||
"";
const rawContext = resolvedContext;
const productBrief =
(rawContext ? formatProductContextForPrompt(rawContext) : null) ||
topic.brief?.trim() ||
account?.productBrief?.trim() ||
null;
const { ctaType, ctaUrl } = getProductCta(rawContext);
const generated = await generateOutreachCommentDrafts({
persona: resolvePersona(settings, account),
aiProvider: settings.aiProvider,
aiModel: settings.aiModel,
apiKeys,
topicLabel: topic.label,
audienceBrief: topic.brief,
productBrief,
placementReason: item.placementReason ?? item.qualityReason,
ctaType,
ctaUrl,
targetText: item.text,
authorName: item.authorName,
replies: item.replies,
count: 2,
});
const existing = item.outreachTargets[0];
if (existing) {
await prisma.outreachDraft.deleteMany({ where: { outreachTargetId: existing.id } });
const target = await prisma.outreachTarget.update({
where: { id: existing.id },
data: {
relevance: generated.relevance,
reason: generated.reason,
status: generated.relevance >= 0.55 ? "DRAFTED" : "LOW_RELEVANCE",
drafts: {
create: generated.drafts.map((draft) => ({
text: draft.text,
angle: draft.angle,
rationale: draft.rationale,
})),
},
},
include: { drafts: true, scanItem: { include: { scan: { include: { topic: true } } } } },
});
return target;
}
const target = await prisma.outreachTarget.create({
data: {
scanItemId: item.id,
relevance: generated.relevance,
reason: generated.reason,
status: generated.relevance >= 0.55 ? "DRAFTED" : "LOW_RELEVANCE",
drafts: {
create: generated.drafts.map((draft) => ({
text: draft.text,
angle: draft.angle,
rationale: draft.rationale,
})),
},
},
include: { drafts: true, scanItem: { include: { scan: { include: { topic: true } } } } },
});
return target;
}