// 模态框管理模块 import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js'; import { handleProviderPasswordToggle } from './event-handlers.js'; // 分页配置 const PROVIDERS_PER_PAGE = 5; let currentPage = 1; let currentProviders = []; let currentProviderType = ''; let cachedModels = []; // 缓存模型列表 /** * 显示提供商管理模态框 * @param {Object} data - 提供商数据 */ function showProviderManagerModal(data) { const { providerType, providers, totalCount, healthyCount } = data; // 保存当前数据用于分页 currentProviders = providers; currentProviderType = providerType; currentPage = 1; cachedModels = []; // 移除已存在的模态框 const existingModal = document.querySelector('.provider-modal'); if (existingModal) { // 清理事件监听器 if (existingModal.cleanup) { existingModal.cleanup(); } existingModal.remove(); } const totalPages = Math.ceil(providers.length / PROVIDERS_PER_PAGE); // 创建模态框 const modal = document.createElement('div'); modal.className = 'provider-modal'; modal.setAttribute('data-provider-type', providerType); modal.innerHTML = `

管理 ${providerType} 提供商配置

总账户数: ${totalCount}
健康账户: ${healthyCount}
${totalPages > 1 ? renderPagination(1, totalPages, providers.length) : ''}
${renderProviderListPaginated(providers, 1)}
${totalPages > 1 ? renderPagination(1, totalPages, providers.length, 'bottom') : ''}
`; // 添加到页面 document.body.appendChild(modal); // 添加模态框事件监听 addModalEventListeners(modal); // 先获取该提供商类型的模型列表(只调用一次API) const pageProviders = providers.slice(0, PROVIDERS_PER_PAGE); loadModelsForProviderType(providerType, pageProviders); } /** * 渲染分页控件 * @param {number} currentPage - 当前页码 * @param {number} totalPages - 总页数 * @param {number} totalItems - 总条目数 * @param {string} position - 位置标识 (top/bottom) * @returns {string} HTML字符串 */ function renderPagination(page, totalPages, totalItems, position = 'top') { const startItem = (page - 1) * PROVIDERS_PER_PAGE + 1; const endItem = Math.min(page * PROVIDERS_PER_PAGE, totalItems); // 生成页码按钮 let pageButtons = ''; const maxVisiblePages = 5; let startPage = Math.max(1, page - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); if (endPage - startPage < maxVisiblePages - 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } if (startPage > 1) { pageButtons += ``; if (startPage > 2) { pageButtons += `...`; } } for (let i = startPage; i <= endPage; i++) { pageButtons += ``; } if (endPage < totalPages) { if (endPage < totalPages - 1) { pageButtons += `...`; } pageButtons += ``; } return `
显示 ${startItem}-${endItem} / 共 ${totalItems} 条
${pageButtons}
跳转到
`; } /** * 跳转到指定页 * @param {number} page - 目标页码 */ function goToProviderPage(page) { const totalPages = Math.ceil(currentProviders.length / PROVIDERS_PER_PAGE); // 验证页码范围 if (page < 1) page = 1; if (page > totalPages) page = totalPages; currentPage = page; // 更新提供商列表 const providerList = document.getElementById('providerList'); if (providerList) { providerList.innerHTML = renderProviderListPaginated(currentProviders, page); } // 更新分页控件 const paginationContainers = document.querySelectorAll('.pagination-container'); paginationContainers.forEach(container => { const position = container.getAttribute('data-position'); container.outerHTML = renderPagination(page, totalPages, currentProviders.length, position); }); // 滚动到顶部 const modalBody = document.querySelector('.provider-modal-body'); if (modalBody) { modalBody.scrollTop = 0; } // 为当前页的提供商加载模型列表 const startIndex = (page - 1) * PROVIDERS_PER_PAGE; const endIndex = Math.min(startIndex + PROVIDERS_PER_PAGE, currentProviders.length); const pageProviders = currentProviders.slice(startIndex, endIndex); // 如果已缓存模型列表,直接使用 if (cachedModels.length > 0) { pageProviders.forEach(provider => { renderNotSupportedModelsSelector(provider.uuid, cachedModels, provider.notSupportedModels || []); }); } else { loadModelsForProviderType(currentProviderType, pageProviders); } } /** * 渲染分页后的提供商列表 * @param {Array} providers - 提供商数组 * @param {number} page - 当前页码 * @returns {string} HTML字符串 */ function renderProviderListPaginated(providers, page) { const startIndex = (page - 1) * PROVIDERS_PER_PAGE; const endIndex = Math.min(startIndex + PROVIDERS_PER_PAGE, providers.length); const pageProviders = providers.slice(startIndex, endIndex); return renderProviderList(pageProviders); } /** * 为提供商类型加载模型列表(优化:只调用一次API,并缓存结果) * @param {string} providerType - 提供商类型 * @param {Array} providers - 提供商列表 */ async function loadModelsForProviderType(providerType, providers) { try { // 如果已有缓存,直接使用 if (cachedModels.length > 0) { providers.forEach(provider => { renderNotSupportedModelsSelector(provider.uuid, cachedModels, provider.notSupportedModels || []); }); return; } // 只调用一次API获取模型列表 const response = await window.apiClient.get(`/provider-models/${encodeURIComponent(providerType)}`); const models = response.models || []; // 缓存模型列表 cachedModels = 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 = '
加载模型列表失败
'; } }); } } /** * 为模态框添加事件监听器 * @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 lastHealthCheckTime = provider.lastHealthCheckTime ? new Date(provider.lastHealthCheckTime).toLocaleString() : '从未检测'; const lastHealthCheckModel = provider.lastHealthCheckModel || '-'; 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'; // 构建错误信息显示 let errorInfoHtml = ''; if (!isHealthy && provider.lastErrorMessage) { const escapedErrorMsg = provider.lastErrorMessage.replace(//g, '>'); errorInfoHtml = `
最后错误: ${escapedErrorMsg}
`; } return `
${provider.uuid}
健康状态: ${healthText} | 状态: ${disabledText} | 使用次数: ${provider.usageCount || 0} | 失败次数: ${provider.errorCount || 0} | 最后使用: ${lastUsed}
最后检测: ${lastHealthCheckTime} | 检测模型: ${lastHealthCheckModel}
${errorInfoHtml}
${renderProviderConfig(provider)}
`; }).join(''); } /** * 渲染提供商配置 * @param {Object} provider - 提供商对象 * @returns {string} HTML字符串 */ function renderProviderConfig(provider) { // 获取字段映射,确保顺序一致 const fieldOrder = getFieldOrder(provider); // 先渲染基础配置字段(checkModelName 和 checkHealth) let html = '
'; 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 += `
`; } else { // checkModelName 字段始终显示 html += `
`; } }); html += '
'; // 渲染其他配置字段,每行2列 const otherFields = fieldOrder.filter(key => !baseFields.includes(key)); for (let i = 0; i < otherFields.length; i += 2) { html += '
'; 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 += `
`; } else if (field1IsOAuthFilePath) { // OAuth凭据文件路径字段,添加上传按钮 html += `
`; } else { html += `
`; } // 如果有第二个字段 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 += `
`; } else if (field2IsOAuthFilePath) { // OAuth凭据文件路径字段,添加上传按钮 html += `
`; } else { html += `
`; } } html += '
'; } // 添加 notSupportedModels 配置区域 html += '
'; html += `
加载模型列表...
`; html += '
'; return html; } /** * 获取字段显示顺序 * @param {Object} provider - 提供商对象 * @returns {Array} 字段键数组 */ function getFieldOrder(provider) { const orderedFields = ['checkModelName', 'checkHealth']; // 需要排除的内部状态字段 const excludedFields = [ 'isHealthy', 'lastUsed', 'usageCount', 'errorCount', 'lastErrorTime', 'uuid', 'isDisabled', 'lastHealthCheckTime', 'lastHealthCheckModel', 'lastErrorMessage' ]; // 获取所有其他配置项 const otherFields = Object.keys(provider).filter(key => !excludedFields.includes(key) && !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 = ` `; }, 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 = ` `; } /** * 保存提供商 * @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) { // 更新缓存的提供商数据 currentProviders = data.providers; currentProviderType = 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 totalPages = Math.ceil(data.providers.length / PROVIDERS_PER_PAGE); // 确保当前页不超过总页数 if (currentPage > totalPages) { currentPage = Math.max(1, totalPages); } // 重新渲染提供商列表(分页) const providerList = modal.querySelector('.provider-list'); if (providerList) { providerList.innerHTML = renderProviderListPaginated(data.providers, currentPage); } // 更新分页控件 const paginationContainers = modal.querySelectorAll('.pagination-container'); if (totalPages > 1) { paginationContainers.forEach(container => { const position = container.getAttribute('data-position'); container.outerHTML = renderPagination(currentPage, totalPages, data.providers.length, position); }); // 如果之前没有分页控件,需要添加 if (paginationContainers.length === 0) { const modalBody = modal.querySelector('.provider-modal-body'); const providerListEl = modal.querySelector('.provider-list'); if (modalBody && providerListEl) { providerListEl.insertAdjacentHTML('beforebegin', renderPagination(currentPage, totalPages, data.providers.length, 'top')); providerListEl.insertAdjacentHTML('afterend', renderPagination(currentPage, totalPages, data.providers.length, 'bottom')); } } } else { // 如果只有一页,移除分页控件 paginationContainers.forEach(container => container.remove()); } // 重新加载当前页的模型列表 const startIndex = (currentPage - 1) * PROVIDERS_PER_PAGE; const endIndex = Math.min(startIndex + PROVIDERS_PER_PAGE, data.providers.length); const pageProviders = data.providers.slice(startIndex, endIndex); loadModelsForProviderType(providerType, pageProviders); } // 同时更新主界面的提供商统计数据 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 = `

