devin15/cursor2api-rust
0
1<!DOCTYPE html>2<html lang="zh">3 4<head>5 <meta charset="UTF-8">6 <link rel="icon" type="image/x-icon" href="data:image/x-icon;,">7 <meta name="viewport" content="width=device-width, initial-scale=1.0">8 <title>请求日志查看</title>9 <!-- 引入共享样式 -->10 <link rel="stylesheet" href="/static/shared-styles.css">11 <script src="/static/shared.js"></script>12 <style>13 /* 创建正确的堆叠上下文 */14 .stats-grid {15 display: grid;16 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));17 gap: 20px;18 margin-bottom: var(--spacing);19 }20 21 .stat-card {22 background: var(--card-background);23 padding: 20px;24 border-radius: var(--border-radius);25 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);26 transition: all var(--transition-fast);27 border: 1px solid var(--border-color);28 }29 30 .stat-card:hover {31 transform: translateY(-2px);32 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);33 }34 35 .stat-card h4 {36 margin: 0 0 8px 0;37 color: var(--primary-color);38 }39 40 .stat-value {41 font-size: 28px;42 font-weight: 600;43 color: var(--primary-color);44 margin-top: 4px;45 }46 47 .refresh-container {48 display: flex;49 justify-content: space-between;50 align-items: center;51 }52 53 .auto-refresh {54 display: flex;55 align-items: center;56 gap: 8px;57 background: var(--card-background);58 padding: 8px 16px;59 border-radius: var(--border-radius);60 border: 1px solid var(--border-color);61 }62 63 .modal {64 display: none;65 position: fixed;66 z-index: 1000;67 left: 0;68 top: 0;69 width: 100%;70 height: 100%;71 background-color: rgba(0, 0, 0, 0.4);72 overflow-y: hidden;73 }74 75 .modal-content {76 background-color: var(--card-background);77 margin: 5% auto;78 padding: 20px;79 border-radius: var(--border-radius);80 width: 90%;81 max-width: 800px;82 max-height: 85vh;83 overflow-y: auto;84 border: 1px solid var(--border-color);85 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);86 }87 88 .close {89 float: right;90 cursor: pointer;91 font-size: 28px;92 }93 94 .info-button {95 padding: 6px 12px;96 font-size: 14px;97 border-radius: var(--border-radius);98 transition: all var(--transition-fast);99 background: var(--primary-color-alpha);100 color: var(--primary-color);101 border: 1px solid var(--primary-color);102 }103 104 .info-button:hover {105 background: var(--primary-color);106 color: white;107 }108 109 .message-table {110 width: 100%;111 border-collapse: collapse;112 margin-top: 10px;113 margin: 0;114 border: 1px solid var(--border-color);115 }116 117 .message-table th,118 .message-table td {119 padding: 12px 16px;120 border-bottom: 1px solid var(--border-color);121 transition: background-color var(--transition-fast);122 }123 124 .message-table td {125 word-break: break-word;126 }127 128 .message-table td:nth-child(2) {129 max-width: 600px;130 }131 132 .message-table td:first-child {133 width: 80px;134 white-space: nowrap;135 }136 137 .modal-header {138 display: flex;139 justify-content: space-between;140 align-items: center;141 margin-bottom: 15px;142 }143 144 .modal-header h3 {145 margin: 0;146 }147 148 .close {149 font-size: 24px;150 font-weight: bold;151 cursor: pointer;152 padding: 5px 10px;153 }154 155 .close:hover {156 color: var(--primary-color);157 }158 159 .usage-progress-container {160 margin: 16px 0;161 height: 8px;162 background-color: var(--border-color);163 border-radius: 4px;164 overflow: hidden;165 }166 167 .usage-progress-bar {168 height: 100%;169 width: 0%;170 transition: width 0.3s ease;171 background-color: var(--primary-color);172 border-radius: 4px;173 }174 175 /* 根据使用比例改变颜色 */176 .usage-progress-bar.low {177 background-color: #4caf50;178 /* 绿色 */179 }180 181 .usage-progress-bar.medium {182 background-color: #ff9800;183 /* 橙色 */184 }185 186 .usage-progress-bar.high {187 background-color: #f44336;188 /* 红色 */189 }190 191 /* Token 信息和对话预览的通用样式 */192 .token-info-tooltip {193 position: relative;194 display: inline-block;195 }196 197 .token-info-tooltip .tooltip-content {198 visibility: hidden;199 position: absolute;200 z-index: 1002;201 background-color: var(--card-background);202 padding: 12px 15px;203 border-radius: var(--border-radius);204 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);205 width: 280px;206 left: 50%;207 transform: translateX(-50%);208 bottom: calc(100% + 8px);209 opacity: 0;210 transition: opacity 0.3s, visibility 0.3s;211 text-align: left;212 line-height: 1.6;213 border: 1px solid var(--border-color);214 pointer-events: none;215 }216 217 .token-info-tooltip:hover .tooltip-content {218 visibility: visible;219 opacity: 1;220 }221 222 /* 添加小三角形指示器 */223 .token-info-tooltip .tooltip-content::after {224 content: '';225 position: absolute;226 top: 100%;227 left: 50%;228 margin-left: -8px;229 border-width: 8px;230 border-style: solid;231 border-color: var(--card-background) transparent transparent transparent;232 }233 234 /* 添加不可见的连接区域 */235 .token-info-tooltip::after {236 content: '';237 position: absolute;238 left: 50%;239 transform: translateX(-50%);240 bottom: 100%;241 width: 100%;242 height: 10px;243 background: transparent;244 }245 246 /* Token 信息特定样式 */247 .token-info-tooltip .tooltip-info-row {248 display: flex;249 justify-content: space-between;250 margin: 2px 0;251 }252 253 .token-info-tooltip .tooltip-info-row .label {254 color: var(--text-secondary);255 margin-right: 10px;256 }257 258 .token-info-tooltip .tooltip-info-row .value {259 font-weight: 500;260 word-break: break-word;261 }262 263 /* 对话预览特定样式 */264 .prompt-preview .tooltip-content {265 width: 320px;266 max-height: 200px;267 overflow-y: auto;268 overflow-x: hidden;269 white-space: pre-wrap;270 word-break: break-word;271 }272 273 .prompt-preview .tooltip-content .message-meta {274 font-size: 0.8em;275 color: var(--text-secondary);276 padding: 0;277 margin: 0 0 4px 0;278 }279 280 .prompt-preview .tooltip-content .last-message {281 font-size: 0.9em;282 line-height: 1.5;283 color: var(--text-primary);284 margin: 0;285 padding: 0;286 white-space: pre-wrap;287 word-break: break-word;288 }289 290 /* 优化滚动条样式 */291 .prompt-preview .tooltip-content::-webkit-scrollbar {292 width: 6px;293 }294 295 .prompt-preview .tooltip-content::-webkit-scrollbar-thumb {296 background-color: var(--border-color);297 border-radius: 3px;298 }299 300 .prompt-preview .tooltip-content::-webkit-scrollbar-track {301 background-color: var(--card-background);302 }303 304 /* 优化表格样式 */305 .table-container {306 border-radius: var(--border-radius);307 overflow: hidden;308 border: 1px solid var(--border-color);309 }310 311 #logsTable {312 position: relative;313 z-index: 1;314 }315 316 #logsTable th {317 position: sticky;318 top: 0;319 z-index: 2;320 background: var(--primary-color);321 white-space: nowrap;322 transition: background-color 0.2s ease;323 }324 325 #logsTable td {326 padding: 12px 16px;327 border-bottom: 1px solid var(--border-color);328 transition: background-color var(--transition-fast);329 }330 331 /* 响应式优化 */332 @media (max-width: 768px) {333 .stats-grid {334 grid-template-columns: repeat(2, 1fr);335 gap: 12px;336 }337 338 .stat-card {339 padding: 16px;340 }341 342 .stat-value {343 font-size: 24px;344 }345 346 .modal-content {347 margin: 2% auto;348 width: 95%;349 padding: 16px;350 }351 }352 353 /* 优化表格悬停效果 */354 #logsTable tr:hover td {355 background-color: var(--hover-color, rgba(0, 0, 0, 0.02));356 }357 </style>358</head>359 360<body>361 <h1>请求日志查看</h1>362 363 <div class="container">364 <div class="form-group">365 <label>认证令牌:</label>366 <input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">367 </div>368 <div class="refresh-container">369 <div class="button-group">370 <button onclick="fetchLogs()">刷新日志</button>371 </div>372 <div class="auto-refresh">373 <input type="checkbox" id="autoRefresh" checked>374 <label for="autoRefresh">自动刷新 (60秒)</label>375 </div>376 </div>377 </div>378 379 <div class="container">380 <div class="stats-grid">381 <div class="stat-card">382 <h4>总请求数</h4>383 <div id="totalRequests" class="stat-value">-</div>384 </div>385 <div class="stat-card">386 <h4>活跃请求数</h4>387 <div id="activeRequests" class="stat-value">-</div>388 </div>389 <div class="stat-card">390 <h4>错误请求数</h4>391 <div id="errorRequests" class="stat-value">-</div>392 </div>393 <div class="stat-card">394 <h4>最后更新</h4>395 <div id="lastUpdate" class="stat-value">-</div>396 </div>397 </div>398 399 <div class="table-container">400 <table id="logsTable">401 <thead>402 <tr>403 <th>序</th>404 <th>时间</th>405 <th>模型</th>406 <th>Token信息</th>407 <th>Prompt</th>408 <th>用时/首字</th>409 <th>流式响应</th>410 <th>状态</th>411 <th>错误信息</th>412 </tr>413 </thead>414 <tbody id="logsBody"></tbody>415 </table>416 </div>417 </div>418 419 <div id="message"></div>420 421 <!-- 添加弹窗组件 -->422 <div id="tokenModal" class="modal">423 <div class="modal-content">424 <div class="modal-header">425 <h3>Token 详细信息</h3>426 <span class="close">×</span>427 </div>428 <table class="message-table">429 <tr>430 <td>Token:</td>431 <td id="modalToken"></td>432 </tr>433 <tr>434 <td>校验和:</td>435 <td id="modalChecksum"></td>436 </tr>437 <tr>438 <td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">439 用户信息440 </td>441 </tr>442 <tr>443 <td>邮箱:</td>444 <td id="modalEmail"></td>445 </tr>446 <tr>447 <td>用户名:</td>448 <td id="modalName"></td>449 </tr>450 <tr>451 <td>用户ID:</td>452 <td id="modalId"></td>453 </tr>454 <tr>455 <td>更新时间:</td>456 <td id="modalUpdatedAt"></td>457 </tr>458 <tr>459 <td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">460 会员信息461 </td>462 </tr>463 <tr>464 <td>会员类型:</td>465 <td id="modalMemberType"></td>466 </tr>467 <tr>468 <td>支付ID:</td>469 <td id="modalPaymentId"></td>470 </tr>471 <tr>472 <td>试用剩余:</td>473 <td id="modalTrialDays"></td>474 </tr>475 <tr>476 <td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">477 使用量统计 (最近30天)478 </td>479 </tr>480 <tr>481 <td>Premium models:</td>482 <td id="modalPremiumUsage"></td>483 </tr>484 <tr>485 <td>Standard models:</td>486 <td id="modalStandardUsage"></td>487 </tr>488 <tr>489 <td>Unknown models:</td>490 <td id="modalUnknownUsage"></td>491 </tr>492 </table>493 <div id="usageProgressContainer"></div>494 </div>495 </div>496 497 <div id="promptModal" class="modal">498 <div class="modal-content">499 <div class="modal-header">500 <h3>对话内容</h3>501 <span class="close">×</span>502 </div>503 <div id="promptContent"></div>504 </div>505 </div>506 507 <script>508 let refreshInterval;509 510 function updateStats(data) {511 document.getElementById('totalRequests').textContent = data.total || 0;512 document.getElementById('activeRequests').textContent = data.active || 0;513 if (data.error) {514 document.getElementById('errorRequests').textContent = data.error;515 document.getElementById('errorRequests').parentElement.style.display = '';516 } else {517 document.getElementById('errorRequests').parentElement.style.display = 'none';518 }519 document.getElementById('lastUpdate').textContent =520 new Date(data.timestamp).toLocaleTimeString();521 }522 523 function getProgressBarClass(percentage) {524 if (percentage <= 60) return 'low';525 if (percentage <= 85) return 'medium';526 return 'high';527 }528 529 function formatMembershipType(type) {530 if (!type) return '-';531 532 switch (type) {533 case 'free_trial': return 'Pro Trial';534 case 'pro': return 'Pro';535 case 'free': return 'Free';536 case 'enterprise': return 'Business';537 default: return type538 .split('_')539 .map(word => word.charAt(0).toUpperCase() + word.slice(1))540 .join(' ');541 }542 }543 544 function showTokenModal(tokenInfo) {545 const modal = document.getElementById('tokenModal');546 document.getElementById('modalToken').textContent = tokenInfo.token || '-';547 document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';548 549 if (tokenInfo.profile) {550 const { user, stripe, usage } = tokenInfo.profile;551 552 // 设置用户信息553 document.getElementById('modalEmail').textContent = user.email || '-';554 document.getElementById('modalName').textContent = user.name || '-';555 document.getElementById('modalId').textContent = user.id || '-';556 document.getElementById('modalUpdatedAt').textContent = user.updated_at ? new Date(user.updated_at).toLocaleString() : '-';557 558 // 设置会员信息559 document.getElementById('modalMemberType').textContent =560 formatMembershipType(stripe.membership_type);561 document.getElementById('modalPaymentId').textContent = stripe.payment_id || '-';562 document.getElementById('modalTrialDays').textContent =563 stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}天` : '-';564 565 // 处理使用量信息566 const container = document.getElementById('usageProgressContainer');567 container.innerHTML = '';568 569 const models = {570 'modalPremiumUsage': usage.premium,571 'modalStandardUsage': usage.standard,572 'modalUnknownUsage': usage.unknown573 };574 575 Object.entries(models).forEach(([elementId, modelData]) => {576 const element = document.getElementById(elementId);577 if (modelData) {578 const { requests, tokens, max_requests } = modelData;579 580 if (max_requests) {581 const percentage = (requests / max_requests * 100).toFixed(1);582 element.textContent = `${requests}/${max_requests} requests (${percentage}%), ${tokens} tokens`;583 584 const progressDiv = document.createElement('div');585 progressDiv.className = 'usage-progress-container';586 const colorClass = getProgressBarClass(parseFloat(percentage));587 progressDiv.innerHTML = `<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>`;588 container.appendChild(progressDiv);589 } else {590 element.textContent = `${requests} requests, ${tokens} tokens`;591 }592 } else {593 element.textContent = '-';594 }595 });596 } else {597 // 如果没有 profile 信息,清空所有字段598 [599 'modalEmail',600 'modalName',601 'modalId',602 'modalUpdatedAt',603 'modalMemberType',604 'modalPaymentId',605 'modalTrialDays',606 'modalPremiumUsage',607 'modalStandardUsage',608 'modalUnknownUsage'609 ].forEach(id => document.getElementById(id).textContent = '-');610 document.getElementById('usageProgressContainer').innerHTML = '';611 }612 613 modal.style.display = 'block';614 }615 616 function formatSimpleTokenInfo(tokenInfo) {617 if (!tokenInfo.profile) return '无用户信息';618 619 const { user, stripe, usage } = tokenInfo.profile;620 const premiumUsage = usage.premium ?621 `${usage.premium.requests}/${usage.premium.max_requests}` : '-';622 623 const rows = [624 ['邮箱', user.email || '-'],625 ...(user.name ? [['用户名', user.name]] : []),626 ['会员', formatMembershipType(stripe.membership_type)],627 ['Premium', premiumUsage]628 ];629 630 return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');631 }632 633 function updateTable(data) {634 const tbody = document.getElementById('logsBody');635 updateStats(data);636 637 tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ?`<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` :'-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');638 }639 640 function formatTiming(total, first) {641 const formattedTotal = total.toFixed(2);642 const formattedFirst = first !== null && first !== undefined ? `${first.toFixed(2)}s` : '-';643 return `${formattedTotal}s / ${formattedFirst}`;644 }645 646 function formatPromptPreview(promptStr) {647 try {648 const messages = parsePrompt(promptStr);649 if (!messages || messages.length === 0) {650 return '无对话内容';651 }652 653 // 获取最后一条消息654 const lastMessage = messages[messages.length - 1];655 const roleLabels = {656 'system': '系统',657 'user': '用户',658 'assistant': '助手'659 };660 661 return `<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div><div class="last-message">${lastMessage.content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}</div>`;662 } catch (e) {663 console.error('预览对话内容失败:', e);664 return '无法解析对话内容';665 }666 }667 668 async function fetchLogs() {669 const data = await makeAuthenticatedRequest('/logs');670 if (data) {671 updateTable(data);672 showGlobalMessage('日志获取成功');673 }674 }675 676 // 自动刷新控制677 document.getElementById('autoRefresh').addEventListener('change', function (e) {678 if (e.target.checked) {679 refreshInterval = setInterval(fetchLogs, 60000);680 } else {681 clearInterval(refreshInterval);682 }683 });684 685 // 页面加载完成后自动获取日志686 document.addEventListener('DOMContentLoaded', () => {687 const authToken = getAuthToken();688 if (authToken) {689 document.getElementById('authToken').value = authToken;690 fetchLogs();691 }692 // 启动自动刷新693 refreshInterval = setInterval(fetchLogs, 60000);694 });695 696 // 初始化 token 处理697 initializeTokenHandling('authToken');698 699 // 添加清理逻辑700 window.addEventListener('beforeunload', () => {701 if (refreshInterval) {702 clearInterval(refreshInterval);703 }704 });705 706 // 添加模态框关闭逻辑707 document.querySelectorAll('.modal .close').forEach(closeBtn => {708 closeBtn.onclick = function () {709 this.closest('.modal').style.display = 'none';710 }711 });712 713 window.onclick = function (event) {714 if (event.target.classList.contains('modal')) {715 event.target.style.display = 'none';716 }717 }718 </script>719</body>720 721</html>