Files
cursor-api/static/logs.html
2025-01-27 14:03:46 +08:00

767 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求日志查看</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
/* 创建正确的堆叠上下文 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: var(--spacing);
}
.stat-card {
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all var(--transition-fast);
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.stat-card h4 {
margin: 0 0 8px 0;
color: var(--primary-color);
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: var(--primary-color);
margin-top: 4px;
}
.refresh-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 8px;
background: var(--card-background);
padding: 8px 16px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
overflow-y: hidden;
}
.modal-content {
background-color: var(--card-background);
margin: 5% auto;
padding: 20px;
border-radius: var(--border-radius);
width: 90%;
max-width: 800px;
max-height: 85vh;
overflow-y: auto;
border: 1px solid var(--border-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.close {
float: right;
cursor: pointer;
font-size: 28px;
}
.info-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
transition: all var(--transition-fast);
background: var(--primary-color-alpha);
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.info-button:hover {
background: var(--primary-color);
color: white;
}
.message-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin: 0;
border: 1px solid var(--border-color);
}
.message-table th,
.message-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
.message-table td {
word-break: break-word;
}
.message-table td:nth-child(2) {
max-width: 600px;
}
.message-table td:first-child {
width: 80px;
white-space: nowrap;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.modal-header h3 {
margin: 0;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 5px 10px;
}
.close:hover {
color: var(--primary-color);
}
.usage-progress-container {
margin: 16px 0;
height: 8px;
background-color: var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.usage-progress-bar {
height: 100%;
width: 0%;
transition: width 0.3s ease;
background-color: var(--primary-color);
border-radius: 4px;
}
/* 根据使用比例改变颜色 */
.usage-progress-bar.low {
background-color: #4caf50;
/* 绿色 */
}
.usage-progress-bar.medium {
background-color: #ff9800;
/* 橙色 */
}
.usage-progress-bar.high {
background-color: #f44336;
/* 红色 */
}
/* Token 信息和对话预览的通用样式 */
.token-info-tooltip {
position: relative;
display: inline-block;
}
.token-info-tooltip .tooltip-content {
visibility: hidden;
position: absolute;
z-index: 1002;
background-color: var(--card-background);
padding: 12px 15px;
border-radius: var(--border-radius);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 280px;
left: 50%;
transform: translateX(-50%);
bottom: calc(100% + 8px);
opacity: 0;
transition: opacity 0.3s, visibility 0.3s;
text-align: left;
line-height: 1.6;
border: 1px solid var(--border-color);
pointer-events: none;
}
.token-info-tooltip:hover .tooltip-content {
visibility: visible;
opacity: 1;
}
/* 添加小三角形指示器 */
.token-info-tooltip .tooltip-content::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
border-width: 8px;
border-style: solid;
border-color: var(--card-background) transparent transparent transparent;
}
/* 添加不可见的连接区域 */
.token-info-tooltip::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100%;
width: 100%;
height: 10px;
background: transparent;
}
/* Token 信息特定样式 */
.token-info-tooltip .tooltip-info-row {
display: flex;
justify-content: space-between;
margin: 2px 0;
}
.token-info-tooltip .tooltip-info-row .label {
color: var(--text-secondary);
margin-right: 10px;
}
.token-info-tooltip .tooltip-info-row .value {
font-weight: 500;
word-break: break-word;
}
/* 对话预览特定样式 */
.prompt-preview .tooltip-content {
width: 320px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
word-break: break-word;
}
.prompt-preview .tooltip-content .message-meta {
font-size: 0.8em;
color: var(--text-secondary);
padding: 0;
margin: 0 0 4px 0;
}
.prompt-preview .tooltip-content .last-message {
font-size: 0.9em;
line-height: 1.5;
color: var(--text-primary);
margin: 0;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
/* 优化滚动条样式 */
.prompt-preview .tooltip-content::-webkit-scrollbar {
width: 6px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 3px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-track {
background-color: var(--card-background);
}
/* 优化表格样式 */
.table-container {
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--border-color);
}
#logsTable {
position: relative;
z-index: 1;
}
#logsTable th {
position: sticky;
top: 0;
z-index: 2;
background: var(--primary-color);
white-space: nowrap;
transition: background-color 0.2s ease;
}
#logsTable td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
/* 响应式优化 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
padding: 16px;
}
.stat-value {
font-size: 24px;
}
.modal-content {
margin: 2% auto;
width: 95%;
padding: 16px;
}
}
/* 优化表格悬停效果 */
#logsTable tr:hover td {
background-color: var(--hover-color, rgba(0, 0, 0, 0.02));
}
.modal-actions {
display: flex;
align-items: center;
gap: 12px;
}
.danger-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
background: var(--error-color-alpha);
color: var(--error-color);
border: 1px solid var(--error-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.danger-button:hover {
background: var(--error-color);
color: white;
}
</style>
</head>
<body>
<h1>请求日志查看</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
<div class="refresh-container">
<div class="button-group">
<button onclick="fetchLogs()">刷新日志</button>
</div>
<div class="auto-refresh">
<input type="checkbox" id="autoRefresh" checked>
<label for="autoRefresh">自动刷新 (60秒)</label>
</div>
</div>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<h4>总请求数</h4>
<div id="totalRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>活跃请求数</h4>
<div id="activeRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>错误请求数</h4>
<div id="errorRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>最后更新</h4>
<div id="lastUpdate" class="stat-value">-</div>
</div>
</div>
<div class="table-container">
<table id="logsTable">
<thead>
<tr>
<th></th>
<th>时间</th>
<th>模型</th>
<th>Token信息</th>
<th>Prompt</th>
<th>用时/首字</th>
<th>流式响应</th>
<th>状态</th>
<th>错误信息</th>
</tr>
</thead>
<tbody id="logsBody"></tbody>
</table>
</div>
</div>
<div id="message"></div>
<!-- 添加弹窗组件 -->
<div id="tokenModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Token 详细信息</h3>
<div class="modal-actions">
<button class="danger-button" id="deleteTokenBtn">删除此Token</button>
<span class="close">&times;</span>
</div>
</div>
<table class="message-table">
<tr>
<td>Token:</td>
<td id="modalToken"></td>
</tr>
<tr>
<td>校验和:</td>
<td id="modalChecksum"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
用户信息
</td>
</tr>
<tr>
<td>邮箱:</td>
<td id="modalEmail"></td>
</tr>
<tr>
<td>用户名:</td>
<td id="modalName"></td>
</tr>
<tr>
<td>用户ID:</td>
<td id="modalId"></td>
</tr>
<tr>
<td>更新时间:</td>
<td id="modalUpdatedAt"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
会员信息
</td>
</tr>
<tr>
<td>会员类型:</td>
<td id="modalMemberType"></td>
</tr>
<tr>
<td>支付ID:</td>
<td id="modalPaymentId"></td>
</tr>
<tr>
<td>试用剩余:</td>
<td id="modalTrialDays"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
使用量统计 (最近30天)
</td>
</tr>
<tr>
<td>Premium models:</td>
<td id="modalPremiumUsage"></td>
</tr>
<tr>
<td>Standard models:</td>
<td id="modalStandardUsage"></td>
</tr>
<tr>
<td>Unknown models:</td>
<td id="modalUnknownUsage"></td>
</tr>
</table>
<div id="usageProgressContainer"></div>
</div>
</div>
<div id="promptModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>对话内容</h3>
<span class="close">&times;</span>
</div>
<div id="promptContent"></div>
</div>
</div>
<script>
let refreshInterval;
function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total || 0;
document.getElementById('activeRequests').textContent = data.active || 0;
if (data.error) {
document.getElementById('errorRequests').textContent = data.error;
document.getElementById('errorRequests').parentElement.style.display = '';
} else {
document.getElementById('errorRequests').parentElement.style.display = 'none';
}
document.getElementById('lastUpdate').textContent =
new Date(data.timestamp).toLocaleTimeString();
}
function getProgressBarClass(percentage) {
if (percentage <= 60) return 'low';
if (percentage <= 85) return 'medium';
return 'high';
}
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
const deleteBtn = document.getElementById('deleteTokenBtn');
// 存储当前token用于删除操作
const currentToken = tokenInfo.token;
// 更新删除按钮点击事件
deleteBtn.onclick = async () => {
if (!currentToken) {
showGlobalMessage('无效的Token', true);
return;
}
if (!confirm('确定要删除此Token吗此操作不可撤销。')) {
return;
}
const data = await makeAuthenticatedRequest('/tokens/delete', {
method: 'POST',
body: JSON.stringify({
tokens: [currentToken],
expectation: 'failed_tokens'
})
});
if (data) {
modal.style.display = 'none';
let message = 'Token删除成功';
if (data.failed_tokens?.length) {
message = 'Token删除失败未找到该Token';
}
showGlobalMessage(message);
// 刷新日志列表
fetchLogs();
}
};
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
if (tokenInfo.profile) {
const { user, stripe, usage } = tokenInfo.profile;
// 设置用户信息
document.getElementById('modalEmail').textContent = user.email || '-';
document.getElementById('modalName').textContent = user.name || '-';
document.getElementById('modalId').textContent = user.id || '-';
document.getElementById('modalUpdatedAt').textContent = user.updated_at ? new Date(user.updated_at).toLocaleString() : '-';
// 设置会员信息
document.getElementById('modalMemberType').textContent =
formatMembershipType(stripe.membership_type);
document.getElementById('modalPaymentId').textContent = stripe.payment_id || '-';
document.getElementById('modalTrialDays').textContent =
stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : '-';
// 处理使用量信息
const container = document.getElementById('usageProgressContainer');
container.innerHTML = '';
const models = {
'modalPremiumUsage': usage.premium,
'modalStandardUsage': usage.standard,
'modalUnknownUsage': usage.unknown
};
Object.entries(models).forEach(([elementId, modelData]) => {
const element = document.getElementById(elementId);
if (modelData) {
const { requests, tokens, max_requests } = modelData;
if (max_requests) {
const percentage = (requests / max_requests * 100).toFixed(1);
element.textContent = `${requests}/${max_requests} requests (${percentage}%), ${tokens} tokens`;
const progressDiv = document.createElement('div');
progressDiv.className = 'usage-progress-container';
const colorClass = getProgressBarClass(parseFloat(percentage));
progressDiv.innerHTML = `<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>`;
container.appendChild(progressDiv);
} else {
element.textContent = `${requests} requests, ${tokens} tokens`;
}
} else {
element.textContent = '-';
}
});
} else {
// 如果没有 profile 信息,清空所有字段
[
'modalEmail',
'modalName',
'modalId',
'modalUpdatedAt',
'modalMemberType',
'modalPaymentId',
'modalTrialDays',
'modalPremiumUsage',
'modalStandardUsage',
'modalUnknownUsage'
].forEach(id => document.getElementById(id).textContent = '-');
document.getElementById('usageProgressContainer').innerHTML = '';
}
modal.style.display = 'block';
}
function formatSimpleTokenInfo(tokenInfo) {
if (!tokenInfo.profile) return '无用户信息';
const { user, stripe, usage } = tokenInfo.profile;
const premiumUsage = usage.premium ?
`${usage.premium.requests}/${usage.premium.max_requests}` : '-';
const rows = [
['邮箱', user.email || '-'],
...(user.name ? [['用户名', user.name]] : []),
['会员', formatMembershipType(stripe.membership_type)],
['Premium', premiumUsage]
];
return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');
}
function updateTable(data) {
const tbody = document.getElementById('logsBody');
updateStats(data);
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('');
}
function formatTiming(total, first) {
const formattedTotal = total.toFixed(2);
const formattedFirst = first !== null && first !== undefined ? `${first.toFixed(2)}s` : '-';
return `${formattedTotal}s / ${formattedFirst}`;
}
function formatPromptPreview(promptStr) {
try {
const messages = parsePrompt(promptStr);
if (!messages || messages.length === 0) {
return '无对话内容';
}
// 获取最后一条消息
const lastMessage = messages[messages.length - 1];
const roleLabels = {
'system': '系统',
'user': '用户',
'assistant': '助手'
};
return `<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div><div class="last-message">${lastMessage.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')}</div>`;
} catch (e) {
console.error('预览对话内容失败:', e);
return '无法解析对话内容';
}
}
async function fetchLogs() {
const data = await makeAuthenticatedRequest('/logs');
if (data) {
updateTable(data);
showGlobalMessage('日志获取成功');
}
}
// 自动刷新控制
document.getElementById('autoRefresh').addEventListener('change', function (e) {
if (e.target.checked) {
refreshInterval = setInterval(fetchLogs, 60000);
} else {
clearInterval(refreshInterval);
}
});
// 页面加载完成后自动获取日志
document.addEventListener('DOMContentLoaded', () => {
const authToken = getAuthToken();
if (authToken) {
document.getElementById('authToken').value = authToken;
fetchLogs();
}
// 启动自动刷新
refreshInterval = setInterval(fetchLogs, 60000);
});
// 初始化 token 处理
initializeTokenHandling('authToken');
// 添加清理逻辑
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
// 添加模态框关闭逻辑
document.querySelectorAll('.modal .close').forEach(closeBtn => {
closeBtn.onclick = function () {
this.closest('.modal').style.display = 'none';
}
});
window.onclick = function (event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
</script>
</body>
</html>