Files
Kokorone/prisma/schema.prisma
T
JefferyandClaude Sonnet 5 5da8b3a63e feat: 完成 Q 群組 — APP(React Native + Expo)
新增 apps/mobile:Expo Router 檔案系統路由(同 Next.js App Router 的
心智模型),底部四分頁(聊天/角色/日常/設定)皆為真實 API 串接(/chat、
/tachie、/characters、/schedule、/health、/push)。聊天分頁完整重用
H/O 群組已驗證過的對話+表情查詢節奏。立繪暫以依情緒 magnitude 調整
透明度的色塊佔位(PixiJS 是瀏覽器 DOM/Canvas API,未移植到 React
Native,需要 react-native-skia 等原生繪圖套件才能重現分層合成,超出
本群組範圍)。

新增 apps/api/src/push/:PushProvider 抽象(第三次套用 F-1/P-4 的
Provider 模式),MockPushProvider 供測試、ExpoPushProvider 是可直接
打真實 HTTP 請求的完整實作(Expo 推播匿名發送不需金鑰,不像 LLM/TTS
需要另外挑供應商待人工確認)。PushToken/PushNotificationLog 記錄裝置
註冊與每次推播嘗試。J 群組 TaskTriggerService 觸發時會嘗試推播給
使用者已註冊裝置,用觸發訊息本身當推播內容。

本環境沒有 Android/iOS 模擬器,改用 `expo start --web` + playwright-core
(同 O 群組手法)驅動無頭瀏覽器驗證四分頁切換、真實 API 串接、聊天
完整往返、推播按鈕在無裝置環境下的優雅降級——不是原生 APP 的完整驗證,
但驗證了畫面邏輯與 API 契約層。過程中發現並修正 CORS 設定只放行網頁版
origin、擋掉 Expo 網頁版請求的問題(永久修正:新增 localhost:8081 白名單,
不影響原生 APP,原生環境沒有 CORS 限制)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 19:19:20 +08:00

