Files
jsc-marketplace/plugins/cockpit/companion/interaction.ts
T

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))