mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
2 Commits
v0.1.5-dev
...
v0.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369cf52346 | ||
|
|
dcfffc716b |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ package-lock.json
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
492
app.js
492
app.js
@@ -17,6 +17,12 @@ class CLIProxyManager {
|
||||
// 状态更新定时器
|
||||
this.statusUpdateTimer = null;
|
||||
|
||||
// 日志自动刷新定时器
|
||||
this.logsRefreshTimer = null;
|
||||
|
||||
// 日志时间戳(用于增量加载)
|
||||
this.latestLogTimestamp = null;
|
||||
|
||||
// 主题管理
|
||||
this.currentTheme = 'light';
|
||||
|
||||
@@ -349,7 +355,6 @@ class CLIProxyManager {
|
||||
// 更新连接信息显示
|
||||
updateConnectionInfo() {
|
||||
const apiUrlElement = document.getElementById('display-api-url');
|
||||
const keyElement = document.getElementById('display-management-key');
|
||||
const statusElement = document.getElementById('display-connection-status');
|
||||
|
||||
// 显示API地址
|
||||
@@ -358,14 +363,6 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 显示密钥(遮蔽显示)
|
||||
if (keyElement) {
|
||||
if (this.managementKey) {
|
||||
const maskedKey = this.maskApiKey(this.managementKey);
|
||||
keyElement.textContent = maskedKey;
|
||||
} else {
|
||||
keyElement.textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示连接状态
|
||||
if (statusElement) {
|
||||
@@ -518,6 +515,31 @@ class CLIProxyManager {
|
||||
usageStatisticsToggle.addEventListener('change', (e) => this.updateUsageStatisticsEnabled(e.target.checked));
|
||||
}
|
||||
|
||||
// 日志记录设置
|
||||
const loggingToFileToggle = document.getElementById('logging-to-file-toggle');
|
||||
if (loggingToFileToggle) {
|
||||
loggingToFileToggle.addEventListener('change', (e) => this.updateLoggingToFile(e.target.checked));
|
||||
}
|
||||
|
||||
// 日志查看
|
||||
const refreshLogs = document.getElementById('refresh-logs');
|
||||
const downloadLogs = document.getElementById('download-logs');
|
||||
const clearLogs = document.getElementById('clear-logs');
|
||||
const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
|
||||
|
||||
if (refreshLogs) {
|
||||
refreshLogs.addEventListener('click', () => this.refreshLogs());
|
||||
}
|
||||
if (downloadLogs) {
|
||||
downloadLogs.addEventListener('click', () => this.downloadLogs());
|
||||
}
|
||||
if (clearLogs) {
|
||||
clearLogs.addEventListener('click', () => this.clearLogs());
|
||||
}
|
||||
if (logsAutoRefreshToggle) {
|
||||
logsAutoRefreshToggle.addEventListener('change', (e) => this.toggleLogsAutoRefresh(e.target.checked));
|
||||
}
|
||||
|
||||
// API 密钥管理
|
||||
const addApiKey = document.getElementById('add-api-key');
|
||||
const addGeminiKey = document.getElementById('add-gemini-key');
|
||||
@@ -814,6 +836,11 @@ class CLIProxyManager {
|
||||
item.classList.add('active');
|
||||
const sectionId = item.getAttribute('data-section');
|
||||
document.getElementById(sectionId).classList.add('active');
|
||||
|
||||
// 如果点击的是日志查看页面,自动加载日志
|
||||
if (sectionId === 'logs') {
|
||||
this.refreshLogs(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1139,7 @@ class CLIProxyManager {
|
||||
const config = await this.getConfig(forceRefresh);
|
||||
|
||||
// 从配置中提取并设置各个设置项
|
||||
this.updateSettingsFromConfig(config);
|
||||
await this.updateSettingsFromConfig(config);
|
||||
|
||||
// 认证文件需要单独加载,因为不在配置中
|
||||
await this.loadAuthFiles();
|
||||
@@ -1130,7 +1157,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 从配置对象更新所有设置
|
||||
updateSettingsFromConfig(config) {
|
||||
async updateSettingsFromConfig(config) {
|
||||
// 调试设置
|
||||
if (config.debug !== undefined) {
|
||||
document.getElementById('debug-toggle').checked = config.debug;
|
||||
@@ -1163,6 +1190,15 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 日志记录设置
|
||||
if (config['logging-to-file'] !== undefined) {
|
||||
const loggingToggle = document.getElementById('logging-to-file-toggle');
|
||||
if (loggingToggle) {
|
||||
loggingToggle.checked = config['logging-to-file'];
|
||||
}
|
||||
// 显示或隐藏日志查看栏目
|
||||
this.toggleLogsNavItem(config['logging-to-file']);
|
||||
}
|
||||
|
||||
// API 密钥
|
||||
if (config['api-keys']) {
|
||||
@@ -1171,22 +1207,22 @@ class CLIProxyManager {
|
||||
|
||||
// Gemini 密钥
|
||||
if (config['generative-language-api-key']) {
|
||||
this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
await this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
}
|
||||
|
||||
// Codex 密钥
|
||||
if (config['codex-api-key']) {
|
||||
this.renderCodexKeys(config['codex-api-key']);
|
||||
await this.renderCodexKeys(config['codex-api-key']);
|
||||
}
|
||||
|
||||
// Claude 密钥
|
||||
if (config['claude-api-key']) {
|
||||
this.renderClaudeKeys(config['claude-api-key']);
|
||||
await this.renderClaudeKeys(config['claude-api-key']);
|
||||
}
|
||||
|
||||
// OpenAI 兼容提供商
|
||||
if (config['openai-compatibility']) {
|
||||
this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
await this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,6 +1389,292 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新日志记录到文件设置
|
||||
async updateLoggingToFile(enabled) {
|
||||
try {
|
||||
await this.makeRequest('/logging-to-file', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: enabled })
|
||||
});
|
||||
this.clearCache();
|
||||
this.showNotification(i18n.t('notification.logging_to_file_updated'), 'success');
|
||||
// 显示或隐藏日志查看栏目
|
||||
this.toggleLogsNavItem(enabled);
|
||||
// 如果启用了日志记录,自动刷新日志
|
||||
if (enabled) {
|
||||
setTimeout(() => this.refreshLogs(), 500);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
const loggingToggle = document.getElementById('logging-to-file-toggle');
|
||||
if (loggingToggle) {
|
||||
loggingToggle.checked = !enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换日志查看栏目的显示/隐藏
|
||||
toggleLogsNavItem(show) {
|
||||
const logsNavItem = document.getElementById('logs-nav-item');
|
||||
if (logsNavItem) {
|
||||
logsNavItem.style.display = show ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日志
|
||||
async refreshLogs(incremental = false) {
|
||||
const logsContent = document.getElementById('logs-content');
|
||||
if (!logsContent) return;
|
||||
|
||||
try {
|
||||
// 如果是增量加载且没有时间戳,则转为全量加载
|
||||
if (incremental && !this.latestLogTimestamp) {
|
||||
incremental = false;
|
||||
}
|
||||
|
||||
// 全量加载时显示加载提示
|
||||
if (!incremental) {
|
||||
logsContent.innerHTML = '<div class="loading-placeholder" data-i18n="logs.loading">' + i18n.t('logs.loading') + '</div>';
|
||||
}
|
||||
|
||||
// 构建请求 URL
|
||||
let url = '/logs';
|
||||
if (incremental && this.latestLogTimestamp) {
|
||||
url += `?after=${this.latestLogTimestamp}`;
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(url, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response && response.lines) {
|
||||
// 更新最新时间戳
|
||||
if (response['latest-timestamp']) {
|
||||
this.latestLogTimestamp = response['latest-timestamp'];
|
||||
}
|
||||
|
||||
if (incremental && response.lines.length > 0) {
|
||||
// 增量加载:追加新日志
|
||||
this.appendLogs(response.lines, response['line-count'] || 0);
|
||||
} else if (!incremental && response.lines.length > 0) {
|
||||
// 全量加载:重新渲染
|
||||
this.renderLogs(response.lines, response['line-count'] || response.lines.length, false);
|
||||
} else if (!incremental) {
|
||||
// 全量加载但没有日志
|
||||
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
|
||||
i18n.t('logs.empty_title') + '</p><p data-i18n="logs.empty_desc">' +
|
||||
i18n.t('logs.empty_desc') + '</p></div>';
|
||||
this.latestLogTimestamp = null;
|
||||
}
|
||||
// 增量加载但没有新日志,不做任何操作
|
||||
} else if (!incremental) {
|
||||
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
|
||||
i18n.t('logs.empty_title') + '</p><p data-i18n="logs.empty_desc">' +
|
||||
i18n.t('logs.empty_desc') + '</p></div>';
|
||||
this.latestLogTimestamp = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志失败:', error);
|
||||
if (!incremental) {
|
||||
// 检查是否是 404 错误(API 不存在)
|
||||
const is404 = error.message && (error.message.includes('404') || error.message.includes('Not Found'));
|
||||
|
||||
if (is404) {
|
||||
// API 不存在,提示升级
|
||||
logsContent.innerHTML = '<div class="upgrade-notice"><i class="fas fa-arrow-circle-up"></i><h3 data-i18n="logs.upgrade_required_title">' +
|
||||
i18n.t('logs.upgrade_required_title') + '</h3><p data-i18n="logs.upgrade_required_desc">' +
|
||||
i18n.t('logs.upgrade_required_desc') + '</p></div>';
|
||||
} else {
|
||||
// 其他错误
|
||||
logsContent.innerHTML = '<div class="error-state"><i class="fas fa-exclamation-triangle"></i><p data-i18n="logs.load_error">' +
|
||||
i18n.t('logs.load_error') + '</p><p>' + error.message + '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染日志内容
|
||||
renderLogs(lines, lineCount, scrollToBottom = true) {
|
||||
const logsContent = document.getElementById('logs-content');
|
||||
if (!logsContent) return;
|
||||
|
||||
if (!lines || lines.length === 0) {
|
||||
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
|
||||
i18n.t('logs.empty_title') + '</p><p data-i18n="logs.empty_desc">' +
|
||||
i18n.t('logs.empty_desc') + '</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤掉 /v0/management/logs 相关的日志
|
||||
const filteredLines = lines.filter(line => !line.includes('/v0/management/logs'));
|
||||
|
||||
// 限制前端显示的最大行数为 10000 行
|
||||
const MAX_DISPLAY_LINES = 10000;
|
||||
let displayedLines = filteredLines;
|
||||
let displayedLineCount = filteredLines.length;
|
||||
|
||||
if (filteredLines.length > MAX_DISPLAY_LINES) {
|
||||
const linesToRemove = filteredLines.length - MAX_DISPLAY_LINES;
|
||||
displayedLines = filteredLines.slice(linesToRemove);
|
||||
displayedLineCount = MAX_DISPLAY_LINES;
|
||||
}
|
||||
|
||||
// 将数组转换为文本
|
||||
const logsText = displayedLines.join('\n');
|
||||
const logHtml = `
|
||||
<div class="logs-info">
|
||||
<span><i class="fas fa-list-ol"></i> ${displayedLineCount} ${i18n.t('logs.lines')}</span>
|
||||
</div>
|
||||
<pre class="logs-text">${this.escapeHtml(logsText)}</pre>
|
||||
`;
|
||||
logsContent.innerHTML = logHtml;
|
||||
|
||||
// 自动滚动到底部
|
||||
if (scrollToBottom) {
|
||||
const logsTextElement = logsContent.querySelector('.logs-text');
|
||||
if (logsTextElement) {
|
||||
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 追加新日志(增量加载)
|
||||
appendLogs(newLines, totalLineCount) {
|
||||
const logsContent = document.getElementById('logs-content');
|
||||
if (!logsContent) return;
|
||||
|
||||
const logsTextElement = logsContent.querySelector('.logs-text');
|
||||
const logsInfoElement = logsContent.querySelector('.logs-info');
|
||||
|
||||
if (!logsTextElement || !newLines || newLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤掉 /v0/management/logs 相关的日志
|
||||
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/logs'));
|
||||
if (filteredNewLines.length === 0) {
|
||||
return; // 如果过滤后没有新日志,直接返回
|
||||
}
|
||||
|
||||
// 检查用户是否正在查看底部(判断是否需要自动滚动)
|
||||
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
|
||||
|
||||
// 追加新日志文本
|
||||
const newLogsText = '\n' + filteredNewLines.join('\n');
|
||||
logsTextElement.textContent += newLogsText;
|
||||
|
||||
// 限制前端显示的最大行数为 10000 行
|
||||
const MAX_DISPLAY_LINES = 10000;
|
||||
const allLines = logsTextElement.textContent.split('\n').filter(line => line.trim());
|
||||
if (allLines.length > MAX_DISPLAY_LINES) {
|
||||
const linesToRemove = allLines.length - MAX_DISPLAY_LINES;
|
||||
logsTextElement.textContent = allLines.slice(linesToRemove).join('\n');
|
||||
}
|
||||
|
||||
// 更新行数统计(只显示实际显示的行数,最多 10000 行)
|
||||
if (logsInfoElement) {
|
||||
const displayedLines = logsTextElement.textContent.split('\n').filter(line => line.trim()).length;
|
||||
logsInfoElement.innerHTML = `<span><i class="fas fa-list-ol"></i> ${displayedLines} ${i18n.t('logs.lines')}</span>`;
|
||||
}
|
||||
|
||||
// 如果用户在底部,自动滚动到新内容
|
||||
if (isAtBottom) {
|
||||
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 下载日志
|
||||
async downloadLogs() {
|
||||
try {
|
||||
const response = await this.makeRequest('/logs', {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response && response.lines && response.lines.length > 0) {
|
||||
// 将数组转换为文本
|
||||
const logsText = response.lines.join('\n');
|
||||
const blob = new Blob([logsText], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `cli-proxy-api-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.log`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
this.showNotification(i18n.t('logs.download_success'), 'success');
|
||||
} else {
|
||||
this.showNotification(i18n.t('logs.empty_title'), 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载日志失败:', error);
|
||||
this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async clearLogs() {
|
||||
if (!confirm(i18n.t('logs.clear_confirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('/logs', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
// 根据返回的 removed 数量显示通知
|
||||
if (response && response.status === 'ok') {
|
||||
const removedCount = response.removed || 0;
|
||||
const message = `${i18n.t('logs.clear_success')} (${i18n.t('logs.removed')}: ${removedCount} ${i18n.t('logs.lines')})`;
|
||||
this.showNotification(message, 'success');
|
||||
} else {
|
||||
this.showNotification(i18n.t('logs.clear_success'), 'success');
|
||||
}
|
||||
|
||||
// 重置时间戳
|
||||
this.latestLogTimestamp = null;
|
||||
// 全量刷新
|
||||
await this.refreshLogs(false);
|
||||
} catch (error) {
|
||||
console.error('清空日志失败:', error);
|
||||
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换日志自动刷新
|
||||
toggleLogsAutoRefresh(enabled) {
|
||||
if (enabled) {
|
||||
// 启动自动刷新
|
||||
if (this.logsRefreshTimer) {
|
||||
clearInterval(this.logsRefreshTimer);
|
||||
}
|
||||
this.logsRefreshTimer = setInterval(() => {
|
||||
const logsSection = document.getElementById('logs');
|
||||
// 只在日志页面可见时刷新
|
||||
if (logsSection && logsSection.classList.contains('active')) {
|
||||
// 使用增量加载
|
||||
this.refreshLogs(true);
|
||||
}
|
||||
}, 5000); // 每5秒刷新一次
|
||||
this.showNotification(i18n.t('logs.auto_refresh_enabled'), 'success');
|
||||
} else {
|
||||
// 停止自动刷新
|
||||
if (this.logsRefreshTimer) {
|
||||
clearInterval(this.logsRefreshTimer);
|
||||
this.logsRefreshTimer = null;
|
||||
}
|
||||
this.showNotification(i18n.t('logs.auto_refresh_disabled'), 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义工具函数
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 更新项目切换设置
|
||||
async updateSwitchProject(enabled) {
|
||||
try {
|
||||
@@ -1557,7 +1879,7 @@ class CLIProxyManager {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['generative-language-api-key']) {
|
||||
this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
await this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Gemini密钥失败:', error);
|
||||
@@ -1565,7 +1887,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Gemini密钥列表
|
||||
renderGeminiKeys(keys) {
|
||||
async renderGeminiKeys(keys) {
|
||||
const container = document.getElementById('gemini-keys-list');
|
||||
|
||||
if (keys.length === 0) {
|
||||
@@ -1579,11 +1901,24 @@ class CLIProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = keys.map((key, index) => `
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((key, index) => {
|
||||
const keyStats = stats[key] || { success: 0, failure: 0 };
|
||||
return `
|
||||
<div class="key-item">
|
||||
<div class="item-content">
|
||||
<div class="item-title">${i18n.t('ai_providers.gemini_item_title')} #${index + 1}</div>
|
||||
<div class="item-value">${this.maskApiKey(key)}</div>
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editGeminiKey(${index}, '${key}')">
|
||||
@@ -1594,7 +1929,7 @@ class CLIProxyManager {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// 显示添加Gemini密钥模态框
|
||||
@@ -1708,7 +2043,7 @@ class CLIProxyManager {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['codex-api-key']) {
|
||||
this.renderCodexKeys(config['codex-api-key']);
|
||||
await this.renderCodexKeys(config['codex-api-key']);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Codex密钥失败:', error);
|
||||
@@ -1716,7 +2051,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Codex密钥列表
|
||||
renderCodexKeys(keys) {
|
||||
async renderCodexKeys(keys) {
|
||||
const container = document.getElementById('codex-keys-list');
|
||||
|
||||
if (keys.length === 0) {
|
||||
@@ -1730,13 +2065,26 @@ class CLIProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = keys.map((config, index) => `
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((config, index) => {
|
||||
const keyStats = stats[config['api-key']] || { success: 0, failure: 0 };
|
||||
return `
|
||||
<div class="provider-item">
|
||||
<div class="item-content">
|
||||
<div class="item-title">${i18n.t('ai_providers.codex_item_title')} #${index + 1}</div>
|
||||
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
|
||||
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
|
||||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})">
|
||||
@@ -1747,7 +2095,7 @@ class CLIProxyManager {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// 显示添加Codex密钥模态框
|
||||
@@ -1898,7 +2246,7 @@ class CLIProxyManager {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['claude-api-key']) {
|
||||
this.renderClaudeKeys(config['claude-api-key']);
|
||||
await this.renderClaudeKeys(config['claude-api-key']);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Claude密钥失败:', error);
|
||||
@@ -1906,7 +2254,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染Claude密钥列表
|
||||
renderClaudeKeys(keys) {
|
||||
async renderClaudeKeys(keys) {
|
||||
const container = document.getElementById('claude-keys-list');
|
||||
|
||||
if (keys.length === 0) {
|
||||
@@ -1920,13 +2268,26 @@ class CLIProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = keys.map((config, index) => `
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((config, index) => {
|
||||
const keyStats = stats[config['api-key']] || { success: 0, failure: 0 };
|
||||
return `
|
||||
<div class="provider-item">
|
||||
<div class="item-content">
|
||||
<div class="item-title">${i18n.t('ai_providers.claude_item_title')} #${index + 1}</div>
|
||||
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
|
||||
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
|
||||
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${keyStats.success}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${keyStats.failure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})">
|
||||
@@ -1937,7 +2298,7 @@ class CLIProxyManager {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// 显示添加Claude密钥模态框
|
||||
@@ -2088,7 +2449,7 @@ class CLIProxyManager {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['openai-compatibility']) {
|
||||
this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
await this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载OpenAI提供商失败:', error);
|
||||
@@ -2096,7 +2457,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 渲染OpenAI提供商列表
|
||||
renderOpenAIProviders(providers) {
|
||||
async renderOpenAIProviders(providers) {
|
||||
const container = document.getElementById('openai-providers-list');
|
||||
|
||||
if (providers.length === 0) {
|
||||
@@ -2110,7 +2471,21 @@ class CLIProxyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = providers.map((provider, index) => `
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = providers.map((provider, index) => {
|
||||
const apiKeyEntries = provider['api-key-entries'] || [];
|
||||
let totalSuccess = 0;
|
||||
let totalFailure = 0;
|
||||
|
||||
apiKeyEntries.forEach(entry => {
|
||||
const keyStats = stats[entry['api-key']] || { success: 0, failure: 0 };
|
||||
totalSuccess += keyStats.success;
|
||||
totalFailure += keyStats.failure;
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="provider-item">
|
||||
<div class="item-content">
|
||||
<div class="item-title">${this.escapeHtml(provider.name)}</div>
|
||||
@@ -2118,6 +2493,14 @@ class CLIProxyManager {
|
||||
<div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}</div>
|
||||
<div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}</div>
|
||||
${this.renderOpenAIModelBadges(provider.models || [])}
|
||||
<div class="item-stats">
|
||||
<span class="stat-badge stat-success">
|
||||
<i class="fas fa-check-circle"></i> 成功: ${totalSuccess}
|
||||
</span>
|
||||
<span class="stat-badge stat-failure">
|
||||
<i class="fas fa-times-circle"></i> 失败: ${totalFailure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(provider).replace(/"/g, '"')})">
|
||||
@@ -2128,7 +2511,7 @@ class CLIProxyManager {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// 显示添加OpenAI提供商模态框
|
||||
@@ -3221,6 +3604,53 @@ class CLIProxyManager {
|
||||
tokensChart = null;
|
||||
currentUsageData = null;
|
||||
|
||||
// 获取API密钥的统计信息
|
||||
async getKeyStats() {
|
||||
try {
|
||||
const response = await this.makeRequest('/usage');
|
||||
const usage = response?.usage || null;
|
||||
|
||||
if (!usage) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sourceStats = {};
|
||||
const apis = usage.apis || {};
|
||||
|
||||
Object.values(apis).forEach(apiEntry => {
|
||||
const models = apiEntry.models || {};
|
||||
|
||||
Object.values(models).forEach(modelEntry => {
|
||||
const details = modelEntry.details || [];
|
||||
|
||||
details.forEach(detail => {
|
||||
const source = detail.source;
|
||||
if (!source) return;
|
||||
|
||||
if (!sourceStats[source]) {
|
||||
sourceStats[source] = {
|
||||
success: 0,
|
||||
failure: 0
|
||||
};
|
||||
}
|
||||
|
||||
const success = detail.success;
|
||||
if (success === false) {
|
||||
sourceStats[source].failure += 1;
|
||||
} else {
|
||||
sourceStats[source].success += 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return sourceStats;
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// 加载使用统计
|
||||
async loadUsageStats() {
|
||||
try {
|
||||
|
||||
50
i18n.js
50
i18n.js
@@ -82,6 +82,7 @@ const i18n = {
|
||||
'nav.ai_providers': 'AI 提供商',
|
||||
'nav.auth_files': '认证文件',
|
||||
'nav.usage_stats': '使用统计',
|
||||
'nav.logs': '日志查看',
|
||||
'nav.system_info': '系统信息',
|
||||
|
||||
// 基础设置
|
||||
@@ -101,6 +102,8 @@ const i18n = {
|
||||
'basic_settings.quota_switch_preview': '切换到预览模型',
|
||||
'basic_settings.usage_statistics_title': '使用统计',
|
||||
'basic_settings.usage_statistics_enable': '启用使用统计',
|
||||
'basic_settings.logging_title': '日志记录',
|
||||
'basic_settings.logging_to_file_enable': '启用日志记录到文件',
|
||||
|
||||
// API 密钥管理
|
||||
'api_keys.title': 'API 密钥管理',
|
||||
@@ -305,6 +308,27 @@ const i18n = {
|
||||
'usage_stats.models': '模型统计',
|
||||
'usage_stats.success_rate': '成功率',
|
||||
|
||||
// 日志查看
|
||||
'logs.title': '日志查看',
|
||||
'logs.refresh_button': '刷新日志',
|
||||
'logs.clear_button': '清空日志',
|
||||
'logs.download_button': '下载日志',
|
||||
'logs.empty_title': '暂无日志记录',
|
||||
'logs.empty_desc': '当启用"日志记录到文件"功能后,日志将显示在这里',
|
||||
'logs.log_content': '日志内容',
|
||||
'logs.loading': '正在加载日志...',
|
||||
'logs.load_error': '加载日志失败',
|
||||
'logs.clear_confirm': '确定要清空所有日志吗?此操作不可恢复!',
|
||||
'logs.clear_success': '日志已清空',
|
||||
'logs.download_success': '日志下载成功',
|
||||
'logs.auto_refresh': '自动刷新',
|
||||
'logs.auto_refresh_enabled': '自动刷新已开启',
|
||||
'logs.auto_refresh_disabled': '自动刷新已关闭',
|
||||
'logs.lines': '行',
|
||||
'logs.removed': '已删除',
|
||||
'logs.upgrade_required_title': '需要升级 CLI Proxy API',
|
||||
'logs.upgrade_required_desc': '当前服务器版本不支持日志查看功能,请升级到最新版本的 CLI Proxy API 以使用此功能。',
|
||||
|
||||
// 系统信息
|
||||
'system_info.title': '系统信息',
|
||||
'system_info.connection_status_title': '连接状态',
|
||||
@@ -324,6 +348,7 @@ const i18n = {
|
||||
'notification.quota_switch_project_updated': '项目切换设置已更新',
|
||||
'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
|
||||
'notification.usage_statistics_updated': '使用统计设置已更新',
|
||||
'notification.logging_to_file_updated': '日志记录设置已更新',
|
||||
'notification.api_key_added': 'API密钥添加成功',
|
||||
'notification.api_key_updated': 'API密钥更新成功',
|
||||
'notification.api_key_deleted': 'API密钥删除成功',
|
||||
@@ -453,6 +478,7 @@ const i18n = {
|
||||
'nav.ai_providers': 'AI Providers',
|
||||
'nav.auth_files': 'Auth Files',
|
||||
'nav.usage_stats': 'Usage Statistics',
|
||||
'nav.logs': 'Logs Viewer',
|
||||
'nav.system_info': 'System Info',
|
||||
|
||||
// Basic settings
|
||||
@@ -472,6 +498,8 @@ const i18n = {
|
||||
'basic_settings.quota_switch_preview': 'Switch to Preview Model',
|
||||
'basic_settings.usage_statistics_title': 'Usage Statistics',
|
||||
'basic_settings.usage_statistics_enable': 'Enable usage statistics',
|
||||
'basic_settings.logging_title': 'Logging',
|
||||
'basic_settings.logging_to_file_enable': 'Enable logging to file',
|
||||
|
||||
// API Keys management
|
||||
'api_keys.title': 'API Keys Management',
|
||||
@@ -675,6 +703,27 @@ const i18n = {
|
||||
'usage_stats.models': 'Model Statistics',
|
||||
'usage_stats.success_rate': 'Success Rate',
|
||||
|
||||
// Logs viewer
|
||||
'logs.title': 'Logs Viewer',
|
||||
'logs.refresh_button': 'Refresh Logs',
|
||||
'logs.clear_button': 'Clear Logs',
|
||||
'logs.download_button': 'Download Logs',
|
||||
'logs.empty_title': 'No Logs Available',
|
||||
'logs.empty_desc': 'When "Enable logging to file" is enabled, logs will be displayed here',
|
||||
'logs.log_content': 'Log Content',
|
||||
'logs.loading': 'Loading logs...',
|
||||
'logs.load_error': 'Failed to load logs',
|
||||
'logs.clear_confirm': 'Are you sure you want to clear all logs? This action cannot be undone!',
|
||||
'logs.clear_success': 'Logs cleared successfully',
|
||||
'logs.download_success': 'Logs downloaded successfully',
|
||||
'logs.auto_refresh': 'Auto Refresh',
|
||||
'logs.auto_refresh_enabled': 'Auto refresh enabled',
|
||||
'logs.auto_refresh_disabled': 'Auto refresh disabled',
|
||||
'logs.lines': 'lines',
|
||||
'logs.removed': 'Removed',
|
||||
'logs.upgrade_required_title': 'Please Upgrade CLI Proxy API',
|
||||
'logs.upgrade_required_desc': 'The current server version does not support the logs viewing feature. Please upgrade to the latest version of CLI Proxy API to use this feature.',
|
||||
|
||||
// System info
|
||||
'system_info.title': 'System Information',
|
||||
'system_info.connection_status_title': 'Connection Status',
|
||||
@@ -694,6 +743,7 @@ const i18n = {
|
||||
'notification.quota_switch_project_updated': 'Project switch settings updated',
|
||||
'notification.quota_switch_preview_updated': 'Preview model switch settings updated',
|
||||
'notification.usage_statistics_updated': 'Usage statistics settings updated',
|
||||
'notification.logging_to_file_updated': 'Logging settings updated',
|
||||
'notification.api_key_added': 'API key added successfully',
|
||||
'notification.api_key_updated': 'API key updated successfully',
|
||||
'notification.api_key_deleted': 'API key deleted successfully',
|
||||
|
||||
62
index.html
62
index.html
@@ -171,6 +171,9 @@
|
||||
<li data-tooltip="使用统计"><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||
</a></li>
|
||||
<li id="logs-nav-item" data-tooltip="日志查看" style="display: none;"><a href="#logs" class="nav-item" data-section="logs">
|
||||
<i class="fas fa-scroll"></i> <span data-i18n="nav.logs">日志查看</span>
|
||||
</a></li>
|
||||
<li data-tooltip="系统信息"><a href="#system-info" class="nav-item" data-section="system-info">
|
||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||
</a></li>
|
||||
@@ -293,6 +296,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志记录设置 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-file-alt"></i> <span
|
||||
data-i18n="basic_settings.logging_title">日志记录</span></h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="logging-to-file-toggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label"
|
||||
data-i18n="basic_settings.logging_to_file_enable">启用日志记录到文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- API 密钥管理 -->
|
||||
@@ -601,6 +622,40 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 日志查看 -->
|
||||
<section id="logs" class="content-section">
|
||||
<h2 data-i18n="logs.title">日志查看</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-scroll"></i> <span data-i18n="logs.log_content">日志内容</span></h3>
|
||||
<div class="header-actions">
|
||||
<div class="toggle-group" style="margin-right: 15px;">
|
||||
<label class="toggle-switch" style="margin-right: 5px;">
|
||||
<input type="checkbox" id="logs-auto-refresh-toggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label" data-i18n="logs.auto_refresh" style="font-size: 0.9em;">自动刷新</span>
|
||||
</div>
|
||||
<button id="refresh-logs" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt"></i> <span data-i18n="logs.refresh_button">刷新日志</span>
|
||||
</button>
|
||||
<button id="download-logs" class="btn btn-secondary">
|
||||
<i class="fas fa-download"></i> <span data-i18n="logs.download_button">下载日志</span>
|
||||
</button>
|
||||
<button id="clear-logs" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> <span data-i18n="logs.clear_button">清空日志</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div id="logs-content" class="logs-container">
|
||||
<div class="loading-placeholder" data-i18n="logs.loading">正在加载日志...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 使用统计 -->
|
||||
<section id="usage-stats" class="content-section">
|
||||
<h2 data-i18n="usage_stats.title">使用统计</h2>
|
||||
@@ -730,13 +785,6 @@
|
||||
</div>
|
||||
<div class="info-value" id="display-api-url">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-key"></i>
|
||||
<span data-i18n="connection.management_key">管理密钥:</span>
|
||||
</div>
|
||||
<div class="info-value" id="display-management-key">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<i class="fas fa-circle"></i>
|
||||
|
||||
260
styles.css
260
styles.css
@@ -2664,4 +2664,264 @@ input:checked+.slider:before {
|
||||
#iflow-oauth-url {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
/* 日志查看样式 */
|
||||
.logs-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 500px;
|
||||
max-height: calc(100vh - 400px);
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.logs-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.logs-info span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.logs-text {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: calc(100vh - 480px);
|
||||
min-height: 450px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logs-text::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.logs-text::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logs-text::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logs-text::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-quaternary);
|
||||
}
|
||||
|
||||
/* 空状态和错误状态 */
|
||||
.logs-container .empty-state,
|
||||
.logs-container .error-state,
|
||||
.logs-container .upgrade-notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.logs-container .empty-state i,
|
||||
.logs-container .error-state i,
|
||||
.logs-container .upgrade-notice i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--border-secondary);
|
||||
}
|
||||
|
||||
.logs-container .error-state i {
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.logs-container .upgrade-notice i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.logs-container .empty-state p,
|
||||
.logs-container .error-state p,
|
||||
.logs-container .upgrade-notice p {
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logs-container .empty-state p:first-of-type,
|
||||
.logs-container .error-state p:first-of-type {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.logs-container .upgrade-notice h3 {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.logs-container .upgrade-notice p {
|
||||
color: var(--text-tertiary);
|
||||
max-width: 500px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 日志页面头部操作区域 */
|
||||
#logs .card-header .header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#logs .card-header .header-actions .toggle-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 日志查看 */
|
||||
@media (max-width: 768px) {
|
||||
.logs-container {
|
||||
min-height: 300px;
|
||||
max-height: calc(100vh - 350px);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.logs-text {
|
||||
font-size: 11px;
|
||||
padding: 12px;
|
||||
max-height: calc(100vh - 430px);
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.logs-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#logs .card-header .header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#logs .card-header .header-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#logs .card-header .header-actions .btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#logs .card-header .header-actions .toggle-group {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
[data-theme="dark"] .logs-text {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-color: var(--border-primary);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-container {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
}
|
||||
|
||||
/* ===== AI提供商统计徽章样式 ===== */
|
||||
|
||||
/* 统计信息容器 */
|
||||
.item-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 统计徽章基础样式 */
|
||||
.stat-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 成功统计徽章 */
|
||||
.stat-badge.stat-success {
|
||||
background-color: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
border: 1px solid var(--success-border);
|
||||
}
|
||||
|
||||
.stat-badge.stat-success i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 失败统计徽章 */
|
||||
.stat-badge.stat-failure {
|
||||
background-color: var(--error-bg);
|
||||
color: var(--error-text);
|
||||
border: 1px solid var(--error-border);
|
||||
}
|
||||
|
||||
.stat-badge.stat-failure i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.stat-badge:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
[data-theme="dark"] .stat-badge.stat-success {
|
||||
background-color: rgba(6, 78, 59, 0.3);
|
||||
color: #6ee7b7;
|
||||
border-color: rgba(5, 150, 105, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stat-badge.stat-failure {
|
||||
background-color: rgba(153, 27, 27, 0.3);
|
||||
color: #fca5a5;
|
||||
border-color: rgba(220, 38, 38, 0.5);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.item-stats {
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stat-badge i {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user