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,70 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service.js";
import { SentimentLedgerService } from "../relationship/sentiment-ledger.service.js";
import { EmotionService } from "../emotion/emotion.service.js";
import { TASK_LEDGER_EVENT, TASK_LEDGER_WEIGHT } from "./constants.js";
// J-5 任務與記憶、關係的迴路:完成/失敗誠實回報(引擎層),但角色演出可以是遺憾/得意的語氣。
@Injectable()
export class TaskFeedbackService {
constructor(
private readonly prisma: PrismaService,
private readonly sentimentLedger: SentimentLedgerService,
private readonly emotion: EmotionService,
) {}
async complete(taskId: string, now: Date = new Date()) {
const task = await this.prisma.client.task.findUnique({ where: { id: taskId } });
if (!task) {
throw new NotFoundException(`任務不存在:${taskId}`);
}
await this.prisma.client.task.update({
where: { id: taskId },
data: { status: "COMPLETED", completedAt: now },
});
await this.sentimentLedger.recordEvent(
task.characterId,
task.userId,
TASK_LEDGER_EVENT.COMPLETED,
TASK_LEDGER_WEIGHT.COMPLETED,
{ now },
);
return this.prisma.client.task.findUnique({ where: { id: taskId } });
}
async fail(taskId: string, now: Date = new Date()) {
const task = await this.prisma.client.task.findUnique({ where: { id: taskId } });
if (!task) {
throw new NotFoundException(`任務不存在:${taskId}`);
}
await this.prisma.client.task.update({ where: { id: taskId }, data: { status: "FAILED" } });
// 誠實回報失敗(引擎層行為固定),負向偏誤已由 E-2 機制放大,不需要在這裡重複加權。
await this.sentimentLedger.recordEvent(
task.characterId,
task.userId,
TASK_LEDGER_EVENT.FAILED,
TASK_LEDGER_WEIGHT.FAILED,
{ now },
);
return this.prisma.client.task.findUnique({ where: { id: taskId } });
}
// 觸發後被忽視(追擊次數已達門檻仍無回應):輕微失落,且如實記錄一筆負向事件。
async markIgnored(taskId: string, now: Date = new Date()) {
const task = await this.prisma.client.task.findUnique({ where: { id: taskId } });
if (!task) {
throw new NotFoundException(`任務不存在:${taskId}`);
}
await this.sentimentLedger.recordEvent(
task.characterId,
task.userId,
TASK_LEDGER_EVENT.IGNORED,
TASK_LEDGER_WEIGHT.IGNORED,
{ now },
);
await this.emotion.processInput(task.characterId, "", {
now,
baselineSignal: { tag: "SAD", intensity: 0.3 },
});
}
}