docs(terminology): 统一术语标准 - 将"供应商"规范为"提供商"

将项目中所有文档、注释和用户界面中的"供应商"术语统一修改为"提供商",提升术语一致性。同时:
- 优化提供商健康检查频率从30分钟调整为10分钟,提高监控及时性
- 新增路由URL动态更新功能,支持不同部署环境的路径适配
- 更新相关样式类名和注释,确保代码与文档术语保持一致
This commit is contained in:
hex2077 2025-11-14 00:10:27 +08:00
parent ee050c77f2
commit 0149bf6558
12 changed files with 137 additions and 106 deletions

View file

@ -202,7 +202,7 @@ install-and-run.bat
**⚙️ 配置管理**实时参数修改支持所有提供商Gemini、OpenAI、Claude、Kiro、Qwen包含高级设置和文件上传 **⚙️ 配置管理**实时参数修改支持所有提供商Gemini、OpenAI、Claude、Kiro、Qwen包含高级设置和文件上传
**🔗 供商池**:监控活动连接、提供商健康统计、启用/禁用管理 **🔗 供商池**:监控活动连接、提供商健康统计、启用/禁用管理
**📁 配置文件**OAuth 凭据集中管理,支持搜索过滤和文件操作 **📁 配置文件**OAuth 凭据集中管理,支持搜索过滤和文件操作
@ -261,17 +261,17 @@ install-and-run.bat
--- ---
### 🔄 模型供商切换 ### 🔄 模型供商切换
本项目提供两种灵活的模型切换方式,满足不同使用场景的需求。 本项目提供两种灵活的模型切换方式,满足不同使用场景的需求。
通过在 API 请求路径中指定供商标识,实现即时切换: 通过在 API 请求路径中指定供商标识,实现即时切换:
| 路由路径 | 说明 | 适用场景 | | 路由路径 | 说明 | 适用场景 |
|---------|------|---------| |---------|------|---------|
| `/claude-custom` | 使用配置文件中的 Claude API | 官方 Claude API 调用 | | `/claude-custom` | 使用配置文件中的 Claude API | 官方 Claude API 调用 |
| `/claude-kiro-oauth` | 通过 Kiro OAuth 访问 Claude | 免费使用 Claude Sonnet 4.5 | | `/claude-kiro-oauth` | 通过 Kiro OAuth 访问 Claude | 免费使用 Claude Sonnet 4.5 |
| `/openai-custom` | 使用 OpenAI 供商处理请求 | 标准 OpenAI API 调用 | | `/openai-custom` | 使用 OpenAI 供商处理请求 | 标准 OpenAI API 调用 |
| `/gemini-cli-oauth` | 通过 Gemini CLI OAuth 访问 | 突破 Gemini 免费限制 | | `/gemini-cli-oauth` | 通过 Gemini CLI OAuth 访问 | 突破 Gemini 免费限制 |
| `/openai-qwen-oauth` | 通过 Qwen OAuth 访问 | 使用 Qwen Code Plus | | `/openai-qwen-oauth` | 通过 Qwen OAuth 访问 | 使用 Qwen Code Plus |
| `/openaiResponses-custom` | OpenAI Responses API | 结构化对话场景 | | `/openaiResponses-custom` | OpenAI Responses API | 结构化对话场景 |

View file

