diff --git a/app.js b/app.js index 794db62..04598fd 100644 --- a/app.js +++ b/app.js @@ -14,7 +14,7 @@ import { aiProvidersModule } from './src/modules/ai-providers.js'; // 工具函数导入 import { escapeHtml } from './src/utils/html.js'; -import { maskApiKey } from './src/utils/string.js'; +import { maskApiKey, formatFileSize } from './src/utils/string.js'; import { normalizeArrayResponse } from './src/utils/array.js'; import { debounce } from './src/utils/dom.js'; import { @@ -329,6 +329,7 @@ class CLIProxyManager { // 日志查看 const refreshLogs = document.getElementById('refresh-logs'); + const selectErrorLog = document.getElementById('select-error-log'); const downloadLogs = document.getElementById('download-logs'); const clearLogs = document.getElementById('clear-logs'); const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle'); @@ -336,6 +337,9 @@ class CLIProxyManager { if (refreshLogs) { refreshLogs.addEventListener('click', () => this.refreshLogs()); } + if (selectErrorLog) { + selectErrorLog.addEventListener('click', () => this.openErrorLogsModal()); + } if (downloadLogs) { downloadLogs.addEventListener('click', () => this.downloadLogs()); } @@ -705,6 +709,7 @@ Object.assign( // 将工具函数绑定到原型上,供模块使用 CLIProxyManager.prototype.escapeHtml = escapeHtml; CLIProxyManager.prototype.maskApiKey = maskApiKey; +CLIProxyManager.prototype.formatFileSize = formatFileSize; CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse; CLIProxyManager.prototype.debounce = debounce; diff --git a/i18n.js b/i18n.js index f71f7fb..0a5f0f1 100644 --- a/i18n.js +++ b/i18n.js @@ -495,6 +495,15 @@ const i18n = { 'logs.refresh_button': '刷新日志', 'logs.clear_button': '清空日志', 'logs.download_button': '下载日志', + 'logs.error_log_button': '选择错误日志', + 'logs.error_logs_modal_title': '错误请求日志', + 'logs.error_logs_description': '请选择要下载的错误请求日志文件(仅在关闭请求日志时生成)。', + 'logs.error_logs_empty': '暂无错误请求日志文件', + 'logs.error_logs_load_error': '加载错误日志列表失败', + 'logs.error_logs_size': '大小', + 'logs.error_logs_modified': '最后修改', + 'logs.error_logs_download': '下载', + 'logs.error_log_download_success': '错误日志下载成功', 'logs.empty_title': '暂无日志记录', 'logs.empty_desc': '当启用"日志记录到文件"功能后,日志将显示在这里', 'logs.log_content': '日志内容', @@ -1104,6 +1113,15 @@ const i18n = { 'logs.refresh_button': 'Refresh Logs', 'logs.clear_button': 'Clear Logs', 'logs.download_button': 'Download Logs', + 'logs.error_log_button': 'Select Error Log', + 'logs.error_logs_modal_title': 'Error Request Logs', + 'logs.error_logs_description': 'Pick an error request log file to download (only generated when request logging is off).', + 'logs.error_logs_empty': 'No error request log files found', + 'logs.error_logs_load_error': 'Failed to load error log list', + 'logs.error_logs_size': 'Size', + 'logs.error_logs_modified': 'Last modified', + 'logs.error_logs_download': 'Download', + 'logs.error_log_download_success': 'Error log downloaded successfully', 'logs.empty_title': 'No Logs Available', 'logs.empty_desc': 'When "Enable logging to file" is enabled, logs will be displayed here', 'logs.log_content': 'Log Content', diff --git a/index.html b/index.html index 06494d6..5c66ed6 100644 --- a/index.html +++ b/index.html @@ -847,16 +847,19 @@ - 自动刷新 - - - - + + + diff --git a/src/modules/logs.js b/src/modules/logs.js index 0c8ed6b..2c55a11 100644 --- a/src/modules/logs.js +++ b/src/modules/logs.js @@ -346,6 +346,162 @@ export const logsModule = { return null; }, + async openErrorLogsModal() { + const modalBody = document.getElementById('modal-body'); + if (!modalBody) return; + + modalBody.innerHTML = ` +

