Compare commits

...

15 Commits

Author SHA1 Message Date
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
Supra4E8C
5f597afb42 update app.js
1.去除了部分无法使用的OAuth逻辑
2.修复了Gemini Web保存功能
2025-09-25 15:52:52 +08:00
Supra4E8C
e5bef7e2b0 Add files via upload 2025-09-25 15:29:48 +08:00
Supra4E8C
d8df9ce680 Add files via upload 2025-09-25 15:29:26 +08:00
Supra4E8C
44594220b2 Add files via upload 2025-09-24 19:26:09 +08:00
Supra4E8C
86fc9fe86e 更新 README.md 2025-09-24 19:25:23 +08:00
Supra4E8C
67f009b81c Delete README_EN.md 2025-09-24 19:22:48 +08:00
Supra4E8C
6618312360 更新 README_EN.md 2025-09-24 19:19:30 +08:00
Supra4E8C
9e49824c52 创建 README_EN.md 2025-09-24 19:18:02 +08:00
Supra4E8C
015754237b 更新 README.md
更新readme 使内容与0.0.1版本匹配
2025-09-24 19:10:23 +08:00
14 changed files with 4629 additions and 173 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 cli-proxy-api-management-center.html
ls -lh cli-proxy-api-management-center.html
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/cli-proxy-api-management-center.html
body: |
## CLI Proxy API Management Center - ${{ github.ref_name }}
### Download and Usage
1. Download the `cli-proxy-api-management-center.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.

214
README.md
View File

@@ -1,145 +1,143 @@
# Cli-Proxy-API-Management-Center
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
This is a modern web interface for managing the CLI Proxy API.
## 功能特点
[中文文档](README_CN.md)
### 认证管理
- 支持管理密钥认证
- 可配置 API 基础地址
- 实时连接状态检测
Main Project:
https://github.com/router-for-me/CLIProxyAPI
### 基础设置
- **调试模式**: 开启/关闭调试功能
- **代理设置**: 配置代理服务器 URL
- **请求重试**: 设置请求重试次数
- **配额管理**: 配置超出配额时的行为
- **本地访问**: 允许本地未认证访问
Example URL:
https://remote.router-for.me/
### API 密钥管理
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥
- **Gemini API**: 管理 Google Gemini 生成式语言 API 密钥
- **Codex API**: 管理 OpenAI Codex API 配置
- **Claude API**: 管理 Anthropic Claude API 配置
- **OpenAI 兼容提供商**: 管理 OpenAI 兼容的第三方提供商
Minimum required version: ≥ 5.0.0
Recommended version: ≥ 5.2.6
### 认证文件管理
- 上传认证 JSON 文件
- 下载现有认证文件
- 删除单个或所有认证文件
- 显示文件详细信息
## Features
### 系统监控
- 实时 API 连接状态
- 最后更新时间跟踪
- 详细的错误信息提示
### Authentication Management
- Supports management key authentication
- Configurable API base address
- Real-time connection status detection
## 使用方法
### Basic Settings
- **Debug Mode**: Enable/disable debugging
- **Proxy Settings**: Configure proxy server URL
- **Request Retries**: Set the number of request retries
- **Quota Management**: Configure behavior when the quota is exceeded
- **Local Access**: Manage local unauthenticated access
### 1. 直接使用(推荐)
直接用浏览器打开 `index.html` 文件即可使用。
### API Key Management
- **Proxy Service Authentication Key**: Manage API keys for the proxy service
- **Gemini API**: Manage Google Gemini generative language API keys
- **Codex API**: Manage OpenAI Codex API configuration
- **Claude API**: Manage Anthropic Claude API configuration
- **OpenAI-Compatible Providers**: Manage OpenAI-compatible third-party providers
### 2. 使用本地服务器
### Authentication File Management
- Upload authentication JSON files
- Download existing authentication files
- Delete single or all authentication files
- Display file details
## How to Use
### 1. Direct Use (Recommended)
Simply open the `index.html` file directly in your browser to use it.
### 2. Use a Local Server
```bash
# 安装依赖
# Install dependencies
npm install
# 使用默认端口(3000
# Start the server on the default port (3000)
npm start
```
### 3. 配置 API 连接
1. 打开管理界面
2. 在认证配置区域输入:
- **API 地址**: `http://localhost:8317`/v0/management将会自动为您补全
- **管理密钥**: 您的管理密钥
3. 点击"测试连接"按钮
4. 连接成功后即可使用所有功能
### 3. Configure API Connection
1. Open the management interface.
2. On the login screen, enter:
- **Remote Address**: `http://localhost:8317` (`/v0/management` will be auto-completed for you)
- **Management Key**: Your management key
3. Click the "Connect" button.
4. Once connected successfully, all features will be available.
## 界面说明
## Interface Description
### 导航菜单
- **基础设置**: 调试、代理、重试等基本配置
- **API 密钥**: 各种 API 服务的密钥管理
- **AI 提供商**: AI 服务提供商配置
- **认证文件**: 认证文件的上传下载管理
- **系统信息**: 连接状态和系统信息
### Navigation Menu
- **Basic Settings**: Basic configurations like debugging, proxy, retries, etc.
- **API Keys**: Management of keys for various API services.
- **AI Providers**: Configuration for AI service providers.
- **Auth Files**: Upload and download management for authentication files.
- **System Info**: Connection status and system information.
### 操作按钮
- **刷新全部**: 重新加载所有配置数据
- **连接状态**: 检查 API 连接状态
- **添加**: 添加新的配置项
- **编辑**: 编辑现有配置
- **删除**: 删除配置项
## Feature Highlights
## 特性亮点
### Modern UI
- Responsive design, supports all screen sizes
- Beautiful gradient colors and shadow effects
- Smooth animations and transition effects
- Intuitive icons and status indicators
### 现代化 UI
- 响应式设计,支持各种屏幕尺寸
- 美观的渐变色彩和阴影效果
- 流畅的动画和过渡效果
- 直观的图标和状态指示
### Real-time Updates
- Configuration changes take effect immediately
- Real-time status feedback
- Automatic data refresh
### 实时更新
- 配置更改立即生效
- 实时状态反馈
- 自动数据刷新
### Security Features
- Masked display for keys
### 安全特性
- 密钥遮蔽显示
### Responsive Design
- Perfectly adapts to desktop and mobile devices
- Adaptive layout
- Touch-friendly interactions
### 响应式设计
- 完美适配桌面和移动设备
- 自适应布局
- 触摸友好的交互
## Tech Stack
## 技术栈
- **Frontend**: Plain HTML, CSS, JavaScript
- **Styling**: CSS3 + Flexbox/Grid
- **Icons**: Font Awesome 6.4.0
- **Fonts**: Segoe UI system font
- **API**: RESTful API calls
- **前端**: 纯 HTML、CSS、JavaScript
- **样式**: CSS3 + Flexbox/Grid
- **图标**: Font Awesome 6.4.0
- **字体**: Segoe UI 系统字体
- **API**: RESTful API 调用
## Troubleshooting
## 故障排除
### Connection Issues
1. Confirm that the CLI Proxy API service is running.
2. Check if the API address is correct.
3. Verify that the management key is valid.
4. Ensure your firewall settings allow the connection.
### 连接问题
1. 确认 CLI Proxy API 服务正在运行
2. 检查 API 地址是否正确
3. 验证管理密钥是否有效
4. 确认防火墙设置允许连接
### Data Not Updating
1. Click the "Refresh All" button.
2. Check your network connection.
3. Check the browser's console for any error messages.
### 数据不更新
1. 点击"刷新全部"按钮
2. 检查网络连接
3. 查看浏览器控制台错误信息
## Development Information
## 开发说明
### 文件结构
### File Structure
```
webui/
├── index.html # 主页面
├── styles.css # 样式文件
├── app.js # 应用逻辑
├── package.json # 项目配置
── README.md # 说明文档
├── index.html # Main page
├── styles.css # Stylesheet
├── app.js # Application logic
├── package.json # Project configuration
── i18n.js # Internationalization support
└── README.md # README document
```
### API 调用
所有 API 调用都通过 `CLIProxyManager` 类的 `makeRequest` 方法处理,包含:
- 自动添加认证头
- 错误处理
- JSON 响应解析
### API Calls
All API calls are handled through the `makeRequest` method of the `ManagerAPI` class, which includes:
- Automatic addition of authentication headers
- Error handling
- JSON response parsing
### 状态管理
- 本地存储保存 API 地址和密钥
- 内存中维护连接状态
- 实时数据刷新机制
### State Management
- API address and key are saved in local storage
- Connection status is maintained in memory
- Real-time data refresh mechanism
## 许可证
## Contributing
We welcome Issues and Pull Requests to improve this project! We encourage more developers to contribute to the enhancement of this WebUI!
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目!
This project is licensed under the MIT License.

