feat: companion 預設角色吹雪補上長相、口癖與簡短經歷 #57
@@ -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〉預定發行' },
|
||||
]
|
||||
@@ -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 })
|
||||
|
||||
@@ -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<string, CanonSeed[]> }
|
||||
|
||||
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<MemoryDoc>(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,
|
||||
|
||||
Reference in New Issue
Block a user