${i18n.t('logs.error_logs_modal_title')}

+
+
+

${i18n.t('logs.error_logs_description')}

+
${i18n.t('common.loading')}
+
+
+ + `; + this.showModal(); + + try { + const response = await this.makeRequest('/request-error-logs', { + method: 'GET' + }); + const files = Array.isArray(response?.files) ? response.files.slice() : []; + if (files.length > 1) { + files.sort((a, b) => (b.modified || 0) - (a.modified || 0)); + } + modalBody.innerHTML = this.buildErrorLogsModal(files); + this.showModal(); + this.bindErrorLogDownloadButtons(); + } catch (error) { + console.error('加载错误日志列表失败:', error); + modalBody.innerHTML = ` +

${i18n.t('logs.error_logs_modal_title')}

+
+
+
+ +

${i18n.t('logs.error_logs_load_error')}

+

${this.escapeHtml(error.message || '')}

+
+
+
+ + `; + this.showNotification(`${i18n.t('logs.error_logs_load_error')}: ${error.message}`, 'error'); + } + }, + + buildErrorLogsModal(files) { + const listHtml = Array.isArray(files) && files.length > 0 + ? files.map(file => this.buildErrorLogCard(file)).join('') + : ` +
+ +

${i18n.t('logs.error_logs_empty')}

+

${i18n.t('logs.error_logs_description')}

+
+ `; + + return ` +

${i18n.t('logs.error_logs_modal_title')}

+

${i18n.t('logs.error_logs_description')}

+
+ ${listHtml} +
+ + `; + }, + + buildErrorLogCard(file) { + const name = file?.name || ''; + const size = typeof file?.size === 'number' ? this.formatFileSize(file.size) : '-'; + const modified = file?.modified ? this.formatErrorLogTime(file.modified) : '-'; + return ` +
+
+
${this.escapeHtml(name)}
+
${i18n.t('logs.error_logs_size')}: ${this.escapeHtml(size)}
+
${i18n.t('logs.error_logs_modified')}: ${this.escapeHtml(modified)}
+
+
+ +
+
+ `; + }, + + bindErrorLogDownloadButtons() { + const modalBody = document.getElementById('modal-body'); + if (!modalBody) return; + const buttons = modalBody.querySelectorAll('.error-log-download-btn'); + buttons.forEach(button => { + button.onclick = () => { + const filename = button.getAttribute('data-log-name'); + if (filename) { + this.downloadErrorLog(filename); + } + }; + }); + }, + + formatErrorLogTime(timestamp) { + const numeric = Number(timestamp); + if (!Number.isFinite(numeric) || numeric <= 0) { + return '-'; + } + const date = new Date(numeric * 1000); + if (Number.isNaN(date.getTime())) { + return '-'; + } + const locale = i18n?.currentLanguage || undefined; + return date.toLocaleString(locale); + }, + + async downloadErrorLog(filename) { + if (!filename) return; + try { + const response = await this.apiClient.requestRaw(`/request-error-logs/${encodeURIComponent(filename)}`, { + method: 'GET' + }); + + if (!response.ok) { + let errorMessage = `HTTP ${response.status}`; + try { + const errorData = await response.json(); + if (errorData && errorData.error) { + errorMessage = errorData.error; + } + } catch (parseError) { + // ignore JSON parse error and use default message + } + throw new Error(errorMessage); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + this.showNotification(i18n.t('logs.error_log_download_success'), 'success'); + } catch (error) { + console.error('下载错误日志失败:', error); + this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error'); + } + }, + async downloadLogs() { try { const response = await this.makeRequest('/logs', {