140
README_CN.md Normal file
View File

@@ -0,0 +1,140 @@
# Cli-Proxy-API-Management-Center
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
主项目
https://github.com/router-for-me/CLIProxyAPI
示例网站:
https://remote.router-for.me/
最低可用版本 ≥ 5.0.0
推荐版本 ≥ 5.2.6
## 功能特点
### 认证管理
- 支持管理密钥认证
- 可配置 API 基础地址
- 实时连接状态检测
### 基础设置
- **调试模式**: 开启/关闭调试功能
- **代理设置**: 配置代理服务器 URL
- **请求重试**: 设置请求重试次数
- **配额管理**: 配置超出配额时的行为
- **本地访问**: 管理本地未认证访问
### API 密钥管理
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥
- **Gemini API**: 管理 Google Gemini 生成式语言 API 密钥
- **Codex API**: 管理 OpenAI Codex API 配置
- **Claude API**: 管理 Anthropic Claude API 配置
- **OpenAI 兼容提供商**: 管理 OpenAI 兼容的第三方提供商
### 认证文件管理
- 上传认证 JSON 文件
- 下载现有认证文件
- 删除单个或所有认证文件
- 显示文件详细信息
## 使用方法
### 1. 直接使用(推荐)
直接用浏览器打开 `index.html` 文件即可使用。
### 2. 使用本地服务器
```bash
# 安装依赖
npm install
# 使用默认端口(3000
npm start
```
### 3. 配置 API 连接
1. 打开管理界面
2. 在登录界面上输入:
- **远程地址**: `http://localhost:8317`/v0/management将会自动为您补全
- **管理密钥**: 您的管理密钥
3. 点击"连接"按钮
4. 连接成功后即可使用所有功能
## 界面说明
### 导航菜单
- **基础设置**: 调试、代理、重试等基本配置
- **API 密钥**: 各种 API 服务的密钥管理
- **AI 提供商**: AI 服务提供商配置
- **认证文件**: 认证文件的上传下载管理
- **系统信息**: 连接状态和系统信息
## 特性亮点
### 现代化 UI
- 响应式设计,支持各种屏幕尺寸
- 美观的渐变色彩和阴影效果
- 流畅的动画和过渡效果
- 直观的图标和状态指示
### 实时更新
- 配置更改立即生效
- 实时状态反馈
- 自动数据刷新
### 安全特性
- 密钥遮蔽显示
### 响应式设计
- 完美适配桌面和移动设备
- 自适应布局
- 触摸友好的交互
## 技术栈
- **前端**: 纯 HTML、CSS、JavaScript
- **样式**: CSS3 + Flexbox/Grid
- **图标**: Font Awesome 6.4.0
- **字体**: Segoe UI 系统字体
- **API**: RESTful API 调用
## 故障排除
### 连接问题
1. 确认 CLI Proxy API 服务正在运行
2. 检查 API 地址是否正确
3. 验证管理密钥是否有效
4. 确认防火墙设置允许连接
### 数据不更新
1. 点击"刷新全部"按钮
2. 检查网络连接
3. 查看浏览器控制台错误信息
## 开发说明
### 文件结构
```
webui/
├── index.html # 主页面
├── styles.css # 样式文件
├── app.js # 应用逻辑
├── package.json # 项目配置
├── i18n.js # 国际化支持
└── README.md # 说明文档
```
### API 调用
所有 API 调用都通过 `ManagerAPI` 类的 `makeRequest` 方法处理,包含:
- 自动添加认证头
- 错误处理
- JSON 响应解析
### 状态管理
- 本地存储保存 API 地址和密钥
- 内存中维护连接状态
- 实时数据刷新机制
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目我们欢迎更多的大佬来对这个WebUI进行更新
本项目采用MIT许可

