157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
|
|
function asString(value: unknown): string {
|
||
|
|
if (typeof value === "string") return value.trim();
|
||
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function asNumber(value: unknown, fallback: number): number {
|
||
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||
|
|
const raw = asString(value);
|
||
|
|
if (!raw) return fallback;
|
||
|
|
const parsed = Number(raw);
|
||
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||
|
|
}
|
||
|
|
|
||
|
|
function pickDraftText(obj: Record<string, unknown>): string {
|
||
|
|
return asString(
|
||
|
|
obj.text ??
|
||
|
|
obj.comment ??
|
||
|
|
obj.reply ??
|
||
|
|
obj.content ??
|
||
|
|
obj.message ??
|
||
|
|
obj.body ??
|
||
|
|
obj.draft ??
|
||
|
|
obj.留言 ??
|
||
|
|
obj.回覆
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function coerceOutreachDraftItem(value: unknown): {
|
||
|
|
text: string;
|
||
|
|
angle: string;
|
||
|
|
rationale: string;
|
||
|
|
} | null {
|
||
|
|
if (typeof value === "string") {
|
||
|
|
const text = value.trim();
|
||
|
|
if (!text) return null;
|
||
|
|
return { text, angle: "置入留言", rationale: "AI 產生的留言草稿" };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!value || typeof value !== "object") return null;
|
||
|
|
const obj = value as Record<string, unknown>;
|
||
|
|
const text = pickDraftText(obj);
|
||
|
|
if (!text) return null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
text,
|
||
|
|
angle:
|
||
|
|
asString(obj.angle ?? obj.strategy ?? obj.type ?? obj.approach ?? obj.切角) ||
|
||
|
|
"置入留言",
|
||
|
|
rationale:
|
||
|
|
asString(obj.rationale ?? obj.reason ?? obj.explanation ?? obj.說明 ?? obj.note) ||
|
||
|
|
"符合貼文情境的自然留言",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function coerceInboundDraftItem(value: unknown): { text: string; rationale: string } | null {
|
||
|
|
if (typeof value === "string") {
|
||
|
|
const text = value.trim();
|
||
|
|
if (!text) return null;
|
||
|
|
return { text, rationale: "AI 產生的回覆草稿" };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!value || typeof value !== "object") return null;
|
||
|
|
const obj = value as Record<string, unknown>;
|
||
|
|
const text = pickDraftText(obj);
|
||
|
|
if (!text) return null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
text,
|
||
|
|
rationale:
|
||
|
|
asString(obj.rationale ?? obj.reason ?? obj.explanation ?? obj.說明) ||
|
||
|
|
"回覆這則留言",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function coerceDraftsArray<T>(
|
||
|
|
value: unknown,
|
||
|
|
coerceItem: (item: unknown) => T | null,
|
||
|
|
count: number
|
||
|
|
): T[] {
|
||
|
|
const items: T[] = [];
|
||
|
|
|
||
|
|
if (Array.isArray(value)) {
|
||
|
|
for (const item of value) {
|
||
|
|
const coerced = coerceItem(item);
|
||
|
|
if (coerced) items.push(coerced);
|
||
|
|
if (items.length >= count) return items;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||
|
|
for (const item of Object.values(value as Record<string, unknown>)) {
|
||
|
|
const coerced = coerceItem(item);
|
||
|
|
if (coerced) items.push(coerced);
|
||
|
|
if (items.length >= count) return items;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return items;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function coerceOutreachDraftsRaw(parsed: unknown, count = 2) {
|
||
|
|
const root =
|
||
|
|
parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
||
|
|
|
||
|
|
const nested =
|
||
|
|
root.result && typeof root.result === "object"
|
||
|
|
? (root.result as Record<string, unknown>)
|
||
|
|
: root;
|
||
|
|
|
||
|
|
const relevance = Math.min(
|
||
|
|
1,
|
||
|
|
Math.max(0, asNumber(nested.relevance ?? nested.score ?? nested.fit, 0.65))
|
||
|
|
);
|
||
|
|
const reason =
|
||
|
|
asString(nested.reason ?? nested.summary ?? nested.why ?? nested.評估) ||
|
||
|
|
"符合置入情境";
|
||
|
|
|
||
|
|
const drafts = coerceDraftsArray(
|
||
|
|
nested.drafts ?? nested.comments ?? nested.replies ?? nested.options ?? nested.草稿,
|
||
|
|
coerceOutreachDraftItem,
|
||
|
|
count
|
||
|
|
);
|
||
|
|
|
||
|
|
return { relevance, reason, drafts };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function coerceInboundReplyDraftsRaw(parsed: unknown, count = 2) {
|
||
|
|
const root =
|
||
|
|
parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
||
|
|
|
||
|
|
const nested =
|
||
|
|
root.result && typeof root.result === "object"
|
||
|
|
? (root.result as Record<string, unknown>)
|
||
|
|
: root;
|
||
|
|
|
||
|
|
const sentimentRaw = asString(nested.sentiment ?? nested.tone ?? nested.情緒).toLowerCase();
|
||
|
|
const sentiment =
|
||
|
|
sentimentRaw === "positive" ||
|
||
|
|
sentimentRaw === "neutral" ||
|
||
|
|
sentimentRaw === "negative" ||
|
||
|
|
sentimentRaw === "question" ||
|
||
|
|
sentimentRaw === "lead"
|
||
|
|
? sentimentRaw
|
||
|
|
: "neutral";
|
||
|
|
|
||
|
|
const intent =
|
||
|
|
asString(nested.intent ?? nested.intention ?? nested.意圖) || "一般互動";
|
||
|
|
|
||
|
|
const drafts = coerceDraftsArray(
|
||
|
|
nested.drafts ?? nested.replies ?? nested.responses ?? nested.options ?? nested.草稿,
|
||
|
|
coerceInboundDraftItem,
|
||
|
|
count
|
||
|
|
);
|
||
|
|
|
||
|
|
return { sentiment, intent, drafts };
|
||
|
|
}
|