41 lines
1.6 KiB
TypeScript
41 lines
1.6 KiB
TypeScript
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<Section, Item[]>
|
||
|
||
/** 穩定內容(角色卡)在前,變動內容(心情、記憶、關心情境)在後 */
|
||
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 ? `<companion>\n${body}\n</companion>` : ''
|
||
}
|