haixunMaster/lib/ai/persona.ts

117 lines
3.9 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
import { sanitizePromptText } from "@/lib/utils";
export interface PersonaFields {
identity: string;
tone: string;
audience: string;
hooks: string;
examples: string;
avoid: string;
styleStrategy: string;
}
const SECTION_KEYS: Array<{ key: keyof PersonaFields; label: string }> = [
{ key: "identity", label: "【我是誰】" },
{ key: "tone", label: "【語氣】" },
{ key: "audience", label: "【對誰說】" },
{ key: "hooks", label: "【開場習慣】" },
{ key: "examples", label: "【代表句範例】" },
{ key: "avoid", label: "【避免】" },
{ key: "styleStrategy", label: "【8D 風格策略】" },
];
const EMPTY_PERSONA: PersonaFields = {
identity: "",
tone: "",
audience: "",
hooks: "",
examples: "",
avoid: "",
styleStrategy: "",
};
export function emptyPersonaFields(): PersonaFields {
return { ...EMPTY_PERSONA };
}
export function hasPersonaConfigured(raw?: string | null): boolean {
const fields = parsePersona(raw);
return Object.values(fields).some((value) => value.trim().length > 0);
}
export function parsePersona(raw?: string | null): PersonaFields {
if (!raw?.trim()) return emptyPersonaFields();
const text = raw.trim();
if (!text.includes("【")) {
return { ...EMPTY_PERSONA, identity: text };
}
const fields = emptyPersonaFields();
const pattern = /【[^】]+】/g;
const markers = [...text.matchAll(pattern)];
if (markers.length === 0) {
return { ...EMPTY_PERSONA, identity: text };
}
for (let i = 0; i < markers.length; i++) {
const marker = markers[i];
const label = marker[0];
const start = (marker.index ?? 0) + label.length;
const end = i + 1 < markers.length ? (markers[i + 1].index ?? text.length) : text.length;
const content = text.slice(start, end).trim();
const section = SECTION_KEYS.find((item) => item.label === label);
if (section) {
fields[section.key] = content;
}
}
return fields;
}
export function serializePersona(fields: PersonaFields): string {
return SECTION_KEYS.map(({ key, label }) => {
const value = fields[key].trim();
return value ? `${label}\n${value}` : "";
})
.filter(Boolean)
.join("\n\n");
}
export function buildPersonaPromptBlock(raw?: string | null): string {
const fields = parsePersona(raw);
if (!hasPersonaConfigured(raw)) {
return `## 創作者人設
Threads AI `;
}
const sections: string[] = [];
if (fields.identity) sections.push(`### 我是誰\n${sanitizePromptText(fields.identity)}`);
if (fields.tone) sections.push(`### 語氣特質\n${sanitizePromptText(fields.tone)}`);
if (fields.audience) sections.push(`### 對誰說\n${sanitizePromptText(fields.audience)}`);
if (fields.hooks) sections.push(`### 開場習慣\n${sanitizePromptText(fields.hooks)}`);
if (fields.examples) {
sections.push(
`### 代表句範例(最高優先)\n${sanitizePromptText(fields.examples)}\n` +
`寫作時語感、句長、用詞、停頓節奏要向以上句子靠攏,讀起來像同一個人在說話。`
);
}
if (fields.avoid) sections.push(`### 絕對避免\n${sanitizePromptText(fields.avoid)}`);
if (fields.styleStrategy) {
sections.push(`### 帳號 8D 風格策略(產文與回覆都必須執行)\n${sanitizePromptText(fields.styleStrategy)}`);
}
return `## 創作者人設(每篇都必須貫徹)
${sections.join("\n\n")}
##
1. AI
2. hook
3.
4.
5. `;
}