100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
|
|
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<typeof accountStrategySchema>["fields"];
|
|||
|
|
|
|||
|
|
function normalizeAccountStrategy(value: unknown): unknown {
|
|||
|
|
if (!value || typeof value !== "object") return value;
|
|||
|
|
let root = value as Record<string, unknown>;
|
|||
|
|
for (const key of ["result", "data", "output", "strategy"]) {
|
|||
|
|
const nested = root[key];
|
|||
|
|
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|||
|
|
root = nested as Record<string, unknown>;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const fieldsValue = root.fields ?? root.form ?? root.account ?? root;
|
|||
|
|
const fields = fieldsValue && typeof fieldsValue === "object" && !Array.isArray(fieldsValue)
|
|||
|
|
? { ...(fieldsValue as Record<string, unknown>) }
|
|||
|
|
: {};
|
|||
|
|
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<AccountStrategyFields> & {
|
|||
|
|
style8D?: Partial<AccountStrategyFields["style8D"]>;
|
|||
|
|
};
|
|||
|
|
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;
|
|||
|
|
}
|