Compare commits

..

19 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
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
15 changed files with 2938 additions and 592 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.

215
README.md
View File

@@ -1,145 +1,144 @@
# Cli-Proxy-API-Management-Center # Cli-Proxy-API-Management-Center
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。 This is a modern web interface for managing the CLI Proxy API.
## 功能特点 [中文文档](README_CN.md)
### 认证管理 Main Project:
- 支持管理密钥认证 https://github.com/router-for-me/CLIProxyAPI
- 可配置 API 基础地址
- 实时连接状态检测
### 基础设置 Example URL:
- **调试模式**: 开启/关闭调试功能 https://remote.router-for.me/
- **代理设置**: 配置代理服务器 URL
- **请求重试**: 设置请求重试次数
- **配额管理**: 配置超出配额时的行为
- **本地访问**: 允许本地未认证访问
### API 密钥管理 Minimum required version: ≥ 6.0.0
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥 Recommended version: ≥ 6.0.19
- **Gemini API**: 管理 Google Gemini 生成式语言 API 密钥 Starting from version 6.0.19, the WebUI has been integrated into the main program and is accessible via `/management.html`.
- **Codex API**: 管理 OpenAI Codex API 配置
- **Claude API**: 管理 Anthropic Claude API 配置
- **OpenAI 兼容提供商**: 管理 OpenAI 兼容的第三方提供商
### 认证文件管理 ## Features
- 上传认证 JSON 文件
- 下载现有认证文件
- 删除单个或所有认证文件
- 显示文件详细信息
### 系统监控 ### Authentication Management
- 实时 API 连接状态 - 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. 直接使用(推荐) ### API Key Management
直接用浏览器打开 `index.html` 文件即可使用。 - **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 ```bash
# 安装依赖 # Install dependencies
npm install npm install
# 使用默认端口(3000 # Start the server on the default port (3000)
npm start npm start
``` ```
### 3. 配置 API 连接 ### 3. Configure API Connection
1. 打开管理界面 1. Open the management interface.
2. 在认证配置区域输入: 2. On the login screen, enter:
- **API 地址**: `http://localhost:8317`/v0/management将会自动为您补全 - **Remote Address**: `http://localhost:8317` (`/v0/management` will be auto-completed for you)
- **管理密钥**: 您的管理密钥 - **Management Key**: Your management key
3. 点击"测试连接"按钮 3. Click the "Connect" button.
4. 连接成功后即可使用所有功能 4. Once connected successfully, all features will be available.
## 界面说明 ## Interface Description
### 导航菜单 ### Navigation Menu
- **基础设置**: 调试、代理、重试等基本配置 - **Basic Settings**: Basic configurations like debugging, proxy, retries, etc.
- **API 密钥**: 各种 API 服务的密钥管理 - **API Keys**: Management of keys for various API services.
- **AI 提供商**: AI 服务提供商配置 - **AI Providers**: Configuration for AI service providers.
- **认证文件**: 认证文件的上传下载管理 - **Auth Files**: Upload and download management for authentication files.
- **系统信息**: 连接状态和系统信息 - **System Info**: Connection status and system information.
### 操作按钮 ## Feature Highlights
- **刷新全部**: 重新加载所有配置数据
- **连接状态**: 检查 API 连接状态
- **添加**: 添加新的配置项
- **编辑**: 编辑现有配置
- **删除**: 删除配置项
## 特性亮点 ### 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 ## Troubleshooting
- **样式**: CSS3 + Flexbox/Grid
- **图标**: Font Awesome 6.4.0
- **字体**: Segoe UI 系统字体
- **API**: RESTful API 调用
## 故障排除 ### 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.
### 连接问题 ### Data Not Updating
1. 确认 CLI Proxy API 服务正在运行 1. Click the "Refresh All" button.
2. 检查 API 地址是否正确 2. Check your network connection.
3. 验证管理密钥是否有效 3. Check the browser's console for any error messages.
4. 确认防火墙设置允许连接
### 数据不更新 ## Development Information
1. 点击"刷新全部"按钮
2. 检查网络连接
3. 查看浏览器控制台错误信息
## 开发说明 ### File Structure
### 文件结构
``` ```
webui/ webui/
├── index.html # 主页面 ├── index.html # Main page
├── styles.css # 样式文件 ├── styles.css # Stylesheet
├── app.js # 应用逻辑 ├── app.js # Application logic
├── package.json # 项目配置 ├── package.json # Project configuration
── README.md # 说明文档 ── i18n.js # Internationalization support
└── README.md # README document
``` ```
### API 调用 ### API Calls
所有 API 调用都通过 `CLIProxyManager` 类的 `makeRequest` 方法处理,包含: All API calls are handled through the `makeRequest` method of the `ManagerAPI` class, which includes:
- 自动添加认证头 - Automatic addition of authentication headers
- 错误处理 - Error handling
- JSON 响应解析 - JSON response parsing
### 状态管理 ### State Management
- 本地存储保存 API 地址和密钥 - 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 This project is licensed under the MIT License.
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目!

