import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service.js"; import { ScheduleService } from "../schedule/schedule.service.js"; import { proactiveCareLine, bedtimeCareLine } from "./task-performance.js"; import { PROACTIVE_CARE_COOLDOWN_HOURS, TODO_SIGNAL_KEYWORDS, LATE_NIGHT_PATTERN_KEYWORDS } from "./constants.js"; const HOUR_MS = 60 * 60 * 1000; // J-6 主動關照:從記憶偵測使用者提過的待辦、交叉比對作息型態,主動關心——但必須稀有且不重複台詞。 @Injectable() export class ProactiveCareService { constructor( private readonly prisma: PrismaService, private readonly schedule: ScheduleService, ) {} async maybeGenerate(characterId: string, userId: string, now: Date = new Date()): Promise { const lastLog = await this.prisma.client.proactiveCareLog.findFirst({ where: { characterId, userId }, orderBy: { triggeredAt: "desc" }, }); if (lastLog && now.getTime() - lastLog.triggeredAt.getTime() < PROACTIVE_CARE_COOLDOWN_HOURS * HOUR_MS) { return null; } const character = await this.prisma.client.character.findUniqueOrThrow({ where: { id: characterId } }); const priorLogs = await this.prisma.client.proactiveCareLog.findMany({ where: { characterId, userId } }); const usedMessages = new Set(priorLogs.map((log) => log.message)); const todoMemories = await this.prisma.client.episodicMemory.findMany({ where: { characterId, relatedUserId: userId, OR: TODO_SIGNAL_KEYWORDS.map((keyword) => ({ content: { contains: keyword } })), }, orderBy: { occurredAt: "desc" }, }); for (const memory of todoMemories) { const line = proactiveCareLine(character.personalityArchetype, memory.content); if (!usedMessages.has(line)) { await this.log(characterId, userId, line, now); return line; } } // 交叉比對作息型態:使用者常提到熬夜,且目前正值睡前時段 → 主動叮嚀早點休息。 const info = await this.schedule.getInfo(characterId, now); if (info.isPreSleep) { const lateNightMemory = await this.prisma.client.episodicMemory.findFirst({ where: { characterId, relatedUserId: userId, OR: LATE_NIGHT_PATTERN_KEYWORDS.map((keyword) => ({ content: { contains: keyword } })), }, }); if (lateNightMemory) { const line = bedtimeCareLine(character.personalityArchetype); if (!usedMessages.has(line)) { await this.log(characterId, userId, line, now); return line; } } } return null; } private async log(characterId: string, userId: string, message: string, now: Date): Promise { await this.prisma.client.proactiveCareLog.create({ data: { characterId, userId, message, triggeredAt: now } }); } }