- POST /chat/:characterId:正式對外對話端點,DialogueService 補上情緒標記與親密度 累積(每輪對話走完整管線:標記情緒→檢索記憶→組裝→生成→過濾) - POST /session/:sessionId/end:觸發睡眠固化 - NestJS 啟用 CORS(WEB_ORIGIN,預設 http://localhost:3100),本清單第一次有瀏覽器直接呼叫 api - apps/web/app/chat:對話頁面(ChatView 用戶端元件),電腦雙欄/行動單欄同一份 RWD 程式, 立繪半身像置頂佔 40% 為行動版背景層 - 情緒晶片+親密度數字顯示,跟著每輪回應即時更新 - 動作描寫標記(*…*)以暮空紫小字獨立呈現,與台詞區分 - HeartbeatWave 元件:載入時由左至右畫出、親密度決定振幅、通知紅點心跳脈動, 皆尊重 prefers-reduced-motion;首頁與對話頁共用同一元件 - 色票換成 wiki 主視覺提案頁的完整 token 組(含官方深色模式數值) - scripts/smoke/H.mjs:端到端驗證對話 API 狀態變化、session 結束固化、頁面結構標記; 另以 Playwright + headless Chromium 實際開瀏覽器驗證桌面/行動版面、送出訊息、 動作標記渲染,過程無 console 錯誤 npm run restart && npm run smoke -- H 皆通過(H-V),A~G 群組冒煙測試無回歸。
93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
import { Inject, Injectable } from "@nestjs/common";
|
|
import { PrismaService } from "../prisma/prisma.service.js";
|
|
import { WorkingMemoryService } from "../memory/working-memory.service.js";
|
|
import { EmotionService } from "../emotion/emotion.service.js";
|
|
import { RelationshipService } from "../relationship/relationship.service.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;
|
|
context: GenerationContext;
|
|
}
|
|
|
|
function fastChannelSituation(reason: string): string {
|
|
return `fast-channel:${reason}`;
|
|
}
|
|
|
|
// F 群組整合入口:串起雙速通道判斷、上下文組裝、Provider 生成、輸出過濾與工作記憶寫入。
|
|
// H-1 的 POST /chat/:characterId 會以此為基礎擴充(見 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 prisma: PrismaService,
|
|
) {}
|
|
|
|
async handleMessage(
|
|
characterId: string,
|
|
userId: string,
|
|
sessionId: string,
|
|
userInput: string,
|
|
options: HandleMessageOptions = {},
|
|
): Promise<HandleMessageResult> {
|
|
const now = options.now ?? new Date();
|
|
this.workingMemory.append(sessionId, { role: "user", content: userInput, timestamp: now });
|
|
|
|
// H-1:每輪對話先標記情緒、累積日常互動的親密度,讓後續組裝的上下文反映本輪的變化。
|
|
await this.emotion.processInput(characterId, userInput, { now });
|
|
await this.relationship.recordInteraction(characterId, userId, now);
|
|
|
|
const { isFastChannel, reason } = this.fastChannel.detect(userInput);
|
|
|
|
const context = await this.contextAssembler.assemble(characterId, userId, sessionId, userInput, {
|
|
skipMemoryRetrieval: isFastChannel,
|
|
seed: options.seed,
|
|
now,
|
|
});
|
|
|
|
let text: string;
|
|
let actions: string[];
|
|
|
|
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;
|
|
}
|
|
|
|
const forbiddenWords = getForbiddenWords(context.character.personalityArchetype);
|
|
const filteredText = applySpeechQuirks(
|
|
this.outputFilter.filter(text, forbiddenWords),
|
|
context.character.personalityArchetype,
|
|
);
|
|
this.workingMemory.append(sessionId, { role: "character", content: filteredText, timestamp: now });
|
|
|
|
return { text: filteredText, actions, isFastChannel, context };
|
|
}
|
|
}
|