mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27948b3d5c | ||
|
|
dff28db227 | ||
|
|
34b16ca886 | ||
|
|
fa86f76289 | ||
|
|
41ca99978f | ||
|
|
6ef674487f | ||
|
|
2be7ced21a | ||
|
|
b61155d215 | ||
|
|
5488d6153d |
98
README.md
98
README.md
@@ -11,7 +11,8 @@ https://remote.router-for.me/
|
||||
|
||||
Minimum required version: ≥ 6.0.0
|
||||
Recommended version: ≥ 6.0.19
|
||||
Starting from version 6.0.19, the WebUI has been integrated into the main program and is accessible via `/management.html`.
|
||||
|
||||
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
|
||||
|
||||
@@ -19,13 +20,16 @@ Starting from version 6.0.19, the WebUI has been integrated into the main progra
|
||||
- Supports management key authentication
|
||||
- Configurable API base address
|
||||
- Real-time connection status detection
|
||||
- Auto-login with saved credentials
|
||||
- Language and theme switching
|
||||
|
||||
### 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
|
||||
- Auto-switch project when quota exceeded
|
||||
- Switch to preview models when quota exceeded
|
||||
|
||||
### API Key Management
|
||||
- **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
|
||||
- Delete single or all authentication files
|
||||
- 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
|
||||
|
||||
### 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.
|
||||
|
||||
### 2. Use a Local Server
|
||||
### 3. Use a Local Server
|
||||
|
||||
#### Option A: Using Node.js (npm)
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
@@ -55,10 +78,19 @@ npm install
|
||||
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.
|
||||
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
|
||||
3. Click the "Connect" button.
|
||||
4. Once connected successfully, all features will be available.
|
||||
@@ -70,8 +102,16 @@ npm start
|
||||
- **API Keys**: Management of keys for various API services.
|
||||
- **AI Providers**: Configuration for AI service providers.
|
||||
- **Auth Files**: Upload and download management for authentication files.
|
||||
- **Usage Stats**: Real-time analytics and usage statistics with interactive charts.
|
||||
- **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
|
||||
|
||||
### Modern UI
|
||||
@@ -79,27 +119,45 @@ npm start
|
||||
- Beautiful gradient colors and shadow effects
|
||||
- Smooth animations and transition effects
|
||||
- Intuitive icons and status indicators
|
||||
- Dark/Light theme support with system preference detection
|
||||
- Mobile-friendly sidebar with overlay
|
||||
|
||||
### Real-time Updates
|
||||
- Configuration changes take effect immediately
|
||||
- Real-time status feedback
|
||||
- Automatic data refresh
|
||||
- Live usage statistics with interactive charts
|
||||
- Real-time connection status monitoring
|
||||
|
||||
### Security Features
|
||||
- Masked display for keys
|
||||
- Secure credential storage
|
||||
- Auto-login with encrypted local storage
|
||||
|
||||
### Responsive Design
|
||||
- Perfectly adapts to desktop and mobile devices
|
||||
- Adaptive layout
|
||||
- Adaptive layout with collapsible sidebar
|
||||
- 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
|
||||
|
||||
- **Frontend**: Plain HTML, CSS, JavaScript
|
||||
- **Styling**: CSS3 + Flexbox/Grid
|
||||
- **Frontend**: Plain HTML, CSS, JavaScript (ES6+)
|
||||
- **Styling**: CSS3 + Flexbox/Grid with CSS Variables
|
||||
- **Icons**: Font Awesome 6.4.0
|
||||
- **Charts**: Chart.js for interactive data visualization
|
||||
- **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
|
||||
|
||||
@@ -119,12 +177,20 @@ npm start
|
||||
### File Structure
|
||||
```
|
||||
webui/
|
||||
├── index.html # Main page
|
||||
├── styles.css # Stylesheet
|
||||
├── app.js # Application logic
|
||||
├── package.json # Project configuration
|
||||
├── i18n.js # Internationalization support
|
||||
└── README.md # README document
|
||||
├── index.html # Main page with responsive layout
|
||||
├── styles.css # Stylesheet with theme support
|
||||
├── app.js # Application logic and API management
|
||||
├── i18n.js # Internationalization support (EN/CN)
|
||||
├── package.json # Project configuration
|
||||
├── 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
|
||||
|
||||
102
README_CN.md
102
README_CN.md
@@ -1,14 +1,15 @@
|
||||
# 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访问
|
||||
最低可用版本 ≥ 6.0.0
|
||||
推荐版本 ≥ 6.0.19
|
||||
自6.0.19起WebUI已经集成在主程序中 可以通过主项目开启的外部端口的`/management.html`访问
|
||||
|
||||
## 功能特点
|
||||
|
||||
@@ -16,13 +17,16 @@ https://remote.router-for.me/
|
||||
- 支持管理密钥认证
|
||||
- 可配置 API 基础地址
|
||||
- 实时连接状态检测
|
||||
- 自动登录保存的凭据
|
||||
- 语言和主题切换
|
||||
|
||||
### 基础设置
|
||||
- **调试模式**: 开启/关闭调试功能
|
||||
- **代理设置**: 配置代理服务器 URL
|
||||
- **请求重试**: 设置请求重试次数
|
||||
- **配额管理**: 配置超出配额时的行为
|
||||
- **本地访问**: 管理本地未认证访问
|
||||
- 超出配额时自动切换项目
|
||||
- 超出配额时切换到预览模型
|
||||
|
||||
### 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` 文件即可使用。
|
||||
|
||||
### 2. 使用本地服务器
|
||||
### 3. 使用本地服务器
|
||||
|
||||
#### 方法A:使用 Node.js (npm)
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
@@ -52,10 +75,19 @@ npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### 3. 配置 API 连接
|
||||
#### 方法B:使用 Python
|
||||
```bash
|
||||
# Python 3.x
|
||||
python -m http.server 8000
|
||||
|
||||
```
|
||||
|
||||
然后在浏览器中打开 `http://localhost:8000`。
|
||||
|
||||
### 3. 配置连接
|
||||
1. 打开管理界面
|
||||
2. 在登录界面上输入:
|
||||
- **远程地址**: `http://localhost:8317`/v0/management将会自动为您补全
|
||||
- **远程地址**: 现版本远程地址将会自动从您的访问地址中获取 当然您也可以自定义
|
||||
- **管理密钥**: 您的管理密钥
|
||||
3. 点击"连接"按钮
|
||||
4. 连接成功后即可使用所有功能
|
||||
@@ -67,8 +99,16 @@ npm start
|
||||
- **API 密钥**: 各种 API 服务的密钥管理
|
||||
- **AI 提供商**: AI 服务提供商配置
|
||||
- **认证文件**: 认证文件的上传下载管理
|
||||
- **使用统计**: 实时分析和使用统计,包含交互式图表
|
||||
- **系统信息**: 连接状态和系统信息
|
||||
|
||||
### 登录界面
|
||||
- **自动连接**: 使用保存的凭据自动尝试连接
|
||||
- **自定义连接**: 手动配置 API 基础地址
|
||||
- **当前地址检测**: 自动检测并使用当前访问地址
|
||||
- **语言切换**: 支持多种语言(英文/中文)
|
||||
- **主题切换**: 支持明暗主题
|
||||
|
||||
## 特性亮点
|
||||
|
||||
### 现代化 UI
|
||||
@@ -76,27 +116,45 @@ npm start
|
||||
- 美观的渐变色彩和阴影效果
|
||||
- 流畅的动画和过渡效果
|
||||
- 直观的图标和状态指示
|
||||
- 明暗主题支持,自动检测系统偏好
|
||||
- 移动端友好的侧边栏和遮罩
|
||||
|
||||
### 实时更新
|
||||
- 配置更改立即生效
|
||||
- 实时状态反馈
|
||||
- 自动数据刷新
|
||||
- 实时使用统计和交互式图表
|
||||
- 实时连接状态监控
|
||||
|
||||
### 安全特性
|
||||
- 密钥遮蔽显示
|
||||
- 安全凭据存储
|
||||
- 加密本地存储自动登录
|
||||
|
||||
### 响应式设计
|
||||
- 完美适配桌面和移动设备
|
||||
- 自适应布局
|
||||
- 自适应布局,可折叠侧边栏
|
||||
- 触摸友好的交互
|
||||
- 移动端菜单和遮罩
|
||||
|
||||
### 分析与监控
|
||||
- Chart.js 驱动的交互式图表
|
||||
- 实时使用统计
|
||||
- 请求趋势可视化
|
||||
- Token 消耗跟踪
|
||||
- API 性能指标
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**: 纯 HTML、CSS、JavaScript
|
||||
- **样式**: CSS3 + Flexbox/Grid
|
||||
- **前端**: 纯 HTML、CSS、JavaScript (ES6+)
|
||||
- **样式**: CSS3 + Flexbox/Grid,支持 CSS 变量
|
||||
- **图标**: Font Awesome 6.4.0
|
||||
- **图表**: Chart.js 交互式数据可视化
|
||||
- **字体**: Segoe UI 系统字体
|
||||
- **API**: RESTful API 调用
|
||||
- **API**: RESTful API 调用,自动认证
|
||||
- **国际化**: 自定义 i18n 系统,支持中英文
|
||||
- **主题系统**: CSS 自定义属性动态主题
|
||||
- **存储**: LocalStorage 用户偏好和凭据存储
|
||||
|
||||
## 故障排除
|
||||
|
||||
@@ -116,12 +174,20 @@ npm start
|
||||
### 文件结构
|
||||
```
|
||||
webui/
|
||||
├── index.html # 主页面
|
||||
├── styles.css # 样式文件
|
||||
├── app.js # 应用逻辑
|
||||
├── package.json # 项目配置
|
||||
├── i18n.js # 国际化支持
|
||||
└── README.md # 说明文档
|
||||
├── index.html # 主页面,响应式布局
|
||||
├── styles.css # 样式文件,支持主题
|
||||
├── app.js # 应用逻辑和 API 管理
|
||||
├── i18n.js # 国际化支持(中英文)
|
||||
├── package.json # 项目配置
|
||||
├── build.js # 生产环境构建脚本
|
||||
├── bundle-entry.js # 打包入口文件
|
||||
├── build-scripts/ # 构建工具
|
||||
│ └── prepare-html.js # HTML 准备脚本
|
||||
├── logo.jpg # 应用图标
|
||||
├── LICENSE # MIT 许可证
|
||||
├── README.md # 英文文档
|
||||
├── README_CN.md # 中文文档
|
||||
└── BUILD_RELEASE.md # 构建和发布说明
|
||||
```
|
||||
|
||||
### API 调用
|
||||
|
||||
420
app.js
420
app.js
@@ -553,6 +553,136 @@ class CLIProxyManager {
|
||||
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);
|
||||
}
|
||||
|
||||
// HTML 转义,防止 XSS
|
||||
escapeHtml(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 显示添加API密钥模态框
|
||||
showAddApiKeyModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
@@ -1439,7 +1580,8 @@ class CLIProxyManager {
|
||||
<div class="item-content">
|
||||
<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>
|
||||
${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 class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editCodexKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})">
|
||||
@@ -1459,18 +1601,22 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>添加Codex API配置</h3>
|
||||
<h3>${i18n.t('ai_providers.codex_add_modal_title')}</h3>
|
||||
<div class="form-group">
|
||||
<label for="new-codex-key">API密钥:</label>
|
||||
<input type="text" id="new-codex-key" placeholder="请输入Codex API密钥">
|
||||
<label for="new-codex-key">${i18n.t('ai_providers.codex_add_modal_key_label')}</label>
|
||||
<input type="text" id="new-codex-key" placeholder="${i18n.t('ai_providers.codex_add_modal_key_placeholder')}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-codex-url">Base URL (可选):</label>
|
||||
<input type="text" id="new-codex-url" placeholder="例如: https://api.example.com">
|
||||
<label for="new-codex-url">${i18n.t('ai_providers.codex_add_modal_url_label')}</label>
|
||||
<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 class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.addCodexKey()">添加</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.addCodexKey()">${i18n.t('common.add')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1481,9 +1627,10 @@ class CLIProxyManager {
|
||||
async addCodexKey() {
|
||||
const apiKey = document.getElementById('new-codex-key').value.trim();
|
||||
const baseUrl = document.getElementById('new-codex-url').value.trim();
|
||||
const proxyUrl = document.getElementById('new-codex-proxy').value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
this.showNotification('请输入API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.field_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1495,6 +1642,9 @@ class CLIProxyManager {
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
|
||||
currentKeys.push(newConfig);
|
||||
|
||||
@@ -1518,18 +1668,22 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>编辑Codex API配置</h3>
|
||||
<h3>${i18n.t('ai_providers.codex_edit_modal_title')}</h3>
|
||||
<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']}">
|
||||
</div>
|
||||
<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'] || ''}">
|
||||
</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">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">更新</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateCodexKey(${index})">${i18n.t('common.update')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1540,9 +1694,10 @@ class CLIProxyManager {
|
||||
async updateCodexKey(index) {
|
||||
const apiKey = document.getElementById('edit-codex-key').value.trim();
|
||||
const baseUrl = document.getElementById('edit-codex-url').value.trim();
|
||||
const proxyUrl = document.getElementById('edit-codex-proxy').value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
this.showNotification('请输入API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.field_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1551,6 +1706,9 @@ class CLIProxyManager {
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
|
||||
await this.makeRequest('/codex-api-key', {
|
||||
method: 'PATCH',
|
||||
@@ -1612,7 +1770,8 @@ class CLIProxyManager {
|
||||
<div class="item-content">
|
||||
<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>
|
||||
${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 class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editClaudeKey(${index}, ${JSON.stringify(config).replace(/"/g, '"')})">
|
||||
@@ -1632,18 +1791,22 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>添加Claude API配置</h3>
|
||||
<h3>${i18n.t('ai_providers.claude_add_modal_title')}</h3>
|
||||
<div class="form-group">
|
||||
<label for="new-claude-key">API密钥:</label>
|
||||
<input type="text" id="new-claude-key" placeholder="请输入Claude API密钥">
|
||||
<label for="new-claude-key">${i18n.t('ai_providers.claude_add_modal_key_label')}</label>
|
||||
<input type="text" id="new-claude-key" placeholder="${i18n.t('ai_providers.claude_add_modal_key_placeholder')}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-claude-url">Base URL (可选):</label>
|
||||
<input type="text" id="new-claude-url" placeholder="例如: https://api.anthropic.com">
|
||||
<label for="new-claude-url">${i18n.t('ai_providers.claude_add_modal_url_label')}</label>
|
||||
<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 class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.addClaudeKey()">添加</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.addClaudeKey()">${i18n.t('common.add')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1654,9 +1817,10 @@ class CLIProxyManager {
|
||||
async addClaudeKey() {
|
||||
const apiKey = document.getElementById('new-claude-key').value.trim();
|
||||
const baseUrl = document.getElementById('new-claude-url').value.trim();
|
||||
const proxyUrl = document.getElementById('new-claude-proxy').value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
this.showNotification('请输入API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.field_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1668,6 +1832,9 @@ class CLIProxyManager {
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
|
||||
currentKeys.push(newConfig);
|
||||
|
||||
@@ -1691,18 +1858,22 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>编辑Claude API配置</h3>
|
||||
<h3>${i18n.t('ai_providers.claude_edit_modal_title')}</h3>
|
||||
<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']}">
|
||||
</div>
|
||||
<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'] || ''}">
|
||||
</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">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">更新</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateClaudeKey(${index})">${i18n.t('common.update')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1713,9 +1884,10 @@ class CLIProxyManager {
|
||||
async updateClaudeKey(index) {
|
||||
const apiKey = document.getElementById('edit-claude-key').value.trim();
|
||||
const baseUrl = document.getElementById('edit-claude-url').value.trim();
|
||||
const proxyUrl = document.getElementById('edit-claude-proxy').value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
this.showNotification('请输入API密钥', 'error');
|
||||
this.showNotification(i18n.t('notification.field_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1724,6 +1896,9 @@ class CLIProxyManager {
|
||||
if (baseUrl) {
|
||||
newConfig['base-url'] = baseUrl;
|
||||
}
|
||||
if (proxyUrl) {
|
||||
newConfig['proxy-url'] = proxyUrl;
|
||||
}
|
||||
|
||||
await this.makeRequest('/claude-api-key', {
|
||||
method: 'PATCH',
|
||||
@@ -1783,10 +1958,11 @@ class CLIProxyManager {
|
||||
container.innerHTML = providers.map((provider, index) => `
|
||||
<div class="provider-item">
|
||||
<div class="item-content">
|
||||
<div class="item-title">${provider.name}</div>
|
||||
<div class="item-subtitle">${i18n.t('common.base_url')}: ${provider['base-url']}</div>
|
||||
<div class="item-subtitle">${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-keys'] || []).length}</div>
|
||||
<div class="item-title">${this.escapeHtml(provider.name)}</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-key-entries'] || []).length}</div>
|
||||
<div class="item-subtitle">${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}</div>
|
||||
${this.renderOpenAIModelBadges(provider.models || [])}
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.editOpenAIProvider(${index}, ${JSON.stringify(provider).replace(/"/g, '"')})">
|
||||
@@ -1806,26 +1982,37 @@ class CLIProxyManager {
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<h3>添加OpenAI兼容提供商</h3>
|
||||
<h3>${i18n.t('ai_providers.openai_add_modal_title')}</h3>
|
||||
<div class="form-group">
|
||||
<label for="new-provider-name">提供商名称:</label>
|
||||
<input type="text" id="new-provider-name" placeholder="例如: openrouter">
|
||||
<label for="new-provider-name">${i18n.t('ai_providers.openai_add_modal_name_label')}</label>
|
||||
<input type="text" id="new-provider-name" placeholder="${i18n.t('ai_providers.openai_add_modal_name_placeholder')}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-provider-url">Base URL:</label>
|
||||
<input type="text" id="new-provider-url" placeholder="例如: https://openrouter.ai/api/v1">
|
||||
<label for="new-provider-url">${i18n.t('ai_providers.openai_add_modal_url_label')}</label>
|
||||
<input type="text" id="new-provider-url" placeholder="${i18n.t('ai_providers.openai_add_modal_url_placeholder')}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-provider-keys">API密钥 (每行一个):</label>
|
||||
<textarea id="new-provider-keys" rows="3" placeholder="sk-key1 sk-key2"></textarea>
|
||||
<label for="new-provider-keys">${i18n.t('ai_providers.openai_add_modal_keys_label')}</label>
|
||||
<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 class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.addOpenAIProvider()">添加</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.addOpenAIProvider()">${i18n.t('common.add')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateModelFields('new-provider-models-wrapper', []);
|
||||
}
|
||||
|
||||
// 添加OpenAI提供商
|
||||
@@ -1833,9 +2020,10 @@ class CLIProxyManager {
|
||||
const name = document.getElementById('new-provider-name').value.trim();
|
||||
const baseUrl = document.getElementById('new-provider-url').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) {
|
||||
this.showNotification('请填写提供商名称和Base URL', 'error');
|
||||
if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1844,12 +2032,17 @@ class CLIProxyManager {
|
||||
const currentProviders = data['openai-compatibility'] || [];
|
||||
|
||||
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 = {
|
||||
name,
|
||||
'base-url': baseUrl,
|
||||
'api-keys': apiKeys,
|
||||
models: []
|
||||
'api-key-entries': apiKeyEntries,
|
||||
models
|
||||
};
|
||||
|
||||
currentProviders.push(newProvider);
|
||||
@@ -1873,29 +2066,42 @@ class CLIProxyManager {
|
||||
const modal = document.getElementById('modal');
|
||||
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 = `
|
||||
<h3>编辑OpenAI兼容提供商</h3>
|
||||
<h3>${i18n.t('ai_providers.openai_edit_modal_title')}</h3>
|
||||
<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}">
|
||||
</div>
|
||||
<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']}">
|
||||
</div>
|
||||
<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>
|
||||
</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">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">更新</button>
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.populateModelFields('edit-provider-models-wrapper', provider.models || []);
|
||||
}
|
||||
|
||||
// 更新OpenAI提供商
|
||||
@@ -1903,20 +2109,26 @@ class CLIProxyManager {
|
||||
const name = document.getElementById('edit-provider-name').value.trim();
|
||||
const baseUrl = document.getElementById('edit-provider-url').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) {
|
||||
this.showNotification('请填写提供商名称和Base URL', 'error');
|
||||
if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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 = {
|
||||
name,
|
||||
'base-url': baseUrl,
|
||||
'api-keys': apiKeys,
|
||||
models: []
|
||||
'api-key-entries': apiKeyEntries,
|
||||
models
|
||||
};
|
||||
|
||||
await this.makeRequest('/openai-compatibility', {
|
||||
@@ -2573,6 +2785,100 @@ class CLIProxyManager {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局管理器实例
|
||||
|
||||
190
index.html
190
index.html
@@ -105,106 +105,81 @@
|
||||
</div>
|
||||
|
||||
<!-- 主页面 -->
|
||||
<div id="main-page" class="container" style="display: none;">
|
||||
<!-- 头部导航 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1 class="brand">
|
||||
<img id="site-logo" alt="Logo" style="display:none" />
|
||||
<span class="brand-title" data-i18n="title.main">CLI Proxy API Management Center</span>
|
||||
</h1>
|
||||
<div class="header-actions">
|
||||
<div class="header-controls">
|
||||
<div class="language-switcher">
|
||||
<button id="language-toggle-main" class="btn btn-secondary language-btn">
|
||||
<i class="fas fa-globe"></i>
|
||||
<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 id="main-page" style="display: none;">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="top-navbar">
|
||||
<div class="top-navbar-left">
|
||||
<button class="mobile-menu-btn" id="mobile-menu-btn">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="sidebar-toggle-btn-desktop" id="sidebar-toggle-btn-desktop" title="收起/展开侧边栏">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="top-navbar-brand">
|
||||
<img id="site-logo" class="top-navbar-brand-logo" alt="Logo" style="display:none" />
|
||||
<span class="top-navbar-brand-text" data-i18n="title.main">CLI Proxy API Management Center</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 连接信息 -->
|
||||
<section class="auth-section">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2><i class="fas fa-server"></i> <span data-i18n="connection.title">连接信息</span></h2>
|
||||
</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 class="top-navbar-actions">
|
||||
<div class="header-controls">
|
||||
<div class="language-switcher">
|
||||
<button id="language-toggle-main" class="btn btn-secondary language-btn">
|
||||
<i class="fas fa-globe"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="theme-switcher">
|
||||
<button id="theme-toggle-main" class="btn btn-secondary theme-btn">
|
||||
<i class="fas fa-moon"></i>
|
||||
</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>
|
||||
</button>
|
||||
<button id="logout-btn" class="btn btn-danger">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 侧边栏导航 -->
|
||||
<nav class="sidebar">
|
||||
<div class="layout" id="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<!-- 导航菜单 -->
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</a></li>
|
||||
</ul>
|
||||
</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">
|
||||
<h2 data-i18n="basic_settings.title">基础设置</h2>
|
||||
@@ -526,6 +501,40 @@
|
||||
<section id="system-info" class="content-section">
|
||||
<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-header">
|
||||
<h3><i class="fas fa-info-circle"></i> <span data-i18n="system_info.connection_status_title">连接状态</span></h3>
|
||||
@@ -548,18 +557,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
<!-- /主页面 -->
|
||||
|
||||
<!-- 模态框 -->
|
||||
<div id="modal" class="modal">
|
||||
|
||||
1093
styles.css
1093
styles.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user