feat(遊戲化): 新增大廳、附身、時間流、後日談進度等遊戲化系統功能

新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、
時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙
測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 09:10:26 +08:00
co-authored by Claude Sonnet 5
parent 127a59dafd
commit 1bea06e3cd
195 changed files with 17219 additions and 738 deletions
+357 -100
View File
@@ -114,32 +114,57 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
relationships Relationship[]
relatedMemories EpisodicMemory[]
tasks Task[]
adultModeConsent AdultModeConsent?
romanceEventLogs RomanceEventLog[]
adultContentAttempts AdultContentAttemptLog[]
pushTokens PushToken[]
pushNotificationLogs PushNotificationLog[]
// T43 人格套用:使用者目前套用中的角色(null=未套用任何角色,以自己身分發話)。
possessingCharacterId String?
possessingCharacter Character? @relation("Possession", fields: [possessingCharacterId], references: [id], onDelete: SetNull)
relationships Relationship[]
relatedMemories EpisodicMemory[]
tasks Task[]
adultModeConsent AdultModeConsent?
romanceEventLogs RomanceEventLog[]
adultContentAttempts AdultContentAttemptLog[]
pushTokens PushToken[]
pushNotificationLogs PushNotificationLog[]
rooms Room[]
roomReadStates RoomReadState[]
callAttemptLogs CallAttemptLog[]
pendingLobbyCalls PendingLobbyCall[]
@@map("users")
}
// T43:大廳呼叫(CallService.say())辨識到的角色目前正被使用者套用時,不能讓人格系統代她判斷
// 接受/婉拒(那正是套用要避免的事),改記一筆待決,等使用者以她的身分明確決定。
model PendingLobbyCall {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
text String // 使用者原本喊的那句話,接受時用同一句話開房,維持與一般呼叫一致的行為
createdAt DateTime @default(now())
@@map("pending_lobby_calls")
}
model Work {
id String @id @default(cuid())
title String
volumeProgress String? // 卷數進度
worldview String? // 世界觀
progressAnchor String? // 作品進度錨定點(M-9/N 群組使用;描述作品實際描寫過的時期,人類可讀)
anchorStoryOrder Int @default(0) // M-9 錨定點的數值刀口:對應 Scene.storyOrder,之前是親身經歷,之後不存在
id String @id @default(cuid())
title String
volumeProgress String? // 卷數進度
worldview String? // 世界觀
progressAnchor String? // 作品進度錨定點(M-9/N 群組使用;描述作品實際描寫過的時期,人類可讀)
anchorStoryOrder Int @default(0) // M-9 錨定點的數值刀口:對應 Scene.storyOrder,之前是親身經歷,之後不存在
// N-1 後日談區間:錨定點抵達已出版內容盡頭後切入,時間繼續流動。
inEpilogue Boolean @default(false)
epilogueStartStoryOrder Int? // 切入後日談當下的錨定點位置,供 N-2 性格漂移計算「已經推演多久」
timeFlowMode String @default("REALTIME") // FAST/REALTIME/SLOW/FROZEN(N-1;後日談成年角色不可設 FAST)
// T34:推演進度時鐘的最後結算時間點(僅供 EpilogueProgressService 內部換算「現實流逝了多久」用,
// 與 updatedAt 分開,避免匯入、改標題等其他寫入動作干擾這個累積換算)。
epilogueProgressCheckpointAt DateTime?
branchedFromCanon Boolean @default(false) // N-5 分支保留:一旦分岔,之後官方新內容不再匯入這個作品實例
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characters Character[]
scenes Scene[]
@@ -169,49 +194,59 @@ model Character {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
aliases CharacterAlias[]
episodicMemories EpisodicMemory[]
semanticMemories SemanticMemory[]
proceduralRules ProceduralRule[]
emotionState EmotionState?
relationships Relationship[]
contrastTriggerLogs ContrastTriggerLog[]
scheduleSlots ScheduleSlot[]
specialDays SpecialDay[]
scheduleExceptions ScheduleExceptionLog[]
tasks Task[]
proactiveCareLogs ProactiveCareLog[]
relationshipsToOthers CharacterRelationship[] @relation("RelationshipFrom")
relationshipsFromOthers CharacterRelationship[] @relation("RelationshipTo")
ageEvidence CharacterAgeEvidence[]
romanceEventLogs RomanceEventLog[]
adultContentAttempts AdultContentAttemptLog[]
scenePresences ScenePresence[]
spokenLines SceneLine[]
costumeCatalogEntries CostumeCatalogEntry[]
poseVocabularyEntries PoseVocabularyEntry[]
languageCorpusEntries LanguageCorpusEntry[]
aliases CharacterAlias[]
episodicMemories EpisodicMemory[]
semanticMemories SemanticMemory[]
proceduralRules ProceduralRule[]
emotionState EmotionState?
sleepDebt SleepDebt?
relationships Relationship[]
contrastTriggerLogs ContrastTriggerLog[]
scheduleSlots ScheduleSlot[]
specialDays SpecialDay[]
scheduleExceptions ScheduleExceptionLog[]
tasks Task[]
proactiveCareLogs ProactiveCareLog[]
relationshipsToOthers CharacterRelationship[] @relation("RelationshipFrom")
relationshipsFromOthers CharacterRelationship[] @relation("RelationshipTo")
ageEvidence CharacterAgeEvidence[]
romanceEventLogs RomanceEventLog[]
adultContentAttempts AdultContentAttemptLog[]
scenePresences ScenePresence[]
spokenLines SceneLine[]
costumeCatalogEntries CostumeCatalogEntry[]
poseVocabularyEntries PoseVocabularyEntry[]
languageCorpusEntries LanguageCorpusEntry[]
personalityTraitSnapshots PersonalityTraitSnapshot[]
relationshipEventLogsFrom CharacterRelationshipEventLog[] @relation("EventLogFrom")
relationshipEventLogsTo CharacterRelationshipEventLog[] @relation("EventLogTo")
epilogueMilestoneLogs EpilogueMilestoneLog[]
voiceSheet VoiceSheet?
roomMemberships RoomMember[]
roomTurns RoomTurn[]
callAttemptsAsCandidate CallAttemptLog[] @relation("CallAttemptCandidate")
callAttemptsAsDelegate CallAttemptLog[] @relation("CallAttemptDelegate")
roomInvitationsReceived RoomInvitation[] @relation("RoomInvitationInvitee")
roomInvitationsSent RoomInvitation[] @relation("RoomInvitationInviter")
emotionBaselines CharacterEmotionBaseline[]
possessedByUsers User[] @relation("Possession")
pendingLobbyCallsAsTarget PendingLobbyCall[]
@@map("characters")
}
// P-1 角色音色設定:語音子系統的角色專屬設定,同 EmotionState 走 1:1 關聯。
model VoiceSheet {
id String @id @default(cuid())
characterId String @unique
id String @id @default(cuid())
characterId String @unique
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
baseTimbre String // 基礎音色描述,例如「清亮少女音」「低沉御姐音」「軟糯蘿莉音」
baseRate Float @default(1.0) // 預設語速倍率:性格決定的基準(元氣快、冷淡慢)
pitchRangeSemitones Float @default(4) // 音域幅度(半音數):語調起伏大小,元氣大、三無近乎單音
verbalTicsJson String @default("[]") // 口頭聲響庫(JSON 字串陣列):SQLite 不支援原生字串陣列欄位
forbiddenSoundsJson String @default("[]") // 禁則(JSON 字串陣列):此角色不會有的聲音表現
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
baseRate Float @default(1.0) // 預設語速倍率:性格決定的基準(元氣快、冷淡慢)
pitchRangeSemitones Float @default(4) // 音域幅度(半音數):語調起伏大小,元氣大、三無近乎單音
verbalTicsJson String @default("[]") // 口頭聲響庫(JSON 字串陣列):SQLite 不支援原生字串陣列欄位
forbiddenSoundsJson String @default("[]") // 禁則(JSON 字串陣列):此角色不會有的聲音表現
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("voice_sheets")
}
@@ -261,7 +296,7 @@ model AdultModeConsent {
// L-7 驗收:任何嘗試(無論是否放行)都留下記錄,證明繞過嘗試會被引擎層攔截並記錄,不會被悄悄放過或悄悄擋掉。
model AdultContentAttemptLog {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
@@ -276,15 +311,15 @@ model AdultContentAttemptLog {
// K-2/K-4 角色間關係:群聊中「角色間關係上場」與發言權分配的「與發言者的關係」都吃這張表。
// 有向邊(characterId 對 otherCharacterId 的觀感),故同作品兩角色互相的觀感可以不對稱。
model CharacterRelationship {
id String @id @default(cuid())
characterId String
character Character @relation("RelationshipFrom", fields: [characterId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
characterId String
character Character @relation("RelationshipFrom", fields: [characterId], references: [id], onDelete: Cascade)
otherCharacterId String
otherCharacter Character @relation("RelationshipTo", fields: [otherCharacterId], references: [id], onDelete: Cascade)
affinity Int @default(50) // 0~100,越高越要好;50 為中性
dynamic String? // 互動基調描述性標籤,例如 loyal-friend/rival/protective/crush
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
otherCharacter Character @relation("RelationshipTo", fields: [otherCharacterId], references: [id], onDelete: Cascade)
affinity Int @default(50) // 0~100,越高越要好;50 為中性
dynamic String? // 互動基調描述性標籤,例如 loyal-friend/rival/protective/crush
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([characterId, otherCharacterId])
@@map("character_relationships")
@@ -292,10 +327,10 @@ model CharacterRelationship {
// G-4 反差萌稀有度控制:記錄每次反差行為觸發的時間,供冷卻期判定。
model ContrastTriggerLog {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
triggeredAt DateTime @default(now())
triggeredAt DateTime @default(now())
@@map("contrast_trigger_logs")
}
@@ -327,11 +362,11 @@ model SpecialDay {
// I-4 破例規則:記錄每種破例最後一次觸發的時間,供稀有度冷卻判定。
model ScheduleExceptionLog {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
exceptionType String // LATE_NIGHT_CHAT/SNEAK_REPLY_IN_CLASS/ADJUST_SCHEDULE
triggeredAt DateTime @default(now())
triggeredAt DateTime @default(now())
@@map("schedule_exception_logs")
}
@@ -365,22 +400,22 @@ model EpisodicMemory {
isPrivate Boolean @default(false) // K-3 隱私邊界:一對一聊過的私密內容,群聊中不主動洩漏(口風不緊原型除外)
// M-7/M-10 可追溯性:這筆記憶是否由某個場景投影而來。只有這裡非 null 的記錄會被「重新投影」刪除重建;
// 使用者互動記憶(source=INTERACTION)永遠不會設這個欄位,因此重新投影永遠不會動到使用者互動記錄。
sourceSceneId String?
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
sourceSceneId String?
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@map("episodic_memories")
}
model SemanticMemory {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
fact String // 去情境化事實
about String // 對象(關於誰/什麼)
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
fact String // 去情境化事實
about String // 對象(關於誰/什麼)
sourceSceneId String? // M-4/M-7 可追溯性:由哪個場景萃取而來(新登場設定)
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@map("semantic_memories")
}
@@ -392,6 +427,11 @@ model ProceduralRule {
situation String // 情境
responsePattern String // 回應模式
weight Float @default(1)
// T25:最近一次新增或強化這條規則的來源場景(章節匯入觸發時才會設值;F-5 對話中即時強化/糾正
// 觸發的規則永遠不設這個欄位)。同一 (situation, responsePattern) 若先後被多個場景強化,這裡只記錄
// 最近一次,是已知的簡化——重投影時只能整條清掉重建,無法只回退「這個場景那一份增量」。
sourceSceneId String?
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -415,8 +455,23 @@ model EmotionState {
@@map("emotion_states")
}
// T32:睡眠債——SLEEP 時段內若仍在房間活動(沒有真的睡),累積該時段時長為債務;某次 SLEEP
// 時段完全沒有活動(真的補眠了)即歸零。影響三處:動態間隔懲罰(回覆變慢)、ACTION 權重提高
// (打瞌睡演出)、LEAVE 動因升級為「撐不住想睡」;另加重行程情緒基線的「累」傾向。
model SleepDebt {
id String @id @default(cuid())
characterId String @unique
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
accumulatedMinutes Float @default(0)
lastReconciledDate DateTime? // 台北當地日期(僅比對年月日),避免同一天重複計入同一段 SLEEP 時段
updatedAt DateTime @updatedAt
@@map("sleep_debts")
}
model Relationship {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
@@ -435,8 +490,8 @@ model Relationship {
lastRomanceEventAt DateTime?
awkwardUntil DateTime? // L-2 告白被拒後的尷尬期:互動變客氣,直到此時間點
consecutiveRejections Int @default(0) // L-2 連續告白失敗次數:累積到門檻會真的傷關係
partnerSince DateTime? // L-4 進入戀人階段的時間點,用於判斷戀人初期/穩定期
romanceEventsInStage Int @default(0) // L-4 進入目前階段後累積的正向心動事件數,同樣用於判斷穩定期
partnerSince DateTime? // L-4 進入戀人階段的時間點,用於判斷戀人初期/穩定期
romanceEventsInStage Int @default(0) // L-4 進入目前階段後累積的正向心動事件數,同樣用於判斷穩定期
ledgerEntries SentimentLedgerEntry[]
@@ -461,11 +516,11 @@ model SentimentLedgerEntry {
// J-1 委託:使用者請角色做的事。執行(觸發準時度)是引擎責任,演出(接案/觸發/追擊的台詞)是角色責任。
model Task {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type TaskType
content String // 委託內容(去除時間片語後的純委託描述,例如「開會」)
@@ -475,13 +530,16 @@ model Task {
cronExpression String? // 週期提醒(node-cron 表達式)
condition String? // 條件提醒的條件描述(MVP 為文字紀錄,不做真實環境條件監測)
status TaskStatus @default(PENDING)
triggeredAt DateTime? // 最近一次實際觸發的時間
status TaskStatus @default(PENDING)
triggeredAt DateTime? // 最近一次實際觸發的時間
triggerMessage String? // 最近一次觸發演出的訊息內容(供前端/測試輪詢)
overdueNudgeCount Int @default(0) // J-4 逾期追擊次數
lastNudgeAt DateTime?
sourceRoomId String? // T7:房內自走委託的來源房間,供追溯到來源房間與該則發言
sourceRoom Room? @relation(fields: [sourceRoomId], references: [id], onDelete: SetNull)
completedAt DateTime?
createdAt DateTime @default(now())
@@ -571,27 +629,30 @@ enum SceneLineStatus {
// M-3 共用場景資料庫:章節文本以場景為單位切分後的客觀記錄單位,角色中立,不含主觀詮釋。
// M-8/M-9 的共用年表以 storyOrder 排序——同作品所有場景共用一條數線,錨定點即在此數線上切一刀。
model Scene {
id String @id @default(cuid())
workId String
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
volume Int
id String @id @default(cuid())
workId String
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
volume Int
chapterLabel String
storyOrder Int // 全作品共用年表排序鍵
sourceType String @default("NOVEL") // M-8 矛盾裁決優先序用:NOVEL/OFFICIAL_SETTEI/ANIME_ADAPTATION
summary String // 場景摘要(客觀記錄:在場者、誰說了哪句、誰做了什麼)
rawText String // 匯入的原始場景文本(僅供測試/自製文本使用,不得為受版權保護原文)
projected Boolean @default(false) // M-9:是否已投影進角色記憶(只有 storyOrder <= 錨定點的場景會被投影)
createdAt DateTime @default(now())
storyOrder Int // 全作品共用年表排序鍵
sourceType String @default("NOVEL") // M-8 矛盾裁決優先序用:NOVEL/OFFICIAL_SETTEI/ANIME_ADAPTATION
summary String // 場景摘要(客觀記錄:在場者、誰說了哪句、誰做了什麼)
rawText String // 匯入的原始場景文本(僅供測試/自製文本使用,不得為受版權保護原文)
projected Boolean @default(false) // M-9:是否已投影進角色記憶(只有 storyOrder <= 錨定點的場景會被投影)
createdAt DateTime @default(now())
participants ScenePresence[]
lines SceneLine[]
illustrations Illustration[]
facts SceneFact[]
episodicMemories EpisodicMemory[]
semanticMemories SemanticMemory[]
relationshipEventLogs CharacterRelationshipEventLog[]
participants ScenePresence[]
lines SceneLine[]
illustrations Illustration[]
facts SceneFact[]
episodicMemories EpisodicMemory[]
semanticMemories SemanticMemory[]
relationshipEventLogs CharacterRelationshipEventLog[]
personalityTraitSnapshots PersonalityTraitSnapshot[]
interactions SceneInteraction[]
interactions SceneInteraction[]
endingDetectionLogs EndingDetectionLog[]
emotionBaselines CharacterEmotionBaseline[]
proceduralRules ProceduralRule[]
@@unique([workId, storyOrder])
@@map("scenes")
@@ -663,15 +724,15 @@ enum ContradictionStatus {
// M-8 矛盾偵測:不同場景描寫衝突時標記待人工確認;裁決優先序見 ContradictionDetectionService。
model SceneContradiction {
id String @id @default(cuid())
id String @id @default(cuid())
workId String
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
eventLabel String
factKey String
description String
status ContradictionStatus @default(PENDING_HUMAN_REVIEW)
status ContradictionStatus @default(PENDING_HUMAN_REVIEW)
resolution String?
createdAt DateTime @default(now())
createdAt DateTime @default(now())
@@map("scene_contradictions")
}
@@ -722,12 +783,12 @@ model CharacterRelationshipEventLog {
// N-4 人生階段推演:記錄已生成過的里程碑,供頻率節流(低頻)與避免重複生成同一件人生大事。
model EpilogueMilestoneLog {
id String @id @default(cuid())
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
storyOrder Int
label String
createdAt DateTime @default(now())
createdAt DateTime @default(now())
@@map("epilogue_milestone_logs")
}
@@ -757,3 +818,199 @@ model PushNotificationLog {
@@map("push_notification_logs")
}
// R1~R8 房間持久化地基(T2)。房間全面持久化到 DB,破壞性取代既有記憶體實作,不留相容層。
enum RoomMode {
LOBBY // 大廳:每位使用者恆有唯一一間,status 永久 ACTIVE
SOLO // 一對一
GROUP // 多人房(含既有 K-2/K-4 互動群聊)
SELF_CHAT // T3:既有 K-6 角色自聊(自走閒聊,使用者插話後改為 GROUP),沿用既有語意直接持久化
}
enum RoomStatus {
ACTIVE
CLOSED
}
enum RoomTurnKind {
TALK // 對話
ACTION // 動作演出
DELEGATE // 委託
LEAVE // 離開
SYSTEM // 系統提示
}
enum RoomInvitationStatus {
PENDING
ACCEPTED
DECLINED
}
enum EndingDetectionResolution {
PENDING
CONFIRMED
REJECTED
}
model Room {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
mode RoomMode
status RoomStatus @default(ACTIVE)
topicSeed String? // 開房緣由摘要,供自走判斷話題方向
autoAdvanceEnabled Boolean @default(true)
muted Boolean @default(false)
lastAdvancedAt DateTime?
closedAt DateTime?
closeReason String? // 例如 ALL_LEFT;沿用既有 schema 慣例(如 ScheduleExceptionLog.exceptionType)以開放字串記錄分類,不預先窮舉封閉列舉
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members RoomMember[]
turns RoomTurn[]
readStates RoomReadState[]
usageLogs RoomUsageLog[]
callAttemptLogs CallAttemptLog[]
invitations RoomInvitation[]
tasks Task[]
@@map("rooms")
}
// RoomMember:房內在場名冊。activeCharacterId 於在場(leftAt 為 null)時等於 characterId、
// 離場時設回 null——SQL 唯一索引忽略 NULL,因此「未離場時 characterId 唯一」(R5:角色不可跨房)
// 直接由資料庫唯一索引擋下,不需仰賴應用層才能保證。
model RoomMember {
id String @id @default(cuid())
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
activeCharacterId String? @unique
joinedAt DateTime @default(now())
leftAt DateTime?
leaveReason String? // 例如 ENTER_ROOM;開放字串記錄動因分類,理由同 Room.closeReason
@@map("room_members")
}
// RoomTurn:房間逐輪訊息紀錄,取代既有記憶體對話陣列;seq 為房內單調遞增序號供分頁與 SSE Last-Event-ID 使用。
model RoomTurn {
id String @id @default(cuid())
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
seq Int
kind RoomTurnKind @default(TALK)
speakerCharacterId String? // null 表示使用者本人發話
speakerCharacter Character? @relation(fields: [speakerCharacterId], references: [id], onDelete: SetNull)
content String
createdAt DateTime @default(now())
@@unique([roomId, seq])
@@map("room_turns")
}
// RoomReadState:每位使用者在每間房間的已讀游標,lastReadSeq 單調遞增、不可回退。
model RoomReadState {
id String @id @default(cuid())
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lastReadSeq Int @default(0)
updatedAt DateTime @updatedAt
@@unique([roomId, userId])
@@map("room_read_states")
}
// RoomUsageLog:每次生成後的 LLM 用量紀錄,僅供觀察,不做自動熔斷。
model RoomUsageLog {
id String @id @default(cuid())
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
model String
promptTokens Int
completionTokens Int
turnSeq Int
createdAt DateTime @default(now())
@@map("room_usage_logs")
}
// CallAttemptLog:大廳對話式呼叫(T5)對每位候選角色的判定紀錄,含代答者(T23)。
model CallAttemptLog {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
characterId String
character Character @relation("CallAttemptCandidate", fields: [characterId], references: [id], onDelete: Cascade)
accepted Boolean
reason String? // 判定依據摘要(行程/情緒/關係/已在其他房間等)
delegatedByCharacterId String?
delegatedByCharacter Character? @relation("CallAttemptDelegate", fields: [delegatedByCharacterId], references: [id], onDelete: SetNull)
resultingRoomId String?
resultingRoom Room? @relation(fields: [resultingRoomId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@map("call_attempt_logs")
}
// RoomInvitation:使用者呼叫與房內角色邀請共用同一套裁決機制;invitedByCharacterId 為 null 表示
// 使用者從大廳直接呼叫,非 null 表示由房內某角色發出邀請。
model RoomInvitation {
id String @id @default(cuid())
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
characterId String
character Character @relation("RoomInvitationInvitee", fields: [characterId], references: [id], onDelete: Cascade)
invitedByCharacterId String?
invitedByCharacter Character? @relation("RoomInvitationInviter", fields: [invitedByCharacterId], references: [id], onDelete: SetNull)
status RoomInvitationStatus @default(PENDING)
createdAt DateTime @default(now())
respondedAt DateTime?
@@map("room_invitations")
}
// EndingDetectionLog:匯入投影完成後,每章由 LLM 判定是否為結局的偵測結果,待使用者確認才會真的切日後談。
model EndingDetectionLog {
id String @id @default(cuid())
sceneId String
scene Scene @relation(fields: [sceneId], references: [id], onDelete: Cascade)
evidence String // LLM 判定理由
resolution EndingDetectionResolution @default(PENDING)
createdAt DateTime @default(now())
respondedAt DateTime?
@@map("ending_detection_logs")
}
// CharacterEmotionBaseline:章節匯入(T17)累積出的情緒基線,與行程基線(ScheduleEmotionBaselineService,
// 純即時計算不落地)在 emotion.service 取 baselineSignal 時同層合成;不得直接寫 EmotionState 六維本身。
model CharacterEmotionBaseline {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
emotionTag EmotionTag
intensity Float
sourceSceneId String?
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@map("character_emotion_baselines")
}