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,
|
|||
|
|
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 };
|
|||
|
|
}
|