From 8b929b58dde7dba37bc11c7ce2ce4df877892774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=B7=B2=E6=B3=A8=E9=94=80?= Date: Fri, 6 Jun 2025 16:52:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20static/js/app.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/{ => js}/app.js | 1198 ++++++++++++++++++++-------------------- 1 file changed, 599 insertions(+), 599 deletions(-) rename static/{ => js}/app.js (97%) diff --git a/static/app.js b/static/js/app.js similarity index 97% rename from static/app.js rename to static/js/app.js index bbe12b9..b5ffbf4 100644 --- a/static/app.js +++ b/static/js/app.js @@ -1,599 +1,599 @@ -// 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); +// 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);