499
app.js
View File

@@ -38,7 +38,6 @@ class CLIProxyManager {
if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
this.currentTheme = savedTheme;
} else {
// 根据系统偏好自动选择
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.currentTheme = 'dark';
} else {
@@ -539,7 +538,6 @@ class CLIProxyManager {
const updateRetry = document.getElementById('update-retry');
const switchProjectToggle = document.getElementById('switch-project-toggle');
const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
const allowLocalhostToggle = document.getElementById('allow-localhost-toggle');
if (debugToggle) {
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
@@ -559,9 +557,6 @@ class CLIProxyManager {
if (switchPreviewToggle) {
switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
}
if (allowLocalhostToggle) {
allowLocalhostToggle.addEventListener('change', (e) => this.updateAllowLocalhost(e.target.checked));
}
// API 密钥管理
const addApiKey = document.getElementById('add-api-key');
@@ -586,6 +581,13 @@ class CLIProxyManager {
addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal());
}
// 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 deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
@@ -601,6 +603,29 @@ class CLIProxyManager {
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');
if (closeBtn) {
@@ -726,8 +751,7 @@ class CLIProxyManager {
this.managementKey = savedKey;
}
// 注意不再处理DOM元素因为认证配置已改为只读显示
// DOM更新由updateConnectionInfo()方法处理
}
// API 请求方法
@@ -934,6 +958,9 @@ class CLIProxyManager {
// 认证文件需要单独加载,因为不在配置中
await this.loadAuthFiles();
// 使用统计需要单独加载
await this.loadUsageStats();
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
} catch (error) {
console.error('加载配置失败:', error);
@@ -970,10 +997,6 @@ class CLIProxyManager {
}
}
// 本地访问设置
if (config['allow-localhost-unauthenticated'] !== undefined) {
document.getElementById('allow-localhost-toggle').checked = config['allow-localhost-unauthenticated'];
}
// API 密钥
if (config['api-keys']) {
@@ -1008,7 +1031,6 @@ class CLIProxyManager {
this.loadProxySettings(),
this.loadRetrySettings(),
this.loadQuotaSettings(),
this.loadLocalhostSettings(),
this.loadApiKeys(),
this.loadGeminiKeys(),
this.loadCodexKeys(),
@@ -1161,32 +1183,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密钥
async loadApiKeys() {
@@ -2187,6 +2183,433 @@ class CLIProxyManager {
}
}
// 显示 Gemini Web Token 模态框
showGeminiWebTokenModal() {
const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-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="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');
if (modalSecure1psid && inlineSecure1psid) {
modalSecure1psid.value = inlineSecure1psid.value.trim();
}
if (modalSecure1psidts && inlineSecure1psidts) {
modalSecure1psidts.value = inlineSecure1psidts.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();
if (!secure1psid || !secure1psidts) {
this.showNotification('请填写完整的 Cookie 信息', 'error');
return;
}
try {
const response = await this.makeRequest('/gemini-web-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
})
});
this.closeModal();
this.loadAuthFiles(); // 刷新认证文件列表
const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
if (inlineSecure1psid) {
inlineSecure1psid.value = secure1psid;
}
if (inlineSecure1psidts) {
inlineSecure1psidts.value = secure1psidts;
}
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
} catch (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() {
const modal = document.getElementById('modal');
if (modal) {
modal.style.display = 'block';
}
}
// 关闭模态框
closeModal() {
document.getElementById('modal').style.display = 'none';

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');

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';
}
});

84
i18n.js
View File

@@ -85,6 +85,7 @@ const i18n = {
'nav.api_keys': 'API 密钥',
'nav.ai_providers': 'AI 提供商',
'nav.auth_files': '认证文件',
'nav.usage_stats': '使用统计',
'nav.system_info': '系统信息',
// 基础设置
@@ -102,8 +103,6 @@ const i18n = {
'basic_settings.quota_title': '配额超出行为',
'basic_settings.quota_switch_project': '自动切换项目',
'basic_settings.quota_switch_preview': '切换到预览模型',
'basic_settings.localhost_title': '本地访问',
'basic_settings.localhost_allow': '允许本地未认证访问',
// API 密钥管理
'api_keys.title': 'API 密钥管理',
@@ -182,9 +181,11 @@ const i18n = {
'ai_providers.openai_keys_count': '密钥数量',
'ai_providers.openai_models_count': '模型数量',
// 认证文件管理
'auth_files.title': '认证文件管理',
'auth_files.title_section': '认证文件',
'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。',
'auth_files.upload_button': '上传文件',
'auth_files.delete_all_button': '删除全部',
'auth_files.empty_title': '暂无认证文件',
@@ -202,6 +203,36 @@ const i18n = {
'auth_files.delete_all_success': '成功删除',
'auth_files.files_count': '个文件',
// Gemini Web Token
'auth_login.gemini_web_title': 'Gemini Web Token',
'auth_login.gemini_web_button': '保存 Gemini Web Token',
'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。',
'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
'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.connection_status_title': '连接状态',
@@ -220,7 +251,6 @@ const i18n = {
'notification.retry_updated': '重试设置已更新',
'notification.quota_switch_project_updated': '项目切换设置已更新',
'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
'notification.localhost_updated': '本地访问设置已更新',
'notification.api_key_added': 'API密钥添加成功',
'notification.api_key_updated': 'API密钥更新成功',
'notification.api_key_deleted': 'API密钥删除成功',
@@ -264,7 +294,11 @@ const i18n = {
'theme.dark': '暗色',
'theme.switch_to_light': '切换到亮色模式',
'theme.switch_to_dark': '切换到暗色模式',
'theme.auto': '跟随系统'
'theme.auto': '跟随系统',
// 页脚
'footer.version': '版本',
'footer.author': '作者'
},
'en-US': {
@@ -346,6 +380,7 @@ const i18n = {
'nav.api_keys': 'API Keys',
'nav.ai_providers': 'AI Providers',
'nav.auth_files': 'Auth Files',
'nav.usage_stats': 'Usage Statistics',
'nav.system_info': 'System Info',
// Basic settings
@@ -363,8 +398,6 @@ const i18n = {
'basic_settings.quota_title': 'Quota Exceeded Behavior',
'basic_settings.quota_switch_project': 'Auto Switch Project',
'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.title': 'API Keys Management',
@@ -443,9 +476,11 @@ const i18n = {
'ai_providers.openai_keys_count': 'Keys Count',
'ai_providers.openai_models_count': 'Models Count',
// Auth files management
'auth_files.title': 'Auth Files Management',
'auth_files.title_section': 'Auth Files',
'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.',
'auth_files.upload_button': 'Upload File',
'auth_files.delete_all_button': 'Delete All',
'auth_files.empty_title': 'No Auth Files',
@@ -463,6 +498,36 @@ const i18n = {
'auth_files.delete_all_success': 'Successfully deleted',
'auth_files.files_count': 'files',
// Gemini Web Token
'auth_login.gemini_web_title': 'Gemini Web Token',
'auth_login.gemini_web_button': 'Save Gemini Web Token',
'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.',
'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
'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.title': 'System Information',
'system_info.connection_status_title': 'Connection Status',
@@ -481,7 +546,6 @@ const i18n = {
'notification.retry_updated': 'Retry settings updated',
'notification.quota_switch_project_updated': 'Project 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_updated': 'API key updated successfully',
'notification.api_key_deleted': 'API key deleted successfully',
@@ -525,7 +589,11 @@ const i18n = {
'theme.dark': 'Dark',
'theme.switch_to_light': 'Switch to light 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>
<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">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="i18n.js"></script>
</head>
<body>
@@ -221,6 +222,9 @@
<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>
</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">
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
</a></li>
@@ -305,21 +309,6 @@
</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>
<!-- API 密钥管理 -->
@@ -394,12 +383,45 @@
<div id="openai-providers-list" class="provider-list"></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>
</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 class="card">
<div class="card-header">
<h3><i class="fas fa-file-alt"></i> <span data-i18n="auth_files.title_section">认证文件</span></h3>
@@ -419,6 +441,112 @@
</div>
</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">
<h2 data-i18n="system_info.title">系统信息</h2>
@@ -447,6 +575,15 @@
</section>
</div>
</main>
<!-- 版本信息 -->
<footer class="version-footer">
<div class="version-info">
<span data-i18n="footer.version">版本</span>: v0.0.3
<span class="separator"></span>
<span data-i18n="footer.author">作者</span>: Supra4E8C
</div>
</footer>
</div>
<!-- 模态框 -->

3136
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@
"scripts": {
"start": "npx serve .",
"dev": "npx serve . --port 3000",
"build": "echo '无需构建,直接使用静态文件'",
"prebuild": "node build-scripts/prepare-html.js",
"build": "webpack --config webpack.config.js",
"postbuild": "rm -f index.build.html",
"lint": "echo '使用浏览器开发者工具检查代码'"
},
"keywords": [
@@ -18,7 +20,14 @@
"author": "CLI Proxy API WebUI",
"license": "MIT",
"devDependencies": {
"serve": "^14.2.1"
"css-loader": "^6.8.1",
"html-inline-script-webpack-plugin": "^3.2.1",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.4",
"serve": "^14.2.1",
"style-loader": "^3.3.3",
"webpack": "^5.102.0",
"webpack-cli": "^5.1.4"
},
"engines": {
"node": ">=14.0.0"
@@ -26,6 +35,5 @@
"repository": {
"type": "git",
"url": "local"
},
"dependencies": {}
}
}

View File

@@ -27,6 +27,9 @@
--accent-primary: linear-gradient(135deg, #475569, #334155);
--accent-secondary: #e2e8f0;
--accent-tertiary: #f8fafc;
--primary-color: #3b82f6;
--card-bg: #ffffff;
--border-color: #e2e8f0;
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
--success-text: #166534;
@@ -69,6 +72,9 @@
--accent-primary: linear-gradient(135deg, #64748b, #475569);
--accent-secondary: #334155;
--accent-tertiary: #1e293b;
--primary-color: #38bdf8;
--card-bg: #1e293b;
--border-color: #334155;
--success-bg: linear-gradient(135deg, #064e3b, #047857);
--success-text: #bbf7d0;
@@ -436,6 +442,7 @@
.local-url-group input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.login-title {
@@ -1572,3 +1579,271 @@ input:checked + .slider:before {
opacity: 1;
}
}
/* 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 {
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);
}

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
}
};