@ -2,7 +2,7 @@
## 概述 ## 概述
AIClient2API 现在包含一个功能完整的可视化 Web UI 管理控制台,允许您通过浏览器轻松管理配置、监控供商池状态、查看实时日志、配置AI模型提供商等。 AIClient2API 现在包含一个功能完整的可视化 Web UI 管理控制台,允许您通过浏览器轻松管理配置、监控供商池状态、查看实时日志、配置AI模型提供商等。
## 功能特性 ## 功能特性
@ -14,7 +14,7 @@ AIClient2API 现在包含一个功能完整的可视化 Web UI 管理控制台
### 📊 实时监控 ### 📊 实时监控
- **仪表盘**显示运行时间、系统信息、Node.js版本、服务器时间、内存使用 - **仪表盘**显示运行时间、系统信息、Node.js版本、服务器时间、内存使用
- **商池管理**:查看各提供商账户状态、使用统计、错误率 - **供商池管理**:查看各提供商账户状态、使用统计、错误率
- **活动统计**:活动连接、活跃提供商、健康提供商数量 - **活动统计**:活动连接、活跃提供商、健康提供商数量
### ⚙️ 配置管理 ### ⚙️ 配置管理
@ -32,7 +32,7 @@ AIClient2API 现在包含一个功能完整的可视化 Web UI 管理控制台
- 提示日志配置 - 提示日志配置
- 请求重试机制(最大重试次数、基础延迟) - 请求重试机制(最大重试次数、基础延迟)
- OAuth令牌自动刷新设置 - OAuth令牌自动刷新设置
- 供商池配置文件路径 - 供商池配置文件路径
### 🔧 上传配置管理 ### 🔧 上传配置管理
- 搜索配置文件功能 - 搜索配置文件功能
@ -87,7 +87,7 @@ node src/api-server.js --port 3000 --api-key 123456
1. **仪表盘** - 系统概览、统计信息和路径路由示例 1. **仪表盘** - 系统概览、统计信息和路径路由示例
2. **配置管理** - 修改服务器配置和提供商设置 2. **配置管理** - 修改服务器配置和提供商设置
3. **商池管理** - 管理多个API提供商账户 3. **供商池管理** - 管理多个API提供商账户
4. **上传配置管理** - 管理配置文件和搜索过滤 4. **上传配置管理** - 管理配置文件和搜索过滤
5. **实时日志** - 查看服务器运行日志 5. **实时日志** - 查看服务器运行日志
@ -103,7 +103,7 @@ node src/api-server.js --port 3000 --api-key 123456
### 系统信息 ### 系统信息
- `GET /api/system` - 获取系统信息 - `GET /api/system` - 获取系统信息
- `GET /api/providers` - 获取供商池信息 - `GET /api/providers` - 获取供商池信息
### 实时数据 ### 实时数据
- `GET /api/events` - Server-Sent Events 流,用于实时更新 - `GET /api/events` - Server-Sent Events 流,用于实时更新
@ -203,7 +203,7 @@ A: 在配置管理页面选择对应的OAuth提供商填写项目ID和OAuth
A: 上传配置文件可以将本地配置文件上传到服务器,方便管理和在不同环境间同步配置。 A: 上传配置文件可以将本地配置文件上传到服务器,方便管理和在不同环境间同步配置。
### Q: 如何查看更详细的提供商信息? ### Q: 如何查看更详细的提供商信息?
A: 在"供商池"页面可以看到每个提供商的使用次数、错误次数、最后使用时间等详细信息。 A: 在"供商池"页面可以看到每个提供商的使用次数、错误次数、最后使用时间等详细信息。
### Q: 配置修改后需要重启服务器吗? ### Q: 配置修改后需要重启服务器吗?
A: 大部分配置如系统提示、API密钥会立即生效但网络端口等更改需要重启服务器。 A: 大部分配置如系统提示、API密钥会立即生效但网络端口等更改需要重启服务器。

View file

