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:
Jeffery
2026-08-13 16:26:55 +08:00
co-authored by Claude Sonnet 5
parent 6125d6e27f
commit cd948f0026
14 changed files with 1165 additions and 9 deletions
@@ -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");