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 = `
${i18n.t('usage_stats.loading_error')}: ${error.message}
`; + } + } + } + + // 更新使用统计概览 + 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 = `
${i18n.t('usage_stats.no_data')}
`; + return; + } + + let tableHtml = ` + + + + + + + + + + + + `; + + Object.entries(apis).forEach(([endpoint, apiData]) => { + const totalRequests = apiData.total_requests || 0; + const successCount = apiData.success_count ?? null; + const successRate = successCount !== null && totalRequests > 0 + ? Math.round((successCount / totalRequests) * 100) + : null; + + // 构建模型详情 + let modelsHtml = ''; + if (apiData.models && Object.keys(apiData.models).length > 0) { + modelsHtml = '
'; + Object.entries(apiData.models).forEach(([modelName, modelData]) => { + const modelRequests = modelData.total_requests ?? 0; + const modelTokens = modelData.total_tokens ?? 0; + modelsHtml += ` +
+ ${modelName} + ${modelRequests} 请求 / ${modelTokens} tokens +
+ `; + }); + modelsHtml += '
'; + } + + tableHtml += ` + + + + + + + + `; + }); + + 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 || '-'}
'; + container.innerHTML = tableHtml; + } + showModal() { const modal = document.getElementById('modal'); if (modal) { diff --git a/i18n.js b/i18n.js index b9ffa2d..6cda4df 100644 --- a/i18n.js +++ b/i18n.js @@ -85,6 +85,7 @@ const i18n = { 'nav.api_keys': 'API 密钥', 'nav.ai_providers': 'AI 提供商', 'nav.auth_files': '认证文件', + 'nav.usage_stats': '使用统计', 'nav.system_info': '系统信息', // 基础设置 @@ -214,6 +215,26 @@ const i18n = { 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', + // 使用统计 + 'usage_stats.title': '使用统计', + 'usage_stats.total_requests': '总请求数', + 'usage_stats.success_requests': '成功请求', + 'usage_stats.failed_requests': '失败请求', + 'usage_stats.total_tokens': '总Token数', + 'usage_stats.requests_trend': '请求趋势', + 'usage_stats.tokens_trend': 'Token 使用趋势', + 'usage_stats.api_details': 'API 详细统计', + 'usage_stats.by_hour': '按小时', + 'usage_stats.by_day': '按天', + 'usage_stats.refresh': '刷新', + 'usage_stats.no_data': '暂无数据', + 'usage_stats.loading_error': '加载失败', + 'usage_stats.api_endpoint': 'API端点', + 'usage_stats.requests_count': '请求次数', + 'usage_stats.tokens_count': 'Token数量', + 'usage_stats.models': '模型统计', + 'usage_stats.success_rate': '成功率', + // 系统信息 'system_info.title': '系统信息', 'system_info.connection_status_title': '连接状态', @@ -358,6 +379,7 @@ const i18n = { 'nav.api_keys': 'API Keys', 'nav.ai_providers': 'AI Providers', 'nav.auth_files': 'Auth Files', + 'nav.usage_stats': 'Usage Statistics', 'nav.system_info': 'System Info', // Basic settings @@ -487,6 +509,26 @@ const i18n = { 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', + // Usage Statistics + 'usage_stats.title': 'Usage Statistics', + 'usage_stats.total_requests': 'Total Requests', + 'usage_stats.success_requests': 'Success Requests', + 'usage_stats.failed_requests': 'Failed Requests', + 'usage_stats.total_tokens': 'Total Tokens', + 'usage_stats.requests_trend': 'Request Trends', + 'usage_stats.tokens_trend': 'Token Usage Trends', + 'usage_stats.api_details': 'API Details', + 'usage_stats.by_hour': 'By Hour', + 'usage_stats.by_day': 'By Day', + 'usage_stats.refresh': 'Refresh', + 'usage_stats.no_data': 'No Data Available', + 'usage_stats.loading_error': 'Loading Failed', + 'usage_stats.api_endpoint': 'API Endpoint', + 'usage_stats.requests_count': 'Request Count', + 'usage_stats.tokens_count': 'Token Count', + 'usage_stats.models': 'Model Statistics', + 'usage_stats.success_rate': 'Success Rate', + // System info 'system_info.title': 'System Information', 'system_info.connection_status_title': 'Connection Status', diff --git a/index.html b/index.html index 9521276..e1587b8 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ CLI Proxy API Management Center + @@ -221,6 +222,9 @@
  • 认证文件
  • +
  • + 使用统计 +
  • 系统信息
  • @@ -452,6 +456,112 @@ + +
    +

    使用统计

    + + +
    +
    +
    + +
    +
    +
    0
    +
    总请求数
    +
    +
    + +
    +
    + +
    +
    +
    0
    +
    成功请求
    +
    +
    + +
    +
    + +
    +
    +
    0
    +
    失败请求
    +
    +
    + +
    +
    + +
    +
    +
    0
    +
    总Token数
    +
    +
    +
    + + +
    + +
    +
    +

    请求趋势

    +
    + + +
    +
    +
    +
    + +
    +
    +
    + + +
    +
    +

    Token 使用趋势

    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +

    API 详细统计

    + +
    +
    +
    +
    正在加载...
    +
    +
    +
    +
    +

    系统信息

    diff --git a/styles.css b/styles.css index f819d0b..3d06473 100644 --- a/styles.css +++ b/styles.css @@ -27,6 +27,9 @@ --accent-primary: linear-gradient(135deg, #475569, #334155); --accent-secondary: #e2e8f0; --accent-tertiary: #f8fafc; + --primary-color: #3b82f6; + --card-bg: #ffffff; + --border-color: #e2e8f0; --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0); --success-text: #166534; @@ -69,6 +72,9 @@ --accent-primary: linear-gradient(135deg, #64748b, #475569); --accent-secondary: #334155; --accent-tertiary: #1e293b; + --primary-color: #38bdf8; + --card-bg: #1e293b; + --border-color: #334155; --success-bg: linear-gradient(135deg, #064e3b, #047857); --success-text: #bbf7d0; @@ -1613,3 +1619,200 @@ input:checked + .slider:before { line-height: 1.4; } +/* 使用统计样式 */ +.stats-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + display: flex; + align-items: center; + gap: 16px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.stat-card:hover { + border-color: var(--border-primary); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.stat-icon { + width: 50px; + height: 50px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-color); + color: white; + font-size: 20px; + flex-shrink: 0; +} + +.stat-icon.success { + background: #10b981; +} + +.stat-icon.error { + background: #ef4444; +} + +.stat-content { + flex: 1; +} + +.stat-number { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + line-height: 1; + margin-bottom: 4px; +} + +.stat-label { + font-size: 14px; + color: var(--text-secondary); + font-weight: 500; +} + +.charts-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 30px; +} + +@media (max-width: 1200px) { + .charts-container { + grid-template-columns: 1fr; + } +} + +.chart-card { + min-height: 400px; +} + +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +.chart-controls { + display: flex; + gap: 8px; +} + +.btn.btn-small { + padding: 6px 12px; + font-size: 12px; + border-radius: 6px; + border: 1px solid var(--border-color); + background: var(--card-bg); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; +} + +.btn.btn-small:hover { + border-color: var(--border-primary); + color: var(--text-primary); +} + +.btn.btn-small.active { + background: var(--primary-color); + border-color: var(--primary-color); + color: white; +} + +.api-stats-table { + overflow-x: auto; +} + +.stats-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.stats-table th, +.stats-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.stats-table th { + background: var(--bg-secondary); + font-weight: 600; + color: var(--text-primary); + font-size: 14px; +} + +.stats-table td { + color: var(--text-secondary); + font-size: 14px; +} + +.stats-table tr:hover { + background: var(--bg-secondary); +} + +.model-details { + margin-top: 8px; + padding: 8px 12px; + background: var(--bg-tertiary); + border-radius: 6px; + font-size: 12px; +} + +.model-item { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: var(--text-tertiary); +} + +.model-name { + font-weight: 500; + color: var(--text-secondary); +} + +.loading-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: var(--text-tertiary); + font-size: 14px; +} + +.no-data-message { + text-align: center; + color: var(--text-tertiary); + font-style: italic; + padding: 40px; +} + +/* 暗色主题适配 */ +[data-theme="dark"] .stat-card { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +[data-theme="dark"] .stat-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +[data-theme="dark"] .btn.btn-small { + background: var(--bg-tertiary); +} +