feat: 完成 J 群組 — 委託子系統
Task 資料表與自然語句解析(單次提醒)、依原型的接案/觸發/逾期追擊演出、 委託與關係帳本/情緒的回饋迴路(含默契旗標)、主動關照(待辦偵測+作息交叉比對, 稀有度節流不重複台詞)、排程器可靠性(服務重啟後重新掃描 PENDING 任務掛回 JobQueue,逾期立即補觸發)。J.mjs 以 spawnSync 實際重啟服務驗證 J-7。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d82b834e47
commit
3503cc0be6
@@ -0,0 +1,71 @@
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user