Compare commits

...

9 Commits

Author SHA1 Message Date
Supra4E8C
27948b3d5c 实现请求和Token使用趋势图表,更新API详细统计表格,优化侧边栏样式,增强移动端体验,修复若干UI问题。 2025-10-03 18:05:33 +08:00
Supra4E8C
dff28db227 增补README 2025-10-03 15:48:21 +08:00
Supra4E8C
34b16ca886 Merge branch 'main' of https://github.com/router-for-me/Cli-Proxy-API-Management-Center 2025-10-03 15:42:22 +08:00
Supra4E8C
fa86f76289 增补README 2025-10-03 15:42:10 +08:00
Supra4E8C
41ca99978f Update README_CN.md 2025-10-03 15:35:39 +08:00
Supra4E8C
6ef674487f 更新README以适配新版本 2025-10-03 15:34:14 +08:00
Supra4E8C
2be7ced21a 实现移动端侧边栏功能,添加移动菜单按钮及遮罩,优化导航项点击事件,更新样式以提升用户体验。 2025-10-03 15:09:41 +08:00
Supra4E8C
b61155d215 v0.0.6
添加代理 URL 支持,更新 API 配置模态框,增强 XSS 防护,优化界面样式,修复若干 UI 问题,版本更新至 0.0.6
2025-10-02 17:34:26 +08:00
Supra4E8C
5488d6153d Update README.md 2025-10-01 16:36:06 +08:00
6 changed files with 2098 additions and 1158 deletions

View File

@@ -11,7 +11,8 @@ https://remote.router-for.me/
Minimum required version: ≥ 6.0.0 Minimum required version: ≥ 6.0.0
Recommended version: ≥ 6.0.19 Recommended version: ≥ 6.0.19
Starting from version 6.0.19, the WebUI has been integrated into the main program and is accessible via `/management.html`.
Since version 6.0.19, the WebUI has been rolled into the main program. You can access it by going to `/management.html` on the external port after firing up the main project.
## Features ## Features
@@ -19,13 +20,16 @@ Starting from version 6.0.19, the WebUI has been integrated into the main progra
- Supports management key authentication - Supports management key authentication
- Configurable API base address - Configurable API base address
- Real-time connection status detection - Real-time connection status detection
- Auto-login with saved credentials
- Language and theme switching
### Basic Settings ### Basic Settings
- **Debug Mode**: Enable/disable debugging - **Debug Mode**: Enable/disable debugging
- **Proxy Settings**: Configure proxy server URL - **Proxy Settings**: Configure proxy server URL
- **Request Retries**: Set the number of request retries - **Request Retries**: Set the number of request retries
- **Quota Management**: Configure behavior when the quota is exceeded - **Quota Management**: Configure behavior when the quota is exceeded
- **Local Access**: Manage local unauthenticated access - Auto-switch project when quota exceeded
- Switch to preview models when quota exceeded
### API Key Management ### API Key Management
- **Proxy Service Authentication Key**: Manage API keys for the proxy service - **Proxy Service Authentication Key**: Manage API keys for the proxy service
@@ -39,14 +43,33 @@ Starting from version 6.0.19, the WebUI has been integrated into the main progra
- Download existing authentication files - Download existing authentication files
- Delete single or all authentication files - Delete single or all authentication files
- Display file details - Display file details
- **Gemini Web Token**: Direct authentication using browser cookies
### Usage Statistics
- **Real-time Analytics**: Track API usage with interactive charts
- **Request Trends**: Visualize request patterns by hour/day
- **Token Usage**: Monitor token consumption over time
- **API Details**: Detailed statistics for each API endpoint
- **Success/Failure Rates**: Track API reliability metrics
### System Information
- **Connection Status**: Real-time connection monitoring
- **Configuration Status**: Track configuration loading state
- **Server Information**: Display server address and management key
- **Last Update**: Show when data was last refreshed
## How to Use ## How to Use
### 1. Direct Use (Recommended) ### 1. Using After CLI Proxy API Program Launch (Recommended)
Once the CLI Proxy API program is up and running, you can access the WebUI at `http://your-server-IP:8317/management.html`.
### 2. Direct Use
Simply open the `index.html` file directly in your browser to use it. Simply open the `index.html` file directly in your browser to use it.
### 2. Use a Local Server ### 3. Use a Local Server
#### Option A: Using Node.js (npm)
```bash ```bash
# Install dependencies # Install dependencies
npm install npm install
@@ -55,10 +78,19 @@ npm install
npm start npm start
``` ```
### 3. Configure API Connection #### Option B: Using Python
```bash
# Python 3.x
python -m http.server 8000
```
Then open `http://localhost:8000` in your browser.
### 3. Configure Connection
1. Open the management interface. 1. Open the management interface.
2. On the login screen, enter: 2. On the login screen, enter:
- **Remote Address**: `http://localhost:8317` (`/v0/management` will be auto-completed for you) - **Remote Address**: The current version automatically picks up the remote address from where you're connecting. But you can also set your own address if you prefer.
- **Management Key**: Your management key - **Management Key**: Your management key
3. Click the "Connect" button. 3. Click the "Connect" button.
4. Once connected successfully, all features will be available. 4. Once connected successfully, all features will be available.
@@ -70,8 +102,16 @@ npm start
- **API Keys**: Management of keys for various API services. - **API Keys**: Management of keys for various API services.
- **AI Providers**: Configuration for AI service providers. - **AI Providers**: Configuration for AI service providers.
- **Auth Files**: Upload and download management for authentication files. - **Auth Files**: Upload and download management for authentication files.
- **Usage Stats**: Real-time analytics and usage statistics with interactive charts.
- **System Info**: Connection status and system information. - **System Info**: Connection status and system information.
### Login Interface
- **Auto-connection**: Automatically attempts to connect using saved credentials
- **Custom Connection**: Manual configuration of API base address
- **Current Address Detection**: Automatically detects and uses current access address
- **Language Switching**: Support for multiple languages (English/Chinese)
- **Theme Switching**: Light and dark theme support
## Feature Highlights ## Feature Highlights
### Modern UI ### Modern UI
@@ -79,27 +119,45 @@ npm start
- Beautiful gradient colors and shadow effects - Beautiful gradient colors and shadow effects
- Smooth animations and transition effects - Smooth animations and transition effects
- Intuitive icons and status indicators - Intuitive icons and status indicators
- Dark/Light theme support with system preference detection
- Mobile-friendly sidebar with overlay
### Real-time Updates ### Real-time Updates
- Configuration changes take effect immediately - Configuration changes take effect immediately
- Real-time status feedback - Real-time status feedback
- Automatic data refresh - Automatic data refresh
- Live usage statistics with interactive charts
- Real-time connection status monitoring
### Security Features ### Security Features
- Masked display for keys - Masked display for keys
- Secure credential storage
- Auto-login with encrypted local storage
### Responsive Design ### Responsive Design
- Perfectly adapts to desktop and mobile devices - Perfectly adapts to desktop and mobile devices
- Adaptive layout - Adaptive layout with collapsible sidebar
- Touch-friendly interactions - Touch-friendly interactions
- Mobile menu with overlay
### Analytics & Monitoring
- Interactive charts powered by Chart.js
- Real-time usage statistics
- Request trend visualization
- Token consumption tracking
- API performance metrics
## Tech Stack ## Tech Stack
- **Frontend**: Plain HTML, CSS, JavaScript - **Frontend**: Plain HTML, CSS, JavaScript (ES6+)
- **Styling**: CSS3 + Flexbox/Grid - **Styling**: CSS3 + Flexbox/Grid with CSS Variables
- **Icons**: Font Awesome 6.4.0 - **Icons**: Font Awesome 6.4.0
- **Charts**: Chart.js for interactive data visualization
- **Fonts**: Segoe UI system font - **Fonts**: Segoe UI system font
- **API**: RESTful API calls - **API**: RESTful API calls with automatic authentication
- **Internationalization**: Custom i18n system with English/Chinese support
- **Theme System**: CSS custom properties for dynamic theming
- **Storage**: LocalStorage for user preferences and credentials
## Troubleshooting ## Troubleshooting
@@ -119,12 +177,20 @@ npm start
### File Structure ### File Structure
``` ```
webui/ webui/
├── index.html # Main page ├── index.html # Main page with responsive layout
├── styles.css # Stylesheet ├── styles.css # Stylesheet with theme support
├── app.js # Application logic ├── app.js # Application logic and API management
├── package.json # Project configuration ├── i18n.js # Internationalization support (EN/CN)
├── i18n.js # Internationalization support ├── package.json # Project configuration
── README.md # README document ── build.js # Build script for production
├── bundle-entry.js # Entry point for bundling
├── build-scripts/ # Build utilities
│ └── prepare-html.js # HTML preparation script
├── logo.jpg # Application logo
├── LICENSE # MIT License
├── README.md # English documentation
├── README_CN.md # Chinese documentation
└── BUILD_RELEASE.md # Build and release notes
``` ```
### API Calls ### API Calls

