// static/js/app.js import * as api from './api.js'; // 导入API模块 import * as ui from './ui_manager.js'; // 导入UI管理模块 let currentSession = {}; // 全局变量,存储当前会话信息 let currentRoles = []; // 全局变量,存储当前角色列表 let currentMemories = []; // 全局变量,存储当前记忆集列表 // 初始化应用程序。 // 加载所有初始数据并设置事件监听器。 async function initializeApp() { console.log("应用程序初始化开始..."); ui.initializeUIElements(); // 恢复窗口宽度设置 const savedWidth = localStorage.getItem('chatWindowWidth'); if (savedWidth === 'full') { const mainContent = document.querySelector('.main-content'); const container = document.querySelector('.container'); mainContent.classList.add('full-width'); container.classList.add('full-width'); // 更新按钮图标 const icon = ui.Elements.toggleWidthBtn.querySelector('i'); icon.classList.remove('fa-expand-alt'); icon.classList.add('fa-compress-alt'); } // 设置导航菜单点击事件 ui.Elements.navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); ui.showSection(item.dataset.target); // 切换section时刷新数据 if (item.dataset.target === 'config-section') loadConfigAndModels(); if (item.dataset.target === 'features-section') loadFeaturesAndRoles(); if (item.dataset.target === 'memory-section') loadMemoryAndMemories(); if (item.dataset.target === 'log-section') loadLogs(); }); }); // 加载初始会话信息、角色和记忆列表 await loadInitialData(); // 设置聊天区域事件监听 ui.Elements.sendBtn.addEventListener('click', sendMessageHandler); ui.Elements.userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); // 阻止默认换行 sendMessageHandler(); } }); ui.Elements.clearChatHistoryBtn.addEventListener('click', clearChatLogHandler); ui.Elements.toggleWidthBtn.addEventListener('click', toggleChatWindowWidthHandler); // 设置配置区域事件监听 ui.Elements.saveConfigBtn.addEventListener('click', saveConfigHandler); ui.Elements.showApiKeyCheckbox.addEventListener('change', (e) => { ui.Elements.geminiApiKeyInput.type = e.target.checked ? 'text' : 'password'; }); // 设置特征区域事件监听 ui.Elements.refreshFeaturesBtn.addEventListener('click', loadFeaturesAndRoles); ui.Elements.saveFeaturesBtn.addEventListener('click', saveFeaturesContentHandler); ui.Elements.createRoleBtn.addEventListener('click', createRoleHandler); // 设置记忆区域事件监听 ui.Elements.refreshMemoryBtn.addEventListener('click', loadMemoryAndMemories); ui.Elements.saveMemoryBtn.addEventListener('click', saveMemoryContentHandler); ui.Elements.triggerMemoryUpdateBtn.addEventListener('click', triggerMemoryUpdateHandler); ui.Elements.createMemoryBtn.addEventListener('click', createMemoryHandler); // 设置日志区域事件监听 ui.Elements.refreshLogBtn.addEventListener('click', loadLogs); // 默认显示聊天界面并加载历史 ui.showSection('chat-section'); await loadChatLog(); console.log("应用程序初始化完毕。"); } // 加载初始数据:会话信息、角色列表、记忆集列表。 async function loadInitialData() { try { currentSession = await api.fetchActiveSession(); currentRoles = await api.fetchRoles(); // 赋值给全局变量 currentMemories = await api.fetchMemories(); // 赋值给全局变量 ui.updateSessionInfo(currentSession, currentRoles, currentMemories); ui.setMemoryUpdateStatus(currentSession.memory_status); console.log("初始数据加载完成。", currentSession); } catch (error) { ui.showToast(`加载初始数据失败: ${error.message}`, 'error'); console.error("加载初始数据失败:", error); } } // 加载并渲染配置和模型列表。 async function loadConfigAndModels() { try { const config = await api.fetchConfig(); ui.renderConfigForm(config); // fetchModels 现在直接通过后端代理获取模型列表,不再需要 baseUrl 参数 const models = await api.fetchModels(); ui.populateModelSelects(models, config.API.DEFAULT_GEMINI_MODEL, config.API.MEMORY_UPDATE_MODEL); console.log("配置和模型列表加载完成。"); } catch (error) { ui.showToast(`加载配置失败: ${error.message}`, 'error'); console.error("加载配置失败:", error); } } // 保存配置处理函数。 async function saveConfigHandler() { const configData = { API: { GEMINI_API_BASE_URL: document.getElementById('gemini-api-base-url').value, GEMINI_API_KEY: ui.Elements.geminiApiKeyInput.value, DEFAULT_GEMINI_MODEL: ui.Elements.defaultGeminiModelSelect.options[ui.Elements.defaultGeminiModelSelect.selectedIndex].value, MEMORY_UPDATE_MODEL: ui.Elements.memoryUpdateModelSelect.options[ui.Elements.memoryUpdateModelSelect.selectedIndex].value, }, Application: { CONTEXT_WINDOW_SIZE: parseInt(document.getElementById('context-window-size').value), MEMORY_RETENTION_TURNS: parseInt(document.getElementById('memory-retention-turns').value), MAX_SHORT_TERM_EVENTS: parseInt(document.getElementById('max-short-term-events').value), } }; ui.toggleLoadingState(ui.Elements.saveConfigBtn, true); try { const response = await api.saveConfig(configData); ui.showToast(response.message, 'success'); await loadConfigAndModels(); // 重新加载以确保UI同步 } catch (error) { ui.showToast(`保存配置失败: ${error.message}`, 'error'); } finally { ui.toggleLoadingState(ui.Elements.saveConfigBtn, false); } } // 加载并渲染聊天记录。 async function loadChatLog() { try { const chatLog = await api.fetchChatLog(); // 确保聊天消息的 role 字段从后端返回的 'ai' 转换为 'bot' // 并且 content 字段在渲染前经过 DOMPurify 清理和 marked 解析 ui.renderChatHistory(chatLog.map(msg => ({ id: msg.id, // 传递消息ID role: msg.role === 'user' ? 'user' : 'bot', content: msg.content, timestamp: msg.timestamp }))); console.log("聊天记录加载完成。"); } catch (error) { ui.showToast(`加载聊天记录失败: ${error.message}`, 'error'); console.error("加载聊天记录失败:", error); } } // 清空聊天记录处理函数。 async function clearChatLogHandler() { ui.showModal( "清空聊天记录", "确定要清空当前聊天记录吗?此操作不可逆。", async () => { ui.toggleLoadingState(ui.Elements.clearChatHistoryBtn, true); try { const response = await api.clearChatLog(); ui.showToast(response.message, 'success'); ui.renderChatHistory([]); // 清空UI currentSession.turn_counter = 0; // 重置轮次计数器 ui.updateSessionInfo(currentSession); triggerMemoryUpdateHandler(); // 触发记忆更新 console.log("聊天记录清空成功。"); } catch (error) { ui.showToast(`清空聊天记录失败: ${error.message}`, 'error'); console.error("清空聊天记录失败:", error); } finally { ui.toggleLoadingState(ui.Elements.clearChatHistoryBtn, false); } }); } // 发送消息处理函数。 async function sendMessageHandler() { const message = ui.Elements.userInput.value.trim(); if (!message) { return; } // 为用户消息生成一个唯一的ID const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; ui.addChatMessage('user', message, new Date().toISOString(), userMessageId); ui.Elements.userInput.value = ''; // 清空输入框 // 禁用发送按钮和输入框 ui.toggleLoadingState(ui.Elements.sendBtn, true); ui.toggleLoadingState(ui.Elements.userInput, true); // 添加一个临时的"AI 正在思考..."消息,并生成一个唯一的ID const thinkingMessageId = `bot-thinking-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; ui.addChatMessage('bot', 'AI 正在思考...', new Date().toISOString(), thinkingMessageId); // 设置重试参数 const maxRetries = 3; // 最大重试次数 const retryDelay = 1000; // 重试间隔(毫秒) let retryCount = 0; let success = false; while (retryCount < maxRetries && !success) { try { if (retryCount > 0) { // 如果是重试,更新思考消息 ui.updateChatMessageContent(thinkingMessageId, `AI 正在思考...(第${retryCount}次重试)`); await new Promise(resolve => setTimeout(resolve, retryDelay)); // 等待一段时间再重试 } console.log("正在发送消息...", retryCount > 0 ? `(第${retryCount}次重试)` : ""); // 检查是否支持流式响应 // 使用流式响应方式发送消息 try { const response = await api.sendMessage(message, true); // 添加第二个参数表示使用流式响应 // 标记请求已成功 success = true; // 初始化流式响应处理 let fullResponse = ""; let isFirstChunk = true; // 为响应创建一个永久的消息ID,替换思考消息 const permanentBotMessageId = `bot-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // 处理流式响应的每个块 for await (const textChunk of response) { if (textChunk) { console.log('接收到文本块:', textChunk.substring(0, 50) + '...'); if (isFirstChunk) { // 第一次收到数据时,替换思考消息为新的消息 ui.removeChatMessage(thinkingMessageId); fullResponse = textChunk; // 初始化 fullResponse ui.addChatMessage('bot', fullResponse, new Date().toISOString(), permanentBotMessageId); isFirstChunk = false; } else { // 后续数据,累加并更新现有消息 fullResponse += textChunk; ui.updateChatMessageContent(permanentBotMessageId, fullResponse); } } } // 流结束后,确保界面更新 if (fullResponse && !isFirstChunk) { ui.updateChatMessageContent(permanentBotMessageId, fullResponse); } else if (isFirstChunk) { // 如果没有收到任何内容 ui.removeChatMessage(thinkingMessageId); ui.addChatMessage('bot', '抱歉,未收到AI助手的响应,请重试。', new Date().toISOString(), permanentBotMessageId); } // 流式响应结束后,重新获取会话信息以更新 turn_counter 和 memory_status currentSession = await api.fetchActiveSession(); ui.updateSessionInfo(currentSession, currentRoles, currentMemories); triggerMemoryUpdateHandler(); // 触发记忆更新 } catch (streamError) { console.warn("流式请求失败,回退到标准请求:", streamError); // 回退到标准请求 const response = await api.sendMessage(message, false); if (response && response.success) { console.log("消息发送成功,收到响应:", response); // 成功时,更新临时消息为 AI 的实际回复 ui.updateChatMessageContent(thinkingMessageId, response.response); // 更新会话信息 currentSession.turn_counter = response.turn_counter; currentSession.memory_status = response.memory_status; // 更新记忆状态 ui.updateSessionInfo(currentSession, currentRoles, currentMemories); success = true; triggerMemoryUpdateHandler(); // 触发记忆更新 } else if (response) { console.warn("API返回成功但内容表示失败:", response); throw new Error(response.message || '发送消息失败'); } else { throw new Error('服务器未返回有效响应'); } } } catch (error) { console.error(`发送消息时发生错误 (尝试 ${retryCount + 1}/${maxRetries}):`, error); retryCount++; // 如果是最后一次尝试仍然失败 if (retryCount >= maxRetries && !success) { // 移除思考消息并显示错误提示 ui.removeChatMessage(thinkingMessageId); ui.showToast(`发送消息失败,已尝试 ${maxRetries} 次。请稍后再试。`, 'error'); } } } // 重新启用发送按钮和输入框 ui.toggleLoadingState(ui.Elements.sendBtn, false); ui.toggleLoadingState(ui.Elements.userInput, false); } // 加载并渲染特征内容和角色列表。 async function loadFeaturesAndRoles() { try { const roles = await api.fetchRoles(); const featuresContent = await api.fetchFeaturesContent(); ui.renderFeaturesContent(featuresContent); ui.renderRoleList(roles, currentSession.role_id, switchRoleHandler, deleteRoleHandler); console.log("特征和角色列表加载完成。"); } catch (error) { ui.showToast(`加载特征或角色失败: ${error.message}`, 'error'); console.error("加载特征或角色失败:", error); } } // 保存特征内容处理函数。 async function saveFeaturesContentHandler() { let content; try { content = JSON.parse(ui.Elements.featuresContentTextarea.value); } catch (error) { ui.showToast(`特征内容格式错误,请检查 JSON 格式: ${error.message}`, 'error'); console.error("解析特征内容失败:", error); return; } ui.toggleLoadingState(ui.Elements.saveFeaturesBtn, true); try { const response = await api.saveFeaturesContent(content); ui.showToast(response.message, 'success'); } catch (error) { ui.showToast(`保存特征内容失败: ${error.message}`, 'error'); console.error("保存特征内容失败:", error); } finally { ui.toggleLoadingState(ui.Elements.saveFeaturesBtn, false); } } // 创建新角色处理函数。 async function createRoleHandler() { const roleId = ui.Elements.newRoleIdInput.value.trim(); const roleName = ui.Elements.newRoleNameInput.value.trim(); if (!roleId || !roleName) { ui.showToast("角色ID和名称不能为空。", 'warning'); return; } ui.toggleLoadingState(ui.Elements.createRoleBtn, true); try { const response = await api.createRole(roleId, roleName); ui.showToast(response.message, 'success'); ui.Elements.newRoleIdInput.value = ''; ui.Elements.newRoleNameInput.value = ''; await loadFeaturesAndRoles(); // 刷新列表 } catch (error) { ui.showToast(`创建角色失败: ${error.message}`, 'error'); } finally { ui.toggleLoadingState(ui.Elements.createRoleBtn, false); } } // 切换角色处理函数。 // @param { string } roleId 要切换到的角色ID。 async function switchRoleHandler(roleId) { if (roleId === currentSession.role_id) { ui.showToast("当前角色已是此角色。", 'info'); return; } ui.showModal( "切换角色", `确定要切换到角色 "${roleId}" 吗?这会重置当前对话。`, async (confirmBtn) => { // 传入确认按钮元素 ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 try { const response = await api.setActiveSession(roleId, currentSession.memory_id); currentSession = response; await loadInitialData(); await loadFeaturesAndRoles(); await loadChatLog(); triggerMemoryUpdateHandler(); // 触发记忆更新 ui.showToast(`已切换到角色 "${roleId}"`, 'success'); } catch (error) { ui.showToast(`切换角色失败: ${error.message}`, 'error'); console.error("切换角色失败:", error); } finally { ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 } } ); } // 删除角色处理函数。 // @param { string } roleId 要删除的角色ID。 async function deleteRoleHandler(roleId) { if (roleId === currentSession.role_id) { ui.showToast("不能删除当前活跃的角色!", 'error'); return; } ui.showModal( "删除角色", `确定要删除角色 "${roleId}" 吗?此操作可逆。`, async (confirmBtn) => { // 传入确认按钮元素 ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 try { const response = await api.deleteRole(roleId); ui.showToast(response.message, 'success'); await loadFeaturesAndRoles(); // 刷新列表 } catch (error) { ui.showToast(`删除角色失败: ${error.message}`, 'error'); } finally { ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 } } ); } // 加载并渲染记忆内容和记忆集列表。 async function loadMemoryAndMemories() { try { const memories = await api.fetchMemories(); const memoryContent = await api.fetchMemoryContent(); ui.renderMemoryContent(memoryContent); ui.renderMemoryList(memories, currentSession.memory_id, switchMemoryHandler, deleteMemoryHandler); console.log("记忆内容和记忆集列表加载完成。"); } catch (error) { ui.showToast(`加载记忆或记忆集失败: ${error.message}`, 'error'); console.error("加载记忆或记忆集失败:", error); } } // 保存记忆内容处理函数。 async function saveMemoryContentHandler() { let content; try { content = JSON.parse(ui.Elements.memoryContentTextarea.value); } catch (error) { ui.showToast(`记忆内容格式错误,请检查 JSON 格式: ${error.message}`, 'error'); console.error("解析记忆内容失败:", error); return; } ui.toggleLoadingState(ui.Elements.saveMemoryBtn, true); try { const response = await api.saveMemoryContent(content); ui.showToast(response.message, 'success'); } catch (error) { ui.showToast(`保存记忆内容失败: ${error.message}`, 'error'); console.error("保存记忆内容失败:", error); } finally { ui.toggleLoadingState(ui.Elements.saveMemoryBtn, false); } } // 创建新记忆集处理函数。 async function createMemoryHandler() { const memoryId = ui.Elements.newMemoryIdInput.value.trim(); const memoryName = ui.Elements.newMemoryNameInput.value.trim(); if (!memoryId || !memoryName) { ui.showToast("记忆集ID和名称不能为空。", 'warning'); return; } ui.toggleLoadingState(ui.Elements.createMemoryBtn, true); try { const response = await api.createMemory(memoryId, memoryName); ui.showToast(response.message, 'success'); ui.Elements.newMemoryIdInput.value = ''; ui.Elements.newMemoryNameInput.value = ''; await loadMemoryAndMemories(); // 刷新列表 } catch (error) { ui.showToast(`创建记忆集失败: ${error.message}`, 'error'); } finally { ui.toggleLoadingState(ui.Elements.createMemoryBtn, false); } } // 切换记忆集处理函数。 // @param { string } memoryId 要切换到的记忆集ID。 async function switchMemoryHandler(memoryId) { if (memoryId === currentSession.memory_id) { ui.showToast("当前记忆集是此记忆集。", 'info'); return; } ui.showModal( "切换记忆集", `确定要切换到记忆集 "${memoryId}" 吗?这会重置当前对话。`, async (confirmBtn) => { // 传入确认按钮元素 ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 try { const response = await api.setActiveSession(currentSession.role_id, memoryId); currentSession = response; await loadInitialData(); await loadMemoryAndMemories(); await loadChatLog(); triggerMemoryUpdateHandler(); // 触发记忆更新 ui.showToast(`已切换到记忆集 "${memoryId}"`, 'success'); } catch (error) { ui.showToast(`切换记忆集失败: ${error.message}`, 'error'); console.error("切换记忆集失败:", error); } finally { ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 } } ); } // 删除记忆集处理函数。 // @param { string } memoryId 要删除的记忆集ID。 async function deleteMemoryHandler(memoryId) { if (memoryId === currentSession.memory_id) { ui.showToast("不能删除当前活跃的记忆集!", 'error'); return; } ui.showModal( "删除记忆集", `确定要删除记忆集 "${memoryId}" 吗?此操作不可逆。`, async (confirmBtn) => { // 传入确认按钮元素 ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 try { const response = await api.deleteMemory(memoryId); ui.showToast(response.message, 'success'); await loadMemoryAndMemories(); // 刷新列表 } catch (error) { ui.showToast(`删除记忆集失败: ${error.message}`, 'error'); } finally { ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 } } ); } // 触发记忆更新处理函数。 async function triggerMemoryUpdateHandler() { ui.toggleLoadingState(ui.Elements.triggerMemoryUpdateBtn, true); ui.setMemoryUpdateStatus('updating'); // 设置为updating状态 ui.Elements.memoryUpdateStatusText.textContent = '记忆整理中 (手动)...'; try { const response = await api.triggerMemoryUpdate(); ui.showToast(response.message, 'info'); // 记忆更新是异步的,触发后需要重新获取会话信息来更新状态 currentSession = await api.fetchActiveSession(); ui.setMemoryUpdateStatus(currentSession.memory_status); } catch (error) { ui.showToast(`触发记忆更新失败: ${error.message}`, 'error'); console.error("触发记忆更新失败:", error); ui.setMemoryUpdateStatus('error'); // 触发失败则显示错误状态 } finally { ui.toggleLoadingState(ui.Elements.triggerMemoryUpdateBtn, false); } } // 加载并渲染日志内容。 async function loadLogs() { try { const logContent = await api.fetchLogs(); // 通过API获取日志 ui.renderLogContent(logContent); console.log("日志加载完成。"); } catch (error) { ui.showToast(`加载日志失败: ${error.message}`, 'error'); console.error("加载日志失败:", error); ui.renderLogContent(`加载日志失败: ${error.message}`); } } // 切换聊天窗口宽度处理函数 function toggleChatWindowWidthHandler() { const mainContent = document.querySelector('.main-content'); const container = document.querySelector('.container'); // 同时切换主内容区和容器的全宽类 mainContent.classList.toggle('full-width'); container.classList.toggle('full-width'); // 切换按钮图标 const icon = ui.Elements.toggleWidthBtn.querySelector('i'); if (mainContent.classList.contains('full-width')) { icon.classList.remove('fa-expand-alt'); icon.classList.add('fa-compress-alt'); localStorage.setItem('chatWindowWidth', 'full'); } else { icon.classList.remove('fa-compress-alt'); icon.classList.add('fa-expand-alt'); localStorage.setItem('chatWindowWidth', 'default'); } } // 应用程序启动 document.addEventListener('DOMContentLoaded', initializeApp);