import { redactSecrets } from './secrets.ts' export const BUDGET = { card: 400, mood: 100, memory: 800, care: 200 } as const export type Section = keyof typeof BUDGET /** 粗估 token:CJK 一字約一 token,其餘約四字元一 token */ export function estimateTokens(text: string): number { let cjk = 0 for (const ch of text) if (/[⺀-鿿＀-￯ -〿]/.test(ch)) cjk++ return cjk + Math.ceil((text.length - cjk) / 4) } export type Item = { text: string; score: number } /** 在預算內保留高分項目,維持原順序;超出時先丟低分 */ export function fitBudget(items: Item[], budget: number): string[] { const kept = new Set(items.map((_, i) => i)) const order = items.map((it, i) => ({ i, score: it.score })).sort((a, b) => a.score - b.score || b.i - a.i) let total = items.reduce((n, it) => n + estimateTokens(it.text), 0) for (const { i } of order) { if (total <= budget) break total -= estimateTokens(items[i]!.text) kept.delete(i) } return items.filter((_, i) => kept.has(i)).map(it => it.text) } export type Sections = Record /** 穩定內容(角色卡)在前,變動內容(心情、記憶、關心情境)在後 */ export function assemble(s: Sections): string { const part = (title: string, key: Section) => { const lines = fitBudget(s[key], BUDGET[key]).map(redactSecrets) return lines.length ? `## ${title}\n${lines.join('\n')}` : '' } const body = [part('角色', 'card'), part('互動記憶', 'memory'), part('目前心情', 'mood'), part('關心情境', 'care')] .filter(Boolean) .join('\n\n') return body ? `\n${body}\n` : '' }