436 lines
13 KiB
TypeScript
436 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";
|
|
import { logger } from "db://assets/Scripts/Main/Common/Logger";
|
|
|
|
/**
|
|
* AI情绪分析服务类
|
|
*
|
|
* 基于Google Gemini API实现的情绪分析系统,支持:
|
|
* - 基于聊天历史的情绪状态分析
|
|
* - 多角色独立的情绪聊天实例管理
|
|
* - 返回标准的VideoEmotion枚举值
|
|
* - 自动初始化历史情绪分析
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const emotionService = EmotionAIService.Instance;
|
|
* const emotion = await emotionService.analyzeEmotionalState(10001);
|
|
* logger.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,
|
|
httpOptions: { timeout: emotionConfig.timeout },
|
|
});
|
|
} 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);
|
|
logger.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) {
|
|
logger.log(
|
|
`Analyzing emotional state for role ${roleId} based on ${messageCount} messages`
|
|
);
|
|
|
|
// 分析历史情绪状态
|
|
emotionalState = await this.analyzeEmotionalState(roleId);
|
|
if (emotionalState == null) {
|
|
emotionalState = VideoEmotion.calm_down;
|
|
}
|
|
logger.log(
|
|
`Initial emotional state for role ${roleId}: ${VideoEmotion[emotionalState]}`
|
|
);
|
|
} else {
|
|
logger.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,
|
|
});
|
|
|
|
logger.log(
|
|
`Emotion initialization completed for role ${roleId}: ${VideoEmotion[emotionalState]}`
|
|
);
|
|
} catch (error) {
|
|
logger.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 null;
|
|
}
|
|
|
|
if (!message || message.trim() === "") {
|
|
ErrorHandler.Instance.handleValidationError(
|
|
"message",
|
|
"消息内容不能为空",
|
|
message
|
|
);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const emotionChat = this.createOrGetEmotionChat(roleId);
|
|
const response = await emotionChat.sendMessage({
|
|
message: message.trim(),
|
|
});
|
|
|
|
if (response && response.text) {
|
|
logger.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 null;
|
|
}
|
|
} catch (error) {
|
|
ErrorHandler.Instance.handleApiError(error, "sendEmotionMessage", {
|
|
roleId,
|
|
message: message.substring(0, 100) + "...",
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 分析指定角色的情绪状态(基于最近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 null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 解析情绪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);
|
|
}
|
|
logger.log(`Cleared emotion chat history for role ${roleId}`);
|
|
}
|
|
|
|
/**
|
|
* 清除所有情绪聊天历史
|
|
*/
|
|
public clearAllEmotionHistory(): void {
|
|
this.emotionChatInstances.clear();
|
|
logger.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);
|
|
logger.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) {
|
|
logger.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进行分析
|
|
let newEmotion = await this.sendEmotionMessage(roleId, analysisPrompt);
|
|
|
|
if (newEmotion == null) {
|
|
newEmotion = currentEmotion;
|
|
}
|
|
// 更新并保存情绪状态
|
|
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 += `user: ${userMessage}\n`;
|
|
analysisText += `model: ${aiResponse}\n\n`;
|
|
analysisText += `请基于对话内容和当前情绪状态,返回以下枚举值之一:calm_down, arousal, desire, passion, orgasm`;
|
|
|
|
return analysisText;
|
|
}
|
|
}
|