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,101 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service.js";
|
||||
import { SentimentLedgerService } from "../relationship/sentiment-ledger.service.js";
|
||||
import { TaskSchedulerService } from "./task-scheduler.service.js";
|
||||
import { parseReminderRequest } from "./task-parser.js";
|
||||
import { acceptanceLine } from "./task-performance.js";
|
||||
import { TASK_LEDGER_EVENT, TASK_LEDGER_WEIGHT, RAPPORT_THRESHOLD } from "./constants.js";
|
||||
|
||||
export interface CreateTaskResult {
|
||||
task: Awaited<ReturnType<PrismaService["client"]["task"]["create"]>>;
|
||||
performanceLine: string;
|
||||
rapport: boolean;
|
||||
}
|
||||
|
||||
// J-1/J-2/J-5 委託子系統核心:解析自然語句、實際建立任務(引擎責任),並附上接案演出(角色責任)。
|
||||
@Injectable()
|
||||
export class TaskService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly sentimentLedger: SentimentLedgerService,
|
||||
private readonly scheduler: TaskSchedulerService,
|
||||
) {}
|
||||
|
||||
// 嘗試把使用者輸入解析為單次提醒委託;無法辨識時回傳 null(呼叫端應視為一般對話)。
|
||||
async tryCreateFromText(
|
||||
characterId: string,
|
||||
userId: string,
|
||||
text: string,
|
||||
now: Date = new Date(),
|
||||
): Promise<CreateTaskResult | null> {
|
||||
const parsed = parseReminderRequest(text, now);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return this.create(
|
||||
characterId,
|
||||
userId,
|
||||
{ type: "ONE_TIME", content: parsed.content, rawText: text, triggerAt: parsed.triggerAt },
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
characterId: string,
|
||||
userId: string,
|
||||
input: { type: "ONE_TIME" | "RECURRING" | "CONDITIONAL" | "QUERY_ORGANIZE" | "TODO_TRACKING"; content: string; rawText: string; triggerAt?: Date; cronExpression?: string; condition?: string },
|
||||
now: Date = new Date(),
|
||||
): Promise<CreateTaskResult> {
|
||||
const character = await this.prisma.client.character.findUniqueOrThrow({ where: { id: characterId } });
|
||||
|
||||
// J-5 默契旗標:同角色/同使用者/同類型/同內容的委託,這是第幾次了。
|
||||
const priorCount = await this.prisma.client.task.count({
|
||||
where: { characterId, userId, type: input.type, content: input.content },
|
||||
});
|
||||
const rapport = priorCount + 1 >= RAPPORT_THRESHOLD;
|
||||
|
||||
const task = await this.prisma.client.task.create({
|
||||
data: {
|
||||
characterId,
|
||||
userId,
|
||||
type: input.type,
|
||||
content: input.content,
|
||||
rawText: input.rawText,
|
||||
triggerAt: input.triggerAt,
|
||||
cronExpression: input.cronExpression,
|
||||
condition: input.condition,
|
||||
},
|
||||
});
|
||||
|
||||
if (task.triggerAt) {
|
||||
this.scheduler.scheduleTask(task.id, task.triggerAt);
|
||||
}
|
||||
|
||||
// J-5 任務與關係的迴路:委託本身(接案)就寫入正向關係帳本事件,不必等到完成。
|
||||
await this.sentimentLedger.recordEvent(
|
||||
characterId,
|
||||
userId,
|
||||
TASK_LEDGER_EVENT.ACCEPTED,
|
||||
TASK_LEDGER_WEIGHT.ACCEPTED,
|
||||
{ now },
|
||||
);
|
||||
|
||||
const performanceLine = acceptanceLine(character.personalityArchetype, priorCount);
|
||||
return { task, performanceLine, rapport };
|
||||
}
|
||||
|
||||
async get(taskId: string) {
|
||||
return this.prisma.client.task.findUnique({ where: { id: taskId } });
|
||||
}
|
||||
|
||||
async listForUser(characterId: string, userId: string) {
|
||||
return this.prisma.client.task.findMany({
|
||||
where: { characterId, userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
async cancel(taskId: string) {
|
||||
return this.prisma.client.task.update({ where: { id: taskId }, data: { status: "CANCELLED" } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user