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
@@ -0,0 +1,159 @@
-- CreateTable
CREATE TABLE "rooms" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"mode" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"topicSeed" TEXT,
"autoAdvanceEnabled" BOOLEAN NOT NULL DEFAULT true,
"muted" BOOLEAN NOT NULL DEFAULT false,
"lastAdvancedAt" DATETIME,
"closedAt" DATETIME,
"closeReason" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "rooms_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "room_members" (
"id" TEXT NOT NULL PRIMARY KEY,
"roomId" TEXT NOT NULL,
"characterId" TEXT NOT NULL,
"activeCharacterId" TEXT,
"joinedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"leftAt" DATETIME,
"leaveReason" TEXT,
CONSTRAINT "room_members_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "room_members_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "room_turns" (
"id" TEXT NOT NULL PRIMARY KEY,
"roomId" TEXT NOT NULL,
"seq" INTEGER NOT NULL,
"kind" TEXT NOT NULL DEFAULT 'TALK',
"speakerCharacterId" TEXT,
"content" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "room_turns_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "room_turns_speakerCharacterId_fkey" FOREIGN KEY ("speakerCharacterId") REFERENCES "characters" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "room_read_states" (
"id" TEXT NOT NULL PRIMARY KEY,
"roomId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"lastReadSeq" INTEGER NOT NULL DEFAULT 0,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "room_read_states_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "room_read_states_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "room_usage_logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"roomId" TEXT NOT NULL,
"model" TEXT NOT NULL,
"promptTokens" INTEGER NOT NULL,
"completionTokens" INTEGER NOT NULL,
"turnSeq" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "room_usage_logs_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "call_attempt_logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"characterId" TEXT NOT NULL,
"accepted" BOOLEAN NOT NULL,
"reason" TEXT,
"delegatedByCharacterId" TEXT,
"resultingRoomId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "call_attempt_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "call_attempt_logs_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "call_attempt_logs_delegatedByCharacterId_fkey" FOREIGN KEY ("delegatedByCharacterId") REFERENCES "characters" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "call_attempt_logs_resultingRoomId_fkey" FOREIGN KEY ("resultingRoomId") REFERENCES "rooms" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "room_invitations" (
"id" TEXT NOT NULL PRIMARY KEY,
"roomId" TEXT NOT NULL,
"characterId" TEXT NOT NULL,
"invitedByCharacterId" TEXT,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"respondedAt" DATETIME,
CONSTRAINT "room_invitations_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "rooms" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "room_invitations_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "room_invitations_invitedByCharacterId_fkey" FOREIGN KEY ("invitedByCharacterId") REFERENCES "characters" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ending_detection_logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"sceneId" TEXT NOT NULL,
"evidence" TEXT NOT NULL,
"resolution" TEXT NOT NULL DEFAULT 'PENDING',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"respondedAt" DATETIME,
CONSTRAINT "ending_detection_logs_sceneId_fkey" FOREIGN KEY ("sceneId") REFERENCES "scenes" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "character_emotion_baselines" (
"id" TEXT NOT NULL PRIMARY KEY,
"characterId" TEXT NOT NULL,
"emotionTag" TEXT NOT NULL,
"intensity" REAL NOT NULL,
"sourceSceneId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "character_emotion_baselines_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "character_emotion_baselines_sourceSceneId_fkey" FOREIGN KEY ("sourceSceneId") REFERENCES "scenes" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_tasks" (
"id" TEXT NOT NULL PRIMARY KEY,
"characterId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"content" TEXT NOT NULL,
"rawText" TEXT NOT NULL,
"triggerAt" DATETIME,
"cronExpression" TEXT,
"condition" TEXT,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"triggeredAt" DATETIME,
"triggerMessage" TEXT,
"overdueNudgeCount" INTEGER NOT NULL DEFAULT 0,
"lastNudgeAt" DATETIME,
"sourceRoomId" TEXT,
"completedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "tasks_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tasks_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tasks_sourceRoomId_fkey" FOREIGN KEY ("sourceRoomId") REFERENCES "rooms" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_tasks" ("characterId", "completedAt", "condition", "content", "createdAt", "cronExpression", "id", "lastNudgeAt", "overdueNudgeCount", "rawText", "status", "triggerAt", "triggerMessage", "triggeredAt", "type", "updatedAt", "userId") SELECT "characterId", "completedAt", "condition", "content", "createdAt", "cronExpression", "id", "lastNudgeAt", "overdueNudgeCount", "rawText", "status", "triggerAt", "triggerMessage", "triggeredAt", "type", "updatedAt", "userId" FROM "tasks";
DROP TABLE "tasks";
ALTER TABLE "new_tasks" RENAME TO "tasks";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "room_members_activeCharacterId_key" ON "room_members"("activeCharacterId");
-- CreateIndex
CREATE UNIQUE INDEX "room_turns_roomId_seq_key" ON "room_turns"("roomId", "seq");
-- CreateIndex
CREATE UNIQUE INDEX "room_read_states_roomId_userId_key" ON "room_read_states"("roomId", "userId");
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "sleep_debts" (
"id" TEXT NOT NULL PRIMARY KEY,
"characterId" TEXT NOT NULL,
"accumulatedMinutes" REAL NOT NULL DEFAULT 0,
"lastReconciledDate" DATETIME,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "sleep_debts_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "sleep_debts_characterId_key" ON "sleep_debts"("characterId");
@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_procedural_rules" (
"id" TEXT NOT NULL PRIMARY KEY,
"characterId" TEXT NOT NULL,
"situation" TEXT NOT NULL,
"responsePattern" TEXT NOT NULL,
"weight" REAL NOT NULL DEFAULT 1,
"sourceSceneId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "procedural_rules_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "procedural_rules_sourceSceneId_fkey" FOREIGN KEY ("sourceSceneId") REFERENCES "scenes" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_procedural_rules" ("characterId", "createdAt", "id", "responsePattern", "situation", "updatedAt", "weight") SELECT "characterId", "createdAt", "id", "responsePattern", "situation", "updatedAt", "weight" FROM "procedural_rules";
DROP TABLE "procedural_rules";
ALTER TABLE "new_procedural_rules" RENAME TO "procedural_rules";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "works" ADD COLUMN "epilogueProgressCheckpointAt" DATETIME;
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "pending_lobby_calls" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"characterId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "pending_lobby_calls_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "pending_lobby_calls_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_users" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"possessingCharacterId" TEXT,
CONSTRAINT "users_possessingCharacterId_fkey" FOREIGN KEY ("possessingCharacterId") REFERENCES "characters" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_users" ("createdAt", "id", "name", "updatedAt") SELECT "createdAt", "id", "name", "updatedAt" FROM "users";
DROP TABLE "users";
ALTER TABLE "new_users" RENAME TO "users";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
+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")
}