feat: companion 核心的人設注入、語氣、心情標記與互動記憶
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>` : ''
|
||||
}
|
||||
Reference in New Issue
Block a user