From f2dc4bcf988ee55b12e12253f902f5b38e634d8c Mon Sep 17 00:00:00 2001 From: Supra4E8C <69194597+LTbinglingfeng@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:04:02 +0800 Subject: [PATCH] Update 0.0.3Beta --- app.js | 363 +++++++++++++++++++++++++++++++++++++++++++++++++++++ i18n.js | 42 +++++++ index.html | 110 ++++++++++++++++ styles.css | 203 ++++++++++++++++++++++++++++++ 4 files changed, 718 insertions(+) diff --git a/app.js b/app.js index 66522ec..c38dbe2 100644 --- a/app.js +++ b/app.js @@ -607,6 +607,29 @@ class CLIProxyManager { authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); } + // 使用统计 + const refreshUsageStats = document.getElementById('refresh-usage-stats'); + const requestsHourBtn = document.getElementById('requests-hour-btn'); + const requestsDayBtn = document.getElementById('requests-day-btn'); + const tokensHourBtn = document.getElementById('tokens-hour-btn'); + const tokensDayBtn = document.getElementById('tokens-day-btn'); + + if (refreshUsageStats) { + refreshUsageStats.addEventListener('click', () => this.loadUsageStats()); + } + if (requestsHourBtn) { + requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour')); + } + if (requestsDayBtn) { + requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day')); + } + if (tokensHourBtn) { + tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour')); + } + if (tokensDayBtn) { + tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day')); + } + // 模态框 const closeBtn = document.querySelector('.close'); if (closeBtn) { @@ -939,6 +962,9 @@ class CLIProxyManager { // 认证文件需要单独加载,因为不在配置中 await this.loadAuthFiles(); + // 使用统计需要单独加载 + await this.loadUsageStats(); + console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); } catch (error) { console.error('加载配置失败:', error); @@ -2275,6 +2301,343 @@ class CLIProxyManager { } } + // ===== 使用统计相关方法 ===== + + // 初始化图表变量 + requestsChart = null; + tokensChart = null; + currentUsageData = null; + + // 加载使用统计 + async loadUsageStats() { + try { + const response = await this.makeRequest('/usage'); + const usage = response?.usage || null; + this.currentUsageData = usage; + + if (!usage) { + throw new Error('usage payload missing'); + } + + // 更新概览卡片 + this.updateUsageOverview(usage); + + // 读取当前图表周期 + const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); + const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); + const requestsPeriod = requestsHourActive ? 'hour' : 'day'; + const tokensPeriod = tokensHourActive ? 'hour' : 'day'; + + // 初始化图表(使用当前周期) + this.initializeRequestsChart(requestsPeriod); + this.initializeTokensChart(tokensPeriod); + + // 更新API详细统计表格 + this.updateApiStatsTable(usage); + + } catch (error) { + console.error('加载使用统计失败:', error); + this.currentUsageData = null; + + // 清空概览数据 + ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = '-'; + }); + + // 清空图表 + if (this.requestsChart) { + this.requestsChart.destroy(); + this.requestsChart = null; + } + if (this.tokensChart) { + this.tokensChart.destroy(); + this.tokensChart = null; + } + + const tableElement = document.getElementById('api-stats-table'); + if (tableElement) { + tableElement.innerHTML = `
`; + } + } + } + + // 更新使用统计概览 + updateUsageOverview(data) { + const safeData = data || {}; + document.getElementById('total-requests').textContent = safeData.total_requests ?? 0; + document.getElementById('success-requests').textContent = safeData.success_count ?? 0; + document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0; + document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0; + } + + // 初始化图表 + initializeCharts() { + const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); + const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); + this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day'); + this.initializeTokensChart(tokensHourActive ? 'hour' : 'day'); + } + + // 初始化请求趋势图表 + initializeRequestsChart(period = 'day') { + const ctx = document.getElementById('requests-chart'); + if (!ctx) return; + + // 销毁现有图表 + if (this.requestsChart) { + this.requestsChart.destroy(); + } + + const data = this.getRequestsChartData(period); + + this.requestsChart = new Chart(ctx, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + title: { + display: true, + text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: i18n.t('usage_stats.requests_count') + } + } + }, + elements: { + line: { + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + fill: true, + tension: 0.4 + }, + point: { + backgroundColor: '#3b82f6', + borderColor: '#ffffff', + borderWidth: 2, + radius: 4 + } + } + } + }); + } + + // 初始化Token使用趋势图表 + initializeTokensChart(period = 'day') { + const ctx = document.getElementById('tokens-chart'); + if (!ctx) return; + + // 销毁现有图表 + if (this.tokensChart) { + this.tokensChart.destroy(); + } + + const data = this.getTokensChartData(period); + + this.tokensChart = new Chart(ctx, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + title: { + display: true, + text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: i18n.t('usage_stats.tokens_count') + } + } + }, + elements: { + line: { + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + fill: true, + tension: 0.4 + }, + point: { + backgroundColor: '#10b981', + borderColor: '#ffffff', + borderWidth: 2, + radius: 4 + } + } + } + }); + } + + // 获取请求图表数据 + getRequestsChartData(period) { + if (!this.currentUsageData) { + return { labels: [], datasets: [{ data: [] }] }; + } + + let dataSource, labels, values; + + if (period === 'hour') { + dataSource = this.currentUsageData.requests_by_hour || {}; + labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); + values = labels.map(hour => dataSource[hour] || 0); + } else { + dataSource = this.currentUsageData.requests_by_day || {}; + labels = Object.keys(dataSource).sort(); + values = labels.map(day => dataSource[day] || 0); + } + + return { + labels: labels, + datasets: [{ + data: values + }] + }; + } + + // 获取Token图表数据 + getTokensChartData(period) { + if (!this.currentUsageData) { + return { labels: [], datasets: [{ data: [] }] }; + } + + let dataSource, labels, values; + + if (period === 'hour') { + dataSource = this.currentUsageData.tokens_by_hour || {}; + labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); + values = labels.map(hour => dataSource[hour] || 0); + } else { + dataSource = this.currentUsageData.tokens_by_day || {}; + labels = Object.keys(dataSource).sort(); + values = labels.map(day => dataSource[day] || 0); + } + + return { + labels: labels, + datasets: [{ + data: values + }] + }; + } + + // 切换请求图表时间周期 + switchRequestsPeriod(period) { + // 更新按钮状态 + document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour'); + document.getElementById('requests-day-btn').classList.toggle('active', period === 'day'); + + // 更新图表数据 + if (this.requestsChart) { + const newData = this.getRequestsChartData(period); + this.requestsChart.data = newData; + this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); + this.requestsChart.update(); + } + } + + // 切换Token图表时间周期 + switchTokensPeriod(period) { + // 更新按钮状态 + document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour'); + document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day'); + + // 更新图表数据 + if (this.tokensChart) { + const newData = this.getTokensChartData(period); + this.tokensChart.data = newData; + this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); + this.tokensChart.update(); + } + } + + // 更新API详细统计表格 + updateApiStatsTable(data) { + const container = document.getElementById('api-stats-table'); + if (!container) return; + + const apis = data.apis || {}; + + if (Object.keys(apis).length === 0) { + container.innerHTML = ``; + return; + } + + let tableHtml = ` +| ${i18n.t('usage_stats.api_endpoint')} | +${i18n.t('usage_stats.requests_count')} | +${i18n.t('usage_stats.tokens_count')} | +${i18n.t('usage_stats.success_rate')} | +${i18n.t('usage_stats.models')} | +
|---|---|---|---|---|
| ${endpoint} | +${totalRequests} | +${apiData.total_tokens || 0} | +${successRate !== null ? successRate + '%' : '-'} | +${modelsHtml || '-'} | +