import { sys } from "cc"; import { DataManager, DataId } from "../data/DataManager"; import { GirlData } from "../data/GirlData"; import proto from "db://assets/Scripts/proto/proto.pb.js"; import { ChatService } from "../network/services/ChatService"; import { logger } from "db://assets/Scripts/Main/Common/Logger"; export interface ChatMessage { role: "user" | "model"; parts: { text: string }[]; timestamp?: number; } export interface ChatHistory { roleId: number; messages: ChatMessage[]; createdAt: number; updatedAt: number; } /** * 聊天历史管理器 * 负责聊天记录的本地存储、加载和管理 */ export class ChatHistoryManager { private static _instance: ChatHistoryManager; public static get Instance(): ChatHistoryManager { if (!this._instance) { this._instance = new ChatHistoryManager(); } return this._instance; } private constructor() {} /** * 保存聊天历史到本地 * @param roleId 角色ID * @param messages 消息列表 */ public saveHistory(roleId: number, messages: ChatMessage[]): void { const key = `chat_history_${roleId}`; try { // 检查是否已存在历史记录以保留创建时间 let createdAt = Date.now(); const existingData = sys.localStorage.getItem(key); if (existingData) { try { const existingHistory: ChatHistory = JSON.parse(existingData); createdAt = existingHistory.createdAt; } catch (parseError) { logger.warn(`Failed to parse existing history for role ${roleId}, using current time as createdAt`); } } const history: ChatHistory = { roleId: roleId, messages: messages, createdAt: createdAt, updatedAt: Date.now(), }; sys.localStorage.setItem(key, JSON.stringify(history)); logger.log( `Saved chat history for role ${roleId}, ${messages.length} messages` ); } catch (error) { logger.error(`Failed to save chat history for role ${roleId}:`, error); } } /** * 从本地加载聊天历史 * @param roleId 角色ID * @returns 消息列表,如果没有历史记录则返回空数组 */ public loadHistory(roleId: number): ChatMessage[] { const key = `chat_history_${roleId}`; try { const data = sys.localStorage.getItem(key); if (data) { const history: ChatHistory = JSON.parse(data); logger.log( `Loaded chat history for role ${roleId}: ${history.messages.length} messages` ); return history.messages; } } catch (error) { logger.error(`Failed to load chat history for role ${roleId}:`, error); // 如果数据损坏,清除错误的数据 this.clearHistory(roleId); } return []; } /** * 清除指定角色的聊天历史 * @param roleId 角色ID */ public clearHistory(roleId: number): void { const key = `chat_history_${roleId}`; sys.localStorage.removeItem(key); logger.log(`Cleared chat history for role ${roleId}`); } /** * 追加消息到历史记录 * @param roleId 角色ID * @param message 消息对象 */ public appendMessage(roleId: number, message: ChatMessage): void { const history = this.loadHistory(roleId); // 添加时间戳 message.timestamp = Date.now(); history.push(message); // 限制历史长度,保留最近100条消息 if (history.length > 100) { history.splice(0, history.length - 100); logger.log(`Trimmed chat history for role ${roleId} to 100 messages`); } this.saveHistory(roleId, history); } /** * 获取指定角色的消息数量 * @param roleId 角色ID * @returns 消息数量 */ public getMessageCount(roleId: number): number { const history = this.loadHistory(roleId); return history.length; } /** * 获取最近的N条消息 * @param roleId 角色ID * @param count 消息数量 * @returns 最近的消息列表 */ public getRecentMessages(roleId: number, count: number = 10): ChatMessage[] { const history = this.loadHistory(roleId); return history.slice(-count); } /** * 清除所有聊天历史 */ public clearAllHistory(): void { // 查找所有chat_history_开头的key const keysToRemove: string[] = []; for (let i = 0; i < sys.localStorage.length; i++) { const key = sys.localStorage.key(i); if (key && key.startsWith("chat_history_")) { keysToRemove.push(key); } } // 删除找到的所有聊天历史 keysToRemove.forEach((key) => { sys.localStorage.removeItem(key); }); logger.log( `Cleared all chat histories, ${keysToRemove.length} records removed` ); } /** * 获取所有有历史记录的角色ID列表 * @returns 角色ID数组 */ public getAllHistoryRoleIds(): number[] { const roleIds: number[] = []; for (let i = 0; i < sys.localStorage.length; i++) { const key = sys.localStorage.key(i); if (key && key.startsWith("chat_history_")) { const roleId = parseInt(key.replace("chat_history_", "")); if (!isNaN(roleId)) { roleIds.push(roleId); } } } return roleIds; } /** * 准备上传到远程服务器(预留接口) * @param roleId 角色ID */ public async syncToRemote(roleId: number): Promise { const history = this.loadHistory(roleId); if (history.length === 0) { logger.log(`No history to sync for role ${roleId}`); return; } try { // TODO: 调用 HttpUnit.ins.api 上传到服务器 // await HttpUnit.ins.api("chat/save_history", { // role_id: roleId, // messages: history // }, "POST"); logger.log( `Ready to sync ${history.length} messages for role ${roleId} to remote server` ); } catch (error) { logger.error(`Failed to sync history for role ${roleId}:`, error); } } /** * 获取指定角色的最后聊天时间 * @param roleId 角色ID * @returns 最后聊天时间戳,如果没有历史记录则返回null */ public getLastChatTime(roleId: number): number | null { const key = `chat_history_${roleId}`; try { const data = sys.localStorage.getItem(key); if (data) { const history: ChatHistory = JSON.parse(data); if (history.messages && history.messages.length > 0) { // 获取最后一条消息的时间戳 const lastMessage = history.messages[history.messages.length - 1]; return lastMessage.timestamp || history.updatedAt; } // 如果有历史但没有消息,返回创建时间 return history.createdAt; } } catch (error) { logger.error(`Failed to get last chat time for role ${roleId}:`, error); } return null; } /** * 从远程服务器下载历史(预留接口) * @param roleId 角色ID */ public async syncFromRemote(roleId: number): Promise { try { // TODO: 调用 HttpUnit.ins.api 从服务器获取历史 // const response = await HttpUnit.ins.api("chat/get_history", { // role_id: roleId // }, "GET"); // if (response && response.messages) { // this.saveHistory(roleId, response.messages); // logger.log(`Synced ${response.messages.length} messages from remote for role ${roleId}`); // } logger.log( `Ready to sync history from remote server for role ${roleId}` ); } catch (error) { logger.error(`Failed to sync from remote for role ${roleId}:`, error); } } /** * 转换服务端聊天数据为本地格式 * @param serverMessages 服务端聊天消息数组 * @returns 本地格式的聊天消息数组 */ private convertServerDataToLocalFormat( serverMessages: proto.cs.IChatMsg[] ): ChatMessage[] { if (!serverMessages || serverMessages.length === 0) { return []; } const messages: ChatMessage[] = []; for (const serverMsg of serverMessages) { if (serverMsg.msg) { const message: ChatMessage = { role: serverMsg.isAi ? "model" : "user", parts: [{ text: serverMsg.msg }], timestamp: Number(serverMsg.msgId) || Date.now(), }; messages.push(message); } } // 按时间戳排序,确保消息顺序正确 messages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); logger.log( `Converted ${serverMessages.length} server messages to ${messages.length} local messages` ); return messages; } /** * 从服务端获取聊天记录 * @param roleId 角色ID * @param page 页码 * @param limit 每页数量 * @returns 是否成功获取数据 */ private async fetchServerChatHistory( roleId: number, page: number = 1, limit: number = 20 ): Promise { try { const reqData = { GirlId: roleId, page, limit, }; logger.log("获取服务端聊天记录:", reqData); const res = await ChatService.I.reqGetChatMsg(reqData); if (res && res.code === proto.cs.EnmRetCode.SUCCESS) { const resData = res.data; // 保存到 GirlData const girlData = DataManager.I.getDataById(DataId.Girl); const category = girlData.getGrilCategoryById(roleId); girlData.setChatRecord(category.toString(), roleId, resData.msgs); girlData.setChatTotalCount( category.toString(), roleId, resData.chatTotalCount ); girlData.setChatRemainCount( category.toString(), roleId, resData.chatRemainCount ); logger.log( `成功从服务端获取聊天记录,roleId: ${roleId}, 消息数: ${ resData.msgs?.length || 0 }` ); return true; } else { logger.warn( `从服务端获取聊天记录失败,roleId: ${roleId}, code: ${res?.code}` ); return false; } } catch (error) { logger.error(`获取服务端聊天记录异常,roleId: ${roleId}:`, error); return false; } } /** * 合并本地和服务端聊天记录 * @param localMessages 本地消息 * @param serverMessages 服务端消息 * @returns 合并后的消息数组 */ private mergeHistories( localMessages: ChatMessage[], serverMessages: ChatMessage[] ): ChatMessage[] { if (!localMessages || localMessages.length === 0) { return serverMessages || []; } if (!serverMessages || serverMessages.length === 0) { return localMessages; } // 使用 Map 进行去重,以时间戳为键 const messageMap = new Map(); // 先添加本地消息(优先级更高) localMessages.forEach((msg) => { if (msg.timestamp) { messageMap.set(msg.timestamp, msg); } }); // 添加服务端消息(如果时间戳不冲突) serverMessages.forEach((msg) => { if (msg.timestamp && !messageMap.has(msg.timestamp)) { messageMap.set(msg.timestamp, msg); } }); // 转换回数组并排序 const mergedMessages = Array.from(messageMap.values()); mergedMessages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); logger.log( `合并聊天记录: 本地 ${localMessages.length} 条, 服务端 ${serverMessages.length} 条, 合并后 ${mergedMessages.length} 条` ); return mergedMessages; } /** * 整合的聊天记录加载方法:优先加载本地数据,如果本地没有再获取服务端数据 * @param roleId 角色ID * @returns 聊天消息数组 */ public async loadChatHistory(roleId: number): Promise { // 1. 首先尝试从本地加载 const localHistory = this.loadHistory(roleId); // 2. 如果本地有数据,直接返回 if (localHistory && localHistory.length > 0) { logger.log( `使用本地聊天记录,roleId: ${roleId}, 消息数: ${localHistory.length}` ); return localHistory; } // 3. 本地没有数据,尝试从服务端获取 logger.log(`本地无聊天记录,尝试从服务端获取,roleId: ${roleId}`); const serverSuccess = await this.fetchServerChatHistory(roleId); if (!serverSuccess) { logger.log(`服务端获取失败,返回空记录,roleId: ${roleId}`); return []; } // 4. 从 GirlData 读取服务端数据并转换格式 const girlData = DataManager.I.getDataById(DataId.Girl); const category = girlData.getGrilCategoryById(roleId); const serverMsgIds = girlData.getChatRecordIds(category.toString(), roleId); const serverMessages: proto.cs.IChatMsg[] = []; for (const id of serverMsgIds) { const isAi = girlData.getChatRecordIsAi(category.toString(), roleId, id); const msg = girlData.getChatRecordMsg(category.toString(), roleId, id); if (msg) { serverMessages.push({ msgId: id.toString(), msg: msg, isAi: isAi, }); } } // 5. 转换服务端数据格式 const convertedMessages = this.convertServerDataToLocalFormat(serverMessages); // 6. 保存到本地存储 if (convertedMessages.length > 0) { this.saveHistory(roleId, convertedMessages); logger.log( `从服务端获取并保存聊天记录,roleId: ${roleId}, 消息数: ${convertedMessages.length}` ); } return convertedMessages; } }