- statusbar:輸入框上方狀態列,指令 /statusbar - companion:小說角色陪伴,指令 /companion,自行渲染角色行 - companion 資料目錄改為 ~/.companion - 整併 M4 檔案至 companion;M2 實作移至 hooks/companion/persona - README 移除本機資料夾安裝,補上更新與移除指令 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import { redactSecrets } from './secrets.ts'
|
|
import type { Fs } from './types.ts'
|
|
|
|
export type Turn = { role: 'user' | 'assistant'; text: string; at: number }
|
|
export type InteractionMemory = {
|
|
recent: Turn[]
|
|
/** 壓縮後的摘要行與累計關係變化 */
|
|
summary: string[]
|
|
relationDelta: number
|
|
}
|
|
|
|
export const RECENT_MAX = 30
|
|
export const COMPRESS_COUNT = 20
|
|
const SUMMARY_MAX = 40
|
|
const LINE_MAX = 60
|
|
|
|
export const empty = (): InteractionMemory => ({ recent: [], summary: [], relationDelta: 0 })
|
|
|
|
// 互動記憶不存大段程式碼;只留對話文字
|
|
export function dialogueOnly(text: string): string {
|
|
return text
|
|
.replace(/```[\s\S]*?(?:```|$)/g, ' ')
|
|
.replace(/`[^`\n]{40,}`/g, ' ')
|
|
.replace(/[ \t]+/g, ' ')
|
|
.trim()
|
|
}
|
|
|
|
export function clean(text: string): string {
|
|
return redactSecrets(dialogueOnly(text))
|
|
}
|
|
|
|
export function add(m: InteractionMemory, turn: Turn, relationDelta: number): InteractionMemory {
|
|
const text = clean(turn.text)
|
|
if (!text) return { ...m, relationDelta: m.relationDelta + relationDelta }
|
|
let next: InteractionMemory = {
|
|
...m,
|
|
recent: [...m.recent, { ...turn, text }],
|
|
relationDelta: m.relationDelta + relationDelta,
|
|
}
|
|
if (next.recent.length > RECENT_MAX) next = compress(next)
|
|
return next
|
|
}
|
|
|
|
/** 把最舊的逐字紀錄壓縮成摘要行 */
|
|
export function compress(m: InteractionMemory): InteractionMemory {
|
|
const old = m.recent.slice(0, COMPRESS_COUNT)
|
|
const lines = old
|
|
.filter(t => t.role === 'user')
|
|
.map(t => (t.text.length > LINE_MAX ? t.text.slice(0, LINE_MAX) + '…' : t.text))
|
|
return { ...m, recent: m.recent.slice(COMPRESS_COUNT), summary: [...m.summary, ...lines].slice(-SUMMARY_MAX) }
|
|
}
|
|
|
|
export async function load(fs: Fs, path: string): Promise<InteractionMemory> {
|
|
const raw = await fs.read(path)
|
|
if (!raw) return empty()
|
|
try {
|
|
return { ...empty(), ...JSON.parse(raw) }
|
|
} catch {
|
|
return empty()
|
|
}
|
|
}
|
|
|
|
export const save = (fs: Fs, path: string, m: InteractionMemory) => fs.write(path, JSON.stringify(m))
|