/**
* 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();