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>; 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 { 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; sourceRoomId?: string; // T7:房內自走委託的來源房間,供追溯到來源房間與該則發言 }, now: Date = new Date(), ): Promise { 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, sourceRoomId: input.sourceRoomId, }, }); 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" } }); } }