import { z } from "zod"; import type { ProviderApiKeys } from "./keys"; import { generateStructuredObject } from "./generate-structured"; import { getModel } from "./provider"; import { withAgentSystem } from "./agent"; import type { RawPost } from "@/lib/ranking"; type UnknownRecord = Record; function asRecord(value: unknown): UnknownRecord | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as UnknownRecord) : null; } function firstValue(record: UnknownRecord, keys: string[]): unknown { for (const key of keys) { if (record[key] !== undefined) return record[key]; } return undefined; } function normalizeDimension(value: unknown) { if (typeof value === "string") return { summary: value, evidence: [] }; const record = asRecord(value); if (!record) return undefined; const summary = firstValue(record, ["summary", "description", "analysis", "conclusion", "摘要", "結論"]); const rawEvidence = firstValue(record, ["evidence", "examples", "quotes", "proof", "證據", "例句"]); const evidence = Array.isArray(rawEvidence) ? rawEvidence.filter((item): item is string => typeof item === "string").slice(0, 4) : typeof rawEvidence === "string" ? [rawEvidence] : []; return typeof summary === "string" ? { summary, evidence } : undefined; } export function normalizeStyle8DOutput(value: unknown): unknown { let root = asRecord(value); if (!root) return value; for (const key of ["analysis", "style8d", "style8D", "result", "data", "output"]) { const nested = asRecord(root[key]); if (nested && !root.d1Tone && !root.d1) { root = { ...root, ...nested }; } } const dimensions = Array.isArray(root.dimensions) ? root.dimensions : []; const dimensionAt = (index: number, aliases: string[]) => { const direct = firstValue(root!, aliases); if (direct !== undefined) return normalizeDimension(direct); const row = dimensions[index]; return normalizeDimension(row); }; const personaValue = firstValue(root, ["personaDraft", "persona", "persona_draft", "voiceProfile", "人設草稿"]); const personaRecord = asRecord(personaValue); const personaString = typeof personaValue === "string" ? personaValue : ""; const personaDraft = { identity: String(firstValue(personaRecord ?? {}, ["identity", "role", "我是誰", "定位"]) ?? personaString), tone: String(firstValue(personaRecord ?? {}, ["tone", "voice", "語氣"]) ?? "依照 D1 與 D6 執行"), audience: String(firstValue(personaRecord ?? {}, ["audience", "targetAudience", "受眾"]) ?? "依帳號目標受眾"), hooks: String(firstValue(personaRecord ?? {}, ["hooks", "openings", "開場"]) ?? "依照 D2 執行"), examples: String(firstValue(personaRecord ?? {}, ["examples", "sample", "範例"]) ?? "依照 D1–D6 抽象仿寫"), avoid: String(firstValue(personaRecord ?? {}, ["avoid", "risks", "避免"]) ?? "依照 D8 執行"), }; return { d1Tone: dimensionAt(0, ["d1Tone", "d1", "D1", "tone", "D1 語氣人格", "語氣人格"]), d2Structure: dimensionAt(1, ["d2Structure", "d2", "D2", "structure", "D2 結構模板", "結構模板"]), d3Interaction: dimensionAt(2, ["d3Interaction", "d3", "D3", "interaction", "D3 互動方式", "互動方式"]), d4Topics: dimensionAt(3, ["d4Topics", "d4", "D4", "topics", "D4 主題分布", "主題分布"]), d5Rhythm: dimensionAt(4, ["d5Rhythm", "d5", "D5", "rhythm", "D5 發文節奏", "發文節奏"]), d6Visual: dimensionAt(5, ["d6Visual", "d6", "D6", "visual", "D6 視覺語法", "視覺語法"]), d7Conversion: dimensionAt(6, ["d7Conversion", "d7", "D7", "conversion", "D7 轉換方式", "轉換方式"]), d8Risk: dimensionAt(7, ["d8Risk", "d8", "D8", "risk", "D8 風險紅線", "風險紅線"]), personaDraft, }; } const dimension = z.object({ summary: z.string(), evidence: z.array(z.string()).max(4) }); const style8dSchema = z.object({ d1Tone: dimension, d2Structure: dimension, d3Interaction: dimension, d4Topics: dimension, d5Rhythm: dimension, d6Visual: dimension, d7Conversion: dimension, d8Risk: dimension, personaDraft: z.object({ identity: z.string(), tone: z.string(), audience: z.string(), hooks: z.string(), examples: z.string(), avoid: z.string(), }), }); export type Style8DAnalysis = z.infer; export interface BenchmarkEngagement { sampleSize: number; measuredPosts: number; medianInteractions: number; averageInteractions: number; postsAboveThreshold: number; threshold: number; verdict: "strong" | "usable" | "unknown"; } export function evaluateBenchmarkEngagement(posts: RawPost[], threshold = 10): BenchmarkEngagement { const values = posts .map((post) => (post.likeCount ?? 0) + (post.replyCount ?? 0) * 2 + (post.repostCount ?? 0) * 1.5) .filter((value) => value > 0) .sort((a, b) => a - b); const median = values.length ? values.length % 2 ? values[Math.floor(values.length / 2)] : (values[values.length / 2 - 1] + values[values.length / 2]) / 2 : 0; const average = values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0; const above = values.filter((value) => value >= threshold).length; const verdict = values.length < 3 ? "unknown" : median >= threshold && above >= 3 ? "strong" : "usable"; return { sampleSize: posts.length, measuredPosts: values.length, medianInteractions: Math.round(median * 10) / 10, averageInteractions: Math.round(average * 10) / 10, postsAboveThreshold: above, threshold, verdict, }; } export function serialize8DPersona(draft: Style8DAnalysis["personaDraft"]): string { return [ `【我是誰】\n${draft.identity}`, `【語氣】\n${draft.tone}`, `【對誰說】\n${draft.audience}`, `【開場習慣】\n${draft.hooks}`, `【代表句範例】\n${draft.examples}`, `【避免】\n${draft.avoid}`, ].join("\n\n"); } export async function analyzeStyle8D(input: { username: string; posts: RawPost[]; aiProvider: string; aiModel: string; apiKeys?: ProviderApiKeys; }): Promise { const model = getModel(input.aiProvider, input.aiModel, input.apiKeys ?? {}); const materials = input.posts.slice(0, 12).map((post, index) => `[${index + 1}] ${post.likeCount ?? 0}讚 ${post.replyCount ?? 0}回覆\n${post.text.slice(0, 500)}` ).join("\n\n"); return generateStructuredObject({ model, provider: input.aiProvider, modelId: input.aiModel, schema: style8dSchema, normalize: normalizeStyle8DOutput, system: withAgentSystem(`你是 Threads 創作者風格研究員。只能根據提供的近期貼文歸納,不可捏造作者背景。逐一輸出八個維度:D1 語氣人格、D2 結構模板、D3 互動方式、D4 主題分布、D5 發文節奏、D6 視覺語法(emoji、標點、換行)、D7 轉換方式、D8 風險紅線。每個維度要有摘要與可核對的文字證據。最後產生「可供另一個帳號借鑑、但不可冒充或抄襲」的人設草稿。代表句必須是抽象仿寫範例,不可逐字複製原文。`), jsonPromptSuffix: `\n\n只回傳以下 JSON 結構,不要 markdown:\n{"d1Tone":{"summary":"","evidence":[]},"d2Structure":{"summary":"","evidence":[]},"d3Interaction":{"summary":"","evidence":[]},"d4Topics":{"summary":"","evidence":[]},"d5Rhythm":{"summary":"","evidence":[]},"d6Visual":{"summary":"","evidence":[]},"d7Conversion":{"summary":"","evidence":[]},"d8Risk":{"summary":"","evidence":[]},"personaDraft":{"identity":"","tone":"","audience":"","hooks":"","examples":"","avoid":""}}`, prompt: `對標帳號:@${input.username}\n近期貼文樣本:\n\n${materials}`, }); }