View File

@@ -1,14 +1,15 @@
# Cli-Proxy-API-Management-Center # Cli-Proxy-API-Management-Center
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。 这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
主项目 主项目
https://github.com/router-for-me/CLIProxyAPI https://github.com/router-for-me/CLIProxyAPI
示例网站: 示例网站:
https://remote.router-for.me/ https://remote.router-for.me/
最低可用版本 ≥ 5.0.0 最低可用版本 ≥ 6.0.0
推荐版本 ≥ 5.2.6 推荐版本 ≥ 6.0.19
自6.0.19起WebUI已经集成在主程序中 可以通过/management.html访问 自6.0.19起WebUI已经集成在主程序中 可以通过主项目开启的外部端口的`/management.html`访问
## 功能特点 ## 功能特点
@@ -16,13 +17,16 @@ https://remote.router-for.me/
- 支持管理密钥认证 - 支持管理密钥认证
- 可配置 API 基础地址 - 可配置 API 基础地址
- 实时连接状态检测 - 实时连接状态检测
- 自动登录保存的凭据
- 语言和主题切换
### 基础设置 ### 基础设置
- **调试模式**: 开启/关闭调试功能 - **调试模式**: 开启/关闭调试功能
- **代理设置**: 配置代理服务器 URL - **代理设置**: 配置代理服务器 URL
- **请求重试**: 设置请求重试次数 - **请求重试**: 设置请求重试次数
- **配额管理**: 配置超出配额时的行为 - **配额管理**: 配置超出配额时的行为
- **本地访问**: 管理本地未认证访问 - 超出配额时自动切换项目
- 超出配额时切换到预览模型
### API 密钥管理 ### API 密钥管理
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥 - **代理服务认证密钥**: 管理用于代理服务的 API 密钥
@@ -36,14 +40,33 @@ https://remote.router-for.me/
- 下载现有认证文件 - 下载现有认证文件
- 删除单个或所有认证文件 - 删除单个或所有认证文件
- 显示文件详细信息 - 显示文件详细信息
- **Gemini Web Token**: 使用浏览器 Cookie 直接认证
### 使用统计
- **实时分析**: 通过交互式图表跟踪 API 使用情况
- **请求趋势**: 按小时/天可视化请求模式
- **Token 使用**: 监控 Token 消耗随时间变化
- **API 详情**: 每个 API 端点的详细统计
- **成功率/失败率**: 跟踪 API 可靠性指标
### 系统信息
- **连接状态**: 实时连接监控
- **配置状态**: 跟踪配置加载状态
- **服务器信息**: 显示服务器地址和管理密钥
- **最后更新**: 显示数据最后刷新时间
## 使用方法 ## 使用方法
### 1. 直接使用(推荐) ### 1. 在CLI Proxy API程序启动后使用 (推荐)
在启动了CLI Proxy API程序后 访问`http://您的服务器IP:8317/management.html`使用
### 2. 直接使用
直接用浏览器打开 `index.html` 文件即可使用。 直接用浏览器打开 `index.html` 文件即可使用。
### 2. 使用本地服务器 ### 3. 使用本地服务器
#### 方法A使用 Node.js (npm)
```bash ```bash
# 安装依赖 # 安装依赖
npm install npm install
@@ -52,10 +75,19 @@ npm install
npm start npm start
``` ```
### 3. 配置 API 连接 #### 方法B使用 Python
```bash
# Python 3.x
python -m http.server 8000
```
然后在浏览器中打开 `http://localhost:8000`
### 3. 配置连接
1. 打开管理界面 1. 打开管理界面
2. 在登录界面上输入: 2. 在登录界面上输入:
- **远程地址**: `http://localhost:8317`/v0/management将会自动为您补全 - **远程地址**: 现版本远程地址将会自动从您的访问地址中获取 当然您也可以自定义
- **管理密钥**: 您的管理密钥 - **管理密钥**: 您的管理密钥
3. 点击"连接"按钮 3. 点击"连接"按钮
4. 连接成功后即可使用所有功能 4. 连接成功后即可使用所有功能
@@ -67,8 +99,16 @@ npm start
- **API 密钥**: 各种 API 服务的密钥管理 - **API 密钥**: 各种 API 服务的密钥管理
- **AI 提供商**: AI 服务提供商配置 - **AI 提供商**: AI 服务提供商配置
- **认证文件**: 认证文件的上传下载管理 - **认证文件**: 认证文件的上传下载管理
- **使用统计**: 实时分析和使用统计,包含交互式图表
- **系统信息**: 连接状态和系统信息 - **系统信息**: 连接状态和系统信息
### 登录界面
- **自动连接**: 使用保存的凭据自动尝试连接
- **自定义连接**: 手动配置 API 基础地址
- **当前地址检测**: 自动检测并使用当前访问地址
- **语言切换**: 支持多种语言(英文/中文)
- **主题切换**: 支持明暗主题
## 特性亮点 ## 特性亮点
### 现代化 UI ### 现代化 UI
@@ -76,27 +116,45 @@ npm start
- 美观的渐变色彩和阴影效果 - 美观的渐变色彩和阴影效果
- 流畅的动画和过渡效果 - 流畅的动画和过渡效果
- 直观的图标和状态指示 - 直观的图标和状态指示
- 明暗主题支持,自动检测系统偏好
- 移动端友好的侧边栏和遮罩
### 实时更新 ### 实时更新
- 配置更改立即生效 - 配置更改立即生效
- 实时状态反馈 - 实时状态反馈
- 自动数据刷新 - 自动数据刷新
- 实时使用统计和交互式图表
- 实时连接状态监控
### 安全特性 ### 安全特性
- 密钥遮蔽显示 - 密钥遮蔽显示
- 安全凭据存储
- 加密本地存储自动登录
### 响应式设计 ### 响应式设计
- 完美适配桌面和移动设备 - 完美适配桌面和移动设备
- 自适应布局 - 自适应布局,可折叠侧边栏
- 触摸友好的交互 - 触摸友好的交互
- 移动端菜单和遮罩
### 分析与监控
- Chart.js 驱动的交互式图表
- 实时使用统计
- 请求趋势可视化
- Token 消耗跟踪
- API 性能指标
## 技术栈 ## 技术栈
- **前端**: 纯 HTML、CSS、JavaScript - **前端**: 纯 HTML、CSS、JavaScript (ES6+)
- **样式**: CSS3 + Flexbox/Grid - **样式**: CSS3 + Flexbox/Grid,支持 CSS 变量
- **图标**: Font Awesome 6.4.0 - **图标**: Font Awesome 6.4.0
- **图表**: Chart.js 交互式数据可视化
- **字体**: Segoe UI 系统字体 - **字体**: Segoe UI 系统字体
- **API**: RESTful API 调用 - **API**: RESTful API 调用,自动认证
- **国际化**: 自定义 i18n 系统,支持中英文
- **主题系统**: CSS 自定义属性动态主题
- **存储**: LocalStorage 用户偏好和凭据存储
## 故障排除 ## 故障排除
@@ -116,12 +174,20 @@ npm start
### 文件结构 ### 文件结构
``` ```
webui/ webui/
├── index.html # 主页面 ├── index.html # 主页面,响应式布局
├── styles.css # 样式文件 ├── styles.css # 样式文件,支持主题
├── app.js # 应用逻辑 ├── app.js # 应用逻辑和 API 管理
├── package.json # 项目配置 ├── i18n.js # 国际化支持(中英文)
├── i18n.js # 国际化支持 ├── package.json # 项目配置
── README.md # 说明文档 ── build.js # 生产环境构建脚本
├── bundle-entry.js # 打包入口文件
├── build-scripts/ # 构建工具
│ └── prepare-html.js # HTML 准备脚本
├── logo.jpg # 应用图标
├── LICENSE # MIT 许可证
├── README.md # 英文文档
├── README_CN.md # 中文文档
└── BUILD_RELEASE.md # 构建和发布说明
``` ```
### API 调用 ### API 调用

