137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
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 };
|
||
}
|