feat: 完成 E 群組 — 關係子系統

- RelationshipService:E-1 未知對象自動建檔(禮貌+防備=intimacy/trust=0)、
  E-3 親密度五階分層+跨階事件(@nestjs/event-emitter,StageChangeLogService 獨立訂閱)、
  E-4 久未互動時親密度半衰期衰減並標記「久未見」
- SentimentLedgerService:E-2 關係帳本,負向事件套用放大係數,驗證同等級正負事件淨值為負
- IntentInterpreterService:E-6 依親密度將同一句模稜兩可的話解讀為玩笑或冒犯
- KeywordMemoryRetriever(C-6)擴充 E-5:新增 relatedUserId 加權,retrieve() 簽名改為接受 options 物件
- schema 新增 EpisodicMemory.relatedUserId,並補上所有子表對父表的 onDelete: Cascade/SetNull
  (E-2 測試時發現先前 B 群組遺漏這些規則會導致刪除關係時外鍵違反)
- apps/api 新增 /relationships/* 端點作為引擎驗證介面
- scripts/smoke/E.mjs:涵蓋 E-1~E-6 全部驗收情境

npm run restart && npm run smoke -- E 皆通過(E-V),A/B/C/D 群組冒煙測試無回歸。

本次另排除一個本機環境限定的 Prisma CLI 網路 checkpoint 卡住問題,並從一次部分套用的
migration 中手動恢復資料庫一致性(過程記錄於 todo.md 實作記錄,供後續群組參考)。
This commit is contained in:
Jeffery
2026-08-13 10:43:33 +08:00
parent 611c9b400c
commit 767b17e257
18 changed files with 733 additions and 25 deletions
+14 -7
View File
@@ -174,13 +174,20 @@ flowchart TB
### E. 關係子系統
- [ ] **E-1 親密度與信任讀寫(XS)**:實作關係檔案的建立(未知對象預設「禮貌+防備」初始值)與讀寫服務。驗收:新對象首次互動自動建檔。依據:§關係如何影響對話行為「未知 → 建立新關係檔案,預設:禮貌+防備」。
- [ ] **E-2 關係帳本與負向偏誤(S)**:互動事件寫入 `SentimentLedgerEntry`,負向事件權重放大係數可由角色參數調整。驗收:同等級正負事件各一次後,淨值為負。依據:§關係如何被大腦儲存與更新「負向偏誤:一次背叛抵銷多次善意」。
- [ ] **E-3 親密度分層(S)**:實作 陌生 0-19 /認識 20-39 /朋友 40-59 /摯友-曖昧 60-79 /羈絆 80-100 五階段與跨階解鎖旗標。驗收:跨越門檻時發出可被其他子系統訂閱的事件。依據:§動漫式關係進展(好感度系統)。
- [ ] **E-4 關係時間衰減(S)**:久未互動自動降親密度,重逢時開場語氣可讀取「久未見」旗標。驗收:模擬長時間未互動後親密度下降且旗標為真。依據:§映射到聊天系統:關係模型「關係衰減:時間衰減函數」。
- [ ] **E-5 關係加權檢索接點(S)**:把「與當前對象相關」納入 C-6 檢索權重。驗收:與 A 對話時 A 相關記憶排序優先於同分數的無關記憶。依據:§與其他子系統的整合「關係 × 記憶」。
- [ ] **E-6 意圖推測(M)**:依對象歷史互動模式解讀當前訊息(同一句話,高親密度判為玩笑、低親密度判為冒犯),輸出解讀標記供生成層使用。驗收:相同輸入在高/低親密度下得到不同解讀標記。依據:§映射到聊天系統:關係模型「心智理論 → 意圖推測」。
- [ ] **E-V 階段驗證(XS)**:`npm run restart && npm run smoke -- E`(E.mjs:帳本累積、負向偏誤、分層跨越事件、衰減、關係加權檢索、意圖推測差異)。
- [x] **E-1 親密度與信任讀寫(XS)**:實作關係檔案的建立(未知對象預設「禮貌+防備」初始值)與讀寫服務。驗收:新對象首次互動自動建檔。依據:§關係如何影響對話行為「未知 → 建立新關係檔案,預設:禮貌+防備」。
- [x] **E-2 關係帳本與負向偏誤(S)**:互動事件寫入 `SentimentLedgerEntry`,負向事件權重放大係數可由角色參數調整。驗收:同等級正負事件各一次後,淨值為負。依據:§關係如何被大腦儲存與更新「負向偏誤:一次背叛抵銷多次善意」。
- [x] **E-3 親密度分層(S)**:實作 陌生 0-19 /認識 20-39 /朋友 40-59 /摯友-曖昧 60-79 /羈絆 80-100 五階段與跨階解鎖旗標。驗收:跨越門檻時發出可被其他子系統訂閱的事件。依據:§動漫式關係進展(好感度系統)。
- [x] **E-4 關係時間衰減(S)**:久未互動自動降親密度,重逢時開場語氣可讀取「久未見」旗標。驗收:模擬長時間未互動後親密度下降且旗標為真。依據:§映射到聊天系統:關係模型「關係衰減:時間衰減函數」。
- [x] **E-5 關係加權檢索接點(S)**:把「與當前對象相關」納入 C-6 檢索權重。驗收:與 A 對話時 A 相關記憶排序優先於同分數的無關記憶。依據:§與其他子系統的整合「關係 × 記憶」。
- [x] **E-6 意圖推測(M)**:依對象歷史互動模式解讀當前訊息(同一句話,高親密度判為玩笑、低親密度判為冒犯),輸出解讀標記供生成層使用。驗收:相同輸入在高/低親密度下得到不同解讀標記。依據:§映射到聊天系統:關係模型「心智理論 → 意圖推測」。
- [x] **E-V 階段驗證(XS)**:`npm run restart && npm run smoke -- E`(E.mjs:帳本累積、負向偏誤、分層跨越事件、衰減、關係加權檢索、意圖推測差異)。
> **實作記錄(E 群組)**:
> - 關係引擎放在 `apps/api/src/relationship/`。E-3 跨階事件用 `@nestjs/event-emitter`(`EventEmitterModule.forRoot()` 已在 `app.module.ts` 註冊),`RelationshipService` 只負責 `emit`,實際訂閱者是獨立的 `StageChangeLogService`(`@OnEvent` 訂閱),兩者互不直接呼叫——之後 I/J 群組要訂閱「關係跨階」時比照這個模式加一個新的訂閱者即可,不要改 `RelationshipService` 本身。
> - E-5 為此新增了 `EpisodicMemory.relatedUserId`(`prisma/migrations/20260813022124_add_episodic_memory_related_user`),讓記憶可以標記「與哪位使用者相關」;`KeywordMemoryRetriever.retrieve()` 簽名因此從 `(characterId, query, limit?)` 改成 `(characterId, query, options?: { limit?, relatedUserId? })`——C 群組完成後才加的參數,之後任何呼叫端都要用新簽名。
> - 親密度只用一條「日常互動」通道累積(`recordInteraction`,預設每次 +2),信任則只透過 `SentimentLedgerService.recordEvent` 的負向偏誤事件調整;兩者刻意分開更新,因為 L 群組的「心動值」也會是第三條獨立通道,現在先把「親密度/信任」两條分清楚,之後加心動值不會互相污染。
> - **重要教訓(本機環境限定,務必記住)**:`prisma migrate dev`/`generate`/`resolve` 這類 CLI 指令在本機環境會呼叫一個網路 checkpoint telemetry(`runCheckpointClientCheck`),此環境下該呼叫**不穩定**,曾多次造成指令看似「卡住無輸出」長達 20~30 秒以上。之後所有 Prisma CLI 呼叫一律加上 `CHECKPOINT_DISABLE=1` 環境變數(例:`CHECKPOINT_DISABLE=1 npx prisma migrate dev ...`),可完全避開這個網路呼叫。
> - **更重要的教訓**:本群組實際發生過一次「`migrate dev` 被判定卡住而中止,結果是 migration 已經對部份資料表執行完成」的部分套用(SQLite 對每張 `RedefineTable` 似乎是個別提交,不是整批一個交易)。之後若怀疑某次 `migrate dev`/`migrate deploy` 執行到一半被中斷:**先跑 `CHECKPOINT_DISABLE=1 npx prisma migrate status` 確認 `_prisma_migrations` 是否有 `finished_at` 為空的紀錄**,若有,需要用 `better-sqlite3` 直接檢查每張受影響資料表的 `sqlite_master.sql`(比對是否已含目標 schema/有無殘留的 `new_<table>` 暫存表),**手動補完尚未套用的部分**(可從 `migration.sql` 擷取對應片段執行),最後才下 `prisma migrate resolve --applied <migration_name>` 讓追蹤表與實際狀態一致。絕不可以在部分套用的狀態下直接重跑整份 `migrate dev`/`deploy`,也絕不可以不經確認就對 `_prisma_migrations` 動手。
### F. 對話生成管線與 LLM Provider 抽象