AIClient-2-API/static/app/modal.js
hex2077 ee050c77f2 feat: 新增Web UI管理控制台和认证系统
新增Web UI管理控制台,支持实时配置管理和健康状态监控
添加登录认证系统,包含token生成和验证机制
实现供应商池的启用/禁用功能
更新README文档,添加安装脚本和Web UI使用说明
优化配置文件管理界面,增加API客户端封装
新增登录页面和认证中间件
2025-11-12 17:37:39 +08:00

945 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');
if (targetInputId && window.fileUploadHandler) {
window.fileUploadHandler.handleFileUpload(button, targetInputId);
}
}
};
// 添加事件监听器
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 });
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}`);
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
});
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 });
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;