添加新提供商配置

`; // 添加动态配置字段 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 += '
'; const field1 = providerFields[i]; // 检查是否为密码类型字段 const isPassword1 = field1.type === 'password'; // 检查是否为OAuth凭据文件路径字段 const isOAuthFilePath1 = field1.id.includes('OauthCredsFilePath'); if (isPassword1) { fields += `
`; } else if (isOAuthFilePath1) { // OAuth凭据文件路径字段,添加上传按钮 const isKiroField = field1.id.includes('Kiro'); fields += `
${isKiroField ? ' 使用 AWS 登录方式时,请确保授权文件中包含 clientIdclientSecret 字段' : ''}
`; } else { fields += `
`; } const field2 = providerFields[i + 1]; if (field2) { // 检查是否为密码类型字段 const isPassword2 = field2.type === 'password'; // 检查是否为OAuth凭据文件路径字段 const isOAuthFilePath2 = field2.id.includes('OauthCredsFilePath'); if (isPassword2) { fields += `
`; } else if (isOAuthFilePath2) { // OAuth凭据文件路径字段,添加上传按钮 const isKiroField = field2.id.includes('Kiro'); fields += `
${isKiroField ? ' 使用 AWS 登录方式时,请确保授权文件中包含 clientIdclientSecret 字段' : ''}
`; } else { fields += `
`; } } fields += '
'; } } else { fields = '

不支持的提供商类型

'; } 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; case 'gemini-antigravity': providerConfig.PROJECT_ID = document.getElementById('newProjectId')?.value || ''; providerConfig.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH = document.getElementById('newAntigravityOauthCredsFilePath')?.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'); } } /** * 执行健康检测 * @param {string} providerType - 提供商类型 */ async function performHealthCheck(providerType) { if (!confirm(`确定要对 ${providerType} 的所有节点执行健康检测吗?\n\n这将向每个节点发送测试请求来验证其可用性。`)) { return; } try { showToast('正在执行健康检测,请稍候...', 'info'); const response = await window.apiClient.post( `/providers/${encodeURIComponent(providerType)}/health-check`, {} ); if (response.success) { const { successCount, failCount, totalCount, results } = response; // 统计跳过的数量(checkHealth 未启用的) const skippedCount = results ? results.filter(r => r.success === null).length : 0; let message = `健康检测完成: ${successCount} 健康`; if (failCount > 0) message += `, ${failCount} 异常`; if (skippedCount > 0) message += `, ${skippedCount} 跳过(未启用)`; showToast(message, failCount > 0 ? 'warning' : '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 = '
该提供商类型暂无可用模型列表
'; return; } // 渲染模型复选框列表 let html = '
'; models.forEach(model => { const isChecked = notSupportedModels.includes(model); html += ` `; }); html += '
'; container.innerHTML = html; } // 导出所有函数,并挂载到window对象供HTML调用 export { showProviderManagerModal, closeProviderModal, toggleProviderDetails, editProvider, cancelEdit, saveProvider, deleteProvider, refreshProviderConfig, showAddProviderForm, addProvider, toggleProviderStatus, resetAllProvidersHealth, performHealthCheck, loadModelsForProviderType, renderNotSupportedModelsSelector, goToProviderPage }; // 将函数挂载到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; window.performHealthCheck = performHealthCheck; window.goToProviderPage = goToProviderPage;