新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
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;
|
|
sourceRoomId?: string; // T7:房內自走委託的來源房間,供追溯到來源房間與該則發言
|
|
},
|
|
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,
|
|
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" } });
|
|
}
|
|
}
|