// 模态框管理模块 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 = `

管理 ${providerType} 提供商配置

总账户数: ${totalCount}
健康账户: ${healthyCount}
${renderProviderList(providers)}
`; // 添加到页面 document.body.appendChild(modal); // 添加模态框事件监听 addModalEventListeners(modal); } /** * 为模态框添加事件监听器 * @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 `
${provider.uuid}
健康状态: ${healthText} | 状态: ${disabledText} | 使用次数: ${provider.usageCount || 0} | 失败次数: ${provider.errorCount || 0} | 最后使用: ${lastUsed}
${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 += '
'; } 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 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 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; }); 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); } } // 同时更新主界面的提供商统计数据 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凭据文件路径字段,添加上传按钮 fields += `
`; } 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凭据文件路径字段,添加上传按钮 fields += `
`; } 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; } 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'); } } // 导出所有函数,并挂载到window对象供HTML调用 export { showProviderManagerModal, closeProviderModal, toggleProviderDetails, editProvider, cancelEdit, saveProvider, deleteProvider, refreshProviderConfig, showAddProviderForm, addProvider, toggleProviderStatus }; // 将函数挂载到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;