420
app.js
View File

@@ -553,6 +553,136 @@ class CLIProxyManager {
this.closeModal(); this.closeModal();
} }
}); });
// 移动端菜单按钮
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const sidebar = document.getElementById('sidebar');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar());
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar());
}
// 侧边栏收起/展开按钮(桌面端)
const sidebarToggleBtnDesktop = document.getElementById('sidebar-toggle-btn-desktop');
if (sidebarToggleBtnDesktop) {
sidebarToggleBtnDesktop.addEventListener('click', () => this.toggleSidebar());
}
// 从本地存储恢复侧边栏状态
this.restoreSidebarState();
// 监听窗口大小变化
window.addEventListener('resize', () => {
const sidebar = document.getElementById('sidebar');
const layout = document.getElementById('layout-container');
if (window.innerWidth <= 1024) {
// 移动端:移除收起状态
if (sidebar && layout) {
sidebar.classList.remove('collapsed');
layout.classList.remove('sidebar-collapsed');
}
} else {
// 桌面端:恢复保存的状态
this.restoreSidebarState();
}
});
// 点击侧边栏导航项时在移动端关闭侧边栏
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 1024) {
this.closeMobileSidebar();
}
});
});
}
// 切换移动端侧边栏
toggleMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
const layout = document.getElementById('layout-container');
const mainWrapper = document.getElementById('main-wrapper');
if (sidebar && overlay) {
const isOpen = sidebar.classList.toggle('mobile-open');
overlay.classList.toggle('active');
if (layout) {
layout.classList.toggle('sidebar-open', isOpen);
}
if (mainWrapper) {
mainWrapper.classList.toggle('sidebar-open', isOpen);
}
}
}
// 关闭移动端侧边栏
closeMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
const layout = document.getElementById('layout-container');
const mainWrapper = document.getElementById('main-wrapper');
if (sidebar && overlay) {
sidebar.classList.remove('mobile-open');
overlay.classList.remove('active');
if (layout) {
layout.classList.remove('sidebar-open');
}
if (mainWrapper) {
mainWrapper.classList.remove('sidebar-open');
}
}
}
// 切换侧边栏收起/展开状态
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const layout = document.getElementById('layout-container');
if (sidebar && layout) {
const isCollapsed = sidebar.classList.toggle('collapsed');
layout.classList.toggle('sidebar-collapsed', isCollapsed);
// 保存状态到本地存储
localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
// 更新按钮提示文本
const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
if (toggleBtn) {
toggleBtn.setAttribute('title', isCollapsed ? '展开侧边栏' : '收起侧边栏');
}
}
}
// 恢复侧边栏状态
restoreSidebarState() {
// 只在桌面端恢复侧栏状态
if (window.innerWidth > 1024) {
const savedState = localStorage.getItem('sidebarCollapsed');
if (savedState === 'true') {
const sidebar = document.getElementById('sidebar');
const layout = document.getElementById('layout-container');
if (sidebar && layout) {
sidebar.classList.add('collapsed');
layout.classList.add('sidebar-collapsed');
// 更新按钮提示文本
const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
if (toggleBtn) {
toggleBtn.setAttribute('title', '展开侧边栏');
}
}
}
}
} }
// 设置导航 // 设置导航
@@ -1150,6 +1280,17 @@ class CLIProxyManager {
return key.substring(0, 4) + '...' + key.substring(key.length - 4); return key.substring(0, 4) + '...' + key.substring(key.length - 4);
} }
// HTML 转义,防止 XSS
escapeHtml(value) {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// 显示添加API密钥模态框 // 显示添加API密钥模态框
showAddApiKeyModal() { showAddApiKeyModal() {
const modal = document.getElementById('modal'); const modal = document.getElementById('modal');
@@ -1439,7 +1580,8 @@ class CLIProxyManager {
<div class="item-content"> <div class="item-content">
<div class="item-title">${i18n.t('ai_providers.codex_item_title')} #${index + 1}</div> <div class="item-title">${i18n.t('ai_providers.codex_item_title')} #${index + 1}</div>
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div> <div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${config['base-url']}</div>` : ''} ${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
</div> </div>
<div class="item-actions"> <div class="item-actions">
<button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/"/g, '&quot;')})"> <button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/"/g, '&quot;')})">
@@ -1459,18 +1601,22 @@ class CLIProxyManager {
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>添加Codex API配置</h3> <h3>${i18n.t('ai_providers.codex_add_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="new-codex-key">API密钥:</label> <label for="new-codex-key">${i18n.t('ai_providers.codex_add_modal_key_label')}</label>
<input type="text" id="new-codex-key" placeholder="请输入Codex API密钥"> <input type="text" id="new-codex-key" placeholder="${i18n.t('ai_providers.codex_add_modal_key_placeholder')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="new-codex-url">Base URL (可选):</label> <label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label>
<input type="text" id="new-codex-url" placeholder="例如: https://api.example.com"> <input type="text" id="new-codex-url" placeholder="${i18n.t('ai_providers.codex_add_modal_url_placeholder')}">
</div>
<div class="form-group">
<label for="new-codex-proxy">${i18n.t('ai_providers.codex_add_modal_proxy_label')}</label>
<input type="text" id="new-codex-proxy" placeholder="${i18n.t('ai_providers.codex_add_modal_proxy_placeholder')}">
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addCodexKey()">添加</button> <button class="btn btn-primary" onclick="manager.addCodexKey()">${i18n.t('common.add')}</button>
</div> </div>
`; `;
@@ -1481,9 +1627,10 @@ class CLIProxyManager {
async addCodexKey() { async addCodexKey() {
const apiKey = document.getElementById('new-codex-key').value.trim(); const apiKey = document.getElementById('new-codex-key').value.trim();
const baseUrl = document.getElementById('new-codex-url').value.trim(); const baseUrl = document.getElementById('new-codex-url').value.trim();
const proxyUrl = document.getElementById('new-codex-proxy').value.trim();
if (!apiKey) { if (!apiKey) {
this.showNotification('请输入API密钥', 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
@@ -1495,6 +1642,9 @@ class CLIProxyManager {
if (baseUrl) { if (baseUrl) {
newConfig['base-url'] = baseUrl; newConfig['base-url'] = baseUrl;
} }
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
currentKeys.push(newConfig); currentKeys.push(newConfig);
@@ -1518,18 +1668,22 @@ class CLIProxyManager {
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>编辑Codex API配置</h3> <h3>${i18n.t('ai_providers.codex_edit_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="edit-codex-key">API密钥:</label> <label for="edit-codex-key">${i18n.t('ai_providers.codex_edit_modal_key_label')}</label>
<input type="text" id="edit-codex-key" value="${config['api-key']}"> <input type="text" id="edit-codex-key" value="${config['api-key']}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-codex-url">Base URL (可选):</label> <label for="edit-codex-url">${i18n.t('ai_providers.codex_edit_modal_url_label')}</label>
<input type="text" id="edit-codex-url" value="${config['base-url'] || ''}"> <input type="text" id="edit-codex-url" value="${config['base-url'] || ''}">
</div> </div>
<div class="form-group">
<label for="edit-codex-proxy">${i18n.t('ai_providers.codex_edit_modal_proxy_label')}</label>
<input type="text" id="edit-codex-proxy" value="${config['proxy-url'] || ''}">
</div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">更新</button> <button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">${i18n.t('common.update')}</button>
</div> </div>
`; `;
@@ -1540,9 +1694,10 @@ class CLIProxyManager {
async updateCodexKey(index) { async updateCodexKey(index) {
const apiKey = document.getElementById('edit-codex-key').value.trim(); const apiKey = document.getElementById('edit-codex-key').value.trim();
const baseUrl = document.getElementById('edit-codex-url').value.trim(); const baseUrl = document.getElementById('edit-codex-url').value.trim();
const proxyUrl = document.getElementById('edit-codex-proxy').value.trim();
if (!apiKey) { if (!apiKey) {
this.showNotification('请输入API密钥', 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
@@ -1551,6 +1706,9 @@ class CLIProxyManager {
if (baseUrl) { if (baseUrl) {
newConfig['base-url'] = baseUrl; newConfig['base-url'] = baseUrl;
} }
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
await this.makeRequest('/codex-api-key', { await this.makeRequest('/codex-api-key', {
method: 'PATCH', method: 'PATCH',
@@ -1612,7 +1770,8 @@ class CLIProxyManager {
<div class="item-content"> <div class="item-content">
<div class="item-title">${i18n.t('ai_providers.claude_item_title')} #${index + 1}</div> <div class="item-title">${i18n.t('ai_providers.claude_item_title')} #${index + 1}</div>
<div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div> <div class="item-subtitle">${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}</div>
${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${config['base-url']}</div>` : ''} ${config['base-url'] ? `<div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}</div>` : ''}
${config['proxy-url'] ? `<div class="item-subtitle">${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}</div>` : ''}
</div> </div>
<div class="item-actions"> <div class="item-actions">
<button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/"/g, '&quot;')})"> <button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/"/g, '&quot;')})">
@@ -1632,18 +1791,22 @@ class CLIProxyManager {
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>添加Claude API配置</h3> <h3>${i18n.t('ai_providers.claude_add_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="new-claude-key">API密钥:</label> <label for="new-claude-key">${i18n.t('ai_providers.claude_add_modal_key_label')}</label>
<input type="text" id="new-claude-key" placeholder="请输入Claude API密钥"> <input type="text" id="new-claude-key" placeholder="${i18n.t('ai_providers.claude_add_modal_key_placeholder')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="new-claude-url">Base URL (可选):</label> <label for="new-claude-url">${i18n.t('ai_providers.claude_add_modal_url_label')}</label>
<input type="text" id="new-claude-url" placeholder="例如: https://api.anthropic.com"> <input type="text" id="new-claude-url" placeholder="${i18n.t('ai_providers.claude_add_modal_url_placeholder')}">
</div>
<div class="form-group">
<label for="new-claude-proxy">${i18n.t('ai_providers.claude_add_modal_proxy_label')}</label>
<input type="text" id="new-claude-proxy" placeholder="${i18n.t('ai_providers.claude_add_modal_proxy_placeholder')}">
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addClaudeKey()">添加</button> <button class="btn btn-primary" onclick="manager.addClaudeKey()">${i18n.t('common.add')}</button>
</div> </div>
`; `;
@@ -1654,9 +1817,10 @@ class CLIProxyManager {
async addClaudeKey() { async addClaudeKey() {
const apiKey = document.getElementById('new-claude-key').value.trim(); const apiKey = document.getElementById('new-claude-key').value.trim();
const baseUrl = document.getElementById('new-claude-url').value.trim(); const baseUrl = document.getElementById('new-claude-url').value.trim();
const proxyUrl = document.getElementById('new-claude-proxy').value.trim();
if (!apiKey) { if (!apiKey) {
this.showNotification('请输入API密钥', 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
@@ -1668,6 +1832,9 @@ class CLIProxyManager {
if (baseUrl) { if (baseUrl) {
newConfig['base-url'] = baseUrl; newConfig['base-url'] = baseUrl;
} }
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
currentKeys.push(newConfig); currentKeys.push(newConfig);
@@ -1691,18 +1858,22 @@ class CLIProxyManager {
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>编辑Claude API配置</h3> <h3>${i18n.t('ai_providers.claude_edit_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="edit-claude-key">API密钥:</label> <label for="edit-claude-key">${i18n.t('ai_providers.claude_edit_modal_key_label')}</label>
<input type="text" id="edit-claude-key" value="${config['api-key']}"> <input type="text" id="edit-claude-key" value="${config['api-key']}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-claude-url">Base URL (可选):</label> <label for="edit-claude-url">${i18n.t('ai_providers.claude_edit_modal_url_label')}</label>
<input type="text" id="edit-claude-url" value="${config['base-url'] || ''}"> <input type="text" id="edit-claude-url" value="${config['base-url'] || ''}">
</div> </div>
<div class="form-group">
<label for="edit-claude-proxy">${i18n.t('ai_providers.claude_edit_modal_proxy_label')}</label>
<input type="text" id="edit-claude-proxy" value="${config['proxy-url'] || ''}">
</div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">更新</button> <button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">${i18n.t('common.update')}</button>
</div> </div>
`; `;
@@ -1713,9 +1884,10 @@ class CLIProxyManager {
async updateClaudeKey(index) { async updateClaudeKey(index) {
const apiKey = document.getElementById('edit-claude-key').value.trim(); const apiKey = document.getElementById('edit-claude-key').value.trim();
const baseUrl = document.getElementById('edit-claude-url').value.trim(); const baseUrl = document.getElementById('edit-claude-url').value.trim();
const proxyUrl = document.getElementById('edit-claude-proxy').value.trim();
if (!apiKey) { if (!apiKey) {
this.showNotification('请输入API密钥', 'error'); this.showNotification(i18n.t('notification.field_required'), 'error');
return; return;
} }
@@ -1724,6 +1896,9 @@ class CLIProxyManager {
if (baseUrl) { if (baseUrl) {
newConfig['base-url'] = baseUrl; newConfig['base-url'] = baseUrl;
} }
if (proxyUrl) {
newConfig['proxy-url'] = proxyUrl;
}
await this.makeRequest('/claude-api-key', { await this.makeRequest('/claude-api-key', {
method: 'PATCH', method: 'PATCH',
@@ -1783,10 +1958,11 @@ class CLIProxyManager {
container.innerHTML = providers.map((provider, index) => ` container.innerHTML = providers.map((provider, index) => `
<div class="provider-item"> <div class="provider-item">
<div class="item-content"> <div class="item-content">
<div class="item-title">${provider.name}</div> <div class="item-title">${this.escapeHtml(provider.name)}</div>
<div class="item-subtitle">${i18n.t('common.base_url')}: ${provider['base-url']}</div> <div class="item-subtitle">${i18n.t('common.base_url')}: ${this.escapeHtml(provider['base-url'])}</div>
<div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-keys'] || []).length}</div> <div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}</div>
<div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}</div> <div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}</div>
${this.renderOpenAIModelBadges(provider.models || [])}
</div> </div>
<div class="item-actions"> <div class="item-actions">
<button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(provider).replace(/"/g, '&quot;')})"> <button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(provider).replace(/"/g, '&quot;')})">
@@ -1806,26 +1982,37 @@ class CLIProxyManager {
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>添加OpenAI兼容提供商</h3> <h3>${i18n.t('ai_providers.openai_add_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="new-provider-name">提供商名称:</label> <label for="new-provider-name">${i18n.t('ai_providers.openai_add_modal_name_label')}</label>
<input type="text" id="new-provider-name" placeholder="例如: openrouter"> <input type="text" id="new-provider-name" placeholder="${i18n.t('ai_providers.openai_add_modal_name_placeholder')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="new-provider-url">Base URL:</label> <label for="new-provider-url">${i18n.t('ai_providers.openai_add_modal_url_label')}</label>
<input type="text" id="new-provider-url" placeholder="例如: https://openrouter.ai/api/v1"> <input type="text" id="new-provider-url" placeholder="${i18n.t('ai_providers.openai_add_modal_url_placeholder')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="new-provider-keys">API密钥 (每行一个):</label> <label for="new-provider-keys">${i18n.t('ai_providers.openai_add_modal_keys_label')}</label>
<textarea id="new-provider-keys" rows="3" placeholder="sk-key1&#10;sk-key2"></textarea> <textarea id="new-provider-keys" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_placeholder')}"></textarea>
</div>
<div class="form-group">
<label for="new-provider-proxies">${i18n.t('ai_providers.openai_add_modal_keys_proxy_label')}</label>
<textarea id="new-provider-proxies" rows="3" placeholder="${i18n.t('ai_providers.openai_add_modal_keys_proxy_placeholder')}"></textarea>
</div>
<div class="form-group">
<label>${i18n.t('ai_providers.openai_add_modal_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
<div id="new-provider-models-wrapper" class="model-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('new-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addOpenAIProvider()">添加</button> <button class="btn btn-primary" onclick="manager.addOpenAIProvider()">${i18n.t('common.add')}</button>
</div> </div>
`; `;
modal.style.display = 'block'; modal.style.display = 'block';
this.populateModelFields('new-provider-models-wrapper', []);
} }
// 添加OpenAI提供商 // 添加OpenAI提供商
@@ -1833,9 +2020,10 @@ class CLIProxyManager {
const name = document.getElementById('new-provider-name').value.trim(); const name = document.getElementById('new-provider-name').value.trim();
const baseUrl = document.getElementById('new-provider-url').value.trim(); const baseUrl = document.getElementById('new-provider-url').value.trim();
const keysText = document.getElementById('new-provider-keys').value.trim(); const keysText = document.getElementById('new-provider-keys').value.trim();
const proxiesText = document.getElementById('new-provider-proxies').value.trim();
const models = this.collectModelInputs('new-provider-models-wrapper');
if (!name || !baseUrl) { if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
this.showNotification('请填写提供商名称和Base URL', 'error');
return; return;
} }
@@ -1844,12 +2032,17 @@ class CLIProxyManager {
const currentProviders = data['openai-compatibility'] || []; const currentProviders = data['openai-compatibility'] || [];
const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
const apiKeyEntries = apiKeys.map((key, idx) => ({
'api-key': key,
'proxy-url': proxies[idx] || ''
}));
const newProvider = { const newProvider = {
name, name,
'base-url': baseUrl, 'base-url': baseUrl,
'api-keys': apiKeys, 'api-key-entries': apiKeyEntries,
models: [] models
}; };
currentProviders.push(newProvider); currentProviders.push(newProvider);
@@ -1873,29 +2066,42 @@ class CLIProxyManager {
const modal = document.getElementById('modal'); const modal = document.getElementById('modal');
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
const apiKeysText = (provider['api-keys'] || []).join('\n'); const apiKeyEntries = provider['api-key-entries'] || [];
const apiKeysText = apiKeyEntries.map(entry => entry['api-key'] || '').join('\n');
const proxiesText = apiKeyEntries.map(entry => entry['proxy-url'] || '').join('\n');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>编辑OpenAI兼容提供商</h3> <h3>${i18n.t('ai_providers.openai_edit_modal_title')}</h3>
<div class="form-group"> <div class="form-group">
<label for="edit-provider-name">提供商名称:</label> <label for="edit-provider-name">${i18n.t('ai_providers.openai_edit_modal_name_label')}</label>
<input type="text" id="edit-provider-name" value="${provider.name}"> <input type="text" id="edit-provider-name" value="${provider.name}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-provider-url">Base URL:</label> <label for="edit-provider-url">${i18n.t('ai_providers.openai_edit_modal_url_label')}</label>
<input type="text" id="edit-provider-url" value="${provider['base-url']}"> <input type="text" id="edit-provider-url" value="${provider['base-url']}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-provider-keys">API密钥 (每行一个):</label> <label for="edit-provider-keys">${i18n.t('ai_providers.openai_edit_modal_keys_label')}</label>
<textarea id="edit-provider-keys" rows="3">${apiKeysText}</textarea> <textarea id="edit-provider-keys" rows="3">${apiKeysText}</textarea>
</div> </div>
<div class="form-group">
<label for="edit-provider-proxies">${i18n.t('ai_providers.openai_edit_modal_keys_proxy_label')}</label>
<textarea id="edit-provider-proxies" rows="3">${proxiesText}</textarea>
</div>
<div class="form-group">
<label>${i18n.t('ai_providers.openai_edit_modal_models_label')}</label>
<p class="form-hint">${i18n.t('ai_providers.openai_models_hint')}</p>
<div id="edit-provider-models-wrapper" class="model-input-list"></div>
<button type="button" class="btn btn-secondary" onclick="manager.addModelField('edit-provider-models-wrapper')">${i18n.t('ai_providers.openai_models_add_btn')}</button>
</div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">更新</button> <button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button>
</div> </div>
`; `;
modal.style.display = 'block'; modal.style.display = 'block';
this.populateModelFields('edit-provider-models-wrapper', provider.models || []);
} }
// 更新OpenAI提供商 // 更新OpenAI提供商
@@ -1903,20 +2109,26 @@ class CLIProxyManager {
const name = document.getElementById('edit-provider-name').value.trim(); const name = document.getElementById('edit-provider-name').value.trim();
const baseUrl = document.getElementById('edit-provider-url').value.trim(); const baseUrl = document.getElementById('edit-provider-url').value.trim();
const keysText = document.getElementById('edit-provider-keys').value.trim(); const keysText = document.getElementById('edit-provider-keys').value.trim();
const proxiesText = document.getElementById('edit-provider-proxies').value.trim();
const models = this.collectModelInputs('edit-provider-models-wrapper');
if (!name || !baseUrl) { if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
this.showNotification('请填写提供商名称和Base URL', 'error');
return; return;
} }
try { try {
const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
const apiKeyEntries = apiKeys.map((key, idx) => ({
'api-key': key,
'proxy-url': proxies[idx] || ''
}));
const updatedProvider = { const updatedProvider = {
name, name,
'base-url': baseUrl, 'base-url': baseUrl,
'api-keys': apiKeys, 'api-key-entries': apiKeyEntries,
models: [] models
}; };
await this.makeRequest('/openai-compatibility', { await this.makeRequest('/openai-compatibility', {
@@ -2573,6 +2785,100 @@ class CLIProxyManager {
customInput.value = this.apiBase || ''; customInput.value = this.apiBase || '';
} }
} }
addModelField(wrapperId, model = {}) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
const row = document.createElement('div');
row.className = 'model-input-row';
row.innerHTML = `
<div class="input-group">
<input type="text" class="model-name-input" placeholder="${i18n.t('ai_providers.openai_model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}">
<input type="text" class="model-alias-input" placeholder="${i18n.t('ai_providers.openai_model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}">
<button type="button" class="btn btn-small btn-danger model-remove-btn"><i class="fas fa-trash"></i></button>
</div>
`;
const removeBtn = row.querySelector('.model-remove-btn');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
wrapper.removeChild(row);
});
}
wrapper.appendChild(row);
}
populateModelFields(wrapperId, models = []) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
wrapper.innerHTML = '';
if (!models.length) {
this.addModelField(wrapperId);
return;
}
models.forEach(model => this.addModelField(wrapperId, model));
}
collectModelInputs(wrapperId) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return [];
const rows = Array.from(wrapper.querySelectorAll('.model-input-row'));
const models = [];
rows.forEach(row => {
const nameInput = row.querySelector('.model-name-input');
const aliasInput = row.querySelector('.model-alias-input');
const name = nameInput ? nameInput.value.trim() : '';
const alias = aliasInput ? aliasInput.value.trim() : '';
if (name) {
const model = { name };
if (alias) {
model.alias = alias;
}
models.push(model);
}
});
return models;
}
renderOpenAIModelBadges(models) {
if (!models || models.length === 0) {
return '';
}
return `
<div class="provider-models">
${models.map(model => `
<span class="provider-model-tag">
<span class="model-name">${this.escapeHtml(model.name || '')}</span>
${model.alias ? `<span class="model-alias">${this.escapeHtml(model.alias)}</span>` : ''}
</span>
`).join('')}
</div>
`;
}
validateOpenAIProviderInput(name, baseUrl, models) {
if (!name || !baseUrl) {
this.showNotification(i18n.t('notification.openai_provider_required'), 'error');
return false;
}
const invalidModel = models.find(model => !model.name);
if (invalidModel) {
this.showNotification(i18n.t('notification.openai_model_name_required'), 'error');
return false;
}
return true;
}
} }
// 全局管理器实例 // 全局管理器实例

1353
i18n.js

File diff suppressed because it is too large Load Diff

View File

@@ -105,106 +105,81 @@
</div> </div>
<!-- 主页面 --> <!-- 主页面 -->
<div id="main-page" class="container" style="display: none;"> <div id="main-page" style="display: none;">
<!-- 部导航 --> <!-- 部导航 -->
<header class="header"> <div class="top-navbar">
<div class="header-content"> <div class="top-navbar-left">
<h1 class="brand"> <button class="mobile-menu-btn" id="mobile-menu-btn">
<img id="site-logo" alt="Logo" style="display:none" /> <i class="fas fa-bars"></i>
<span class="brand-title" data-i18n="title.main">CLI Proxy API Management Center</span> </button>
</h1> <button class="sidebar-toggle-btn-desktop" id="sidebar-toggle-btn-desktop" title="收起/展开侧边栏">
<div class="header-actions"> <i class="fas fa-bars"></i>
<div class="header-controls"> </button>
<div class="language-switcher"> <div class="top-navbar-brand">
<button id="language-toggle-main" class="btn btn-secondary language-btn"> <img id="site-logo" class="top-navbar-brand-logo" alt="Logo" style="display:none" />
<i class="fas fa-globe"></i> <span class="top-navbar-brand-text" data-i18n="title.main">CLI Proxy API Management Center</span>
<span data-i18n="language.switch">语言</span>
</button>
</div>
<div class="theme-switcher">
<button id="theme-toggle-main" class="btn btn-secondary theme-btn">
<i class="fas fa-moon"></i>
<span data-i18n="theme.switch">主题</span>
</button>
</div>
</div>
<button id="connection-status" class="btn btn-secondary">
<i class="fas fa-circle"></i> <span data-i18n="header.check_connection">检查连接</span>
</button>
<button id="refresh-all" class="btn btn-primary">
<i class="fas fa-sync-alt"></i> <span data-i18n="header.refresh_all">刷新全部</span>
</button>
<button id="logout-btn" class="btn btn-danger">
<i class="fas fa-sign-out-alt"></i> <span data-i18n="header.logout">登出</span>
</button>
</div> </div>
</div> </div>
</header> <div class="top-navbar-actions">
<div class="header-controls">
<!-- 连接信息 --> <div class="language-switcher">
<section class="auth-section"> <button id="language-toggle-main" class="btn btn-secondary language-btn">
<div class="card"> <i class="fas fa-globe"></i>
<div class="card-header"> </button>
<h2><i class="fas fa-server"></i> <span data-i18n="connection.title">连接信息</span></h2> </div>
</div> <div class="theme-switcher">
<div class="card-content"> <button id="theme-toggle-main" class="btn btn-secondary theme-btn">
<div class="connection-info"> <i class="fas fa-moon"></i>
<div class="info-item"> </button>
<div class="info-label">
<i class="fas fa-globe"></i>
<span data-i18n="connection.server_address">服务器地址:</span>
</div>
<div class="info-value" id="display-api-url">-</div>
</div>
<div class="info-item">
<div class="info-label">
<i class="fas fa-key"></i>
<span data-i18n="connection.management_key">管理密钥:</span>
</div>
<div class="info-value" id="display-management-key">-</div>
</div>
<div class="info-item">
<div class="info-label">
<i class="fas fa-circle"></i>
<span data-i18n="connection.status">连接状态:</span>
</div>
<div class="info-value" id="display-connection-status">
<span class="status-indicator disconnected" data-i18n="common.disconnected">未连接</span>
</div>
</div>
</div> </div>
</div> </div>
<button id="connection-status" class="btn btn-secondary">
<i class="fas fa-circle"></i> <span data-i18n="header.check_connection">检查连接</span>
</button>
<button id="refresh-all" class="btn btn-primary">
<i class="fas fa-sync-alt"></i>
</button>
<button id="logout-btn" class="btn btn-danger">
<i class="fas fa-sign-out-alt"></i>
</button>
</div> </div>
</section> </div>
<!-- 主要内容区域 --> <div class="layout" id="layout-container">
<main class="main-content"> <!-- 侧边栏 -->
<!-- 侧边栏导航 --> <nav class="sidebar" id="sidebar">
<nav class="sidebar"> <!-- 导航菜单 -->
<ul class="nav-menu"> <ul class="nav-menu">
<li><a href="#basic-settings" class="nav-item active" data-section="basic-settings"> <li data-tooltip="基础设置"><a href="#basic-settings" class="nav-item active" data-section="basic-settings">
<i class="fas fa-sliders-h"></i> <span data-i18n="nav.basic_settings">基础设置</span> <i class="fas fa-sliders-h"></i> <span data-i18n="nav.basic_settings">基础设置</span>
</a></li> </a></li>
<li><a href="#api-keys" class="nav-item" data-section="api-keys"> <li data-tooltip="API 密钥"><a href="#api-keys" class="nav-item" data-section="api-keys">
<i class="fas fa-key"></i> <span data-i18n="nav.api_keys">API 密钥</span> <i class="fas fa-key"></i> <span data-i18n="nav.api_keys">API 密钥</span>
</a></li> </a></li>
<li><a href="#ai-providers" class="nav-item" data-section="ai-providers"> <li data-tooltip="AI 提供商"><a href="#ai-providers" class="nav-item" data-section="ai-providers">
<i class="fas fa-robot"></i> <span data-i18n="nav.ai_providers">AI 提供商</span> <i class="fas fa-robot"></i> <span data-i18n="nav.ai_providers">AI 提供商</span>
</a></li> </a></li>
<li><a href="#auth-files" class="nav-item" data-section="auth-files"> <li data-tooltip="认证文件"><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"> <li data-tooltip="使用统计"><a href="#usage-stats" class="nav-item" data-section="usage-stats">
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span> <i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
</a></li> </a></li>
<li><a href="#system-info" class="nav-item" data-section="system-info"> <li data-tooltip="系统信息"><a href="#system-info" class="nav-item" data-section="system-info">
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span> <i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
</a></li> </a></li>
</ul> </ul>
</nav> </nav>
<!-- 内容区域 --> <!-- 侧边栏遮罩(移动端) -->
<div class="content-area"> <div class="sidebar-overlay" id="sidebar-overlay"></div>
<!-- 主内容包装器 -->
<div class="main-wrapper" id="main-wrapper">
<!-- 主内容区域 -->
<div class="main-content">
<!-- 内容区域 -->
<div class="content-area">
<!-- 基础设置 --> <!-- 基础设置 -->
<section id="basic-settings" class="content-section active"> <section id="basic-settings" class="content-section active">
<h2 data-i18n="basic_settings.title">基础设置</h2> <h2 data-i18n="basic_settings.title">基础设置</h2>
@@ -526,6 +501,40 @@
<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>
<!-- 连接信息卡片 -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-server"></i> <span data-i18n="connection.title">连接信息</span></h3>
</div>
<div class="card-content">
<div class="connection-info">
<div class="info-item">
<div class="info-label">
<i class="fas fa-globe"></i>
<span data-i18n="connection.server_address">服务器地址:</span>
</div>
<div class="info-value" id="display-api-url">-</div>
</div>
<div class="info-item">
<div class="info-label">
<i class="fas fa-key"></i>
<span data-i18n="connection.management_key">管理密钥:</span>
</div>
<div class="info-value" id="display-management-key">-</div>
</div>
<div class="info-item">
<div class="info-label">
<i class="fas fa-circle"></i>
<span data-i18n="connection.status">连接状态:</span>
</div>
<div class="info-value" id="display-connection-status">
<span class="status-indicator disconnected" data-i18n="common.disconnected">未连接</span>
</div>
</div>
</div>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-info-circle"></i> <span data-i18n="system_info.connection_status_title">连接状态</span></h3> <h3><i class="fas fa-info-circle"></i> <span data-i18n="system_info.connection_status_title">连接状态</span></h3>
@@ -548,18 +557,23 @@
</div> </div>
</div> </div>
</section> </section>
</div>
<!-- /内容区域 -->
<!-- 版本信息 -->
<footer class="version-footer">
<div class="version-info">
<span data-i18n="footer.version">版本</span>: v0.1.0
<span class="separator"></span>
<span data-i18n="footer.author">作者</span>: Supra4E8C
</div>
</footer>
</div> </div>
</main> <!-- /主内容区域 -->
</div>
<!-- 版本信息 --> <!-- /主内容包装器 -->
<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>
<!-- /主页面 -->
<!-- 模态框 --> <!-- 模态框 -->
<div id="modal" class="modal"> <div id="modal" class="modal">

1093
styles.css

File diff suppressed because it is too large Load Diff