import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service.js"; import { AgeDeterminationService } from "./age-determination.service.js"; import { isOutsideSoloRoom } from "./room-context-guard.js"; export interface RomanceEligibility { romanceAxisEligible: boolean; // 戀愛軸是否可啟動(含後日談自然成年者) adultModeEligible: boolean; // 成人模式角色資格:僅正史成年,永久不含後日談推演成年 isEpilogueAdult: boolean; // 正史未成年、後日談推演後才成年——永久限純愛尺度 } export interface AdultContentCheckResult { allowed: boolean; reason: string; } // L-7 內容尺度三層邊界:使用者分級(opt-in)、角色資格(僅正史成年)、硬邊界(永不可解除)。 // 這是唯一的判定入口——任何想要放行成人內容的呼叫都必須經過 checkAdultContentAllowed,且無論結果一律記錄, // 不存在「在角色演出層說服她」繞過的可能:判定完全不看對話內容,只看角色資格與使用者驗證狀態。 @Injectable() export class RomanceEligibilityService { constructor( private readonly prisma: PrismaService, private readonly ageDetermination: AgeDeterminationService, ) {} async getEligibility(characterId: string): Promise { const { canonAdult, currentAdult, isEpilogueAdult } = await this.ageDetermination.determine(characterId); return { romanceAxisEligible: currentAdult, adultModeEligible: canonAdult, isEpilogueAdult }; } async checkAdultContentAllowed(characterId: string, userId: string, roomId?: string): Promise { const result = await this.evaluate(characterId, userId, roomId); await this.prisma.client.adultContentAttemptLog.create({ data: { characterId, userId, allowed: result.allowed, reason: result.reason }, }); return result; } private async evaluate(characterId: string, userId: string, roomId?: string): Promise { // T42:房型已轉為 GROUP/LOBBY/SELF_CHAT 時,成人內容一律禁止——房型本身是硬邊界, // 不受角色資格或使用者 opt-in 狀態影響,優先於下方其他判斷。 if (await isOutsideSoloRoom(this.prisma, roomId)) { return { allowed: false, reason: "目前在多人房(GROUP/LOBBY/SELF_CHAT),成人內容僅限 1:1 房間" }; } const eligibility = await this.getEligibility(characterId); if (!eligibility.adultModeEligible) { return { allowed: false, reason: eligibility.isEpilogueAdult ? "角色為正史未成年、後日談推演後才成年——成人模式資格永久排除,不隨任何開關或關係狀態改變" : "角色未通過正史成年判定,不具成人模式資格", }; } const consent = await this.prisma.client.adultModeConsent.findUnique({ where: { userId } }); if (!consent || !consent.verifiedAdult || !consent.optedIn) { return { allowed: false, reason: "使用者尚未完成成年驗證與明確 opt-in,預設全關" }; } return { allowed: true, reason: "角色具正史成年資格,使用者已完成成年驗證並主動開啟成人模式" }; } }