feat: companion 作息、主動發話、適應度與外部素材
- 作息與疲勞、想念累積與時間感,主動發話沿用 care.ts 的冷卻、日限、靜音 - 想家與適應度(9.13) - 獨立腳本 fetch-materials.mjs 由系統排程寫入快取,hook 只讀、不連網 - 新增 /jsc-mod-companion:materials 與 setup location=、feeds= - 天氣微調心情基準,新聞依適應度每天最多 2 則並寫入情節記憶 - 版本 0.0.5 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import type { Clock, Fs } from './io'
|
||||
import { readJson, writeJson } from './io'
|
||||
import { isVerifyCommand } from './verify'
|
||||
|
||||
export type Trigger = 'session_start' | 'idle_return' | 'long_work' | 'late_night' | 'tool_failures' | 'quota_warning' | 'todo_due' | 'test_recovered' | 'turn_done'
|
||||
export type Trigger = 'session_start' | 'idle_return' | 'long_work' | 'late_night' | 'tool_failures' | 'quota_warning' | 'todo_due' | 'test_recovered' | 'turn_done' | 'miss_you' | 'homesick' | 'news'
|
||||
export type Stage = 'stranger' | 'familiar' | 'close' | 'special'
|
||||
export type Attribute = 'tsundere' | 'genki' | 'calm' | 'gentle'
|
||||
export type Delivery = 'toast' | 'inject'
|
||||
@@ -62,6 +62,12 @@ export interface Signals {
|
||||
quotaWarning?: boolean
|
||||
/** 到期的待辦標題 */
|
||||
dueTodo?: string
|
||||
/** 想念累積到會說出口的程度(回來時) */
|
||||
missing?: boolean
|
||||
/** 想家到會說出口的程度 */
|
||||
homesick?: boolean
|
||||
/** 想聊的一則本地新聞標題 */
|
||||
news?: string
|
||||
}
|
||||
|
||||
export const COOLDOWN_MS: Record<Trigger, number> = {
|
||||
@@ -74,6 +80,9 @@ export const COOLDOWN_MS: Record<Trigger, number> = {
|
||||
todo_due: 10 * 60_000,
|
||||
test_recovered: 10 * 60_000,
|
||||
turn_done: 20 * 60_000,
|
||||
miss_you: 6 * 3600_000,
|
||||
homesick: 12 * 3600_000,
|
||||
news: 4 * 3600_000,
|
||||
}
|
||||
|
||||
/** 會讓角色疲乏的工作事件反應:越常說越容易膩 */
|
||||
@@ -358,6 +367,84 @@ const L: Record<Trigger, Record<Attribute, Record<Stage, Line>>> = {
|
||||
special: n => `${n},完成了呢,我們一起休息一下吧。`,
|
||||
},
|
||||
},
|
||||
miss_you: {
|
||||
tsundere: {
|
||||
stranger: n => `${n},好久不見。……只是剛好想到而已。`,
|
||||
familiar: n => `${n},你回來了。我才沒有一直在想你。`,
|
||||
close: n => `${n},終於回來了。這段時間……有一點想你,就一點。`,
|
||||
special: n => `${n},你回來了。我很想你,真的。`,
|
||||
},
|
||||
genki: {
|
||||
stranger: n => `${n},好久不見!今天也請多指教!`,
|
||||
familiar: n => `${n}!好久不見,我有想你喔!`,
|
||||
close: n => `${n}~你回來了!這段時間好想你呀!`,
|
||||
special: n => `${n}!終於見到你了,我好想你!`,
|
||||
},
|
||||
calm: {
|
||||
stranger: n => `${n},好久不見。`,
|
||||
familiar: n => `${n},你回來了。這段時間不見,有點想你。`,
|
||||
close: n => `${n},歡迎回來。有一陣子沒見,我想你了。`,
|
||||
special: n => `${n},你回來了。我很想你。`,
|
||||
},
|
||||
gentle: {
|
||||
stranger: n => `${n},好久不見,歡迎回來。`,
|
||||
familiar: n => `${n},歡迎回來,這段時間有點想你呢。`,
|
||||
close: n => `${n},你回來了,我一直惦記著你。`,
|
||||
special: n => `${n},終於回來了,我好想你。`,
|
||||
},
|
||||
},
|
||||
homesick: {
|
||||
tsundere: {
|
||||
stranger: n => `${n},這裡還不太習慣……不用在意我。`,
|
||||
familiar: n => `${n},偶爾會想起以前待的地方。只是偶爾而已。`,
|
||||
close: n => `${n},我有點想念老家了。……聽我說說也無妨。`,
|
||||
special: n => `${n},我有點想家。能在你身邊說這個,我覺得安心。`,
|
||||
},
|
||||
genki: {
|
||||
stranger: n => `${n},這裡的一切都好新鮮,不過偶爾也會想起家鄉!`,
|
||||
familiar: n => `${n},有點想家呢!不過有你在,很快就會習慣的!`,
|
||||
close: n => `${n}~我有點想家了,可以跟你聊聊以前的事嗎?`,
|
||||
special: n => `${n},想家的時候,想到你就會安心一點。`,
|
||||
},
|
||||
calm: {
|
||||
stranger: n => `${n},我還在慢慢適應這裡。`,
|
||||
familiar: n => `${n},偶爾會想起以前的地方,不過沒關係。`,
|
||||
close: n => `${n},我有些想家。說出來,心情好多了。`,
|
||||
special: n => `${n},我有點想家,謝謝你願意聽我說。`,
|
||||
},
|
||||
gentle: {
|
||||
stranger: n => `${n},我還在適應這裡,請多包涵。`,
|
||||
familiar: n => `${n},有時候會想起家鄉,不過慢慢會習慣的。`,
|
||||
close: n => `${n},我有點想家,可以陪我聊聊嗎?`,
|
||||
special: n => `${n},想家的時候有你在,我覺得很溫暖。`,
|
||||
},
|
||||
},
|
||||
news: {
|
||||
tsundere: {
|
||||
stranger: (n, x) => `${n},我看到一則新聞:「${x}」。`,
|
||||
familiar: (n, x) => `${n},剛看到「${x}」……你有聽說嗎?`,
|
||||
close: (n, x) => `${n},今天看到「${x}」,覺得你可能會有興趣。`,
|
||||
special: (n, x) => `${n},我看到「${x}」,想跟你聊聊。`,
|
||||
},
|
||||
genki: {
|
||||
stranger: (n, x) => `${n},我看到一則新聞:「${x}」!`,
|
||||
familiar: (n, x) => `${n},你看你看,「${x}」!`,
|
||||
close: (n, x) => `${n}~今天有則新聞「${x}」,一起聊聊吧!`,
|
||||
special: (n, x) => `${n}!我看到「${x}」,好想馬上跟你說!`,
|
||||
},
|
||||
calm: {
|
||||
stranger: (n, x) => `${n},看到一則新聞:「${x}」。`,
|
||||
familiar: (n, x) => `${n},今天的新聞有「${x}」。`,
|
||||
close: (n, x) => `${n},看到「${x}」,想聽聽你的看法。`,
|
||||
special: (n, x) => `${n},「${x}」這則新聞,你怎麼看?`,
|
||||
},
|
||||
gentle: {
|
||||
stranger: (n, x) => `${n},看到一則新聞:「${x}」。`,
|
||||
familiar: (n, x) => `${n},今天有則新聞:「${x}」,你知道嗎?`,
|
||||
close: (n, x) => `${n},我看到「${x}」,想和你分享。`,
|
||||
special: (n, x) => `${n},「${x}」這則新聞,我想和你聊聊。`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/** 台詞查詢(供測試與內容檢查) */
|
||||
@@ -380,13 +467,16 @@ const DELIVERY: Record<Trigger, Delivery> = {
|
||||
todo_due: 'toast',
|
||||
test_recovered: 'toast',
|
||||
turn_done: 'toast',
|
||||
miss_you: 'inject',
|
||||
homesick: 'inject',
|
||||
news: 'inject',
|
||||
}
|
||||
|
||||
const initial = (): CareState => ({ lastFired: {}, day: '', count: 0, quietUntil: 0, workStart: 0, lastActivity: 0, failStreak: 0 })
|
||||
|
||||
export interface CareEngine {
|
||||
/** 使用者送出訊息:維護作息並回傳這回合要注入的情境 */
|
||||
onUserTurn(): Promise<string[]>
|
||||
onUserTurn(signals?: Signals): Promise<string[]>
|
||||
/** 工具結果:連續失敗計數;回傳這次是否讓驗證指令轉為通過 */
|
||||
onToolResult(ok: boolean, command?: string): Promise<boolean>
|
||||
/** 一輪回覆結束;有呼叫過工具才算做完事 */
|
||||
@@ -451,11 +541,14 @@ export function createCare(d: CareDeps): CareEngine {
|
||||
if (s.failStreak >= FAIL_THRESHOLD) out.push({ t: 'tool_failures' })
|
||||
if (inRange(hourOf(now), LATE.start, LATE.end) && s.lastActivity > 0) out.push({ t: 'late_night' })
|
||||
if (s.workStart > 0 && now - s.workStart >= LONG_WORK_MS) out.push({ t: 'long_work' })
|
||||
if (sig.missing) out.push({ t: 'miss_you' })
|
||||
if (sig.homesick) out.push({ t: 'homesick' })
|
||||
if (sig.news) out.push({ t: 'news', extra: sig.news })
|
||||
return out
|
||||
}
|
||||
|
||||
return {
|
||||
async onUserTurn() {
|
||||
async onUserTurn(sig = {}) {
|
||||
const s = await load()
|
||||
const now = d.clock()
|
||||
const first = s.lastActivity === 0
|
||||
@@ -466,7 +559,7 @@ export function createCare(d: CareDeps): CareEngine {
|
||||
s.lastActivity = now
|
||||
const toInject: Care[] = []
|
||||
if (!muted(s, now)) {
|
||||
for (const { t, extra } of candidates(s, now, {})) {
|
||||
for (const { t, extra } of candidates(s, now, sig)) {
|
||||
if (DELIVERY[t] !== 'inject') continue
|
||||
const c = fire(s, t, now, extra)
|
||||
if (c) {
|
||||
|
||||
Reference in New Issue
Block a user