Spaces:
Sleeping
Sleeping
| /** | |
| * 模型选择器类 - 处理模型选择下拉列表的所有交互 | |
| */ | |
| class ModelSelector { | |
| /** | |
| * 构造函数 | |
| * @param {Object} options 配置选项 | |
| * @param {Function} options.onChange 选择变更回调 | |
| * @param {Object} options.models 模型定义对象 | |
| * @param {Object} options.providers 提供商定义对象 | |
| */ | |
| constructor(options) { | |
| this.options = options || {}; | |
| this.models = this.options.models || {}; | |
| this.providers = this.options.providers || {}; | |
| this.onChange = this.options.onChange || (() => {}); | |
| // 元素引用 | |
| this.container = document.getElementById('modelSelector'); | |
| this.display = this.container?.querySelector('.model-display'); | |
| this.currentNameEl = document.getElementById('currentModelName'); | |
| this.currentProviderEl = document.getElementById('currentModelProvider'); | |
| this.badgesContainer = document.getElementById('modelBadges'); | |
| this.originalSelect = document.getElementById('modelSelect'); | |
| // 创建下拉面板和遮罩 | |
| this.createDropdownElements(); | |
| // 当前选中的模型ID | |
| this.selectedModelId = null; | |
| // 初始化 | |
| this.initEvents(); | |
| } | |
| /** | |
| * 创建下拉面板和遮罩元素 | |
| */ | |
| createDropdownElements() { | |
| // 创建遮罩层 | |
| this.overlay = document.createElement('div'); | |
| this.overlay.className = 'model-dropdown-overlay'; | |
| document.body.appendChild(this.overlay); | |
| // 创建下拉面板 | |
| this.dropdown = document.createElement('div'); | |
| this.dropdown.className = 'model-dropdown-panel'; | |
| document.body.appendChild(this.dropdown); | |
| } | |
| /** | |
| * 初始化事件监听 | |
| */ | |
| initEvents() { | |
| if (!this.display) return; | |
| // 点击选择器显示下拉框 | |
| this.display.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.toggleDropdown(); | |
| }); | |
| // 点击遮罩层关闭下拉框 | |
| this.overlay.addEventListener('click', () => { | |
| this.closeDropdown(); | |
| }); | |
| // 监听原始select变化,保持同步 | |
| if (this.originalSelect) { | |
| this.originalSelect.addEventListener('change', () => { | |
| const modelId = this.originalSelect.value; | |
| if (modelId && modelId !== this.selectedModelId) { | |
| this.selectModel(modelId, false); // 不触发onChange避免循环 | |
| } | |
| }); | |
| } | |
| // 防止面板内部点击冒泡到遮罩 | |
| this.dropdown.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| console.log('模型选择器事件初始化完成'); | |
| } | |
| /** | |
| * 加载模型选项到下拉面板 | |
| */ | |
| loadModelOptions() { | |
| if (!this.dropdown) return; | |
| // 清空下拉面板 | |
| this.dropdown.innerHTML = ''; | |
| // 按提供商分组模型 | |
| const groupedModels = {}; | |
| Object.entries(this.models).forEach(([modelId, model]) => { | |
| const providerId = model.provider; | |
| if (!groupedModels[providerId]) { | |
| groupedModels[providerId] = []; | |
| } | |
| groupedModels[providerId].push({ id: modelId, ...model }); | |
| }); | |
| // 创建分组选项 | |
| Object.entries(groupedModels).forEach(([providerId, models]) => { | |
| const provider = this.providers[providerId] || { name: providerId }; | |
| // 创建分组容器 | |
| const group = document.createElement('div'); | |
| group.className = 'model-group'; | |
| // 创建分组标题 | |
| const title = document.createElement('div'); | |
| title.className = 'model-group-title'; | |
| title.textContent = provider.name; | |
| group.appendChild(title); | |
| // 添加该分组的模型选项 | |
| models.sort((a, b) => a.name.localeCompare(b.name)) | |
| .forEach(model => { | |
| const option = document.createElement('div'); | |
| option.className = 'model-option'; | |
| option.dataset.modelId = model.id; | |
| if (model.id === this.selectedModelId) { | |
| option.classList.add('selected'); | |
| } | |
| // 模型名称 - 移除版本显示 | |
| const nameEl = document.createElement('div'); | |
| nameEl.className = 'model-option-name'; | |
| nameEl.textContent = model.name; | |
| option.appendChild(nameEl); | |
| // 能力徽章 | |
| if (model.supportsMultimodal || model.isReasoning) { | |
| const badges = document.createElement('div'); | |
| badges.className = 'model-option-badges'; | |
| if (model.supportsMultimodal) { | |
| const badge = document.createElement('div'); | |
| badge.className = 'model-option-badge'; | |
| badge.title = '支持图像'; | |
| badge.innerHTML = '<i class="fas fa-image"></i>'; | |
| badges.appendChild(badge); | |
| } | |
| if (model.isReasoning) { | |
| const badge = document.createElement('div'); | |
| badge.className = 'model-option-badge'; | |
| badge.title = '支持深度推理'; | |
| badge.innerHTML = '<i class="fas fa-brain"></i>'; | |
| badges.appendChild(badge); | |
| } | |
| option.appendChild(badges); | |
| } | |
| // 点击选项选择模型 | |
| option.addEventListener('click', () => { | |
| this.selectModel(model.id); | |
| this.closeDropdown(); | |
| }); | |
| group.appendChild(option); | |
| }); | |
| // 只添加有模型的分组 | |
| if (group.childElementCount > 1) { // > 1 因为包含标题 | |
| this.dropdown.appendChild(group); | |
| } | |
| }); | |
| console.log('模型选项加载完成'); | |
| } | |
| /** | |
| * 打开下拉面板 | |
| */ | |
| openDropdown() { | |
| if (!this.container || !this.dropdown || !this.overlay) return; | |
| // 加载模型选项 | |
| this.loadModelOptions(); | |
| // 显示遮罩和下拉面板 | |
| this.overlay.style.display = 'block'; | |
| this.dropdown.style.display = 'block'; | |
| // 设置面板位置 - 相对于视口 | |
| this.adjustDropdownPosition(); | |
| // 添加窗口调整大小和滚动事件监听器 | |
| window.addEventListener('resize', this.adjustDropdownPosition.bind(this)); | |
| window.addEventListener('scroll', this.adjustDropdownPosition.bind(this)); | |
| // 延迟添加可见类以启用过渡效果 | |
| setTimeout(() => { | |
| this.dropdown.classList.add('visible'); | |
| }, 10); | |
| // 添加打开状态类 | |
| this.container.classList.add('open'); | |
| // 确保当前选中的选项可见 | |
| setTimeout(() => { | |
| const selectedOption = this.dropdown.querySelector('.model-option.selected'); | |
| if (selectedOption) { | |
| selectedOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| } | |
| }, 100); | |
| } | |
| /** | |
| * 调整下拉面板位置 | |
| */ | |
| adjustDropdownPosition() { | |
| if (!this.display || !this.dropdown) return; | |
| // 获取模型选择器的位置 | |
| const rect = this.display.getBoundingClientRect(); | |
| // 设置下拉面板宽度 | |
| this.dropdown.style.width = `${rect.width}px`; | |
| // 计算最佳位置 | |
| const viewportHeight = window.innerHeight; | |
| const spaceBelow = viewportHeight - rect.bottom; | |
| const spaceAbove = rect.top; | |
| const dropdownHeight = Math.min(300, Math.max(this.dropdown.scrollHeight, 100)); | |
| // 检查下方空间是否足够 | |
| if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) { | |
| // 显示在下方 | |
| this.dropdown.style.top = `${rect.bottom + 5}px`; | |
| this.dropdown.style.bottom = 'auto'; | |
| } else { | |
| // 显示在上方 | |
| this.dropdown.style.bottom = `${viewportHeight - rect.top + 5}px`; | |
| this.dropdown.style.top = 'auto'; | |
| } | |
| // 水平定位 | |
| this.dropdown.style.left = `${rect.left}px`; | |
| // 检查是否超出右侧边界 | |
| const rightEdge = rect.left + rect.width; | |
| const viewportWidth = window.innerWidth; | |
| if (rightEdge > viewportWidth) { | |
| this.dropdown.style.left = 'auto'; | |
| this.dropdown.style.right = '10px'; | |
| } | |
| } | |
| /** | |
| * 关闭下拉面板 | |
| */ | |
| closeDropdown() { | |
| if (!this.container || !this.dropdown || !this.overlay) return; | |
| // 移除可见类 | |
| this.dropdown.classList.remove('visible'); | |
| // 移除打开状态类 | |
| this.container.classList.remove('open'); | |
| // 移除事件监听器 | |
| window.removeEventListener('resize', this.adjustDropdownPosition.bind(this)); | |
| window.removeEventListener('scroll', this.adjustDropdownPosition.bind(this)); | |
| // 延迟隐藏元素,以便完成过渡动画 | |
| setTimeout(() => { | |
| this.overlay.style.display = 'none'; | |
| this.dropdown.style.display = 'none'; | |
| }, 200); | |
| } | |
| /** | |
| * 切换下拉面板显示状态 | |
| */ | |
| toggleDropdown() { | |
| if (this.container.classList.contains('open')) { | |
| this.closeDropdown(); | |
| } else { | |
| this.openDropdown(); | |
| } | |
| } | |
| /** | |
| * 选择模型 | |
| * @param {string} modelId 模型ID | |
| * @param {boolean} triggerChange 是否触发onChange回调,默认true | |
| */ | |
| selectModel(modelId, triggerChange = true) { | |
| const model = this.models[modelId]; | |
| if (!model) return; | |
| // 更新选中的模型ID | |
| this.selectedModelId = modelId; | |
| // 更新显示信息 | |
| this.updateDisplayInfo(model); | |
| // 更新原始select | |
| if (this.originalSelect && this.originalSelect.value !== modelId) { | |
| this.originalSelect.value = modelId; | |
| } | |
| // 触发变更回调 | |
| if (triggerChange) { | |
| this.onChange(modelId, model); | |
| } | |
| console.log('已选择模型:', modelId); | |
| } | |
| /** | |
| * 更新显示信息 | |
| * @param {Object} model 模型信息 | |
| */ | |
| updateDisplayInfo(model) { | |
| if (!this.currentNameEl || !this.currentProviderEl || !this.badgesContainer) return; | |
| // 更新模型名称 - 不在这里显示版本号 | |
| this.currentNameEl.textContent = model.name; | |
| // 更新提供商 | |
| const provider = this.providers[model.provider] || { name: model.provider }; | |
| this.currentProviderEl.textContent = provider.name; | |
| // 更新能力徽章 | |
| this.badgesContainer.innerHTML = ''; | |
| if (model.supportsMultimodal) { | |
| const badge = document.createElement('div'); | |
| badge.className = 'model-badge'; | |
| badge.title = '支持图像'; | |
| badge.innerHTML = '<i class="fas fa-image"></i>'; | |
| this.badgesContainer.appendChild(badge); | |
| } | |
| if (model.isReasoning) { | |
| const badge = document.createElement('div'); | |
| badge.className = 'model-badge'; | |
| badge.title = '支持深度推理'; | |
| badge.innerHTML = '<i class="fas fa-brain"></i>'; | |
| this.badgesContainer.appendChild(badge); | |
| } | |
| } | |
| /** | |
| * 设置模型数据 | |
| * @param {Object} models 模型定义对象 | |
| * @param {Object} providers 提供商定义对象 | |
| */ | |
| setModelData(models, providers) { | |
| this.models = models || {}; | |
| this.providers = providers || {}; | |
| } | |
| } | |
| class SettingsManager { | |
| constructor() { | |
| // 初始化属性 | |
| this.modelDefinitions = {}; | |
| this.providerDefinitions = {}; | |
| // 初始化界面元素 | |
| this.initializeElements(); | |
| // 提示词配置 | |
| this.prompts = {}; | |
| this.currentPromptId = 'default'; | |
| // 模型选择器对象 | |
| this.modelSelector = null; | |
| // OCR源配置 | |
| this.ocrSource = 'auto'; // 默认自动选择 | |
| // 存储API密钥的对象 | |
| this.apiKeyValues = { | |
| 'AnthropicApiKey': '', | |
| 'OpenaiApiKey': '', | |
| 'DeepseekApiKey': '', | |
| 'AlibabaApiKey': '', | |
| 'GoogleApiKey': '', | |
| 'DoubaoApiKey': '', | |
| 'BaiduApiKey': '', | |
| 'BaiduSecretKey': '', | |
| 'MathpixAppId': '', | |
| 'MathpixAppKey': '' | |
| }; | |
| // 存储API基础URL的对象 | |
| this.apiBaseUrlValues = { | |
| 'AnthropicApiBaseUrl': '', | |
| 'OpenaiApiBaseUrl': '', | |
| 'DeepseekApiBaseUrl': '', | |
| 'AlibabaApiBaseUrl': '', | |
| 'GoogleApiBaseUrl': '', | |
| 'DoubaoApiBaseUrl': '' | |
| }; | |
| // 加载模型配置 | |
| this.isInitialized = false; | |
| this.initialize(); | |
| } | |
| async initialize() { | |
| try { | |
| // 加载模型配置 | |
| await this.loadModelConfig(); | |
| // 成功加载配置后,执行后续初始化 | |
| this.updateModelOptions(); | |
| await this.loadSettings(); | |
| await this.loadPrompts(); // 加载提示词配置 | |
| this.setupEventListeners(); | |
| this.updateUIBasedOnModelType(); | |
| // 刷新API密钥状态 | |
| await this.refreshApiKeyStatus(); | |
| // 刷新API基础URL状态 | |
| await this.refreshApiBaseUrlStatus(); | |
| // 初始化可折叠内容逻辑 | |
| this.initCollapsibleContent(); | |
| // 初始化Token显示 | |
| if (this.maxTokens && this.maxTokensValue) { | |
| this.updateTokenValueDisplay(); | |
| this.highlightActivePreset(); | |
| } | |
| // 初始化模型选择器 | |
| this.initModelSelector(); | |
| // 添加到window对象,方便在控制台调试 | |
| window.debugModelSelector = { | |
| open: () => this.modelSelector?.openDropdown(), | |
| close: () => this.modelSelector?.closeDropdown(), | |
| toggle: () => this.modelSelector?.toggleDropdown(), | |
| instance: this.modelSelector | |
| }; | |
| // 绑定提示词预览区域点击事件 | |
| this.initPromptPreviewEvents(); | |
| this.isInitialized = true; | |
| console.log('设置管理器初始化完成'); | |
| } catch (error) { | |
| console.error('初始化设置管理器失败:', error); | |
| window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error'); | |
| // 使用默认配置作为备份 | |
| this.setupDefaultModels(); | |
| this.updateModelOptions(); | |
| await this.loadSettings(); | |
| await this.loadPrompts(); // 加载提示词配置 | |
| this.setupEventListeners(); | |
| this.updateUIBasedOnModelType(); | |
| // 刷新API密钥状态(即使在出错情况下也尝试) | |
| try { | |
| await this.refreshApiKeyStatus(); | |
| await this.refreshApiBaseUrlStatus(); | |
| } catch (e) { | |
| console.error('刷新API状态失败:', e); | |
| } | |
| // 初始化可折叠内容逻辑 | |
| this.initCollapsibleContent(); | |
| // 初始化模型选择器 | |
| this.initModelSelector(); | |
| this.isInitialized = true; | |
| } | |
| } | |
| // 初始化新的模型选择器 | |
| initModelSelector() { | |
| if (this.modelSelector) { | |
| // 如果已存在,更新数据 | |
| this.modelSelector.setModelData(this.modelDefinitions, this.providerDefinitions); | |
| } else { | |
| // 创建新实例 | |
| this.modelSelector = new ModelSelector({ | |
| models: this.modelDefinitions, | |
| providers: this.providerDefinitions, | |
| onChange: (modelId) => { | |
| // 处理模型变更 | |
| console.log('模型已变更:', modelId); | |
| this.updateVisibleApiKey(modelId); | |
| this.updateUIBasedOnModelType(); | |
| this.updateModelVersionDisplay(modelId); | |
| this.saveSettings(); | |
| } | |
| }); | |
| } | |
| // 设置当前选择的模型 | |
| if (this.modelSelect && this.modelSelect.value) { | |
| this.modelSelector.selectModel(this.modelSelect.value, false); | |
| } | |
| } | |
| // 更新模型选择下拉框 | |
| updateModelOptions() { | |
| // 清空现有选项 | |
| this.modelSelect.innerHTML = ''; | |
| // 提取提供商信息 | |
| const providers = {}; | |
| Object.entries(this.providerDefinitions).forEach(([providerId, provider]) => { | |
| providers[providerId] = provider.name; | |
| }); | |
| // 为每个提供商创建一个选项组 | |
| for (const [providerId, providerName] of Object.entries(providers)) { | |
| const optgroup = document.createElement('optgroup'); | |
| optgroup.label = providerName; | |
| // 过滤该提供商的模型 | |
| const providerModels = Object.entries(this.modelDefinitions) | |
| .filter(([_, modelInfo]) => modelInfo.provider === providerId) | |
| .sort((a, b) => a[1].name.localeCompare(b[1].name)); | |
| // 添加该提供商的模型选项 | |
| for (const [modelId, modelInfo] of providerModels) { | |
| // 添加到原始select元素 | |
| const option = document.createElement('option'); | |
| option.value = modelId; | |
| // 只显示模型名称,不再显示版本号 | |
| option.textContent = modelInfo.name; | |
| optgroup.appendChild(option); | |
| } | |
| // 只添加有模型的提供商 | |
| if (optgroup.children.length > 0) { | |
| this.modelSelect.appendChild(optgroup); | |
| } | |
| } | |
| } | |
| async loadSettings() { | |
| try { | |
| // 先从localStorage加载大部分设置 | |
| const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}'); | |
| // 刷新API密钥状态(自动从服务器获取最新状态) | |
| await this.refreshApiKeyStatus(); | |
| console.log('已自动刷新API密钥状态'); | |
| // 加载 API 基础 URL 设置 | |
| if (settings.apiBaseUrlValues) { | |
| this.apiBaseUrlValues = settings.apiBaseUrlValues; | |
| await this.refreshApiBaseUrlStatus(); | |
| console.log('已加载 API 基础 URL 设置'); | |
| } | |
| // 加载其他设置 | |
| // Load model selection | |
| if (settings.model && this.modelExists(settings.model)) { | |
| this.modelSelect.value = settings.model; | |
| this.updateVisibleApiKey(settings.model); | |
| // 使用新的模型选择器更新UI | |
| if (this.modelSelector) { | |
| this.modelSelector.selectModel(settings.model, false); | |
| } | |
| } | |
| // Load max tokens setting - 现在直接设置输入框值 | |
| if (settings.maxTokens) { | |
| this.maxTokens.value = settings.maxTokens; | |
| this.updateTokenValueDisplay(); | |
| this.highlightActivePreset(); | |
| } | |
| // Load reasoning depth & think budget settings | |
| if (settings.reasoningDepth) { | |
| this.reasoningDepthSelect.value = settings.reasoningDepth; | |
| // 更新推理深度选项UI | |
| this.updateReasoningOptionUI(settings.reasoningDepth); | |
| } | |
| // 加载豆包思考模式设置 | |
| if (settings.doubaoThinkingMode && this.doubaoThinkingModeSelect) { | |
| this.doubaoThinkingModeSelect.value = settings.doubaoThinkingMode; | |
| // 更新豆包思考选项UI | |
| this.updateDoubaoThinkingOptionUI(settings.doubaoThinkingMode); | |
| } | |
| // 加载思考预算百分比 | |
| const thinkBudgetPercent = parseInt(settings.thinkBudgetPercent || '50'); | |
| if (this.thinkBudgetPercentInput) { | |
| this.thinkBudgetPercentInput.value = thinkBudgetPercent; | |
| } | |
| // 更新思考预算显示和滑块背景 | |
| this.updateThinkBudgetDisplay(); | |
| this.updateThinkBudgetSliderBackground(); | |
| this.highlightActiveThinkPreset(); | |
| // Load temperature setting | |
| if (settings.temperature) { | |
| this.temperatureInput.value = settings.temperature; | |
| } | |
| // 先记录用户设置的提示词ID(如果有) | |
| if (settings.currentPromptId) { | |
| this.currentPromptId = settings.currentPromptId; | |
| } | |
| // 如果系统提示词内容已保存在设置中,先恢复它 | |
| if (settings.systemPrompt) { | |
| this.systemPromptInput.value = settings.systemPrompt; | |
| } | |
| if (settings.language) { | |
| this.languageInput.value = settings.language; | |
| } | |
| // Load proxy settings | |
| if (settings.proxyEnabled !== undefined) { | |
| this.proxyEnabledInput.checked = settings.proxyEnabled; | |
| this.proxySettings.style.display = settings.proxyEnabled ? 'block' : 'none'; | |
| } | |
| if (settings.proxyHost) { | |
| this.proxyHostInput.value = settings.proxyHost; | |
| } | |
| if (settings.proxyPort) { | |
| this.proxyPortInput.value = settings.proxyPort; | |
| } | |
| // Load OCR source setting | |
| if (settings.ocrSource) { | |
| this.ocrSource = settings.ocrSource; | |
| if (this.ocrSourceSelect) { | |
| this.ocrSourceSelect.value = settings.ocrSource; | |
| } | |
| } | |
| // Update UI based on model type | |
| this.updateUIBasedOnModelType(); | |
| } catch (error) { | |
| console.error('加载设置出错:', error); | |
| window.uiManager?.showToast('加载设置出错', 'error'); | |
| } | |
| } | |
| modelExists(modelId) { | |
| return this.modelDefinitions.hasOwnProperty(modelId); | |
| } | |
| // 更新模型版本显示 | |
| updateModelVersionDisplay(modelId) { | |
| const modelVersionInfo = document.getElementById('modelVersionInfo'); | |
| const modelVersionText = document.getElementById('modelVersionText'); | |
| if (!modelVersionText || !modelVersionInfo) return; | |
| const model = this.modelDefinitions[modelId]; | |
| if (!model) { | |
| modelVersionText.textContent = '-'; | |
| modelVersionInfo.classList.remove('has-version'); | |
| return; | |
| } | |
| // 显示版本信息(如果有) | |
| if (model.version && model.version !== 'latest') { | |
| // 设置版本文本 | |
| modelVersionText.textContent = model.version; | |
| // 添加具有版本的类 | |
| modelVersionInfo.classList.add('has-version'); | |
| // 统一使用分支图标和紫色 | |
| const icon = modelVersionInfo.querySelector('i'); | |
| if (icon) { | |
| icon.className = 'fas fa-code-branch'; | |
| icon.title = `版本 ${model.version}`; | |
| } | |
| // 移除所有特定版本类型的类 | |
| modelVersionInfo.classList.remove('date-version', 'semantic-version'); | |
| } else if (model.version === 'latest') { | |
| // 使用英文"latest"而不是中文 | |
| modelVersionText.textContent = 'latest'; | |
| modelVersionInfo.classList.add('has-version'); | |
| // 对latest版本也使用相同的分支图标 | |
| const icon = modelVersionInfo.querySelector('i'); | |
| if (icon) { | |
| icon.className = 'fas fa-code-branch'; | |
| icon.title = '最新版本'; | |
| } | |
| // 移除所有特定版本类型的类 | |
| modelVersionInfo.classList.remove('date-version', 'semantic-version'); | |
| } else { | |
| modelVersionText.textContent = '-'; | |
| modelVersionInfo.classList.remove('has-version', 'date-version', 'semantic-version'); | |
| } | |
| } | |
| /** | |
| * 根据选择的模型类型更新UI显示 | |
| */ | |
| updateUIBasedOnModelType() { | |
| // 更新UI元素显示,根据所选模型类型 | |
| const selectedModel = this.modelSelect.value; | |
| const modelInfo = this.modelDefinitions[selectedModel]; | |
| // 更新当前可见的API密钥 | |
| this.updateVisibleApiKey(selectedModel); | |
| if (!modelInfo) return; | |
| // 处理温度设置显示 | |
| if (this.temperatureGroup) { | |
| this.temperatureGroup.style.display = modelInfo.isReasoning ? 'none' : 'block'; | |
| } | |
| // 处理深度推理设置显示 | |
| const isAnthropicReasoning = modelInfo.isReasoning && modelInfo.provider === 'anthropic'; | |
| // 只有对Claude 3.7 Sonnet这样的Anthropic推理模型才显示深度推理设置 | |
| if (this.reasoningSettingGroup) { | |
| this.reasoningSettingGroup.style.display = isAnthropicReasoning ? 'block' : 'none'; | |
| } | |
| // 只有当启用深度推理且是Anthropic推理模型时才显示思考预算设置 | |
| if (this.thinkBudgetGroup) { | |
| const showThinkBudget = isAnthropicReasoning && | |
| this.reasoningDepthSelect && | |
| this.reasoningDepthSelect.value === 'extended'; | |
| this.thinkBudgetGroup.style.display = showThinkBudget ? 'block' : 'none'; | |
| } | |
| // 处理豆包深度思考设置显示 | |
| const isDoubaoReasoning = modelInfo.isReasoning && modelInfo.provider === 'doubao'; | |
| // 只有对豆包推理模型才显示深度思考设置 | |
| if (this.doubaoThinkingGroup) { | |
| this.doubaoThinkingGroup.style.display = isDoubaoReasoning ? 'block' : 'none'; | |
| } | |
| // 控制最大Token设置的显示 | |
| // 阿里巴巴模型不支持自定义Token设置 | |
| const maxTokensGroup = this.maxTokens ? this.maxTokens.closest('.setting-group') : null; | |
| if (maxTokensGroup) { | |
| // 如果是阿里巴巴模型,隐藏Token设置 | |
| const isAlibabaModel = modelInfo.provider === 'alibaba'; | |
| maxTokensGroup.style.display = isAlibabaModel ? 'none' : 'block'; | |
| } | |
| // 更新模型版本显示 | |
| this.updateModelVersionDisplay(selectedModel); | |
| } | |
| /** | |
| * 根据选择的模型更新显示的API密钥 | |
| * @param {string} modelType 模型类型 | |
| */ | |
| updateVisibleApiKey(modelType) { | |
| // 高亮显示对应的API密钥 | |
| const allApiKeys = document.querySelectorAll('.api-key-status'); | |
| // 先清除所有高亮 | |
| allApiKeys.forEach(key => { | |
| key.classList.remove('highlight'); | |
| }); | |
| // 根据模型类型高亮相应的API密钥 | |
| let apiKeyToHighlight = null; | |
| if (modelType && modelType.toLowerCase().includes('claude')) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(1)'); // Anthropic | |
| } else if (modelType && (modelType.toLowerCase().includes('gpt') || modelType.toLowerCase().includes('openai'))) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(2)'); // OpenAI | |
| } else if (modelType && modelType.toLowerCase().includes('deepseek')) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(3)'); // DeepSeek | |
| } else if (modelType && (modelType.toLowerCase().includes('qwen') || modelType.toLowerCase().includes('qvq') || modelType.toLowerCase().includes('alibaba'))) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba | |
| } else if (modelType && (modelType.toLowerCase().includes('gemini') || modelType.toLowerCase().includes('google'))) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(5)'); // Google | |
| } else if (modelType && modelType.toLowerCase().includes('doubao')) { | |
| apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(6)'); // 豆包 | |
| } | |
| if (apiKeyToHighlight) { | |
| apiKeyToHighlight.classList.add('highlight'); | |
| } | |
| } | |
| async saveSettings() { | |
| try { | |
| // 保存UI设置到localStorage(不包含API密钥) | |
| const settings = { | |
| apiKeys: this.apiKeyValues, // 保存到localStorage(向后兼容) | |
| apiBaseUrlValues: this.apiBaseUrlValues, // 添加API基础URL保存到localStorage | |
| model: this.modelSelect.value, | |
| maxTokens: this.maxTokens.value, | |
| reasoningDepth: this.reasoningDepthSelect?.value || 'standard', | |
| doubaoThinkingMode: this.doubaoThinkingModeSelect?.value || 'auto', | |
| thinkBudgetPercent: this.thinkBudgetPercentInput?.value || '50', | |
| temperature: this.temperatureInput.value, | |
| language: this.languageInput.value, | |
| systemPrompt: this.systemPromptInput.value, | |
| currentPromptId: this.currentPromptId, | |
| proxyEnabled: this.proxyEnabledInput.checked, | |
| proxyHost: this.proxyHostInput.value, | |
| proxyPort: this.proxyPortInput.value, | |
| ocrSource: this.ocrSource // 添加OCR源配置保存 | |
| }; | |
| // 保存设置到localStorage | |
| localStorage.setItem('aiSettings', JSON.stringify(settings)); | |
| window.uiManager?.showToast('设置已保存', 'success'); | |
| } catch (error) { | |
| console.error('保存设置出错:', error); | |
| window.uiManager?.showToast('保存设置出错: ' + error.message, 'error'); | |
| } | |
| } | |
| getApiKey() { | |
| const selectedModel = this.modelSelect.value; | |
| const modelInfo = this.modelDefinitions[selectedModel]; | |
| if (!modelInfo) return ''; | |
| const apiKeyId = modelInfo.apiKeyId; | |
| const apiKey = this.apiKeyInputs[apiKeyId]?.value; | |
| if (!apiKey) { | |
| window.showToast('Please enter API key for the selected model', 'error'); | |
| return ''; | |
| } | |
| return apiKey; | |
| } | |
| getSettings() { | |
| const language = this.languageInput.value || '中文'; | |
| const basePrompt = this.systemPromptInput.value || ''; | |
| // 直接使用带语言提示的系统提示词 | |
| // 注意:systemPromptInput.value 可能已经在 loadPrompt 中设置了语言提示 | |
| // 为避免重复添加,我们先提取提示词的主体部分(不含语言提示) | |
| const promptMainPart = basePrompt.split("\n\n请务必使用")[0]; | |
| const systemPrompt = `${promptMainPart}\n\n请务必使用${language}回答。`; | |
| const selectedModel = this.modelSelect.value; | |
| const modelInfo = this.modelDefinitions[selectedModel] || {}; | |
| // 获取最大Token数 | |
| const maxTokens = parseInt(this.maxTokens?.value || '8192'); | |
| // 获取推理深度设置 | |
| const reasoningDepth = this.reasoningDepthSelect?.value || 'standard'; | |
| const thinkBudgetPercent = parseInt(this.thinkBudgetPercentInput?.value || '50'); | |
| // 获取豆包思考模式设置 | |
| const doubaoThinkingMode = this.doubaoThinkingModeSelect?.value || 'auto'; | |
| // 计算思考预算的实际Token数 | |
| const thinkBudget = Math.floor(maxTokens * (thinkBudgetPercent / 100)); | |
| // 构建推理配置参数 | |
| const reasoningConfig = {}; | |
| // 处理不同模型的推理配置 | |
| if (modelInfo.isReasoning) { | |
| // 对于Anthropic模型 | |
| if (modelInfo.provider === 'anthropic') { | |
| if (reasoningDepth === 'extended') { | |
| reasoningConfig.reasoning_depth = 'extended'; | |
| reasoningConfig.think_budget = thinkBudget; | |
| } else { | |
| reasoningConfig.speed_mode = 'instant'; | |
| } | |
| } | |
| // 对于豆包模型 | |
| if (modelInfo.provider === 'doubao') { | |
| reasoningConfig.thinking_mode = doubaoThinkingMode; | |
| } | |
| } | |
| // 从apiKeyValues获取Mathpix信息,而不是直接从DOM读取 | |
| const mathpixAppId = this.apiKeyValues['MathpixAppId'] || ''; | |
| const mathpixAppKey = this.apiKeyValues['MathpixAppKey'] || ''; | |
| const mathpixApiKey = mathpixAppId && mathpixAppKey ? `${mathpixAppId}:${mathpixAppKey}` : ''; | |
| // 从apiBaseUrlValues映射到服务器API所需格式 | |
| const apiBaseUrls = {}; | |
| if (this.apiBaseUrlValues) { | |
| if (this.apiBaseUrlValues['AnthropicApiBaseUrl']) { | |
| apiBaseUrls.anthropic = this.apiBaseUrlValues['AnthropicApiBaseUrl']; | |
| } | |
| if (this.apiBaseUrlValues['OpenaiApiBaseUrl']) { | |
| apiBaseUrls.openai = this.apiBaseUrlValues['OpenaiApiBaseUrl']; | |
| } | |
| if (this.apiBaseUrlValues['DeepseekApiBaseUrl']) { | |
| apiBaseUrls.deepseek = this.apiBaseUrlValues['DeepseekApiBaseUrl']; | |
| } | |
| if (this.apiBaseUrlValues['AlibabaApiBaseUrl']) { | |
| apiBaseUrls.alibaba = this.apiBaseUrlValues['AlibabaApiBaseUrl']; | |
| } | |
| if (this.apiBaseUrlValues['GoogleApiBaseUrl']) { | |
| apiBaseUrls.google = this.apiBaseUrlValues['GoogleApiBaseUrl']; | |
| } | |
| if (this.apiBaseUrlValues['DoubaoApiBaseUrl']) { | |
| apiBaseUrls.doubao = this.apiBaseUrlValues['DoubaoApiBaseUrl']; | |
| } | |
| } | |
| return { | |
| model: selectedModel, | |
| maxTokens: maxTokens, | |
| temperature: this.temperatureInput.value, | |
| language: language, | |
| systemPrompt: systemPrompt, | |
| proxyEnabled: this.proxyEnabledInput.checked, | |
| proxyHost: this.proxyHostInput.value, | |
| proxyPort: this.proxyPortInput.value, | |
| mathpixApiKey: mathpixApiKey, | |
| ocrSource: this.ocrSource, // 添加OCR源配置 | |
| doubaoThinkingMode: doubaoThinkingMode, // 添加豆包思考模式配置 | |
| modelInfo: { | |
| supportsMultimodal: modelInfo.supportsMultimodal || false, | |
| isReasoning: modelInfo.isReasoning || false, | |
| provider: modelInfo.provider || 'unknown' | |
| }, | |
| reasoningConfig: reasoningConfig, | |
| apiBaseUrls: apiBaseUrls, | |
| apiKeys: this.apiKeyValues // 确保传递API密钥 | |
| }; | |
| } | |
| getModelCapabilities(modelId) { | |
| const model = this.modelDefinitions[modelId]; | |
| if (!model) return { supportsMultimodal: false, isReasoning: false }; | |
| return { | |
| supportsMultimodal: model.supportsMultimodal, | |
| isReasoning: model.isReasoning | |
| }; | |
| } | |
| setupEventListeners() { | |
| this.modelSelect.addEventListener('change', (e) => { | |
| this.updateVisibleApiKey(e.target.value); | |
| this.updateUIBasedOnModelType(); | |
| this.updateModelVersionDisplay(e.target.value); | |
| this.saveSettings(); | |
| // 通知应用更新图像操作按钮 | |
| if (window.app && typeof window.app.updateImageActionButtons === 'function') { | |
| window.app.updateImageActionButtons(); | |
| } | |
| }); | |
| // 提示词相关事件监听 | |
| if (this.promptSelect) { | |
| this.promptSelect.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 加载选中的提示词 | |
| this.loadPrompt(e.target.value); | |
| }); | |
| } | |
| // 保存提示词按钮 | |
| if (this.savePromptBtn) { | |
| this.savePromptBtn.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 打开编辑对话框 | |
| this.openEditPromptDialog(); | |
| }); | |
| } | |
| // 新建提示词按钮 | |
| if (this.newPromptBtn) { | |
| this.newPromptBtn.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 打开新建对话框 | |
| this.openNewPromptDialog(); | |
| }); | |
| } | |
| // 删除提示词按钮 | |
| if (this.deletePromptBtn) { | |
| this.deletePromptBtn.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 删除当前提示词 | |
| this.deletePrompt(); | |
| }); | |
| } | |
| // 提示词对话框相关事件 | |
| if (this.cancelPromptBtn) { | |
| this.cancelPromptBtn.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 关闭对话框 | |
| this.closePromptDialog(); | |
| }); | |
| } | |
| if (this.confirmPromptBtn) { | |
| this.confirmPromptBtn.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 保存提示词 | |
| this.savePrompt(); | |
| }); | |
| } | |
| if (this.promptDialogOverlay) { | |
| this.promptDialogOverlay.addEventListener('click', (e) => { | |
| // 点击遮罩关闭对话框 | |
| this.closePromptDialog(); | |
| }); | |
| } | |
| // 最大Token输入框事件处理 | |
| if (this.maxTokens) { | |
| this.maxTokens.addEventListener('input', () => { | |
| this.updateTokenValueDisplay(); | |
| this.updateTokenSliderBackground(); | |
| this.highlightActivePreset(); | |
| this.saveSettings(); | |
| }); | |
| this.maxTokens.addEventListener('change', () => { | |
| this.saveSettings(); | |
| }); | |
| } | |
| // 推理深度选择事件处理 - 新增标签式UI | |
| if (this.reasoningOptions && this.reasoningOptions.length > 0) { | |
| this.reasoningOptions.forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 获取选择的值 | |
| const value = option.getAttribute('data-value'); | |
| // 更新隐藏的select元素值 | |
| if (this.reasoningDepthSelect) { | |
| this.reasoningDepthSelect.value = value; | |
| } | |
| // 更新视觉效果 | |
| this.reasoningOptions.forEach(opt => { | |
| opt.classList.remove('active'); | |
| }); | |
| option.classList.add('active'); | |
| // 更新思考预算组的可见性 | |
| if (this.thinkBudgetGroup) { | |
| const showThinkBudget = value === 'extended'; | |
| this.thinkBudgetGroup.style.display = showThinkBudget ? 'block' : 'none'; | |
| } | |
| this.saveSettings(); | |
| }); | |
| }); | |
| } | |
| // 思考预算预设按钮事件 | |
| if (this.thinkPresets && this.thinkPresets.length > 0) { | |
| this.thinkPresets.forEach(preset => { | |
| preset.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 获取预设值 | |
| const value = parseInt(preset.getAttribute('data-value')); | |
| // 更新滑块值 | |
| if (this.thinkBudgetPercentInput) { | |
| this.thinkBudgetPercentInput.value = value; | |
| // 更新显示和滑块背景 | |
| this.updateThinkBudgetDisplay(); | |
| this.updateThinkBudgetSliderBackground(); | |
| } | |
| // 更新预设按钮样式 | |
| this.highlightActiveThinkPreset(); | |
| this.saveSettings(); | |
| }); | |
| }); | |
| } | |
| // 思考预算占比滑块事件处理 | |
| if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) { | |
| this.thinkBudgetPercentInput.addEventListener('input', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 更新思考预算显示 | |
| this.updateThinkBudgetDisplay(); | |
| // 更新滑块背景 | |
| this.updateThinkBudgetSliderBackground(); | |
| // 更新预设按钮高亮状态 | |
| this.highlightActiveThinkPreset(); | |
| }); | |
| this.thinkBudgetPercentInput.addEventListener('change', () => { | |
| this.saveSettings(); | |
| }); | |
| } | |
| this.temperatureInput.addEventListener('input', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| this.saveSettings(); | |
| }); | |
| this.languageInput.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 如果当前有加载提示词,重新加载它以更新语言 | |
| if (this.currentPromptId && this.prompts[this.currentPromptId]) { | |
| this.loadPrompt(this.currentPromptId); | |
| } else { | |
| // 没有当前提示词,只保存设置 | |
| this.saveSettings(); | |
| } | |
| }); | |
| this.proxyEnabledInput.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| this.proxySettings.style.display = e.target.checked ? 'block' : 'none'; | |
| this.saveSettings(); | |
| }); | |
| this.proxyHostInput.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| this.saveSettings(); | |
| }); | |
| this.proxyPortInput.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| this.saveSettings(); | |
| }); | |
| // OCR源选择器事件监听 | |
| if (this.ocrSourceSelect) { | |
| this.ocrSourceSelect.addEventListener('change', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 更新OCR源配置 | |
| this.ocrSource = e.target.value; | |
| this.saveSettings(); | |
| console.log('OCR源已切换为:', this.ocrSource); | |
| }); | |
| } | |
| // Panel visibility | |
| if (this.settingsToggle) { | |
| this.settingsToggle.addEventListener('click', () => { | |
| this.toggleSettingsPanel(); | |
| }); | |
| } | |
| if (this.closeSettings) { | |
| this.closeSettings.addEventListener('click', () => { | |
| this.closeSettingsPanel(); | |
| }); | |
| } | |
| // 确保设置面板自身的点击不会干扰内部操作 | |
| if (this.settingsPanel) { | |
| const settingsSections = this.settingsPanel.querySelectorAll('.settings-section'); | |
| settingsSections.forEach(section => { | |
| section.addEventListener('click', (e) => { | |
| // 只阻止直接点击设置部分的事件 | |
| if (e.target === section) { | |
| e.stopPropagation(); | |
| } | |
| }); | |
| }); | |
| // 设置内容区域防止冒泡 | |
| const settingsContent = this.settingsPanel.querySelector('.settings-content'); | |
| if (settingsContent) { | |
| settingsContent.addEventListener('click', (e) => { | |
| // 只阻止直接点击设置内容区域的事件 | |
| if (e.target === settingsContent) { | |
| e.stopPropagation(); | |
| } | |
| }); | |
| } | |
| } | |
| if (this.tokenPresets) { | |
| this.tokenPresets.forEach(preset => { | |
| preset.addEventListener('click', e => { | |
| const value = parseInt(e.currentTarget.dataset.value); | |
| this.maxTokens.value = value; | |
| this.updateTokenValueDisplay(); | |
| this.highlightActivePreset(); | |
| this.saveSettings(); | |
| }); | |
| }); | |
| } | |
| // 主题切换监听 | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', () => { | |
| const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; | |
| const newTheme = currentTheme === 'light' ? 'dark' : 'light'; | |
| document.documentElement.setAttribute('data-theme', newTheme); | |
| localStorage.setItem('theme', newTheme); | |
| // 更新滑块背景 | |
| this.updateTokenSliderBackground(); | |
| this.updateThinkBudgetSliderBackground(); | |
| }); | |
| } | |
| // 确保自定义模型选择器事件监听器被初始化 | |
| if (this.modelSelectorDisplay && this.modelDropdown) { | |
| this.initCustomSelectorEvents(); | |
| } | |
| // 初始化API基础URL编辑功能 | |
| this.initApiBaseUrlEditFunctions(); | |
| // 初始化API密钥编辑功能 | |
| this.initApiKeyEditFunctions(); | |
| // 初始化推理选项事件 | |
| this.initReasoningOptionEvents(); | |
| // 初始化豆包思考选项事件 | |
| this.initDoubaoThinkingOptionEvents(); | |
| } | |
| // 初始化推理选项事件 | |
| initReasoningOptionEvents() { | |
| const reasoningOptions = document.querySelectorAll('.reasoning-option'); | |
| reasoningOptions.forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const value = option.getAttribute('data-value'); | |
| if (value && this.reasoningDepthSelect) { | |
| // 更新select值 | |
| this.reasoningDepthSelect.value = value; | |
| // 更新UI | |
| this.updateReasoningOptionUI(value); | |
| // 保存设置 | |
| this.saveSettings(); | |
| } | |
| }); | |
| }); | |
| } | |
| // 初始化豆包思考选项事件 | |
| initDoubaoThinkingOptionEvents() { | |
| const doubaoThinkingOptions = document.querySelectorAll('.doubao-thinking-option'); | |
| doubaoThinkingOptions.forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const value = option.getAttribute('data-value'); | |
| if (value && this.doubaoThinkingModeSelect) { | |
| // 更新select值 | |
| this.doubaoThinkingModeSelect.value = value; | |
| // 更新UI | |
| this.updateDoubaoThinkingOptionUI(value); | |
| // 保存设置 | |
| this.saveSettings(); | |
| } | |
| }); | |
| }); | |
| } | |
| // 更新豆包思考选项UI | |
| updateDoubaoThinkingOptionUI(value) { | |
| const doubaoThinkingOptions = document.querySelectorAll('.doubao-thinking-option'); | |
| doubaoThinkingOptions.forEach(option => { | |
| const optionValue = option.getAttribute('data-value'); | |
| if (optionValue === value) { | |
| option.classList.add('active'); | |
| } else { | |
| option.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| // 更新思考预算显示 | |
| updateThinkBudgetDisplay() { | |
| if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) { | |
| const percent = parseInt(this.thinkBudgetPercentInput.value); | |
| this.thinkBudgetPercentValue.textContent = `${percent}%`; | |
| } | |
| } | |
| // 更新思考预算滑块背景 | |
| updateThinkBudgetSliderBackground() { | |
| if (!this.thinkBudgetPercentInput) return; | |
| const min = parseInt(this.thinkBudgetPercentInput.min); | |
| const max = parseInt(this.thinkBudgetPercentInput.max); | |
| const value = parseInt(this.thinkBudgetPercentInput.value); | |
| const percentage = ((value - min) / (max - min)) * 100; | |
| // 获取当前主题 | |
| const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const primaryColor = isDarkMode ? 'rgba(72, 149, 239, 0.8)' : 'rgba(58, 134, 255, 0.8)'; | |
| const secondaryColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | |
| this.thinkBudgetPercentInput.style.background = `linear-gradient(to right, | |
| ${primaryColor} 0%, | |
| ${primaryColor} ${percentage}%, | |
| ${secondaryColor} ${percentage}%, | |
| ${secondaryColor} 100%)`; | |
| } | |
| // 更新推理深度选项UI | |
| updateReasoningOptionUI(value) { | |
| if (!this.reasoningOptions) return; | |
| this.reasoningOptions.forEach(option => { | |
| const optionValue = option.getAttribute('data-value'); | |
| if (optionValue === value) { | |
| option.classList.add('active'); | |
| } else { | |
| option.classList.remove('active'); | |
| } | |
| }); | |
| // 更新思考预算组的可见性 | |
| if (this.thinkBudgetGroup) { | |
| const showThinkBudget = value === 'extended'; | |
| this.thinkBudgetGroup.style.display = showThinkBudget ? 'block' : 'none'; | |
| } | |
| } | |
| // 高亮当前激活的思考预算预设按钮 | |
| highlightActiveThinkPreset() { | |
| if (!this.thinkPresets || !this.thinkBudgetPercentInput) return; | |
| const value = parseInt(this.thinkBudgetPercentInput.value); | |
| this.thinkPresets.forEach(preset => { | |
| const presetValue = parseInt(preset.getAttribute('data-value')); | |
| if (presetValue === value) { | |
| preset.classList.add('active'); | |
| } else { | |
| preset.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| /** | |
| * 初始化可折叠内容的交互逻辑 | |
| */ | |
| initCollapsibleContent() { | |
| const collapsibleHeaders = document.querySelectorAll('.collapsible-header'); | |
| collapsibleHeaders.forEach(header => { | |
| header.addEventListener('click', () => { | |
| const content = header.nextElementSibling; | |
| if (content && content.classList.contains('collapsible-content')) { | |
| // 切换展开/折叠状态 | |
| content.classList.toggle('expanded'); | |
| // 切换箭头方向 | |
| const arrow = header.querySelector('i.fa-chevron-down, i.fa-chevron-up'); | |
| if (arrow) { | |
| arrow.classList.toggle('fa-chevron-down'); | |
| arrow.classList.toggle('fa-chevron-up'); | |
| } | |
| } | |
| }); | |
| }); | |
| // 默认展开中转 API url 设置区域 | |
| const apiBaseUrlHeader = document.querySelector('.api-url-settings .collapsible-header'); | |
| if (apiBaseUrlHeader) { | |
| const content = apiBaseUrlHeader.nextElementSibling; | |
| if (content) { | |
| content.classList.add('expanded'); | |
| const arrow = apiBaseUrlHeader.querySelector('i.fa-chevron-down'); | |
| if (arrow) { | |
| arrow.classList.remove('fa-chevron-down'); | |
| arrow.classList.add('fa-chevron-up'); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * 初始化API密钥编辑相关功能 | |
| */ | |
| initApiKeyEditFunctions() { | |
| // 1. 编辑按钮点击事件 | |
| document.querySelectorAll('.edit-api-key').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const keyType = e.currentTarget.getAttribute('data-key-type'); | |
| const keyStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (keyStatus) { | |
| // 隐藏显示区域 | |
| const displayArea = keyStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.add('hidden'); | |
| // 显示编辑区域 | |
| const editArea = keyStatus.querySelector('.key-edit'); | |
| if (editArea) { | |
| editArea.classList.remove('hidden'); | |
| // 获取当前密钥值并填入输入框 | |
| const keyInput = editArea.querySelector('.key-input'); | |
| if (keyInput) { | |
| // 从状态文本中获取当前值(如果不是"未设置") | |
| const statusElement = keyStatus.querySelector('.key-status'); | |
| if (statusElement && statusElement.textContent !== '未设置') { | |
| keyInput.value = this.apiKeyValues[keyType] || ''; | |
| } else { | |
| keyInput.value = ''; | |
| } | |
| // 聚焦输入框 | |
| setTimeout(() => { | |
| keyInput.focus(); | |
| }, 100); | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| // 2. 保存按钮点击事件 | |
| document.querySelectorAll('.save-api-key').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const keyType = e.currentTarget.getAttribute('data-key-type'); | |
| const keyStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (keyStatus) { | |
| // 获取输入的新密钥值 | |
| const keyInput = keyStatus.querySelector('.key-input'); | |
| if (keyInput) { | |
| const newValue = keyInput.value.trim(); | |
| // 保存到内存中 | |
| this.apiKeyValues[keyType] = newValue; | |
| // 创建要保存的API密钥对象 | |
| const apiKeysToSave = {}; | |
| apiKeysToSave[keyType] = newValue; | |
| // 保存到服务器 | |
| this.saveApiKey(keyType, newValue, keyStatus); | |
| // 隐藏编辑区域 | |
| const editArea = keyStatus.querySelector('.key-edit'); | |
| if (editArea) editArea.classList.add('hidden'); | |
| // 显示状态区域 | |
| const displayArea = keyStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.remove('hidden'); | |
| } | |
| } | |
| }); | |
| }); | |
| // 3. 切换密码可见性按钮 | |
| document.querySelectorAll('.toggle-visibility').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const keyInput = e.currentTarget.closest('.key-edit').querySelector('.key-input'); | |
| if (keyInput) { | |
| const type = keyInput.type === 'password' ? 'text' : 'password'; | |
| keyInput.type = type; | |
| // 更新图标 | |
| const icon = e.currentTarget.querySelector('i'); | |
| if (icon) { | |
| icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`; | |
| } | |
| } | |
| }); | |
| }); | |
| // 4. 输入框按下Enter保存 | |
| document.querySelectorAll('.key-input').forEach(input => { | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const saveButton = e.currentTarget.closest('.key-edit').querySelector('.save-api-key'); | |
| if (saveButton) { | |
| saveButton.click(); | |
| } | |
| } else if (e.key === 'Escape') { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 取消编辑 | |
| const keyStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (keyStatus) { | |
| const editArea = keyStatus.querySelector('.key-edit'); | |
| if (editArea) editArea.classList.add('hidden'); | |
| const displayArea = keyStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.remove('hidden'); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 更新API密钥状态显示 | |
| * @param {Object} apiKeys 密钥对象 | |
| */ | |
| updateApiKeyStatus(apiKeys) { | |
| if (!this.apiKeysList) return; | |
| // 保存API密钥值到内存中 | |
| for (const [key, value] of Object.entries(apiKeys)) { | |
| this.apiKeyValues[key] = value; | |
| } | |
| // 找到所有密钥状态元素 | |
| Object.keys(apiKeys).forEach(keyId => { | |
| const statusElement = document.getElementById(`${keyId}Status`); | |
| if (!statusElement) return; | |
| const value = apiKeys[keyId]; | |
| if (value && value.trim() !== '') { | |
| // 显示密钥状态 - 已设置 | |
| statusElement.className = 'key-status set'; | |
| statusElement.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`; | |
| } else { | |
| // 显示密钥状态 - 未设置 | |
| statusElement.className = 'key-status not-set'; | |
| statusElement.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`; | |
| } | |
| }); | |
| } | |
| /** | |
| * 保存单个API密钥 | |
| * @param {string} keyType 密钥类型 | |
| * @param {string} value 密钥值 | |
| * @param {HTMLElement} keyStatus 密钥状态容器 | |
| */ | |
| async saveApiKey(keyType, value, keyStatus) { | |
| try { | |
| // 显示保存中状态 | |
| const saveToast = this.createToast('正在保存密钥...', 'info', true); | |
| // 创建要保存的数据对象 | |
| const apiKeysData = {}; | |
| apiKeysData[keyType] = value; | |
| // 发送到服务器 | |
| const response = await fetch('/api/keys', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(apiKeysData) | |
| }); | |
| // 移除保存中提示 | |
| if (saveToast) { | |
| saveToast.remove(); | |
| } | |
| if (response.ok) { | |
| const result = await response.json(); | |
| if (result.success) { | |
| // 更新密钥状态显示 | |
| const statusElem = document.getElementById(`${keyType}Status`); | |
| if (statusElem) { | |
| if (value && value.trim() !== '') { | |
| statusElem.className = 'key-status set'; | |
| statusElem.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`; | |
| } else { | |
| statusElem.className = 'key-status not-set'; | |
| statusElem.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`; | |
| } | |
| } | |
| // 改用全局UIManager的showToast方法来显示成功消息 | |
| if (window.uiManager) { | |
| window.uiManager.showToast('密钥已保存', 'success'); | |
| } else { | |
| // 如果UIManager不可用,使用自己的方法作为备选 | |
| this.createToast('密钥已保存', 'success'); | |
| } | |
| } else { | |
| if (window.uiManager) { | |
| window.uiManager.showToast('保存密钥失败: ' + result.message, 'error'); | |
| } else { | |
| this.createToast('保存密钥失败: ' + result.message, 'error'); | |
| } | |
| } | |
| } else { | |
| if (window.uiManager) { | |
| window.uiManager.showToast('无法连接到服务器', 'error'); | |
| } else { | |
| this.createToast('无法连接到服务器', 'error'); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('保存密钥出错:', error); | |
| if (window.uiManager) { | |
| window.uiManager.showToast('保存密钥出错: ' + error.message, 'error'); | |
| } else { | |
| this.createToast('保存密钥出错: ' + error.message, 'error'); | |
| } | |
| } | |
| } | |
| /** | |
| * 创建一个Toast提示消息 | |
| * @param {string} message 提示消息内容 | |
| * @param {string} type 提示类型:'success', 'error', 'warning', 'info' | |
| * @param {boolean} persistent 是否为持久性提示(需要手动关闭) | |
| */ | |
| createToast(message, type = 'success', persistent = false) { | |
| const toastContainer = document.querySelector('.toast-container') || this.createToastContainer(); | |
| // 创建Toast元素 | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| if (persistent) { | |
| toast.classList.add('persistent'); | |
| } | |
| // 设置消息内容 | |
| toast.textContent = message; | |
| // 如果是持久性提示,添加关闭按钮 | |
| if (persistent) { | |
| const closeBtn = document.createElement('button'); | |
| closeBtn.className = 'toast-close'; | |
| closeBtn.innerHTML = '×'; | |
| closeBtn.addEventListener('click', () => { | |
| toast.remove(); | |
| }); | |
| toast.appendChild(closeBtn); | |
| } | |
| // 添加到容器 | |
| toastContainer.appendChild(toast); | |
| // 非持久性提示自动消失 | |
| if (!persistent) { | |
| setTimeout(() => { | |
| toast.remove(); | |
| }, 3000); | |
| } | |
| return toast; | |
| } | |
| /** | |
| * 创建Toast容器 | |
| * @returns {HTMLElement} Toast容器元素 | |
| */ | |
| createToastContainer() { | |
| const container = document.createElement('div'); | |
| container.className = 'toast-container'; | |
| document.body.appendChild(container); | |
| return container; | |
| } | |
| /** | |
| * 刷新API密钥状态 | |
| * 每次加载设置时自动调用,无需用户手动点击按钮 | |
| */ | |
| async refreshApiKeyStatus() { | |
| try { | |
| // 先将所有状态显示为"检查中" | |
| Object.keys(this.apiKeyValues).forEach(keyId => { | |
| const statusElement = document.getElementById(`${keyId}Status`); | |
| if (statusElement) { | |
| statusElement.className = 'key-status checking'; | |
| statusElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 检查中...'; | |
| } | |
| }); | |
| // 发送请求获取API密钥状态 | |
| const response = await fetch('/api/keys', { | |
| method: 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| } | |
| }); | |
| if (response.ok) { | |
| const apiKeys = await response.json(); | |
| this.updateApiKeyStatus(apiKeys); | |
| console.log('API密钥状态已刷新'); | |
| } else { | |
| console.error('刷新API密钥状态失败'); | |
| } | |
| } catch (error) { | |
| console.error('刷新API密钥状态出错:', error); | |
| } | |
| } | |
| toggleSettingsPanel() { | |
| if (this.settingsPanel) { | |
| this.settingsPanel.classList.toggle('active'); | |
| } | |
| } | |
| closeSettingsPanel() { | |
| if (this.settingsPanel) { | |
| this.settingsPanel.classList.remove('active'); | |
| } | |
| } | |
| /** | |
| * 从服务器加载提示词列表 | |
| */ | |
| async loadPrompts() { | |
| try { | |
| // 从服务器获取提示词配置 | |
| const response = await fetch('/api/prompts'); | |
| if (response.ok) { | |
| // 解析提示词数据 | |
| const prompts = await response.json(); | |
| // 存储提示词数据(保持原始顺序) | |
| this.prompts = prompts; | |
| // 添加提示词顺序属性,记录从服务器获取的原始顺序 | |
| this.promptsOrder = Object.keys(prompts); | |
| // 更新提示词选择下拉框 | |
| if (this.promptSelect) { | |
| this.updatePromptSelect(); | |
| } | |
| // 如果当前已有选中的提示词,尝试加载它 | |
| if (this.currentPromptId && this.prompts[this.currentPromptId]) { | |
| this.loadPrompt(this.currentPromptId); | |
| } | |
| // 否则尝试加载默认提示词 | |
| else if (this.prompts.default) { | |
| this.loadPrompt('default'); | |
| } | |
| // 否则加载第一个提示词 | |
| else if (Object.keys(this.prompts).length > 0) { | |
| const promptIdToLoad = Object.keys(this.prompts)[0]; | |
| this.loadPrompt(promptIdToLoad); | |
| } | |
| console.log('提示词加载成功:', this.prompts); | |
| } else { | |
| console.error('加载提示词失败:', response.statusText); | |
| } | |
| } catch (error) { | |
| console.error('加载提示词错误:', error); | |
| // 显示错误描述 | |
| if (this.promptDescriptionElement) { | |
| this.promptDescriptionElement.innerHTML = '<p>加载提示词错误,请检查网络连接</p>'; | |
| } | |
| } | |
| } | |
| /** | |
| * 更新提示词选择下拉框 | |
| */ | |
| updatePromptSelect() { | |
| if (!this.promptSelect) return; | |
| // 暂存当前选中的提示词ID | |
| const currentPromptId = this.promptSelect.value; | |
| // 清空下拉框 | |
| this.promptSelect.innerHTML = ''; | |
| // 按prompts.json中的原始顺序添加提示词选项 | |
| // 使用this.promptsOrder来保持原始顺序 | |
| if (this.promptsOrder && this.promptsOrder.length > 0) { | |
| for (const promptId of this.promptsOrder) { | |
| if (this.prompts[promptId]) { | |
| const prompt = this.prompts[promptId]; | |
| const option = document.createElement('option'); | |
| option.value = promptId; | |
| option.textContent = prompt.name; | |
| this.promptSelect.appendChild(option); | |
| } | |
| } | |
| } else { | |
| // 如果没有保存顺序,则使用对象键的顺序(不推荐,但作为后备方案) | |
| for (const promptId in this.prompts) { | |
| const prompt = this.prompts[promptId]; | |
| const option = document.createElement('option'); | |
| option.value = promptId; | |
| option.textContent = prompt.name; | |
| this.promptSelect.appendChild(option); | |
| } | |
| } | |
| // 恢复之前选中的提示词或选择第一个提示词 | |
| if (currentPromptId && this.prompts[currentPromptId]) { | |
| this.promptSelect.value = currentPromptId; | |
| } else if (this.promptsOrder && this.promptsOrder.length > 0) { | |
| // 选择原始顺序的第一个提示词 | |
| this.promptSelect.value = this.promptsOrder[0]; | |
| // 更新当前提示词ID和描述显示 | |
| this.loadPrompt(this.promptSelect.value); | |
| } else if (Object.keys(this.prompts).length > 0) { | |
| // 如果没有原始顺序,选择第一个提示词 | |
| this.promptSelect.value = Object.keys(this.prompts)[0]; | |
| // 更新当前提示词ID和描述显示 | |
| this.loadPrompt(this.promptSelect.value); | |
| } | |
| } | |
| /** | |
| * 加载指定的提示词 | |
| * @param {string} promptId 提示词ID | |
| */ | |
| loadPrompt(promptId) { | |
| if (!this.prompts[promptId]) return; | |
| // 更新当前提示词ID | |
| this.currentPromptId = promptId; | |
| // 获取当前选择的语言 | |
| const language = this.languageInput.value || '中文'; | |
| // 获取原始提示词内容 | |
| const basePrompt = this.prompts[promptId].content; | |
| // 添加语言指令(如果原始提示词中不包含语言指令) | |
| let systemPrompt = basePrompt; | |
| if (!basePrompt.includes('Please respond in') && !basePrompt.includes('请用') && !basePrompt.includes('使用')) { | |
| systemPrompt = `${basePrompt}\n\n请务必使用${language}回答。`; | |
| } | |
| // 更新提示词输入框 (隐藏,但仍需保存正确的内容) | |
| this.systemPromptInput.value = systemPrompt; | |
| // 更新提示词描述显示 - 使用完整的系统提示词,包括语言指令 | |
| if (this.promptDescriptionElement) { | |
| const description = this.prompts[promptId].description || systemPrompt; | |
| this.promptDescriptionElement.innerHTML = `<p>${description}</p>`; | |
| } | |
| // 更新提示词选择下拉框 | |
| if (this.promptSelect) { | |
| this.promptSelect.value = promptId; | |
| } | |
| // 保存设置 | |
| this.saveSettings(); | |
| } | |
| /** | |
| * 保存当前提示词到服务器 | |
| * @param {boolean} isNew 是否是新建提示词 | |
| */ | |
| async savePrompt(isNew = false) { | |
| try { | |
| // 获取输入的提示词信息 | |
| const promptId = this.promptIdInput.value.trim(); | |
| const promptName = this.promptNameInput.value.trim(); | |
| const promptContent = this.promptContentInput.value.trim(); | |
| const promptDescription = this.promptDescriptionInput.value.trim(); | |
| // 验证必填字段 | |
| if (!promptId) { | |
| window.uiManager?.showToast('提示词ID不能为空', 'error'); | |
| return; | |
| } | |
| if (!promptName) { | |
| window.uiManager?.showToast('提示词名称不能为空', 'error'); | |
| return; | |
| } | |
| if (!promptContent) { | |
| window.uiManager?.showToast('提示词内容不能为空', 'error'); | |
| return; | |
| } | |
| // 构建提示词数据 | |
| const promptData = { | |
| id: promptId, | |
| name: promptName, | |
| content: promptContent, | |
| description: promptDescription | |
| }; | |
| // 发送到服务器 | |
| const response = await fetch('/api/prompts', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(promptData) | |
| }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| if (result.success) { | |
| // 更新本地提示词列表 | |
| this.prompts[promptId] = { | |
| name: promptName, | |
| content: promptContent, | |
| description: promptDescription | |
| }; | |
| // 如果是新增提示词,将其添加到顺序数组末尾 | |
| if (isNew && this.promptsOrder && !this.promptsOrder.includes(promptId)) { | |
| this.promptsOrder.push(promptId); | |
| } | |
| // 更新提示词选择下拉框 | |
| this.updatePromptSelect(); | |
| // 加载新保存的提示词 | |
| this.loadPrompt(promptId); | |
| // 关闭对话框 | |
| this.closePromptDialog(); | |
| window.uiManager?.showToast('提示词已保存', 'success'); | |
| } else { | |
| window.uiManager?.showToast('保存提示词失败: ' + result.error, 'error'); | |
| } | |
| } else { | |
| window.uiManager?.showToast('无法连接到服务器', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('保存提示词出错:', error); | |
| window.uiManager?.showToast('保存提示词出错: ' + error.message, 'error'); | |
| } | |
| } | |
| /** | |
| * 删除当前提示词 | |
| */ | |
| async deletePrompt() { | |
| try { | |
| // 获取当前提示词ID | |
| const promptId = this.currentPromptId; | |
| if (!promptId || !this.prompts[promptId]) { | |
| window.uiManager?.showToast('未选择提示词', 'error'); | |
| return; | |
| } | |
| // 弹窗确认删除 | |
| if (!confirm(`确定要删除提示词 "${this.prompts[promptId].name}" 吗?`)) { | |
| return; | |
| } | |
| // 发送到服务器 | |
| const response = await fetch(`/api/prompts/${promptId}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| if (result.success) { | |
| // 从顺序数组中移除该提示词 | |
| if (this.promptsOrder && this.promptsOrder.includes(promptId)) { | |
| this.promptsOrder = this.promptsOrder.filter(id => id !== promptId); | |
| } | |
| // 删除本地提示词 | |
| delete this.prompts[promptId]; | |
| // 更新提示词选择下拉框 | |
| this.updatePromptSelect(); | |
| // 如果还有其他提示词,加载第一个 | |
| if (this.promptsOrder && this.promptsOrder.length > 0) { | |
| this.loadPrompt(this.promptsOrder[0]); | |
| } else if (Object.keys(this.prompts).length > 0) { | |
| this.loadPrompt(Object.keys(this.prompts)[0]); | |
| } else { | |
| // 如果没有提示词了,清空输入框和描述显示 | |
| this.systemPromptInput.value = ''; | |
| if (this.promptDescriptionElement) { | |
| this.promptDescriptionElement.innerHTML = '<p>暂无提示词,请点击"+"创建新提示词</p>'; | |
| } | |
| this.currentPromptId = ''; | |
| } | |
| window.uiManager?.showToast('提示词已删除', 'success'); | |
| } else { | |
| window.uiManager?.showToast('删除提示词失败: ' + result.error, 'error'); | |
| } | |
| } else { | |
| window.uiManager?.showToast('无法连接到服务器', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('删除提示词出错:', error); | |
| window.uiManager?.showToast('删除提示词出错: ' + error.message, 'error'); | |
| } | |
| } | |
| /** | |
| * 打开新建提示词对话框 | |
| */ | |
| openNewPromptDialog() { | |
| // 清空输入框 | |
| this.promptIdInput.value = ''; | |
| this.promptNameInput.value = ''; | |
| this.promptContentInput.value = ''; | |
| this.promptDescriptionInput.value = ''; | |
| // 启用ID输入框 | |
| this.promptIdInput.disabled = false; | |
| // 显示对话框 | |
| this.promptDialog.classList.add('active'); | |
| this.promptDialogOverlay.classList.add('active'); | |
| } | |
| /** | |
| * 打开编辑提示词对话框 | |
| */ | |
| openEditPromptDialog() { | |
| // 获取当前提示词ID | |
| const promptId = this.currentPromptId; | |
| if (!promptId || !this.prompts[promptId]) { | |
| // 如果没有选择提示词,但有系统提示词内容,将其作为新提示词 | |
| if (this.systemPromptInput.value.trim()) { | |
| this.openNewPromptDialog(); | |
| return; | |
| } | |
| window.uiManager?.showToast('未选择提示词', 'error'); | |
| return; | |
| } | |
| // 填充输入框 | |
| this.promptIdInput.value = promptId; | |
| this.promptNameInput.value = this.prompts[promptId].name; | |
| this.promptContentInput.value = this.prompts[promptId].content; | |
| this.promptDescriptionInput.value = this.prompts[promptId].description || ''; | |
| // 禁用ID输入框(不允许修改ID) | |
| this.promptIdInput.disabled = true; | |
| // 显示对话框 | |
| this.promptDialog.classList.add('active'); | |
| this.promptDialogOverlay.classList.add('active'); | |
| } | |
| /** | |
| * 关闭提示词对话框 | |
| */ | |
| closePromptDialog() { | |
| if (this.promptDialog) { | |
| this.promptDialog.classList.remove('active'); | |
| } | |
| if (this.promptDialogOverlay) { | |
| this.promptDialogOverlay.classList.remove('active'); | |
| } | |
| } | |
| // 更新token值显示 | |
| updateTokenValueDisplay() { | |
| const value = parseInt(this.maxTokens.value); | |
| let displayValue = value.toString(); | |
| // 格式化大数字显示 | |
| if (value >= 1000) { | |
| if (value % 1000 === 0) { | |
| displayValue = (value / 1000) + 'K'; | |
| } else { | |
| displayValue = (value / 1000).toFixed(1) + 'K'; | |
| } | |
| } | |
| this.maxTokensValue.textContent = displayValue; | |
| this.updateTokenSliderBackground(); | |
| } | |
| // 更新滑块背景 | |
| updateTokenSliderBackground() { | |
| const min = parseInt(this.maxTokens.min); | |
| const max = parseInt(this.maxTokens.max); | |
| const value = parseInt(this.maxTokens.value); | |
| const percentage = ((value - min) / (max - min)) * 100; | |
| // 获取当前主题 | |
| const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const primaryColor = isDarkMode ? 'rgba(72, 149, 239, 0.8)' : 'rgba(58, 134, 255, 0.8)'; | |
| const secondaryColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | |
| this.maxTokens.style.background = `linear-gradient(to right, | |
| ${primaryColor} 0%, | |
| ${primaryColor} ${percentage}%, | |
| ${secondaryColor} ${percentage}%, | |
| ${secondaryColor} 100%)`; | |
| } | |
| // 高亮当前激活的预设按钮 | |
| highlightActivePreset() { | |
| const value = parseInt(this.maxTokens.value); | |
| this.tokenPresets.forEach(preset => { | |
| const presetValue = parseInt(preset.dataset.value); | |
| if (presetValue === value) { | |
| preset.classList.add('active'); | |
| } else { | |
| preset.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| // 从配置文件加载模型定义 | |
| async loadModelConfig() { | |
| try { | |
| // 使用API端点获取模型列表 | |
| const response = await fetch('/api/models'); | |
| if (!response.ok) { | |
| throw new Error(`加载模型列表失败: ${response.status} ${response.statusText}`); | |
| } | |
| // 获取模型列表 | |
| const modelsList = await response.json(); | |
| // 获取提供商配置 | |
| const configResponse = await fetch('/config/models.json'); | |
| if (!configResponse.ok) { | |
| throw new Error(`加载提供商配置失败: ${configResponse.status} ${configResponse.statusText}`); | |
| } | |
| const config = await configResponse.json(); | |
| // 保存提供商定义 | |
| this.providerDefinitions = config.providers || {}; | |
| // 处理模型定义 | |
| this.modelDefinitions = {}; | |
| // 从API获取的模型列表创建模型定义 | |
| modelsList.forEach(model => { | |
| this.modelDefinitions[model.id] = { | |
| name: model.display_name, | |
| provider: this.getProviderIdByModel(model.id, config), | |
| supportsMultimodal: model.is_multimodal, | |
| isReasoning: model.is_reasoning, | |
| apiKeyId: this.getApiKeyIdByModel(model.id, config), | |
| description: model.description, | |
| version: this.getVersionByModel(model.id, config) | |
| }; | |
| }); | |
| console.log('模型配置加载成功:', this.modelDefinitions); | |
| } catch (error) { | |
| console.error('加载模型配置时出错:', error); | |
| throw error; | |
| } | |
| } | |
| // 从配置中根据模型ID获取提供商ID | |
| getProviderIdByModel(modelId, config) { | |
| const modelConfig = config.models[modelId]; | |
| return modelConfig ? modelConfig.provider : 'unknown'; | |
| } | |
| // 从配置中根据模型ID获取API密钥ID | |
| getApiKeyIdByModel(modelId, config) { | |
| const modelConfig = config.models[modelId]; | |
| if (!modelConfig) return null; | |
| const providerId = modelConfig.provider; | |
| const provider = config.providers[providerId]; | |
| return provider ? provider.api_key_id : null; | |
| } | |
| // 从配置中根据模型ID获取版本信息 | |
| getVersionByModel(modelId, config) { | |
| const modelConfig = config.models[modelId]; | |
| return modelConfig ? modelConfig.version : 'latest'; | |
| } | |
| // 设置默认模型定义(当配置加载失败时使用) | |
| setupDefaultModels() { | |
| this.providerDefinitions = { | |
| 'anthropic': { | |
| name: 'Anthropic', | |
| api_key_id: 'AnthropicApiKey' | |
| }, | |
| 'openai': { | |
| name: 'OpenAI', | |
| api_key_id: 'OpenaiApiKey' | |
| }, | |
| 'deepseek': { | |
| name: 'DeepSeek', | |
| api_key_id: 'DeepseekApiKey' | |
| } | |
| }; | |
| this.modelDefinitions = { | |
| 'claude-3-7-sonnet-20250219': { | |
| name: 'Claude 3.7 Sonnet', | |
| provider: 'anthropic', | |
| supportsMultimodal: true, | |
| isReasoning: true, | |
| apiKeyId: 'AnthropicApiKey', | |
| version: '20250219' | |
| }, | |
| 'gpt-4o-2024-11-20': { | |
| name: 'GPT-4o', | |
| provider: 'openai', | |
| supportsMultimodal: true, | |
| isReasoning: false, | |
| apiKeyId: 'OpenaiApiKey', | |
| version: '2024-11-20' | |
| }, | |
| 'deepseek-reasoner': { | |
| name: 'DeepSeek Reasoner', | |
| provider: 'deepseek', | |
| supportsMultimodal: false, | |
| isReasoning: true, | |
| apiKeyId: 'DeepseekApiKey', | |
| version: 'latest' | |
| } | |
| }; | |
| console.log('使用默认模型配置'); | |
| } | |
| initializeElements() { | |
| // Settings panel elements | |
| this.settingsPanel = document.getElementById('settingsPanel'); | |
| this.modelSelect = document.getElementById('modelSelect'); | |
| this.temperatureInput = document.getElementById('temperature'); | |
| this.temperatureGroup = document.querySelector('.setting-group:has(#temperature)') || | |
| document.querySelector('div.setting-group:has(input[id="temperature"])'); | |
| this.systemPromptInput = document.getElementById('systemPrompt'); | |
| this.promptDescriptionElement = document.getElementById('promptDescription'); | |
| this.languageInput = document.getElementById('language'); | |
| this.proxyEnabledInput = document.getElementById('proxyEnabled'); | |
| this.proxyHostInput = document.getElementById('proxyHost'); | |
| this.proxyPortInput = document.getElementById('proxyPort'); | |
| this.proxySettings = document.getElementById('proxySettings'); | |
| // API基础URL相关元素 | |
| this.apiBaseUrlsList = document.getElementById('apiBaseUrlsList'); | |
| // 获取所有API基础URL状态元素 | |
| this.apiBaseUrlStatusElements = { | |
| 'AnthropicApiBaseUrl': document.getElementById('AnthropicApiBaseUrlStatus'), | |
| 'OpenaiApiBaseUrl': document.getElementById('OpenaiApiBaseUrlStatus'), | |
| 'DeepseekApiBaseUrl': document.getElementById('DeepseekApiBaseUrlStatus'), | |
| 'AlibabaApiBaseUrl': document.getElementById('AlibabaApiBaseUrlStatus'), | |
| 'GoogleApiBaseUrl': document.getElementById('GoogleApiBaseUrlStatus') | |
| }; | |
| // 提示词管理相关元素 | |
| this.promptSelect = document.getElementById('promptSelect'); | |
| this.savePromptBtn = document.getElementById('savePromptBtn'); | |
| this.newPromptBtn = document.getElementById('newPromptBtn'); | |
| this.deletePromptBtn = document.getElementById('deletePromptBtn'); | |
| // 提示词对话框元素 | |
| this.promptDialog = document.getElementById('promptDialog'); | |
| this.promptDialogOverlay = document.getElementById('promptDialogOverlay'); | |
| this.promptIdInput = document.getElementById('promptId'); | |
| this.promptNameInput = document.getElementById('promptName'); | |
| this.promptContentInput = document.getElementById('promptContent'); | |
| this.promptDescriptionInput = document.getElementById('promptDescriptionEdit'); | |
| this.cancelPromptBtn = document.getElementById('cancelPromptBtn'); | |
| this.confirmPromptBtn = document.getElementById('confirmPromptBtn'); | |
| // 最大Token设置元素 - 现在是输入框而不是滑块 | |
| this.maxTokens = document.getElementById('maxTokens'); | |
| this.maxTokensValue = document.getElementById('maxTokensValue'); | |
| this.tokenPresets = document.querySelectorAll('.token-preset'); | |
| // 理性推理相关元素 | |
| this.reasoningDepthSelect = document.getElementById('reasoningDepth'); | |
| this.reasoningSettingGroup = document.querySelector('.reasoning-setting-group'); | |
| this.thinkBudgetPercentInput = document.getElementById('thinkBudgetPercent'); | |
| this.thinkBudgetPercentValue = document.getElementById('thinkBudgetPercentValue'); | |
| this.thinkBudgetGroup = document.querySelector('.think-budget-group'); | |
| // 豆包深度思考相关元素 | |
| this.doubaoThinkingModeSelect = document.getElementById('doubaoThinkingMode'); | |
| this.doubaoThinkingGroup = document.querySelector('.doubao-thinking-group'); | |
| // Initialize Mathpix inputs | |
| this.mathpixAppIdInput = document.getElementById('mathpixAppId'); | |
| this.mathpixAppKeyInput = document.getElementById('mathpixAppKey'); | |
| // OCR源选择器 | |
| this.ocrSourceSelect = document.getElementById('ocrSourceSelect'); | |
| // API Key elements - 所有的密钥输入框 | |
| this.apiKeyInputs = { | |
| 'AnthropicApiKey': document.getElementById('AnthropicApiKey'), | |
| 'OpenaiApiKey': document.getElementById('OpenaiApiKey'), | |
| 'DeepseekApiKey': document.getElementById('DeepseekApiKey'), | |
| 'AlibabaApiKey': document.getElementById('AlibabaApiKey'), | |
| 'GoogleApiKey': document.getElementById('GoogleApiKey'), | |
| 'mathpixAppId': this.mathpixAppIdInput, | |
| 'mathpixAppKey': this.mathpixAppKeyInput | |
| }; | |
| // API密钥状态显示相关元素 | |
| this.apiKeysList = document.getElementById('apiKeysList'); | |
| // 防止API密钥区域的点击事件冒泡 | |
| if (this.apiKeysList) { | |
| this.apiKeysList.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| } | |
| // Settings toggle elements | |
| this.settingsToggle = document.getElementById('settingsToggle'); | |
| this.closeSettings = document.getElementById('closeSettings'); | |
| // 获取所有密钥输入组元素 | |
| this.apiKeyGroups = document.querySelectorAll('.api-key-group'); | |
| // Initialize API key toggle buttons | |
| document.querySelectorAll('.toggle-api-key').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| const input = e.currentTarget.closest('.input-group').querySelector('input'); | |
| const type = input.type === 'password' ? 'text' : 'password'; | |
| input.type = type; | |
| const icon = e.currentTarget.querySelector('i'); | |
| if (icon) { | |
| icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`; | |
| } | |
| }); | |
| }); | |
| // 存储API密钥的对象 | |
| this.apiKeyValues = { | |
| 'AnthropicApiKey': '', | |
| 'OpenaiApiKey': '', | |
| 'DeepseekApiKey': '', | |
| 'AlibabaApiKey': '', | |
| 'GoogleApiKey': '', | |
| 'DoubaoApiKey': '', | |
| 'BaiduApiKey': '', | |
| 'BaiduSecretKey': '', | |
| 'MathpixAppId': '', | |
| 'MathpixAppKey': '' | |
| }; | |
| this.reasoningOptions = document.querySelectorAll('.reasoning-option'); | |
| this.thinkPresets = document.querySelectorAll('.think-preset'); | |
| } | |
| // 绑定提示词预览区域点击事件 | |
| initPromptPreviewEvents() { | |
| const promptPreview = document.querySelector('.prompt-preview'); | |
| if (promptPreview) { | |
| promptPreview.addEventListener('click', () => { | |
| // 触发保存按钮点击事件,打开编辑对话框 | |
| document.getElementById('savePromptBtn').click(); | |
| }); | |
| } | |
| } | |
| /** | |
| * 打开提示词编辑对话框 | |
| * @param {string|null} promptId 要编辑的提示词ID,为空则表示新建 | |
| */ | |
| openPromptDialog(promptId = null) { | |
| // 判断是否为新建提示词 | |
| const isNew = !promptId || !this.prompts[promptId]; | |
| if (isNew) { | |
| // 新建提示词 - 清空输入框 | |
| this.promptIdInput.value = ''; | |
| this.promptNameInput.value = ''; | |
| this.promptContentInput.value = ''; | |
| this.promptDescriptionInput.value = ''; | |
| // 设置对话框标题 | |
| if (this.promptDialogTitle) { | |
| this.promptDialogTitle.textContent = '新建提示词'; | |
| } | |
| // 启用ID输入框 | |
| this.promptIdInput.disabled = false; | |
| // 设置保存按钮动作 | |
| this.promptSaveBtn.onclick = () => this.savePrompt(true); // 明确传递isNew=true | |
| } else { | |
| // 编辑现有提示词 - 填充现有内容 | |
| this.promptIdInput.value = promptId; | |
| this.promptNameInput.value = this.prompts[promptId].name; | |
| this.promptContentInput.value = this.prompts[promptId].content; | |
| this.promptDescriptionInput.value = this.prompts[promptId].description || ''; | |
| // 设置对话框标题 | |
| if (this.promptDialogTitle) { | |
| this.promptDialogTitle.textContent = '编辑提示词'; | |
| } | |
| // 禁用ID输入框(不允许修改ID) | |
| this.promptIdInput.disabled = true; | |
| // 设置保存按钮动作 | |
| this.promptSaveBtn.onclick = () => this.savePrompt(false); // 明确传递isNew=false | |
| } | |
| // 显示对话框 | |
| if (this.promptDialogMask) { | |
| this.promptDialogMask.classList.remove('hidden'); | |
| } | |
| } | |
| /** | |
| * 刷新API基础URL状态 | |
| */ | |
| async refreshApiBaseUrlStatus() { | |
| try { | |
| // 先将所有状态显示为"检查中" | |
| Object.keys(this.apiBaseUrlValues).forEach(urlId => { | |
| const statusElement = document.getElementById(`${urlId}Status`); | |
| if (statusElement) { | |
| statusElement.className = 'key-status checking'; | |
| statusElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 检查中...'; | |
| } | |
| }); | |
| // 发送请求获取API基础URL | |
| const response = await fetch('/api/proxy-api', { | |
| method: 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| } | |
| }); | |
| if (response.ok) { | |
| const proxyApiConfig = await response.json(); | |
| // 提取APIs对象并更新状态 | |
| const apiBaseUrls = { | |
| 'AnthropicApiBaseUrl': proxyApiConfig.apis?.anthropic || '', | |
| 'OpenaiApiBaseUrl': proxyApiConfig.apis?.openai || '', | |
| 'DeepseekApiBaseUrl': proxyApiConfig.apis?.deepseek || '', | |
| 'AlibabaApiBaseUrl': proxyApiConfig.apis?.alibaba || '', | |
| 'GoogleApiBaseUrl': proxyApiConfig.apis?.google || '', | |
| 'DoubaoApiBaseUrl': proxyApiConfig.apis?.doubao || '' | |
| }; | |
| this.updateApiBaseUrlStatus(apiBaseUrls); | |
| console.log('API基础URL状态已刷新'); | |
| } else { | |
| console.error('刷新API基础URL状态失败'); | |
| } | |
| } catch (error) { | |
| console.error('刷新API基础URL状态出错:', error); | |
| } | |
| } | |
| /** | |
| * 更新API基础URL状态显示 | |
| * @param {Object} apiBaseUrls 基础URL对象 | |
| */ | |
| updateApiBaseUrlStatus(apiBaseUrls) { | |
| if (!this.apiBaseUrlsList) return; | |
| // 保存API基础URL值到内存中 | |
| for (const [key, value] of Object.entries(apiBaseUrls)) { | |
| this.apiBaseUrlValues[key] = value; | |
| } | |
| // 找到所有基础URL状态元素 | |
| Object.keys(apiBaseUrls).forEach(urlId => { | |
| const statusElement = document.getElementById(`${urlId}Status`); | |
| if (!statusElement) return; | |
| const value = apiBaseUrls[urlId]; | |
| if (value && value.trim() !== '') { | |
| // 显示基础URL状态 - 已设置 | |
| statusElement.className = 'key-status set'; | |
| statusElement.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`; | |
| } else { | |
| // 显示基础URL状态 - 未设置 | |
| statusElement.className = 'key-status not-set'; | |
| statusElement.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`; | |
| } | |
| }); | |
| } | |
| /** | |
| * 保存单个API基础URL | |
| * @param {string} urlType URL类型 | |
| * @param {string} value URL值 | |
| * @param {HTMLElement} urlStatus URL状态容器 | |
| */ | |
| async saveApiBaseUrl(urlType, value, urlStatus) { | |
| try { | |
| // 显示保存中状态 | |
| const saveToast = this.createToast('正在保存API基础URL...', 'info', true); | |
| // 获取当前中转API配置 | |
| const response = await fetch('/api/proxy-api', { | |
| method: 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error('获取现有配置失败'); | |
| } | |
| const config = await response.json(); | |
| // 确保apis对象存在 | |
| if (!config.apis) { | |
| config.apis = {}; | |
| } | |
| // 根据URL类型更新对应的API URL | |
| switch(urlType) { | |
| case 'AnthropicApiBaseUrl': | |
| config.apis.anthropic = value; | |
| break; | |
| case 'OpenaiApiBaseUrl': | |
| config.apis.openai = value; | |
| break; | |
| case 'DeepseekApiBaseUrl': | |
| config.apis.deepseek = value; | |
| break; | |
| case 'AlibabaApiBaseUrl': | |
| config.apis.alibaba = value; | |
| break; | |
| case 'GoogleApiBaseUrl': | |
| config.apis.google = value; | |
| break; | |
| case 'DoubaoApiBaseUrl': | |
| config.apis.doubao = value; | |
| break; | |
| } | |
| // 确保启用中转API | |
| if (value && value.trim() !== '') { | |
| config.enabled = true; | |
| } | |
| // 保存到服务器 | |
| const saveResponse = await fetch('/api/proxy-api', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(config) | |
| }); | |
| // 移除保存中提示 | |
| if (saveToast) { | |
| saveToast.remove(); | |
| } | |
| if (saveResponse.ok) { | |
| const result = await saveResponse.json(); | |
| if (result.success) { | |
| // 更新基础URL状态显示 | |
| const statusElem = document.getElementById(`${urlType}Status`); | |
| if (statusElem) { | |
| if (value && value.trim() !== '') { | |
| statusElem.className = 'key-status set'; | |
| statusElem.innerHTML = `<i class="fas fa-check-circle"></i> 已设置`; | |
| } else { | |
| statusElem.className = 'key-status not-set'; | |
| statusElem.innerHTML = `<i class="fas fa-times-circle"></i> 未设置`; | |
| } | |
| } | |
| // 保存到内存 | |
| this.apiBaseUrlValues[urlType] = value; | |
| // 显示成功提示 | |
| this.createToast('API基础URL已保存', 'success'); | |
| } else { | |
| this.createToast(`保存失败: ${result.message || '未知错误'}`, 'error'); | |
| } | |
| } else { | |
| this.createToast('保存API基础URL失败', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('保存API基础URL错误:', error); | |
| this.createToast(`保存失败: ${error.message || '未知错误'}`, 'error'); | |
| } | |
| } | |
| /** | |
| * 初始化API基础URL编辑相关功能 | |
| */ | |
| initApiBaseUrlEditFunctions() { | |
| // 1. 编辑按钮点击事件 | |
| document.querySelectorAll('.edit-api-base-url').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const urlType = e.currentTarget.getAttribute('data-key-type'); | |
| const urlStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (urlStatus) { | |
| // 隐藏显示区域 | |
| const displayArea = urlStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.add('hidden'); | |
| // 显示编辑区域 | |
| const editArea = urlStatus.querySelector('.key-edit'); | |
| if (editArea) { | |
| editArea.classList.remove('hidden'); | |
| // 获取当前URL值并填入输入框 | |
| const urlInput = editArea.querySelector('.key-input'); | |
| if (urlInput) { | |
| // 从状态文本中获取当前值(如果不是"未设置") | |
| const statusElement = urlStatus.querySelector('.key-status'); | |
| if (statusElement && statusElement.textContent !== '未设置') { | |
| urlInput.value = this.apiBaseUrlValues[urlType] || ''; | |
| } else { | |
| urlInput.value = ''; | |
| } | |
| // 聚焦输入框 | |
| setTimeout(() => { | |
| urlInput.focus(); | |
| }, 100); | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| // 2. 保存按钮点击事件 | |
| document.querySelectorAll('.save-api-base-url').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const urlType = e.currentTarget.getAttribute('data-key-type'); | |
| const urlStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (urlStatus) { | |
| // 获取输入的新URL值 | |
| const urlInput = urlStatus.querySelector('.key-input'); | |
| if (urlInput) { | |
| const newValue = urlInput.value.trim(); | |
| // 保存到服务器 | |
| this.saveApiBaseUrl(urlType, newValue, urlStatus); | |
| // 隐藏编辑区域 | |
| const editArea = urlStatus.querySelector('.key-edit'); | |
| if (editArea) editArea.classList.add('hidden'); | |
| // 显示状态区域 | |
| const displayArea = urlStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.remove('hidden'); | |
| } | |
| } | |
| }); | |
| }); | |
| // 3. 输入框按下Enter保存 | |
| document.querySelectorAll('#apiBaseUrlsList .key-input').forEach(input => { | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| const saveButton = e.currentTarget.closest('.key-edit').querySelector('.save-api-base-url'); | |
| if (saveButton) { | |
| saveButton.click(); | |
| } | |
| } else if (e.key === 'Escape') { | |
| // 阻止事件冒泡 | |
| e.stopPropagation(); | |
| // 取消编辑 | |
| const urlStatus = e.currentTarget.closest('.key-status-wrapper'); | |
| if (urlStatus) { | |
| const editArea = urlStatus.querySelector('.key-edit'); | |
| if (editArea) editArea.classList.add('hidden'); | |
| const displayArea = urlStatus.querySelector('.key-display'); | |
| if (displayArea) displayArea.classList.remove('hidden'); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| // Export for use in other modules | |
| window.SettingsManager = SettingsManager; | |