141
README_CN.md Normal file
View File

@@ -0,0 +1,141 @@
# 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
自6.0.19起WebUI已经集成在主程序中 可以通过/management.html访问
## 功能特点
### 认证管理
- 支持管理密钥认证
- 可配置 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许可

789
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;
@@ -38,7 +39,6 @@ class CLIProxyManager {
if (savedTheme && ['light', 'dark'].includes(savedTheme)) { if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
this.currentTheme = savedTheme; this.currentTheme = savedTheme;
} else { } else {
// 根据系统偏好自动选择
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.currentTheme = 'dark'; this.currentTheme = 'dark';
} else { } else {
@@ -108,6 +108,7 @@ class CLIProxyManager {
this.setupLanguageSwitcher(); this.setupLanguageSwitcher();
this.setupThemeSwitcher(); this.setupThemeSwitcher();
// loadSettings 将在登录成功后调用 // loadSettings 将在登录成功后调用
this.updateLoginConnectionInfo();
} }
// 检查登录状态 // 检查登录状态
@@ -186,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();
} }
// 显示主页面 // 显示主页面
@@ -237,55 +239,40 @@ 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') { this.showLoginError(i18n.t('login.error_required'));
// 本地连接从端口号构建URL return;
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'));
return;
}
apiUrl = `http://localhost:${port}`;
} else {
// 远程连接使用完整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(); if (apiBaseInput && apiBaseInput.value.trim()) {
this.setApiBase(apiBaseInput.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 {
submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`; if (submitBtn) {
submitBtn.disabled = true; submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`;
this.hideLoginError(); submitBtn.disabled = true;
// 如果设置了代理,先保存代理设置
if (proxyUrl) {
localStorage.setItem('proxyUrl', proxyUrl);
} }
this.hideLoginError();
await this.login(apiUrl, managementKey);
this.managementKey = managementKey;
localStorage.setItem('managementKey', this.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 {
submitBtn.innerHTML = originalText; if (submitBtn) {
submitBtn.disabled = false; submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
} }
} }
@@ -356,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) {
loginKeyInput.value = savedKey;
} }
// 设置代理
if (proxyInput && savedProxy) {
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()) {
@@ -426,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));
}
};
const saveProxyDebounced = this.debounce(saveProxy, 500);
// 绑定本地端口输入框
if (localPortInput) {
localPortInput.addEventListener('change', (e) => saveLocalBase(e.target.value));
localPortInput.addEventListener('input', (e) => saveLocalBaseDebounced(e.target.value));
} }
// 绑定远程API输入框 if (apiBaseInput) {
if (remoteApiInput) { const persistBase = (val) => {
remoteApiInput.addEventListener('change', (e) => saveRemoteBase(e.target.value)); const normalized = this.normalizeBase(val);
remoteApiInput.addEventListener('input', (e) => saveRemoteBaseDebounced(e.target.value)); if (normalized) {
this.setApiBase(normalized);
}
};
const persistBaseDebounced = this.debounce(persistBase, 500);
apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
} }
// 绑定本地密钥输入框 if (resetButton) {
if (localKeyInput) { resetButton.addEventListener('click', () => {
localKeyInput.addEventListener('change', (e) => saveKey(e.target.value)); const detected = this.detectApiBaseFromLocation();
localKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value)); this.setApiBase(detected);
} if (apiBaseInput) {
apiBaseInput.value = detected;
// 绑定远程密钥输入框 }
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));
} }
} }
@@ -477,9 +418,6 @@ class CLIProxyManager {
logoutBtn.addEventListener('click', () => this.logout()); logoutBtn.addEventListener('click', () => this.logout());
} }
// 选项卡切换事件
this.setupTabSwitching();
// 密钥可见性切换事件 // 密钥可见性切换事件
this.setupKeyVisibilityToggle(); this.setupKeyVisibilityToggle();
@@ -487,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');
@@ -539,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));
@@ -559,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');
@@ -586,6 +496,13 @@ class CLIProxyManager {
addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal()); 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 uploadAuthFile = document.getElementById('upload-auth-file');
const deleteAllAuthFiles = document.getElementById('delete-all-auth-files'); const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
@@ -601,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) {
@@ -704,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();
} }
// 加载设置(简化版,仅加载内部状态) // 加载设置(简化版,仅加载内部状态)
@@ -711,23 +652,21 @@ class CLIProxyManager {
const savedBase = localStorage.getItem('apiBase'); const savedBase = localStorage.getItem('apiBase');
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;
} }
// 注意不再处理DOM元素因为认证配置已改为只读显示 this.updateLoginConnectionInfo();
// DOM更新由updateConnectionInfo()方法处理
} }
// API 请求方法 // API 请求方法
@@ -934,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);
@@ -970,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']) {
@@ -1008,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(),
@@ -1161,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() {
@@ -2187,10 +2098,481 @@ class CLIProxyManager {
} }
} }
// 显示 Gemini Web Token 模态框
showGeminiWebTokenModal() {
const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = `
<h3>${i18n.t('auth_login.gemini_web_button')}</h3>
<div class="gemini-web-form">
<div class="form-group">
<label for="modal-secure-1psid">${i18n.t('auth_login.secure_1psid_label')}</label>
<input type="text" id="modal-secure-1psid" placeholder="${i18n.t('auth_login.secure_1psid_placeholder')}" required>
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
</div>
<div class="form-group">
<label for="modal-secure-1psidts">${i18n.t('auth_login.secure_1psidts_label')}</label>
<input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required>
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
</div>
<div class="form-group">
<label for="modal-gemini-web-label">${i18n.t('auth_login.gemini_web_label_label')}</label>
<input type="text" id="modal-gemini-web-label" placeholder="${i18n.t('auth_login.gemini_web_label_placeholder')}">
<div class="form-hint">为此认证文件设置一个标签名称(可选)</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button>
</div>
</div>
`;
this.showModal();
const modalSecure1psid = document.getElementById('modal-secure-1psid');
const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
const modalLabel = document.getElementById('modal-gemini-web-label');
if (modalSecure1psid && inlineSecure1psid) {
modalSecure1psid.value = inlineSecure1psid.value.trim();
}
if (modalSecure1psidts && inlineSecure1psidts) {
modalSecure1psidts.value = inlineSecure1psidts.value.trim();
}
if (modalLabel && inlineLabel) {
modalLabel.value = inlineLabel.value.trim();
}
if (modalSecure1psid) {
modalSecure1psid.focus();
}
}
// 保存 Gemini Web Token
async saveGeminiWebToken() {
const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
const label = document.getElementById('modal-gemini-web-label').value.trim();
if (!secure1psid || !secure1psidts) {
this.showNotification('请填写完整的 Cookie 信息', 'error');
return;
}
try {
const requestBody = {
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
};
// 如果提供了 label则添加到请求体中
if (label) {
requestBody.label = label;
}
const response = await this.makeRequest('/gemini-web-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
this.closeModal();
this.loadAuthFiles(); // 刷新认证文件列表
const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
if (inlineSecure1psid) {
inlineSecure1psid.value = secure1psid;
}
if (inlineSecure1psidts) {
inlineSecure1psidts.value = secure1psidts;
}
if (inlineLabel) {
inlineLabel.value = label;
}
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
} catch (error) {
this.showNotification(`保存失败: ${error.message}`, 'error');
}
}
// ===== 使用统计相关方法 =====
// 初始化图表变量
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() { 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 || '';
}
}
} }
// 全局管理器实例 // 全局管理器实例
@@ -2202,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';
}
});

