haixunMaster/lib/services/viral.ts

137 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
import { prisma, resolvePersona } from "@/lib/db";
import { getOrCreateSettings } from "@/lib/user-settings";
import { getActiveAccountProfile } from "@/lib/account-context";
import { analyzeViralPost } from "@/lib/ai/analyze-viral";
import { parseProviderApiKeys } from "@/lib/ai/keys";
import { replicateViralPost } from "@/lib/ai/replicate-viral";
import { ensureActiveSession } from "@/lib/threads-browser";
import { scrapePostMedia } from "@/lib/threads-browser/media";
import { parseMediaUrls, parseViralAnalysis } from "@/lib/types/viral";
export async function analyzeScanItemViral(scanItemId: string) {
const item = await prisma.scanItem.findUnique({
where: { id: scanItemId },
include: {
replies: { orderBy: { likeCount: "desc" } },
scan: { include: { topic: true } },
},
});
if (!item) throw new Error("找不到貼文");
const session = await ensureActiveSession();
let mediaUrls = parseMediaUrls(item.mediaUrls);
let mediaType = item.mediaType;
if (mediaUrls.length === 0 && item.permalink) {
const media = await scrapePostMedia(session.storageState, item.permalink, session);
mediaUrls = media.urls;
mediaType = media.mediaType;
await prisma.scanItem.update({
where: { id: item.id },
data: {
mediaUrls: JSON.stringify(mediaUrls),
mediaType: media.mediaType,
},
});
}
const settings = await getOrCreateSettings();
const account = await getActiveAccountProfile();
const apiKeys = parseProviderApiKeys(settings.providerApiKeys);
const analysis = await analyzeViralPost({
postText: item.text,
authorName: item.authorName,
likeCount: item.likeCount,
replyCount: item.replyCount,
searchTag: item.searchTag,
mediaUrls,
mediaType,
topicLabel: item.scan.topic.label,
topicBrief: item.scan.topic.brief,
persona: resolvePersona(settings, account),
aiProvider: settings.aiProvider,
aiModel: settings.aiModel,
apiKeys,
replies: item.replies.map((r) => ({
text: r.text,
authorName: r.authorName,
likeCount: r.likeCount,
})),
});
await prisma.scanItem.update({
where: { id: item.id },
data: { viralAnalysis: JSON.stringify(analysis) },
});
return { item: { ...item, mediaUrls: JSON.stringify(mediaUrls), mediaType }, analysis };
}
export async function analyzeScanTopViral(scanId: string, limit = 5) {
const items = await prisma.scanItem.findMany({
where: {
scanId,
qualityTier: { not: "EXCLUDE" },
},
orderBy: [{ combinedScore: "desc" }, { score: "desc" }],
take: limit,
});
const results = [];
for (const item of items) {
const result = await analyzeScanItemViral(item.id);
results.push(result);
}
return results;
}
export async function replicateScanItem(scanItemId: string) {
const item = await prisma.scanItem.findUnique({
where: { id: scanItemId },
include: { scan: { include: { topic: true } } },
});
if (!item) throw new Error("找不到貼文");
const viralAnalysis = parseViralAnalysis(item.viralAnalysis);
if (!viralAnalysis) throw new Error("爆款分析資料損毀,請重新分析後再試");
const settings = await getOrCreateSettings();
const account = await getActiveAccountProfile();
const apiKeys = parseProviderApiKeys(settings.providerApiKeys);
const replica = await replicateViralPost({
originalText: item.text,
authorName: item.authorName,
viralAnalysis,
topicLabel: item.scan.topic.label,
topicBrief: item.scan.topic.brief,
persona: resolvePersona(settings, account),
aiProvider: settings.aiProvider,
aiModel: settings.aiModel,
apiKeys,
});
const draft = await prisma.draft.create({
data: {
accountId: item.scan.topic.accountId,
topicId: item.scan.topicId,
text: replica.text,
angle: replica.angle,
hook: replica.hook,
rationale: `${replica.rationale}\n\n結構筆記${replica.structureNotes}`,
imageBrief: replica.imageBrief,
draftType: "viral-replica",
scanItemId: item.id,
sources: item.permalink ? JSON.stringify([item.permalink]) : null,
referenceNotes: viralAnalysis.keyTakeaways?.join("\n") ?? null,
status: "PENDING",
},
});
return { draft, replica };
}