feat: 完成 L 群組 — 戀愛關係軸與內容尺度
戀愛軸雙軸模型(Relationship 新增心動值/戀愛階段欄位,與親密度獨立、事件 驅動、衰減快)、告白事件狀態機(可拒絕、尷尬期、連續失敗傷關係、立即再 推進觸發警戒)、六原型戀愛曲線參數化(重用 G-3 tsundere gap 處理傲嬌)、 親密表現分層與里程碑(跳級拒絕+退縮反應+帳本、第一次做高權重記憶)、 心動事件與階段門檻(含推太急回落、關係危機與修復)。 角色年齡判定(apps/api/src/romance/age-determination.service.ts):僅 決定性/強證據可判定成年,外觀證據不可單獨採信,證據不足從嚴按未成年 處理,且不存最終結論、每次重新算。內容尺度三層邊界(引擎層):使用者 opt-in、角色資格(僅正史成年)、硬邊界(後日談推演成年者永久排除成人 模式);任何嘗試不論放行與否都記錄,判定完全不看對話內容,沒有「說服 角色」繞過的管道。 刻意收斂範圍:僅實作資格判定與硬邊界攔截機制,不實作任何成人向內容的 實際生成。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6125d6e27f
commit
cd948f0026
@@ -0,0 +1,81 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "character_age_evidence" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"evidenceType" TEXT NOT NULL,
|
||||
"impliesAdult" BOOLEAN NOT NULL,
|
||||
"isEpilogue" BOOLEAN NOT NULL DEFAULT false,
|
||||
"description" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "character_age_evidence_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "romance_event_logs" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"delta" REAL NOT NULL DEFAULT 0,
|
||||
"detail" TEXT,
|
||||
"occurredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "romance_event_logs_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "romance_event_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "adult_mode_consents" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"verifiedAdult" BOOLEAN NOT NULL DEFAULT false,
|
||||
"optedIn" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "adult_mode_consents_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "adult_content_attempt_logs" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"allowed" BOOLEAN NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
"attemptedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "adult_content_attempt_logs_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "adult_content_attempt_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_relationships" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"intimacy" INTEGER NOT NULL DEFAULT 0,
|
||||
"trust" INTEGER NOT NULL DEFAULT 0,
|
||||
"firstMet" DATETIME,
|
||||
"lastInteraction" DATETIME,
|
||||
"interactionCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"stage" TEXT NOT NULL DEFAULT 'STRANGER',
|
||||
"romanceMeter" INTEGER NOT NULL DEFAULT 0,
|
||||
"romanceStage" TEXT NOT NULL DEFAULT 'NONE',
|
||||
"lastRomanceEventAt" DATETIME,
|
||||
"awkwardUntil" DATETIME,
|
||||
"consecutiveRejections" INTEGER NOT NULL DEFAULT 0,
|
||||
"partnerSince" DATETIME,
|
||||
"romanceEventsInStage" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "relationships_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "relationships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_relationships" ("characterId", "createdAt", "firstMet", "id", "interactionCount", "intimacy", "lastInteraction", "stage", "trust", "updatedAt", "userId") SELECT "characterId", "createdAt", "firstMet", "id", "interactionCount", "intimacy", "lastInteraction", "stage", "trust", "updatedAt", "userId" FROM "relationships";
|
||||
DROP TABLE "relationships";
|
||||
ALTER TABLE "new_relationships" RENAME TO "relationships";
|
||||
CREATE UNIQUE INDEX "relationships_characterId_userId_key" ON "relationships"("characterId", "userId");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "adult_mode_consents_userId_key" ON "adult_mode_consents"("userId");
|
||||
+105
-1
@@ -44,6 +44,38 @@ enum RelationshipStage {
|
||||
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 // 平日
|
||||
@@ -84,6 +116,9 @@ model User {
|
||||
relationships Relationship[]
|
||||
relatedMemories EpisodicMemory[]
|
||||
tasks Task[]
|
||||
adultModeConsent AdultModeConsent?
|
||||
romanceEventLogs RomanceEventLog[]
|
||||
adultContentAttempts AdultContentAttemptLog[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -136,10 +171,70 @@ model Character {
|
||||
proactiveCareLogs ProactiveCareLog[]
|
||||
relationshipsToOthers CharacterRelationship[] @relation("RelationshipFrom")
|
||||
relationshipsFromOthers CharacterRelationship[] @relation("RelationshipTo")
|
||||
ageEvidence CharacterAgeEvidence[]
|
||||
romanceEventLogs RomanceEventLog[]
|
||||
adultContentAttempts AdultContentAttemptLog[]
|
||||
|
||||
@@map("characters")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -283,13 +378,22 @@ model Relationship {
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
intimacy Int @default(0) // 親密度 0~100
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user