import { Inject, Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service.js"; import { WorkingMemoryService } from "../memory/working-memory.service.js"; import { EmotionService, dominantState } from "../emotion/emotion.service.js"; import { RelationshipService } from "../relationship/relationship.service.js"; import { ScheduleService } from "../schedule/schedule.service.js"; import { ScheduleExceptionService } from "../schedule/schedule-exception.service.js"; import { ScheduleEmotionBaselineService } from "../schedule/schedule-emotion-baseline.service.js"; import { OfflineEventService } from "../schedule/offline-event.service.js"; import { availabilityForStatus, type AvailabilityMode, type ExceptionType } from "../schedule/constants.js"; import { LLM_PROVIDER, type LLMProvider } from "./llm-provider.js"; import { ContextAssemblerService } from "./context-assembler.service.js"; import { OutputFilterService } from "./output-filter.service.js"; import { FastChannelDetector, DEFAULT_FAST_REPLIES } from "./fast-channel.service.js"; import { extractActions } from "./action-markup.js"; import { getForbiddenWords, applySpeechQuirks } from "../personality/language-style.js"; import type { GenerationContext } from "./types.js"; export interface HandleMessageOptions { seed?: number; now?: Date; } export interface HandleMessageResult { text: string; actions: string[]; isFastChannel: boolean; availability: AvailabilityMode; exceptionType: ExceptionType | null; context: GenerationContext | null; } function fastChannelSituation(reason: string): string { return `fast-channel:${reason}`; } const DELAYED_ACKNOWLEDGEMENT = "抱歉剛剛在忙,現在才看到——"; // F 群組整合入口:串起雙速通道判斷、上下文組裝、Provider 生成、輸出過濾與工作記憶寫入。 // I 群組擴充:作息可用度/破例規則/作息情緒基線/離線事件生成(見 todo.md 實作記錄)。 @Injectable() export class DialogueService { constructor( @Inject(LLM_PROVIDER) private readonly llmProvider: LLMProvider, private readonly fastChannel: FastChannelDetector, private readonly contextAssembler: ContextAssemblerService, private readonly outputFilter: OutputFilterService, private readonly workingMemory: WorkingMemoryService, private readonly emotion: EmotionService, private readonly relationship: RelationshipService, private readonly schedule: ScheduleService, private readonly scheduleException: ScheduleExceptionService, private readonly scheduleBaseline: ScheduleEmotionBaselineService, private readonly offlineEvent: OfflineEventService, private readonly prisma: PrismaService, ) {} async handleMessage( characterId: string, userId: string, sessionId: string, userInput: string, options: HandleMessageOptions = {}, ): Promise { const now = options.now ?? new Date(); await this.workingMemory.append(sessionId, { role: "user", content: userInput, timestamp: now }); // I-6:按需生成離線事件(今天只生成一次),成為話題來源。 await this.offlineEvent.maybeGenerate(characterId, now); // I-5:作息驅動的情緒基線,只在本輪沒有明確情緒訊號時才介入。 const baselineSignal = await this.scheduleBaseline.computeBaseline(characterId, now); const emotionResult = await this.emotion.processInput(characterId, userInput, { now, baselineSignal: baselineSignal ?? undefined, }); await this.relationship.recordInteraction(characterId, userId, now); // I-2:目前時段狀態 → 回應可用度;I-4:條件符合且未在冷卻期時才破例。 const scheduleInfo = await this.schedule.getInfo(characterId, now); const availability = availabilityForStatus(scheduleInfo.slot?.status ?? null); let exceptionType: ExceptionType | null = null; let effectiveAvailability = availability; if (availability !== "NORMAL") { const { relationship } = await this.relationship.getState(characterId, userId, now); exceptionType = await this.scheduleException.tryApply( characterId, userId, availability, { intimacy: relationship.intimacy, dominantEmotion: dominantState(emotionResult.state), userInput }, now, ); if (exceptionType) { effectiveAvailability = "NORMAL"; } } if (effectiveAvailability === "NO_REPLY") { return { text: "", actions: [], isFastChannel: false, availability: effectiveAvailability, exceptionType, context: null }; } const { isFastChannel, reason } = this.fastChannel.detect(userInput); const skipMemoryRetrieval = isFastChannel || effectiveAvailability === "BRIEF"; const context = await this.contextAssembler.assemble(characterId, userId, sessionId, userInput, { skipMemoryRetrieval, seed: options.seed, now, }); let text: string; let actions: string[]; if (effectiveAvailability === "DELAYED" && !exceptionType) { text = DELAYED_ACKNOWLEDGEMENT; actions = []; } else if (isFastChannel && reason) { const rule = await this.prisma.client.proceduralRule.findFirst({ where: { characterId, situation: fastChannelSituation(reason) }, orderBy: { weight: "desc" }, }); text = rule?.responsePattern ?? DEFAULT_FAST_REPLIES[reason]; actions = extractActions(text); } else { const generated = await this.llmProvider.generate(context); text = generated.text; actions = generated.actions; } // I-2 剛醒/睡前:以拖長語尾的簡單標記呈現迷糊演出(完整口吻交由模板庫日後擴充)。 if (scheduleInfo.isWaking || scheduleInfo.isPreSleep) { text = `……${text}`; } const forbiddenWords = getForbiddenWords(context.character.personalityArchetype); const filteredText = applySpeechQuirks( this.outputFilter.filter(text, forbiddenWords), context.character.personalityArchetype, ); await this.workingMemory.append(sessionId, { role: "character", content: filteredText, timestamp: now }); return { text: filteredText, actions, isFastChannel, availability: effectiveAvailability, exceptionType, context }; } }