Files
jsc-marketplace/plugins/companion/hooks/companion/persona/mood.ts
T
rootandClaude Sonnet 5.5 ba5b50ad1d refactor: 拆分 cockpit 為可個別安裝的 statusbar 與 companion
- statusbar:輸入框上方狀態列,指令 /statusbar
- companion:小說角色陪伴,指令 /companion,自行渲染角色行
- companion 資料目錄改為 ~/.companion
- 整併 M4 檔案至 companion;M2 實作移至 hooks/companion/persona
- README 移除本機資料夾安裝,補上更新與移除指令

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 13:57:24 +08:00

94 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { clamp } from './types.ts'
import type { Pad } from './types.ts'
export const TURN_CAP = 0.3
export const EVENTS = {
praise: { p: 0.15, a: 0.05, d: 0.05 },
scold: { p: -0.2, a: 0.15, d: -0.1 },
success: { p: 0.08, a: 0.03, d: 0.05 },
toolFailures: { p: -0.08, a: 0.1, d: -0.08 },
returnAfterIdle: { p: 0.1, a: 0.08, d: 0 },
lateNight: { p: -0.05, a: -0.1, d: 0 },
likedTopic: { p: 0.12, a: 0.08, d: 0.03 },
careIgnored: { p: -0.05, a: -0.03, d: -0.03 },
} as const satisfies Record<string, Pad>
export type MoodEvent = keyof typeof EVENTS
export const ZERO: Pad = { p: 0, a: 0, d: 0 }
/** 一回合內累計的變化量;單軸總變化不得超過 ±0.3 */
export function applyDelta(mood: Pad, turn: Pad, delta: Pad): { mood: Pad; turn: Pad } {
const out = { mood: { ...mood }, turn: { ...turn } }
for (const k of ['p', 'a', 'd'] as const) {
const next = clamp(turn[k] + delta[k], -TURN_CAP, TURN_CAP)
const step = next - turn[k]
out.turn[k] = next
out.mood[k] = clamp(mood[k] + step, -1, 1)
}
return out
}
/** 讀取時依經過時間往基線衰減:x = B + (x − B)·0.5^(Δ/τ) */
export function decay(mood: Pad, baseline: Pad, elapsedMs: number, halfLifeMs: number): Pad {
const f = Math.pow(0.5, Math.max(0, elapsedMs) / halfLifeMs)
return {
p: baseline.p + (mood.p - baseline.p) * f,
a: baseline.a + (mood.a - baseline.a) * f,
d: baseline.d + (mood.d - baseline.d) * f,
}
}
const MOODS: Record<string, { label: string; face: string }> = {
'+++': { label: '開心', face: '(≧▽≦)' },
'++-': { label: '安心', face: '(´▽`)' },
'+-+': { label: '放鬆', face: '(´ω`)' },
'+--': { label: '溫柔', face: '(。・ω・。)' },
'-++': { label: '生氣', face: '(`Д´)' },
'-+-': { label: '焦慮', face: '(;´Д`)' },
'--+': { label: '無聊', face: '(¬_¬)' },
'---': { label: '低落', face: '(´;ω;`)' },
}
/** 八分區對應情緒標籤與顏文字 */
export function moodLabel(m: Pad): { label: string; face: string } {
const s = (x: number) => (x >= 0 ? '+' : '-')
return MOODS[s(m.p) + s(m.a) + s(m.d)]!
}
// 心情標記:回覆末尾的隱藏標記,如 <!--mood p=0.1 a=-0.05 d=0-->
const TAG = /<!--\s*mood\b([^>]*?)-->\s*$/
export type ParsedReply = { text: string; delta: Pad | null }
export function parseMoodTag(reply: string): ParsedReply {
const m = TAG.exec(reply)
if (!m) return { text: stripStray(reply), delta: null }
const text = stripStray(reply.slice(0, m.index).trimEnd())
const vals: Partial<Pad> = {}
for (const kv of (m[1] ?? '').trim().split(/\s+/)) {
const mm = /^([pad])=(-?\d+(?:\.\d+)?)$/.exec(kv)
if (mm) vals[mm[1] as keyof Pad] = Number(mm[2])
}
if (vals.p === undefined || vals.a === undefined || vals.d === undefined) return { text, delta: null }
return { text, delta: { p: vals.p, a: vals.a, d: vals.d } }
}
// 格式錯誤的標記也不能露給使用者
function stripStray(s: string): string {
return s.replace(/\s*<!--\s*mood\b[^>]*?(?:-->|$)/g, '').trimEnd()
}
const KEYWORDS: [MoodEvent, RegExp][] = [
['scold', /笨|白痴|廢物|爛|你很煩|閉嘴|stupid|useless|idiot/i],
['praise', /謝謝|感謝|太棒|好棒|厲害|讚|thanks?|thank you|great job|awesome/i],
['success', /成功|完成了|通過|搞定|passed|done|fixed/i],
]
/** 標記缺失或格式錯誤時的後備:用使用者文字的關鍵字對應事件 */
export function keywordEvent(text: string): MoodEvent | null {
for (const [ev, re] of KEYWORDS) if (re.test(text)) return ev
return null
}