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
9 Commits
v0.1.2
...
v0.1.6-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcfffc716b | ||
|
|
7de5280824 | ||
|
|
86d60aad77 | ||
|
|
020fccc032 | ||
|
|
c162ab3a54 | ||
|
|
85d12e15d8 | ||
|
|
ebffb49f52 | ||
|
|
316c1ffc0d | ||
|
|
b3e54e7f14 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -27,6 +27,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build all-in-one HTML
|
- name: Build all-in-one HTML
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Prepare release assets
|
- name: Prepare release assets
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ package-lock.json
|
|||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
@@ -10,7 +10,7 @@ Example URL:
|
|||||||
https://remote.router-for.me/
|
https://remote.router-for.me/
|
||||||
|
|
||||||
Minimum required version: ≥ 6.0.0
|
Minimum required version: ≥ 6.0.0
|
||||||
Recommended version: ≥ 6.0.19
|
Recommended version: ≥ 6.1.3
|
||||||
|
|
||||||
Since version 6.0.19, the WebUI has been rolled into the main program. You can access it by going to `/management.html` on the external port after firing up the main project.
|
Since version 6.0.19, the WebUI has been rolled into the main program. You can access it by going to `/management.html` on the external port after firing up the main project.
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ Since version 6.0.19, the WebUI has been rolled into the main program. You can a
|
|||||||
- Download existing authentication files
|
- Download existing authentication files
|
||||||
- Delete single or all authentication files
|
- Delete single or all authentication files
|
||||||
- Display file details
|
- Display file details
|
||||||
- **Gemini Web Token**: Direct authentication using browser cookies
|
|
||||||
|
|
||||||
### Usage Statistics
|
### Usage Statistics
|
||||||
- **Real-time Analytics**: Track API usage with interactive charts
|
- **Real-time Analytics**: Track API usage with interactive charts
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ https://github.com/router-for-me/CLIProxyAPI
|
|||||||
https://remote.router-for.me/
|
https://remote.router-for.me/
|
||||||
|
|
||||||
最低可用版本 ≥ 6.0.0
|
最低可用版本 ≥ 6.0.0
|
||||||
推荐版本 ≥ 6.0.19
|
|
||||||
|
推荐版本 ≥ 6.1.3
|
||||||
|
|
||||||
自6.0.19起WebUI已经集成在主程序中 可以通过主项目开启的外部端口的`/management.html`访问
|
自6.0.19起WebUI已经集成在主程序中 可以通过主项目开启的外部端口的`/management.html`访问
|
||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
@@ -40,7 +42,6 @@ https://remote.router-for.me/
|
|||||||
- 下载现有认证文件
|
- 下载现有认证文件
|
||||||
- 删除单个或所有认证文件
|
- 删除单个或所有认证文件
|
||||||
- 显示文件详细信息
|
- 显示文件详细信息
|
||||||
- **Gemini Web Token**: 使用浏览器 Cookie 直接认证
|
|
||||||
|
|
||||||
### 使用统计
|
### 使用统计
|
||||||
- **实时分析**: 通过交互式图表跟踪 API 使用情况
|
- **实时分析**: 通过交互式图表跟踪 API 使用情况
|
||||||
|
|||||||
483
app.js
483
app.js
@@ -17,6 +17,12 @@ class CLIProxyManager {
|
|||||||
// 状态更新定时器
|
// 状态更新定时器
|
||||||
this.statusUpdateTimer = null;
|
this.statusUpdateTimer = null;
|
||||||
|
|
||||||
|
// 日志自动刷新定时器
|
||||||
|
this.logsRefreshTimer = null;
|
||||||
|
|
||||||
|
// 日志时间戳(用于增量加载)
|
||||||
|
this.latestLogTimestamp = null;
|
||||||
|
|
||||||
// 主题管理
|
// 主题管理
|
||||||
this.currentTheme = 'light';
|
this.currentTheme = 'light';
|
||||||
|
|
||||||
@@ -494,6 +500,7 @@ class CLIProxyManager {
|
|||||||
const updateRetry = document.getElementById('update-retry');
|
const updateRetry = document.getElementById('update-retry');
|
||||||
const switchProjectToggle = document.getElementById('switch-project-toggle');
|
const switchProjectToggle = document.getElementById('switch-project-toggle');
|
||||||
const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
|
const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
|
||||||
|
const usageStatisticsToggle = document.getElementById('usage-statistics-enabled-toggle');
|
||||||
|
|
||||||
if (debugToggle) {
|
if (debugToggle) {
|
||||||
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
|
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
|
||||||
@@ -513,6 +520,34 @@ class CLIProxyManager {
|
|||||||
if (switchPreviewToggle) {
|
if (switchPreviewToggle) {
|
||||||
switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
|
switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
|
||||||
}
|
}
|
||||||
|
if (usageStatisticsToggle) {
|
||||||
|
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 密钥管理
|
// API 密钥管理
|
||||||
const addApiKey = document.getElementById('add-api-key');
|
const addApiKey = document.getElementById('add-api-key');
|
||||||
@@ -538,12 +573,6 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Gemini Web Token
|
|
||||||
const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn');
|
|
||||||
if (geminiWebTokenBtn) {
|
|
||||||
geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 认证文件管理
|
// 认证文件管理
|
||||||
const uploadAuthFile = document.getElementById('upload-auth-file');
|
const uploadAuthFile = document.getElementById('upload-auth-file');
|
||||||
const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
|
const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
|
||||||
@@ -816,6 +845,11 @@ class CLIProxyManager {
|
|||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
const sectionId = item.getAttribute('data-section');
|
const sectionId = item.getAttribute('data-section');
|
||||||
document.getElementById(sectionId).classList.add('active');
|
document.getElementById(sectionId).classList.add('active');
|
||||||
|
|
||||||
|
// 如果点击的是日志查看页面,自动加载日志
|
||||||
|
if (sectionId === 'logs') {
|
||||||
|
this.refreshLogs(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1158,6 +1192,22 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config['usage-statistics-enabled'] !== undefined) {
|
||||||
|
const usageToggle = document.getElementById('usage-statistics-enabled-toggle');
|
||||||
|
if (usageToggle) {
|
||||||
|
usageToggle.checked = config['usage-statistics-enabled'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志记录设置
|
||||||
|
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 密钥
|
// API 密钥
|
||||||
if (config['api-keys']) {
|
if (config['api-keys']) {
|
||||||
@@ -1192,6 +1242,7 @@ class CLIProxyManager {
|
|||||||
this.loadProxySettings(),
|
this.loadProxySettings(),
|
||||||
this.loadRetrySettings(),
|
this.loadRetrySettings(),
|
||||||
this.loadQuotaSettings(),
|
this.loadQuotaSettings(),
|
||||||
|
this.loadUsageStatisticsSettings(),
|
||||||
this.loadApiKeys(),
|
this.loadApiKeys(),
|
||||||
this.loadGeminiKeys(),
|
this.loadGeminiKeys(),
|
||||||
this.loadCodexKeys(),
|
this.loadCodexKeys(),
|
||||||
@@ -1314,6 +1365,325 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载使用统计设置
|
||||||
|
async loadUsageStatisticsSettings() {
|
||||||
|
try {
|
||||||
|
const config = await this.getConfig();
|
||||||
|
if (config['usage-statistics-enabled'] !== undefined) {
|
||||||
|
const usageToggle = document.getElementById('usage-statistics-enabled-toggle');
|
||||||
|
if (usageToggle) {
|
||||||
|
usageToggle.checked = config['usage-statistics-enabled'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载使用统计设置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新使用统计设置
|
||||||
|
async updateUsageStatisticsEnabled(enabled) {
|
||||||
|
try {
|
||||||
|
await this.makeRequest('/usage-statistics-enabled', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ value: enabled })
|
||||||
|
});
|
||||||
|
this.clearCache();
|
||||||
|
this.showNotification(i18n.t('notification.usage_statistics_updated'), 'success');
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||||
|
const usageToggle = document.getElementById('usage-statistics-enabled-toggle');
|
||||||
|
if (usageToggle) {
|
||||||
|
usageToggle.checked = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志记录到文件设置
|
||||||
|
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) {
|
async updateSwitchProject(enabled) {
|
||||||
try {
|
try {
|
||||||
@@ -2430,107 +2800,6 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 显示 Gemini Web Token 模态框
|
|
||||||
showGeminiWebTokenModal() {
|
|
||||||
const inlineSecure1psid = document.getElementById('secure-1psid-input');
|
|
||||||
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
|
|
||||||
const inlineLabel = document.getElementById('gemini-web-label-input');
|
|
||||||
const modalBody = document.getElementById('modal-body');
|
|
||||||
modalBody.innerHTML = `
|
|
||||||
<h3>${i18n.t('auth_login.gemini_web_button')}</h3>
|
|
||||||
<div class="gemini-web-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="modal-secure-1psid">${i18n.t('auth_login.secure_1psid_label')}</label>
|
|
||||||
<input type="text" id="modal-secure-1psid" placeholder="${i18n.t('auth_login.secure_1psid_placeholder')}" required>
|
|
||||||
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="modal-secure-1psidts">${i18n.t('auth_login.secure_1psidts_label')}</label>
|
|
||||||
<input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required>
|
|
||||||
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="modal-gemini-web-label">${i18n.t('auth_login.gemini_web_label_label')}</label>
|
|
||||||
<input type="text" id="modal-gemini-web-label" placeholder="${i18n.t('auth_login.gemini_web_label_placeholder')}">
|
|
||||||
<div class="form-hint">为此认证文件设置一个标签名称(可选)</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
|
||||||
<button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
this.showModal();
|
|
||||||
|
|
||||||
const modalSecure1psid = document.getElementById('modal-secure-1psid');
|
|
||||||
const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
|
|
||||||
const modalLabel = document.getElementById('modal-gemini-web-label');
|
|
||||||
|
|
||||||
if (modalSecure1psid && inlineSecure1psid) {
|
|
||||||
modalSecure1psid.value = inlineSecure1psid.value.trim();
|
|
||||||
}
|
|
||||||
if (modalSecure1psidts && inlineSecure1psidts) {
|
|
||||||
modalSecure1psidts.value = inlineSecure1psidts.value.trim();
|
|
||||||
}
|
|
||||||
if (modalLabel && inlineLabel) {
|
|
||||||
modalLabel.value = inlineLabel.value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modalSecure1psid) {
|
|
||||||
modalSecure1psid.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存 Gemini Web Token
|
|
||||||
async saveGeminiWebToken() {
|
|
||||||
const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
|
|
||||||
const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
|
|
||||||
const label = document.getElementById('modal-gemini-web-label').value.trim();
|
|
||||||
|
|
||||||
if (!secure1psid || !secure1psidts) {
|
|
||||||
this.showNotification('请填写完整的 Cookie 信息', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestBody = {
|
|
||||||
secure_1psid: secure1psid,
|
|
||||||
secure_1psidts: secure1psidts
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果提供了 label,则添加到请求体中
|
|
||||||
if (label) {
|
|
||||||
requestBody.label = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.makeRequest('/gemini-web-token', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.closeModal();
|
|
||||||
this.loadAuthFiles(); // 刷新认证文件列表
|
|
||||||
const inlineSecure1psid = document.getElementById('secure-1psid-input');
|
|
||||||
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
|
|
||||||
const inlineLabel = document.getElementById('gemini-web-label-input');
|
|
||||||
if (inlineSecure1psid) {
|
|
||||||
inlineSecure1psid.value = secure1psid;
|
|
||||||
}
|
|
||||||
if (inlineSecure1psidts) {
|
|
||||||
inlineSecure1psidts.value = secure1psidts;
|
|
||||||
}
|
|
||||||
if (inlineLabel) {
|
|
||||||
inlineLabel.value = label;
|
|
||||||
}
|
|
||||||
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
|
|
||||||
} catch (error) {
|
|
||||||
this.showNotification(`保存失败: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Codex OAuth 相关方法 =====
|
// ===== Codex OAuth 相关方法 =====
|
||||||
|
|
||||||
// 开始 Codex OAuth 流程
|
// 开始 Codex OAuth 流程
|
||||||
|
|||||||
34
build.js
34
build.js
@@ -49,6 +49,35 @@ function escapeForStyle(content) {
|
|||||||
return content.replace(/<\/(style)/gi, '<\\/$1');
|
return content.replace(/<\/(style)/gi, '<\\/$1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVersion() {
|
||||||
|
// 1. 优先从环境变量获取(GitHub Actions 会设置)
|
||||||
|
if (process.env.VERSION) {
|
||||||
|
return process.env.VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试从 git tag 获取
|
||||||
|
try {
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const gitTag = execSync('git describe --tags --exact-match 2>/dev/null || git describe --tags 2>/dev/null || echo ""', { encoding: 'utf8' }).trim();
|
||||||
|
if (gitTag) {
|
||||||
|
return gitTag;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('无法从 git 获取版本号');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 回退到 package.json
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
||||||
|
return 'v' + packageJson.version;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('无法从 package.json 读取版本号');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 最后使用默认值
|
||||||
|
return 'v0.0.0-dev';
|
||||||
|
}
|
||||||
|
|
||||||
function ensureDistDir() {
|
function ensureDistDir() {
|
||||||
if (fs.existsSync(distDir)) {
|
if (fs.existsSync(distDir)) {
|
||||||
fs.rmSync(distDir, { recursive: true, force: true });
|
fs.rmSync(distDir, { recursive: true, force: true });
|
||||||
@@ -82,6 +111,11 @@ function build() {
|
|||||||
const css = escapeForStyle(readFile(sourceFiles.css));
|
const css = escapeForStyle(readFile(sourceFiles.css));
|
||||||
const i18n = escapeForScript(readFile(sourceFiles.i18n));
|
const i18n = escapeForScript(readFile(sourceFiles.i18n));
|
||||||
const app = escapeForScript(readFile(sourceFiles.app));
|
const app = escapeForScript(readFile(sourceFiles.app));
|
||||||
|
|
||||||
|
// 获取版本号并替换
|
||||||
|
const version = getVersion();
|
||||||
|
console.log(`使用版本号: ${version}`);
|
||||||
|
html = html.replace(/__VERSION__/g, version);
|
||||||
|
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<link rel="stylesheet" href="styles.css">',
|
'<link rel="stylesheet" href="styles.css">',
|
||||||
|
|||||||
116
index.html
116
index.html
@@ -171,6 +171,9 @@
|
|||||||
<li data-tooltip="使用统计"><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
<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>
|
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||||
</a></li>
|
</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">
|
<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>
|
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
@@ -275,6 +278,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用统计设置 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-chart-bar"></i> <span
|
||||||
|
data-i18n="basic_settings.usage_statistics_title">使用统计</span></h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="toggle-group">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="usage-statistics-enabled-toggle">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label"
|
||||||
|
data-i18n="basic_settings.usage_statistics_enable">启用使用统计</span>
|
||||||
|
</div>
|
||||||
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- API 密钥管理 -->
|
<!-- API 密钥管理 -->
|
||||||
@@ -372,45 +411,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gemini Web Token -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3><i class="fab fa-google"></i> <span
|
|
||||||
data-i18n="auth_login.gemini_web_title">Gemini Web Token</span></h3>
|
|
||||||
<button id="gemini-web-token-btn" class="btn btn-primary">
|
|
||||||
<i class="fas fa-save"></i> <span data-i18n="auth_login.gemini_web_button">保存
|
|
||||||
Gemini Web Token</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<p class="form-hint" style="margin-bottom: 20px;"
|
|
||||||
data-i18n="auth_login.gemini_web_hint">
|
|
||||||
从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
|
|
||||||
</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="secure-1psid-input"
|
|
||||||
data-i18n="auth_login.secure_1psid_label">__Secure-1PSID Cookie:</label>
|
|
||||||
<input type="text" id="secure-1psid-input"
|
|
||||||
data-i18n="auth_login.secure_1psid_placeholder"
|
|
||||||
placeholder="输入 __Secure-1PSID cookie 值">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="secure-1psidts-input"
|
|
||||||
data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label>
|
|
||||||
<input type="text" id="secure-1psidts-input"
|
|
||||||
data-i18n="auth_login.secure_1psidts_placeholder"
|
|
||||||
placeholder="输入 __Secure-1PSIDTS cookie 值">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="gemini-web-label-input"
|
|
||||||
data-i18n="auth_login.gemini_web_label_label">Label (Optional):</label>
|
|
||||||
<input type="text" id="gemini-web-label-input"
|
|
||||||
data-i18n="auth_login.gemini_web_label_placeholder"
|
|
||||||
placeholder="输入标签名称 (可选)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 认证文件 -->
|
<!-- 认证文件 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -622,6 +622,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section id="usage-stats" class="content-section">
|
||||||
<h2 data-i18n="usage_stats.title">使用统计</h2>
|
<h2 data-i18n="usage_stats.title">使用统计</h2>
|
||||||
@@ -806,9 +840,9 @@
|
|||||||
<!-- 版本信息 -->
|
<!-- 版本信息 -->
|
||||||
<footer class="version-footer">
|
<footer class="version-footer">
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<span data-i18n="footer.version">版本</span>: v0.1.0
|
<span data-i18n="footer.version">版本</span>: __VERSION__
|
||||||
<span class="separator">•</span>
|
<span class="separator">•</span>
|
||||||
<span data-i18n="footer.author">作者</span>: Supra4E8C
|
<span data-i18n="footer.author">作者</span>: CLI Proxy API Team
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
220
styles.css
220
styles.css
@@ -1923,44 +1923,6 @@ input:checked+.slider:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Gemini Web Token 模态框样式 */
|
|
||||||
.gemini-web-form .form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gemini-web-form .form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gemini-web-form .form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid var(--border-primary);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gemini-web-form .form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--border-focus);
|
|
||||||
box-shadow: 0 0 0 3px var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gemini-web-form .form-hint {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 使用统计样式 */
|
/* 使用统计样式 */
|
||||||
.stats-overview {
|
.stats-overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2702,4 +2664,184 @@ input:checked+.slider:before {
|
|||||||
#iflow-oauth-url {
|
#iflow-oauth-url {
|
||||||
font-size: 12px;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user