mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat: add error log selection and download functionality with UI updates and internationalization support
This commit is contained in:
7
app.js
7
app.js
@@ -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
18
i18n.js
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
Reference in New Issue
Block a user