import { z } from "zod"; import type { ProviderApiKeys } from "./keys"; import { generateStructuredObject } from "./generate-structured"; import { getModel } from "./provider"; const nullableText = z.string().nullable().default(null); const style8DSchema = z.object({ d1Tone: nullableText, d2Structure: nullableText, d3Interaction: nullableText, d4Topics: nullableText, d5Rhythm: nullableText, d6Visual: nullableText, d7Conversion: nullableText, d8Risk: nullableText, }).default({}); const accountStrategySchema = z.object({ message: z.string().default("我已依照你的方向更新帳號策略。"), fields: z.object({ displayName: nullableText, username: nullableText, brief: nullableText, persona: nullableText, targetAudience: nullableText, productBrief: nullableText, goals: nullableText, style8D: style8DSchema, }).default({}), }); export type AccountStrategyFields = z.infer["fields"]; function normalizeAccountStrategy(value: unknown): unknown { if (!value || typeof value !== "object") return value; let root = value as Record; for (const key of ["result", "data", "output", "strategy"]) { const nested = root[key]; if (nested && typeof nested === "object" && !Array.isArray(nested)) { root = nested as Record; break; } } const fieldsValue = root.fields ?? root.form ?? root.account ?? root; const fields = fieldsValue && typeof fieldsValue === "object" && !Array.isArray(fieldsValue) ? { ...(fieldsValue as Record) } : {}; fields.style8D ??= fields.style8d ?? root.style8D ?? root.style8d; return { message: root.message ?? root.reply ?? root.summary, fields, }; } export async function generateAccountStrategy(input: { instruction: string; current: Partial & { style8D?: Partial; }; aiProvider: string; aiModel: string; apiKeys?: ProviderApiKeys; }) { const model = getModel(input.aiProvider, input.aiModel, input.apiKeys ?? {}); const object = await generateStructuredObject({ model, provider: input.aiProvider, modelId: input.aiModel, schema: accountStrategySchema, system: `你是 Threads 帳號策略顧問,也是產品 onboarding 助手。 你的任務:根據使用者自然語言,幫他把「帳號策略表單」補成可直接使用的內容。 填寫原則: - 使用繁體中文、台灣語感 - 內容要具體、可執行,不要空泛 - 不要使用誇大承諾或保證結果 - 如果使用者提到醫療、健康、營養、心理等高敏感領域,語氣要合規:避免療效保證、診斷語氣、恐嚇式行銷;可以強調衛教、陪伴、風險提醒、就醫建議 - 優先補齊空欄位;已有內容可優化,但不要完全無視原本方向 - username 不確定時回傳 null,不要亂編 - displayName 可用使用者描述推測一個給自己看的名稱 - 使用者提到 8D、語氣、結構、互動、主題、節奏、視覺、轉換或風險時,直接更新 fields.style8D 對應欄位 - 沒有要求修改的 8D 維度回傳 null,不要覆蓋現有內容 - message 用 1-2 句說明你幫他補了什麼`, prompt: `目前表單內容: ${JSON.stringify(input.current, null, 2)} 使用者跟小幫手說: ${input.instruction} 請回傳可套用到表單的欄位。`, normalize: normalizeAccountStrategy, jsonPromptSuffix: `\n\n只回傳以下 JSON,不要 markdown:\n{"message":"","fields":{"displayName":null,"username":null,"brief":null,"persona":null,"targetAudience":null,"productBrief":null,"goals":null,"style8D":{"d1Tone":null,"d2Structure":null,"d3Interaction":null,"d4Topics":null,"d5Rhythm":null,"d6Visual":null,"d7Conversion":null,"d8Risk":null}}}`, }); return object; }