Compare commits

..

10 Commits

Author SHA1 Message Date
Supra4E8C
30f5300bb4 Update README_CN.md 2025-10-01 16:10:47 +08:00
Supra4E8C
52169200f1 Update README.md 2025-10-01 16:10:22 +08:00
Supra4E8C
80b2597611 0.0.5
为Cli Proxy API主程序兼容做准备
2025-10-01 16:06:12 +08:00
Luis Pater
04f21eea98 change release file name 2025-10-01 02:00:53 +08:00
Luis Pater
f6a4bae8c6 add auto release script 2025-10-01 01:57:53 +08:00
Supra4E8C
c9f09ccf37 Update README_CN.md 2025-09-25 17:17:44 +08:00
Supra4E8C
5b8fd04ba3 Update README.md 2025-09-25 17:17:00 +08:00
Supra4E8C
3c791a2313 Update README.md 2025-09-25 17:15:15 +08:00
Supra4E8C
2ef64d8064 Add files via upload 2025-09-25 17:14:50 +08:00
Supra4E8C
f2dc4bcf98 Update 0.0.3Beta 2025-09-25 17:04:02 +08:00
14 changed files with 1463 additions and 506 deletions

61
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build all-in-one HTML
run: npm run build
- name: Prepare release assets
run: |
cd dist
mv index.html management.html
ls -lh management.html
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/management.html
body: |
## CLI Proxy API Management Center - ${{ github.ref_name }}
### Download and Usage
1. Download the `management.html` file
2. Open it directly in your browser
3. All assets (CSS, JavaScript, images) are bundled into this single file
### Features
- Single file, no external dependencies required
- Complete management interface for CLI Proxy API
- Support for local and remote connections
- Multi-language support (Chinese/English)
- Dark/Light theme support
---
🤖 Generated with GitHub Actions
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Node modules
node_modules/
# Build output
dist/
# Temporary build files
index.build.html
# npm lock files
package-lock.json
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db

69
BUILD_RELEASE.md Normal file
View File

@@ -0,0 +1,69 @@
# Build and Release Instructions
## Overview
This project uses webpack to bundle all HTML, CSS, JavaScript, and images into a single all-in-one HTML file. The GitHub workflow automatically builds and releases this file when you create a new tag.
## How to Create a Release
1. Make sure all your changes are committed
2. Create and push a new tag:
```bash
git tag v1.0.0
git push origin v1.0.0
```
3. The GitHub workflow will automatically:
- Install dependencies
- Build the all-in-one HTML file using webpack
- Create a new release with the tag
- Upload the bundled HTML file to the release
## Manual Build
To build locally:
```bash
# Install dependencies
npm install
# Build the all-in-one HTML file
npm run build
```
The output will be in the `dist/` directory as `index.html`.
## How It Works
1. **build-scripts/prepare-html.js**: Pre-build script
- Reads the original `index.html`
- Removes local CSS and JavaScript references
- Generates temporary `index.build.html` for webpack
2. **webpack.config.js**: Configures webpack to bundle all assets
- Uses `style-loader` to inline CSS
- Uses `asset/inline` to embed images as base64
- Uses `html-inline-script-webpack-plugin` to inline JavaScript
- Uses `index.build.html` as template (generated dynamically)
3. **bundle-entry.js**: Entry point that imports all resources
- Imports CSS files
- Imports JavaScript modules
- Imports and sets logo image
4. **package.json scripts**:
- `prebuild`: Automatically runs before build to generate `index.build.html`
- `build`: Runs webpack to bundle everything
- `postbuild`: Cleans up temporary `index.build.html` file
5. **.github/workflows/release.yml**: GitHub workflow
- Triggers on tag push
- Builds the project (prebuild → build → postbuild)
- Creates a release with the bundled HTML file
## External Dependencies
The bundled HTML file still relies on these CDN resources:
- Font Awesome (icons)
- Chart.js (charts and graphs)
These are loaded from CDN to keep the file size reasonable and leverage browser caching.

View File

@@ -1,13 +1,17 @@
# Cli-Proxy-API-Management-Center # Cli-Proxy-API-Management-Center
This is a modern web interface for managing the CLI Proxy API. This is a modern web interface for managing the CLI Proxy API.
[中文](README_CN.md) [中文文档](README_CN.md)
Main Project: Main Project:
https://github.com/router-for-me/CLIProxyAPI https://github.com/router-for-me/CLIProxyAPI
Minimum required version: ≥ 5.0.0 Example URL:
Recommended version: ≥ 5.1.1 https://remote.router-for.me/
Minimum required version: ≥ 6.0.0
Recommended version: ≥ 6.0.19
Starting from version 6.0.19, the WebUI has been integrated into the main program and is accessible via `/management.html`.
## Features ## Features

View File

@@ -3,8 +3,12 @@
主项目 主项目
https://github.com/router-for-me/CLIProxyAPI https://github.com/router-for-me/CLIProxyAPI
示例网站:
https://remote.router-for.me/
最低可用版本 ≥ 5.0.0 最低可用版本 ≥ 5.0.0
推荐版本 ≥ 5.1.1 推荐版本 ≥ 5.2.6
自6.0.19起WebUI已经集成在主程序中 可以通过/management.html访问
## 功能特点 ## 功能特点

654
app.js
View File

