1100 lines
No EOL
44 KiB
JavaScript
1100 lines
No EOL
44 KiB
JavaScript
// 模态框管理模块
|
||
|
||
import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js';
|
||
import { handleProviderPasswordToggle } from './event-handlers.js';
|
||
|
||
/**
|
||
* 显示提供商管理模态框
|
||
* @param {Object} data - 提供商数据
|
||
*/
|
||
function showProviderManagerModal(data) {
|
||
const { providerType, providers, totalCount, healthyCount } = data;
|
||
|
||
// 移除已存在的模态框
|
||
const existingModal = document.querySelector('.provider-modal');
|
||
if (existingModal) {
|
||
// 清理事件监听器
|
||
if (existingModal.cleanup) {
|
||
existingModal.cleanup();
|
||
}
|
||
existingModal.remove();
|
||
}
|
||
|
||
// 创建模态框
|
||
const modal = document.createElement('div');
|
||
modal.className = 'provider-modal';
|
||
modal.setAttribute('data-provider-type', providerType);
|
||
modal.innerHTML = `
|
||
<div class="provider-modal-content">
|
||
<div class="provider-modal-header">
|
||
<h3><i class="fas fa-cogs"></i> 管理 ${providerType} 提供商配置</h3>
|
||
<button class="modal-close" onclick="window.closeProviderModal(this)">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
<div class="provider-modal-body">
|
||
<div class="provider-summary">
|
||
<div class="provider-summary-item">
|
||
<span class="label">总账户数:</span>
|
||
<span class="value">${totalCount}</span>
|
||
</div>
|
||
<div class="provider-summary-item">
|
||
<span class="label">健康账户:</span>
|
||
<span class="value">${healthyCount}</span>
|
||
</div>
|
||
<div class="provider-summary-actions">
|
||
<button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')">
|
||
<i class="fas fa-plus"></i> 添加新提供商
|
||
</button>
|
||
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" title="将所有节点的健康状态重置为健康">
|
||
<i class="fas fa-heartbeat"></i> 重置为健康
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="provider-list" id="providerList">
|
||
${renderProviderList(providers)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 添加到页面
|
||
document.body.appendChild(modal);
|
||
|
||
// 添加模态框事件监听
|
||
addModalEventListeners(modal);
|
||
|
||
// 先获取该提供商类型的模型列表(只调用一次API)
|
||
loadModelsForProviderType(providerType, providers);
|
||
}
|
||
|
||
/**
|
||
* 为提供商类型加载模型列表(优化:只调用一次API)
|
||
* @param {string} providerType - 提供商类型
|
||
* @param {Array} providers - 提供商列表
|
||
*/
|
||
async function loadModelsForProviderType(providerType, providers) {
|
||
try {
|
||
// 只调用一次API获取模型列表
|
||
const response = await window.apiClient.get(`/provider-models/${encodeURIComponent(providerType)}`);
|
||
const models = response.models || [];
|
||
|
||
// 为每个提供商渲染模型选择器
|
||
providers.forEach(provider => {
|
||
renderNotSupportedModelsSelector(provider.uuid, models, provider.notSupportedModels || []);
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to load models for provider type:', error);
|
||
// 如果加载失败,为每个提供商显示错误信息
|
||
providers.forEach(provider => {
|
||
const container = document.querySelector(`.not-supported-models-container[data-uuid="${provider.uuid}"]`);
|
||
if (container) {
|
||
container.innerHTML = '<div class="error-message">加载模型列表失败</div>';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为模态框添加事件监听器
|
||
* @param {HTMLElement} modal - 模态框元素
|
||
*/
|
||
function addModalEventListeners(modal) {
|
||
// ESC键关闭模态框
|
||
const handleEscKey = (event) => {
|
||
if (event.key === 'Escape') {
|
||
modal.remove();
|
||
document.removeEventListener('keydown', handleEscKey);
|
||
}
|
||
};
|
||
|
||
// 点击背景关闭模态框
|
||
const handleBackgroundClick = (event) => {
|
||
if (event.target === modal) {
|
||
modal.remove();
|
||
document.removeEventListener('keydown', handleEscKey);
|
||
}
|
||
};
|
||
|
||
// 防止模态框内容区域点击时关闭模态框
|
||
const modalContent = modal.querySelector('.provider-modal-content');
|
||
const handleContentClick = (event) => {
|
||
event.stopPropagation();
|
||
};
|
||
|
||
// 密码切换按钮事件处理
|
||
const handlePasswordToggleClick = (event) => {
|
||
const button = event.target.closest('.password-toggle');
|
||
if (button) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
handleProviderPasswordToggle(button);
|
||
}
|
||
};
|
||
|
||
// 上传按钮事件处理
|
||
const handleUploadButtonClick = (event) => {
|
||
const button = event.target.closest('.upload-btn');
|
||
if (button) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const targetInputId = button.getAttribute('data-target');
|
||
const providerType = modal.getAttribute('data-provider-type');
|
||
if (targetInputId && window.fileUploadHandler) {
|
||
window.fileUploadHandler.handleFileUpload(button, targetInputId, providerType);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 添加事件监听器
|
||
document.addEventListener('keydown', handleEscKey);
|
||
modal.addEventListener('click', handleBackgroundClick);
|
||
if (modalContent) {
|
||
modalContent.addEventListener('click', handleContentClick);
|
||
modalContent.addEventListener('click', handlePasswordToggleClick);
|
||
modalContent.addEventListener('click', handleUploadButtonClick);
|
||
}
|
||
|
||
// 清理函数,在模态框关闭时调用
|
||
modal.cleanup = () => {
|
||
document.removeEventListener('keydown', handleEscKey);
|
||
modal.removeEventListener('click', handleBackgroundClick);
|
||
if (modalContent) {
|
||
modalContent.removeEventListener('click', handleContentClick);
|
||
modalContent.removeEventListener('click', handlePasswordToggleClick);
|
||
modalContent.removeEventListener('click', handleUploadButtonClick);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 关闭模态框并清理事件监听器
|
||
* @param {HTMLElement} button - 关闭按钮
|
||
*/
|
||
function closeProviderModal(button) {
|
||
const modal = button.closest('.provider-modal');
|
||
if (modal) {
|
||
if (modal.cleanup) {
|
||
modal.cleanup();
|
||
}
|
||
modal.remove();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染提供商列表
|
||
* @param {Array} providers - 提供商数组
|
||
* @returns {string} HTML字符串
|
||
*/
|
||
function renderProviderList(providers) {
|
||
return providers.map(provider => {
|
||
const isHealthy = provider.isHealthy;
|
||
const isDisabled = provider.isDisabled || false;
|
||
const lastUsed = provider.lastUsed ? new Date(provider.lastUsed).toLocaleString() : '从未使用';
|
||
const healthClass = isHealthy ? 'healthy' : 'unhealthy';
|
||
const disabledClass = isDisabled ? 'disabled' : '';
|
||
const healthIcon = isHealthy ? 'fas fa-check-circle text-success' : 'fas fa-exclamation-triangle text-warning';
|
||
const healthText = isHealthy ? '正常' : '异常';
|
||
const disabledText = isDisabled ? '已禁用' : '已启用';
|
||
const disabledIcon = isDisabled ? 'fas fa-ban text-muted' : 'fas fa-play text-success';
|
||
const toggleButtonText = isDisabled ? '启用' : '禁用';
|
||
const toggleButtonIcon = isDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||
const toggleButtonClass = isDisabled ? 'btn-success' : 'btn-warning';
|
||
|
||
return `
|
||
<div class="provider-item-detail ${healthClass} ${disabledClass}" data-uuid="${provider.uuid}">
|
||
<div class="provider-item-header" onclick="window.toggleProviderDetails('${provider.uuid}')">
|
||
<div class="provider-info">
|
||
<div class="provider-name">${provider.uuid}</div>
|
||
<div class="provider-meta">
|
||
<span class="health-status">
|
||
<i class="${healthIcon}"></i>
|
||
健康状态: ${healthText}
|
||
</span> |
|
||
<span class="disabled-status">
|
||
<i class="${disabledIcon}"></i>
|
||
状态: ${disabledText}
|
||
</span> |
|
||
使用次数: ${provider.usageCount || 0} |
|
||
失败次数: ${provider.errorCount || 0} |
|
||
最后使用: ${lastUsed}
|
||
</div>
|
||
</div>
|
||
<div class="provider-actions-group">
|
||
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${provider.uuid}', event)" title="${toggleButtonText}此提供商">
|
||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||
</button>
|
||
<button class="btn-small btn-edit" onclick="window.editProvider('${provider.uuid}', event)">
|
||
<i class="fas fa-edit"></i> 编辑
|
||
</button>
|
||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${provider.uuid}', event)">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="provider-item-content" id="content-${provider.uuid}">
|
||
<div class="">
|
||
${renderProviderConfig(provider)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
/**
|
||
* 渲染提供商配置
|
||
* @param {Object} provider - 提供商对象
|
||
* @returns {string} HTML字符串
|
||
*/
|
||
function renderProviderConfig(provider) {
|
||
// 获取字段映射,确保顺序一致
|
||
const fieldOrder = getFieldOrder(provider);
|
||
|
||
// 先渲染基础配置字段(checkModelName 和 checkHealth)
|
||
let html = '<div class="form-grid">';
|
||
const baseFields = ['checkModelName', 'checkHealth'];
|
||
|
||
baseFields.forEach(fieldKey => {
|
||
const displayLabel = getFieldLabel(fieldKey);
|
||
const value = provider[fieldKey];
|
||
const displayValue = value || '';
|
||
|
||
// 如果是 checkHealth 字段,使用下拉选择框
|
||
if (fieldKey === 'checkHealth') {
|
||
// 如果没有值,默认为 false
|
||
const actualValue = value !== undefined ? value : false;
|
||
const isEnabled = actualValue === true || actualValue === 'true';
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${displayLabel}</label>
|
||
<select class="form-control"
|
||
data-config-key="${fieldKey}"
|
||
data-config-value="${actualValue}"
|
||
disabled>
|
||
<option value="true" ${isEnabled ? 'selected' : ''}>启用</option>
|
||
<option value="false" ${!isEnabled ? 'selected' : ''}>禁用</option>
|
||
</select>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// checkModelName 字段始终显示
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${displayLabel}</label>
|
||
<input type="text"
|
||
value="${displayValue}"
|
||
readonly
|
||
data-config-key="${fieldKey}"
|
||
data-config-value="${value || ''}">
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
html += '</div>';
|
||
|
||
// 渲染其他配置字段,每行2列
|
||
const otherFields = fieldOrder.filter(key => !baseFields.includes(key));
|
||
|
||
for (let i = 0; i < otherFields.length; i += 2) {
|
||
html += '<div class="form-grid">';
|
||
|
||
const field1Key = otherFields[i];
|
||
const field1Label = getFieldLabel(field1Key);
|
||
const field1Value = provider[field1Key];
|
||
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
|
||
const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH');
|
||
const field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : (field1Value || '');
|
||
|
||
if (field1IsPassword) {
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field1Label}</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password"
|
||
value="${field1DisplayValue}"
|
||
readonly
|
||
data-config-key="${field1Key}"
|
||
data-config-value="${field1Value || ''}">
|
||
<button type="button" class="password-toggle" data-target="${field1Key}">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (field1IsOAuthFilePath) {
|
||
// OAuth凭据文件路径字段,添加上传按钮
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field1Label}</label>
|
||
<div class="file-input-group">
|
||
<input type="text"
|
||
id="edit-${provider.uuid}-${field1Key}"
|
||
value="${field1Value || ''}"
|
||
readonly
|
||
data-config-key="${field1Key}"
|
||
data-config-value="${field1Value || ''}">
|
||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
|
||
<i class="fas fa-upload"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field1Label}</label>
|
||
<input type="text"
|
||
value="${field1DisplayValue}"
|
||
readonly
|
||
data-config-key="${field1Key}"
|
||
data-config-value="${field1Value || ''}">
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 如果有第二个字段
|
||
if (i + 1 < otherFields.length) {
|
||
const field2Key = otherFields[i + 1];
|
||
const field2Label = getFieldLabel(field2Key);
|
||
const field2Value = provider[field2Key];
|
||
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
|
||
const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH');
|
||
const field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : (field2Value || '');
|
||
|
||
if (field2IsPassword) {
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field2Label}</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password"
|
||
value="${field2DisplayValue}"
|
||
readonly
|
||
data-config-key="${field2Key}"
|
||
data-config-value="${field2Value || ''}">
|
||
<button type="button" class="password-toggle" data-target="${field2Key}">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (field2IsOAuthFilePath) {
|
||
// OAuth凭据文件路径字段,添加上传按钮
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field2Label}</label>
|
||
<div class="file-input-group">
|
||
<input type="text"
|
||
id="edit-${provider.uuid}-${field2Key}"
|
||
value="${field2Value || ''}"
|
||
readonly
|
||
data-config-key="${field2Key}"
|
||
data-config-value="${field2Value || ''}">
|
||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field2Key}" aria-label="上传文件" disabled>
|
||
<i class="fas fa-upload"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
html += `
|
||
<div class="config-item">
|
||
<label>${field2Label}</label>
|
||
<input type="text"
|
||
value="${field2DisplayValue}"
|
||
readonly
|
||
data-config-key="${field2Key}"
|
||
data-config-value="${field2Value || ''}">
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
html += '</div>';
|
||
}
|
||
|
||
// 添加 notSupportedModels 配置区域
|
||
html += '<div class="form-grid full-width">';
|
||
html += `
|
||
<div class="config-item not-supported-models-section">
|
||
<label>
|
||
<i class="fas fa-ban"></i> 不支持的模型
|
||
<span class="help-text">选择此提供商不支持的模型,系统会自动排除这些模型</span>
|
||
</label>
|
||
<div class="not-supported-models-container" data-uuid="${provider.uuid}">
|
||
<div class="models-loading">
|
||
<i class="fas fa-spinner fa-spin"></i> 加载模型列表...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* 获取字段显示顺序
|
||
* @param {Object} provider - 提供商对象
|
||
* @returns {Array} 字段键数组
|
||
*/
|
||
function getFieldOrder(provider) {
|
||
const orderedFields = ['checkModelName', 'checkHealth'];
|
||
|
||
// 获取所有其他配置项
|
||
const otherFields = Object.keys(provider).filter(key =>
|
||
key !== 'isHealthy' && key !== 'lastUsed' && key !== 'usageCount' &&
|
||
key !== 'errorCount' && key !== 'lastErrorTime' && key !== 'uuid' &&
|
||
key !== 'isDisabled' && !orderedFields.includes(key)
|
||
);
|
||
|
||
// 按字母顺序排序其他字段
|
||
otherFields.sort();
|
||
|
||
return [...orderedFields, ...otherFields].filter(key => provider.hasOwnProperty(key));
|
||
}
|
||
|
||
/**
|
||
* 切换提供商详情显示
|
||
* @param {string} uuid - 提供商UUID
|
||
*/
|
||
function toggleProviderDetails(uuid) {
|
||
const content = document.getElementById(`content-${uuid}`);
|
||
if (content) {
|
||
content.classList.toggle('expanded');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 编辑提供商
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Event} event - 事件对象
|
||
*/
|
||
function editProvider(uuid, event) {
|
||
event.stopPropagation();
|
||
|
||
const providerDetail = event.target.closest('.provider-item-detail');
|
||
const configInputs = providerDetail.querySelectorAll('input[data-config-key]');
|
||
const configSelects = providerDetail.querySelectorAll('select[data-config-key]');
|
||
const content = providerDetail.querySelector(`#content-${uuid}`);
|
||
|
||
// 如果还没有展开,则自动展开编辑框
|
||
if (content && !content.classList.contains('expanded')) {
|
||
toggleProviderDetails(uuid);
|
||
}
|
||
|
||
// 等待一小段时间让展开动画完成,然后切换输入框为可编辑状态
|
||
setTimeout(() => {
|
||
// 切换输入框为可编辑状态
|
||
configInputs.forEach(input => {
|
||
input.readOnly = false;
|
||
if (input.type === 'password') {
|
||
const actualValue = input.dataset.configValue;
|
||
input.value = actualValue;
|
||
}
|
||
});
|
||
|
||
// 启用文件上传按钮
|
||
const uploadButtons = providerDetail.querySelectorAll('.upload-btn');
|
||
uploadButtons.forEach(button => {
|
||
button.disabled = false;
|
||
});
|
||
|
||
// 启用下拉选择框
|
||
configSelects.forEach(select => {
|
||
select.disabled = false;
|
||
});
|
||
|
||
// 启用模型复选框
|
||
const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox');
|
||
modelCheckboxes.forEach(checkbox => {
|
||
checkbox.disabled = false;
|
||
});
|
||
|
||
// 添加编辑状态类
|
||
providerDetail.classList.add('editing');
|
||
|
||
// 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮
|
||
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
||
const toggleButton = actionsGroup.querySelector('[onclick*="toggleProviderStatus"]');
|
||
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
|
||
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
|
||
const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用';
|
||
const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
|
||
|
||
actionsGroup.innerHTML = `
|
||
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此提供商">
|
||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||
</button>
|
||
<button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)">
|
||
<i class="fas fa-save"></i> 保存
|
||
</button>
|
||
<button class="btn-small btn-cancel" onclick="window.cancelEdit('${uuid}', event)">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
`;
|
||
}, 100);
|
||
}
|
||
|
||
/**
|
||
* 取消编辑
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Event} event - 事件对象
|
||
*/
|
||
function cancelEdit(uuid, event) {
|
||
event.stopPropagation();
|
||
|
||
const providerDetail = event.target.closest('.provider-item-detail');
|
||
const configInputs = providerDetail.querySelectorAll('input[data-config-key]');
|
||
const configSelects = providerDetail.querySelectorAll('select[data-config-key]');
|
||
|
||
// 恢复输入框为只读状态
|
||
configInputs.forEach(input => {
|
||
input.readOnly = true;
|
||
// 恢复显示为密码格式(如果有的话)
|
||
if (input.type === 'password') {
|
||
const actualValue = input.dataset.configValue;
|
||
input.value = actualValue ? '••••••••' : '';
|
||
}
|
||
});
|
||
|
||
// 禁用模型复选框
|
||
const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox');
|
||
modelCheckboxes.forEach(checkbox => {
|
||
checkbox.disabled = true;
|
||
});
|
||
|
||
// 移除编辑状态类
|
||
providerDetail.classList.remove('editing');
|
||
|
||
// 禁用文件上传按钮
|
||
const uploadButtons = providerDetail.querySelectorAll('.upload-btn');
|
||
uploadButtons.forEach(button => {
|
||
button.disabled = true;
|
||
});
|
||
|
||
// 禁用下拉选择框
|
||
configSelects.forEach(select => {
|
||
select.disabled = true;
|
||
// 恢复原始值
|
||
const originalValue = select.dataset.configValue;
|
||
select.value = originalValue || '';
|
||
});
|
||
|
||
// 恢复原来的编辑和删除按钮,但保留禁用/启用按钮
|
||
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
||
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
|
||
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
|
||
const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用';
|
||
const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
|
||
|
||
actionsGroup.innerHTML = `
|
||
<button class="btn-small ${toggleButtonClass}" onclick="window.toggleProviderStatus('${uuid}', event)" title="${toggleButtonText}此提供商">
|
||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||
</button>
|
||
<button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)">
|
||
<i class="fas fa-edit"></i> 编辑
|
||
</button>
|
||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${uuid}', event)">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 保存提供商
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Event} event - 事件对象
|
||
*/
|
||
async function saveProvider(uuid, event) {
|
||
event.stopPropagation();
|
||
|
||
const providerDetail = event.target.closest('.provider-item-detail');
|
||
const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type');
|
||
|
||
const configInputs = providerDetail.querySelectorAll('input[data-config-key]');
|
||
const configSelects = providerDetail.querySelectorAll('select[data-config-key]');
|
||
const providerConfig = {};
|
||
|
||
configInputs.forEach(input => {
|
||
const key = input.dataset.configKey;
|
||
const value = input.value;
|
||
providerConfig[key] = value;
|
||
});
|
||
|
||
configSelects.forEach(select => {
|
||
const key = select.dataset.configKey;
|
||
const value = select.value === 'true';
|
||
providerConfig[key] = value;
|
||
});
|
||
|
||
// 收集不支持的模型列表
|
||
const modelCheckboxes = providerDetail.querySelectorAll(`.model-checkbox[data-uuid="${uuid}"]:checked`);
|
||
const notSupportedModels = Array.from(modelCheckboxes).map(checkbox => checkbox.value);
|
||
providerConfig.notSupportedModels = notSupportedModels;
|
||
|
||
try {
|
||
await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig });
|
||
await window.apiClient.post('/reload-config');
|
||
showToast('提供商配置更新成功', 'success');
|
||
// 重新获取该提供商类型的最新配置
|
||
await refreshProviderConfig(providerType);
|
||
} catch (error) {
|
||
console.error('Failed to update provider:', error);
|
||
showToast('更新失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除提供商
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Event} event - 事件对象
|
||
*/
|
||
async function deleteProvider(uuid, event) {
|
||
event.stopPropagation();
|
||
|
||
if (!confirm('确定要删除这个提供商配置吗?此操作不可恢复。')) {
|
||
return;
|
||
}
|
||
|
||
const providerDetail = event.target.closest('.provider-item-detail');
|
||
const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type');
|
||
|
||
try {
|
||
await window.apiClient.delete(`/providers/${encodeURIComponent(providerType)}/${uuid}`);
|
||
await window.apiClient.post('/reload-config');
|
||
showToast('提供商配置删除成功', 'success');
|
||
// 重新获取最新配置
|
||
await refreshProviderConfig(providerType);
|
||
} catch (error) {
|
||
console.error('Failed to delete provider:', error);
|
||
showToast('删除失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重新获取并刷新提供商配置
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
async function refreshProviderConfig(providerType) {
|
||
try {
|
||
// 重新获取该提供商类型的最新数据
|
||
const data = await window.apiClient.get(`/providers/${encodeURIComponent(providerType)}`);
|
||
|
||
// 如果当前显示的是该提供商类型的模态框,则更新模态框
|
||
const modal = document.querySelector('.provider-modal');
|
||
if (modal && modal.getAttribute('data-provider-type') === providerType) {
|
||
// 更新统计信息
|
||
const totalCountElement = modal.querySelector('.provider-summary-item .value');
|
||
if (totalCountElement) {
|
||
totalCountElement.textContent = data.totalCount;
|
||
}
|
||
|
||
const healthyCountElement = modal.querySelectorAll('.provider-summary-item .value')[1];
|
||
if (healthyCountElement) {
|
||
healthyCountElement.textContent = data.healthyCount;
|
||
}
|
||
|
||
// 重新渲染提供商列表
|
||
const providerList = modal.querySelector('.provider-list');
|
||
if (providerList) {
|
||
providerList.innerHTML = renderProviderList(data.providers);
|
||
}
|
||
|
||
// 重新加载模型列表
|
||
loadModelsForProviderType(providerType, data.providers);
|
||
}
|
||
|
||
// 同时更新主界面的提供商统计数据
|
||
if (typeof window.loadProviders === 'function') {
|
||
await window.loadProviders();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Failed to refresh provider config:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示添加提供商表单
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
function showAddProviderForm(providerType) {
|
||
const modal = document.querySelector('.provider-modal');
|
||
const existingForm = modal.querySelector('.add-provider-form');
|
||
|
||
if (existingForm) {
|
||
existingForm.remove();
|
||
return;
|
||
}
|
||
|
||
const form = document.createElement('div');
|
||
form.className = 'add-provider-form';
|
||
form.innerHTML = `
|
||
<h4><i class="fas fa-plus"></i> 添加新提供商配置</h4>
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label>检查模型名称 <span class="optional-mark">(选填)</span></label>
|
||
<input type="text" id="newCheckModelName" placeholder="例如: gpt-3.5-turbo">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>健康检查</label>
|
||
<select id="newCheckHealth">
|
||
<option value="false">禁用</option>
|
||
<option value="true">启用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div id="dynamicConfigFields">
|
||
<!-- 动态配置字段将在这里显示 -->
|
||
</div>
|
||
<div class="form-actions" style="margin-top: 15px;">
|
||
<button class="btn btn-success" onclick="window.addProvider('${providerType}')">
|
||
<i class="fas fa-save"></i> 保存
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="this.closest('.add-provider-form').remove()">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 添加动态配置字段
|
||
addDynamicConfigFields(form, providerType);
|
||
|
||
// 为添加表单中的密码切换按钮绑定事件监听器
|
||
bindAddFormPasswordToggleListeners(form);
|
||
|
||
// 插入到提供商列表前面
|
||
const providerList = modal.querySelector('.provider-list');
|
||
providerList.parentNode.insertBefore(form, providerList);
|
||
}
|
||
|
||
/**
|
||
* 添加动态配置字段
|
||
* @param {HTMLElement} form - 表单元素
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
function addDynamicConfigFields(form, providerType) {
|
||
const configFields = form.querySelector('#dynamicConfigFields');
|
||
|
||
// 获取该提供商类型的字段配置
|
||
const providerFields = getProviderTypeFields(providerType);
|
||
let fields = '';
|
||
|
||
if (providerFields.length > 0) {
|
||
// 分组显示,每行两个字段
|
||
for (let i = 0; i < providerFields.length; i += 2) {
|
||
fields += '<div class="form-grid">';
|
||
|
||
const field1 = providerFields[i];
|
||
// 检查是否为密码类型字段
|
||
const isPassword1 = field1.type === 'password';
|
||
// 检查是否为OAuth凭据文件路径字段
|
||
const isOAuthFilePath1 = field1.id.includes('OauthCredsFilePath');
|
||
|
||
if (isPassword1) {
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field1.label}</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password" id="new${field1.id}" placeholder="${field1.placeholder || ''}" value="${field1.value || ''}">
|
||
<button type="button" class="password-toggle" data-target="new${field1.id}">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (isOAuthFilePath1) {
|
||
// OAuth凭据文件路径字段,添加上传按钮
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field1.label}</label>
|
||
<div class="file-input-group">
|
||
<input type="text" id="new${field1.id}" class="form-control" placeholder="${field1.placeholder || ''}" value="${field1.value || ''}">
|
||
<button type="button" class="btn btn-outline upload-btn" data-target="new${field1.id}" aria-label="上传文件">
|
||
<i class="fas fa-upload"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field1.label}</label>
|
||
<input type="${field1.type}" id="new${field1.id}" placeholder="${field1.placeholder || ''}" value="${field1.value || ''}">
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const field2 = providerFields[i + 1];
|
||
if (field2) {
|
||
// 检查是否为密码类型字段
|
||
const isPassword2 = field2.type === 'password';
|
||
// 检查是否为OAuth凭据文件路径字段
|
||
const isOAuthFilePath2 = field2.id.includes('OauthCredsFilePath');
|
||
|
||
if (isPassword2) {
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field2.label}</label>
|
||
<div class="password-input-wrapper">
|
||
<input type="password" id="new${field2.id}" placeholder="${field2.placeholder || ''}" value="${field2.value || ''}">
|
||
<button type="button" class="password-toggle" data-target="new${field2.id}">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (isOAuthFilePath2) {
|
||
// OAuth凭据文件路径字段,添加上传按钮
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field2.label}</label>
|
||
<div class="file-input-group">
|
||
<input type="text" id="new${field2.id}" class="form-control" placeholder="${field2.placeholder || ''}" value="${field2.value || ''}">
|
||
<button type="button" class="btn btn-outline upload-btn" data-target="new${field2.id}" aria-label="上传文件">
|
||
<i class="fas fa-upload"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
fields += `
|
||
<div class="form-group">
|
||
<label>${field2.label}</label>
|
||
<input type="${field2.type}" id="new${field2.id}" placeholder="${field2.placeholder || ''}" value="${field2.value || ''}">
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
fields += '</div>';
|
||
}
|
||
} else {
|
||
fields = '<p>不支持的提供商类型</p>';
|
||
}
|
||
|
||
configFields.innerHTML = fields;
|
||
}
|
||
|
||
/**
|
||
* 为添加新提供商表单中的密码切换按钮绑定事件监听器
|
||
* @param {HTMLElement} form - 表单元素
|
||
*/
|
||
function bindAddFormPasswordToggleListeners(form) {
|
||
const passwordToggles = form.querySelectorAll('.password-toggle');
|
||
passwordToggles.forEach(button => {
|
||
button.addEventListener('click', function() {
|
||
const targetId = this.getAttribute('data-target');
|
||
const input = document.getElementById(targetId);
|
||
const icon = this.querySelector('i');
|
||
|
||
if (!input || !icon) return;
|
||
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
icon.className = 'fas fa-eye-slash';
|
||
} else {
|
||
input.type = 'password';
|
||
icon.className = 'fas fa-eye';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加新提供商
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
async function addProvider(providerType) {
|
||
const checkModelName = document.getElementById('newCheckModelName')?.value;
|
||
const checkHealth = document.getElementById('newCheckHealth')?.value === 'true';
|
||
|
||
const providerConfig = {
|
||
checkModelName: checkModelName || '', // 允许为空
|
||
checkHealth
|
||
};
|
||
|
||
// 根据提供商类型收集配置
|
||
switch (providerType) {
|
||
case 'openai-custom':
|
||
providerConfig.OPENAI_API_KEY = document.getElementById('newOpenaiApiKey')?.value || '';
|
||
providerConfig.OPENAI_BASE_URL = document.getElementById('newOpenaiBaseUrl')?.value || '';
|
||
break;
|
||
case 'openaiResponses-custom':
|
||
providerConfig.OPENAI_API_KEY = document.getElementById('newOpenaiApiKey')?.value || '';
|
||
providerConfig.OPENAI_BASE_URL = document.getElementById('newOpenaiBaseUrl')?.value || '';
|
||
break;
|
||
case 'claude-custom':
|
||
providerConfig.CLAUDE_API_KEY = document.getElementById('newClaudeApiKey')?.value || '';
|
||
providerConfig.CLAUDE_BASE_URL = document.getElementById('newClaudeBaseUrl')?.value || '';
|
||
break;
|
||
case 'gemini-cli-oauth':
|
||
providerConfig.PROJECT_ID = document.getElementById('newProjectId')?.value || '';
|
||
providerConfig.GEMINI_OAUTH_CREDS_FILE_PATH = document.getElementById('newGeminiOauthCredsFilePath')?.value || '';
|
||
break;
|
||
case 'claude-kiro-oauth':
|
||
providerConfig.KIRO_OAUTH_CREDS_FILE_PATH = document.getElementById('newKiroOauthCredsFilePath')?.value || '';
|
||
break;
|
||
case 'openai-qwen-oauth':
|
||
providerConfig.QWEN_OAUTH_CREDS_FILE_PATH = document.getElementById('newQwenOauthCredsFilePath')?.value || '';
|
||
break;
|
||
}
|
||
|
||
try {
|
||
await window.apiClient.post('/providers', {
|
||
providerType,
|
||
providerConfig
|
||
});
|
||
await window.apiClient.post('/reload-config');
|
||
showToast('提供商配置添加成功', 'success');
|
||
// 移除添加表单
|
||
const form = document.querySelector('.add-provider-form');
|
||
if (form) {
|
||
form.remove();
|
||
}
|
||
// 重新获取最新配置数据
|
||
await refreshProviderConfig(providerType);
|
||
} catch (error) {
|
||
console.error('Failed to add provider:', error);
|
||
showToast('添加失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换提供商禁用/启用状态
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Event} event - 事件对象
|
||
*/
|
||
async function toggleProviderStatus(uuid, event) {
|
||
event.stopPropagation();
|
||
|
||
const providerDetail = event.target.closest('.provider-item-detail');
|
||
const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type');
|
||
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
|
||
|
||
// 获取当前提供商信息
|
||
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
|
||
const action = isCurrentlyDisabled ? 'enable' : 'disable';
|
||
const confirmMessage = isCurrentlyDisabled ?
|
||
`确定要启用这个提供商配置吗?` :
|
||
`确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。`;
|
||
|
||
if (!confirm(confirmMessage)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await window.apiClient.post(`/providers/${encodeURIComponent(providerType)}/${uuid}/${action}`, { action });
|
||
await window.apiClient.post('/reload-config');
|
||
showToast(`提供商${isCurrentlyDisabled ? '启用' : '禁用'}成功`, 'success');
|
||
// 重新获取该提供商类型的最新配置
|
||
await refreshProviderConfig(providerType);
|
||
} catch (error) {
|
||
console.error('Failed to toggle provider status:', error);
|
||
showToast(`操作失败: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置所有提供商的健康状态
|
||
* @param {string} providerType - 提供商类型
|
||
*/
|
||
async function resetAllProvidersHealth(providerType) {
|
||
if (!confirm(`确定要将 ${providerType} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showToast('正在重置健康状态...', 'info');
|
||
|
||
const response = await window.apiClient.post(
|
||
`/providers/${encodeURIComponent(providerType)}/reset-health`,
|
||
{}
|
||
);
|
||
|
||
if (response.success) {
|
||
showToast(`成功重置 ${response.resetCount} 个节点的健康状态`, 'success');
|
||
|
||
// 重新加载配置
|
||
await window.apiClient.post('/reload-config');
|
||
|
||
// 刷新提供商配置显示
|
||
await refreshProviderConfig(providerType);
|
||
} else {
|
||
showToast('重置健康状态失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('重置健康状态失败:', error);
|
||
showToast(`重置健康状态失败: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
|
||
* @param {string} uuid - 提供商UUID
|
||
* @param {Array} models - 模型列表
|
||
* @param {Array} notSupportedModels - 当前不支持的模型列表
|
||
*/
|
||
function renderNotSupportedModelsSelector(uuid, models, notSupportedModels = []) {
|
||
const container = document.querySelector(`.not-supported-models-container[data-uuid="${uuid}"]`);
|
||
if (!container) return;
|
||
|
||
if (models.length === 0) {
|
||
container.innerHTML = '<div class="no-models">该提供商类型暂无可用模型列表</div>';
|
||
return;
|
||
}
|
||
|
||
// 渲染模型复选框列表
|
||
let html = '<div class="models-checkbox-grid">';
|
||
models.forEach(model => {
|
||
const isChecked = notSupportedModels.includes(model);
|
||
html += `
|
||
<label class="model-checkbox-label">
|
||
<input type="checkbox"
|
||
class="model-checkbox"
|
||
value="${model}"
|
||
data-uuid="${uuid}"
|
||
${isChecked ? 'checked' : ''}
|
||
disabled>
|
||
<span class="model-name">${model}</span>
|
||
</label>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// 导出所有函数,并挂载到window对象供HTML调用
|
||
export {
|
||
showProviderManagerModal,
|
||
closeProviderModal,
|
||
toggleProviderDetails,
|
||
editProvider,
|
||
cancelEdit,
|
||
saveProvider,
|
||
deleteProvider,
|
||
refreshProviderConfig,
|
||
showAddProviderForm,
|
||
addProvider,
|
||
toggleProviderStatus,
|
||
resetAllProvidersHealth,
|
||
loadModelsForProviderType,
|
||
renderNotSupportedModelsSelector
|
||
};
|
||
|
||
// 将函数挂载到window对象
|
||
window.closeProviderModal = closeProviderModal;
|
||
window.toggleProviderDetails = toggleProviderDetails;
|
||
window.editProvider = editProvider;
|
||
window.cancelEdit = cancelEdit;
|
||
window.saveProvider = saveProvider;
|
||
window.deleteProvider = deleteProvider;
|
||
window.showAddProviderForm = showAddProviderForm;
|
||
window.addProvider = addProvider;
|
||
window.toggleProviderStatus = toggleProviderStatus;
|
||
window.resetAllProvidersHealth = resetAllProvidersHealth; |