130
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 密钥管理',
@@ -182,9 +175,11 @@ const i18n = {
'ai_providers.openai_keys_count': '密钥数量', 'ai_providers.openai_keys_count': '密钥数量',
'ai_providers.openai_models_count': '模型数量', 'ai_providers.openai_models_count': '模型数量',
// 认证文件管理 // 认证文件管理
'auth_files.title': '认证文件管理', 'auth_files.title': '认证文件管理',
'auth_files.title_section': '认证文件', 'auth_files.title_section': '认证文件',
'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。',
'auth_files.upload_button': '上传文件', 'auth_files.upload_button': '上传文件',
'auth_files.delete_all_button': '删除全部', 'auth_files.delete_all_button': '删除全部',
'auth_files.empty_title': '暂无认证文件', 'auth_files.empty_title': '暂无认证文件',
@@ -202,6 +197,38 @@ const i18n = {
'auth_files.delete_all_success': '成功删除', 'auth_files.delete_all_success': '成功删除',
'auth_files.files_count': '个文件', '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_label_label': '标签 (可选):',
'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)',
'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': '连接状态',
@@ -220,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密钥删除成功',
@@ -264,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': {
@@ -308,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...',
@@ -346,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
@@ -363,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',
@@ -443,9 +466,11 @@ const i18n = {
'ai_providers.openai_keys_count': 'Keys Count', 'ai_providers.openai_keys_count': 'Keys Count',
'ai_providers.openai_models_count': 'Models Count', 'ai_providers.openai_models_count': 'Models Count',
// Auth files management // Auth files management
'auth_files.title': 'Auth Files Management', 'auth_files.title': 'Auth Files Management',
'auth_files.title_section': 'Auth Files', '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.upload_button': 'Upload File',
'auth_files.delete_all_button': 'Delete All', 'auth_files.delete_all_button': 'Delete All',
'auth_files.empty_title': 'No Auth Files', 'auth_files.empty_title': 'No Auth Files',
@@ -463,6 +488,38 @@ const i18n = {
'auth_files.delete_all_success': 'Successfully deleted', 'auth_files.delete_all_success': 'Successfully deleted',
'auth_files.files_count': 'files', '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_label_label': 'Label (Optional):',
'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)',
'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',
@@ -481,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',
@@ -525,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> </div>
</button> <p class="form-hint" data-i18n="login.connection_auto_hint">系统将自动使用当前访问地址进行连接</p>
<button class="tab-button" data-tab="remote"> </div>
<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>
</button>
</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>
<p class="form-hint" data-i18n="login.custom_connection_hint">默认使用当前访问地址,若需要可手动输入其他地址。</p>
</div> </div>
</form>
</div>
<!-- 远程连接表单 -->
<div id="remote-form" class="connection-form">
<form class="login-form">
<div class="form-group"> <div class="form-group">
<label for="remote-api-url" data-i18n="login.api_url_label">API 基础地址:</label> <label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</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 class="form-group">
<label for="remote-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 密钥管理 -->
@@ -400,6 +361,42 @@
<section id="auth-files" class="content-section"> <section id="auth-files" class="content-section">
<h2 data-i18n="auth_files.title">认证文件管理</h2> <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 -->
<div class="card">
<div class="card-header">
<h3><i class="fab fa-google"></i> <span data-i18n="auth_login.gemini_web_title">Gemini Web Token</span></h3>
<button id="gemini-web-token-btn" class="btn btn-primary">
<i class="fas fa-save"></i> <span data-i18n="auth_login.gemini_web_button">保存 Gemini Web Token</span>
</button>
</div>
<div class="card-content">
<p class="form-hint" style="margin-bottom: 20px;" data-i18n="auth_login.gemini_web_hint">
从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
</p>
<div class="form-group">
<label for="secure-1psid-input" data-i18n="auth_login.secure_1psid_label">__Secure-1PSID Cookie:</label>
<input type="text" id="secure-1psid-input" data-i18n="auth_login.secure_1psid_placeholder" placeholder="输入 __Secure-1PSID cookie 值">
</div>
<div class="form-group">
<label for="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label>
<input type="text" id="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_placeholder" placeholder="输入 __Secure-1PSIDTS cookie 值">
</div>
<div class="form-group">
<label for="gemini-web-label-input" data-i18n="auth_login.gemini_web_label_label">Label (Optional):</label>
<input type="text" id="gemini-web-label-input" data-i18n="auth_login.gemini_web_label_placeholder" placeholder="输入标签名称 (可选)">
</div>
</div>
</div>
<!-- 认证文件 -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-file-alt"></i> <span data-i18n="auth_files.title_section">认证文件</span></h3> <h3><i class="fas fa-file-alt"></i> <span data-i18n="auth_files.title_section">认证文件</span></h3>
@@ -419,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>
@@ -447,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>
<!-- 模态框 --> <!-- 模态框 -->

1064
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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,154 +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;
}
.login-title { .login-title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -468,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 {
@@ -637,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;
@@ -1572,3 +1450,297 @@ input:checked + .slider:before {
opacity: 1; 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);
}
.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
}
};