@@ -2,7 +2,8 @@
class CLIProxyManager { class CLIProxyManager {
constructor() { constructor() {
// 仅保存基础地址(不含 /v0/management请求时自动补齐 // 仅保存基础地址(不含 /v0/management请求时自动补齐
this.apiBase = 'http://localhost:8317'; const detectedBase = this.detectApiBaseFromLocation();
this.apiBase = detectedBase;
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiUrl = this.computeApiUrl(this.apiBase);
this.managementKey = ''; this.managementKey = '';
this.isConnected = false; this.isConnected = false;
@@ -107,6 +108,7 @@ class CLIProxyManager {
this.setupLanguageSwitcher(); this.setupLanguageSwitcher();
this.setupThemeSwitcher(); this.setupThemeSwitcher();
// loadSettings 将在登录成功后调用 // loadSettings 将在登录成功后调用
this.updateLoginConnectionInfo();
} }
// 检查登录状态 // 检查登录状态
@@ -185,6 +187,7 @@ class CLIProxyManager {
document.getElementById('login-page').style.display = 'flex'; document.getElementById('login-page').style.display = 'flex';
document.getElementById('main-page').style.display = 'none'; document.getElementById('main-page').style.display = 'none';
this.isLoggedIn = false; this.isLoggedIn = false;
this.updateLoginConnectionInfo();
} }
// 显示主页面 // 显示主页面
@@ -236,57 +239,42 @@ class CLIProxyManager {
// 处理登录表单提交 // 处理登录表单提交
async handleLogin() { async handleLogin() {
// 获取当前活动的选项卡 const apiBaseInput = document.getElementById('login-api-base');
const activeTab = document.querySelector('.tab-button.active').getAttribute('data-tab'); const managementKeyInput = document.getElementById('login-management-key');
const managementKey = managementKeyInput ? managementKeyInput.value.trim() : '';
let apiUrl, managementKey; if (!managementKey) {
if (activeTab === 'local') {
// 本地连接从端口号构建URL
const port = document.getElementById('local-port').value.trim();
managementKey = document.getElementById('local-management-key').value.trim();
if (!port || !managementKey) {
this.showLoginError(i18n.t('login.error_required')); this.showLoginError(i18n.t('login.error_required'));
return; return;
} }
apiUrl = `http://localhost:${port}`; if (apiBaseInput && apiBaseInput.value.trim()) {
} else { this.setApiBase(apiBaseInput.value.trim());
// 远程连接使用完整URL
apiUrl = document.getElementById('remote-api-url').value.trim();
managementKey = document.getElementById('remote-management-key').value.trim();
if (!apiUrl || !managementKey) {
this.showLoginError(i18n.t('login.error_required'));
return;
} }
}
const proxyUrl = document.getElementById('login-proxy-url').value.trim();
const submitBtn = document.getElementById('login-submit'); const submitBtn = document.getElementById('login-submit');
const originalText = submitBtn.innerHTML; const originalText = submitBtn ? submitBtn.innerHTML : '';
try { try {
if (submitBtn) {
submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`; submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`;
submitBtn.disabled = true; submitBtn.disabled = true;
}
this.hideLoginError(); this.hideLoginError();
// 如果设置了代理,先保存代理设置 this.managementKey = managementKey;
if (proxyUrl) { localStorage.setItem('managementKey', this.managementKey);
localStorage.setItem('proxyUrl', proxyUrl);
}
await this.login(apiUrl, managementKey);
await this.login(this.apiBase, this.managementKey);
} catch (error) { } catch (error) {
this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`); this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
} finally { } finally {
if (submitBtn) {
submitBtn.innerHTML = originalText; submitBtn.innerHTML = originalText;
submitBtn.disabled = false; submitBtn.disabled = false;
} }
} }
}
// 切换登录页面密钥可见性 // 切换登录页面密钥可见性
toggleLoginKeyVisibility(button) { toggleLoginKeyVisibility(button) {
@@ -355,67 +343,30 @@ class CLIProxyManager {
loadLoginSettings() { loadLoginSettings() {
const savedBase = localStorage.getItem('apiBase'); const savedBase = localStorage.getItem('apiBase');
const savedKey = localStorage.getItem('managementKey'); const savedKey = localStorage.getItem('managementKey');
const savedProxy = localStorage.getItem('proxyUrl'); const loginKeyInput = document.getElementById('login-management-key');
const apiBaseInput = document.getElementById('login-api-base');
// 检查元素是否存在(确保在登录页面)
const localPortInput = document.getElementById('local-port');
const remoteApiInput = document.getElementById('remote-api-url');
const localKeyInput = document.getElementById('local-management-key');
const remoteKeyInput = document.getElementById('remote-management-key');
const proxyInput = document.getElementById('login-proxy-url');
// 设置本地端口和远程API地址
if (savedBase) { if (savedBase) {
if (savedBase.includes('localhost')) { this.setApiBase(savedBase);
// 从本地URL中提取端口号 } else {
const match = savedBase.match(/localhost:(\d+)/); this.setApiBase(this.detectApiBaseFromLocation());
if (match && localPortInput) {
localPortInput.value = match[1];
}
} else if (remoteApiInput) {
remoteApiInput.value = savedBase;
}
} }
// 设置密钥 if (apiBaseInput) {
if (localKeyInput && savedKey) { apiBaseInput.value = this.apiBase || '';
localKeyInput.value = savedKey;
}
if (remoteKeyInput && savedKey) {
remoteKeyInput.value = savedKey;
} }
// 设置代理 if (loginKeyInput && savedKey) {
if (proxyInput && savedProxy) { loginKeyInput.value = savedKey;
proxyInput.value = savedProxy;
} }
// 设置实时保存监听器
this.setupLoginAutoSave(); this.setupLoginAutoSave();
} }
// 设置登录页面自动保存
setupLoginAutoSave() { setupLoginAutoSave() {
const localPortInput = document.getElementById('local-port'); const loginKeyInput = document.getElementById('login-management-key');
const remoteApiInput = document.getElementById('remote-api-url'); const apiBaseInput = document.getElementById('login-api-base');
const localKeyInput = document.getElementById('local-management-key'); const resetButton = document.getElementById('login-reset-api-base');
const remoteKeyInput = document.getElementById('remote-management-key');
const proxyInput = document.getElementById('login-proxy-url');
const saveLocalBase = (port) => {
if (port.trim()) {
const apiUrl = `http://localhost:${port}`;
this.setApiBase(apiUrl);
}
};
const saveLocalBaseDebounced = this.debounce(saveLocalBase, 500);
const saveRemoteBase = (val) => {
if (val.trim()) {
this.setApiBase(val);
}
};
const saveRemoteBaseDebounced = this.debounce(saveRemoteBase, 500);
const saveKey = (val) => { const saveKey = (val) => {
if (val.trim()) { if (val.trim()) {
@@ -425,41 +376,32 @@ class CLIProxyManager {
}; };
const saveKeyDebounced = this.debounce(saveKey, 500); const saveKeyDebounced = this.debounce(saveKey, 500);
const saveProxy = (val) => { if (loginKeyInput) {
if (val.trim()) { loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
localStorage.setItem('proxyUrl', val); loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
}
if (apiBaseInput) {
const persistBase = (val) => {
const normalized = this.normalizeBase(val);
if (normalized) {
this.setApiBase(normalized);
} }
}; };
const saveProxyDebounced = this.debounce(saveProxy, 500); const persistBaseDebounced = this.debounce(persistBase, 500);
// 绑定本地端口输入框 apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
if (localPortInput) { apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
localPortInput.addEventListener('change', (e) => saveLocalBase(e.target.value));
localPortInput.addEventListener('input', (e) => saveLocalBaseDebounced(e.target.value));
} }
// 绑定远程API输入框 if (resetButton) {
if (remoteApiInput) { resetButton.addEventListener('click', () => {
remoteApiInput.addEventListener('change', (e) => saveRemoteBase(e.target.value)); const detected = this.detectApiBaseFromLocation();
remoteApiInput.addEventListener('input', (e) => saveRemoteBaseDebounced(e.target.value)); this.setApiBase(detected);
if (apiBaseInput) {
apiBaseInput.value = detected;
} }
});
// 绑定本地密钥输入框
if (localKeyInput) {
localKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
localKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
}
// 绑定远程密钥输入框
if (remoteKeyInput) {
remoteKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
remoteKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
}
// 绑定代理输入框
if (proxyInput) {
proxyInput.addEventListener('change', (e) => saveProxy(e.target.value));
proxyInput.addEventListener('input', (e) => saveProxyDebounced(e.target.value));
} }
} }
@@ -476,9 +418,6 @@ class CLIProxyManager {
logoutBtn.addEventListener('click', () => this.logout()); logoutBtn.addEventListener('click', () => this.logout());
} }
// 选项卡切换事件
this.setupTabSwitching();
// 密钥可见性切换事件 // 密钥可见性切换事件
this.setupKeyVisibilityToggle(); this.setupKeyVisibilityToggle();
@@ -486,30 +425,6 @@ class CLIProxyManager {
this.bindMainPageEvents(); this.bindMainPageEvents();
} }
// 设置选项卡切换
setupTabSwitching() {
const tabButtons = document.querySelectorAll('.tab-button');
const connectionForms = document.querySelectorAll('.connection-form');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// 更新选项卡状态
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 切换表单
connectionForms.forEach(form => {
form.classList.remove('active');
if (form.id === `${targetTab}-form`) {
form.classList.add('active');
}
});
});
});
}
// 设置密钥可见性切换 // 设置密钥可见性切换
setupKeyVisibilityToggle() { setupKeyVisibilityToggle() {
const toggleButtons = document.querySelectorAll('.toggle-key-visibility'); const toggleButtons = document.querySelectorAll('.toggle-key-visibility');
@@ -538,7 +453,6 @@ 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 allowLocalhostToggle = document.getElementById('allow-localhost-toggle');
if (debugToggle) { if (debugToggle) {
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked)); debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
@@ -558,9 +472,6 @@ 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 (allowLocalhostToggle) {
allowLocalhostToggle.addEventListener('change', (e) => this.updateAllowLocalhost(e.target.checked));
}
// API 密钥管理 // API 密钥管理
const addApiKey = document.getElementById('add-api-key'); const addApiKey = document.getElementById('add-api-key');
@@ -607,6 +518,29 @@ class CLIProxyManager {
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
} }
// 使用统计
const refreshUsageStats = document.getElementById('refresh-usage-stats');
const requestsHourBtn = document.getElementById('requests-hour-btn');
const requestsDayBtn = document.getElementById('requests-day-btn');
const tokensHourBtn = document.getElementById('tokens-hour-btn');
const tokensDayBtn = document.getElementById('tokens-day-btn');
if (refreshUsageStats) {
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
}
if (requestsHourBtn) {
requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour'));
}
if (requestsDayBtn) {
requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day'));
}
if (tokensHourBtn) {
tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour'));
}
if (tokensDayBtn) {
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
}
// 模态框 // 模态框
const closeBtn = document.querySelector('.close'); const closeBtn = document.querySelector('.close');
if (closeBtn) { if (closeBtn) {
@@ -710,6 +644,7 @@ class CLIProxyManager {
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiUrl = this.computeApiUrl(this.apiBase);
localStorage.setItem('apiBase', this.apiBase); localStorage.setItem('apiBase', this.apiBase);
localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
this.updateLoginConnectionInfo();
} }
// 加载设置(简化版,仅加载内部状态) // 加载设置(简化版,仅加载内部状态)
@@ -718,21 +653,20 @@ class CLIProxyManager {
const savedUrl = localStorage.getItem('apiUrl'); const savedUrl = localStorage.getItem('apiUrl');
const savedKey = localStorage.getItem('managementKey'); const savedKey = localStorage.getItem('managementKey');
// 只设置内部状态不操作DOM元素
if (savedBase) { if (savedBase) {
this.setApiBase(savedBase); this.setApiBase(savedBase);
} else if (savedUrl) { } else if (savedUrl) {
const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, ''); const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, '');
this.setApiBase(base); this.setApiBase(base);
} else { } else {
this.setApiBase(this.apiBase); this.setApiBase(this.detectApiBaseFromLocation());
} }
if (savedKey) { if (savedKey) {
this.managementKey = savedKey; this.managementKey = savedKey;
} }
this.updateLoginConnectionInfo();
} }
// API 请求方法 // API 请求方法
@@ -939,6 +873,9 @@ class CLIProxyManager {
// 认证文件需要单独加载,因为不在配置中 // 认证文件需要单独加载,因为不在配置中
await this.loadAuthFiles(); await this.loadAuthFiles();
// 使用统计需要单独加载
await this.loadUsageStats();
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
} catch (error) { } catch (error) {
console.error('加载配置失败:', error); console.error('加载配置失败:', error);
@@ -975,10 +912,6 @@ class CLIProxyManager {
} }
} }
// 本地访问设置
if (config['allow-localhost-unauthenticated'] !== undefined) {
document.getElementById('allow-localhost-toggle').checked = config['allow-localhost-unauthenticated'];
}
// API 密钥 // API 密钥
if (config['api-keys']) { if (config['api-keys']) {
@@ -1013,7 +946,6 @@ class CLIProxyManager {
this.loadProxySettings(), this.loadProxySettings(),
this.loadRetrySettings(), this.loadRetrySettings(),
this.loadQuotaSettings(), this.loadQuotaSettings(),
this.loadLocalhostSettings(),
this.loadApiKeys(), this.loadApiKeys(),
this.loadGeminiKeys(), this.loadGeminiKeys(),
this.loadCodexKeys(), this.loadCodexKeys(),
@@ -1166,32 +1098,6 @@ class CLIProxyManager {
} }
} }
// 加载本地访问设置
async loadLocalhostSettings() {
try {
const config = await this.getConfig();
if (config['allow-localhost-unauthenticated'] !== undefined) {
document.getElementById('allow-localhost-toggle').checked = config['allow-localhost-unauthenticated'];
}
} catch (error) {
console.error('加载本地访问设置失败:', error);
}
}
// 更新本地访问设置
async updateAllowLocalhost(enabled) {
try {
await this.makeRequest('/allow-localhost-unauthenticated', {
method: 'PUT',
body: JSON.stringify({ value: enabled })
});
this.clearCache(); // 清除缓存
this.showNotification(i18n.t('notification.localhost_updated'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
document.getElementById('allow-localhost-toggle').checked = !enabled;
}
}
// 加载API密钥 // 加载API密钥
async loadApiKeys() { async loadApiKeys() {
@@ -2200,6 +2106,7 @@ class CLIProxyManager {
showGeminiWebTokenModal() { showGeminiWebTokenModal() {
const inlineSecure1psid = document.getElementById('secure-1psid-input'); const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>${i18n.t('auth_login.gemini_web_button')}</h3> <h3>${i18n.t('auth_login.gemini_web_button')}</h3>
@@ -2214,6 +2121,11 @@ class CLIProxyManager {
<input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required> <input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required>
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div> <div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
</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"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> <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> <button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button>
@@ -2224,6 +2136,7 @@ class CLIProxyManager {
const modalSecure1psid = document.getElementById('modal-secure-1psid'); const modalSecure1psid = document.getElementById('modal-secure-1psid');
const modalSecure1psidts = document.getElementById('modal-secure-1psidts'); const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
const modalLabel = document.getElementById('modal-gemini-web-label');
if (modalSecure1psid && inlineSecure1psid) { if (modalSecure1psid && inlineSecure1psid) {
modalSecure1psid.value = inlineSecure1psid.value.trim(); modalSecure1psid.value = inlineSecure1psid.value.trim();
@@ -2231,6 +2144,9 @@ class CLIProxyManager {
if (modalSecure1psidts && inlineSecure1psidts) { if (modalSecure1psidts && inlineSecure1psidts) {
modalSecure1psidts.value = inlineSecure1psidts.value.trim(); modalSecure1psidts.value = inlineSecure1psidts.value.trim();
} }
if (modalLabel && inlineLabel) {
modalLabel.value = inlineLabel.value.trim();
}
if (modalSecure1psid) { if (modalSecure1psid) {
modalSecure1psid.focus(); modalSecure1psid.focus();
@@ -2241,6 +2157,7 @@ class CLIProxyManager {
async saveGeminiWebToken() { async saveGeminiWebToken() {
const secure1psid = document.getElementById('modal-secure-1psid').value.trim(); const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim(); const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
const label = document.getElementById('modal-gemini-web-label').value.trim();
if (!secure1psid || !secure1psidts) { if (!secure1psid || !secure1psidts) {
this.showNotification('请填写完整的 Cookie 信息', 'error'); this.showNotification('请填写完整的 Cookie 信息', 'error');
@@ -2248,33 +2165,381 @@ class CLIProxyManager {
} }
try { try {
const requestBody = {
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
};
// 如果提供了 label则添加到请求体中
if (label) {
requestBody.label = label;
}
const response = await this.makeRequest('/gemini-web-token', { const response = await this.makeRequest('/gemini-web-token', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody)
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
})
}); });
this.closeModal(); this.closeModal();
this.loadAuthFiles(); // 刷新认证文件列表 this.loadAuthFiles(); // 刷新认证文件列表
const inlineSecure1psid = document.getElementById('secure-1psid-input'); const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
if (inlineSecure1psid) { if (inlineSecure1psid) {
inlineSecure1psid.value = secure1psid; inlineSecure1psid.value = secure1psid;
} }
if (inlineSecure1psidts) { if (inlineSecure1psidts) {
inlineSecure1psidts.value = secure1psidts; inlineSecure1psidts.value = secure1psidts;
} }
if (inlineLabel) {
inlineLabel.value = label;
}
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success'); this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
} catch (error) { } catch (error) {
this.showNotification(`保存失败: ${error.message}`, 'error'); this.showNotification(`保存失败: ${error.message}`, 'error');
} }
} }
// ===== 使用统计相关方法 =====
// 初始化图表变量
requestsChart = null;
tokensChart = null;
currentUsageData = null;
// 加载使用统计
async loadUsageStats() {
try {
const response = await this.makeRequest('/usage');
const usage = response?.usage || null;
this.currentUsageData = usage;
if (!usage) {
throw new Error('usage payload missing');
}
// 更新概览卡片
this.updateUsageOverview(usage);
// 读取当前图表周期
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
const requestsPeriod = requestsHourActive ? 'hour' : 'day';
const tokensPeriod = tokensHourActive ? 'hour' : 'day';
// 初始化图表(使用当前周期)
this.initializeRequestsChart(requestsPeriod);
this.initializeTokensChart(tokensPeriod);
// 更新API详细统计表格
this.updateApiStatsTable(usage);
} catch (error) {
console.error('加载使用统计失败:', error);
this.currentUsageData = null;
// 清空概览数据
['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '-';
});
// 清空图表
if (this.requestsChart) {
this.requestsChart.destroy();
this.requestsChart = null;
}
if (this.tokensChart) {
this.tokensChart.destroy();
this.tokensChart = null;
}
const tableElement = document.getElementById('api-stats-table');
if (tableElement) {
tableElement.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.loading_error')}: ${error.message}</div>`;
}
}
}
// 更新使用统计概览
updateUsageOverview(data) {
const safeData = data || {};
document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0;
document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0;
}
// 初始化图表
initializeCharts() {
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
}
// 初始化请求趋势图表
initializeRequestsChart(period = 'day') {
const ctx = document.getElementById('requests-chart');
if (!ctx) return;
// 销毁现有图表
if (this.requestsChart) {
this.requestsChart.destroy();
}
const data = this.getRequestsChartData(period);
this.requestsChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
title: {
display: true,
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: i18n.t('usage_stats.requests_count')
}
}
},
elements: {
line: {
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
},
point: {
backgroundColor: '#3b82f6',
borderColor: '#ffffff',
borderWidth: 2,
radius: 4
}
}
}
});
}
// 初始化Token使用趋势图表
initializeTokensChart(period = 'day') {
const ctx = document.getElementById('tokens-chart');
if (!ctx) return;
// 销毁现有图表
if (this.tokensChart) {
this.tokensChart.destroy();
}
const data = this.getTokensChartData(period);
this.tokensChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
title: {
display: true,
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: i18n.t('usage_stats.tokens_count')
}
}
},
elements: {
line: {
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4
},
point: {
backgroundColor: '#10b981',
borderColor: '#ffffff',
borderWidth: 2,
radius: 4
}
}
}
});
}
// 获取请求图表数据
getRequestsChartData(period) {
if (!this.currentUsageData) {
return { labels: [], datasets: [{ data: [] }] };
}
let dataSource, labels, values;
if (period === 'hour') {
dataSource = this.currentUsageData.requests_by_hour || {};
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
values = labels.map(hour => dataSource[hour] || 0);
} else {
dataSource = this.currentUsageData.requests_by_day || {};
labels = Object.keys(dataSource).sort();
values = labels.map(day => dataSource[day] || 0);
}
return {
labels: labels,
datasets: [{
data: values
}]
};
}
// 获取Token图表数据
getTokensChartData(period) {
if (!this.currentUsageData) {
return { labels: [], datasets: [{ data: [] }] };
}
let dataSource, labels, values;
if (period === 'hour') {
dataSource = this.currentUsageData.tokens_by_hour || {};
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
values = labels.map(hour => dataSource[hour] || 0);
} else {
dataSource = this.currentUsageData.tokens_by_day || {};
labels = Object.keys(dataSource).sort();
values = labels.map(day => dataSource[day] || 0);
}
return {
labels: labels,
datasets: [{
data: values
}]
};
}
// 切换请求图表时间周期
switchRequestsPeriod(period) {
// 更新按钮状态
document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour');
document.getElementById('requests-day-btn').classList.toggle('active', period === 'day');
// 更新图表数据
if (this.requestsChart) {
const newData = this.getRequestsChartData(period);
this.requestsChart.data = newData;
this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
this.requestsChart.update();
}
}
// 切换Token图表时间周期
switchTokensPeriod(period) {
// 更新按钮状态
document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour');
document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day');
// 更新图表数据
if (this.tokensChart) {
const newData = this.getTokensChartData(period);
this.tokensChart.data = newData;
this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
this.tokensChart.update();
}
}
// 更新API详细统计表格
updateApiStatsTable(data) {
const container = document.getElementById('api-stats-table');
if (!container) return;
const apis = data.apis || {};
if (Object.keys(apis).length === 0) {
container.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.no_data')}</div>`;
return;
}
let tableHtml = `
<table class="stats-table">
<thead>
<tr>
<th>${i18n.t('usage_stats.api_endpoint')}</th>
<th>${i18n.t('usage_stats.requests_count')}</th>
<th>${i18n.t('usage_stats.tokens_count')}</th>
<th>${i18n.t('usage_stats.success_rate')}</th>
<th>${i18n.t('usage_stats.models')}</th>
</tr>
</thead>
<tbody>
`;
Object.entries(apis).forEach(([endpoint, apiData]) => {
const totalRequests = apiData.total_requests || 0;
const successCount = apiData.success_count ?? null;
const successRate = successCount !== null && totalRequests > 0
? Math.round((successCount / totalRequests) * 100)
: null;
// 构建模型详情
let modelsHtml = '';
if (apiData.models && Object.keys(apiData.models).length > 0) {
modelsHtml = '<div class="model-details">';
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
const modelRequests = modelData.total_requests ?? 0;
const modelTokens = modelData.total_tokens ?? 0;
modelsHtml += `
<div class="model-item">
<span class="model-name">${modelName}</span>
<span>${modelRequests} 请求 / ${modelTokens} tokens</span>
</div>
`;
});
modelsHtml += '</div>';
}
tableHtml += `
<tr>
<td>${endpoint}</td>
<td>${totalRequests}</td>
<td>${apiData.total_tokens || 0}</td>
<td>${successRate !== null ? successRate + '%' : '-'}</td>
<td>${modelsHtml || '-'}</td>
</tr>
`;
});
tableHtml += '</tbody></table>';
container.innerHTML = tableHtml;
}
showModal() { showModal() {
const modal = document.getElementById('modal'); const modal = document.getElementById('modal');
if (modal) { if (modal) {
@@ -2286,6 +2551,28 @@ class CLIProxyManager {
closeModal() { closeModal() {
document.getElementById('modal').style.display = 'none'; document.getElementById('modal').style.display = 'none';
} }
detectApiBaseFromLocation() {
try {
const { protocol, hostname, port } = window.location;
const normalizedPort = port ? `:${port}` : '';
return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`);
} catch (error) {
console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error);
return this.normalizeBase(this.apiBase || 'http://localhost:8317');
}
}
updateLoginConnectionInfo() {
const connectionUrlElement = document.getElementById('login-connection-url');
const customInput = document.getElementById('login-api-base');
if (connectionUrlElement) {
connectionUrlElement.textContent = this.apiBase || '-';
}
if (customInput && customInput !== document.activeElement) {
customInput.value = this.apiBase || '';
}
}
} }
// 全局管理器实例 // 全局管理器实例
@@ -2297,6 +2584,19 @@ function setupSiteLogo() {
const loginImg = document.getElementById('login-logo'); const loginImg = document.getElementById('login-logo');
if (!img && !loginImg) return; if (!img && !loginImg) return;
const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null;
if (inlineLogo) {
if (img) {
img.src = inlineLogo;
img.style.display = 'inline-block';
}
if (loginImg) {
loginImg.src = inlineLogo;
loginImg.style.display = 'inline-block';
}
return;
}
const candidates = [ const candidates = [
'../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif', '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif',
'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif', 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif',

View File

@@ -0,0 +1,30 @@
const fs = require('fs');
const path = require('path');
// Read the original index.html
const indexPath = path.resolve(__dirname, '../index.html');
const outputPath = path.resolve(__dirname, '../index.build.html');
let htmlContent = fs.readFileSync(indexPath, 'utf8');
// Remove local CSS reference
htmlContent = htmlContent.replace(
/<link rel="stylesheet" href="styles\.css">\n?/g,
''
);
// Remove local JavaScript references
htmlContent = htmlContent.replace(
/<script src="i18n\.js"><\/script>\n?/g,
''
);
htmlContent = htmlContent.replace(
/<script src="app\.js"><\/script>\n?/g,
''
);
// Write the modified HTML to a temporary build file
fs.writeFileSync(outputPath, htmlContent, 'utf8');
console.log('✓ Generated index.build.html for webpack processing');

132
build.js Normal file
View File

@@ -0,0 +1,132 @@
'use strict';
const fs = require('fs');
const path = require('path');
const projectRoot = __dirname;
const distDir = path.join(projectRoot, 'dist');
const sourceFiles = {
html: path.join(projectRoot, 'index.html'),
css: path.join(projectRoot, 'styles.css'),
i18n: path.join(projectRoot, 'i18n.js'),
app: path.join(projectRoot, 'app.js')
};
const logoCandidates = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.svg', 'logo.webp', 'logo.gif'];
const logoMimeMap = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.gif': 'image/gif'
};
function readFile(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (err) {
console.error(`读取文件失败: ${filePath}`);
throw err;
}
}
function readBinary(filePath) {
try {
return fs.readFileSync(filePath);
} catch (err) {
console.error(`读取文件失败: ${filePath}`);
throw err;
}
}
function escapeForScript(content) {
return content.replace(/<\/(script)/gi, '<\\/$1');
}
function escapeForStyle(content) {
return content.replace(/<\/(style)/gi, '<\\/$1');
}
function ensureDistDir() {
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true });
}
fs.mkdirSync(distDir);
}
function loadLogoDataUrl() {
for (const candidate of logoCandidates) {
const filePath = path.join(projectRoot, candidate);
if (!fs.existsSync(filePath)) continue;
const ext = path.extname(candidate).toLowerCase();
const mime = logoMimeMap[ext];
if (!mime) {
console.warn(`未知 Logo 文件类型,跳过内联: ${candidate}`);
continue;
}
const buffer = readBinary(filePath);
const base64 = buffer.toString('base64');
return `data:${mime};base64,${base64}`;
}
return null;
}
function build() {
ensureDistDir();
let html = readFile(sourceFiles.html);
const css = escapeForStyle(readFile(sourceFiles.css));
const i18n = escapeForScript(readFile(sourceFiles.i18n));
const app = escapeForScript(readFile(sourceFiles.app));
html = html.replace(
'<link rel="stylesheet" href="styles.css">',
`<style>
${css}
</style>`
);
html = html.replace(
'<script src="i18n.js"></script>',
`<script>
${i18n}
</script>`
);
html = html.replace(
'<script src="app.js"></script>',
`<script>
${app}
</script>`
);
const logoDataUrl = loadLogoDataUrl();
if (logoDataUrl) {
const logoScript = `<script>window.__INLINE_LOGO__ = "${logoDataUrl}";</script>`;
if (html.includes('</body>')) {
html = html.replace('</body>', `${logoScript}\n</body>`);
} else {
html += `\n${logoScript}`;
}
} else {
console.warn('未找到可内联的 Logo 文件,将保持运行时加载。');
}
const outputPath = path.join(distDir, 'index.html');
fs.writeFileSync(outputPath, html, 'utf8');
console.log('构建完成: dist/index.html');
}
try {
build();
} catch (error) {
console.error('构建失败:', error);
process.exit(1);
}

25
bundle-entry.js Normal file
View File

@@ -0,0 +1,25 @@
// Import CSS
import './styles.css';
// Import JavaScript modules
import './i18n.js';
import './app.js';
// Import logo image
import logoImg from './logo.jpg';
// Set logo after DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const loginLogo = document.getElementById('login-logo');
const siteLogo = document.getElementById('site-logo');
if (loginLogo) {
loginLogo.src = logoImg;
loginLogo.style.display = 'block';
}
if (siteLogo) {
siteLogo.src = logoImg;
siteLogo.style.display = 'block';
}
});

106
i18n.js
View File

@@ -47,19 +47,13 @@ const i18n = {
// 登录页面 // 登录页面
'login.subtitle': '请输入连接信息以访问管理界面', 'login.subtitle': '请输入连接信息以访问管理界面',
'login.tab_local_title': 'Local', 'login.connection_title': '连接地址',
'login.tab_local_subtitle': '在本地运行 Cli Web 服务器', 'login.connection_current': '当前地址',
'login.tab_remote_title': 'Remote', 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接',
'login.tab_remote_subtitle': '远程连接到 Cli Web 服务器', 'login.custom_connection_label': '自定义连接地址:',
'login.proxy_label': '代理服务器 (可选):', 'login.custom_connection_placeholder': '例如: https://example.com:8317',
'login.proxy_placeholder': 'http://ip:port 或 https://ip:port 或 socks5://user:pass@ip:port', 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。',
'login.local_port_label': '端口号:', 'login.use_current_address': '使用当前地址',
'login.local_port_placeholder': '8317',
'login.local_url_hint': '将连接到 http://localhost:端口/v0/management',
'login.api_url_label': 'API 基础地址:',
'login.api_url_placeholder': '例如: http://localhost:8317 或 127.0.0.1:8317',
'login.remote_api_url_placeholder': '例如: https://example.com:8317',
'login.api_url_hint': '将自动补全 /v0/management',
'login.management_key_label': '管理密钥:', 'login.management_key_label': '管理密钥:',
'login.management_key_placeholder': '请输入管理密钥', 'login.management_key_placeholder': '请输入管理密钥',
'login.connect_button': '连接', 'login.connect_button': '连接',
@@ -85,6 +79,7 @@ const i18n = {
'nav.api_keys': 'API 密钥', 'nav.api_keys': 'API 密钥',
'nav.ai_providers': 'AI 提供商', 'nav.ai_providers': 'AI 提供商',
'nav.auth_files': '认证文件', 'nav.auth_files': '认证文件',
'nav.usage_stats': '使用统计',
'nav.system_info': '系统信息', 'nav.system_info': '系统信息',
// 基础设置 // 基础设置
@@ -102,8 +97,6 @@ const i18n = {
'basic_settings.quota_title': '配额超出行为', 'basic_settings.quota_title': '配额超出行为',
'basic_settings.quota_switch_project': '自动切换项目', 'basic_settings.quota_switch_project': '自动切换项目',
'basic_settings.quota_switch_preview': '切换到预览模型', 'basic_settings.quota_switch_preview': '切换到预览模型',
'basic_settings.localhost_title': '本地访问',
'basic_settings.localhost_allow': '允许本地未认证访问',
// API 密钥管理 // API 密钥管理
'api_keys.title': 'API 密钥管理', 'api_keys.title': 'API 密钥管理',
@@ -212,8 +205,30 @@ const i18n = {
'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值', 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
'auth_login.gemini_web_label_label': '标签 (可选):',
'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)',
'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
// 使用统计
'usage_stats.title': '使用统计',
'usage_stats.total_requests': '总请求数',
'usage_stats.success_requests': '成功请求',
'usage_stats.failed_requests': '失败请求',
'usage_stats.total_tokens': '总Token数',
'usage_stats.requests_trend': '请求趋势',
'usage_stats.tokens_trend': 'Token 使用趋势',
'usage_stats.api_details': 'API 详细统计',
'usage_stats.by_hour': '按小时',
'usage_stats.by_day': '按天',
'usage_stats.refresh': '刷新',
'usage_stats.no_data': '暂无数据',
'usage_stats.loading_error': '加载失败',
'usage_stats.api_endpoint': 'API端点',
'usage_stats.requests_count': '请求次数',
'usage_stats.tokens_count': 'Token数量',
'usage_stats.models': '模型统计',
'usage_stats.success_rate': '成功率',
// 系统信息 // 系统信息
'system_info.title': '系统信息', 'system_info.title': '系统信息',
'system_info.connection_status_title': '连接状态', 'system_info.connection_status_title': '连接状态',
@@ -232,7 +247,6 @@ const i18n = {
'notification.retry_updated': '重试设置已更新', 'notification.retry_updated': '重试设置已更新',
'notification.quota_switch_project_updated': '项目切换设置已更新', 'notification.quota_switch_project_updated': '项目切换设置已更新',
'notification.quota_switch_preview_updated': '预览模型切换设置已更新', 'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
'notification.localhost_updated': '本地访问设置已更新',
'notification.api_key_added': 'API密钥添加成功', 'notification.api_key_added': 'API密钥添加成功',
'notification.api_key_updated': 'API密钥更新成功', 'notification.api_key_updated': 'API密钥更新成功',
'notification.api_key_deleted': 'API密钥删除成功', 'notification.api_key_deleted': 'API密钥删除成功',
@@ -276,7 +290,11 @@ const i18n = {
'theme.dark': '暗色', 'theme.dark': '暗色',
'theme.switch_to_light': '切换到亮色模式', 'theme.switch_to_light': '切换到亮色模式',
'theme.switch_to_dark': '切换到暗色模式', 'theme.switch_to_dark': '切换到暗色模式',
'theme.auto': '跟随系统' 'theme.auto': '跟随系统',
// 页脚
'footer.version': '版本',
'footer.author': '作者'
}, },
'en-US': { 'en-US': {
@@ -320,21 +338,15 @@ const i18n = {
// Login page // Login page
'login.subtitle': 'Please enter connection information to access the management interface', 'login.subtitle': 'Please enter connection information to access the management interface',
'login.tab_local_title': 'Local', 'login.connection_title': 'Connection Address',
'login.tab_local_subtitle': 'Run Cli Web server on your local machine', 'login.connection_current': 'Current URL',
'login.tab_remote_title': 'Remote', 'login.connection_auto_hint': 'The system will automatically use the current URL for connection',
'login.tab_remote_subtitle': 'Remote connection for a remote Cli Web server', 'login.custom_connection_label': 'Custom Connection URL:',
'login.proxy_label': 'Proxy Server (Optional):', 'login.custom_connection_placeholder': 'Eg: https://example.com:8317',
'login.proxy_placeholder': 'http://ip:port or https://ip:port or socks5://user:pass@ip:port', 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.',
'login.local_port_label': 'Port:', 'login.use_current_address': 'Use Current URL',
'login.local_port_placeholder': '8317',
'login.local_url_hint': 'Will connect to http://localhost:port/v0/management',
'login.api_url_label': 'API Base URL:',
'login.api_url_placeholder': 'e.g.: http://localhost:8317 or 127.0.0.1:8317',
'login.remote_api_url_placeholder': 'e.g.: https://example.com:8317',
'login.api_url_hint': 'Will automatically append /v0/management',
'login.management_key_label': 'Management Key:', 'login.management_key_label': 'Management Key:',
'login.management_key_placeholder': 'Please enter management key', 'login.management_key_placeholder': 'Enter the management key',
'login.connect_button': 'Connect', 'login.connect_button': 'Connect',
'login.submit_button': 'Login', 'login.submit_button': 'Login',
'login.submitting': 'Connecting...', 'login.submitting': 'Connecting...',
@@ -358,6 +370,7 @@ const i18n = {
'nav.api_keys': 'API Keys', 'nav.api_keys': 'API Keys',
'nav.ai_providers': 'AI Providers', 'nav.ai_providers': 'AI Providers',
'nav.auth_files': 'Auth Files', 'nav.auth_files': 'Auth Files',
'nav.usage_stats': 'Usage Statistics',
'nav.system_info': 'System Info', 'nav.system_info': 'System Info',
// Basic settings // Basic settings
@@ -375,8 +388,6 @@ const i18n = {
'basic_settings.quota_title': 'Quota Exceeded Behavior', 'basic_settings.quota_title': 'Quota Exceeded Behavior',
'basic_settings.quota_switch_project': 'Auto Switch Project', 'basic_settings.quota_switch_project': 'Auto Switch Project',
'basic_settings.quota_switch_preview': 'Switch to Preview Model', 'basic_settings.quota_switch_preview': 'Switch to Preview Model',
'basic_settings.localhost_title': 'Local Access',
'basic_settings.localhost_allow': 'Allow Localhost Unauthenticated Access',
// API Keys management // API Keys management
'api_keys.title': 'API Keys Management', 'api_keys.title': 'API Keys Management',
@@ -485,8 +496,30 @@ const i18n = {
'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value', 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
'auth_login.gemini_web_label_label': 'Label (Optional):',
'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)',
'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
// Usage Statistics
'usage_stats.title': 'Usage Statistics',
'usage_stats.total_requests': 'Total Requests',
'usage_stats.success_requests': 'Success Requests',
'usage_stats.failed_requests': 'Failed Requests',
'usage_stats.total_tokens': 'Total Tokens',
'usage_stats.requests_trend': 'Request Trends',
'usage_stats.tokens_trend': 'Token Usage Trends',
'usage_stats.api_details': 'API Details',
'usage_stats.by_hour': 'By Hour',
'usage_stats.by_day': 'By Day',
'usage_stats.refresh': 'Refresh',
'usage_stats.no_data': 'No Data Available',
'usage_stats.loading_error': 'Loading Failed',
'usage_stats.api_endpoint': 'API Endpoint',
'usage_stats.requests_count': 'Request Count',
'usage_stats.tokens_count': 'Token Count',
'usage_stats.models': 'Model Statistics',
'usage_stats.success_rate': 'Success Rate',
// System info // System info
'system_info.title': 'System Information', 'system_info.title': 'System Information',
'system_info.connection_status_title': 'Connection Status', 'system_info.connection_status_title': 'Connection Status',
@@ -505,7 +538,6 @@ const i18n = {
'notification.retry_updated': 'Retry settings updated', 'notification.retry_updated': 'Retry settings updated',
'notification.quota_switch_project_updated': 'Project switch settings updated', 'notification.quota_switch_project_updated': 'Project switch settings updated',
'notification.quota_switch_preview_updated': 'Preview model switch settings updated', 'notification.quota_switch_preview_updated': 'Preview model switch settings updated',
'notification.localhost_updated': 'Localhost access settings updated',
'notification.api_key_added': 'API key added successfully', 'notification.api_key_added': 'API key added successfully',
'notification.api_key_updated': 'API key updated successfully', 'notification.api_key_updated': 'API key updated successfully',
'notification.api_key_deleted': 'API key deleted successfully', 'notification.api_key_deleted': 'API key deleted successfully',
@@ -549,7 +581,11 @@ const i18n = {
'theme.dark': 'Dark', 'theme.dark': 'Dark',
'theme.switch_to_light': 'Switch to light mode', 'theme.switch_to_light': 'Switch to light mode',
'theme.switch_to_dark': 'Switch to dark mode', 'theme.switch_to_dark': 'Switch to dark mode',
'theme.auto': 'Follow system' 'theme.auto': 'Follow system',
// Footer
'footer.version': 'Version',
'footer.author': 'Author'
} }
}, },

View File

@@ -6,6 +6,7 @@
<title data-i18n="title.login">CLI Proxy API Management Center</title> <title data-i18n="title.login">CLI Proxy API Management Center</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="i18n.js"></script> <script src="i18n.js"></script>
</head> </head>
<body> <body>
@@ -48,67 +49,39 @@
</div> </div>
</div> </div>
<!-- 选项卡导航 --> <div class="login-body">
<div class="connection-tabs"> <div class="login-connection-info">
<button class="tab-button active" data-tab="local"> <div class="connection-summary">
<i class="fas fa-home"></i> <i class="fas fa-link"></i>
<div class="tab-content"> <div>
<span class="tab-title" data-i18n="login.tab_local_title">Local</span> <h3 data-i18n="login.connection_title">连接地址</h3>
<span class="tab-subtitle" data-i18n="login.tab_local_subtitle">Run CLI Web server on your local machine</span> <p class="connection-url">
<span data-i18n="login.connection_current">当前地址</span>
<span class="connection-url-separator">:</span>
<span id="login-connection-url">-</span>
</p>
</div> </div>
</button>
<button class="tab-button" data-tab="remote">
<i class="fas fa-cloud"></i>
<div class="tab-content">
<span class="tab-title" data-i18n="login.tab_remote_title">Remote</span>
<span class="tab-subtitle" data-i18n="login.tab_remote_subtitle">Remote connection for a remote CLI Web server</span>
</div> </div>
</button> <p class="form-hint" data-i18n="login.connection_auto_hint">系统将自动使用当前访问地址进行连接</p>
</div> </div>
<!-- 代理服务器设置(可选) -->
<div class="proxy-settings">
<label data-i18n="login.proxy_label">Proxy Server (Optional):</label>
<input type="text" id="login-proxy-url" data-i18n="login.proxy_placeholder" placeholder="http://ip:port or https://ip:port or socks5://user:pass@ip:port">
</div>
<!-- 本地连接表单 -->
<div id="local-form" class="connection-form active">
<form class="login-form"> <form class="login-form">
<div class="form-group"> <div class="form-group">
<label for="local-port" data-i18n="login.local_port_label">端口号:</label> <label for="login-api-base" data-i18n="login.custom_connection_label">自定义连接地址:</label>
<div class="local-url-group">
<span class="url-prefix">http://localhost:</span>
<input type="number" id="local-port" value="8317" min="1" max="65535" data-i18n="login.local_port_placeholder" placeholder="8317" required>
</div>
<div class="form-hint" data-i18n="login.local_url_hint">将连接到 http://localhost:端口/v0/management</div>
</div>
<div class="form-group">
<label for="local-management-key" data-i18n="login.management_key_label">管理密钥:</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="local-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required> <input type="text" id="login-api-base" data-i18n="login.custom_connection_placeholder" placeholder="例如: https://example.com:8317">
<button type="button" class="btn btn-secondary toggle-key-visibility"> <button type="button" id="login-reset-api-base" class="btn btn-secondary connection-reset-btn">
<i class="fas fa-eye"></i> <i class="fas fa-location-arrow"></i>
<span data-i18n="login.use_current_address">使用当前地址</span>
</button> </button>
</div> </div>
</div> <p class="form-hint" data-i18n="login.custom_connection_hint">默认使用当前访问地址,若需要可手动输入其他地址。</p>
</form>
</div>
<!-- 远程连接表单 -->
<div id="remote-form" class="connection-form">
<form class="login-form">
<div class="form-group">
<label for="remote-api-url" data-i18n="login.api_url_label">API 基础地址:</label>
<input type="text" id="remote-api-url" data-i18n="login.remote_api_url_placeholder" placeholder="例如: https://example.com:8317" required>
<div class="form-hint" data-i18n="login.api_url_hint">将自动补全 /v0/management</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="remote-management-key" data-i18n="login.management_key_label">管理密钥:</label> <label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="remote-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required> <input type="password" id="login-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
<button type="button" class="btn btn-secondary toggle-key-visibility"> <button type="button" class="btn btn-secondary toggle-key-visibility">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
@@ -221,6 +194,9 @@
<li><a href="#auth-files" class="nav-item" data-section="auth-files"> <li><a href="#auth-files" class="nav-item" data-section="auth-files">
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span> <i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
</a></li> </a></li>
<li><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><a href="#system-info" class="nav-item" data-section="system-info"> <li><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>
@@ -305,21 +281,6 @@
</div> </div>
</div> </div>
<!-- 本地访问设置 -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-home"></i> <span data-i18n="basic_settings.localhost_title">本地访问</span></h3>
</div>
<div class="card-content">
<div class="toggle-group">
<label class="toggle-switch">
<input type="checkbox" id="allow-localhost-toggle">
<span class="slider"></span>
</label>
<span class="toggle-label" data-i18n="basic_settings.localhost_allow">允许本地未认证访问</span>
</div>
</div>
</div>
</section> </section>
<!-- API 密钥管理 --> <!-- API 密钥管理 -->
@@ -394,7 +355,19 @@
<div id="openai-providers-list" class="provider-list"></div> <div id="openai-providers-list" class="provider-list"></div>
</div> </div>
</div> </div>
</section>
<!-- 认证文件管理 -->
<section id="auth-files" class="content-section">
<h2 data-i18n="auth_files.title">认证文件管理</h2>
<div class="card" style="margin-bottom: 20px;">
<div class="card-content">
<p class="form-hint" data-i18n="auth_files.description">
这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
</p>
</div>
</div>
<!-- Gemini Web Token --> <!-- Gemini Web Token -->
<div class="card"> <div class="card">
@@ -416,20 +389,11 @@
<label for="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label> <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 值"> <input type="text" id="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_placeholder" placeholder="输入 __Secure-1PSIDTS cookie 值">
</div> </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>
</section>
<!-- 认证文件管理 -->
<section id="auth-files" class="content-section">
<h2 data-i18n="auth_files.title">认证文件管理</h2>
<div class="card" style="margin-bottom: 20px;">
<div class="card-content">
<p class="form-hint" data-i18n="auth_files.description">
这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
</p>
</div>
</div> </div>
<!-- 认证文件 --> <!-- 认证文件 -->
@@ -452,6 +416,112 @@
</div> </div>
</section> </section>
<!-- 使用统计 -->
<section id="usage-stats" class="content-section">
<h2 data-i18n="usage_stats.title">使用统计</h2>
<!-- 概览统计卡片 -->
<div class="stats-overview">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-paper-plane"></i>
</div>
<div class="stat-content">
<div class="stat-number" id="total-requests">0</div>
<div class="stat-label" data-i18n="usage_stats.total_requests">总请求数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-content">
<div class="stat-number" id="success-requests">0</div>
<div class="stat-label" data-i18n="usage_stats.success_requests">成功请求</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon error">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="stat-content">
<div class="stat-number" id="failed-requests">0</div>
<div class="stat-label" data-i18n="usage_stats.failed_requests">失败请求</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-coins"></i>
</div>
<div class="stat-content">
<div class="stat-number" id="total-tokens">0</div>
<div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-container">
<!-- 请求趋势图 -->
<div class="card chart-card">
<div class="card-header">
<h3><i class="fas fa-chart-line"></i> <span data-i18n="usage_stats.requests_trend">请求趋势</span></h3>
<div class="chart-controls">
<button class="btn btn-small" data-period="hour" id="requests-hour-btn">
<span data-i18n="usage_stats.by_hour">按小时</span>
</button>
<button class="btn btn-small active" data-period="day" id="requests-day-btn">
<span data-i18n="usage_stats.by_day">按天</span>
</button>
</div>
</div>
<div class="card-content">
<div class="chart-container">
<canvas id="requests-chart"></canvas>
</div>
</div>
</div>
<!-- Token使用趋势图 -->
<div class="card chart-card">
<div class="card-header">
<h3><i class="fas fa-chart-area"></i> <span data-i18n="usage_stats.tokens_trend">Token 使用趋势</span></h3>
<div class="chart-controls">
<button class="btn btn-small" data-period="hour" id="tokens-hour-btn">
<span data-i18n="usage_stats.by_hour">按小时</span>
</button>
<button class="btn btn-small active" data-period="day" id="tokens-day-btn">
<span data-i18n="usage_stats.by_day">按天</span>
</button>
</div>
</div>
<div class="card-content">
<div class="chart-container">
<canvas id="tokens-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- API详细统计 -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-list"></i> <span data-i18n="usage_stats.api_details">API 详细统计</span></h3>
<button id="refresh-usage-stats" class="btn btn-primary">
<i class="fas fa-sync-alt"></i> <span data-i18n="usage_stats.refresh">刷新</span>
</button>
</div>
<div class="card-content">
<div id="api-stats-table" class="api-stats-table">
<div class="loading-placeholder" data-i18n="common.loading">正在加载...</div>
</div>
</div>
</div>
</section>
<!-- 系统信息 --> <!-- 系统信息 -->
<section id="system-info" class="content-section"> <section id="system-info" class="content-section">
<h2 data-i18n="system_info.title">系统信息</h2> <h2 data-i18n="system_info.title">系统信息</h2>
@@ -480,6 +550,15 @@
</section> </section>
</div> </div>
</main> </main>
<!-- 版本信息 -->
<footer class="version-footer">
<div class="version-info">
<span data-i18n="footer.version">版本</span>: v0.0.5
<span class="separator"></span>
<span data-i18n="footer.author">作者</span>: Supra4E8C
</div>
</footer>
</div> </div>
<!-- 模态框 --> <!-- 模态框 -->

View File

@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"start": "npx serve .", "start": "npx serve .",
"dev": "npx serve . --port 3000", "dev": "npx serve . --port 3000",
"build": "echo '无需构建,直接使用静态文件'", "build": "node build.js",
"lint": "echo '使用浏览器开发者工具检查代码'" "lint": "echo '使用浏览器开发者工具检查代码'"
}, },
"keywords": [ "keywords": [

View File

@@ -27,6 +27,9 @@
--accent-primary: linear-gradient(135deg, #475569, #334155); --accent-primary: linear-gradient(135deg, #475569, #334155);
--accent-secondary: #e2e8f0; --accent-secondary: #e2e8f0;
--accent-tertiary: #f8fafc; --accent-tertiary: #f8fafc;
--primary-color: #3b82f6;
--card-bg: #ffffff;
--border-color: #e2e8f0;
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0); --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
--success-text: #166534; --success-text: #166534;
@@ -69,6 +72,9 @@
--accent-primary: linear-gradient(135deg, #64748b, #475569); --accent-primary: linear-gradient(135deg, #64748b, #475569);
--accent-secondary: #334155; --accent-secondary: #334155;
--accent-tertiary: #1e293b; --accent-tertiary: #1e293b;
--primary-color: #38bdf8;
--card-bg: #1e293b;
--border-color: #334155;
--success-bg: linear-gradient(135deg, #064e3b, #047857); --success-bg: linear-gradient(135deg, #064e3b, #047857);
--success-text: #bbf7d0; --success-text: #bbf7d0;
@@ -290,155 +296,6 @@
margin-bottom: 25px; margin-bottom: 25px;
} }
/* 选项卡导航样式 */
.connection-tabs {
display: flex;
margin-bottom: 25px;
margin-top: 10px;
border-radius: 12px;
background: var(--bg-tertiary);
padding: 4px;
border: 1px solid var(--border-primary);
}
.tab-button {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
color: var(--text-tertiary);
}
.tab-button:hover {
background: var(--accent-tertiary);
}
.tab-button.active {
background: var(--accent-primary);
color: var(--text-inverse);
box-shadow: var(--shadow-primary);
}
.tab-button i {
font-size: 20px;
min-width: 20px;
}
.tab-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.tab-title {
font-weight: 600;
font-size: 16px;
}
.tab-subtitle {
font-size: 12px;
opacity: 0.8;
line-height: 1.3;
}
/* 代理设置样式 */
.proxy-settings {
margin-bottom: 25px;
padding: 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 10px;
}
.proxy-settings label {
display: block;
margin-bottom: 8px;
color: var(--text-secondary);
font-weight: 600;
font-size: 14px;
}
.proxy-settings 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-secondary);
color: var(--text-primary);
}
.proxy-settings input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--border-primary);
}
/* 连接表单样式 */
.connection-form {
display: none;
}
.connection-form.active {
display: block;
}
/* 本地URL组合输入框样式 */
.local-url-group {
display: flex;
align-items: center;
border: 2px solid var(--border-primary);
border-radius: 8px;
background: var(--bg-secondary);
transition: all 0.3s ease;
overflow: hidden;
}
.local-url-group:focus-within {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--border-primary);
}
.url-prefix {
padding: 12px 16px;
background: var(--bg-tertiary);
color: var(--text-tertiary);
font-weight: 500;
font-size: 14px;
border-right: 1px solid var(--border-primary);
white-space: nowrap;
}
.local-url-group input {
flex: 1;
padding: 12px 16px;
border: none;
font-size: 14px;
background: transparent;
color: var(--text-primary);
outline: none;
min-width: 80px;
}
.local-url-group input::-webkit-outer-spin-button,
.local-url-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.local-url-group input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.login-title { .login-title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -469,8 +326,63 @@
text-align: center; text-align: center;
} }
.login-body {
display: flex;
flex-direction: column;
gap: 24px;
}
.login-connection-info {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
[data-theme="dark"] .login-connection-info {
background: rgba(30, 41, 59, 0.7);
border-color: rgba(100, 116, 139, 0.3);
}
.connection-summary {
display: flex;
align-items: center;
gap: 16px;
color: var(--text-secondary);
}
.connection-summary i {
font-size: 24px;
color: var(--primary-color);
}
.connection-url {
font-size: 16px;
color: var(--text-secondary);
}
.connection-url-separator {
margin: 0 8px;
color: var(--text-tertiary);
}
#login-connection-url {
font-family: "Fira Code", "Consolas", "Courier New", monospace;
color: var(--text-primary);
word-break: break-all;
}
[data-theme="dark"] #login-connection-url {
color: var(--text-secondary);
}
.login-form { .login-form {
width: 100%; display: flex;
flex-direction: column;
gap: 20px;
} }
.login-form .form-group { .login-form .form-group {
@@ -638,41 +550,6 @@
height: 50px; height: 50px;
} }
.connection-tabs {
flex-direction: column;
gap: 4px;
}
.tab-button {
flex-direction: row;
gap: 12px;
padding: 14px;
}
.tab-subtitle {
display: none;
}
.proxy-settings {
padding: 12px;
}
.local-url-group {
flex-direction: column;
}
.url-prefix {
border-right: none;
border-bottom: 1px solid #e2e8f0;
border-radius: 8px 8px 0 0;
text-align: center;
}
.local-url-group input {
border-radius: 0 0 8px 8px;
text-align: center;
}
.login-form .input-group { .login-form .input-group {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -1613,3 +1490,257 @@ input:checked + .slider:before {
line-height: 1.4; line-height: 1.4;
} }
/* 使用统计样式 */
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-card:hover {
border-color: var(--border-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-color);
color: white;
font-size: 20px;
flex-shrink: 0;
}
.stat-icon.success {
background: #10b981;
}
.stat-icon.error {
background: #ef4444;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.charts-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
@media (max-width: 1200px) {
.charts-container {
grid-template-columns: 1fr;
}
}
.chart-card {
min-height: 400px;
}
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
.chart-controls {
display: flex;
gap: 8px;
}
.btn.btn-small {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--card-bg);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.btn.btn-small:hover {
border-color: var(--border-primary);
color: var(--text-primary);
}
.btn.btn-small.active {
background: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.api-stats-table {
overflow-x: auto;
}
.stats-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
.stats-table th,
.stats-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.stats-table th {
background: var(--bg-secondary);
font-weight: 600;
color: var(--text-primary);
font-size: 14px;
}
.stats-table td {
color: var(--text-secondary);
font-size: 14px;
}
.stats-table tr:hover {
background: var(--bg-secondary);
}
.model-details {
margin-top: 8px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 12px;
}
.model-item {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
color: var(--text-tertiary);
}
.model-name {
font-weight: 500;
color: var(--text-secondary);
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
color: var(--text-tertiary);
font-size: 14px;
}
.no-data-message {
text-align: center;
color: var(--text-tertiary);
font-style: italic;
padding: 40px;
}
/* 暗色主题适配 */
[data-theme="dark"] .stat-card {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] .stat-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
[data-theme="dark"] .btn.btn-small {
background: var(--bg-tertiary);
}
/* 版本信息样式 */
.version-footer {
margin-top: 2rem;
padding: 1rem 0;
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
.version-info {
text-align: center;
font-size: 0.875rem;
color: var(--text-secondary);
opacity: 0.8;
}
.version-info .separator {
margin: 0 0.75rem;
color: var(--text-secondary);
opacity: 0.6;
}
/* 暗黑主题下的版本信息 */
[data-theme="dark"] .version-footer {
border-top-color: var(--border);
background: var(--bg-secondary);
}
[data-theme="dark"] .version-info {
color: var(--text-secondary);
}
.connection-reset-btn {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.connection-reset-btn i {
margin: 0;
}
.connection-reset-btn span {
font-size: 13px;
}
[data-theme="dark"] .connection-reset-btn {
background: rgba(30, 41, 59, 0.9);
border-color: rgba(100, 116, 139, 0.4);
color: #cbd5e1;
}
[data-theme="dark"] .connection-reset-btn:hover {
background: rgba(51, 65, 85, 0.95);
border-color: rgba(100, 116, 139, 0.6);
}

64
webpack.config.js Normal file
View File

@@ -0,0 +1,64 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'production',
entry: {
main: './bundle-entry.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset/inline'
},
{
test: /\.html$/i,
loader: 'html-loader',
options: {
sources: {
list: [
{
tag: 'img',
attribute: 'src',
type: 'src'
}
]
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.build.html',
filename: 'index.html',
inject: 'body',
minify: {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}
}),
new HtmlInlineScriptPlugin({
htmlMatchPattern: [/index.html$/],
scriptMatchPattern: [/.js$/]
})
],
optimization: {
minimize: true
}
};