心音(Kokorone)操作手冊
更新時間:2026/08/17 18:26:47(Asia/Taipei)
適用版本:kokorone@0.1.0(monorepo:pnpm workspace + Turborepo)
本手冊只描述「怎麼操作」;系統設計理念與部署背景見 README.md。
1. 系統組成
| 元件 |
目錄 |
技術 |
預設埠 |
啟動方式 |
| api |
apps/api |
NestJS(ESM) |
3001 |
node apps/api/dist/main.js |
| web |
apps/web |
Next.js 16 + Tailwind + pixi.js |
3100 |
next dev / next start |
| mobile |
apps/mobile |
Expo + expo-router |
8081(Expo web) |
pnpm --filter @kokorone/mobile start |
| shared |
packages/shared |
TypeScript 型別/log() |
— |
由 api、web、mobile、腳本共用 |
| db |
packages/db |
Prisma Client 包裝(@kokorone/db) |
— |
由 api 與冒煙測試共用 |
| 資料庫 |
prisma/dev.db |
SQLite(可換 PostgreSQL) |
— |
Prisma migrate |
| 快取 |
外部 |
Redis(選用) |
6379 |
連不到時自動退回 DB/行程記憶體 |
| LLM |
外部 |
CLIProxy → Claude(或直連官方 API) |
3000 |
LLM_PROVIDER=claude 時才呼叫 |
2. 首次安裝
前置條件:Node.js ≥ 20(本機為 v26.7.0)、pnpm 11.21.0(packageManager 已鎖定)。
不需要 Docker——本專案刻意以「Node 行程直跑」為部署模式。
pnpm run restart 全綠時會輸出:
3. 日常操作指令
| 目的 |
指令 |
說明 |
| 建置全部 |
pnpm run build |
turbo 依相依順序建置,含 web production bundle |
| 只建 TypeScript |
npx tsc -b |
restart 內部使用的同一步 |
| 重啟 api/web |
pnpm run restart |
冪等;會先殺掉 .dev-pids 記錄的整個 process group |
| 單一群組冒煙測試 |
pnpm run smoke A |
不要加 --(見下方注意事項) |
| 全部冒煙測試 |
pnpm run smoke all |
依字母序跑 scripts/smoke/*.mjs |
| 看 api 日誌 |
tail -f .dev-logs/api.log |
restart 啟動的 stdout/stderr |
| 看 web 日誌 |
tail -f .dev-logs/web.log |
同上 |
| 查目前行程 |
cat .dev-pids |
{"api": pid, "web": pid} |
| 手動全停 |
kill -TERM -<pid>(pid 取自 .dev-pids,前面的減號代表整個 process group) |
沒有獨立的 stop script;停止邏輯已包在 restart 的第一步 |
| 啟動 mobile |
pnpm --filter @kokorone/mobile start |
另有 android/ios/web 三個 script |
-- 注意事項:pnpm 不像 npm 會吃掉分隔符。pnpm run smoke -- A 會把字面上的 --
當成群組代號而找不到檔案;請寫 pnpm run smoke A。
行程管理細節(scripts/dev-restart.mjs):
- 子行程以
detached: true 啟動,因此訊號一律送給 整個 process group(kill -pid),
避免 next dev 多層 fork 留下孤兒佔用埠號。
- SIGTERM 後 5 秒未結束會補 SIGKILL。
- 健康檢查逾時 60 秒(每秒輪詢):api 檢查
GET /health 的 status === "ok",web 檢查首頁 200。
- 埠號可用
PORT_API/PORT_WEB 覆寫。
4. 環境變數與 Provider 切換
根目錄 .env(api 與腳本共用);apps/web/.env.local 另外設定瀏覽器端要用的 NEXT_PUBLIC_API_URL。
| 變數 |
預設 |
操作重點 |
DATABASE_URL |
file:./prisma/dev.db |
換 PostgreSQL 時同步把 prisma/schema.prisma 的 provider 改成 postgresql |
REDIS_URL |
redis://127.0.0.1:6379 |
選用。連不到只會出現一次 [啟動][WRN] 並退回 DB/行程記憶體 |
PORT |
3001 |
api 監聽埠 |
PORT_API / PORT_WEB |
3001 / 3100 |
僅 pnpm run restart 使用 |
WEB_ORIGIN |
http://localhost:3100 |
CORS 白名單;http://localhost:8081(Expo web)已硬編加入 |
NEXT_PUBLIC_API_URL |
http://localhost:3001 |
build time 注入,改了要重新 build web |
LLM_PROVIDER |
mock |
mock=模板庫(冒煙測試固定用這個);claude=真的打 API |
CLAUDE_BASE_URL |
http://localhost:3000/api/v1 |
走 CLIProxy;直連官方改 https://api.anthropic.com/v1 |
CLAUDE_API_KEY |
(空) |
LLM_PROVIDER=claude 時必填 |
CLAUDE_MODEL |
claude-sonnet-4-5 |
CLIProxy 端需有能接此模型的 Worker,否則回報「沒有任何可用的組合」 |
TTS_PROVIDER |
mock |
real 目前是空殼(P-4),尚未接真實供應商 |
PUSH_PROVIDER |
mock |
expo 可直接用,不需金鑰 |
切換 provider 的操作順序:改 .env → pnpm run restart。
provider 是在模組初始化時決定的(llm.module.ts/voice.module.ts/push.module.ts),不重啟不會生效。
切到 claude 時啟動日誌會有一行 [啟動][INF]: LLM_PROVIDER=claude,對話生成將呼叫真實 API(CLAUDE_BASE_URL=…),可用來確認生效。
5. 使用者端操作
5.1 web(http://localhost:3100)
| 頁面 |
路徑 |
操作 |
| 首頁 |
/ |
心跳波動畫 + 「開始聊天」按鈕 |
| 對話 |
/chat |
輸入訊息送出;右側/上方立繪即時換表情,左上顯示親密度、右上顯示情緒標籤 |
/chat 的行為要點:
- 角色與使用者目前是寫死的
seed-character-genki/seed-user-primary;sessionId 每次開頁用 crypto.randomUUID() 產生。
- 送出後會先查
GET /tachie/:characterId/:userId/expression 套用表情,220ms 後才顯示文字,讓表情先於台詞。
- 若回應
availability === "NO_REPLY"(她在睡覺且未破例),這一輪不會有訊息出現——這是預期行為,不是壞掉。
- 錯誤訊息「訊息傳送失敗,請確認 api 服務是否已啟動」=api 沒起來或 CORS 被擋。
5.2 mobile(Expo)
| 分頁 |
功能 |
備註 |
| 聊天 |
同 web 的對話流程 |
底部分頁籤是 APP 相對行動網頁的唯一差異 |
| 角色 |
角色資訊 |
— |
| 日常 |
顯示 GET /schedule/:characterId/info:作息類型、目前時段、回應可用度、剛睡醒/快睡了 |
角色沒掛作息表時 slot 為 null,顯示「(未設定作息表)」 |
| 設定 |
「檢查連線」按 /health;「啟用推播」請求權限→取 Expo token→POST /push/register |
推播 token 只有實機/支援的模擬器取得到,其他環境會顯示提示訊息 |
6. API 操作參考
Base URL:http://localhost:3001(無全域前綴)。所有 now? 參數都是測試用的「模擬現在時間」(ISO 字串),
seed? 用來固定隨機結果,正式呼叫可省略。
6.1 健康檢查與角色
| 方法 |
路徑 |
說明 |
| GET |
/health |
{ status, version, uptime },restart 與冒煙測試 A 都靠這支 |
| GET |
/characters |
角色清單 |
6.2 對話(正式使用者端點)
| 方法 |
路徑 |
Body |
| POST |
/chat/:characterId |
{ userId, sessionId, text, seed?, now? } → 走完整管線(記憶檢索→情緒→關係→作息→生成→輸出過濾) |
| POST |
/session/:sessionId/end |
{ characterId } → 觸發「睡眠固化」(記憶整併) |
/dialogue/* 是 F 群組留下的工程內部驗證端點,不是給前端用的:
| 方法 |
路徑 |
Body/Query |
| POST |
/dialogue/:characterId/sessions/:sessionId/messages |
{ userId, text, seed?, now? } |
| POST |
/dialogue/:characterId/reinforce |
{ situation, responsePattern } |
| POST |
/dialogue/:characterId/correct |
{ situation, oldResponsePattern, newResponsePattern } |
| GET |
/dialogue/:characterId/top-rule |
?situation= |
6.3 記憶
| 方法 |
路徑 |
說明 |
| POST |
/memory/:characterId/sessions/:sessionId/messages |
{ role: "user"|"character", content, emotionTag?, emotionIntensity? } 寫入工作記憶 |
| GET |
/memory/:characterId/sessions/:sessionId/messages |
取回該 session 上下文 |
| POST |
/memory/:characterId/sessions/:sessionId/consolidate |
手動固化該 session |
| GET |
/memory/:characterId/retrieve |
?query=&limit= 記憶檢索 |
| POST |
/memory/forgetting-sweep |
{ now? } 手動跑遺忘掃描,回 { decayed, deleted } |
| POST/GET |
/memory/test-scheduled-job、/memory/test-scheduled-job/:id |
排程佇列自測用 |
6.4 情緒
| 方法 |
路徑 |
說明 |
| GET |
/emotion/:characterId |
?now= 目前六維情緒狀態(calm/joy/sad/alert/shy/grumpy) |
| GET |
/emotion/:characterId/response-style |
?now= 由情緒推出的語氣/句長/主動性 |
| POST |
/emotion/:characterId/input |
{ text, triggerThreshold?, halfLifeMs?, now?, threatConfirmedSafe?, conflictContinues?, comforted?, placated? } |
6.5 關係
| 方法 |
路徑 |
說明 |
| GET |
/relationships/stage-events |
階段事件定義表 |
| GET |
/relationships/:characterId/:userId |
?now= 親密度/信任/階段 |
| POST |
/relationships/:characterId/:userId/interactions |
{ intimacyDelta?, now? } |
| POST |
/relationships/:characterId/:userId/events |
{ event, weight, negativeBiasMultiplier?, now? } 記入情感帳本 |
| GET |
/relationships/:characterId/:userId/ledger-net |
帳本淨值 |
| GET |
/relationships/:characterId/:userId/intent |
?text= 意圖判讀 |
6.6 人格與角色建立
| 方法 |
路徑 |
說明 |
| POST |
/personality/characters |
建角色。必填 source(ORIGINAL|EXISTING_WORK)、formalName、basicInfo、backgroundStory、personalityArchetype、likesDislikes、goalsObsessions、speechStyle;選填 id、buildStatus、workId、initialRelationValue、initialRelationship{userId,intimacy?,trust?}。會一併產生別名、初始語意/情節記憶、情緒狀態與關係初值 |
| GET |
/personality/archetypes、/personality/archetypes/:archetype |
原型參數表 |
| GET |
/personality/:archetype/expressed-intimacy |
?intimacy= 外顯親密度 |
| GET |
/personality/address |
?userName=&intimacy= 稱呼 |
| GET |
/personality/address-transition |
?userName=&previous=¤t= 稱呼變化 |
| POST |
/personality/:characterId/contrast-trigger |
{ intimacy, emotionTag, now?, roll? } |
| GET |
/personality/cuteness/spoiled |
?intimacy=&emotionTag= |
| GET |
/personality/cuteness/grumpy-recovery |
?turns= |
| GET |
/personality/cuteness/third-party |
?text= |
| GET |
/personality/cuteness/exclusive |
?trust= |
6.7 作息
| 方法 |
路徑 |
說明 |
| POST |
/schedule/:characterId/seed-default |
掛上預設作息表(新角色要先跑這支,日常分頁才看得到時段) |
| POST |
/schedule/:characterId/special-days |
{ date, label } |
| GET |
/schedule/:characterId/info |
?now= 目前時段/可用度 |
| POST |
/schedule/:characterId/exception |
{ userId, mode, intimacy, dominantEmotion, userInput, now? } 嘗試破例 |
| GET |
/schedule/:characterId/baseline |
?now= 作息造成的情緒基線 |
| POST |
/schedule/:characterId/offline-event |
{ now? } 生成離線日常事件(每天一次) |
6.8 任務
| 方法 |
路徑 |
說明 |
| GET |
/task?characterId=&userId= |
任務清單 |
| GET |
/task/:taskId |
單筆 |
| POST |
/task/:characterId/parse |
{ userId, text, now? } 自然語言解析成任務草稿 |
| POST |
/task/:characterId |
{ userId, type, content, rawText, triggerAt?, cronExpression?, condition?, now? };type:ONE_TIME|RECURRING|CONDITIONAL|QUERY_ORGANIZE|TODO_TRACKING |
| POST |
/task/:taskId/fire-now |
{ now? } 立刻觸發 |
| POST |
/task/:taskId/check-overdue/complete/fail |
{ now? } |
| POST |
/task/proactive-care/:characterId |
{ userId, now? } 主動關心 |
6.9 多人房間
| 方法 |
路徑 |
說明 |
| POST |
/room |
{ userId, characterIds[], mode?: "GROUP"|"SELF_CHAT", topicSeed? } |
| GET |
/room/:roomId |
房間狀態與發言記錄 |
| POST |
/room/:roomId/message |
{ userId, userLabel?, text, now? } |
| POST |
/room/:roomId/self-chat/advance |
{ now? } 推進角色間自聊 |
| POST |
/room/:roomId/interject |
同 message body,插話 |
| POST |
/room/:roomId/end |
結束並固化 |
| POST |
/room/relationship/:characterId |
{ otherCharacterId, affinity, dynamic? } 設定角色間關係 |
6.10 戀愛線與分級
| 方法 |
路徑 |
說明 |
| POST |
/romance/:characterId/age-evidence |
{ evidenceType, impliesAdult, isEpilogue?, description } |
| GET |
/romance/:characterId/age-determination |
年齡判定結果 |
| GET |
/romance/:characterId/eligibility |
戀愛線可用性 |
| POST |
/romance/adult-mode/consent |
{ userId, verifiedAdult, optedIn } |
| POST |
/romance/:characterId/adult-content-check |
{ userId, now? };被拒會寫入嘗試記錄 |
| GET |
/romance/:characterId/:userId/state |
?now= 戀愛階段狀態 |
| POST |
/romance/:characterId/event |
{ userId, eventType, severe?, now? } |
| POST |
/romance/:characterId/confess |
{ userId, now? } |
| POST |
/romance/:characterId/milestone |
{ userId, milestoneType, now? } |
6.11 正史研究管線(canon)
| 方法 |
路徑 |
說明 |
| POST |
/canon/scenes |
匯入場景(自動做台詞歸屬判定) |
| GET |
/canon/scenes/:sceneId、/canon/works/:workId/scenes |
查場景 |
| POST |
/canon/scene-lines/:lineId/adjudicate |
{ speakerCharacterId } |
| POST |
/canon/scenes/:sceneId/reattempt-quarantined |
重試被隔離的台詞 |
| POST |
/canon/scene-lines/:lineId/correct |
{ newSpeakerCharacterId, now? } 人工更正 |
| GET |
/canon/characters/:characterId/roster-progress |
角色表建構進度 |
| POST |
/canon/illustrations、GET /canon/scenes/:sceneId/illustrations |
插圖登錄/查詢 |
| GET |
/canon/characters/:characterId/costume-catalog、.../pose-vocabulary |
服裝/姿勢詞彙 |
| POST |
/canon/works/:workId/anchor |
{ storyOrder } 設定時間軸錨點 |
| POST/GET |
/canon/works/:workId/detect-contradictions、/contradictions |
矛盾偵測與清單 |
6.12 後日談(epilogue)
| 方法 |
路徑 |
說明 |
| POST |
/epilogue/works/:workId/check-transition |
檢查是否進入後日談 |
| POST |
/epilogue/works/:workId/time-flow |
{ mode } 時間流速 |
| GET |
/epilogue/characters/:characterId/drift |
?atStoryOrder= 人格漂移 |
| POST |
/epilogue/characters/:characterId/milestone |
提案人生里程碑 |
| POST |
/epilogue/works/:workId/reclaim |
{ newCanonScenes: [...] } 正史回收 |
| POST |
/epilogue/works/:workId/branch |
分支 |
6.13 立繪與語音
| 方法 |
路徑 |
說明 |
| GET |
/tachie/:characterId/manifest |
立繪素材清單 |
| GET |
/tachie/:characterId/:userId/expression |
?isThinking=&isDeflecting= 目前表情差分 |
| POST |
/voice/:characterId/voice-sheet/seed-default |
{ verbalTics?, forbiddenSounds? } |
| GET |
/voice/:characterId/voice-sheet |
聲音設定表 |
| GET |
/voice/:characterId/:userId/prosody、.../nonverbal-cue |
語調/非語言提示 |
| POST |
/voice/:characterId/:userId/synthesize |
{ text, now?, seed? }(TTS_PROVIDER=mock 時回模擬結果) |
| POST |
/voice/:characterId/analyze-input |
{ userId, text, paralinguistic, now? } |
6.14 推播
| 方法 |
路徑 |
說明 |
| POST |
/push/register |
{ userId, token, platform } |
| POST |
/push/send-test |
{ userId, title, body };PUSH_PROVIDER=mock 只寫記錄不真的送 |
7. 冒煙測試
- 執行前必須先
pnpm run restart——測試是打真實 HTTP 端點 + 直接讀寫資料庫的。
LLM_PROVIDER 請保持 mock,群組斷言依賴模板庫的確定性輸出。
- 全綠時每組輸出
[冒煙測試][INF]: X 群組冒煙測試全綠;任一組失敗整體 exit code 為 1,
但 all 不會中斷,會把每組跑完再回報。
| 群組 |
覆蓋範圍 |
主要驗證對象 |
| A |
基礎環境 |
/health 200、web 首頁可服務 |
| B |
資料庫 schema |
每張表都 count() 得動(缺表就拋錯) |
| C |
記憶系統 |
工作記憶寫入/固化、retrieve、遺忘掃描、排程佇列 |
| D |
情緒引擎 |
六維狀態機、衰減、標記器(直接操作 emotionState) |
| E |
關係系統 |
互動/事件、情感帳本淨值、階段事件、意圖判讀 |
| F |
LLM 對話管線 |
/dialogue/*、行為強化與更正、(會 spawn 子行程驗證重啟行為) |
| G |
人格 |
原型參數、稱呼、反差觸發、角色建立、可愛度行為 |
| H |
對話 API + 前端 |
/chat/:id、/session/:id/end、關係與工作記憶連動、web 頁面 |
| I |
作息 |
預設作息表、時段判定、破例、情緒基線、離線事件(固定時間軸) |
| J |
任務 |
解析、建立、觸發、逾期、完成/失敗、主動關心 |
| K |
多人房間 |
建房、發言權、自聊推進、插話、結束固化、跨角色記憶 |
| L |
戀愛與分級 |
年齡判定、成人模式同意、內容閘門、告白、里程碑 |
| M |
正史管線 |
場景匯入、台詞歸屬、隔離重試、人工更正、插圖、矛盾偵測 |
| N |
後日談 |
時間流速、人格漂移、正史回收 |
| O |
立繪表情 |
manifest、表情差分(含 thinking/deflecting)、與對話的先後順序 |
| P |
語音 |
voice sheet、語調、非語言提示、合成、輸入分析 |
| Q |
推播與 APP |
/health、token 註冊、測試推播、任務觸發推播 |
註:README 提到「A~R」,但 scripts/smoke/ 目前只有 A~Q(17 個檔案)。
R 群組(Redis 快取、PostgreSQL 遷移、真實 LLM provider)沒有對應的冒煙測試腳本,
pnpm run smoke all 也就不會涵蓋——R 的變更要靠 .env 切換後手動驗證。
8. 資料庫操作
| 目的 |
指令 |
| 套用既有遷移(正式/CI) |
npx prisma migrate deploy |
| 開發時改 schema 後產生遷移 |
npx prisma migrate dev --name <說明> |
| 檢查遷移狀態 |
npx prisma migrate status |
| 重新產生 Prisma Client |
npx prisma generate |
| 灌種子資料 |
npx prisma db seed |
| 視覺化瀏覽 |
npx prisma studio |
prisma.config.ts 已指定 schema: prisma/schema.prisma、migrations.path: prisma/migrations、
seed: node prisma/seed.ts(Node 26 直接跑 TS,不需要額外 loader)。
種子資料(prisma/seed.ts,upsert 冪等,可重複執行):
| 項目 |
ID |
內容 |
| 使用者 |
seed-user-primary |
測試使用者 |
| 角色 |
seed-character-genki |
別名「小陽」,元氣型原型 |
| 關係 |
seed-relationship-genki-primary |
親密度 25/信任 10/階段 ACQUAINTANCE |
| 情緒 |
— |
calm=70, joy=20,其餘 0 |
| 記憶 |
seed-memory-episodic-cheer、seed-memory-semantic-sport、seed-rule-greeting |
情節/語意/程序記憶各一 |
| 帳本 |
seed-ledger-first-help |
權重 8 的正向事件 |
web 與 mobile 目前都寫死用這兩個 ID,所以沒灌種子資料前端會直接報錯。
種子角色沒有掛作息表,要看「日常」分頁的時段資訊,需先呼叫
POST /schedule/seed-character-genki/seed-default。
切換到 PostgreSQL 的操作順序:改 DATABASE_URL → 改 schema.prisma 的 provider
→ npx prisma migrate deploy → npx prisma db seed → pnpm run restart。
9. 背景排程與快取行為
| 機制 |
位置 |
操作影響 |
| 遺忘掃描 |
memory.module.ts,cron 0 * * * *(每小時整點) |
情緒強度低且 24 小時未被提取的情節記憶權重 ×0.5,低於 0.05 直接刪除。要立即驗證用 POST /memory/forgetting-sweep |
| 任務排程 |
InProcessJobQueue(setTimeout + node-cron) |
計時器只在記憶體裡,重啟即消失;api 啟動時 TaskSchedulerService.onModuleInit 會重掃 status=PENDING 且有 triggerAt 的任務重新掛回,已逾期的立刻補觸發 |
| 排程錯誤 |
同上 |
失敗只記 [排程][ERR]: 工作 <name> 執行失敗:…,不會讓行程崩掉——排查請看 .dev-logs/api.log |
| Redis 快取 |
cache/redis-client.ts |
情緒狀態即時讀寫與 session 快取的加速層。連不到時只在啟動印一次 WRN,之後靜默退回 Prisma/行程記憶體 Map;功能不會少,只是慢 |
10. 疑難排解
| 症狀 |
判斷與處置 |
restart 報「健康檢查逾時」 |
看 .dev-logs/api.log/web.log;常見是埠被占用或 tsc -b 產物過舊 |
restart 報「行程已提前結束」 |
同上看 log;api 多為 DATABASE_URL 指向不存在的資料庫或遷移未套用 |
埠號被占用但 .dev-pids 是空的 |
上次不是用 restart 停的,孤兒行程仍在:lsof -i :3001/:3100 找出後 kill -TERM -<pgid> |
| web 顯示「訊息傳送失敗」 |
api 沒起來,或前端來源不在 CORS 白名單(設 WEB_ORIGIN 後重啟 api) |
| 前端 API 位址改不動 |
NEXT_PUBLIC_API_URL 是 build time 注入,改完要重新 pnpm run build/重啟 next dev |
| 送訊息沒有任何回覆 |
檢查回應的 availability:NO_REPLY 是作息(睡眠)造成的預期行為,可用 ?now= 或作息破例端點驗證 |
| 冒煙測試找不到群組 |
用了 pnpm run smoke -- A;改成 pnpm run smoke A |
| 冒煙測試 B 群組拋錯 |
遷移沒套用完:npx prisma migrate deploy |
| 對話內容變成模板句 |
LLM_PROVIDER 還是 mock;要真實生成需設 claude + CLAUDE_API_KEY 後重啟 |
| CLIProxy 回「沒有任何可用的 {模型}-{執行者}-{工具} 組合」 |
CLIProxy 端沒有能接 CLAUDE_MODEL 的 Worker 註冊,屬代理端問題 |
| 啟動時出現 Redis WRN |
正常,Redis 是選用加速層;要消掉就把 Redis 跑起來或忽略 |
| 推播拿不到 token |
非實機環境取不到 Expo push token(設定頁會顯示提示),改用 POST /push/send-test 驗證後端流程 |
11. 正式環境部署(摘要)
完整說明見 README.md 的「部署路徑」。操作順序:
pnpm install --frozen-lockfile && pnpm run build
npx prisma migrate deploy
- 啟動:api
node apps/api/dist/main.js;web 在 apps/web 下 npx next start -p <PORT_WEB>
- 用 systemd(或 pm2)常駐兩個 Node 行程,
Restart=on-failure,健康檢查沿用 GET /health 與 web 首頁 200
- nginx/負載平衡器終止 TLS 後轉發,對外只開 443
- mobile 走
eas build 產出安裝檔,不隨 api/web 常駐部署