Chat相关表现&逻辑分离,新增ChatController

新增情绪机器人逻辑,
新增情绪切换视频逻辑
This commit is contained in:
2025-08-26 19:41:13 +08:00
parent 58bac34580
commit 1507fff7c5
25 changed files with 2763 additions and 6933 deletions
@@ -60,6 +60,20 @@ export class ApiConfig {
return { ...this.config };
}
/**
* 获取情绪AI配置
*/
public getEmotionAIConfig(): AIConfig {
const config = ConfigManager.tables.TbGlobalConfig;
return {
apiKey: config.emotionApiKey,
model: config.Model,
temperature: config.Temperature,
maxTokens: config.MaxTokens,
timeout: config.Timeout,
};
}
/**
* 更新生成参数
* @param temperature 温度参数
@@ -5,6 +5,7 @@ import { RoleConfigLoader } from "./RoleConfigLoader";
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
import { ApiConfig } from "./ApiConfigLoader";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
import { EmotionAIService } from "./EmotionAIService";
/**
* AI聊天服务类
@@ -123,6 +124,8 @@ export class ChatAIService {
this.currentRoleId = roleId;
// 预创建聊天实例
this.createOrGetChat(roleId);
// 预创建情绪聊天实例
EmotionAIService.Instance.ensureEmotionChatExists(roleId);
}
/**
@@ -186,6 +189,14 @@ export class ChatAIService {
role: "model",
parts: [{ text: response.text }],
});
// 更新情绪状态
try {
await EmotionAIService.Instance.updateEmotionFromChat(roleId, message, response.text);
} catch (emotionError) {
console.warn(`Failed to update emotion for role ${roleId}:`, emotionError);
// 情绪更新失败不影响聊天功能
}
} catch (storageError) {
ErrorHandler.Instance.handleError(
storageError as Error,
@@ -224,6 +235,8 @@ export class ChatAIService {
if (this.chatInstances.has(roleId)) {
this.chatInstances.delete(roleId);
}
// 清除情绪聊天历史
EmotionAIService.Instance.clearEmotionHistory(roleId);
// 清除本地存储的历史
ChatHistoryManager.Instance.clearHistory(roleId);
console.log(`Cleared chat history for role ${roleId}`);
@@ -234,6 +247,8 @@ export class ChatAIService {
*/
public clearAllChatHistory(): void {
this.chatInstances.clear();
// 清除所有情绪聊天历史
EmotionAIService.Instance.clearAllEmotionHistory();
// 清除本地存储的所有历史
ChatHistoryManager.Instance.clearAllHistory();
console.log("Cleared all chat histories");
@@ -0,0 +1,250 @@
import { ChatAIService } from "./ChatAIService";
import { EmotionAIService } from "./EmotionAIService";
import { DialogManager } from "../manager/DialogManager";
import { VideoEmotion } from "../../schema/schema";
import ConfigManager from "../manager/ConfigManager";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
/**
* 聊天控制器接口 - 定义Panel和Controller之间的通信协议
*/
export interface IChatPanelCallback {
/**
* 消息发送开始回调
* @param message 用户发送的消息
*/
onMessageSent(message: string): void;
/**
* 接收到AI回复回调
* @param response AI的回复内容
*/
onMessageReceived(response: string): void;
/**
* 情绪状态更新回调
* @param emotion 更新后的情绪状态
*/
onEmotionUpdated(emotion: VideoEmotion): void;
/**
* 对话更新回调
*/
onDialogUpdated(): void;
/**
* 错误处理回调
* @param error 错误信息
*/
onError(error: Error): void;
}
/**
* 聊天控制器类
*
* 负责处理聊天相关的所有业务逻辑,包括:
* - 角色数据管理
* - 消息发送和接收
* - 情绪状态管理
* - 与AI服务的交互
* - 对话历史管理
*
* @example
* ```typescript
* const controller = new ChatController();
* controller.initialize(10001, panelCallback);
* const response = await controller.sendMessage("Hello");
* ```
*/
export class ChatController {
private roleId: number | null = null;
private callback: IChatPanelCallback | null = null;
private dialogManager: DialogManager | null = null;
/**
* 初始化聊天控制器
* @param roleId 角色ID
* @param callback 回调接口实现
*/
public initialize(roleId: number, callback: IChatPanelCallback): void {
this.roleId = roleId;
this.callback = callback;
this.dialogManager = DialogManager.getInstance();
// 设置当前聊天的角色ID
if (roleId && roleId > 0) {
ChatAIService.Instance.setCurrentRole(roleId);
console.log(`ChatController initialized with role ${roleId}`);
} else {
const error = new Error(`Invalid roleId: ${roleId}`);
this.handleError(error);
}
}
/**
* 发送消息给AI并处理回复
* @param message 用户消息内容
* @returns Promise<string | null> AI的回复,失败时返回null
*/
public async sendMessage(message: string): Promise<string | null> {
if (!this.validateSendMessage(message)) {
return null;
}
try {
// 通知界面消息发送开始
this.callback?.onMessageSent(message);
// 更新对话显示 - 用户消息
this.dialogManager?.updateDialog(true, message, true);
this.callback?.onDialogUpdated();
console.log(`Sending message to role ${this.roleId}: ${message}`);
// 发送消息给AI服务
const response = await ChatAIService.Instance.sendMessage(this.roleId!, message);
if (response) {
// 更新对话显示 - AI回复
this.dialogManager?.updateDialog(false, response);
this.callback?.onDialogUpdated();
// 通知界面收到回复
this.callback?.onMessageReceived(response);
// 获取更新后的情绪状态
try {
const currentEmotion = EmotionAIService.Instance.getCurrentEmotion(this.roleId!);
console.log(`Current emotion for role ${this.roleId}: ${VideoEmotion[currentEmotion]}`);
// 通知界面情绪更新
this.callback?.onEmotionUpdated(currentEmotion);
} catch (emotionError) {
console.warn("Failed to get current emotion:", emotionError);
// 情绪获取失败不影响聊天功能
}
console.log(`Response received from role ${this.roleId}: ${response}`);
return response;
} else {
const error = new Error("AI返回了空响应");
this.handleError(error);
return null;
}
} catch (error) {
ErrorHandler.Instance.handleApiError(error, "ChatController.sendMessage", {
roleId: this.roleId,
message: message.substring(0, 100) + "..."
});
this.handleError(error as Error);
return null;
}
}
/**
* 获取当前角色的情绪状态
* @returns VideoEmotion 当前情绪状态
*/
public getCurrentEmotion(): VideoEmotion {
if (!this.roleId) {
return VideoEmotion.calm_down;
}
return EmotionAIService.Instance.getCurrentEmotion(this.roleId);
}
/**
* 获取当前角色数据
* @returns 角色数据对象,失败时返回null
*/
public getRoleData(): any {
if (!this.roleId) {
return null;
}
try {
const roleData = ConfigManager.tables.TbGirls.get(this.roleId);
const roleDetail = ConfigManager.tables.TbGirlsDetail.get(this.roleId);
return {
basic: roleData,
detail: roleDetail
};
} catch (error) {
console.error(`Failed to get role data for ${this.roleId}:`, error);
this.handleError(error as Error);
return null;
}
}
/**
* 清除当前角色的聊天历史
*/
public clearChatHistory(): void {
if (!this.roleId) {
console.warn("Cannot clear history: roleId is null");
return;
}
try {
ChatAIService.Instance.clearChatHistory(this.roleId);
console.log(`Chat history cleared for role ${this.roleId}`);
} catch (error) {
console.error("Failed to clear chat history:", error);
this.handleError(error as Error);
}
}
/**
* 获取当前角色ID
* @returns 当前角色ID
*/
public getCurrentRoleId(): number | null {
return this.roleId;
}
/**
* 销毁控制器,清理资源
*/
public destroy(): void {
this.roleId = null;
this.callback = null;
this.dialogManager = null;
console.log("ChatController destroyed");
}
/**
* 验证发送消息的参数
* @param message 消息内容
* @returns 验证是否通过
*/
private validateSendMessage(message: string): boolean {
if (!this.roleId || this.roleId <= 0) {
const error = new Error("角色ID无效");
this.handleError(error);
return false;
}
if (!message || message.trim() === "") {
const error = new Error("消息内容不能为空");
this.handleError(error);
return false;
}
if (!this.callback) {
const error = new Error("回调接口未设置");
this.handleError(error);
return false;
}
return true;
}
/**
* 统一错误处理
* @param error 错误对象
*/
private handleError(error: Error): void {
console.error("ChatController error:", error);
this.callback?.onError(error);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "458edd93-5ff2-4c11-a573-f8985c48cc15",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,418 @@
// 首先加载 polyfills 以确保兼容性
import "../utils/polyfills";
import { GoogleGenAI } from "@google/genai";
import { RoleConfigLoader } from "./RoleConfigLoader";
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
import { ApiConfig } from "./ApiConfigLoader";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
import { VideoEmotion } from "../../schema/schema";
import ConfigManager from "../manager/ConfigManager";
import LanguageUtils, { LanguageType } from "../../Main/Common/LanguageUtils";
import Utils from "../../Main/Common/Utils";
import { InnerMsgCode } from "../../Main/Config/InnerMsgCode";
/**
* AI情绪分析服务类
*
* 基于Google Gemini API实现的情绪分析系统,支持:
* - 基于聊天历史的情绪状态分析
* - 多角色独立的情绪聊天实例管理
* - 返回标准的VideoEmotion枚举值
* - 自动初始化历史情绪分析
*
* @example
* ```typescript
* const emotionService = EmotionAIService.Instance;
* const emotion = await emotionService.analyzeEmotionalState(10001);
* console.log(VideoEmotion[emotion]); // "calm_down"
* ```
*
* @author AI Chat System
* @version 1.0.0
*/
export class EmotionAIService {
private static _instance: EmotionAIService;
private emotionAI: GoogleGenAI;
private emotionChatInstances: Map<number, any> = new Map();
private currentEmotions: Map<number, VideoEmotion> = new Map();
private constructor() {
const emotionConfig = ApiConfig.Instance.getEmotionAIConfig();
try {
this.emotionAI = new GoogleGenAI({ apiKey: emotionConfig.apiKey });
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ config: { ...emotionConfig, apiKey: "***" } }, // 隐藏API密钥
true
);
throw error;
}
}
/**
* 获取EmotionAIService的单例实例
*
* @returns {EmotionAIService} 情绪分析服务实例
* @static
*/
public static get Instance(): EmotionAIService {
if (!this._instance) {
this._instance = new EmotionAIService();
}
return this._instance;
}
/**
* 创建或获取指定角色的情绪聊天实例
* @param roleId 角色ID
*/
private createOrGetEmotionChat(roleId: number): any {
if (!this.emotionChatInstances.has(roleId)) {
const systemInstruction = RoleConfigLoader.getEmotionInstruction(roleId);
const config = ApiConfig.Instance.getEmotionAIConfig();
const chat = this.emotionAI.chats.create({
model: config.model,
config: {
temperature: config.temperature,
systemInstruction: systemInstruction,
},
});
this.emotionChatInstances.set(roleId, chat);
console.log(`Created new emotion chat instance for role ${roleId}`);
// 异步分析历史情绪状态
this.initializeEmotionAnalysis(roleId);
}
return this.emotionChatInstances.get(roleId);
}
/**
* 初始化情绪分析(检查是否有历史记录并进行分析)
* @param roleId 角色ID
*/
private async initializeEmotionAnalysis(roleId: number): Promise<void> {
try {
// 检查是否有聊天历史
const messageCount = ChatHistoryManager.Instance.getMessageCount(roleId);
let emotionalState: VideoEmotion;
if (messageCount > 0) {
console.log(
`Analyzing emotional state for role ${roleId} based on ${messageCount} messages`
);
// 分析历史情绪状态
emotionalState = await this.analyzeEmotionalState(roleId);
console.log(
`Initial emotional state for role ${roleId}: ${VideoEmotion[emotionalState]}`
);
} else {
console.log(
`No history found for role ${roleId}, using default emotion: calm_down`
);
emotionalState = VideoEmotion.calm_down;
}
// 发送情绪初始化完成事件
Utils.sendInnerMsg(InnerMsgCode.Chat_EmotionInitialized, {
roleId: roleId,
emotion: emotionalState
});
console.log(`Emotion initialization completed for role ${roleId}: ${VideoEmotion[emotionalState]}`);
} catch (error) {
console.error(
`Failed to initialize emotion analysis for role ${roleId}:`,
error
);
// 即使发生错误,也发送默认情绪状态
Utils.sendInnerMsg(InnerMsgCode.Chat_EmotionInitialized, {
roleId: roleId,
emotion: VideoEmotion.calm_down
});
}
}
/**
* 格式化聊天历史用于情绪分析
* @param messages 聊天消息数组
* @returns 格式化后的分析提示
*/
private formatHistoryForEmotionAnalysis(messages: any[]): string {
if (messages.length === 0) {
return "没有聊天历史,请返回默认情绪状态:calm_down";
}
let conversationText =
"请分析以下对话的情绪状态,返回对应的VideoEmotion枚举值:\n\n";
messages.forEach((message, index) => {
const speaker = message.role === "user" ? "用户" : "AI";
const text = message.parts[0]?.text || "";
conversationText += `${speaker}: ${text}\n`;
});
conversationText +=
"\n请返回以下枚举值之一:calm_down, arousal, desire, passion, orgasm";
return conversationText;
}
/**
* 向指定角色的情绪AI发送消息并获取分析回复,返回VideoEmotion枚举值
* @param roleId 角色ID
* @param message 用户发送的消息内容
* @returns Promise<VideoEmotion> 情绪AI的分析结果,如果发生错误返回默认值calm_down
*/
public async sendEmotionMessage(
roleId: number,
message: string
): Promise<VideoEmotion> {
// 输入验证
if (!roleId || roleId <= 0) {
ErrorHandler.Instance.handleValidationError(
"roleId",
"角色ID必须是正整数",
roleId
);
return VideoEmotion.calm_down;
}
if (!message || message.trim() === "") {
ErrorHandler.Instance.handleValidationError(
"message",
"消息内容不能为空",
message
);
return VideoEmotion.calm_down;
}
try {
const emotionChat = this.createOrGetEmotionChat(roleId);
const response = await emotionChat.sendMessage({
message: message.trim(),
});
if (response && response.text) {
console.log(`Emotion analysis from role ${roleId}:`, response.text);
return this.parseEmotionResponse(response.text);
} else {
const warningMsg = `情绪AI返回了空响应 (角色ID: ${roleId})`;
ErrorHandler.Instance.handleError(
new Error(warningMsg),
ErrorType.API_ERROR,
{ roleId, message, response },
true
);
return VideoEmotion.calm_down;
}
} catch (error) {
ErrorHandler.Instance.handleApiError(error, "sendEmotionMessage", {
roleId,
message: message.substring(0, 100) + "...",
});
return VideoEmotion.calm_down;
}
}
/**
* 分析指定角色的情绪状态(基于最近10条聊天记录)
* @param roleId 角色ID
* @returns Promise<VideoEmotion> 情绪分析结果
*/
public async analyzeEmotionalState(roleId: number): Promise<VideoEmotion> {
try {
// 获取最近10条消息
const recentMessages = ChatHistoryManager.Instance.getRecentMessages(
roleId,
10
);
// 格式化历史记录用于分析
const analysisPrompt =
this.formatHistoryForEmotionAnalysis(recentMessages);
// 发送给情绪AI进行分析
return await this.sendEmotionMessage(roleId, analysisPrompt);
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ roleId },
false
);
return VideoEmotion.calm_down;
}
}
/**
* 解析情绪AI返回的文本为VideoEmotion枚举值
* @param responseText AI返回的文本
* @returns VideoEmotion枚举值
*/
private parseEmotionResponse(responseText: string): VideoEmotion {
const text = responseText.toLowerCase().trim();
if (text.includes("arousal")) {
return VideoEmotion.arousal;
} else if (text.includes("desire")) {
return VideoEmotion.desire;
} else if (text.includes("passion")) {
return VideoEmotion.passion;
} else if (text.includes("orgasm")) {
return VideoEmotion.orgasm;
} else {
return VideoEmotion.calm_down; // 默认值
}
}
/**
* 确保指定角色的情绪聊天实例已创建
* @param roleId 角色ID
*/
public ensureEmotionChatExists(roleId: number): void {
this.createOrGetEmotionChat(roleId);
}
/**
* 清除指定角色的情绪聊天历史
* @param roleId 角色ID
*/
public clearEmotionHistory(roleId: number): void {
if (this.emotionChatInstances.has(roleId)) {
this.emotionChatInstances.delete(roleId);
}
console.log(`Cleared emotion chat history for role ${roleId}`);
}
/**
* 清除所有情绪聊天历史
*/
public clearAllEmotionHistory(): void {
this.emotionChatInstances.clear();
console.log("Cleared all emotion chat histories");
}
/**
* 获取当前活跃的情绪聊天实例数量
*/
public getActiveEmotionChatCount(): number {
return this.emotionChatInstances.size;
}
/**
* 获取指定角色的当前情绪状态
* @param roleId 角色ID
* @returns VideoEmotion 当前情绪状态
*/
public getCurrentEmotion(roleId: number): VideoEmotion {
return this.currentEmotions.get(roleId) || VideoEmotion.calm_down;
}
/**
* 设置指定角色的情绪状态
* @param roleId 角色ID
* @param emotion 情绪状态
*/
private setCurrentEmotion(roleId: number, emotion: VideoEmotion): void {
this.currentEmotions.set(roleId, emotion);
console.log(`Updated emotion for role ${roleId}: ${VideoEmotion[emotion]}`);
}
/**
* 获取角色的英文名
* @param roleId 角色ID
* @returns 角色英文名,如果未找到返回"AI"
*/
private getRoleEnglishName(roleId: number): string {
try {
if (ConfigManager.tables && ConfigManager.tables.TbGirls) {
const girlData = ConfigManager.tables.TbGirls.get(roleId);
if (girlData && girlData.nameKey) {
return (
LanguageUtils.getTextByLanguage(
girlData.nameKey,
LanguageType.EN
) || "AI"
);
}
}
} catch (error) {
console.warn(`Failed to get role name for ${roleId}:`, error);
}
return "AI";
}
/**
* 根据聊天对话更新情绪状态
* @param roleId 角色ID
* @param userMessage 用户消息
* @param aiResponse AI回复
* @returns Promise<VideoEmotion> 更新后的情绪状态
*/
public async updateEmotionFromChat(
roleId: number,
userMessage: string,
aiResponse: string
): Promise<VideoEmotion> {
try {
// 构建情绪分析提示,包含当前情绪上下文
const currentEmotion = this.getCurrentEmotion(roleId);
const analysisPrompt = this.formatChatForEmotionAnalysis(
roleId,
userMessage,
aiResponse,
currentEmotion
);
// 发送给情绪AI进行分析
const newEmotion = await this.sendEmotionMessage(roleId, analysisPrompt);
// 更新并保存情绪状态
this.setCurrentEmotion(roleId, newEmotion);
return newEmotion;
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ roleId, userMessage: userMessage.substring(0, 50) },
false
);
return this.getCurrentEmotion(roleId); // 返回当前情绪状态
}
}
/**
* 格式化聊天对话用于情绪分析
* @param roleId 角色ID
* @param userMessage 用户消息
* @param aiResponse AI回复
* @param currentEmotion 当前情绪状态
* @returns 格式化后的分析提示
*/
private formatChatForEmotionAnalysis(
roleId: number,
userMessage: string,
aiResponse: string,
currentEmotion: VideoEmotion
): string {
const roleName = this.getRoleEnglishName(roleId);
let analysisText = `当前情绪状态: ${VideoEmotion[currentEmotion]}\n\n`;
analysisText += `请分析以下最新对话的情绪变化,返回对应的VideoEmotion枚举值:\n\n`;
analysisText += `用户: ${userMessage}\n`;
analysisText += `${roleName}: ${aiResponse}\n\n`;
analysisText += `请基于对话内容和当前情绪状态,返回以下枚举值之一:calm_down, arousal, desire, passion, orgasm`;
return analysisText;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "624f6a81-539f-4347-aef1-d7c98c226a68",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -13,10 +13,26 @@ export class RoleConfigLoader {
*/
public static getRoleInstruction(roleId: number): string {
const AiCharacter = ConfigManager.tables.TbAiCharacters.get(roleId);
const globalPrompt = ConfigManager.tables.TbGlobalConfig.girlBasePrompt;
const prompt =
globalPrompt +
"\n" +
AiCharacter.basePrompt +
"\n" +
AiCharacter.additionPrompt;
return prompt;
}
return AiCharacter
? AiCharacter.systemInstruction
: ConfigManager.tables.TbAiCharacters.get(10001).systemInstruction;
/**
* 根据角色ID获取对应的情绪机器人prompt
* @param roleId 角色ID
* @returns System Instruction字符串,如果未找到则返回默认Role_1
*/
public static getEmotionInstruction(roleId: number): string {
const AiCharacter = ConfigManager.tables.TbAiCharacters.get(roleId);
const globalPrompt = ConfigManager.tables.TbGlobalConfig.EmotionRating;
const prompt = globalPrompt + "\n" + AiCharacter.basePrompt;
return prompt;
}
/**