mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f5300bb4 | ||
|
|
52169200f1 | ||
|
|
80b2597611 | ||
|
|
04f21eea98 | ||
|
|
f6a4bae8c6 | ||
|
|
c9f09ccf37 | ||
|
|
5b8fd04ba3 | ||
|
|
3c791a2313 | ||
|
|
2ef64d8064 | ||
|
|
f2dc4bcf98 | ||
|
|
5f597afb42 | ||
|
|
e5bef7e2b0 | ||
|
|
d8df9ce680 | ||
|
|
44594220b2 | ||
|
|
86fc9fe86e | ||
|
|
67f009b81c | ||
|
|
6618312360 | ||
|
|
9e49824c52 | ||
|
|
015754237b |
61
.github/workflows/release.yml
vendored
Normal file
61
.github/workflows/release.yml
vendored
Normal 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
22
.gitignore
vendored
Normal 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
69
BUILD_RELEASE.md
Normal 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
215
README.md
@@ -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
141
README_CN.md
Normal 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许可
|
||||||
745
app.js
745
app.js
@@ -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,57 +239,42 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
// 处理登录表单提交
|
// 处理登录表单提交
|
||||||
async handleLogin() {
|
async handleLogin() {
|
||||||
// 获取当前活动的选项卡
|
const apiBaseInput = document.getElementById('login-api-base');
|
||||||
const activeTab = document.querySelector('.tab-button.active').getAttribute('data-tab');
|
const managementKeyInput = document.getElementById('login-management-key');
|
||||||
|
const managementKey = managementKeyInput ? managementKeyInput.value.trim() : '';
|
||||||
|
|
||||||
let apiUrl, managementKey;
|
if (!managementKey) {
|
||||||
|
|
||||||
if (activeTab === 'local') {
|
|
||||||
// 本地连接:从端口号构建URL
|
|
||||||
const port = document.getElementById('local-port').value.trim();
|
|
||||||
managementKey = document.getElementById('local-management-key').value.trim();
|
|
||||||
|
|
||||||
if (!port || !managementKey) {
|
|
||||||
this.showLoginError(i18n.t('login.error_required'));
|
this.showLoginError(i18n.t('login.error_required'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
apiUrl = `http://localhost:${port}`;
|
if (apiBaseInput && apiBaseInput.value.trim()) {
|
||||||
} else {
|
this.setApiBase(apiBaseInput.value.trim());
|
||||||
// 远程连接:使用完整URL
|
|
||||||
apiUrl = document.getElementById('remote-api-url').value.trim();
|
|
||||||
managementKey = document.getElementById('remote-management-key').value.trim();
|
|
||||||
|
|
||||||
if (!apiUrl || !managementKey) {
|
|
||||||
this.showLoginError(i18n.t('login.error_required'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const proxyUrl = document.getElementById('login-proxy-url').value.trim();
|
|
||||||
|
|
||||||
const submitBtn = document.getElementById('login-submit');
|
const submitBtn = document.getElementById('login-submit');
|
||||||
const originalText = submitBtn.innerHTML;
|
const originalText = submitBtn ? submitBtn.innerHTML : '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (submitBtn) {
|
||||||
submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`;
|
submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`;
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
}
|
||||||
this.hideLoginError();
|
this.hideLoginError();
|
||||||
|
|
||||||
// 如果设置了代理,先保存代理设置
|
this.managementKey = managementKey;
|
||||||
if (proxyUrl) {
|
localStorage.setItem('managementKey', this.managementKey);
|
||||||
localStorage.setItem('proxyUrl', proxyUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.login(apiUrl, managementKey);
|
|
||||||
|
|
||||||
|
await this.login(this.apiBase, this.managementKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
|
this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (submitBtn) {
|
||||||
submitBtn.innerHTML = originalText;
|
submitBtn.innerHTML = originalText;
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 切换登录页面密钥可见性
|
// 切换登录页面密钥可见性
|
||||||
toggleLoginKeyVisibility(button) {
|
toggleLoginKeyVisibility(button) {
|
||||||
@@ -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) {
|
||||||
if (proxyInput && savedProxy) {
|
loginKeyInput.value = savedKey;
|
||||||
proxyInput.value = savedProxy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置实时保存监听器
|
|
||||||
this.setupLoginAutoSave();
|
this.setupLoginAutoSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置登录页面自动保存
|
|
||||||
setupLoginAutoSave() {
|
setupLoginAutoSave() {
|
||||||
const localPortInput = document.getElementById('local-port');
|
const loginKeyInput = document.getElementById('login-management-key');
|
||||||
const remoteApiInput = document.getElementById('remote-api-url');
|
const apiBaseInput = document.getElementById('login-api-base');
|
||||||
const localKeyInput = document.getElementById('local-management-key');
|
const resetButton = document.getElementById('login-reset-api-base');
|
||||||
const remoteKeyInput = document.getElementById('remote-management-key');
|
|
||||||
const proxyInput = document.getElementById('login-proxy-url');
|
|
||||||
|
|
||||||
const saveLocalBase = (port) => {
|
|
||||||
if (port.trim()) {
|
|
||||||
const apiUrl = `http://localhost:${port}`;
|
|
||||||
this.setApiBase(apiUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const saveLocalBaseDebounced = this.debounce(saveLocalBase, 500);
|
|
||||||
|
|
||||||
const saveRemoteBase = (val) => {
|
|
||||||
if (val.trim()) {
|
|
||||||
this.setApiBase(val);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const saveRemoteBaseDebounced = this.debounce(saveRemoteBase, 500);
|
|
||||||
|
|
||||||
const saveKey = (val) => {
|
const saveKey = (val) => {
|
||||||
if (val.trim()) {
|
if (val.trim()) {
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiBaseInput) {
|
||||||
|
const persistBase = (val) => {
|
||||||
|
const normalized = this.normalizeBase(val);
|
||||||
|
if (normalized) {
|
||||||
|
this.setApiBase(normalized);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const saveProxyDebounced = this.debounce(saveProxy, 500);
|
const persistBaseDebounced = this.debounce(persistBase, 500);
|
||||||
|
|
||||||
// 绑定本地端口输入框
|
apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
|
||||||
if (localPortInput) {
|
apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
|
||||||
localPortInput.addEventListener('change', (e) => saveLocalBase(e.target.value));
|
|
||||||
localPortInput.addEventListener('input', (e) => saveLocalBaseDebounced(e.target.value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定远程API输入框
|
if (resetButton) {
|
||||||
if (remoteApiInput) {
|
resetButton.addEventListener('click', () => {
|
||||||
remoteApiInput.addEventListener('change', (e) => saveRemoteBase(e.target.value));
|
const detected = this.detectApiBaseFromLocation();
|
||||||
remoteApiInput.addEventListener('input', (e) => saveRemoteBaseDebounced(e.target.value));
|
this.setApiBase(detected);
|
||||||
|
if (apiBaseInput) {
|
||||||
|
apiBaseInput.value = detected;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// 绑定本地密钥输入框
|
|
||||||
if (localKeyInput) {
|
|
||||||
localKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
|
|
||||||
localKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定远程密钥输入框
|
|
||||||
if (remoteKeyInput) {
|
|
||||||
remoteKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
|
|
||||||
remoteKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定代理输入框
|
|
||||||
if (proxyInput) {
|
|
||||||
proxyInput.addEventListener('change', (e) => saveProxy(e.target.value));
|
|
||||||
proxyInput.addEventListener('input', (e) => saveProxyDebounced(e.target.value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载设置(简化版,仅加载内部状态)
|
// 加载设置(简化版,仅加载内部状态)
|
||||||
@@ -712,22 +653,20 @@ class CLIProxyManager {
|
|||||||
const savedUrl = localStorage.getItem('apiUrl');
|
const savedUrl = localStorage.getItem('apiUrl');
|
||||||
const savedKey = localStorage.getItem('managementKey');
|
const savedKey = localStorage.getItem('managementKey');
|
||||||
|
|
||||||
// 只设置内部状态,不操作DOM元素
|
|
||||||
if (savedBase) {
|
if (savedBase) {
|
||||||
this.setApiBase(savedBase);
|
this.setApiBase(savedBase);
|
||||||
} else if (savedUrl) {
|
} else if (savedUrl) {
|
||||||
const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, '');
|
const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, '');
|
||||||
this.setApiBase(base);
|
this.setApiBase(base);
|
||||||
} else {
|
} else {
|
||||||
this.setApiBase(this.apiBase);
|
this.setApiBase(this.detectApiBaseFromLocation());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedKey) {
|
if (savedKey) {
|
||||||
this.managementKey = savedKey;
|
this.managementKey = savedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:不再处理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',
|
||||||
|
|||||||
30
build-scripts/prepare-html.js
Normal file
30
build-scripts/prepare-html.js
Normal 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
132
build.js
Normal 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
25
bundle-entry.js
Normal 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
130
i18n.js
@@ -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'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
238
index.html
238
index.html
@@ -6,6 +6,7 @@
|
|||||||
<title data-i18n="title.login">CLI Proxy API Management Center</title>
|
<title data-i18n="title.login">CLI Proxy API Management Center</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="i18n.js"></script>
|
<script src="i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -48,67 +49,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选项卡导航 -->
|
<div class="login-body">
|
||||||
<div class="connection-tabs">
|
<div class="login-connection-info">
|
||||||
<button class="tab-button active" data-tab="local">
|
<div class="connection-summary">
|
||||||
<i class="fas fa-home"></i>
|
<i class="fas fa-link"></i>
|
||||||
<div class="tab-content">
|
<div>
|
||||||
<span class="tab-title" data-i18n="login.tab_local_title">Local</span>
|
<h3 data-i18n="login.connection_title">连接地址</h3>
|
||||||
<span class="tab-subtitle" data-i18n="login.tab_local_subtitle">Run CLI Web server on your local machine</span>
|
<p class="connection-url">
|
||||||
|
<span data-i18n="login.connection_current">当前地址</span>
|
||||||
|
<span class="connection-url-separator">:</span>
|
||||||
|
<span id="login-connection-url">-</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
<button class="tab-button" data-tab="remote">
|
|
||||||
<i class="fas fa-cloud"></i>
|
|
||||||
<div class="tab-content">
|
|
||||||
<span class="tab-title" data-i18n="login.tab_remote_title">Remote</span>
|
|
||||||
<span class="tab-subtitle" data-i18n="login.tab_remote_subtitle">Remote connection for a remote CLI Web server</span>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<p class="form-hint" data-i18n="login.connection_auto_hint">系统将自动使用当前访问地址进行连接</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 代理服务器设置(可选) -->
|
|
||||||
<div class="proxy-settings">
|
|
||||||
<label data-i18n="login.proxy_label">Proxy Server (Optional):</label>
|
|
||||||
<input type="text" id="login-proxy-url" data-i18n="login.proxy_placeholder" placeholder="http://ip:port or https://ip:port or socks5://user:pass@ip:port">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 本地连接表单 -->
|
|
||||||
<div id="local-form" class="connection-form active">
|
|
||||||
<form class="login-form">
|
<form class="login-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="local-port" data-i18n="login.local_port_label">端口号:</label>
|
<label for="login-api-base" data-i18n="login.custom_connection_label">自定义连接地址:</label>
|
||||||
<div class="local-url-group">
|
|
||||||
<span class="url-prefix">http://localhost:</span>
|
|
||||||
<input type="number" id="local-port" value="8317" min="1" max="65535" data-i18n="login.local_port_placeholder" placeholder="8317" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-hint" data-i18n="login.local_url_hint">将连接到 http://localhost:端口/v0/management</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="local-management-key" data-i18n="login.management_key_label">管理密钥:</label>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" id="local-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
|
<input type="text" id="login-api-base" data-i18n="login.custom_connection_placeholder" placeholder="例如: https://example.com:8317">
|
||||||
<button type="button" class="btn btn-secondary toggle-key-visibility">
|
<button type="button" id="login-reset-api-base" class="btn btn-secondary connection-reset-btn">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-location-arrow"></i>
|
||||||
|
<span data-i18n="login.use_current_address">使用当前地址</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="form-hint" data-i18n="login.custom_connection_hint">默认使用当前访问地址,若需要可手动输入其他地址。</p>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 远程连接表单 -->
|
|
||||||
<div id="remote-form" class="connection-form">
|
|
||||||
<form class="login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="remote-api-url" data-i18n="login.api_url_label">API 基础地址:</label>
|
|
||||||
<input type="text" id="remote-api-url" data-i18n="login.remote_api_url_placeholder" placeholder="例如: https://example.com:8317" required>
|
|
||||||
<div class="form-hint" data-i18n="login.api_url_hint">将自动补全 /v0/management</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="remote-management-key" data-i18n="login.management_key_label">管理密钥:</label>
|
<label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" id="remote-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
|
<input type="password" id="login-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
|
||||||
<button type="button" class="btn btn-secondary toggle-key-visibility">
|
<button type="button" class="btn btn-secondary toggle-key-visibility">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -221,6 +194,9 @@
|
|||||||
<li><a href="#auth-files" class="nav-item" data-section="auth-files">
|
<li><a href="#auth-files" class="nav-item" data-section="auth-files">
|
||||||
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
|
<li><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||||
|
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||||
|
</a></li>
|
||||||
<li><a href="#system-info" class="nav-item" data-section="system-info">
|
<li><a href="#system-info" class="nav-item" data-section="system-info">
|
||||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
@@ -305,21 +281,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 本地访问设置 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3><i class="fas fa-home"></i> <span data-i18n="basic_settings.localhost_title">本地访问</span></h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="toggle-group">
|
|
||||||
<label class="toggle-switch">
|
|
||||||
<input type="checkbox" id="allow-localhost-toggle">
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
<span class="toggle-label" data-i18n="basic_settings.localhost_allow">允许本地未认证访问</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- API 密钥管理 -->
|
<!-- API 密钥管理 -->
|
||||||
@@ -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
1064
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||||
|
|||||||
540
styles.css
540
styles.css
@@ -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
64
webpack.config.js
Normal 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
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user