feat: companion 核心的人設注入、語氣、心情標記與互動記憶

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:27:34 +08:00
co-authored by Claude Sonnet 5.5
parent 030c599208
commit 83fd180872
10 changed files with 638 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
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>` : ''
}