haixunMaster/lib/services/viral.ts

137 lines
4.2 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 { 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,
OR: [{ qualityTier: null }, { 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 };
}