feat: companion 注入角色語氣,並接上記憶、待辦與主動關心

拆分 cockpit 後 companion 只剩指令與狀態列,沒有任何 hook 把角色
內容送給模型,所以回覆沒有角色感。

- prompt.compose 在 system prompt 尾端加入角色區塊(人設、語氣、心情、
  好感階段、招牌短語、記憶、待辦)
- prompt.submit 以關鍵字更新心情與好感,檢索並寫入互動記憶,附加關心情境
- tool.call 記錄連續失敗;定時 tick 以 toast 提醒到期待辦
- 新增 /companion todo 與 quiet 指令
- 移除未引用的 persona/ 與 nodeFs,測試改成引擎可執行的寫法

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-10-12 14:28:57 +08:00
co-authored by Claude Sonnet 5.5
parent 6b2a6b9779
commit 473b7d720b
23 changed files with 392 additions and 678 deletions
+58
View File
@@ -5,6 +5,12 @@ import { createCompanion } from './companion/core'
import { engineFs } from './companion/engine-fs'
let companion: ReturnType<typeof createCompanion> | undefined
let replying = false
let typedAt = 0
let timer: { cancel(): void } | undefined
const TICK_MS = 60_000
const TYPING_MS = 5_000
export const register: Register = on => {
on('session.start', async ($, e, next) => {
@@ -18,6 +24,15 @@ export const register: Register = on => {
const first = await companion.ensureActivated()
if (first.first) $.ui.toast(`已啟用 ${first.name}。用 /companion off 可關閉`)
// 定時檢查 toast 類關心(到期待辦、作息);失敗不影響主流程
timer?.cancel()
timer = $.clock.every(TICK_MS, () => {
companion
?.tick({ replying, typing: Date.now() - typedAt < TYPING_MS })
.then(cares => cares.forEach(c => $.ui.toast(c.text)))
.catch(() => {})
})
return next(e)
})
@@ -25,6 +40,49 @@ export const register: Register = on => {
text: companion ? await companion.command(e.args) : '尚未就緒。',
}))
// 角色卡附加在 system prompt 尾端(session 區段),語氣才會進到模型
on('prompt.compose', async ($, e, next) => {
const result = await next(e)
const text = await companion?.prompt()
if (!text) return result
return { sections: [...result.sections, { id: 'companion:persona', text, scope: 'session' as const }] }
})
on('prompt.submit', async ($, e, next) => {
// 更新心情失敗不能擋下使用者的提示
const care = (await companion?.onPrompt(e.text).catch(() => [] as string[])) ?? []
return next(care.length ? { ...e, context: [...(e.context ?? []), ...care] } : e)
})
on('prompt.edit', async ($, e, next) => {
typedAt = Date.now()
return next(e)
})
on('turn.start', async ($, e, next) => {
replying = true
return next(e)
})
on('turn.complete', async ($, e, next) => {
replying = false
return next(e)
})
// 連續工具失敗會觸發關心,並影響心情
on('tool.call', async ($, e, next) => {
const r = await next(e)
companion?.onToolResult(!r.isError).catch(() => {})
return r
})
on('session.end', async ($, e, next) => {
timer?.cancel()
timer = undefined
await companion?.end().catch(() => {})
return next(e)
})
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const rest = await next(e)
const view = await companion?.view()