@ -12,7 +12,7 @@ export class ProviderPoolManager {
this.providerStatus = {}; // Tracks health and usage for each provider instance this.providerStatus = {}; // Tracks health and usage for each provider instance
this.roundRobinIndex = {}; // Tracks the current index for round-robin selection for each provider type this.roundRobinIndex = {}; // Tracks the current index for round-robin selection for each provider type
this.maxErrorCount = options.maxErrorCount || 3; // Default to 1 errors before marking unhealthy this.maxErrorCount = options.maxErrorCount || 3; // Default to 1 errors before marking unhealthy
this.healthCheckInterval = options.healthCheckInterval || 30 * 60 * 1000; // Default to 30 minutes this.healthCheckInterval = options.healthCheckInterval || 10 * 60 * 1000; // Default to 10 minutes
// 优化1: 添加防抖机制,避免频繁的文件 I/O 操作 // 优化1: 添加防抖机制,避免频繁的文件 I/O 操作
this.saveDebounceTime = options.saveDebounceTime || 1000; // 默认1秒防抖 this.saveDebounceTime = options.saveDebounceTime || 1000; // 默认1秒防抖
@ -140,9 +140,9 @@ export class ProviderPoolManager {
} }
/** /**
* 禁用指定 * 禁用指定供商
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
* @param {object} providerConfig - 商配置 * @param {object} providerConfig - 供商配置
*/ */
disableProvider(providerType, providerConfig) { disableProvider(providerType, providerConfig) {
const pool = this.providerStatus[providerType]; const pool = this.providerStatus[providerType];
@ -159,9 +159,9 @@ export class ProviderPoolManager {
} }
/** /**
* 启用指定 * 启用指定供商
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
* @param {object} providerConfig - 商配置 * @param {object} providerConfig - 供商配置
*/ */
enableProvider(providerType, providerConfig) { enableProvider(providerType, providerConfig) {
const pool = this.providerStatus[providerType]; const pool = this.providerStatus[providerType];

View file

@ -628,7 +628,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
// 广播商更新事件 // 广播供商更新事件
broadcastEvent('provider_update', { broadcastEvent('provider_update', {
action: 'add', action: 'add',
providerType, providerType,
@ -1216,13 +1216,13 @@ async function scanConfigFiles(currentConfig, providerPoolManager) {
} }
} }
// 使用最新的商池数据 // 使用最新的供商池数据
let providerPools = currentConfig.providerPools; let providerPools = currentConfig.providerPools;
if (providerPoolManager && providerPoolManager.providerPools) { if (providerPoolManager && providerPoolManager.providerPools) {
providerPools = providerPoolManager.providerPools; providerPools = providerPoolManager.providerPools;
} }
// 检查商池文件中的所有OAuth凭据路径 - 标准化路径格式 // 检查供商池文件中的所有OAuth凭据路径 - 标准化路径格式
if (providerPools) { if (providerPools) {
for (const [providerType, providers] of Object.entries(providerPools)) { for (const [providerType, providers] of Object.entries(providerPools)) {
for (const provider of providers) { for (const provider of providers) {
@ -1409,7 +1409,7 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) {
}); });
} }
// 检查商池中的使用情况 // 检查供商池中的使用情况
if (currentConfig.providerPools) { if (currentConfig.providerPools) {
// 使用 flatMap 将双重循环优化为单层循环 O(n) // 使用 flatMap 将双重循环优化为单层循环 O(n)
const allProviders = Object.entries(currentConfig.providerPools).flatMap( const allProviders = Object.entries(currentConfig.providerPools).flatMap(
@ -1424,7 +1424,7 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) {
(pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH) || (pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH) ||
pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) {
providerUsages.push({ providerUsages.push({
type: '商池', type: '供商池',
location: `Gemini OAuth凭据 (节点${index + 1})`, location: `Gemini OAuth凭据 (节点${index + 1})`,
providerType: providerType, providerType: providerType,
providerIndex: index, providerIndex: index,
@ -1436,7 +1436,7 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) {
(pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH) || (pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH) ||
pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) {
providerUsages.push({ providerUsages.push({
type: '商池', type: '供商池',
location: `Kiro OAuth凭据 (节点${index + 1})`, location: `Kiro OAuth凭据 (节点${index + 1})`,
providerType: providerType, providerType: providerType,
providerIndex: index, providerIndex: index,
@ -1448,7 +1448,7 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) {
(pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH) || (pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH) ||
pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) {
providerUsages.push({ providerUsages.push({
type: '商池', type: '供商池',
location: `Qwen OAuth凭据 (节点${index + 1})`, location: `Qwen OAuth凭据 (节点${index + 1})`,
providerType: providerType, providerType: providerType,
providerIndex: index, providerIndex: index,

View file

@ -86,7 +86,7 @@ async function loadConfiguration() {
if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false; if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH || ''; if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH || '';
// 触发商配置显示 // 触发供商配置显示
handleProviderChange(); handleProviderChange();
// 根据Gemini凭据类型设置显示 // 根据Gemini凭据类型设置显示
@ -105,7 +105,7 @@ async function loadConfiguration() {
handleKiroCredsTypeChange({ target: kiroRadio }); handleKiroCredsTypeChange({ target: kiroRadio });
} }
// 检查并设置商池菜单显示状态 // 检查并设置供商池菜单显示状态
const providerPoolsFilePath = data.PROVIDER_POOLS_FILE_PATH; const providerPoolsFilePath = data.PROVIDER_POOLS_FILE_PATH;
const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]'); const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]');
if (providerPoolsFilePath && providerPoolsFilePath.trim() !== '') { if (providerPoolsFilePath && providerPoolsFilePath.trim() !== '') {
@ -131,7 +131,7 @@ async function saveConfiguration() {
systemPrompt: document.getElementById('systemPrompt')?.value || '', systemPrompt: document.getElementById('systemPrompt')?.value || '',
}; };
// 根据不同商保存不同的配置 // 根据不同供商保存不同的配置
const provider = document.getElementById('modelProvider')?.value; const provider = document.getElementById('modelProvider')?.value;
switch (provider) { switch (provider) {
@ -194,12 +194,12 @@ async function saveConfiguration() {
showToast('配置已保存', 'success'); showToast('配置已保存', 'success');
// 检查当前是否在商池管理页面,如果是则刷新数据 // 检查当前是否在供商池管理页面,如果是则刷新数据
const providersSection = document.getElementById('providers'); const providersSection = document.getElementById('providers');
if (providersSection && providersSection.classList.contains('active')) { if (providersSection && providersSection.classList.contains('active')) {
// 当前在商池页面,刷新数据 // 当前在供商池页面,刷新数据
await loadProviders(); await loadProviders();
showToast('商池数据已刷新', 'success'); showToast('供商池数据已刷新', 'success');
} }
} catch (error) { } catch (error) {
console.error('Failed to save configuration:', error); console.error('Failed to save configuration:', error);

View file

@ -66,7 +66,7 @@ function initEventListeners() {
button.addEventListener('click', handlePasswordToggle); button.addEventListener('click', handlePasswordToggle);
}); });
// 商池配置监听 // 供商池配置监听
const providerPoolsInput = document.getElementById('providerPoolsFilePath'); const providerPoolsInput = document.getElementById('providerPoolsFilePath');
if (providerPoolsInput) { if (providerPoolsInput) {
providerPoolsInput.addEventListener('input', handleProviderPoolsConfigChange); providerPoolsInput.addEventListener('input', handleProviderPoolsConfigChange);
@ -92,7 +92,7 @@ function initEventListeners() {
} }
/** /**
* 商配置切换处理 * 供商配置切换处理
*/ */
function handleProviderChange() { function handleProviderChange() {
const selectedProvider = elements.modelProvider?.value; const selectedProvider = elements.modelProvider?.value;
@ -100,12 +100,12 @@ function handleProviderChange() {
const allProviderConfigs = document.querySelectorAll('.provider-config'); const allProviderConfigs = document.querySelectorAll('.provider-config');
// 隐藏所有商配置 // 隐藏所有供商配置
allProviderConfigs.forEach(config => { allProviderConfigs.forEach(config => {
config.style.display = 'none'; config.style.display = 'none';
}); });
// 显示当前选中的商配置 // 显示当前选中的供商配置
const targetConfig = document.querySelector(`[data-provider="${selectedProvider}"]`); const targetConfig = document.querySelector(`[data-provider="${selectedProvider}"]`);
if (targetConfig) { if (targetConfig) {
targetConfig.style.display = 'block'; targetConfig.style.display = 'block';
@ -172,7 +172,7 @@ function handlePasswordToggle(event) {
} }
/** /**
* 商池配置变化处理 * 供商池配置变化处理
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
function handleProviderPoolsConfigChange(event) { function handleProviderPoolsConfigChange(event) {
@ -180,13 +180,13 @@ function handleProviderPoolsConfigChange(event) {
const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]'); const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]');
if (filePath) { if (filePath) {
// 显示商池菜单 // 显示供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'flex'; if (providersMenuItem) providersMenuItem.style.display = 'flex';
} else { } else {
// 隐藏商池菜单 // 隐藏供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'none'; if (providersMenuItem) providersMenuItem.style.display = 'none';
// 如果当前在商池页面,切换到仪表盘 // 如果当前在供商池页面,切换到仪表盘
if (providersMenuItem && providersMenuItem.classList.contains('active')) { if (providersMenuItem && providersMenuItem.classList.contains('active')) {
const dashboardItem = document.querySelector('.nav-item[data-section="dashboard"]'); const dashboardItem = document.querySelector('.nav-item[data-section="dashboard"]');
const dashboardSection = document.getElementById('dashboard'); const dashboardSection = document.getElementById('dashboard');

View file

@ -106,19 +106,19 @@ function updateProviderStatus(data) {
} }
/** /**
* 处理商更新事件 * 处理供商更新事件
* @param {Object} data - 更新数据 * @param {Object} data - 更新数据
*/ */
function handleProviderUpdate(data) { function handleProviderUpdate(data) {
if (data.action && data.providerType) { if (data.action && data.providerType) {
// 如果当前打开的模态框是更新事件的商类型,则刷新该模态框 // 如果当前打开的模态框是更新事件的供商类型,则刷新该模态框
const modal = document.querySelector('.provider-modal'); const modal = document.querySelector('.provider-modal');
if (modal && modal.getAttribute('data-provider-type') === data.providerType) { if (modal && modal.getAttribute('data-provider-type') === data.providerType) {
if (typeof refreshProviderConfig === 'function') { if (typeof refreshProviderConfig === 'function') {
refreshProviderConfig(data.providerType); refreshProviderConfig(data.providerType);
} }
} else { } else {
// 否则更新主界面的商列表 // 否则更新主界面的供商列表
if (typeof loadProviders === 'function') { if (typeof loadProviders === 'function') {
loadProviders(); loadProviders();
} }

View file

@ -4,8 +4,8 @@ import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js';
import { handleProviderPasswordToggle } from './event-handlers.js'; import { handleProviderPasswordToggle } from './event-handlers.js';
/** /**
* 显示商管理模态框 * 显示供商管理模态框
* @param {Object} data - 商数据 * @param {Object} data - 供商数据
*/ */
function showProviderManagerModal(data) { function showProviderManagerModal(data) {
const { providerType, providers, totalCount, healthyCount } = data; const { providerType, providers, totalCount, healthyCount } = data;
@ -27,7 +27,7 @@ function showProviderManagerModal(data) {
modal.innerHTML = ` modal.innerHTML = `
<div class="provider-modal-content"> <div class="provider-modal-content">
<div class="provider-modal-header"> <div class="provider-modal-header">
<h3><i class="fas fa-cogs"></i> ${providerType} </h3> <h3><i class="fas fa-cogs"></i> ${providerType} </h3>
<button class="modal-close" onclick="window.closeProviderModal(this)"> <button class="modal-close" onclick="window.closeProviderModal(this)">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@ -44,7 +44,7 @@ function showProviderManagerModal(data) {
</div> </div>
<div class="provider-summary-actions"> <div class="provider-summary-actions">
<button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')"> <button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
</div> </div>
</div> </div>
@ -149,8 +149,8 @@ function closeProviderModal(button) {
} }
/** /**
* 渲染商列表 * 渲染供商列表
* @param {Array} providers - 商数组 * @param {Array} providers - 供商数组
* @returns {string} HTML字符串 * @returns {string} HTML字符串
*/ */
function renderProviderList(providers) { function renderProviderList(providers) {
@ -188,7 +188,7 @@ function renderProviderList(providers) {
</div> </div>
</div> </div>
<div class="provider-actions-group"> <div class="provider-actions-group">
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${provider.uuid}', event)" title="${toggleButtonText}此商"> <button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${provider.uuid}', event)" title="${toggleButtonText}此供商">
<i class="${toggleButtonIcon}"></i> ${toggleButtonText} <i class="${toggleButtonIcon}"></i> ${toggleButtonText}
</button> </button>
<button class="btn-small btn-edit" onclick="window.editProvider('${provider.uuid}', event)"> <button class="btn-small btn-edit" onclick="window.editProvider('${provider.uuid}', event)">
@ -210,8 +210,8 @@ function renderProviderList(providers) {
} }
/** /**
* 渲染商配置 * 渲染供商配置
* @param {Object} provider - 商对象 * @param {Object} provider - 供商对象
* @returns {string} HTML字符串 * @returns {string} HTML字符串
*/ */
function renderProviderConfig(provider) { function renderProviderConfig(provider) {
@ -385,7 +385,7 @@ function renderProviderConfig(provider) {
/** /**
* 获取字段显示顺序 * 获取字段显示顺序
* @param {Object} provider - 商对象 * @param {Object} provider - 供商对象
* @returns {Array} 字段键数组 * @returns {Array} 字段键数组
*/ */
function getFieldOrder(provider) { function getFieldOrder(provider) {
@ -405,8 +405,8 @@ function getFieldOrder(provider) {
} }
/** /**
* 切换商详情显示 * 切换供商详情显示
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
*/ */
function toggleProviderDetails(uuid) { function toggleProviderDetails(uuid) {
const content = document.getElementById(`content-${uuid}`); const content = document.getElementById(`content-${uuid}`);
@ -416,8 +416,8 @@ function toggleProviderDetails(uuid) {
} }
/** /**
* 编辑 * 编辑供商
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
function editProvider(uuid, event) { function editProvider(uuid, event) {
@ -465,7 +465,7 @@ function editProvider(uuid, event) {
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning'; const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
actionsGroup.innerHTML = ` actionsGroup.innerHTML = `
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此商"> <button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此供商">
<i class="${toggleButtonIcon}"></i> ${toggleButtonText} <i class="${toggleButtonIcon}"></i> ${toggleButtonText}
</button> </button>
<button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)"> <button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)">
@ -480,7 +480,7 @@ function editProvider(uuid, event) {
/** /**
* 取消编辑 * 取消编辑
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
function cancelEdit(uuid, event) { function cancelEdit(uuid, event) {
@ -523,7 +523,7 @@ function cancelEdit(uuid, event) {
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning'; const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
actionsGroup.innerHTML = ` actionsGroup.innerHTML = `
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此商"> <button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此供商">
<i class="${toggleButtonIcon}"></i> ${toggleButtonText} <i class="${toggleButtonIcon}"></i> ${toggleButtonText}
</button> </button>
<button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)"> <button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)">
@ -536,8 +536,8 @@ function cancelEdit(uuid, event) {
} }
/** /**
* 保存 * 保存供商
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
async function saveProvider(uuid, event) { async function saveProvider(uuid, event) {
@ -564,8 +564,8 @@ async function saveProvider(uuid, event) {
try { try {
await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig }); await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig });
showToast('商配置更新成功', 'success'); showToast('供商配置更新成功', 'success');
// 重新获取该商类型的最新配置 // 重新获取该供商类型的最新配置
await refreshProviderConfig(providerType); await refreshProviderConfig(providerType);
} catch (error) { } catch (error) {
console.error('Failed to update provider:', error); console.error('Failed to update provider:', error);
@ -574,14 +574,14 @@ async function saveProvider(uuid, event) {
} }
/** /**
* 删除 * 删除供商
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
async function deleteProvider(uuid, event) { async function deleteProvider(uuid, event) {
event.stopPropagation(); event.stopPropagation();
if (!confirm('确定要删除这个商配置吗?此操作不可恢复。')) { if (!confirm('确定要删除这个供商配置吗?此操作不可恢复。')) {
return; return;
} }
@ -590,7 +590,7 @@ async function deleteProvider(uuid, event) {
try { try {
await window.apiClient.delete(`/providers/${encodeURIComponent(providerType)}/${uuid}`); await window.apiClient.delete(`/providers/${encodeURIComponent(providerType)}/${uuid}`);
showToast('商配置删除成功', 'success'); showToast('供商配置删除成功', 'success');
// 重新获取最新配置 // 重新获取最新配置
await refreshProviderConfig(providerType); await refreshProviderConfig(providerType);
} catch (error) { } catch (error) {
@ -600,15 +600,15 @@ async function deleteProvider(uuid, event) {
} }
/** /**
* 重新获取并刷新商配置 * 重新获取并刷新供商配置
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
*/ */
async function refreshProviderConfig(providerType) { async function refreshProviderConfig(providerType) {
try { try {
// 重新获取该商类型的最新数据 // 重新获取该供商类型的最新数据
const data = await window.apiClient.get(`/providers/${encodeURIComponent(providerType)}`); const data = await window.apiClient.get(`/providers/${encodeURIComponent(providerType)}`);
// 如果当前显示的是该商类型的模态框,则更新模态框 // 如果当前显示的是该供商类型的模态框,则更新模态框
const modal = document.querySelector('.provider-modal'); const modal = document.querySelector('.provider-modal');
if (modal && modal.getAttribute('data-provider-type') === providerType) { if (modal && modal.getAttribute('data-provider-type') === providerType) {
// 更新统计信息 // 更新统计信息
@ -622,14 +622,14 @@ async function refreshProviderConfig(providerType) {
healthyCountElement.textContent = data.healthyCount; healthyCountElement.textContent = data.healthyCount;
} }
// 重新渲染商列表 // 重新渲染供商列表
const providerList = modal.querySelector('.provider-list'); const providerList = modal.querySelector('.provider-list');
if (providerList) { if (providerList) {
providerList.innerHTML = renderProviderList(data.providers); providerList.innerHTML = renderProviderList(data.providers);
} }
} }
// 同时更新主界面的商统计数据 // 同时更新主界面的供商统计数据
if (typeof window.loadProviders === 'function') { if (typeof window.loadProviders === 'function') {
await window.loadProviders(); await window.loadProviders();
} }
@ -640,8 +640,8 @@ async function refreshProviderConfig(providerType) {
} }
/** /**
* 显示添加商表单 * 显示添加供商表单
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
*/ */
function showAddProviderForm(providerType) { function showAddProviderForm(providerType) {
const modal = document.querySelector('.provider-modal'); const modal = document.querySelector('.provider-modal');
@ -655,7 +655,7 @@ function showAddProviderForm(providerType) {
const form = document.createElement('div'); const form = document.createElement('div');
form.className = 'add-provider-form'; form.className = 'add-provider-form';
form.innerHTML = ` form.innerHTML = `
<h4><i class="fas fa-plus"></i> </h4> <h4><i class="fas fa-plus"></i> </h4>
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label>检查模型名称 <span class="optional-mark">(选填)</span></label> <label>检查模型名称 <span class="optional-mark">(选填)</span></label>
@ -696,7 +696,7 @@ function showAddProviderForm(providerType) {
/** /**
* 添加动态配置字段 * 添加动态配置字段
* @param {HTMLElement} form - 表单元素 * @param {HTMLElement} form - 表单元素
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
*/ */
function addDynamicConfigFields(form, providerType) { function addDynamicConfigFields(form, providerType) {
const configFields = form.querySelector('#dynamicConfigFields'); const configFields = form.querySelector('#dynamicConfigFields');
@ -802,7 +802,7 @@ function addDynamicConfigFields(form, providerType) {
} }
/** /**
* 为添加新商表单中的密码切换按钮绑定事件监听器 * 为添加新供商表单中的密码切换按钮绑定事件监听器
* @param {HTMLElement} form - 表单元素 * @param {HTMLElement} form - 表单元素
*/ */
function bindAddFormPasswordToggleListeners(form) { function bindAddFormPasswordToggleListeners(form) {
@ -827,8 +827,8 @@ function bindAddFormPasswordToggleListeners(form) {
} }
/** /**
* 添加新 * 添加新供商
* @param {string} providerType - 商类型 * @param {string} providerType - 供商类型
*/ */
async function addProvider(providerType) { async function addProvider(providerType) {
const checkModelName = document.getElementById('newCheckModelName')?.value; const checkModelName = document.getElementById('newCheckModelName')?.value;
@ -870,7 +870,7 @@ async function addProvider(providerType) {
providerType, providerType,
providerConfig providerConfig
}); });
showToast('商配置添加成功', 'success'); showToast('供商配置添加成功', 'success');
// 移除添加表单 // 移除添加表单
const form = document.querySelector('.add-provider-form'); const form = document.querySelector('.add-provider-form');
if (form) { if (form) {
@ -885,8 +885,8 @@ async function addProvider(providerType) {
} }
/** /**
* 切换商禁用/启用状态 * 切换供商禁用/启用状态
* @param {string} uuid - 商UUID * @param {string} uuid - 供商UUID
* @param {Event} event - 事件对象 * @param {Event} event - 事件对象
*/ */
async function toggleProviderStatus(uuid, event) { async function toggleProviderStatus(uuid, event) {
@ -896,12 +896,12 @@ async function toggleProviderStatus(uuid, event) {
const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type'); const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type');
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`); const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
// 获取当前商信息 // 获取当前供商信息
const isCurrentlyDisabled = currentProvider.classList.contains('disabled'); const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
const action = isCurrentlyDisabled ? 'enable' : 'disable'; const action = isCurrentlyDisabled ? 'enable' : 'disable';
const confirmMessage = isCurrentlyDisabled ? const confirmMessage = isCurrentlyDisabled ?
`确定要启用这个商配置吗?` : `确定要启用这个供商配置吗?` :
`确定要禁用这个商配置吗?禁用后该供商将不会被选中使用。`; `确定要禁用这个供商配置吗?禁用后该供商将不会被选中使用。`;
if (!confirm(confirmMessage)) { if (!confirm(confirmMessage)) {
return; return;
@ -909,8 +909,8 @@ async function toggleProviderStatus(uuid, event) {
try { try {
await window.apiClient.post(`/providers/${encodeURIComponent(providerType)}/${uuid}/${action}`, { action }); await window.apiClient.post(`/providers/${encodeURIComponent(providerType)}/${uuid}/${action}`, { action });
showToast(`${isCurrentlyDisabled ? '启用' : '禁用'}成功`, 'success'); showToast(`供商${isCurrentlyDisabled ? '启用' : '禁用'}成功`, 'success');
// 重新获取该商类型的最新配置 // 重新获取该供商类型的最新配置
await refreshProviderConfig(providerType); await refreshProviderConfig(providerType);
} catch (error) { } catch (error) {
console.error('Failed to toggle provider status:', error); console.error('Failed to toggle provider status:', error);

View file

@ -1,4 +1,4 @@
// 商管理功能模块 // 供商管理功能模块
import { providerStats, updateProviderStats } from './constants.js'; import { providerStats, updateProviderStats } from './constants.js';
import { showToast } from './utils.js'; import { showToast } from './utils.js';
@ -96,7 +96,7 @@ function renderProviders(providers) {
container.innerHTML = ''; container.innerHTML = '';
// 检查是否有商池数据 // 检查是否有供商池数据
const hasProviders = Object.keys(providers).length > 0; const hasProviders = Object.keys(providers).length > 0;
const statsGrid = document.querySelector('#providers .stats-grid'); const statsGrid = document.querySelector('#providers .stats-grid');
@ -114,10 +114,10 @@ function renderProviders(providers) {
]; ];
// 获取所有提供商类型并按指定顺序排序 // 获取所有提供商类型并按指定顺序排序
// 优先显示预定义的所有商类型,即使某些供商没有数据也要显示 // 优先显示预定义的所有供商类型,即使某些供商没有数据也要显示
let allProviderTypes; let allProviderTypes;
if (hasProviders) { if (hasProviders) {
// 合并预定义类型和实际存在的类型,确保显示所有预定义 // 合并预定义类型和实际存在的类型,确保显示所有预定义供商
const actualProviderTypes = Object.keys(providers); const actualProviderTypes = Object.keys(providers);
allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])]; allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])];
} else { } else {
@ -205,7 +205,7 @@ function renderProviders(providers) {
providerDiv.classList.add('empty-provider'); providerDiv.classList.add('empty-provider');
} }
// 添加点击事件 - 整个商组都可以点击 // 添加点击事件 - 整个供商组都可以点击
providerDiv.addEventListener('click', (e) => { providerDiv.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
openProviderManager(providerType); openProviderManager(providerType);
@ -286,7 +286,7 @@ function updateProviderStatsDisplay(activeProviders, healthyProviders, totalAcco
} }
/** /**
* 打开商管理模态框 * 打开供商管理模态框
* @param {string} providerType - 提供商类型 * @param {string} providerType - 提供商类型
*/ */
async function openProviderManager(providerType) { async function openProviderManager(providerType) {
@ -296,7 +296,7 @@ async function openProviderManager(providerType) {
showProviderManagerModal(data); showProviderManagerModal(data);
} catch (error) { } catch (error) {
console.error('Failed to load provider details:', error); console.error('Failed to load provider details:', error);
showToast('加载商详情失败', 'error'); showToast('加载供商详情失败', 'error');
} }
} }

View file

@ -450,7 +450,7 @@ textarea.form-control {
cursor: pointer; cursor: pointer;
} }
/* 商配置组 */ /* 供商配置组 */
.provider-config { .provider-config {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.5rem; border-radius: 0.5rem;
@ -1191,7 +1191,7 @@ input:checked + .toggle-slider:before {
font-style: italic; font-style: italic;
} }
/* 商类型显示 */ /* 供商类型显示 */
.provider-type-text { .provider-type-text {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
@ -2288,7 +2288,7 @@ input:checked + .toggle-slider:before {
background: var(--success-color); background: var(--success-color);
} }
/* 商池关联的特殊样式 */ /* 供商池关联的特殊样式 */
.usage-detail-item[data-usage-type="provider_pool"] { .usage-detail-item[data-usage-type="provider_pool"] {
background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%);
border-color: #0ea5e9; border-color: #0ea5e9;
@ -2700,7 +2700,7 @@ input:checked + .toggle-slider:before {
} }
} }
/* 禁用商状态样式 */ /* 禁用供商状态样式 */
.provider-item-detail.disabled { .provider-item-detail.disabled {
opacity: 0.6; opacity: 0.6;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
@ -2776,7 +2776,7 @@ input:checked + .toggle-slider:before {
transform: translateY(0); transform: translateY(0);
} }
/* 商状态指示器 */ /* 供商状态指示器 */
.provider-status .disabled-indicator { .provider-status .disabled-indicator {
position: relative; position: relative;
} }

View file

@ -169,7 +169,7 @@ function generateUsageInfoHtml(config) {
const typeLabels = { const typeLabels = {
'main_config': '主要配置', 'main_config': '主要配置',
'provider_pool': '商池', 'provider_pool': '供商池',
'multiple': '多种用途' 'multiple': '多种用途'
}; };
@ -567,7 +567,7 @@ function showDeleteConfirmModal(config) {
<ul> <ul>
<li>相关的AI服务无法正常工作</li> <li>相关的AI服务无法正常工作</li>
<li>配置管理中的设置失效</li> <li>配置管理中的设置失效</li>
<li>商池配置丢失</li> <li>供商池配置丢失</li>
</ul> </ul>
<p><strong>建议</strong></p> <p><strong>建议</strong></p>
</div> </div>

View file

@ -42,8 +42,8 @@
<a href="#config" class="nav-item" data-section="config" aria-label="配置管理"> <a href="#config" class="nav-item" data-section="config" aria-label="配置管理">
<i class="fas fa-cog" aria-hidden="true"></i> <span>配置管理</span> <i class="fas fa-cog" aria-hidden="true"></i> <span>配置管理</span>
</a> </a>
<a href="#providers" class="nav-item" data-section="providers" aria-label="商池管理"> <a href="#providers" class="nav-item" data-section="providers" aria-label="供商池管理">
<i class="fas fa-network-wired" aria-hidden="true"></i> <span>商池管理</span> <i class="fas fa-network-wired" aria-hidden="true"></i> <span>供商池管理</span>
</a> </a>
<a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="上传配置管理"> <a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="上传配置管理">
<i class="fas fa-upload" aria-hidden="true"></i> <span>上传配置管理</span> <i class="fas fa-upload" aria-hidden="true"></i> <span>上传配置管理</span>
@ -602,9 +602,9 @@
</div> </div>
<div class="form-group pool-section"> <div class="form-group pool-section">
<label for="providerPoolsFilePath">商池配置文件路径</label> <label for="providerPoolsFilePath">供商池配置文件路径</label>
<input type="text" id="providerPoolsFilePath" class="form-control" value="" placeholder="例如: provider_pools.json"> <input type="text" id="providerPoolsFilePath" class="form-control" value="" placeholder="例如: provider_pools.json">
<small class="form-text">配置了供商池后,可在供商池管理中查看详细信息</small> <small class="form-text">配置了供商池后,可在供商池管理中查看详细信息</small>
</div> </div>
<!-- 系统提示配置移到最下面 --> <!-- 系统提示配置移到最下面 -->
@ -680,7 +680,7 @@
<!-- Provider Pools Section --> <!-- Provider Pools Section -->
<section id="providers" class="section" aria-labelledby="providers-title"> <section id="providers" class="section" aria-labelledby="providers-title">
<h2 id="providers-title">商池管理</h2> <h2 id="providers-title">供商池管理</h2>
<!-- Provider Pool Stats --> <!-- Provider Pool Stats -->
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
@ -764,7 +764,38 @@
} }
}); });
} }
// 更新路径路由示例中的URL前缀
updateRoutingExamplesURLs();
})(); })();
// 更新路径路由示例中的URL前缀
function updateRoutingExamplesURLs() {
// 获取当前页面的基础URL去掉index.html
const currentURL = window.location.href;
const baseURL = currentURL.replace(/\/index\.html$/, '');
// 更新所有端点路径
const endpointPaths = document.querySelectorAll('.endpoint-path');
endpointPaths.forEach(element => {
const originalPath = element.textContent;
if (!originalPath.startsWith(baseURL)) {
// 确保baseURL不以斜杠结尾然后正确拼接路径
const cleanBaseURL = baseURL.replace(/\/$/, '');
const cleanPath = originalPath.startsWith('/') ? originalPath : '/' + originalPath;
element.textContent = cleanBaseURL + cleanPath;
}
});
// 更新curl命令中的URL
const curlCodes = document.querySelectorAll('.usage-example pre code');
curlCodes.forEach(element => {
const curlCommand = element.textContent;
// 替换curl命令中的http://localhost:3000部分
const updatedCommand = curlCommand.replace(/curl http:\/\/localhost:3000/g, `curl ${baseURL}`);
element.textContent = updatedCommand;
});
}
</script> </script>
<script type="module" src="app/app.js"></script> <script type="module" src="app/app.js"></script>
</body> </body>