2025-08-11 16:08:59 +08:00
|
|
|
|
// 首先加载 polyfills 以确保兼容性
|
|
|
|
|
|
import "../utils/polyfills";
|
2025-08-27 16:12:19 +08:00
|
|
|
|
import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from "@google/genai";
|
2025-08-13 11:31:21 +08:00
|
|
|
|
import { RoleConfigLoader } from "./RoleConfigLoader";
|
|
|
|
|
|
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
|
|
|
|
|
|
import { ApiConfig } from "./ApiConfigLoader";
|
2025-08-11 16:08:59 +08:00
|
|
|
|
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
|
2025-08-26 19:41:13 +08:00
|
|
|
|
import { EmotionAIService } from "./EmotionAIService";
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* AI聊天服务类
|
2025-08-13 11:31:21 +08:00
|
|
|
|
*
|
2025-08-11 16:08:59 +08:00
|
|
|
|
* 基于Google Gemini API实现的多角色聊天系统,支持:
|
|
|
|
|
|
* - 多个独立的聊天实例管理
|
|
|
|
|
|
* - 每个角色拥有独立的对话上下文和历史记录
|
|
|
|
|
|
* - 本地聊天历史存储和加载
|
|
|
|
|
|
* - 动态角色配置和切换
|
2025-08-13 11:31:21 +08:00
|
|
|
|
*
|
2025-08-11 16:08:59 +08:00
|
|
|
|
* @example
|
|
|
|
|
|
* ```typescript
|
|
|
|
|
|
* const chatService = ChatAIService.Instance;
|
|
|
|
|
|
* chatService.setCurrentRole(10001);
|
|
|
|
|
|
* const response = await chatService.sendMessage(10001, "Hello");
|
|
|
|
|
|
* ```
|
2025-08-13 11:31:21 +08:00
|
|
|
|
*
|
2025-08-11 16:08:59 +08:00
|
|
|
|
* @author AI Chat System
|
|
|
|
|
|
* @version 2.0.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class ChatAIService {
|
2025-08-13 11:31:21 +08:00
|
|
|
|
private static _instance: ChatAIService;
|
|
|
|
|
|
private ai: GoogleGenAI;
|
|
|
|
|
|
private chatInstances: Map<number, any> = new Map();
|
|
|
|
|
|
private currentRoleId: number | null = null;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
private constructor() {
|
|
|
|
|
|
const config = ApiConfig.Instance.getAIConfig();
|
|
|
|
|
|
|
|
|
|
|
|
// 验证配置
|
|
|
|
|
|
if (!ApiConfig.Instance.validateConfig()) {
|
|
|
|
|
|
ErrorHandler.Instance.handleError(
|
|
|
|
|
|
new Error("AI配置验证失败"),
|
|
|
|
|
|
ErrorType.CONFIG_ERROR,
|
|
|
|
|
|
{ config },
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
throw new Error("AI服务初始化失败:配置无效");
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
try {
|
|
|
|
|
|
this.ai = new GoogleGenAI({ apiKey: config.apiKey });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ErrorHandler.Instance.handleError(
|
|
|
|
|
|
error as Error,
|
|
|
|
|
|
ErrorType.API_ERROR,
|
|
|
|
|
|
{ config: { ...config, apiKey: "***" } }, // 隐藏API密钥
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取ChatAIService的单例实例
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {ChatAIService} 聊天服务实例
|
|
|
|
|
|
* @static
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static get Instance(): ChatAIService {
|
|
|
|
|
|
if (!this._instance) {
|
|
|
|
|
|
this._instance = new ChatAIService();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._instance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建或获取指定角色的聊天实例
|
|
|
|
|
|
* @param roleId 角色ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
private createOrGetChat(roleId: number): any {
|
|
|
|
|
|
if (!this.chatInstances.has(roleId)) {
|
|
|
|
|
|
const systemInstruction = RoleConfigLoader.getRoleInstruction(roleId);
|
|
|
|
|
|
const config = ApiConfig.Instance.getAIConfig();
|
|
|
|
|
|
|
|
|
|
|
|
// 从本地加载历史记录
|
|
|
|
|
|
const savedHistory = ChatHistoryManager.Instance.loadHistory(roleId);
|
|
|
|
|
|
let chat;
|
|
|
|
|
|
if (savedHistory && savedHistory.length > 0) {
|
|
|
|
|
|
chat = this.ai.chats.create({
|
|
|
|
|
|
model: config.model,
|
|
|
|
|
|
config: {
|
|
|
|
|
|
temperature: config.temperature,
|
|
|
|
|
|
systemInstruction: systemInstruction,
|
2025-08-27 16:12:19 +08:00
|
|
|
|
safetySettings: [
|
|
|
|
|
|
{
|
|
|
|
|
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
|
|
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-08-13 11:31:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
history: savedHistory,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chat = this.ai.chats.create({
|
|
|
|
|
|
model: config.model,
|
|
|
|
|
|
config: {
|
|
|
|
|
|
temperature: config.temperature,
|
|
|
|
|
|
systemInstruction: systemInstruction,
|
2025-08-27 16:12:19 +08:00
|
|
|
|
safetySettings: [
|
|
|
|
|
|
{
|
|
|
|
|
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
|
|
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-08-13 11:31:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.chatInstances.set(roleId, chat);
|
|
|
|
|
|
|
|
|
|
|
|
if (savedHistory.length > 0) {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`Loaded ${savedHistory.length} history messages for role ${roleId}`
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`Created new chat instance for role ${roleId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.chatInstances.get(roleId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置当前活动的角色ID
|
|
|
|
|
|
* @param roleId 角色ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
public setCurrentRole(roleId: number): void {
|
|
|
|
|
|
this.currentRoleId = roleId;
|
|
|
|
|
|
// 预创建聊天实例
|
|
|
|
|
|
this.createOrGetChat(roleId);
|
2025-08-26 19:41:13 +08:00
|
|
|
|
// 预创建情绪聊天实例
|
|
|
|
|
|
EmotionAIService.Instance.ensureEmotionChatExists(roleId);
|
2025-08-13 11:31:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前角色ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
public getCurrentRoleId(): number | null {
|
|
|
|
|
|
return this.currentRoleId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 向指定角色发送消息并获取AI回复
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number} roleId - 角色ID,用于区分不同的聊天实例
|
|
|
|
|
|
* @param {string} message - 用户发送的消息内容
|
|
|
|
|
|
* @returns {Promise<string>} AI的回复消息,如果发生错误返回null
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
* ```typescript
|
|
|
|
|
|
* const response = await chatService.sendMessage(10001, "你好");
|
|
|
|
|
|
* console.log(response); // AI的回复
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*/
|
|
|
|
|
|
public async sendMessage(roleId: number, message: string): Promise<string> {
|
|
|
|
|
|
// 输入验证
|
|
|
|
|
|
if (!roleId || roleId <= 0) {
|
|
|
|
|
|
ErrorHandler.Instance.handleValidationError(
|
|
|
|
|
|
"roleId",
|
|
|
|
|
|
"角色ID必须是正整数",
|
|
|
|
|
|
roleId
|
|
|
|
|
|
);
|
|
|
|
|
|
return null;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
if (!message || message.trim() === "") {
|
|
|
|
|
|
ErrorHandler.Instance.handleValidationError(
|
|
|
|
|
|
"message",
|
|
|
|
|
|
"消息内容不能为空",
|
|
|
|
|
|
message
|
|
|
|
|
|
);
|
|
|
|
|
|
return null;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const chat = this.createOrGetChat(roleId);
|
|
|
|
|
|
const response = await chat.sendMessage({
|
|
|
|
|
|
message: message.trim(),
|
|
|
|
|
|
});
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
if (response && response.text) {
|
|
|
|
|
|
console.log(`Response from role ${roleId}:`, response.text);
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-13 11:31:21 +08:00
|
|
|
|
// 保存用户消息
|
|
|
|
|
|
ChatHistoryManager.Instance.appendMessage(roleId, {
|
|
|
|
|
|
role: "user",
|
|
|
|
|
|
parts: [{ text: message }],
|
|
|
|
|
|
});
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
// 保存AI回复
|
|
|
|
|
|
ChatHistoryManager.Instance.appendMessage(roleId, {
|
|
|
|
|
|
role: "model",
|
|
|
|
|
|
parts: [{ text: response.text }],
|
|
|
|
|
|
});
|
2025-08-26 19:41:13 +08:00
|
|
|
|
|
2025-08-27 16:12:19 +08:00
|
|
|
|
// 异步更新情绪状态(非阻塞)
|
|
|
|
|
|
EmotionAIService.Instance.updateEmotionFromChat(
|
|
|
|
|
|
roleId,
|
|
|
|
|
|
message,
|
|
|
|
|
|
response.text
|
|
|
|
|
|
).catch(emotionError => {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`Failed to update emotion for role ${roleId}:`,
|
|
|
|
|
|
emotionError
|
|
|
|
|
|
);
|
2025-08-26 19:41:13 +08:00
|
|
|
|
// 情绪更新失败不影响聊天功能
|
2025-08-27 16:12:19 +08:00
|
|
|
|
});
|
2025-08-13 11:31:21 +08:00
|
|
|
|
} catch (storageError) {
|
|
|
|
|
|
ErrorHandler.Instance.handleError(
|
|
|
|
|
|
storageError as Error,
|
|
|
|
|
|
ErrorType.STORAGE_ERROR,
|
|
|
|
|
|
{ roleId, message: message.substring(0, 100) },
|
|
|
|
|
|
false
|
|
|
|
|
|
);
|
|
|
|
|
|
// 即使存储失败,也返回AI回复
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
return response.text;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const warningMsg = `AI返回了空响应 (角色ID: ${roleId})`;
|
|
|
|
|
|
ErrorHandler.Instance.handleError(
|
|
|
|
|
|
new Error(warningMsg),
|
|
|
|
|
|
ErrorType.API_ERROR,
|
2025-08-27 16:12:19 +08:00
|
|
|
|
{ roleId, chat, response },
|
2025-08-13 11:31:21 +08:00
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ErrorHandler.Instance.handleApiError(error, "sendMessage", {
|
|
|
|
|
|
roleId,
|
|
|
|
|
|
message: message.substring(0, 100) + "...",
|
|
|
|
|
|
});
|
|
|
|
|
|
return null;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
2025-08-13 11:31:21 +08:00
|
|
|
|
}
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清除指定角色的聊天历史
|
|
|
|
|
|
* @param roleId 角色ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
public clearChatHistory(roleId: number): void {
|
|
|
|
|
|
if (this.chatInstances.has(roleId)) {
|
|
|
|
|
|
this.chatInstances.delete(roleId);
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
2025-08-26 19:41:13 +08:00
|
|
|
|
// 清除情绪聊天历史
|
|
|
|
|
|
EmotionAIService.Instance.clearEmotionHistory(roleId);
|
2025-08-13 11:31:21 +08:00
|
|
|
|
// 清除本地存储的历史
|
|
|
|
|
|
ChatHistoryManager.Instance.clearHistory(roleId);
|
|
|
|
|
|
console.log(`Cleared chat history for role ${roleId}`);
|
|
|
|
|
|
}
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清除所有聊天历史
|
|
|
|
|
|
*/
|
|
|
|
|
|
public clearAllChatHistory(): void {
|
|
|
|
|
|
this.chatInstances.clear();
|
2025-08-26 19:41:13 +08:00
|
|
|
|
// 清除所有情绪聊天历史
|
|
|
|
|
|
EmotionAIService.Instance.clearAllEmotionHistory();
|
2025-08-13 11:31:21 +08:00
|
|
|
|
// 清除本地存储的所有历史
|
|
|
|
|
|
ChatHistoryManager.Instance.clearAllHistory();
|
|
|
|
|
|
console.log("Cleared all chat histories");
|
|
|
|
|
|
}
|
2025-08-11 16:08:59 +08:00
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前活跃的聊天实例数量
|
|
|
|
|
|
*/
|
|
|
|
|
|
public getActiveChatCount(): number {
|
|
|
|
|
|
return this.chatInstances.size;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 兼容旧接口的Post方法
|
|
|
|
|
|
* @deprecated 请使用sendMessage方法,此方法将在下个版本中移除
|
|
|
|
|
|
*/
|
|
|
|
|
|
public async Post(data: GPTRequest): Promise<string> {
|
|
|
|
|
|
console.warn("Post方法已废弃,请使用sendMessage方法");
|
|
|
|
|
|
const roleId = this.currentRoleId || 10001; // 默认使用第一个角色
|
|
|
|
|
|
const message = data.messages[0]?.content || "";
|
|
|
|
|
|
return this.sendMessage(roleId, message);
|
|
|
|
|
|
}
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 请求数据类型定义
|
|
|
|
|
|
export interface GPTRequest {
|
2025-08-13 11:31:21 +08:00
|
|
|
|
model: string;
|
|
|
|
|
|
messages: { role: string; content: string }[];
|
|
|
|
|
|
temperature: number;
|
|
|
|
|
|
id: string;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 兼容旧名称(已废弃,建议使用 GPTRequest)
|
|
|
|
|
|
/** @deprecated 请使用 GPTRequest */
|
|
|
|
|
|
export type GPTResquest = GPTRequest;
|
|
|
|
|
|
|
|
|
|
|
|
// 响应数据类型定义(当前未使用,预留用于未来API调用统计)
|
|
|
|
|
|
/** @deprecated 当前未使用,考虑移除或实现API统计功能时使用 */
|
|
|
|
|
|
export interface GPTResult {
|
2025-08-13 11:31:21 +08:00
|
|
|
|
id: string;
|
|
|
|
|
|
object: string;
|
|
|
|
|
|
created: number;
|
|
|
|
|
|
model: string;
|
|
|
|
|
|
usage: {
|
|
|
|
|
|
prompt_tokens: number;
|
|
|
|
|
|
completion_tokens: number;
|
|
|
|
|
|
total_tokens: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
choices: {
|
|
|
|
|
|
message: {
|
|
|
|
|
|
role: string;
|
|
|
|
|
|
content: string;
|
2025-08-11 16:08:59 +08:00
|
|
|
|
};
|
2025-08-13 11:31:21 +08:00
|
|
|
|
finish_reason: string;
|
|
|
|
|
|
index: number;
|
|
|
|
|
|
}[];
|
|
|
|
|
|
}
|