/** * API配置管理器 v2.0 * 支持动态服务商管理和多模型配置 */ import { app } from "../../../../scripts/app.js"; import { logger } from '../utils/logger.js'; import { createSettingsDialog, createFormGroup, createInputGroup, createSelectGroup, createHorizontalFormGroup, createSwitchControl, createConfirmPopup, createContextMenu, createTooltip, createMultiSelectListbox } from "./uiComponents.js"; import { APIService } from "../services/api.js"; import { tUI } from "../utils/uiI18n.js"; // Sortable库已通过script标签加载,直接使用全局变量 class APIConfigManager { // 预置服务商ID列表(不可编辑/删除) static PRESET_SERVICE_IDS = ['zhipu', 'xFlow', 'ollama']; constructor() { // 服务商数据 this.services = []; this.currentServices = { llm: null, vlm: null }; // 百度翻译配置 this.baiduConfig = { app_id: '', secret_key: '' }; } /** * 通知系统 API 配置已更新 * 触发 pa-config-updated 事件,通知 settings.js 等模块刷新 */ notifyConfigChange() { logger.debug('分发 API 配置更新事件: pa-config-updated'); window.dispatchEvent(new CustomEvent('pa-config-updated')); } /** * 显示API配置弹窗 */ async showAPIConfigModal() { try { logger.debug('打开API配置弹窗 v2.0'); createSettingsDialog({ title: `${tUI('API管理器')}`, dialogClassName: 'api-config-dialog-v2', disableBackdropAndCloseOnClickOutside: true, hideFooter: true, // 不显示底部的保存/取消按钮 renderNotice: (noticeArea) => { const subtitle = document.createElement('div'); subtitle.className = 'api-config-warning'; subtitle.textContent = `*${tUI('免责声明:本插件仅提供 API 调用工具,第三方服务责任与本插件无关,插件所涉用户配置信息均存储于本地。对于因账号使用产生的任何问题,本插件不承担责任!')}`; noticeArea.appendChild(subtitle); }, renderContent: async (container) => { await this._loadAllConfigs(); this._createAPIConfigUI(container); }, onSave: async () => { // 不再需要手动保存,因为已经实时保存了 } }); } catch (error) { logger.error(`打开API配置弹窗失败: ${error.message}`); app.extensionManager.toast.add({ severity: "error", summary: "打开配置失败", detail: error.message, life: 3000 }); } } /** * 加载所有配置 */ async _loadAllConfigs() { try { // 加载服务商列表 const servicesRes = await fetch(APIService.getApiUrl('/services')); const servicesData = await servicesRes.json(); if (servicesData.success) { this.services = servicesData.services || []; } // 加载百度翻译配置 const baiduRes = await fetch(APIService.getApiUrl('/config/baidu_translate')); this.baiduConfig = await baiduRes.json(); // 加载当前服务配置以获取current_services const llmRes = await fetch(APIService.getApiUrl('/config/llm')); const llmConfig = await llmRes.json(); if (llmConfig.provider) { this.currentServices.llm = llmConfig.provider; } const vlmRes = await fetch(APIService.getApiUrl('/config/vision')); const vlmConfig = await vlmRes.json(); if (vlmConfig.provider) { this.currentServices.vlm = vlmConfig.provider; } logger.debug('配置加载完成', { services: this.services.length, currentLLM: this.currentServices.llm, currentVLM: this.currentServices.vlm }); } catch (error) { logger.error('加载配置失败', error); throw error; } } /** * 保存所有配置 */ async _saveAllConfigs() { try { // 保存百度翻译配置 await fetch(APIService.getApiUrl('/config/baidu_translate'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.baiduConfig) }); app.extensionManager.toast.add({ severity: "success", summary: "配置已保存", life: 3000 }); } catch (error) { logger.error('保存配置失败', error); app.extensionManager.toast.add({ severity: "error", summary: "保存失败", detail: error.message, life: 3000 }); throw error; } } /** * 保存百度翻译配置 */ async _saveBaiduConfig() { try { await fetch(APIService.getApiUrl('/config/baidu_translate'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.baiduConfig) }); logger.debug('百度翻译配置已保存'); // 触发配置同步事件 this.notifyConfigChange(); // 显示成功提示 app.extensionManager.toast.add({ severity: "success", summary: "百度翻译配置已保存", life: 2000 }); } catch (error) { logger.error('保存百度翻译配置失败', error); app.extensionManager.toast.add({ severity: "error", summary: "保存失败", detail: error.message, life: 3000 }); } } /** * 创建API配置UI */ _createAPIConfigUI(container) { // 创建标签页容器 const tabContainer = document.createElement('div'); tabContainer.className = 'api-config-tabs'; // 创建标签页头部(动态生成所有服务商标签) const tabHeader = this._createTabHeader(); tabContainer.appendChild(tabHeader); // 创建标签页内容容器 const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; // 创建百度翻译标签页 const baiduContent = this._createBaiduTab(); tabContent.appendChild(baiduContent); // 动态创建每个服务商的标签页内容 this.services.forEach(service => { const serviceContent = this._createServiceContentTab(service); tabContent.appendChild(serviceContent); }); tabContainer.appendChild(tabContent); container.appendChild(tabContainer); // 默认显示第一个标签页 this._switchTab('baidu', tabHeader, tabContent); } /** * 创建标签页头部(包含所有服务商) */ _createTabHeader() { const header = document.createElement('div'); header.className = 'tab-header'; // 百度翻译标签 const baiduTab = this._createTabButton('baidu', tUI('百度翻译'), tUI('机器翻译')); header.appendChild(baiduTab); // 动态创建服务商标签 this.services.forEach(service => { const tabButton = this._createTabButton( service.id, service.name || '未命名服务', service.description || '' ); header.appendChild(tabButton); }); // 创建"+"新增标签按钮 const addButton = document.createElement('button'); addButton.className = 'service-tab-add'; addButton.innerHTML = ''; addButton.addEventListener('click', () => this._addNewService(header, header.nextElementSibling)); header.appendChild(addButton); // 初始化拖拽排序 new Sortable(header, { handle: '.tab-button', draggable: '.tab-button', filter: '.service-tab-add', // 排除"+"按钮 animation: 150, onEnd: async (evt) => { await this._updateServicesOrder(); } }); return header; } /** * 更新服务商顺序 */ async _updateServicesOrder() { try { // 从DOM读取当前标签顺序 const header = document.querySelector('.tab-header'); const buttons = header.querySelectorAll('.tab-button'); const serviceIds = []; buttons.forEach(btn => { const tabId = btn.dataset.tab; // 排除特殊标签(如百度翻译) if (tabId && tabId !== 'baidu') { serviceIds.push(tabId); } }); // 调用后端API保存顺序 const res = await fetch(APIService.getApiUrl('/services/order'), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service_ids: serviceIds }) }); const result = await res.json(); if (result.success) { // 更新本地服务列表顺序 const orderedServices = []; serviceIds.forEach(id => { const service = this.services.find(s => s.id === id); if (service) { orderedServices.push(service); } }); // 添加未在orderedServices中的服务 this.services.forEach(s => { if (!orderedServices.find(os => os.id === s.id)) { orderedServices.push(s); } }); this.services = orderedServices; logger.debug('服务商顺序已更新', { order: serviceIds }); // 触发配置同步事件 this.notifyConfigChange(); } else { throw new Error(result.error || '更新顺序失败'); } } catch (error) { logger.error('更新服务商顺序失败', error); app.extensionManager.toast.add({ severity: "error", summary: "更新顺序失败", detail: error.message, life: 3000 }); } } /** * 创建单个标签按钮 */ _createTabButton(tabId, title, subtitle) { const button = document.createElement('button'); button.className = 'tab-button'; button.dataset.tab = tabId; // 标签标题 const titleEl = document.createElement('div'); titleEl.className = 'tab-title'; titleEl.textContent = title; button.appendChild(titleEl); // 标签小字(介绍) if (subtitle) { const subtitleEl = document.createElement('div'); subtitleEl.className = 'tab-subtitle'; subtitleEl.textContent = subtitle; button.appendChild(subtitleEl); } // 点击切换标签 button.addEventListener('click', () => { this._switchTab(tabId, button.parentElement, button.parentElement.nextElementSibling); }); // 为服务商标签添加右键菜单(百度翻译和预置服务商除外) // 预置服务商不可编辑/删除,只有用户自定义的服务商才能使用右键菜单 const isPresetService = APIConfigManager.PRESET_SERVICE_IDS.includes(tabId); if (tabId !== 'baidu' && !isPresetService) { this._attachServiceContextMenu(button, tabId, title); } return button; } /** * 切换标签页 */ _switchTab(tabId, header, contentContainer) { // 更新标签按钮状态 header.querySelectorAll('.tab-button').forEach(btn => { if (btn.dataset.tab === tabId) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // 显示对应内容 contentContainer.querySelectorAll('.tab-pane').forEach(pane => { pane.style.display = pane.dataset.tab === tabId ? 'block' : 'none'; }); } /** * 为服务标签附加右键菜单 */ _attachServiceContextMenu(button, serviceId, serviceName) { createContextMenu({ target: button, items: [ { label: '修改服务商名称', icon: 'pi-pencil', onClick: () => { this._editServiceName(button, serviceId, serviceName); } }, { separator: true }, { label: '删除服务', icon: 'pi-trash', danger: true, // 标记为危险操作,图标显示红色 onClick: () => { this._deleteService(serviceId, serviceName); } } ] }); } /** * 修改服务商名称 */ _editServiceName(triggerButton, serviceId, currentName) { const service = this.services.find(s => s.id === serviceId); if (!service) return; createConfirmPopup({ target: triggerButton, message: '修改服务商信息', icon: 'pi-pencil', position: 'bottom', confirmLabel: '保存', cancelLabel: '取消', renderFormContent: (formContainer) => { // 服务商名称输入框 const nameInput = createInputGroup('服务商名称', '请输入服务商名称'); nameInput.input.value = service.name || currentName; nameInput.input.dataset.fieldName = 'serviceName'; formContainer.appendChild(nameInput.group); // 服务商介绍输入框 const descInput = createInputGroup('服务商介绍', '请输入服务商介绍(可选)'); descInput.input.value = service.description || ''; descInput.input.dataset.fieldName = 'serviceDescription'; formContainer.appendChild(descInput.group); }, onConfirm: async (formContainer) => { try { const nameInput = formContainer.querySelector('[data-field-name="serviceName"]'); const descInput = formContainer.querySelector('[data-field-name="serviceDescription"]'); const newName = nameInput.value.trim(); const newDescription = descInput.value.trim(); if (!newName) { app.extensionManager.toast.add({ severity: "warn", summary: "请输入服务商名称", life: 2000 }); throw new Error('服务商名称不能为空'); } // 更新服务商信息 await this._updateService(serviceId, { name: newName, description: newDescription }); // 更新按钮显示 const titleEl = triggerButton.querySelector('.tab-title'); const subtitleEl = triggerButton.querySelector('.tab-subtitle'); if (titleEl) { titleEl.textContent = newName; } if (subtitleEl) { subtitleEl.textContent = newDescription; } else if (newDescription) { // 如果之前没有副标题,现在添加一个 const newSubtitleEl = document.createElement('div'); newSubtitleEl.className = 'tab-subtitle'; newSubtitleEl.textContent = newDescription; triggerButton.appendChild(newSubtitleEl); } app.extensionManager.toast.add({ severity: "success", summary: "服务商信息已更新", detail: `${newName} ${tUI('更新成功')}`, life: 2000 }); } catch (error) { logger.error('更新服务商信息失败', error); app.extensionManager.toast.add({ severity: "error", summary: "更新失败", detail: error.message, life: 3000 }); throw error; } } }); } /** * 创建服务商内容标签页 */ _createServiceContentTab(service) { const pane = document.createElement('div'); pane.className = 'tab-pane'; pane.dataset.tab = service.id; pane.style.display = 'none'; pane.style.padding = '16px'; // 服务商配置卡片(复用现有的卡片创建逻辑) const card = this._createServiceCard(service); pane.appendChild(card); return pane; } /** * 新增服务商 */ async _addNewService(headerElement, contentElement) { // 获取触发按钮作为定位参考 const triggerButton = headerElement.querySelector('.service-tab-add'); // 显示确认气泡框 createConfirmPopup({ target: triggerButton, message: '创建新的服务商', icon: 'pi-plus-circle', position: 'left', confirmLabel: '创建', cancelLabel: '取消', renderFormContent: (formContainer) => { // 服务商名称输入框 const nameInput = createInputGroup('服务商名称', '请输入服务商名称'); nameInput.input.value = tUI('新服务商'); nameInput.input.dataset.fieldName = 'serviceName'; formContainer.appendChild(nameInput.group); // 服务商介绍输入框 const descInput = createInputGroup('服务商介绍', '请输入服务商介绍(可选)'); descInput.input.dataset.fieldName = 'serviceDescription'; formContainer.appendChild(descInput.group); }, onConfirm: async (formContainer) => { try { // 获取表单数据 const nameInput = formContainer.querySelector('[data-field-name="serviceName"]'); const descInput = formContainer.querySelector('[data-field-name="serviceDescription"]'); const serviceName = nameInput.value.trim(); const serviceDescription = descInput.value.trim(); if (!serviceName) { app.extensionManager.toast.add({ severity: "warn", summary: "请输入服务商名称", life: 2000 }); throw new Error('服务商名称不能为空'); } // 创建服务商 const res = await fetch(APIService.getApiUrl('/services'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'openai_compatible', name: serviceName, description: serviceDescription, base_url: 'https://api.example.com/v1', api_key: '' }) }); const result = await res.json(); if (result.success) { app.extensionManager.toast.add({ severity: "success", summary: "新服务商已创建", detail: `${serviceName} ${tUI('创建成功')}`, life: 3000 }); // 重新加载配置 await this._loadAllConfigs(); // 获取新创建的服务 const newService = this.services.find(s => s.id === result.service_id); if (newService) { // 创建新标签按钮(插入到"+"按钮前) const addButton = headerElement.querySelector('.service-tab-add'); const newTabButton = this._createTabButton( newService.id, newService.name || '未命名服务', newService.description || '' ); headerElement.insertBefore(newTabButton, addButton); // 创建新内容标签页 const newContentPane = this._createServiceContentTab(newService); contentElement.appendChild(newContentPane); // 切换到新标签 this._switchTab(newService.id, headerElement, contentElement); } // 触发配置同步事件 this.notifyConfigChange(); } else { throw new Error(result.error || '创建失败'); } } catch (error) { logger.error('创建服务商失败', error); app.extensionManager.toast.add({ severity: "error", summary: "创建失败", detail: error.message, life: 3000 }); throw error; } } }); } /** * 创建百度翻译标签页 */ _createBaiduTab() { const pane = document.createElement('div'); pane.className = 'tab-pane'; pane.dataset.tab = 'baidu'; const section = createFormGroup('百度翻译配置', [ { text: '开通百度翻译服务', url: 'https://fanyi-api.baidu.com/' } ]); section.classList.add('baidu-translate-section'); // 为链接添加图标,与其他服务保持统一 const linkElement = section.querySelector('.settings-service-link'); if (linkElement) { const icon = document.createElement('i'); icon.className = 'pi pi-star'; icon.style.marginRight = '4px'; linkElement.insertBefore(icon, linkElement.firstChild); } const appIdInput = createInputGroup('AppID', '请输入百度翻译 AppID'); appIdInput.input.value = this.baiduConfig.app_id || ''; appIdInput.input.addEventListener('input', (e) => { this.baiduConfig.app_id = e.target.value; }); // 添加失焦保存 appIdInput.input.addEventListener('blur', async () => { await this._saveBaiduConfig(); }); const secretInput = createInputGroup('Secret Key', '请输入百度翻译密钥'); secretInput.input.type = 'password'; secretInput.input.value = this.baiduConfig.secret_key || ''; secretInput.input.addEventListener('input', (e) => { this.baiduConfig.secret_key = e.target.value; }); // 添加失焦保存 secretInput.input.addEventListener('blur', async () => { await this._saveBaiduConfig(); }); section.appendChild(appIdInput.group); section.appendChild(secretInput.group); pane.appendChild(section); return pane; } /** * 创建通用服务商标签页(二级标签页结构) */ _createServicesTab() { const pane = document.createElement('div'); pane.className = 'tab-pane services-tab-pane'; pane.dataset.tab = 'services'; // 样式已移至CSS // 二级标签页导航 const subTabNav = document.createElement('div'); subTabNav.className = 'service-sub-tabs'; // 样式已移至CSS // 二级标签页内容容器 const subTabContent = document.createElement('div'); subTabContent.className = 'service-sub-content'; // 获取通用服务商 const genericServices = this.services.filter(s => s.type === 'openai_compatible'); // 创建服务商标签 genericServices.forEach((service, index) => { // 创建标签按钮 const tabButton = this._createServiceTabButton(service); subTabNav.appendChild(tabButton); // 创建标签内容 const tabContentPane = this._createServiceTabContent(service); subTabContent.appendChild(tabContentPane); // 默认选中第一个 if (index === 0) { tabButton.classList.add('active'); tabContentPane.style.display = 'block'; } }); // 创建"+"新增标签按钮 const addTabButton = document.createElement('button'); addTabButton.className = 'service-tab-add'; addTabButton.textContent = '+'; addTabButton.addEventListener('click', () => this._addNewServiceTab(subTabNav, subTabContent)); subTabNav.appendChild(addTabButton); // 如果没有任何服务商,显示空状态 if (genericServices.length === 0) { const emptyHint = document.createElement('div'); emptyHint.className = 'empty-state-hint'; emptyHint.innerHTML = `
📦
暂无服务商
点击右上角"+"按钮新增第一个服务商
`; subTabContent.appendChild(emptyHint); } pane.appendChild(subTabNav); pane.appendChild(subTabContent); return pane; } /** * 创建服务商标签按钮 */ _createServiceTabButton(service) { const button = document.createElement('button'); button.className = 'service-tab-button'; button.dataset.serviceId = service.id; // 标签标题 const title = document.createElement('div'); title.className = 'service-tab-title'; title.textContent = service.name || '未命名服务'; // 标签小字(介绍) const subtitle = document.createElement('div'); subtitle.className = 'service-tab-subtitle'; subtitle.textContent = service.description || ''; button.appendChild(title); if (service.description) { button.appendChild(subtitle); } // 点击切换 button.addEventListener('click', () => { this._switchServiceTab(service.id); }); return button; } /** * 切换服务商标签 */ _switchServiceTab(serviceId) { const container = document.querySelector('.services-tab-pane'); if (!container) return; // 更新标签按钮状态 const buttons = container.querySelectorAll('.service-tab-button'); buttons.forEach(btn => { if (btn.dataset.serviceId === serviceId) { btn.classList.add('active'); btn.style.background = 'var(--p-primary-500)'; btn.style.color = 'white'; btn.querySelector('.service-tab-title').style.color = 'white'; const subtitle = btn.querySelector('.service-tab-subtitle'); if (subtitle) { subtitle.style.color = 'rgba(255, 255, 255, 0.8)'; } } else { btn.classList.remove('active'); btn.style.background = 'transparent'; btn.style.color = 'var(--p-text-color)'; btn.querySelector('.service-tab-title').style.color = 'var(--p-text-color)'; const subtitle = btn.querySelector('.service-tab-subtitle'); if (subtitle) { subtitle.style.color = 'var(--p-text-muted-color)'; } } }); // 更新内容显示 const panes = container.querySelectorAll('.service-content-pane'); panes.forEach(pane => { pane.style.display = pane.dataset.serviceId === serviceId ? 'block' : 'none'; }); } /** * 创建服务商标签内容 */ _createServiceTabContent(service) { const contentPane = document.createElement('div'); contentPane.className = 'service-content-pane'; contentPane.dataset.serviceId = service.id; contentPane.style.cssText = ` display: none; `; // 这里先创建一个简单的占位内容,后续会完善 const card = this._createServiceCard(service); contentPane.appendChild(card); return contentPane; } /** * 添加新服务商标签 */ async _addNewServiceTab(navContainer, contentContainer) { // 调用后端API创建新服务商 try { const res = await fetch(APIService.getApiUrl('/services'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'openai_compatible', name: tUI('新服务商'), description: '', base_url: 'https://api.example.com/v1', api_key: '' }) }); const result = await res.json(); if (result.success) { app.extensionManager.toast.add({ severity: "success", summary: "新服务商已创建", detail: tUI("请填写配置信息"), life: 3000 }); // 重新加载配置 await this._loadAllConfigs(); // 获取新创建的服务 const newService = this.services.find(s => s.id === result.service_id); if (newService) { // 创建新标签按钮(插入到"+"按钮前) const newTabButton = this._createServiceTabButton(newService); const addButton = navContainer.querySelector('.service-tab-add'); navContainer.insertBefore(newTabButton, addButton); // 创建新内容 const newContentPane = this._createServiceTabContent(newService); contentContainer.appendChild(newContentPane); // 移除空状态提示(如果有) const emptyHint = contentContainer.querySelector('.empty-state-hint'); if (emptyHint) { emptyHint.remove(); } // 切换到新标签 this._switchServiceTab(newService.id); } } else { throw new Error(result.error || '创建失败'); } } catch (error) { logger.error('创建服务商失败', error); app.extensionManager.toast.add({ severity: "error", summary: "创建失败", detail: error.message, life: 3000 }); } } /** * 创建Ollama标签页 */ _createOllamaTab() { const pane = document.createElement('div'); pane.className = 'tab-pane'; pane.dataset.tab = 'ollama'; // 样式已移至CSS const ollamaService = this.services.find(s => s.type === 'ollama'); if (ollamaService) { const card = this._createServiceCard(ollamaService); pane.appendChild(card); } else { const hint = document.createElement('div'); hint.className = 'empty-state-hint-small'; hint.textContent = 'Ollama服务未配置'; pane.appendChild(hint); } return pane; } /** * 创建服务商卡片 */ _createServiceCard(service) { const card = document.createElement('div'); card.className = 'service-card'; card.dataset.serviceId = service.id; // 添加serviceId到dataset // 服务商标题 - 根据服务名称检测是否需要添加外部链接 const titleText = service.name || service.id; const descText = service.description ? ` ${tUI('信息配置')}` : ''; const fullTitle = `1️⃣ ${titleText}${descText}`; // 检测服务名称,添加对应的申请链接 const links = []; const serviceName = (service.name || '').toLowerCase(); const serviceId = (service.id || '').toLowerCase(); const searchText = `${serviceName} ${serviceId}`.toLowerCase(); // 智谱服务检测 if (searchText.includes('智谱') || searchText.includes('zhipu')) { links.push({ text: '开通智谱API服务', url: 'https://www.bigmodel.cn/invite?icode=Wz1tQAT40T9M8vwp%2F1db7nHEaazDlIZGj9HxftzTbt4%3D', icon: 'pi-star' }); } // 硅基流动服务检测 if (searchText.includes('硅基') || searchText.includes('siliconflow') || searchText.includes('silicon')) { links.push({ text: '开通硅基流动API服务', url: 'https://cloud.siliconflow.cn/i/FCDL2zBQ', icon: 'pi-star' }); } // xflow服务检测 if (searchText.includes('xflow')) { links.push({ text: '开通xflow API服务', url: 'https://api.xflow.cc/register?aff=Z063', icon: 'pi-star' }); } // 使用createFormGroup创建带链接的标题,或者普通标题 let titleSection; if (links.length > 0) { titleSection = createFormGroup(fullTitle, links.map(link => ({ text: link.text, url: link.url }))); // 为链接添加图标 const linkElements = titleSection.querySelectorAll('.settings-service-link'); linkElements.forEach((linkElem, index) => { if (links[index] && links[index].icon) { const icon = document.createElement('i'); icon.className = `pi ${links[index].icon}`; icon.style.marginRight = '4px'; linkElem.insertBefore(icon, linkElem.firstChild); } }); } else { // 没有链接时,创建普通标题 titleSection = document.createElement('div'); titleSection.className = 'settings-form-section'; const titleElement = document.createElement('h3'); titleElement.className = 'settings-form-section-title'; titleElement.textContent = fullTitle; titleSection.appendChild(titleElement); } // 如果是 Ollama 服务,在标题后方添加提示 tooltip if (service.type === 'ollama') { const titleElement = titleSection.querySelector('.settings-form-section-title'); if (titleElement) { // 确保 h3 可以包含其他元素,设置为 flex 以对齐图标 titleElement.style.display = 'inline-flex'; titleElement.style.alignItems = 'center'; const icon = document.createElement('i'); icon.className = 'pi pi-info-circle service-setting-info-icon'; icon.style.marginLeft = '8px'; icon.style.fontSize = '14px'; icon.style.color = 'var(--p-text-muted-color)'; icon.style.cursor = 'help'; titleElement.appendChild(icon); createTooltip({ target: icon, content: '建议不要在地址后方添加 /v1。不加 /v1 会走原生 Ollama API,加了 /v1 则会走 OpenAI 兼容请求格式。', position: 'top' }); } } card.appendChild(titleSection); // 基本信息 const baseUrlInput = createInputGroup('Base URL', 'https://api.example.com/v1'); baseUrlInput.input.value = service.base_url || ''; // 智谱和 xflow 服务的 Base URL 禁用修改 if (service.id === 'zhipu' || service.id === 'xFlow') { baseUrlInput.input.disabled = true; baseUrlInput.input.title = tUI('该预置服务商的 Base URL 不可修改'); baseUrlInput.input.classList.add('pa-input-disabled'); } baseUrlInput.input.addEventListener('change', async (e) => { await this._updateService(service.id, { base_url: e.target.value }); }); // API Key输入框(简化版,直接使用明文) const apiKeyInput = createInputGroup('API Key', '请输入API Key'); apiKeyInput.input.type = 'password'; apiKeyInput.input.value = service.api_key || ''; // 失焦时保存 apiKeyInput.input.addEventListener('blur', async (e) => { const newApiKey = e.target.value.trim(); if (newApiKey !== service.api_key) { await this._updateService(service.id, { api_key: newApiKey }); service.api_key = newApiKey; } }); card.appendChild(baseUrlInput.group); card.appendChild(apiKeyInput.group); // === 服务配置区域(简化版) === // 创建配置项容器 const settingsInlineContainer = document.createElement('div'); settingsInlineContainer.className = 'service-settings-inline'; // 思维链控制开关 const thinkingContainer = document.createElement('div'); thinkingContainer.className = 'service-setting-item'; const thinkingLabel = document.createElement('span'); thinkingLabel.className = 'service-setting-label'; thinkingLabel.textContent = tUI('关闭思维链'); const thinkingIcon = document.createElement('i'); thinkingIcon.className = 'pi pi-info-circle service-setting-info-icon'; // 添加 tooltip createTooltip({ target: thinkingIcon, content: '针对部分支持关闭思维链的模型进行关闭。⚠️:并不是所有模型都支持,关闭思维链的模型会在日志中的模型信息后面多出一个“✏️”符号。', position: 'top' }); const thinkingLabelWrapper = document.createElement('div'); thinkingLabelWrapper.className = 'service-setting-label-wrapper'; thinkingLabelWrapper.appendChild(thinkingLabel); thinkingLabelWrapper.appendChild(thinkingIcon); // 创建开关 const thinkingSwitchWrapper = document.createElement('label'); thinkingSwitchWrapper.className = 'switch-wrapper'; const thinkingInput = document.createElement('input'); thinkingInput.type = 'checkbox'; thinkingInput.checked = service.disable_thinking ?? true; const thinkingSlider = document.createElement('span'); thinkingSlider.className = `switch-slider${thinkingInput.checked ? ' checked' : ''}`; const thinkingButton = document.createElement('span'); thinkingButton.className = `switch-button${thinkingInput.checked ? ' checked' : ''}`; thinkingSlider.appendChild(thinkingButton); thinkingInput.addEventListener('change', async (e) => { const isChecked = e.target.checked; if (isChecked) { thinkingSlider.classList.add('checked'); thinkingButton.classList.add('checked'); } else { thinkingSlider.classList.remove('checked'); thinkingButton.classList.remove('checked'); } await this._updateService(service.id, { disable_thinking: isChecked }); service.disable_thinking = isChecked; }); thinkingSwitchWrapper.appendChild(thinkingInput); thinkingSwitchWrapper.appendChild(thinkingSlider); thinkingContainer.appendChild(thinkingLabelWrapper); thinkingContainer.appendChild(thinkingSwitchWrapper); settingsInlineContainer.appendChild(thinkingContainer); // ---启用高级参数开关--- const advancedParamsContainer = document.createElement('div'); advancedParamsContainer.className = 'service-setting-item'; const advancedParamsLabel = document.createElement('span'); advancedParamsLabel.className = 'service-setting-label'; advancedParamsLabel.textContent = tUI('启用高级参数'); const advancedParamsIcon = document.createElement('i'); advancedParamsIcon.className = 'pi pi-info-circle service-setting-info-icon'; // 添加 tooltip createTooltip({ target: advancedParamsIcon, content: '启用后将发送 temperature、top_p、max_tokens 参数以精细控制模型行为,限制最大tonken数来提升速度。如果关闭则可以提升兼容性。', position: 'top' }); const advancedParamsLabelWrapper = document.createElement('div'); advancedParamsLabelWrapper.className = 'service-setting-label-wrapper'; advancedParamsLabelWrapper.appendChild(advancedParamsLabel); advancedParamsLabelWrapper.appendChild(advancedParamsIcon); // 创建开关 const advancedParamsSwitchWrapper = document.createElement('label'); advancedParamsSwitchWrapper.className = 'switch-wrapper'; const advancedParamsInput = document.createElement('input'); advancedParamsInput.type = 'checkbox'; advancedParamsInput.checked = service.enable_advanced_params ?? false; const advancedParamsSlider = document.createElement('span'); advancedParamsSlider.className = `switch-slider${advancedParamsInput.checked ? ' checked' : ''}`; const advancedParamsButton = document.createElement('span'); advancedParamsButton.className = `switch-button${advancedParamsInput.checked ? ' checked' : ''}`; advancedParamsSlider.appendChild(advancedParamsButton); advancedParamsInput.addEventListener('change', async (e) => { const isChecked = e.target.checked; if (isChecked) { advancedParamsSlider.classList.add('checked'); advancedParamsButton.classList.add('checked'); } else { advancedParamsSlider.classList.remove('checked'); advancedParamsButton.classList.remove('checked'); } await this._updateService(service.id, { enable_advanced_params: isChecked }); service.enable_advanced_params = isChecked; }); advancedParamsSwitchWrapper.appendChild(advancedParamsInput); advancedParamsSwitchWrapper.appendChild(advancedParamsSlider); advancedParamsContainer.appendChild(advancedParamsLabelWrapper); advancedParamsContainer.appendChild(advancedParamsSwitchWrapper); settingsInlineContainer.appendChild(advancedParamsContainer); // ---过滤思维链输出开关--- const filterThinkingContainer = document.createElement('div'); filterThinkingContainer.className = 'service-setting-item'; const filterThinkingLabel = document.createElement('span'); filterThinkingLabel.className = 'service-setting-label'; filterThinkingLabel.textContent = tUI('过滤思维链输出'); const filterThinkingIcon = document.createElement('i'); filterThinkingIcon.className = 'pi pi-info-circle service-setting-info-icon'; // 添加 tooltip createTooltip({ target: filterThinkingIcon, content: '针对无法关闭思维链模型,移除思考过程内容。默认开启。', position: 'top' }); const filterThinkingLabelWrapper = document.createElement('div'); filterThinkingLabelWrapper.className = 'service-setting-label-wrapper'; filterThinkingLabelWrapper.appendChild(filterThinkingLabel); filterThinkingLabelWrapper.appendChild(filterThinkingIcon); // 创建开关 const filterThinkingSwitchWrapper = document.createElement('label'); filterThinkingSwitchWrapper.className = 'switch-wrapper'; const filterThinkingInput = document.createElement('input'); filterThinkingInput.type = 'checkbox'; filterThinkingInput.checked = service.filter_thinking_output ?? true; const filterThinkingSlider = document.createElement('span'); filterThinkingSlider.className = `switch-slider${filterThinkingInput.checked ? ' checked' : ''}`; const filterThinkingButton = document.createElement('span'); filterThinkingButton.className = `switch-button${filterThinkingInput.checked ? ' checked' : ''}`; filterThinkingSlider.appendChild(filterThinkingButton); filterThinkingInput.addEventListener('change', async (e) => { const isChecked = e.target.checked; if (isChecked) { filterThinkingSlider.classList.add('checked'); filterThinkingButton.classList.add('checked'); } else { filterThinkingSlider.classList.remove('checked'); filterThinkingButton.classList.remove('checked'); } await this._updateService(service.id, { filter_thinking_output: isChecked }); service.filter_thinking_output = isChecked; }); filterThinkingSwitchWrapper.appendChild(filterThinkingInput); filterThinkingSwitchWrapper.appendChild(filterThinkingSlider); filterThinkingContainer.appendChild(filterThinkingLabelWrapper); filterThinkingContainer.appendChild(filterThinkingSwitchWrapper); settingsInlineContainer.appendChild(filterThinkingContainer); // Ollama专属:自动释放模型开关(仅前端UI) if (service.type === 'ollama') { const autoUnloadContainer = document.createElement('div'); autoUnloadContainer.className = 'service-setting-item'; const autoUnloadLabel = document.createElement('span'); autoUnloadLabel.className = 'service-setting-label'; autoUnloadLabel.textContent = tUI('自动释放模型'); const autoUnloadIcon = document.createElement('i'); autoUnloadIcon.className = 'pi pi-info-circle service-setting-info-icon'; // 添加 tooltip createTooltip({ target: autoUnloadIcon, content: '请求完成后自动卸载模型以释放显存。⚠️该选项对针对前端小助手生效,节点有独立的选项。', position: 'top' }); const autoUnloadLabelWrapper = document.createElement('div'); autoUnloadLabelWrapper.className = 'service-setting-label-wrapper'; autoUnloadLabelWrapper.appendChild(autoUnloadLabel); autoUnloadLabelWrapper.appendChild(autoUnloadIcon); // 创建开关 const autoUnloadSwitchWrapper = document.createElement('label'); autoUnloadSwitchWrapper.className = 'switch-wrapper'; const autoUnloadInput = document.createElement('input'); autoUnloadInput.type = 'checkbox'; autoUnloadInput.checked = service.auto_unload !== false; const autoUnloadSlider = document.createElement('span'); autoUnloadSlider.className = `switch-slider${autoUnloadInput.checked ? ' checked' : ''}`; const autoUnloadButton = document.createElement('span'); autoUnloadButton.className = `switch-button${autoUnloadInput.checked ? ' checked' : ''}`; autoUnloadSlider.appendChild(autoUnloadButton); autoUnloadInput.addEventListener('change', async (e) => { const isChecked = e.target.checked; if (isChecked) { autoUnloadSlider.classList.add('checked'); autoUnloadButton.classList.add('checked'); } else { autoUnloadSlider.classList.remove('checked'); autoUnloadButton.classList.remove('checked'); } await this._updateService(service.id, { auto_unload: isChecked }); service.auto_unload = isChecked; }); autoUnloadSwitchWrapper.appendChild(autoUnloadInput); autoUnloadSwitchWrapper.appendChild(autoUnloadSlider); autoUnloadContainer.appendChild(autoUnloadLabelWrapper); autoUnloadContainer.appendChild(autoUnloadSwitchWrapper); settingsInlineContainer.appendChild(autoUnloadContainer); } card.appendChild(settingsInlineContainer); // LLM模型部分 const llmSection = this._createModelSection(service, 'llm'); card.appendChild(llmSection); // VLM模型部分 const vlmSection = this._createModelSection(service, 'vlm'); card.appendChild(vlmSection); return card; } /** * 创建模型配置部分 */ _createModelSection(service, modelType) { const section = document.createElement('div'); section.className = 'settings-form-section'; section.style.marginTop = '16px'; // 标题行(包含模型类型和+按钮) const titleRow = document.createElement('div'); titleRow.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; `; const title = document.createElement('h5'); title.className = 'settings-form-section-title'; title.textContent = modelType === 'llm' ? tUI('2️⃣ 添加翻译、提示词优化的大语言模型 (LLM)') : tUI('3️⃣ 添加图像、视频反推的视觉模型 (VLM)'); title.style.margin = '0'; title.style.display = 'inline-flex'; title.style.alignItems = 'center'; const modelHintIcon = document.createElement('i'); modelHintIcon.className = 'pi pi-exclamation-circle service-setting-info-icon'; modelHintIcon.style.marginLeft = '8px'; modelHintIcon.style.fontSize = '14px'; modelHintIcon.style.color = 'var(--p-text-muted-color)'; modelHintIcon.style.cursor = 'help'; title.appendChild(modelHintIcon); createTooltip({ target: modelHintIcon, content: '建议优先选择非思考模型或指令型(-instruct)模型,以减少思维链输出、截断和响应不稳定的问题。', position: 'top' }); // 添加模型按钮 const addButton = document.createElement('button'); addButton.className = 'p-button p-component p-button-sm'; addButton.innerHTML = `${tUI('添加模型')}`; addButton.addEventListener('click', () => this._showAddModelDialog(service, modelType, modelsContainer)); titleRow.appendChild(title); titleRow.appendChild(addButton); section.appendChild(titleRow); // 模型标签容器(可拖动排序) const modelsContainer = document.createElement('div'); modelsContainer.className = 'models-container'; modelsContainer.dataset.serviceId = service.id; modelsContainer.dataset.modelType = modelType; const models = modelType === 'llm' ? service.llm_models : service.vlm_models; if (models && models.length > 0) { models.forEach((model) => { const modelTag = this._createModelTag(model, service, modelType); modelsContainer.appendChild(modelTag); }); // 初始化Sortable拖动排序并保存实例 modelsContainer.sortableInstance = new Sortable(modelsContainer, { animation: 150, ghostClass: 'sortable-ghost', handle: '.model-tag', // 整个标签都可以拖动 onEnd: async (evt) => { // 拖动结束后更新模型顺序 await this._updateModelOrder(service.id, modelType, modelsContainer); } }); } else { const emptyHint = document.createElement('div'); emptyHint.className = 'empty-hint'; emptyHint.textContent = tUI('暂无配置模型,点击"+ 添加模型"开始配置'); emptyHint.style.cssText = ` font-size: 12px; color: var(--p-text-muted-color); padding: 8px; `; modelsContainer.appendChild(emptyHint); } section.appendChild(modelsContainer); // 移除固定的高级设置区域 - 现在点击模型标签时弹出气泡框编辑 return section; } /** * 创建模型标签 */ _createModelTag(model, service, modelType) { const tag = document.createElement('div'); tag.className = `model-tag${model.is_default ? ' default' : ''}`; tag.dataset.modelName = model.name; tag.dataset.selected = 'false'; // 模型图标 const iconSpan = document.createElement('i'); iconSpan.className = 'pi pi-sparkles model-tag-icon'; tag.appendChild(iconSpan); // 模型名称 const nameSpan = document.createElement('span'); nameSpan.className = 'model-tag-name'; nameSpan.textContent = model.name; tag.appendChild(nameSpan); // 默认标记 if (model.is_default) { const defaultBadge = document.createElement('span'); defaultBadge.className = 'model-tag-badge'; defaultBadge.textContent = tUI('默认'); tag.appendChild(defaultBadge); } // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '×'; deleteBtn.className = 'model-delete-btn'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); this._deleteModel(service, modelType, model.name, tag); }); tag.appendChild(deleteBtn); // ---点击选中状态--- tag.addEventListener('click', (e) => { // 如果点击的是删除按钮,不触发选中 if (e.target.closest('.model-delete-btn')) { return; } // 移除同容器内其他标签的选中状态 const container = tag.parentElement; if (container) { container.querySelectorAll('.model-tag.selected').forEach(t => { t.classList.remove('selected'); }); } // 添加当前标签的选中状态 tag.classList.add('selected'); }); // ---右键菜单--- // 使用函数形式动态获取菜单项,确保每次显示菜单时都能获取最新的模型状态 const getMenuItems = () => { // 从本地数据中获取最新的模型状态 const models = modelType === 'llm' ? service.llm_models : service.vlm_models; const currentModel = models.find(m => m.name === model.name); const isDefault = currentModel ? currentModel.is_default : false; return [ { label: '设为默认模型', icon: 'pi-star', disabled: isDefault, // 动态获取当前是否为默认模型 onClick: () => { this._setDefaultModel(service, modelType, model.name, tag); } }, { separator: true }, // 分隔线 { label: '修改模型参数设置', icon: 'pi-cog', onClick: () => { this._selectModelForEdit(service, modelType, model.name, tag); } } ]; }; createContextMenu({ target: tag, items: getMenuItems }); return tag; } /** * 选中模型进行编辑(弹出气泡框) */ _selectModelForEdit(service, modelType, modelName, tagElement) { // 保存this引用 const self = this; // 获取模型数据 const models = modelType === 'llm' ? service.llm_models : service.vlm_models; const selectedModel = models.find(m => m.name === modelName); if (!selectedModel) return; // 弹出气泡框编辑参数 createConfirmPopup({ target: tagElement, message: `模型参数设置`, icon: 'pi-cog', position: 'top', confirmLabel: '保存', cancelLabel: '取消', renderFormContent: (formContainer) => { // 为表单容器添加横向布局类 formContainer.classList.add('model-params-form'); // 温度 (Temperature) const tempInput = createInputGroup('温度 (Temperature)', '0.0 - 2.0', 'number'); tempInput.input.min = '0'; tempInput.input.max = '2'; tempInput.input.step = '0.1'; tempInput.input.value = selectedModel.temperature ?? 0.7; tempInput.input.dataset.fieldName = 'temperature'; tempInput.group.style.width = '135px'; formContainer.appendChild(tempInput.group); // 核采样 (Top-P) const topPInput = createInputGroup('核采样 (Top-P)', '0.0 - 1.0', 'number'); topPInput.input.min = '0'; topPInput.input.max = '1'; topPInput.input.step = '0.1'; topPInput.input.value = selectedModel.top_p ?? 0.9; topPInput.input.dataset.fieldName = 'top_p'; topPInput.group.style.width = '135px'; formContainer.appendChild(topPInput.group); // 最大Token数 const maxTokensInput = createInputGroup('最大Token数', '1 - 8192', 'number'); maxTokensInput.input.min = '1'; maxTokensInput.input.max = '8192'; maxTokensInput.input.step = '1'; maxTokensInput.input.value = selectedModel.max_tokens ?? 4096; maxTokensInput.input.dataset.fieldName = 'max_tokens'; maxTokensInput.group.style.width = '135px'; formContainer.appendChild(maxTokensInput.group); }, onConfirm: async (formContainer) => { try { // 获取表单数据 const temperature = parseFloat(formContainer.querySelector('[data-field-name="temperature"]').value); const top_p = parseFloat(formContainer.querySelector('[data-field-name="top_p"]').value); const max_tokens = parseInt(formContainer.querySelector('[data-field-name="max_tokens"]').value); // 验证数据 if (isNaN(temperature) || temperature < 0 || temperature > 2) { app.extensionManager.toast.add({ severity: "warn", summary: "温度值无效", detail: "温度值应在 0 到 2 之间", life: 2000 }); throw new Error('温度值无效'); } if (isNaN(top_p) || top_p < 0 || top_p > 1) { app.extensionManager.toast.add({ severity: "warn", summary: "核采样值无效", detail: "核采样值应在 0 到 1 之间", life: 2000 }); throw new Error('核采样值无效'); } if (isNaN(max_tokens) || max_tokens < 1 || max_tokens > 8192) { app.extensionManager.toast.add({ severity: "warn", summary: "最大Token数无效", detail: "最大Token数应在 1 到 8192 之间", life: 2000 }); throw new Error('最大Token数无效'); } // 使用self代替this来调用方法 await self._updateModelParams(service.id, modelType, modelName, { temperature, top_p, max_tokens }); // 更新本地数据 selectedModel.temperature = temperature; selectedModel.top_p = top_p; selectedModel.max_tokens = max_tokens; app.extensionManager.toast.add({ severity: "success", summary: "参数已更新", detail: `${modelName} ${tUI('的参数已保存')}`, life: 2000 }); } catch (error) { logger.error('更新模型参数失败', error); app.extensionManager.toast.add({ severity: "error", summary: "更新失败", detail: error.message, life: 3000 }); throw error; } } }); } /** * 批量更新模型参数 */ async _updateModelParams(serviceId, modelType, modelName, params) { if (!serviceId) { logger.error("更新模型参数失败: serviceId为空"); throw new Error("服务ID不能为空"); } try { // 依次更新每个参数 for (const [paramName, paramValue] of Object.entries(params)) { const url = APIService.getApiUrl(`/services/${encodeURIComponent(serviceId)}/models/parameter`); logger.debug(`[v2] 正在更新参数: ${url}`, { modelType, modelName, paramName, paramValue }); const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: modelType, model_name: modelName, parameter_name: paramName, parameter_value: paramValue }) }); if (!res.ok) { const text = await res.text(); logger.error(`更新参数请求失败: ${res.status} ${res.statusText}`, text); throw new Error(`请求失败: ${res.status} ${res.statusText}`); } const text = await res.text(); try { const result = JSON.parse(text); if (!result.success) { throw new Error(result.error || '更新参数失败'); } } catch (e) { logger.error(`解析响应JSON失败: ${text}`, e); throw new Error(`解析响应失败: ${e.message}`); } } logger.debug(`已批量更新模型参数: ${modelName}`, params); } catch (error) { logger.error('批量更新模型参数失败', error); throw error; } } /** * 获取可用模型列表 */ async _getAvailableModels(service, modelType) { try { // 调用后端API获取模型列表 const res = await fetch(APIService.getApiUrl(`/services/${service.id}/models?model_type=${modelType}`)); const result = await res.json(); // 返回结果包含success、models或error return result; } catch (error) { logger.error(`获取模型列表异常: ${error.message}`); return { success: false, error: `网络错误: ${error.message}` }; } } /** * 显示添加模型列表框(使用多选组件) */ _showAddModelDialog(service, modelType, container) { // 获取触发按钮 const addBtn = event.target.closest('button'); // 使用新的多选listbox组件 createMultiSelectListbox({ triggerElement: addBtn, placeholder: `${tUI('搜索')}${modelType === 'llm' ? 'LLM' : 'VLM'}${tUI('模型...')}`, fetchItems: async () => { const result = await this._getAvailableModels(service, modelType); if (!result.success) { throw new Error(result.error || '获取模型列表失败'); } return result.models[modelType] || []; }, onConfirm: async (selectedModels, searchInputValue) => { // 如果没有勾选模型,但搜索框有内容,则将搜索框内容作为模型名称添加 if (selectedModels.length === 0 && searchInputValue && searchInputValue.trim()) { const modelName = searchInputValue.trim(); await this._addModel(service, modelType, modelName, container); } else { // 批量添加选中的模型 for (const modelName of selectedModels) { await this._addModel(service, modelType, modelName, container); } } } }); } /** * 获取推荐模型列表(已移除,返回空数组) */ async _getRecommendedModels(modelType) { // 推荐模型已移除,所有模型从服务商API获取 return []; } /** * 添加模型 */ async _addModel(service, modelType, modelName, container) { try { // 调用后端API添加模型 const res = await fetch(APIService.getApiUrl(`/services/${service.id}/models`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: modelType, model_name: modelName, temperature: 0.7, top_p: 0.9, max_tokens: 4096 }) }); const result = await res.json(); if (!result.success) { throw new Error(result.error || '添加模型失败'); } // 更新本地数据 const modelList = modelType === 'llm' ? service.llm_models : service.vlm_models; if (!modelList) { if (modelType === 'llm') { service.llm_models = []; } else { service.vlm_models = []; } } const updatedList = modelType === 'llm' ? service.llm_models : service.vlm_models; updatedList.push({ name: modelName, is_default: updatedList.length === 0, temperature: 0.7, top_p: 0.9, max_tokens: 4096 }); // 移除空提示 const emptyHint = container.querySelector('.empty-hint'); if (emptyHint) { emptyHint.remove(); } // 添加新标签 const newTag = this._createModelTag({ name: modelName, is_default: updatedList.length === 1 }, service, modelType); container.appendChild(newTag); // 初始化或更新Sortable(确保新添加的标签可以拖动) // 先销毁旧的Sortable实例(如果存在) if (container.sortableInstance) { container.sortableInstance.destroy(); } // 创建新的Sortable实例 container.sortableInstance = new Sortable(container, { animation: 150, ghostClass: 'sortable-ghost', handle: '.model-tag', onEnd: async (evt) => { await this._updateModelOrder(service.id, modelType, container); } }); app.extensionManager.toast.add({ severity: "success", summary: "模型已添加", life: 2000 }); } catch (error) { logger.error('添加模型失败', error); app.extensionManager.toast.add({ severity: "error", summary: "添加失败", detail: error.message, life: 3000 }); } } /** * 删除模型 */ async _deleteModel(service, modelType, modelName, tagElement) { // 使用createSettingsDialog创建确认窗口 createSettingsDialog({ title: `${tUI('确认删除')}`, isConfirmDialog: true, dialogClassName: 'confirm-dialog', saveButtonText: tUI('删除'), saveButtonIcon: 'pi-trash', isDangerButton: true, cancelButtonText: tUI('取消'), renderContent: (content) => { content.className = 'confirm-dialog-content-simple'; const confirmMessage = document.createElement('p'); confirmMessage.className = 'confirm-dialog-message-simple'; confirmMessage.textContent = `${tUI('确定要删除模型')} "${modelName}" ${tUI('吗?')}`; content.appendChild(confirmMessage); }, onSave: async () => { try { // 调用后端API删除模型 const res = await fetch(APIService.getApiUrl(`/services/${service.id}/models/${modelType}/${encodeURIComponent(modelName)}`), { method: 'DELETE' }); const result = await res.json(); if (!result.success) { throw new Error(result.error || '删除模型失败'); } // 更新本地数据 const models = modelType === 'llm' ? service.llm_models : service.vlm_models; const index = models.findIndex(m => m.name === modelName); if (index >= 0) { models.splice(index, 1); } // 移除标签 tagElement.remove(); // 如果删除后为空,显示空提示 const container = tagElement.parentElement; if (container && container.children.length === 0) { const emptyHint = document.createElement('div'); emptyHint.className = 'empty-hint'; emptyHint.textContent = tUI('暂无配置模型,点击"+ 添加模型"开始配置'); emptyHint.style.cssText = ` font-size: 12px; color: var(--p-text-muted-color); padding: 8px; `; container.appendChild(emptyHint); } app.extensionManager.toast.add({ severity: "success", summary: "模型已删除", life: 2000 }); return true; // 允许关闭对话框 } catch (error) { logger.error('删除模型失败', error); app.extensionManager.toast.add({ severity: "error", summary: "删除失败", detail: error.message, life: 3000 }); return false; // 阻止关闭对话框 } } }); } /** * 设置默认模型 */ async _setDefaultModel(service, modelType, modelName, tagElement) { try { // 调用后端API设置默认模型 const res = await fetch(APIService.getApiUrl(`/services/${service.id}/models/default`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: modelType, model_name: modelName }) }); const result = await res.json(); if (!result.success) { throw new Error(result.error || '设置默认模型失败'); } // 更新本地数据 const models = modelType === 'llm' ? service.llm_models : service.vlm_models; models.forEach(m => { m.is_default = m.name === modelName; }); // ---直接更新DOM,无需重新加载--- const container = tagElement?.parentElement; if (container) { // 移除所有标签的默认状态 container.querySelectorAll('.model-tag').forEach(tag => { tag.classList.remove('default'); // 移除旧的默认标记 const oldBadge = tag.querySelector('.model-tag-badge'); if (oldBadge) { oldBadge.remove(); } }); // 为新的默认模型添加样式和标记 if (tagElement) { tagElement.classList.add('default'); // 在名称后面添加默认标记 const nameSpan = tagElement.querySelector('.model-tag-name'); if (nameSpan) { const defaultBadge = document.createElement('span'); defaultBadge.className = 'model-tag-badge'; defaultBadge.textContent = tUI('默认'); nameSpan.after(defaultBadge); } } } app.extensionManager.toast.add({ severity: "success", summary: `${tUI('已设置')} "${modelName}" ${tUI('为默认模型')}`, life: 2000 }); } catch (error) { logger.error('设置默认模型失败', error); app.extensionManager.toast.add({ severity: "error", summary: "设置失败", detail: error.message, life: 3000 }); } } /** * 更新模型顺序 */ async _updateModelOrder(serviceId, modelType, container) { try { const modelTags = container.querySelectorAll('.model-tag'); const newOrder = Array.from(modelTags).map(tag => tag.dataset.modelName); // 调用后端API更新顺序 const res = await fetch(APIService.getApiUrl(`/services/${serviceId}/models/order`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: modelType, model_names: newOrder }) }); const result = await res.json(); if (!result.success) { throw new Error(result.error || '更新模型顺序失败'); } app.extensionManager.toast.add({ severity: "success", summary: "模型顺序已更新", life: 2000 }); } catch (error) { logger.error('更新模型顺序失败', error); app.extensionManager.toast.add({ severity: "error", summary: "更新失败", detail: error.message, life: 3000 }); } } /** * 删除服务商 */ async _deleteService(serviceId) { // 查找服务名称 const service = this.services.find(s => s.id === serviceId); const serviceName = service ? service.name : serviceId; // 使用createSettingsDialog创建确认窗口 createSettingsDialog({ title: `${tUI('确认删除')}`, isConfirmDialog: true, dialogClassName: 'confirm-dialog', saveButtonText: tUI('删除'), saveButtonIcon: 'pi-trash', isDangerButton: true, cancelButtonText: tUI('取消'), renderContent: (content) => { content.className = 'confirm-dialog-content-simple'; const confirmMessage = document.createElement('p'); confirmMessage.className = 'confirm-dialog-message-simple'; confirmMessage.textContent = `${tUI('确定要删除服务商')} "${serviceName}" ${tUI('吗?')}`; content.appendChild(confirmMessage); }, onSave: async () => { try { const res = await fetch(APIService.getApiUrl(`/services/${serviceId}`), { method: 'DELETE' }); const result = await res.json(); if (result.success) { app.extensionManager.toast.add({ severity: "success", summary: "删除成功", life: 3000 }); // 重新加载配置并刷新UI await this._loadAllConfigs(); // 查找并移除对应的标签和内容 const tabButton = document.querySelector(`.tab-button[data-tab="${serviceId}"]`); if (tabButton) { tabButton.remove(); } const tabPane = document.querySelector(`.tab-pane[data-tab="${serviceId}"]`); if (tabPane) { tabPane.remove(); } // 自动切换到百度翻译标签 const header = document.querySelector('.tab-header'); const contentContainer = document.querySelector('.tab-content'); if (header && contentContainer) { this._switchTab('baidu', header, contentContainer); } // 如果是最后一个服务商,显示空提示 const listContainer = document.querySelector('.services-list'); if (listContainer && this.services.length === 0) { const emptyHint = document.createElement('div'); emptyHint.style.cssText = ` text-align: center; padding: 40px; color: var(--p-text-muted-color); `; emptyHint.textContent = tUI('暂无服务商,点击"新增服务商"开始配置'); listContainer.appendChild(emptyHint); } // 触发配置同步事件 this.notifyConfigChange(); return true; // 允许关闭对话框 } else { throw new Error(result.error || '删除失败'); } } catch (error) { logger.error('删除服务商失败', error); app.extensionManager.toast.add({ severity: "error", summary: "删除失败", detail: error.message, life: 3000 }); return false; // 阻止关闭对话框 } } }); } /** * 更新服务商配置 */ async _updateService(serviceId, updates) { try { const res = await fetch(APIService.getApiUrl(`/services/${serviceId}`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); const result = await res.json(); if (!result.success) { throw new Error(result.error || '更新失败'); } // 同步更新本地内存中的服务商数据 const service = this.services.find(s => s.id === serviceId); if (service) { Object.assign(service, updates); } // 触发配置同步事件 this.notifyConfigChange(); logger.debug('服务商配置已更新', serviceId); // 显示成功提示 app.extensionManager.toast.add({ severity: "success", summary: "服务商配置已更新", life: 2000 }); } catch (error) { logger.error('更新服务商失败', error); app.extensionManager.toast.add({ severity: "error", summary: "更新失败", detail: error.message, life: 3000 }); } } /** * 加载掩码后的API Key * @param {string} serviceId 服务商ID * @returns {Promise} 掩码后的API Key */ async _loadMaskedApiKey(serviceId) { try { const res = await fetch(APIService.getApiUrl(`/services/${serviceId}/masked`)); const result = await res.json(); if (result.success && result.service) { return result.service.api_key_masked || null; } return null; } catch (error) { logger.error('加载掩码API Key失败', error); return null; } } } // 导出API配置管理器实例 export const apiConfigManager = new APIConfigManager();