feat: add error log selection and download functionality with UI updates and internationalization support

This commit is contained in:
Supra4E8C
2025-12-03 00:49:27 +08:00
parent c77527cd13
commit fc8b02f58e
4 changed files with 193 additions and 11 deletions

7
app.js
View File

@@ -14,7 +14,7 @@ import { aiProvidersModule } from './src/modules/ai-providers.js';
// 工具函数导入 // 工具函数导入
import { escapeHtml } from './src/utils/html.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 { normalizeArrayResponse } from './src/utils/array.js';
import { debounce } from './src/utils/dom.js'; import { debounce } from './src/utils/dom.js';
import { import {
@@ -329,6 +329,7 @@ class CLIProxyManager {
// 日志查看 // 日志查看
const refreshLogs = document.getElementById('refresh-logs'); const refreshLogs = document.getElementById('refresh-logs');
const selectErrorLog = document.getElementById('select-error-log');
const downloadLogs = document.getElementById('download-logs'); const downloadLogs = document.getElementById('download-logs');
const clearLogs = document.getElementById('clear-logs'); const clearLogs = document.getElementById('clear-logs');
const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle'); const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
@@ -336,6 +337,9 @@ class CLIProxyManager {
if (refreshLogs) { if (refreshLogs) {
refreshLogs.addEventListener('click', () => this.refreshLogs()); refreshLogs.addEventListener('click', () => this.refreshLogs());
} }
if (selectErrorLog) {
selectErrorLog.addEventListener('click', () => this.openErrorLogsModal());
}
if (downloadLogs) { if (downloadLogs) {
downloadLogs.addEventListener('click', () => this.downloadLogs()); downloadLogs.addEventListener('click', () => this.downloadLogs());
} }
@@ -705,6 +709,7 @@ Object.assign(
// 将工具函数绑定到原型上,供模块使用 // 将工具函数绑定到原型上,供模块使用
CLIProxyManager.prototype.escapeHtml = escapeHtml; CLIProxyManager.prototype.escapeHtml = escapeHtml;
CLIProxyManager.prototype.maskApiKey = maskApiKey; CLIProxyManager.prototype.maskApiKey = maskApiKey;
CLIProxyManager.prototype.formatFileSize = formatFileSize;
CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse; CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse;
CLIProxyManager.prototype.debounce = debounce; CLIProxyManager.prototype.debounce = debounce;

18
i18n.js
View File

@@ -495,6 +495,15 @@ const i18n = {
'logs.refresh_button': '刷新日志', 'logs.refresh_button': '刷新日志',
'logs.clear_button': '清空日志', 'logs.clear_button': '清空日志',
'logs.download_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_title': '暂无日志记录',
'logs.empty_desc': '当启用"日志记录到文件"功能后,日志将显示在这里', 'logs.empty_desc': '当启用"日志记录到文件"功能后,日志将显示在这里',
'logs.log_content': '日志内容', 'logs.log_content': '日志内容',
@@ -1104,6 +1113,15 @@ const i18n = {
'logs.refresh_button': 'Refresh Logs', 'logs.refresh_button': 'Refresh Logs',
'logs.clear_button': 'Clear Logs', 'logs.clear_button': 'Clear Logs',
'logs.download_button': 'Download 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_title': 'No Logs Available',
'logs.empty_desc': 'When "Enable logging to file" is enabled, logs will be displayed here', 'logs.empty_desc': 'When "Enable logging to file" is enabled, logs will be displayed here',
'logs.log_content': 'Log Content', 'logs.log_content': 'Log Content',

View File

@@ -852,6 +852,9 @@
<button id="refresh-logs" class="btn btn-primary"> <button id="refresh-logs" class="btn btn-primary">
<i class="fas fa-sync-alt"></i> <span data-i18n="logs.refresh_button">刷新日志</span> <i class="fas fa-sync-alt"></i> <span data-i18n="logs.refresh_button">刷新日志</span>
</button> </button>
<button id="select-error-log" class="btn btn-secondary">
<i class="fas fa-file-circle-exclamation"></i> <span data-i18n="logs.error_log_button">选择错误日志</span>
</button>
<button id="download-logs" class="btn btn-secondary"> <button id="download-logs" class="btn btn-secondary">
<i class="fas fa-download"></i> <span data-i18n="logs.download_button">下载日志</span> <i class="fas fa-download"></i> <span data-i18n="logs.download_button">下载日志</span>
</button> </button>

View File

@@ -346,6 +346,162 @@ export const logsModule = {
return null; return null;
}, },
async openErrorLogsModal() {
const modalBody = document.getElementById('modal-body');
if (!modalBody) return;
modalBody.innerHTML = `
<h3>${i18n.t('logs.error_logs_modal_title')}</h3>
<div class="provider-item">
<div class="item-content">
<p class="form-hint">${i18n.t('logs.error_logs_description')}</p>
<div class="loading-placeholder">${i18n.t('common.loading')}</div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.close')}</button>
</div>
`;
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 = `
<h3>${i18n.t('logs.error_logs_modal_title')}</h3>
<div class="provider-item">
<div class="item-content">
<div class="error-state">
<i class="fas fa-exclamation-triangle"></i>
<p>${i18n.t('logs.error_logs_load_error')}</p>
<p>${this.escapeHtml(error.message || '')}</p>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.close')}</button>
</div>
`;
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('')
: `
<div class="empty-state">
<i class="fas fa-inbox"></i>
<h3>${i18n.t('logs.error_logs_empty')}</h3>
<p>${i18n.t('logs.error_logs_description')}</p>
</div>
`;
return `
<h3>${i18n.t('logs.error_logs_modal_title')}</h3>
<p class="form-hint">${i18n.t('logs.error_logs_description')}</p>
<div class="provider-list">
${listHtml}
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.close')}</button>
</div>
`;
},
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 `
<div class="provider-item">
<div class="item-content">
<div class="item-title">${this.escapeHtml(name)}</div>
<div class="item-subtitle">${i18n.t('logs.error_logs_size')}: ${this.escapeHtml(size)}</div>
<div class="item-subtitle">${i18n.t('logs.error_logs_modified')}: ${this.escapeHtml(modified)}</div>
</div>
<div class="item-actions">
<button class="btn btn-secondary error-log-download-btn" data-log-name="${this.escapeHtml(name)}">
<i class="fas fa-download"></i> ${i18n.t('logs.error_logs_download')}
</button>
</div>
</div>
`;
},
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() { async downloadLogs() {
try { try {
const response = await this.makeRequest('/logs', { const response = await this.makeRequest('/logs', {