Spaces:
Runtime error
Runtime error
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Abacus Chat Proxy - 仪表盘</title> | |
| <style> | |
| :root { | |
| --primary-color: #6f42c1; | |
| --secondary-color: #4a32a8; | |
| --accent-color: #5e85f1; | |
| --bg-color: #0a0a1a; | |
| --text-color: #e6e6ff; | |
| --card-bg: rgba(30, 30, 60, 0.7); | |
| --input-bg: rgba(40, 40, 80, 0.6); | |
| --success-color: #36d399; | |
| --warning-color: #fbbd23; | |
| --error-color: #f87272; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| min-height: 100vh; | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%), | |
| radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%); | |
| color: var(--text-color); | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| /* 动态背景网格 */ | |
| .grid-background { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px); | |
| background-size: 30px 30px; | |
| z-index: -1; | |
| animation: grid-move 20s linear infinite; | |
| } | |
| @keyframes grid-move { | |
| 0% { | |
| transform: translateY(0); | |
| } | |
| 100% { | |
| transform: translateY(30px); | |
| } | |
| } | |
| /* 顶部导航栏 */ | |
| .navbar { | |
| padding: 1rem 2rem; | |
| background: rgba(15, 15, 30, 0.8); | |
| backdrop-filter: blur(10px); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .navbar-brand { | |
| display: flex; | |
| align-items: center; | |
| text-decoration: none; | |
| color: var(--text-color); | |
| } | |
| .navbar-logo { | |
| font-size: 1.5rem; | |
| margin-right: 0.75rem; | |
| animation: pulse 2s infinite alternate; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| transform: scale(1); | |
| text-shadow: 0 0 5px rgba(111, 66, 193, 0.5); | |
| } | |
| 100% { | |
| transform: scale(1.05); | |
| text-shadow: 0 0 15px rgba(111, 66, 193, 0.8); | |
| } | |
| } | |
| .navbar-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| background: linear-gradient(45deg, #6f42c1, #5181f1); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .navbar-actions { | |
| display: flex; | |
| gap: 1rem; | |
| } | |
| .btn-logout { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-color); | |
| border: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-logout:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| /* 主内容区域 */ | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| /* 信息卡片样式 */ | |
| .card { | |
| background: var(--card-bg); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); | |
| backdrop-filter: blur(8px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| animation: card-fade-in 0.6s ease-out; | |
| } | |
| @keyframes card-fade-in { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .card-header { | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .card-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .card-icon { | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(45deg, rgba(111, 66, 193, 0.2), rgba(94, 133, 241, 0.2)); | |
| border-radius: 8px; | |
| font-size: 1.25rem; | |
| } | |
| /* 状态项样式 */ | |
| .status-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.75rem 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .status-item:last-child { | |
| border-bottom: none; | |
| } | |
| .status-label { | |
| color: rgba(230, 230, 255, 0.7); | |
| font-weight: 500; | |
| } | |
| .status-value { | |
| color: var(--text-color); | |
| font-weight: 600; | |
| } | |
| .status-value.success { | |
| color: var(--success-color); | |
| } | |
| .status-value.warning { | |
| color: var(--warning-color); | |
| } | |
| .status-value.danger { | |
| color: var(--error-color); | |
| } | |
| /* 模型标签 */ | |
| .models-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| } | |
| .model-tag { | |
| background: rgba(111, 66, 193, 0.2); | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 16px; | |
| font-size: 0.875rem; | |
| color: var(--text-color); | |
| border: 1px solid rgba(111, 66, 193, 0.3); | |
| } | |
| /* 表格样式 */ | |
| .table-container { | |
| overflow-x: auto; | |
| margin-top: 1rem; | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| text-align: left; | |
| } | |
| .data-table th { | |
| background-color: rgba(50, 50, 100, 0.3); | |
| padding: 0.75rem 1rem; | |
| font-weight: 600; | |
| color: rgba(230, 230, 255, 0.9); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .data-table td { | |
| padding: 0.75rem 1rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .data-table tbody tr { | |
| transition: background-color 0.2s; | |
| } | |
| .data-table tbody tr:hover { | |
| background-color: rgba(50, 50, 100, 0.2); | |
| } | |
| /* 特殊值样式 */ | |
| .token-count { | |
| font-family: 'Consolas', monospace; | |
| color: var(--accent-color); | |
| font-weight: bold; | |
| } | |
| .call-count { | |
| font-family: 'Consolas', monospace; | |
| color: var(--success-color); | |
| font-weight: bold; | |
| } | |
| .compute-points { | |
| font-family: 'Consolas', monospace; | |
| color: var(--primary-color); | |
| font-weight: bold; | |
| } | |
| /* 进度条 */ | |
| .progress-container { | |
| width: 100%; | |
| height: 8px; | |
| background-color: rgba(100, 100, 150, 0.2); | |
| border-radius: 4px; | |
| margin-top: 0.5rem; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| border-radius: 4px; | |
| background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .progress-bar.warning { | |
| background: linear-gradient(90deg, #fbbd23, #f59e0b); | |
| } | |
| .progress-bar.danger { | |
| background: linear-gradient(90deg, #f87272, #ef4444); | |
| } | |
| /* 添加进度条闪光效果 */ | |
| .progress-bar::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, | |
| transparent, | |
| rgba(255, 255, 255, 0.2), | |
| transparent); | |
| animation: progress-shine 3s infinite; | |
| } | |
| @keyframes progress-shine { | |
| 0% { | |
| left: -100%; | |
| } | |
| 50%, 100% { | |
| left: 100%; | |
| } | |
| } | |
| /* API端点卡片 */ | |
| .endpoint-item { | |
| background: rgba(50, 50, 100, 0.2); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| border-left: 3px solid var(--primary-color); | |
| } | |
| .endpoint-url { | |
| font-family: 'Consolas', monospace; | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| margin-top: 0.25rem; | |
| display: inline-block; | |
| color: var(--text-color); | |
| text-decoration: none; | |
| transition: all 0.2s; | |
| } | |
| .endpoint-url:hover { | |
| background: rgba(111, 66, 193, 0.3); | |
| color: var(--text-color); | |
| } | |
| /* 响应式布局 */ | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| /* 页脚 */ | |
| .footer { | |
| text-align: center; | |
| padding: 2rem 0; | |
| color: rgba(230, 230, 255, 0.5); | |
| font-size: 0.9rem; | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| margin-top: 2rem; | |
| } | |
| /* 悬浮图标按钮 */ | |
| .float-btn { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.5rem; | |
| box-shadow: 0 4px 20px rgba(111, 66, 193, 0.4); | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| z-index: 50; | |
| } | |
| .float-btn:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 25px rgba(111, 66, 193, 0.5); | |
| } | |
| /* 滚动条美化 */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(50, 50, 100, 0.1); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(111, 66, 193, 0.5); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(111, 66, 193, 0.7); | |
| } | |
| /* 模型统计折叠样式 */ | |
| .hidden-model { | |
| display: none; | |
| } | |
| .btn-toggle { | |
| background: rgba(111, 66, 193, 0.2); | |
| border: 1px solid rgba(111, 66, 193, 0.3); | |
| border-radius: 4px; | |
| padding: 0.3rem 0.7rem; | |
| color: rgba(230, 230, 255, 0.9); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.85rem; | |
| margin-left: auto; | |
| } | |
| .btn-toggle:hover { | |
| background: rgba(111, 66, 193, 0.4); | |
| } | |
| /* Token注释样式 */ | |
| .token-note { | |
| margin-top: 0.75rem; | |
| color: rgba(230, 230, 255, 0.6); | |
| font-style: italic; | |
| line-height: 1.4; | |
| padding: 0.5rem; | |
| border-top: 1px dashed rgba(255, 255, 255, 0.1); | |
| } | |
| .token-model-table { | |
| margin-top: 1rem; | |
| } | |
| /* Token计算方法标签样式 */ | |
| .token-method { | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .token-method-exact { | |
| background-color: rgba(54, 211, 153, 0.2); | |
| color: #36d399; | |
| } | |
| .token-method-estimate { | |
| background-color: rgba(251, 189, 35, 0.2); | |
| color: #fbbd23; | |
| } | |
| /* 时间日期样式 */ | |
| .datetime { | |
| font-family: 'Consolas', monospace; | |
| color: rgba(230, 230, 255, 0.8); | |
| font-size: 0.9rem; | |
| } | |
| /* 媒体查询 */ | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 1rem; | |
| } | |
| .navbar { | |
| padding: 1rem; | |
| } | |
| .card { | |
| padding: 1rem; | |
| } | |
| .grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .token-model-table td, .token-model-table th { | |
| white-space: nowrap; | |
| } | |
| /* 开关按钮样式 */ | |
| .toggle-switch-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 50px; | |
| height: 24px; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(100, 100, 150, 0.3); | |
| transition: .4s; | |
| border-radius: 24px; | |
| } | |
| .toggle-slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 18px; | |
| width: 18px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: #e6e6ff; | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .toggle-slider { | |
| background-color: var(--primary-color); | |
| } | |
| input:checked + .toggle-slider:before { | |
| transform: translateX(26px); | |
| } | |
| .toggle-status { | |
| font-weight: 600; | |
| } | |
| .info-text { | |
| font-size: 0.85rem; | |
| color: rgba(230, 230, 255, 0.7); | |
| } | |
| /* 通知样式 */ | |
| .notification { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| color: white; | |
| font-weight: 500; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| z-index: 1000; | |
| transform: translateY(-20px); | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| max-width: 300px; | |
| } | |
| .notification.show { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| .notification.success { | |
| background-color: var(--success-color); | |
| } | |
| .notification.error { | |
| background-color: var(--error-color); | |
| } | |
| .notification.info { | |
| background-color: var(--accent-color); | |
| } | |
| /* 响应式样式 */ | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 1rem; | |
| } | |
| .navbar { | |
| padding: 1rem; | |
| } | |
| .card { | |
| padding: 1rem; | |
| } | |
| .grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="grid-background"></div> | |
| <nav class="navbar"> | |
| <a href="/" class="navbar-brand"> | |
| <span class="navbar-logo">🤖</span> | |
| <span class="navbar-title">Abacus Chat Proxy</span> | |
| </a> | |
| <div class="navbar-actions"> | |
| <a href="/logout" class="btn-logout"> | |
| <span>退出</span> | |
| <span>↗</span> | |
| </a> | |
| </div> | |
| </nav> | |
| <div class="container"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">📊</span> | |
| 系统状态 | |
| </h2> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">服务状态</span> | |
| <span class="status-value success">运行中</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">运行时间</span> | |
| <span class="status-value">{{ uptime }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">健康检查次数</span> | |
| <span class="status-value">{{ health_checks }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">已配置用户数</span> | |
| <span class="status-value">{{ user_count }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">可用模型</span> | |
| <div class="models-list"> | |
| {% for model in models %} | |
| <span class="model-tag">{{ model }}</span> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">💰</span> | |
| 计算点总计 | |
| </h2> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">总计算点</span> | |
| <span class="status-value compute-points">{{ compute_points.total|int }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">已使用</span> | |
| <span class="status-value compute-points">{{ compute_points.used|int }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">剩余</span> | |
| <span class="status-value compute-points">{{ compute_points.left|int }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">使用比例</span> | |
| <div style="width: 100%; text-align: right;"> | |
| <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}"> | |
| {{ compute_points.percentage }}% | |
| </span> | |
| <div class="progress-container"> | |
| <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| {% if compute_points.last_update %} | |
| <div class="status-item"> | |
| <span class="status-label">最后更新时间</span> | |
| <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">🔍</span> | |
| Token 使用统计 | |
| </h2> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">总输入Token</span> | |
| <span class="status-value token-count">{{ total_tokens.prompt|int }}</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">总输出Token</span> | |
| <span class="status-value token-count">{{ total_tokens.completion|int }}</span> | |
| </div> | |
| <div class="token-note"> | |
| <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small> | |
| </div> | |
| <div class="table-container"> | |
| <table class="data-table token-model-table"> | |
| <thead> | |
| <tr> | |
| <th>模型</th> | |
| <th>调用次数</th> | |
| <th>输入Token</th> | |
| <th>输出Token</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for model, stats in model_stats.items() %} | |
| <tr> | |
| <td>{{ model }}</td> | |
| <td class="call-count">{{ stats.count }}</td> | |
| <td class="token-count">{{ stats.prompt_tokens|int }}</td> | |
| <td class="token-count">{{ stats.completion_tokens|int }}</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| {% if users_compute_points|length > 0 %} | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">👥</span> | |
| 用户计算点详情 | |
| </h2> | |
| </div> | |
| <div class="table-container"> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| <th>用户</th> | |
| <th>总计算点</th> | |
| <th>已使用</th> | |
| <th>剩余</th> | |
| <th>使用比例</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for user in users_compute_points %} | |
| <tr> | |
| <td>用户 {{ user.user_id }}</td> | |
| <td class="compute-points">{{ user.total|int }}</td> | |
| <td class="compute-points">{{ user.used|int }}</td> | |
| <td class="compute-points">{{ user.left|int }}</td> | |
| <td> | |
| <div style="width: 100%; position: relative;"> | |
| <span class="status-value compute-points {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}"> | |
| {{ user.percentage }}% | |
| </span> | |
| <div class="progress-container"> | |
| <div class="progress-bar {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}" style="width: {{ user.percentage }}%"></div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {% endif %} | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">📊</span> | |
| 计算点使用日志 | |
| </h2> | |
| </div> | |
| <div class="table-container"> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| {% for key, value in compute_points_log.columns.items() %} | |
| <th>{{ value }}</th> | |
| {% endfor %} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for entry in compute_points_log.log %} | |
| <tr> | |
| {% for key, value in compute_points_log.columns.items() %} | |
| <td class="compute-points">{{ entry.get(key, 0) }}</td> | |
| {% endfor %} | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">📈</span> | |
| 模型调用记录 | |
| </h2> | |
| <button id="toggleModelStats" class="btn-toggle">显示全部</button> | |
| </div> | |
| <div class="table-container"> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| <th>调用时间 (北京时间)</th> | |
| <th>模型</th> | |
| <th>输入Token</th> | |
| <th>输出Token</th> | |
| <th>总Token</th> | |
| <th>计算方式</th> | |
| <th>计算点数</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for record in model_usage_records|reverse %} | |
| <tr class="model-row {% if loop.index > 10 %}hidden-model{% endif %}"> | |
| <td class="datetime">{{ record.call_time }}</td> | |
| <td>{{ record.model }}</td> | |
| <td class="token-count">{{ record.prompt_tokens }}</td> | |
| <td class="token-count">{{ record.completion_tokens }}</td> | |
| <td>{{ record.prompt_tokens + record.completion_tokens }}</td> | |
| <td> | |
| {% if record.calculation_method == "精确" %} | |
| <span class="token-method token-method-exact">精确</span> | |
| {% else %} | |
| <span class="token-method token-method-estimate">估算</span> | |
| {% endif %} | |
| </td> | |
| <td>{{ record.compute_points if record.compute_points is not none and record.compute_points != 0 else 'null' }}</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| <div class="token-note"> | |
| <small>* Token计算方式:<span class="token-method token-method-exact">精确</span> 表示调用官方API精确计算,<span class="token-method token-method-estimate">估算</span> 表示使用gpt-4o模型估算。所有统计数据仅供参考,不代表实际计费标准。</small> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <span class="card-icon">📡</span> | |
| API 端点 | |
| </h2> | |
| </div> | |
| <div class="endpoint-item"> | |
| <p>获取模型列表:</p> | |
| {% if space_url %} | |
| <a href="{{ space_url }}/v1/models" class="endpoint-url" target="_blank">GET {{ space_url }}/v1/models</a> | |
| {% else %} | |
| <a href="/v1/models" class="endpoint-url" target="_blank">GET /v1/models</a> | |
| {% endif %} | |
| </div> | |
| <div class="endpoint-item"> | |
| <p>聊天补全:</p> | |
| {% if space_url %} | |
| <code class="endpoint-url">POST {{ space_url }}/v1/chat/completions</code> | |
| {% else %} | |
| <code class="endpoint-url">POST /v1/chat/completions</code> | |
| {% endif %} | |
| </div> | |
| <div class="endpoint-item"> | |
| <p>健康检查:</p> | |
| {% if space_url %} | |
| <a href="{{ space_url }}/health" class="endpoint-url" target="_blank">GET {{ space_url }}/health</a> | |
| {% else %} | |
| <a href="/health" class="endpoint-url" target="_blank">GET /health</a> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p> | |
| </div> | |
| </div> | |
| <a href="#" class="float-btn" title="回到顶部">↑</a> | |
| <script> | |
| // 回到顶部按钮 | |
| document.querySelector('.float-btn').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| }); | |
| // 显示/隐藏回到顶部按钮 | |
| window.addEventListener('scroll', () => { | |
| const floatBtn = document.querySelector('.float-btn'); | |
| if (window.pageYOffset > 300) { | |
| floatBtn.style.opacity = '1'; | |
| } else { | |
| floatBtn.style.opacity = '0'; | |
| } | |
| }); | |
| // 初始化隐藏回到顶部按钮 | |
| document.querySelector('.float-btn').style.opacity = '0'; | |
| // 模型统计折叠功能 | |
| const toggleBtn = document.getElementById('toggleModelStats'); | |
| const hiddenModels = document.querySelectorAll('.hidden-model'); | |
| let isExpanded = false; | |
| if (toggleBtn) { | |
| toggleBtn.addEventListener('click', () => { | |
| hiddenModels.forEach(model => { | |
| model.classList.toggle('hidden-model'); | |
| }); | |
| isExpanded = !isExpanded; | |
| toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部'; | |
| }); | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initCharts(); | |
| // 显示/隐藏更多模型使用记录 | |
| const toggleModelStats = document.getElementById('toggleModelStats'); | |
| if (toggleModelStats) { | |
| toggleModelStats.addEventListener('click', function() { | |
| const hiddenRows = document.querySelectorAll('.hidden-model'); | |
| hiddenRows.forEach(row => { | |
| row.classList.toggle('show-model'); | |
| }); | |
| toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部'; | |
| }); | |
| } | |
| // 处理计算点数记录开关 | |
| const computePointToggle = document.getElementById('compute-point-toggle'); | |
| const computePointStatus = document.getElementById('compute-point-status'); | |
| if (computePointToggle && computePointStatus) { | |
| computePointToggle.addEventListener('change', function() { | |
| const isChecked = this.checked; | |
| computePointStatus.textContent = isChecked ? '开启' : '关闭'; | |
| // 发送更新请求到后端 | |
| fetch('/update_compute_point_toggle', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ always_display: isChecked }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| // 显示成功提示 | |
| showNotification(isChecked ? '已开启计算点数记录功能' : '已关闭计算点数记录功能', 'success'); | |
| } else { | |
| // 显示错误提示 | |
| showNotification('设置更新失败: ' + data.error, 'error'); | |
| // 回滚UI状态 | |
| computePointToggle.checked = !isChecked; | |
| computePointStatus.textContent = !isChecked ? '开启' : '关闭'; | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('更新设置出错:', error); | |
| showNotification('更新设置失败,请重试', 'error'); | |
| // 回滚UI状态 | |
| computePointToggle.checked = !isChecked; | |
| computePointStatus.textContent = !isChecked ? '开启' : '关闭'; | |
| }); | |
| }); | |
| } | |
| }); | |
| // 通知函数 | |
| function showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.className = `notification ${type}`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| // 显示动画 | |
| setTimeout(() => { | |
| notification.classList.add('show'); | |
| }, 10); | |
| // 3秒后淡出 | |
| setTimeout(() => { | |
| notification.classList.remove('show'); | |
| setTimeout(() => { | |
| notification.remove(); | |
| }, 300); | |
| }, 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> |