408 lines
12 KiB
TypeScript
408 lines
12 KiB
TypeScript
// 首先加载 polyfills 以确保兼容性
|
||
import "../utils/polyfills";
|
||
import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from "@google/genai";
|
||
import { RoleConfigLoader } from "./RoleConfigLoader";
|
||
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
|
||
import { ApiConfig } from "./ApiConfigLoader";
|
||
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
|
||
import { EmotionAIService } from "./EmotionAIService";
|
||
import { ChatService } from "db://assets/Scripts/chat18x/network/services/ChatService";
|
||
import { DataManager, DataId } from "../data/DataManager";
|
||
import { GirlData } from "../data/GirlData";
|
||
import proto from "db://assets/Scripts/proto/proto.pb.js";
|
||
import { TipsPanel } from "../ui/panels/TipsPanel";
|
||
|
||
/**
|
||
* AI聊天服务类
|
||
*
|
||
* 基于Google Gemini API实现的多角色聊天系统,支持:
|
||
* - 多个独立的聊天实例管理
|
||
* - 每个角色拥有独立的对话上下文和历史记录
|
||
* - 本地聊天历史存储和加载
|
||
* - 动态角色配置和切换
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const chatService = ChatAIService.Instance;
|
||
* chatService.setCurrentRole(10001);
|
||
* const response = await chatService.sendMessage(10001, "Hello");
|
||
* ```
|
||
*
|
||
* @author AI Chat System
|
||
* @version 2.0.0
|
||
*/
|
||
export class ChatAIService {
|
||
private static _instance: ChatAIService;
|
||
private ai: GoogleGenAI;
|
||
private chatInstances: Map<number, any> = new Map();
|
||
private currentRoleId: number | null = null;
|
||
|
||
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服务初始化失败:配置无效");
|
||
}
|
||
|
||
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
|
||
*/
|
||
async createOrGetChat(roleId: number): Promise<any> {
|
||
if (!this.chatInstances.has(roleId)) {
|
||
const systemInstruction = RoleConfigLoader.getRoleInstruction(roleId);
|
||
const config = ApiConfig.Instance.getAIConfig();
|
||
|
||
// 使用 ChatHistoryManager 的统一加载方法(优先本地,无则获取服务端)
|
||
let savedHistory = await ChatHistoryManager.Instance.loadChatHistory(
|
||
roleId
|
||
);
|
||
let chat;
|
||
if (savedHistory && savedHistory.length > 0) {
|
||
chat = this.ai.chats.create({
|
||
model: config.model,
|
||
config: {
|
||
temperature: config.temperature,
|
||
systemInstruction: systemInstruction,
|
||
safetySettings: [
|
||
{
|
||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||
},
|
||
],
|
||
},
|
||
history: savedHistory,
|
||
});
|
||
console.log(
|
||
`Loaded ${savedHistory.length} history messages for role ${roleId}`
|
||
);
|
||
} else {
|
||
chat = this.ai.chats.create({
|
||
model: config.model,
|
||
config: {
|
||
temperature: config.temperature,
|
||
systemInstruction: systemInstruction,
|
||
safetySettings: [
|
||
{
|
||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||
},
|
||
],
|
||
},
|
||
});
|
||
console.log(`Created new chat instance for role ${roleId}`);
|
||
}
|
||
|
||
this.chatInstances.set(roleId, chat);
|
||
}
|
||
return this.chatInstances.get(roleId);
|
||
}
|
||
|
||
/**
|
||
* 设置当前活动的角色ID
|
||
* @param roleId 角色ID
|
||
*/
|
||
public async setCurrentRole(roleId: number): Promise<void> {
|
||
this.currentRoleId = roleId;
|
||
// 预创建聊天实例
|
||
await this.createOrGetChat(roleId);
|
||
// 预创建情绪聊天实例
|
||
EmotionAIService.Instance.ensureEmotionChatExists(roleId);
|
||
}
|
||
|
||
/**
|
||
* 获取当前角色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;
|
||
}
|
||
|
||
if (!message || message.trim() === "") {
|
||
ErrorHandler.Instance.handleValidationError(
|
||
"message",
|
||
"消息内容不能为空",
|
||
message
|
||
);
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const chat = await this.createOrGetChat(roleId);
|
||
const response = await chat.sendMessage({
|
||
message: message.trim(),
|
||
});
|
||
|
||
if (response && response.text) {
|
||
console.log(`Response from role ${roleId}:`, response.text);
|
||
|
||
try {
|
||
// 保存用户消息
|
||
ChatHistoryManager.Instance.appendMessage(roleId, {
|
||
role: "user",
|
||
parts: [{ text: message }],
|
||
});
|
||
|
||
// 保存AI回复
|
||
ChatHistoryManager.Instance.appendMessage(roleId, {
|
||
role: "model",
|
||
parts: [{ text: response.text }],
|
||
});
|
||
|
||
// 上报聊天数据到服务器
|
||
let msgList: proto.cs.IChatMsg[] = [];
|
||
let userData = {
|
||
isAi: false,
|
||
msg: message,
|
||
msgId: new Date().getTime().toString(),
|
||
};
|
||
msgList.push(userData);
|
||
let aiData = {
|
||
isAi: true,
|
||
msg: response.text,
|
||
msgId: (new Date().getTime() + 1).toString(),
|
||
};
|
||
msgList.push(aiData);
|
||
this.reqChatMsg(roleId, msgList);
|
||
|
||
// 异步更新情绪状态(非阻塞)
|
||
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,
|
||
ErrorType.STORAGE_ERROR,
|
||
{ roleId, message: message.substring(0, 100) },
|
||
false
|
||
);
|
||
// 即使存储失败,也返回AI回复
|
||
}
|
||
|
||
return response.text;
|
||
} else {
|
||
const warningMsg = `AI返回了空响应 (角色ID: ${roleId})`;
|
||
ErrorHandler.Instance.handleError(
|
||
new Error(warningMsg),
|
||
ErrorType.API_ERROR,
|
||
{ roleId, chat, response },
|
||
true
|
||
);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
ErrorHandler.Instance.handleApiError(error, "sendMessage", {
|
||
roleId,
|
||
message: message.substring(0, 100) + "...",
|
||
});
|
||
TipsPanel.show("api调用失败,请使用vpn并重新启动");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除指定角色的聊天历史
|
||
* @param roleId 角色ID
|
||
*/
|
||
public clearChatHistory(roleId: number): void {
|
||
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}`);
|
||
}
|
||
|
||
/**
|
||
* 清除所有聊天历史
|
||
*/
|
||
public clearAllChatHistory(): void {
|
||
this.chatInstances.clear();
|
||
// 清除所有情绪聊天历史
|
||
EmotionAIService.Instance.clearAllEmotionHistory();
|
||
// 清除本地存储的所有历史
|
||
ChatHistoryManager.Instance.clearAllHistory();
|
||
console.log("Cleared all chat histories");
|
||
}
|
||
|
||
/**
|
||
* 获取当前活跃的聊天实例数量
|
||
*/
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 上报聊天数据到服务器
|
||
*/
|
||
async reqChatMsg(girlId: number, msgList: proto.cs.IChatMsg[]) {
|
||
// 请求数据
|
||
const reqData = {
|
||
girlId,
|
||
msgs: msgList,
|
||
};
|
||
console.log("请求上报聊天数据的请求数据:", reqData);
|
||
let res = await ChatService.I.reqChatMsg(reqData);
|
||
console.log("请求上报聊天数据的响应数据:", res);
|
||
if (res && res.code === proto.cs.EnmRetCode.SUCCESS) {
|
||
const resData = res.data;
|
||
// 保存数据
|
||
const girlData = DataManager.I.getDataById<GirlData>(DataId.Girl);
|
||
const category = girlData.getGrilCategoryById(girlId);
|
||
girlData.setChatTotalCount(
|
||
category.toString(),
|
||
girlId,
|
||
resData.chatTotalCount
|
||
);
|
||
girlData.setChatRemainCount(
|
||
category.toString(),
|
||
girlId,
|
||
resData.chatRemainCount
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取聊天数据
|
||
*/
|
||
async reqGetChatMsg(girlId: number, page: number, limit: number) {
|
||
// 请求数据
|
||
const reqData = {
|
||
GirlId: girlId,
|
||
page,
|
||
limit,
|
||
};
|
||
console.log("获取聊天数据的请求数据:", reqData);
|
||
let res = await ChatService.I.reqGetChatMsg(reqData);
|
||
console.log("获取聊天数据的响应数据:", res);
|
||
if (res && res.code === proto.cs.EnmRetCode.SUCCESS) {
|
||
const resData = res.data;
|
||
// 保存数据
|
||
const girlData = DataManager.I.getDataById<GirlData>(DataId.Girl);
|
||
const category = girlData.getGrilCategoryById(girlId);
|
||
girlData.setChatRecord(category.toString(), girlId, resData.msgs);
|
||
girlData.setChatTotalCount(
|
||
category.toString(),
|
||
girlId,
|
||
resData.chatTotalCount
|
||
);
|
||
girlData.setChatRemainCount(
|
||
category.toString(),
|
||
girlId,
|
||
resData.chatRemainCount
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 请求数据类型定义
|
||
export interface GPTRequest {
|
||
model: string;
|
||
messages: { role: string; content: string }[];
|
||
temperature: number;
|
||
id: string;
|
||
}
|
||
|
||
// 兼容旧名称(已废弃,建议使用 GPTRequest)
|
||
/** @deprecated 请使用 GPTRequest */
|
||
export type GPTResquest = GPTRequest;
|
||
|
||
// 响应数据类型定义(当前未使用,预留用于未来API调用统计)
|
||
/** @deprecated 当前未使用,考虑移除或实现API统计功能时使用 */
|
||
export interface GPTResult {
|
||
id: string;
|
||
object: string;
|
||
created: number;
|
||
model: string;
|
||
usage: {
|
||
prompt_tokens: number;
|
||
completion_tokens: number;
|
||
total_tokens: number;
|
||
};
|
||
choices: {
|
||
message: {
|
||
role: string;
|
||
content: string;
|
||
};
|
||
finish_reason: string;
|
||
index: number;
|
||
}[];
|
||
}
|