From 13d367803282e210f9dbaacfd04b6cbcee864fa4 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Oct 2026 10:03:31 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20companion=20=E7=8B=80=E6=85=8B=E5=88=97?= =?UTF-8?q?=E8=88=87=20info=20=E8=BC=B8=E5=87=BA=E5=B0=8D=E9=BD=8A?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 狀態列的 PAD 條改為最強的兩種情緒(不含精力) - info 短版只列名字、關係、前 3 強情緒、適應度與待辦 - info --all 補上 12 格情緒與關係長條、人格 18 參數三層值、世界與時間感,基本資訊移到這裡 - 版本號改為 0.0.4 Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- .../.claude-plugin/plugin.json | 2 +- plugins/jsc-mod-companion/README.md | 6 ++- .../hooks/companion/band.test.ts | 7 +-- .../jsc-mod-companion/hooks/companion/band.ts | 8 +-- .../hooks/companion/companion.test.ts | 6 +-- .../jsc-mod-companion/hooks/companion/core.ts | 51 ++++++++++++++----- .../hooks/companion/emotion.test.ts | 4 +- .../hooks/companion/emotion.ts | 11 ++-- .../hooks/companion/world.test.ts | 16 ++++-- 10 files changed, 74 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 9f944e2..b3939fc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ JSC 自用 Claude Code mods 的 marketplace。每個插件皆可單獨安裝、 輸入框上方單行狀態列:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟。 -### [jsc-mod-companion](plugins/jsc-mod-companion) `0.0.3` +### [jsc-mod-companion](plugins/jsc-mod-companion) `0.0.4` 小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色。 diff --git a/plugins/jsc-mod-companion/.claude-plugin/plugin.json b/plugins/jsc-mod-companion/.claude-plugin/plugin.json index 5d714e6..36a864c 100644 --- a/plugins/jsc-mod-companion/.claude-plugin/plugin.json +++ b/plugins/jsc-mod-companion/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-mod-companion", - "version": "0.0.3", + "version": "0.0.4", "description": "小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、作息與主動關心、外部素材與待辦,並可用 ingest skill 解析小說建立角色", "license": "MIT", "author": { diff --git a/plugins/jsc-mod-companion/README.md b/plugins/jsc-mod-companion/README.md index 8067dc9..9f351b8 100644 --- a/plugins/jsc-mod-companion/README.md +++ b/plugins/jsc-mod-companion/README.md @@ -4,9 +4,11 @@ ### `/jsc-mod-companion:info` -查看目前角色、心情、關係階段與四維度(好感、信賴、親密、依賴)與待辦數量。 +預設是短版:名字與主情緒、關係階段(好感)、前 3 強情緒、適應度、待辦數。 -查看目前角色、心情、好感與待辦數量。情緒有 8 個維度(心情、精力、害羞、忌妒、想念、心跳、想家、不悅),預設列出前 3 強,`--all` 顯示完整數值(▰▱ 量表依維度上色);band 上的情緒名稱為目前最強的主情緒。 +`--all` 顯示完整數值:時間感與天氣、8 維度情緒(心情、精力、害羞、忌妒、想念、心跳、想家、不悅,12 格 ▰▱ 量表依維度上色)、關係四維度(好感、信賴、親密、依賴)長條、人格 18 參數(基準、存檔偏移、今日偏移與有效值)、世界(適應度、想家)與角色基本資訊。 + +輸入框上方的狀態列顯示最強的兩種情緒(不含精力)與關係階段;寬度不夠時依序捨棄氣泡、待辦、情緒條、階段。 ```text /jsc-mod-companion:info diff --git a/plugins/jsc-mod-companion/hooks/companion/band.test.ts b/plugins/jsc-mod-companion/hooks/companion/band.test.ts index a70d774..29c5193 100644 --- a/plugins/jsc-mod-companion/hooks/companion/band.test.ts +++ b/plugins/jsc-mod-companion/hooks/companion/band.test.ts @@ -7,6 +7,7 @@ import { lineWidth } from './seg' const VIEW: View = { core: { id: 'miko', name: '月見ミコ', facts: {} as never, neuroticism: 50, baseline: { p: 0, a: 0, d: 0 } }, mood: { p: 0.5, a: 0, d: -0.5 }, + emotions: { mood: 0.75, energy: 0.5, shy: 0.4, jealous: 0, missing: 0.1, heartbeat: 0, homesick: 0, upset: 0 }, label: '放鬆', kaomoji: '(´ᴗ`)', relation: { affection: 35, trust: 20, intimacy: 10, dependence: 5 }, @@ -17,8 +18,8 @@ const VIEW: View = { const text = (v: View, cols: number, compact = false, bubble?: string) => fitCompanion(v, cols, compact, bubble).map(s => s.text).join('') describe('角色行', () => { - test('寬版顯示顏文字、名字、心情、PAD 條、階段、待辦、氣泡', () => { - expect(text(VIEW, 200, false, '早安')).toBe('(´ᴗ`) 月見ミコ 放鬆 · P▰▰▱ A▰▰▱ D▰▱▱ · 熟人 · ☐2 · 💬 早安') + test('寬版顯示顏文字、名字、心情、情緒條、階段、待辦、氣泡', () => { + expect(text(VIEW, 200, false, '早安')).toBe('(´ᴗ`) 月見ミコ 放鬆 · 心情▰▰▰▱ 害羞▰▰▱▱ · 熟人 · ☐2 · 💬 早安') }) test('緊湊模式只剩「顏文字 名字 心情」', () => { @@ -36,7 +37,7 @@ describe('角色行', () => { } expect(text(VIEW, 40, false, '早安')).not.toContain('早安') expect(text(VIEW, 36, false, '早安')).not.toContain('☐') - expect(text(VIEW, 30, false, '早安')).not.toContain('PAD') + expect(text(VIEW, 30, false, '早安')).not.toContain('心情▰') expect(text(VIEW, 22, false, '早安')).toBe('(´ᴗ`) 月見ミコ 放鬆') expect(lineWidth(fitCompanion(VIEW, 24))).toBeLessThanOrEqual(24) }) diff --git a/plugins/jsc-mod-companion/hooks/companion/band.ts b/plugins/jsc-mod-companion/hooks/companion/band.ts index 97cca84..0857978 100644 --- a/plugins/jsc-mod-companion/hooks/companion/band.ts +++ b/plugins/jsc-mod-companion/hooks/companion/band.ts @@ -1,10 +1,12 @@ +import { DIM_INFO, ranked } from './emotion' import { bar, lineWidth } from './seg' import type { Seg } from './seg' import type { View } from './core' -const axis = (v: number) => bar(((v + 1) / 2) * 100, 3) +// 精力隨作息起伏,不列入主要情緒;取最強的兩種 +const top2 = (v: View) => ranked(v.emotions).filter(d => d !== 'energy').slice(0, 2) -// 由完整版往下捨棄次要資訊:氣泡 → 待辦 → PAD 條 → 好感階段;緊湊版只留「顏文字 名字 心情」 +// 由完整版往下捨棄次要資訊:氣泡 → 待辦 → 情緒條 → 好感階段;緊湊版只留「顏文字 名字 心情」 function line(v: View, level: number, compact: boolean, bubble?: string): Seg[] { const out: Seg[] = [ { text: `${v.kaomoji} `, color: 'claude' }, @@ -14,7 +16,7 @@ function line(v: View, level: number, compact: boolean, bubble?: string): Seg[] ] if (compact) return out const dot = { text: ' · ', dim: true } - if (level < 3) out.push(dot, { text: `P${axis(v.mood.p)} A${axis(v.mood.a)} D${axis(v.mood.d)}`, dim: true }) + if (level < 3) out.push(dot, { text: top2(v).map(d => `${DIM_INFO[d].name}${bar(v.emotions[d] * 100, 4)}`).join(' '), dim: true }) if (level < 4) out.push(dot, { text: v.stage }) if (level < 2 && v.todos > 0) out.push(dot, { text: `☐${v.todos}`, color: 'warning' }) if (level < 1 && bubble) out.push(dot, { text: `💬 ${bubble}`, dim: true }) diff --git a/plugins/jsc-mod-companion/hooks/companion/companion.test.ts b/plugins/jsc-mod-companion/hooks/companion/companion.test.ts index 7aea2a6..bad63bc 100644 --- a/plugins/jsc-mod-companion/hooks/companion/companion.test.ts +++ b/plugins/jsc-mod-companion/hooks/companion/companion.test.ts @@ -285,10 +285,10 @@ describe('舊 affinity 存檔升級', () => { }) describe('指令與設定', () => { - test('status 展開事實欄位與來源,查不到為 unknown', async () => { + test('info --all 展開事實欄位與來源,查不到為 unknown', async () => { const { c } = setup() await c.ensureActivated() - const out = await c.command('status') + const out = await c.command('status --all') expect(out).toContain('月見ミコ') expect(out).toContain('birthday:3/3(novel,信心 1,test)') expect(out).toContain('bloodType:unknown') @@ -310,7 +310,7 @@ describe('指令與設定', () => { const fs = memoryFs() const c = createCompanion({ fs, clock: fakeClock(), root: ROOT }) await c.ensureActivated() - const out = await c.command('status') + const out = await c.command('status --all') expect(out).toContain('白上吹雪') expect(out).toContain('hololivepro.com') expect(out).toContain('weight:unknown') diff --git a/plugins/jsc-mod-companion/hooks/companion/core.ts b/plugins/jsc-mod-companion/hooks/companion/core.ts index 7c575da..e806705 100644 --- a/plugins/jsc-mod-companion/hooks/companion/core.ts +++ b/plugins/jsc-mod-companion/hooks/companion/core.ts @@ -3,11 +3,13 @@ import type { LegacyAffinity } from './relation' import { FUBUKI } from './defaults' import { readJson, writeJson } from './fs' import type { Clock, Fs } from './fs' -import { applyEvent, baselineOf, clampDim, decayEmotions, dominant, fromPad, gauge, ranked, toPad } from './emotion' +import { applyEvent, baselineOf, clampDim, decayEmotions, DIM_INFO, dominant, fromPad, gauge, ranked, toPad } from './emotion' import type { Emotions } from './emotion' import { keywordEvent, MOOD_EVENTS, moodLabel } from './mood' import type { MoodEvent } from './mood' -import { extraversionOf, neuroticismOf } from './core/params' +import { extraversionOf, neuroticismOf, PARAM_LABELS, PARAMS, value } from './core/params' +import type { ParamLayers } from './core/params' +import { bar, width } from './seg' import { extractTag, stripTag } from './tags' import { createCare, IDLE_MS } from './care' import type { Care, CareProfile, CareSettings, CareEngine, Signals } from './care' @@ -53,6 +55,8 @@ export type View = { period: string /** 快取中仍有效的天氣標題;未啟用或過期為 null */ weather: string | null + /** 人格 18 參數的三層值;角色包沒有 params 時為 null */ + layers: ParamLayers | null } export type CompanionOptions = { fs: Fs; clock: Clock; root: string; builtins?: CompanionCore[] } @@ -164,6 +168,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan return { core, mood, emotions, ...(main ?? moodLabel(mood)), relation: effective(rel), stage: STAGES[rel.stage]!, todos, adapt: adaptOf(core, st, now, tzMinutes(env.user.tz)).value, absentMs, period: periodOf(now, tzMinutes(env.user.tz)), weather: env.mats.weather?.title ?? null, + layers: core.params ? { base: core.params, save: st.params?.save ?? {}, today: st.params?.today ?? {} } : null, } } @@ -193,22 +198,40 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan await mutate((core, st, now) => ({ ...st, relation: interact(st.relation, kind, now, core.relationTraits) })) } - const bar = (v: number) => (v >= 0 ? '+' : '') + v.toFixed(2) - async function status(all = false): Promise { const v = await view() if (!v) return '目前沒有啟用角色。' - const lines = [`${v.kaomoji} ${v.core.name}|${v.label}`] - lines.push(`心情 PAD:P ${bar(v.mood.p)} A ${bar(v.mood.a)} D ${bar(v.mood.d)}`) const dims = ranked(v.emotions) - lines.push(all ? '情緒(完整):' : '情緒(前 3 強,/jsc-mod-companion:info --all 看完整):') - for (const d of all ? dims : dims.slice(0, 3)) lines.push(` ${gauge(d, v.emotions)}`) - lines.push(`好感:${v.stage}(${v.relation.affection.toFixed(1)})`) - lines.push(`關係:好感 ${v.relation.affection.toFixed(1)} 信賴 ${v.relation.trust.toFixed(1)} 親密 ${v.relation.intimacy.toFixed(1)} 依賴 ${v.relation.dependence.toFixed(1)}`) - lines.push(`適應度:${Math.round(v.adapt)}(${describeAdapt(v.adapt)})`) - lines.push(`時間感:現在是${v.period},距離上次互動 ${describeAbsence(v.absentMs)}${v.weather ? `|天氣:${v.weather}` : ''}`) - lines.push(`待辦:${v.todos}`) - lines.push('基本資訊:') + const head = `${v.core.name} ${v.kaomoji} ${v.label}` + if (!all) { + return [ + head, + `關係 ${v.stage}(好感 ${Math.round(v.relation.affection)})`, + `情緒 ${dims.slice(0, 3).map(d => `${DIM_INFO[d].name} ${Math.round(v.emotions[d] * 100)}`).join('、')}`, + `適應度 ${Math.round(v.adapt)}(${describeAdapt(v.adapt)}) 待辦 ${v.todos} 件`, + '加 --all 看完整數值', + ].join('\n') + } + const rel = Object.entries({ 好感: v.relation.affection, 信賴: v.relation.trust, 親密: v.relation.intimacy, 依賴: v.relation.dependence }) + const lines = [ + `${head} 現在是${v.period},距離上次互動 ${describeAbsence(v.absentMs)}${v.weather ? `|天氣:${v.weather}` : ''}`, + '', + '情緒(8 維度)', + ...dims.map(d => ` ${gauge(d, v.emotions)}`), + '', + `關係(${v.stage})`, + ...rel.map(([k, x]) => ` ${k} ${bar(x, 12)} ${String(Math.round(x)).padStart(3)}`), + '', + `人格(${PARAMS.length} 參數:基準 + 存檔偏移 + 今日偏移)`, + ] + if (v.layers) { + const sign = (n: number) => (n >= 0 ? '+' : '') + Math.round(n) + for (const p of PARAMS) { + const l = v.layers + lines.push(` ${PARAM_LABELS[p]}${' '.repeat(Math.max(1, 20 - width(PARAM_LABELS[p])))}基準 ${Math.round(l.base[p])} 偏移 ${sign(l.save[p] ?? 0)} 今日 ${sign(l.today[p] ?? 0)} 有效 ${Math.round(value(l, p))}`) + } + } else lines.push(' (此角色包沒有人格參數)') + lines.push('', '世界', ` 適應度 ${Math.round(v.adapt)}(${describeAdapt(v.adapt)}) 想家 ${Math.round(v.emotions.homesick * 100)}`, '', '基本資訊') for (const [k, f] of Object.entries(v.core.facts)) { lines.push(f === 'unknown' ? ` ${k}:unknown` : ` ${k}:${f.value}(${f.kind ?? '?'},信心 ${f.confidence ?? '?'},${f.source ?? '無來源'})`) } diff --git a/plugins/jsc-mod-companion/hooks/companion/emotion.test.ts b/plugins/jsc-mod-companion/hooks/companion/emotion.test.ts index fb1c54b..4b893af 100644 --- a/plugins/jsc-mod-companion/hooks/companion/emotion.test.ts +++ b/plugins/jsc-mod-companion/hooks/companion/emotion.test.ts @@ -142,8 +142,8 @@ describe('/status', () => { const c = createCompanion({ fs, clock: fakeClock(), root: '/h/.companion', builtins: [MIKO] }) await c.ensureActivated() await c.moodEvent('scold') - const brief = (await c.command('status')).split('\n').filter(l => l.startsWith(' ') && l.includes('\x1b[')) - expect(brief).toHaveLength(3) + const brief = (await c.command('status')).split('\n').find(l => l.startsWith('情緒 '))! + expect(brief.slice(4).split('、')).toHaveLength(3) const all = (await c.command('status --all')).split('\n').filter(l => l.startsWith(' ') && l.includes('\x1b[')) expect(all).toHaveLength(8) }) diff --git a/plugins/jsc-mod-companion/hooks/companion/emotion.ts b/plugins/jsc-mod-companion/hooks/companion/emotion.ts index c3a700f..e750266 100644 --- a/plugins/jsc-mod-companion/hooks/companion/emotion.ts +++ b/plugins/jsc-mod-companion/hooks/companion/emotion.ts @@ -1,6 +1,6 @@ import { clamp, MOOD_EVENTS } from './mood' import type { MoodEvent } from './mood' -import { bar } from './seg' +import { bar, width } from './seg' import type { Pad } from './types' const HOUR = 3_600_000 @@ -172,9 +172,8 @@ export function dominant(e: Emotions): { dim: Dim; label: string; kaomoji: strin return { dim, label: poles ? poles[e[dim] >= 0.5 ? 1 : 0] : DIM_INFO[dim].name, kaomoji: DIM_INFO[dim].kaomoji } } -/** /status 的情緒量表:▰▱ 依維度上色 */ -export function gauge(d: Dim, e: Emotions): string { - const poles = POLES[d] - const name = poles ? `${DIM_INFO[d].name}(${poles[e[d] >= 0.5 ? 1 : 0]})` : DIM_INFO[d].name - return `${name} \x1b[${DIM_INFO[d].ansi}m${bar(e[d] * 100)}\x1b[0m ${Math.round(e[d] * 100)}` +/** info --all 的情緒量表:▰▱ 依維度上色,名稱補齊到同寬 */ +export function gauge(d: Dim, e: Emotions, cells = 12): string { + const name = DIM_INFO[d].name + return `${name}${' '.repeat(Math.max(0, 6 - width(name)))}\x1b[${DIM_INFO[d].ansi}m${bar(e[d] * 100, cells)}\x1b[0m ${String(Math.round(e[d] * 100)).padStart(3)}` } diff --git a/plugins/jsc-mod-companion/hooks/companion/world.test.ts b/plugins/jsc-mod-companion/hooks/companion/world.test.ts index 559c0cb..4abfb41 100644 --- a/plugins/jsc-mod-companion/hooks/companion/world.test.ts +++ b/plugins/jsc-mod-companion/hooks/companion/world.test.ts @@ -182,7 +182,7 @@ describe('適應度', () => { const v = (await c.view())! expect(v.adapt).toBeGreaterThan(15) expect(v.adapt).toBeLessThan(25) - expect((await c.command('status'))).toContain('適應度:') + expect((await c.command('status'))).toContain('適應度 ') }) }) @@ -424,11 +424,19 @@ describe('setup 與 materials 指令', () => { }) }) -describe('/status 相容', () => { - test('保留原有各行,並多出適應度與時間感', async () => { +describe('info 輸出', () => { + test('短版只有名字、關係、情緒、適應度與待辦', async () => { const { c } = setup() await c.ensureActivated() const s = await c.command('status') - for (const k of ['心情 PAD:', '情緒(前 3 強', '好感:', '關係:', '待辦:', '基本資訊:', '適應度:', '時間感:']) expect(s).toContain(k) + for (const k of ['關係 ', '情緒 ', '適應度 ', '待辦 ', '--all']) expect(s).toContain(k) + for (const k of ['PAD', '基本資訊', '時間感']) expect(s).not.toContain(k) + }) + + test('--all 多出關係長條、人格參數、世界與時間感', async () => { + const { c } = setup() + await c.ensureActivated() + const s = await c.command('status --all') + for (const k of ['情緒(8 維度)', '關係(', '人格(', '世界', '基本資訊', '現在是', '距離上次互動']) expect(s).toContain(k) }) })