Files
Kokorone/docs/操作手冊.md
T

24 KiB
Raw Blame History

心音(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 時才呼叫
flowchart LR
  subgraph 用戶端
    W["web :3100<br/>/ 與 /chat"]
    M["mobile APP<br/>聊天/角色/日常/設定"]
  end
  W -->|NEXT_PUBLIC_API_URL| A
  M -->|API_BASE_URL| A
  A["api :3001<br/>NestJS 17 個模組"]
  A --> DB[("SQLite / PostgreSQL<br/>Prisma")]
  A -.選用.-> R[("Redis :6379<br/>情緒/session 快取")]
  A -.LLM_PROVIDER=claude.-> P["CLIProxy :3000<br/>→ Claude"]
  A -.PUSH_PROVIDER=expo.-> E["Expo Push"]

2. 首次安裝

前置條件:Node.js ≥ 20(本機為 v26.7.0)、pnpm 11.21.0(packageManager 已鎖定)。 不需要 Docker——本專案刻意以「Node 行程直跑」為部署模式。

cd /root/jiantw83/Kokorone

pnpm install                      # 安裝所有 workspace 套件
cp .env.example .env              # 依需要調整(見第 4 節)

npx prisma migrate deploy         # 套用 prisma/migrations(15 筆)
npx prisma db seed                # 建立種子使用者/角色(見第 8 節)

pnpm run build                    # turbo:shared → db → api → web
pnpm run restart                  # 停舊行程、tsc -b、啟動 api/web 並等健康檢查

pnpm run restart 全綠時會輸出:

[…][重啟][INF]: 啟動 api(pid=…,port=3001)
[…][重啟][INF]: 啟動 web(pid=…,port=3100)
[…][重啟][INF]: api /health 健康檢查通過
[…][重啟][INF]: web 首頁 健康檢查通過
[…][重啟][INF]: api 與 web 皆已就緒

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)

pnpm --filter @kokorone/mobile start      # 掃 QR 進 Expo Go
pnpm --filter @kokorone/mobile web        # 瀏覽器版(:8081,已在 api CORS 白名單)
分頁 功能 備註
聊天 同 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=&current= 稱呼變化
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 smoke A         # 單一群組
pnpm run smoke all       # 全部群組(依字母序)
  • 執行前必須先 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 的「部署路徑」。操作順序:

  1. pnpm install --frozen-lockfile && pnpm run build
  2. npx prisma migrate deploy
  3. 啟動:api node apps/api/dist/main.js;web 在 apps/web 下 npx next start -p <PORT_WEB>
  4. 用 systemd(或 pm2)常駐兩個 Node 行程,Restart=on-failure,健康檢查沿用 GET /health 與 web 首頁 200
  5. nginx/負載平衡器終止 TLS 後轉發,對外只開 443
  6. mobile 走 eas build 產出安裝檔,不隨 api/web 常駐部署