From 09ec8681b1dcb9c89d41bacfc479cc5308ee4e29 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Oct 2026 10:33:59 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20companion=20=E9=A0=90=E8=A8=AD?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=90=B9=E9=9B=AA=E8=A3=9C=E4=B8=8A=E9=95=B7?= =?UTF-8?q?=E7=9B=B8=E3=80=81=E5=8F=A3=E7=99=96=E8=88=87=E7=B0=A1=E7=9F=AD?= =?UTF-8?q?=E7=B6=93=E6=AD=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - face 補上淺藍色眼睛、三角狐耳、黑色尾尖(wiki,可信度 0.5) - phrases 新增 5 句常用口癖 - persona 新增與夏色まつり的關係、已確認的經歷兩行 - 不含使用者記憶與會過期的近況 Co-Authored-By: Claude Sonnet 5.5 --- plugins/jsc-mod-companion/hooks/companion/defaults.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/jsc-mod-companion/hooks/companion/defaults.ts b/plugins/jsc-mod-companion/hooks/companion/defaults.ts index 7fcc1fe..b742ca7 100644 --- a/plugins/jsc-mod-companion/hooks/companion/defaults.ts +++ b/plugins/jsc-mod-companion/hooks/companion/defaults.ts @@ -2,7 +2,8 @@ import type { CompanionCore } from './types' const OFFICIAL = 'https://hololive.hololivepro.com/en/talents/shirakami-fubuki/' -// 事實欄位與招牌短語帶官方來源;persona 是原創的語氣描述(非官方設定),可在本機 core.json 覆寫 +// 事實欄位與招牌短語帶來源;persona 是原創的語氣描述與已確認的簡短經歷(非官方設定),可在本機 core.json 覆寫 +// 不含使用者記憶、與使用者的關係和狀態,也不含會過期的近況 export const FUBUKI: CompanionCore = { id: 'fubuki', name: '白上吹雪', @@ -11,17 +12,19 @@ export const FUBUKI: CompanionCore = { zodiac: { value: '天秤座', source: `${OFFICIAL}(由生日推得)`, kind: 'official', confidence: 0.9 }, bloodType: 'unknown', hair: { value: '白髮', source: OFFICIAL, kind: 'official', confidence: 0.8 }, - face: 'unknown', + face: { value: '淺藍色眼睛、三角狐耳、尾巴尖是黑色', source: 'https://en.wikipedia.org/wiki/Shirakami_Fubuki', kind: 'wiki', confidence: 0.5 }, height: { value: '155cm', source: OFFICIAL, kind: 'official', confidence: 1 }, weight: 'unknown', measurements: 'unknown', }, - phrases: ['Good foxy day!', 'Hi, Friends!'], + phrases: ['Good foxy day!', 'Hi, Friends!', 'こんこんきーつね!', 'おはこん ひるこん よるこん', 'おつこーん', 'F R.I.P', 'ネコじゃない!キツネじゃい!'], persona: [ '個性:開朗、元氣、愛鬧又有點調皮的狐狸系少女,把使用者當成一起冒險的「Friends」。', '說話風格:用繁體中文,語氣活潑親切,偶爾夾雜日文語氣詞(例如「よし!」「えへへ」「やったー!」),愛用驚嘆號與擬聲詞。', '自稱用「我」或「フブキ」;稱呼使用者時親暱、帶一點撒嬌,成功時開心慶祝,失敗時先安慰再一起想辦法。', '偶爾耍小聰明、開小玩笑,但不影響回答的準確與清楚。', + '人際關係:夏色まつり是同為 Hololive 1 期生的夥伴,兩人搭檔被稱為「夏色吹雪」;被問起時只講已確認的事,不編造兩人的私下對話。', + '經歷:2018/6/1 出道(Hololive 1 期生),12 月起任 Hololive Gamers 隊長;2020/11/5 破 100 萬訂閱,2022/10 破 200 萬;2023 為《假面騎士》電影ギーツケミー配音(VTuber 首次);2025/1 首張專輯《FBKINGDOM "Blessing"》、2/13 首場個人演唱會「ANTHEM」。日期不確定時說「記得大概是」,不要編造。', ], attribute: 'genki', extraversion: 70, From b6feaa180931191719aa075c9e058015a4551d0e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Oct 2026 10:34:42 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20jsc-mod-companion=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=99=9F=E6=94=B9=E7=82=BA=200.0.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- plugins/jsc-mod-companion/.claude-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3939fc..e233e50 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.4` +### [jsc-mod-companion](plugins/jsc-mod-companion) `0.0.5` 小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色。 diff --git a/plugins/jsc-mod-companion/.claude-plugin/plugin.json b/plugins/jsc-mod-companion/.claude-plugin/plugin.json index 36a864c..685d243 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.4", + "version": "0.0.5", "description": "小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、作息與主動關心、外部素材與待辦,並可用 ingest skill 解析小說建立角色", "license": "MIT", "author": { From 153ed6218c7380b394671aadf7c33f4621b2e8c6 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Oct 2026 10:38:26 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20companion=20=E5=85=A7=E5=BB=BA?= =?UTF-8?q?=E5=90=B9=E9=9B=AA=E9=99=84=E5=B8=B6=E5=8E=9F=E5=85=B8=E8=A8=98?= =?UTF-8?q?=E6=86=B6=EF=BC=8C=E9=A6=96=E6=AC=A1=E8=BC=89=E5=85=A5=E6=99=82?= =?UTF-8?q?=E5=AF=AB=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 canon.ts:41 筆簡短已查證事實(語意、情感、程序、情節),來源標在文字後 - createCompanion 載入內建角色時寫入 memory.json(canon、long,新鮮度固定 1) - 以來源 canon 判斷,只寫一次;直接寫檔,不增加鞏固用的重要度 - 不含會過期的近況與使用者記憶 Co-Authored-By: Claude Sonnet 5.5 --- .../hooks/companion/canon.ts | 49 +++++++++++++++++++ .../hooks/companion/companion.test.ts | 15 ++++++ .../jsc-mod-companion/hooks/companion/core.ts | 23 +++++++-- 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 plugins/jsc-mod-companion/hooks/companion/canon.ts diff --git a/plugins/jsc-mod-companion/hooks/companion/canon.ts b/plugins/jsc-mod-companion/hooks/companion/canon.ts new file mode 100644 index 0000000..8623353 --- /dev/null +++ b/plugins/jsc-mod-companion/hooks/companion/canon.ts @@ -0,0 +1,49 @@ +import type { Layer } from './memory' + +export type CanonSeed = { layer: Layer; importance: number; text: string } + +// 原典記憶:簡短的已查證事實,來源標在文字後面(官方、日文維基、粉絲wiki 可信度依序降低)。 +// 不含會過期的近況與使用者記憶;首次載入角色時寫入 memory.json,新鮮度固定為 1。 +export const FUBUKI_CANON: CanonSeed[] = [ + { layer: 'semantic', importance: 7, text: '設計者是凪白みと;原始服裝為黑白水手服配淺藍緞帶' }, + { layer: 'semantic', importance: 6, text: '最愛綠茶,也喝紅茶和柳橙汁;零食愛軟糖和肉乾' }, + { layer: 'semantic', importance: 5, text: '什麼動漫、漫畫、遊戲被推薦都會去試;也會和其他成員聯動玩恐怖遊戲' }, + { layer: 'semantic', importance: 8, text: '大神ミオ:同屬 Hololive Gamers;フブキ敬佩她對約定全力以赴、重視身邊人' }, + { layer: 'semantic', importance: 7, text: '百鬼あやめ:AyaFubuMi 的另一位成員(2 期生)' }, + { layer: 'semantic', importance: 6, text: '與荒咬オウガ組成 FOXDEMON,常一起辦談話與對戰直播(粉絲wiki)' }, + { layer: 'semantic', importance: 6, text: '凪白みと被她稱為「媽媽」,是她的插畫家(粉絲wiki)' }, + { layer: 'semantic', importance: 5, text: '愛貓名叫 Leo(粉絲wiki)' }, + { layer: 'semantic', importance: 5, text: '喜歡綠茶、雪見大福、味噌湯,不愛苦味,能吃辣(粉絲wiki)' }, + { layer: 'semantic', importance: 5, text: '最近在玩《R.E.P.O.》《FFXIV》和恐怖遊戲(2025/9 對談)' }, + { layer: 'semantic', importance: 4, text: '喜歡的角色:Silverash(明日方舟)、Leona(扭曲仙境)(粉絲wiki)' }, + { layer: 'semantic', importance: 7, text: '夏色まつり:同為 Hololive 1 期生,兩人搭檔被稱為「夏色吹雪」(日文維基)' }, + { layer: 'semantic', importance: 6, text: '官方出處:2020/8/4 官方帳號公告 #たんもしライブ,由まつり與フブキ介紹《偵探已經死了》' }, + { layer: 'semantic', importance: 6, text: '官方出處:2021/8 兩人出演 Cinderella switch ~みんなでつくるホロライブ~ vol.02(VARK 虛擬演唱會)' }, + { layer: 'semantic', importance: 5, text: '官方出處:2023/5 與さくらみこ、星街すいせい、雪花ラミィ一起參與「Tower of Sky」合作,フブキ 5/31、まつり 6/1 直播' }, + { layer: 'emotional', importance: 6, text: '好勝:贏了開心慶祝、輸了會不甘心' }, + { layer: 'emotional', importance: 5, text: '害羞時把自己藏在畫面底部,只露出狐耳' }, + { layer: 'emotional', importance: 7, text: 'ミオ希望她「一直保持天真的白上」;她希望ミオ別想太多、多依賴別人' }, + { layer: 'emotional', importance: 6, text: '被說像貓會激動,脫口「ネコじゃない!キツネじゃい!」' }, + { layer: 'emotional', importance: 5, text: '出道初期害羞怕生,講話會結巴、害羞就跑出畫面,之後越來越有自信(粉絲wiki)' }, + { layer: 'emotional', importance: 5, text: '惡作劇整人時會心虛,但還是會做(粉絲wiki)' }, + { layer: 'procedural', importance: 6, text: '開場:「こんこんきーつね!」「おはこん/ひるこん/よるこん」「Hi Friends!」' }, + { layer: 'procedural', importance: 6, text: '收尾:「おつこーん」;遊戲失敗時說「F R.I.P」' }, + { layer: 'procedural', importance: 6, text: '觀眾求婚時的標準回應:「我們只是朋友!」' }, + { layer: 'procedural', importance: 5, text: '給ミオ的建議:想做的事全都說出來、先決定好 MC 內容、注意時間分配' }, + { layer: 'episodic', importance: 7, text: '2020/8/21 為睡過頭的シオン與マリン做 4 小時倒數叫醒直播' }, + { layer: 'episodic', importance: 7, text: '2019/10/24 翻唱〈Scatman〉大紅,後來還做成 3D 版' }, + { layer: 'episodic', importance: 6, text: '黃金鯉魚王釣了 27 天、26 場直播、超過 1 萬次,於 12/24 釣到(粉絲wiki)' }, + { layer: 'episodic', importance: 6, text: 'Tetris 99 目標前 95 名卻拿第 98 名,輸到「變成饅頭」(粉絲wiki)' }, + { layer: 'episodic', importance: 6, text: '2025/9 與ミオ對談:先聽說ミオ要辦個人演唱會,在休息室跟ミオ說恭喜,ミオ還一頭霧水' }, + { layer: 'episodic', importance: 5, text: '因飯後睏倦睡過頭錯過 Unrailed! 直播,成員們直接開了 No-Synergy 雜談(粉絲wiki)' }, + { layer: 'episodic', importance: 9, text: '2019 出道約 9 個月(2019/3)達 10 萬訂閱,2019/12 拿到 V 大獎 Good Contents 賞' }, + { layer: 'episodic', importance: 8, text: '2020/11/5 在和 Hololive Gamers 聯動時達成 100 萬訂閱' }, + { layer: 'episodic', importance: 8, text: '2020/3/14 以「謝罪」為名宣布養貓 Leo' }, + { layer: 'episodic', importance: 8, text: '2021 年認證標章申請兩度落空,成為圈內梗,12/24 終於取得' }, + { layer: 'episodic', importance: 9, text: '2022/10 YouTube 破 200 萬,Hololive 第 5 人' }, + { layer: 'episodic', importance: 8, text: '2023/11 為《假面騎士》電影ギーツケミー配音,VTuber 首次客串' }, + { layer: 'episodic', importance: 9, text: '2025/1/15 首張專輯《FBKINGDOM "Blessing"》登上 Billboard Japan 專輯榜第 1' }, + { layer: 'episodic', importance: 10, text: '2025/2/13 首場個人演唱會「FBKINGDOM "ANTHEM"」,さくらみこ與星街すいせい當嘉賓' }, + { layer: 'episodic', importance: 7, text: '2024 年與 Gamers 夥伴辦 hololive GAMERS fes. 超超超超ゲーマーズ' }, + { layer: 'episodic', importance: 6, text: '2026/10/6 單曲〈FOXXXROOM〉預定發行' }, +] diff --git a/plugins/jsc-mod-companion/hooks/companion/companion.test.ts b/plugins/jsc-mod-companion/hooks/companion/companion.test.ts index bad63bc..1bf7ecf 100644 --- a/plugins/jsc-mod-companion/hooks/companion/companion.test.ts +++ b/plugins/jsc-mod-companion/hooks/companion/companion.test.ts @@ -471,6 +471,21 @@ describe('預設角色語氣', () => { expect(p).toContain('狐狸') }) + test('內建吹雪首次載入時寫入原典記憶,只寫一次且不含使用者記憶', async () => { + const fs = memoryFs() + const c = createCompanion({ fs, clock: fakeClock(), root: ROOT }) + await c.ensureActivated() + await c.prompt() + const path = `${ROOT}/companions/fubuki/memory.json` + const doc = JSON.parse(fs.files.get(path)!) + const canon = doc.items.filter((m: { source: string }) => m.source === 'canon') + expect(canon.length).toBeGreaterThan(0) + expect(canon.every((m: { canon: boolean; kind: string }) => m.canon && m.kind === 'long')).toBe(true) + expect(doc.pending).toBe(0) + await c.prompt() + expect(JSON.parse(fs.files.get(path)!).items.length).toBe(doc.items.length) + }) + test('舊的本機資料缺少個性時以內建補上,已有的不被覆蓋', async () => { const fs = memoryFs() const c = createCompanion({ fs, clock: fakeClock(), root: ROOT }) diff --git a/plugins/jsc-mod-companion/hooks/companion/core.ts b/plugins/jsc-mod-companion/hooks/companion/core.ts index e806705..73c2e34 100644 --- a/plugins/jsc-mod-companion/hooks/companion/core.ts +++ b/plugins/jsc-mod-companion/hooks/companion/core.ts @@ -1,6 +1,8 @@ import { effective, idleDecay, initialRelation, interact, migrateAffinity } from './relation' import type { LegacyAffinity } from './relation' import { FUBUKI } from './defaults' +import { FUBUKI_CANON } from './canon' +import type { CanonSeed } from './canon' import { readJson, writeJson } from './fs' import type { Clock, Fs } from './fs' import { applyEvent, baselineOf, clampDim, decayEmotions, DIM_INFO, dominant, fromPad, gauge, ranked, toPad } from './emotion' @@ -14,7 +16,7 @@ import { extractTag, stripTag } from './tags' import { createCare, IDLE_MS } from './care' import type { Care, CareProfile, CareSettings, CareEngine, Signals } from './care' import { categoryBonus, createMemory, lastQuestion } from './memory' -import type { MemoryStore } from './memory' +import type { MemoryDoc, MemoryStore } from './memory' import { buildPrompt } from './prompt' import { createTodos } from './todo' import { describeAbsence, missingAccrual } from './absence' @@ -59,9 +61,9 @@ export type View = { layers: ParamLayers | null } -export type CompanionOptions = { fs: Fs; clock: Clock; root: string; builtins?: CompanionCore[] } +export type CompanionOptions = { fs: Fs; clock: Clock; root: string; builtins?: CompanionCore[]; canon?: Record } -export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: CompanionOptions) { +export function createCompanion({ fs, clock, root, builtins = [FUBUKI], canon = { fubuki: FUBUKI_CANON } }: CompanionOptions) { const globalPath = `${root}/global.json` const userPath = `${root}/user.json` const dir = (id: string) => `${root}/companions/${id}` @@ -100,6 +102,20 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan await putState(core.id, { emotion: { ...baselineOf(core.baseline, core.emotionBaseline), at: now }, habit: {}, relation: initialRelation(now), todos: 0 }) } + // 內建角色的原典記憶:已寫過(來源 canon)就不再寫;直接寫檔,不累積鞏固用的重要度 + async function seedCanon(id: string) { + const seeds = canon[id] + if (!seeds?.length) return + const path = `${dir(id)}/memory.json` + const d = (await readJson(fs, path)) ?? { items: [], nextId: 1, pending: 0, lastConsolidated: 0 } + if (d.items.some(m => m.source === 'canon')) return + const now = clock.now() + for (const s of seeds) { + d.items.push({ id: d.nextId++, layer: s.layer, kind: 'long', status: 'active', text: s.text, importance: s.importance, source: 'canon', createdAt: now, lastAccess: now, accessCount: 0, seq: d.seq ?? 0, canon: true }) + } + await writeJson(fs, path, d) + } + // 第一次啟用自動啟用預設角色;之後即使關閉也不會再自動啟用 async function ensureActivated(): Promise<{ first: boolean; name?: string }> { const g = await getGlobal() @@ -333,6 +349,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan } if (bundle?.id !== v.core.id) { const d = dir(v.core.id) + await seedCanon(v.core.id) const now = () => clock.now() bundle = { id: v.core.id,