Task 資料表與自然語句解析(單次提醒)、依原型的接案/觸發/逾期追擊演出、 委託與關係帳本/情緒的回饋迴路(含默契旗標)、主動關照(待辦偵測+作息交叉比對, 稀有度節流不重複台詞)、排程器可靠性(服務重啟後重新掃描 PENDING 任務掛回 JobQueue,逾期立即補觸發)。J.mjs 以 spawnSync 實際重啟服務驗證 J-7。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
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<string | null> {
|
|
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<void> {
|
|
await this.prisma.client.proactiveCareLog.create({ data: { characterId, userId, message, triggeredAt: now } });
|
|
}
|
|
}
|