feat: 完成 J 群組 — 委託子系統

Task 資料表與自然語句解析(單次提醒)、依原型的接案/觸發/逾期追擊演出、
委託與關係帳本/情緒的回饋迴路(含默契旗標)、主動關照(待辦偵測+作息交叉比對,
稀有度節流不重複台詞)、排程器可靠性(服務重啟後重新掃描 PENDING 任務掛回
JobQueue,逾期立即補觸發)。J.mjs 以 spawnSync 實際重啟服務驗證 J-7。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-08-13 14:35:16 +08:00
co-authored by Claude Sonnet 5
parent d82b834e47
commit 3503cc0be6
16 changed files with 1117 additions and 8 deletions
@@ -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 } });
}
}