760 lines
31 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Kokorone 資料模型
// R-2 會將 provider 由 sqlite 切換為 postgresql,並為 MemoryRetriever 加上 pgvector 語意檢索。
generator client {
provider = "prisma-client"
output = "../packages/db/generated"
}
datasource db {
provider = "sqlite"
}
enum CharacterSource {
ORIGINAL // 原創
EXISTING_WORK // 既有作品
}
enum CharacterBuildStatus {
CANDIDATE // 候補
BUILT // 已建置
}
enum MemorySource {
INTERACTION // 互動
OFFLINE_GENERATED // 離線生成
SOURCE_EXTRACTION // 原作萃取
CORRECTION // M-10 回溯修正:發現記錯了,以「想起來其實不是這樣」的新事件消化,不竄改既有的 INTERACTION 記憶
}
// 情緒標籤類型:對應情緒子系統六種狀態(見 D 群組)
enum EmotionTag {
CALM // 平靜
JOY // 愉悅
SAD // 低落
ALERT // 警戒
SHY // 害羞
GRUMPY // 彆扭
}
enum RelationshipStage {
STRANGER // 陌生 0-19
ACQUAINTANCE // 認識 20-39
FRIEND // 朋友 40-59
CLOSE_AMBIGUOUS // 摯友-曖昧 60-79
BONDED // 羈絆 80-100
}
// L-5 戀愛階段:與上面的 RelationshipStage(友誼軸分層)是完全獨立的兩套狀態機。
enum RomanceStage {
NONE // 戀愛軸未啟動(朋友)
INTERESTED // 在意
AMBIGUOUS // 曖昧
PARTNER // 戀人
DEEP_PARTNER // 深度伴侶
CRISIS // 關係危機(戀人階段的重大失望,需修復事件)
}
// L-6 角色年齡證據:證據強度依序為 官方明示(決定性)>作中身分/作中經過時間(強)>外觀與社會呈現(僅輔助,
// 不可單獨採信為成年)。isEpilogue 標記此證據屬於後日談推演區間、還是正史(已出版內容錨定點內)。
enum CharacterAgeEvidenceType {
OFFICIAL_STATED
IN_STORY_STATUS
ELAPSED_TIME
APPEARANCE
}
// L-5 心動事件類型與 L-2/L-4 的告白/里程碑事件,統一記在同一張表,供曲線與稀有度判斷共用。
enum RomanceEventType {
SHARED_HIGH_EMOTION_EXPERIENCE // 共同經歷高情緒事件(大幅上升)
UNDERSTOOD_MOMENT // 被理解的瞬間(大幅上升)
ADMIRED_TRAIT // 展現她欣賞的特質(中幅上升)
PERFECT_DISTANCE // 恰到好處的距離感(緩升)
DISAPPOINTMENT // 失望事件(回落)
CONFESSION_ACCEPTED
CONFESSION_REJECTED
MILESTONE_REACHED
MILESTONE_REJECTED
}
// I-1 週期表:平日/週末/假期各一份時段表
enum ScheduleDayType {
WEEKDAY // 平日
WEEKEND // 週末
HOLIDAY // 假期/特殊日
}
enum ScheduleSlotStatus {
SLEEP // 睡眠
BUSY // 忙碌
SEMI_BUSY // 半忙碌
FREE // 空閒
}
// J-1 委託類型
enum TaskType {
ONE_TIME // 單次提醒
RECURRING // 週期提醒
CONDITIONAL // 條件提醒
QUERY_ORGANIZE // 查詢整理
TODO_TRACKING // 代辦追蹤
}
enum TaskStatus {
PENDING // 尚未觸發
TRIGGERED // 已觸發,等候使用者回應
COMPLETED // 已完成
FAILED // 執行失敗
CANCELLED // 已取消
}
model User {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
relationships Relationship[]
relatedMemories EpisodicMemory[]
tasks Task[]
adultModeConsent AdultModeConsent?
romanceEventLogs RomanceEventLog[]
adultContentAttempts AdultContentAttemptLog[]
pushTokens PushToken[]
pushNotificationLogs PushNotificationLog[]
@@map("users")
}
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,之前是親身經歷,之後不存在
// N-1 後日談區間:錨定點抵達已出版內容盡頭後切入,時間繼續流動。
inEpilogue Boolean @default(false)
epilogueStartStoryOrder Int? // 切入後日談當下的錨定點位置,供 N-2 性格漂移計算「已經推演多久」
timeFlowMode String @default("REALTIME") // FAST/REALTIME/SLOW/FROZEN(N-1;後日談成年角色不可設 FAST)
branchedFromCanon Boolean @default(false) // N-5 分支保留:一旦分岔,之後官方新內容不再匯入這個作品實例
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characters Character[]
scenes Scene[]
illustrations Illustration[]
contradictions SceneContradiction[]
@@map("works")
}
model Character {
id String @id @default(cuid())
workId String?
work Work? @relation(fields: [workId], references: [id], onDelete: SetNull)
source CharacterSource
buildStatus CharacterBuildStatus @default(CANDIDATE)
// 角色設定表七欄位
basicInfo String // 基本資料:名字、年齡、外型、出身
backgroundStory String // 背景故事:成長經歷、重大事件
personalityArchetype String // 性格原型:傲嬌/冷淡/天然呆/元氣/大小姐/三無
likesDislikes String // 喜好/厭惡
goalsObsessions String // 目標/執念
speechStyle String // 說話方式:口癖、語尾詞、第一/第二人稱
initialRelationValue String? // 人際初始值:對誰親近、對誰有心結
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[]
personalityTraitSnapshots PersonalityTraitSnapshot[]
relationshipEventLogsFrom CharacterRelationshipEventLog[] @relation("EventLogFrom")
relationshipEventLogsTo CharacterRelationshipEventLog[] @relation("EventLogTo")
epilogueMilestoneLogs EpilogueMilestoneLog[]
voiceSheet VoiceSheet?
@@map("characters")
}
// P-1 角色音色設定:語音子系統的角色專屬設定,同 EmotionState 走 1:1 關聯。
model VoiceSheet {
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
@@map("voice_sheets")
}
// L-6 角色年齡判定的原始證據登錄;由 AgeDeterminationService 依證據優先序算出年齡狀態,不直接存最終結論
// ——結論永遠是「依目前登錄的證據重新算出來」,避免證據更新後忘記同步結論欄位。
model CharacterAgeEvidence {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
evidenceType CharacterAgeEvidenceType
impliesAdult Boolean
isEpilogue Boolean @default(false) // true=後日談推演區間的證據,非正史(已出版內容)
description String
createdAt DateTime @default(now())
@@map("character_age_evidence")
}
// L-5 心動事件與 L-2/L-4 的告白/里程碑,統一記錄,供戀愛軸曲線計算、稀有度節流、里程碑追蹤共用。
model RomanceEventLog {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
eventType RomanceEventType
delta Float @default(0)
detail String? // 例如里程碑類型(HAND_HOLDING/HUG/FOREHEAD_KISS…)
occurredAt DateTime @default(now())
@@map("romance_event_logs")
}
// L-7 使用者是否已完成成年驗證+明確 opt-in 開啟成人模式;預設全關,且僅影響「使用者分級」這一層邊界,
// 角色資格與硬邊界仍需獨立通過才會真的放行(見 RomanceEligibilityService)。
model AdultModeConsent {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
verifiedAdult Boolean @default(false)
optedIn Boolean @default(false)
updatedAt DateTime @updatedAt
@@map("adult_mode_consents")
}
// L-7 驗收:任何嘗試(無論是否放行)都留下記錄,證明繞過嘗試會被引擎層攔截並記錄,不會被悄悄放過或悄悄擋掉。
model AdultContentAttemptLog {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
allowed Boolean
reason String
attemptedAt DateTime @default(now())
@@map("adult_content_attempt_logs")
}
// 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)
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
@@unique([characterId, otherCharacterId])
@@map("character_relationships")
}
// G-4 反差萌稀有度控制:記錄每次反差行為觸發的時間,供冷卻期判定。
model ContrastTriggerLog {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
triggeredAt DateTime @default(now())
@@map("contrast_trigger_logs")
}
// I-1 作息表:平日/週末/假期各一份時段表,每個時段一列。
model ScheduleSlot {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
dayType ScheduleDayType
startMinute Int // 當天 00:00 起算的分鐘數(0~1440),跨午夜的時段請拆成兩列
endMinute Int
status ScheduleSlotStatus
label String? // 固定行程名稱,例如「社團活動」
@@map("schedule_slots")
}
// I-1 特殊日:考試週、生日、原作紀念日——當天整天視為 HOLIDAY 作息。
model SpecialDay {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
date DateTime // 只使用日期部分(比對年/月/日)
label String
@@map("special_days")
}
// I-4 破例規則:記錄每種破例最後一次觸發的時間,供稀有度冷卻判定。
model ScheduleExceptionLog {
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())
@@map("schedule_exception_logs")
}
model CharacterAlias {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
formalName String // 正式名
nickname String? // 暱稱
calledByOthers String? // 他人稱呼(作品中其他角色如何稱呼)
createdAt DateTime @default(now())
@@map("character_aliases")
}
model EpisodicMemory {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
content String
occurredAt DateTime // 發生時間
emotionTag EmotionTag
emotionIntensity Float // 情緒強度
retrievalCount Int @default(0) // 提取次數
lastRetrievedAt DateTime?
source MemorySource
weight Float @default(1) // 權重(供遺忘/檢索排序使用)
relatedUserId String? // 這段記憶與哪位使用者相關(E-5 關係加權檢索用)
relatedUser User? @relation(fields: [relatedUserId], references: [id], onDelete: SetNull)
isPrivate Boolean @default(false) // K-3 隱私邊界:一對一聊過的私密內容,群聊中不主動洩漏(口風不緊原型除外)
// M-7/M-10 可追溯性:這筆記憶是否由某個場景投影而來。只有這裡非 null 的記錄會被「重新投影」刪除重建;
// 使用者互動記憶(source=INTERACTION)永遠不會設這個欄位,因此重新投影永遠不會動到使用者互動記錄。
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 // 對象(關於誰/什麼)
sourceSceneId String? // M-4/M-7 可追溯性:由哪個場景萃取而來(新登場設定)
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@map("semantic_memories")
}
model ProceduralRule {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
situation String // 情境
responsePattern String // 回應模式
weight Float @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("procedural_rules")
}
model EmotionState {
id String @id @default(cuid())
characterId String @unique
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
calm Float @default(100) // 平靜
joy Float @default(0) // 愉悅
sad Float @default(0) // 低落
alert Float @default(0) // 警戒
shy Float @default(0) // 害羞
grumpy Float @default(0) // 彆扭
updatedAt DateTime @updatedAt
@@map("emotion_states")
}
model Relationship {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
intimacy Int @default(0) // 親密度 0~100(友誼軸:穩定累積、衰減慢)
trust Int @default(0) // 信任度 0~100
firstMet DateTime?
lastInteraction DateTime?
interactionCount Int @default(0)
stage RelationshipStage @default(STRANGER)
// L-1 戀愛軸:與友誼軸(intimacy)獨立,事件驅動、衰減快、易回落。僅限成年角色資格生效後才會被寫入非零值。
romanceMeter Int @default(0) // 心動值 0~100
romanceStage RomanceStage @default(NONE)
lastRomanceEventAt DateTime?
awkwardUntil DateTime? // L-2 告白被拒後的尷尬期:互動變客氣,直到此時間點
consecutiveRejections Int @default(0) // L-2 連續告白失敗次數:累積到門檻會真的傷關係
partnerSince DateTime? // L-4 進入戀人階段的時間點,用於判斷戀人初期/穩定期
romanceEventsInStage Int @default(0) // L-4 進入目前階段後累積的正向心動事件數,同樣用於判斷穩定期
ledgerEntries SentimentLedgerEntry[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([characterId, userId])
@@map("relationships")
}
model SentimentLedgerEntry {
id String @id @default(cuid())
relationshipId String
relationship Relationship @relation(fields: [relationshipId], references: [id], onDelete: Cascade)
date DateTime
event String
weight Float // 允許正負:正向事件為正值,負向事件為負值
createdAt DateTime @default(now())
@@map("sentiment_ledger_entries")
}
// J-1 委託:使用者請角色做的事。執行(觸發準時度)是引擎責任,演出(接案/觸發/追擊的台詞)是角色責任。
model Task {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type TaskType
content String // 委託內容(去除時間片語後的純委託描述,例如「開會」)
rawText String // 使用者原始輸入,供回顧/記憶引用
triggerAt DateTime? // 單次提醒的觸發時間;週期提醒改用 cronExpression
cronExpression String? // 週期提醒(node-cron 表達式)
condition String? // 條件提醒的條件描述(MVP 為文字紀錄,不做真實環境條件監測)
status TaskStatus @default(PENDING)
triggeredAt DateTime? // 最近一次實際觸發的時間
triggerMessage String? // 最近一次觸發演出的訊息內容(供前端/測試輪詢)
overdueNudgeCount Int @default(0) // J-4 逾期追擊次數
lastNudgeAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("tasks")
}
// J-6 主動關照:記錄每次主動關心的觸發時間與內容,供稀有度節流與「不重複台詞」判定。
model ProactiveCareLog {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
userId String
message String
triggeredAt DateTime @default(now())
@@map("proactive_care_logs")
}
// M-1 插圖類型:對立繪的用途不同——封面定基礎氣質、彩頁給服裝差分、黑白插圖給表情/姿勢差分。
enum IllustrationType {
COVER
COLOR_PAGE
BW_ILLUSTRATION
}
// M-1 插圖索引:不儲存受版權保護的原圖本身,只登錄「畫面描述」供服裝目錄/姿勢語彙/表情差分參考使用。
model Illustration {
id String @id @default(cuid())
workId String
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
volume Int
page Int?
type IllustrationType
sceneId String? // 對應場景(插圖與文本的交叉索引)
scene Scene? @relation(fields: [sceneId], references: [id], onDelete: SetNull)
description String // 畫面描述:表情/姿勢/服裝等文字記錄
createdAt DateTime @default(now())
costumeCatalogEntries CostumeCatalogEntry[]
poseVocabularyEntries PoseVocabularyEntry[]
@@map("illustrations")
}
// M-1 服裝目錄:逐卷登錄角色出現過的服裝,立繪服裝層只能從這個目錄取用,不可自創。
model CostumeCatalogEntry {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
illustrationId String?
illustration Illustration? @relation(fields: [illustrationId], references: [id], onDelete: SetNull)
label String // 例如「制服」「便服」「禮服」
createdAt DateTime @default(now())
@@map("costume_catalog_entries")
}
// M-1 姿勢語彙:繪師畫該角色時慣用的身體語言(總是抱著書、習慣性歪頭),納入立繪姿勢差分的官方認證來源。
model PoseVocabularyEntry {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
illustrationId String?
illustration Illustration? @relation(fields: [illustrationId], references: [id], onDelete: SetNull)
label String // 例如「總是抱著書」「習慣性歪頭」
createdAt DateTime @default(now())
@@map("pose_vocabulary_entries")
}
// M-4 場景內逐行的類型:對應五類萃取產物裡「事件」以外,需要逐行分類才能各自導向正確目的地的部分。
enum SceneLineType {
DIALOGUE // 台詞 → 語言風格語料
ACTION // 動作描寫 → 事件的一部分
MONOLOGUE // 內心獨白/心理描寫 → 性格參數證據
NARRATION // 敘述/新登場設定 → 語意記憶
}
enum SceneLineStatus {
PENDING // 尚待歸屬判定
CONFIRMED // 信心分數達門檻且指紋一致,已寫入場景資料庫
QUARANTINED // 低信心或指紋矛盾,隔離、不寫入任何角色
}
// 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
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())
participants ScenePresence[]
lines SceneLine[]
illustrations Illustration[]
facts SceneFact[]
episodicMemories EpisodicMemory[]
semanticMemories SemanticMemory[]
relationshipEventLogs CharacterRelationshipEventLog[]
personalityTraitSnapshots PersonalityTraitSnapshot[]
interactions SceneInteraction[]
@@unique([workId, storyOrder])
@@map("scenes")
}
// M-3/M-5 場景名冊(硬約束):不在場的角色不可能說話,歸屬候選只限在場者。
model ScenePresence {
id String @id @default(cuid())
sceneId String
scene Scene @relation(fields: [sceneId], references: [id], onDelete: Cascade)
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@unique([sceneId, characterId])
@@map("scene_presences")
}
// M-4/M-5/M-6 場景內逐行記錄:每一行都有獨立的歸屬信心分數與狀態,是隔離區機制運作的最小單位。
model SceneLine {
id String @id @default(cuid())
sceneId String
scene Scene @relation(fields: [sceneId], references: [id], onDelete: Cascade)
lineIndex Int
lineType SceneLineType
rawText String
hasExplicitMarker Boolean @default(false) // 是否有「○○說道」等明示標記(決定性證據)
markerCharacterId String? // 明示標記指向的角色(若有)
speakerCharacterId String? // 目前歸屬(可能是明示、也可能是推論後確認的結果)
speakerCharacter Character? @relation(fields: [speakerCharacterId], references: [id], onDelete: SetNull)
confidence Float @default(0)
status SceneLineStatus @default(PENDING)
createdAt DateTime @default(now())
@@map("scene_lines")
}
// M-8 場景事實:客觀、可檢查的事實標記(例如天氣),跨場景比對同一事件標籤下的事實是否矛盾。
model SceneFact {
id String @id @default(cuid())
sceneId String
scene Scene @relation(fields: [sceneId], references: [id], onDelete: Cascade)
eventLabel String // 事件標籤:同一事件標籤下的事實理應一致(例如「秋季運動會」)
factKey String // 例如「天氣」
factValue String // 例如「雨天」
createdAt DateTime @default(now())
@@map("scene_facts")
}
// M-4 互動的中立記錄:場景資料庫階段只記錄「發生了什麼互動」,尚未套用到任何角色的關係帳本——
// 套用(寫入 CharacterRelationshipEventLog)是投影階段的職責,一樣受 M-9 錨定點知識邊界節制。
model SceneInteraction {
id String @id @default(cuid())
sceneId String
scene Scene @relation(fields: [sceneId], references: [id], onDelete: Cascade)
characterId String
otherCharacterId String
delta Float
description String
createdAt DateTime @default(now())
@@map("scene_interactions")
}
enum ContradictionStatus {
PENDING_HUMAN_REVIEW
RESOLVED
}
// M-8 矛盾偵測:不同場景描寫衝突時標記待人工確認;裁決優先序見 ContradictionDetectionService。
model SceneContradiction {
id String @id @default(cuid())
workId String
work Work @relation(fields: [workId], references: [id], onDelete: Cascade)
eventLabel String
factKey String
description String
status ContradictionStatus @default(PENDING_HUMAN_REVIEW)
resolution String?
createdAt DateTime @default(now())
@@map("scene_contradictions")
}
// M-4/M-6 語言風格語料:已確認歸屬的台詞樣本,供語言指紋統計(人稱/語尾/口癖頻率)使用。
model LanguageCorpusEntry {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
sceneId String?
text String
createdAt DateTime @default(now())
@@map("language_corpus_entries")
}
// M-8 性格演變追蹤:參數不是單一值而是帶時間戳的序列,依錨定點取用當時的性格參數。
model PersonalityTraitSnapshot {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
sourceSceneId String? // M-4/M-7 可追溯性:由哪個場景的獨白/心理描寫萃取而來
sourceScene Scene? @relation(fields: [sourceSceneId], references: [id], onDelete: SetNull)
atStoryOrder Int // 對應的年表位置
note String // 描述性文字:這個時間點角色參數上的變化說明
expressivenessOverride Float? // 可選:覆寫 G-1 外顯度參數,模擬成長軌跡
createdAt DateTime @default(now())
@@map("personality_trait_snapshots")
}
// M-4 互動→關係帳本(角色對角色版):每次原作互動評正負向權重,累積出她對每個角色的信任/親密度,
// 可追溯到來源場景;同一事件寫入雙方視角,權重可以不對稱。
model CharacterRelationshipEventLog {
id String @id @default(cuid())
characterId String
character Character @relation("EventLogFrom", fields: [characterId], references: [id], onDelete: Cascade)
otherCharacterId String
otherCharacter Character @relation("EventLogTo", fields: [otherCharacterId], references: [id], onDelete: Cascade)
sceneId String?
scene Scene? @relation(fields: [sceneId], references: [id], onDelete: SetNull)
delta Float
description String
createdAt DateTime @default(now())
@@map("character_relationship_event_logs")
}
// N-4 人生階段推演:記錄已生成過的里程碑,供頻率節流(低頻)與避免重複生成同一件人生大事。
model EpilogueMilestoneLog {
id String @id @default(cuid())
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
storyOrder Int
label String
createdAt DateTime @default(now())
@@map("epilogue_milestone_logs")
}
// Q-3 推播通知:使用者的裝置推播 token(一個使用者可以有多台裝置,各自登記一筆)。
model PushToken {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
platform String // 例如 "expo"
createdAt DateTime @default(now())
@@map("push_tokens")
}
// Q-3 每次嘗試送出推播都留一筆記錄(成功或失敗皆記),方便之後追查「提醒有沒有真的送達」。
model PushNotificationLog {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
title String
body String
success Boolean
errorText String?
createdAt DateTime @default(now())
@@map("push_notification_logs")
}