Files
18xchat/assets/Scripts/chat18x/core/EmotionAIService.ts
T

425 lines
13 KiB
TypeScript

// 首先加载 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);
// 发送情绪更新完成事件
Utils.sendInnerMsg(InnerMsgCode.Chat_EmotionUpdated, {
roleId: roleId,
emotion: 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;
}
}