AIClient-2-API/static/app/modal.js
hex2077 7746e94154 feat(config): 添加提供商最大错误次数配置和动态重载功能
新增MAX_ERROR_COUNT配置项用于控制提供商连续错误次数阈值,
当达到此阈值时提供商将被标记为不健康。同时实现配置动态重载功能,
支持在不重启服务的情况下更新配置,包括提供商池管理和UI配置界面的实时同步。

- 在config.json.example中添加MAX_ERROR_COUNT默认值
- 在config-manager.js中实现命令行参数--max-error-count解析
- 在service-manager.js中将maxErrorCount传递给ProviderPoolManager
- 在ui-manager.js中实现reloadConfig函数和配置重载API
- 更新前端配置管理界面,添加maxErrorCount输入控件
- 改进文件上传处理,支持模态框提供商类型识别
- 在所有提供商操作后自动触发配置重载
2025-11-16 18:18:43 +08:00

950 lines
No EOL
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 模态框管理模块
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>
</div>
</div>
<div class="provider-list" id="providerList">
${renderProviderList(providers)}
</div>
</div>
</div>
`;
// 添加到页面
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 `
<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>';
}
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 = `
<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 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;
});
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 = `
<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="true">启用</option>
<option value="false">禁用</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');
}
}
// 导出所有函数并挂载到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;