/** * ChatbotModelSelectorUI 模型选择器界面渲染与交互逻辑 * * 主要功能: * 1. 渲染自定义模型选择界面,支持多模型、Batch模型、参数调节等。 * 2. 负责模型、温度、最大token、并发等参数的选择与本地存储。 * 3. 绑定所有相关交互事件(切换、参数同步、帮助提示、返回等)。 * 4. 支持参数的本地持久化与回显。 */ /** * XSS 防护:安全地转义 HTML(优先使用 ChatbotUtils,降级到本地实现) * @param {string} str - 需要转义的字符串 * @returns {string} 转义后的安全字符串 */ function safeEscapeHtmlForModel(str) { if (typeof str !== 'string') return ''; if (typeof window.ChatbotUtils !== 'undefined' && typeof window.ChatbotUtils.escapeHtml === 'function') { return window.ChatbotUtils.escapeHtml(str); } // 降级方案 return str.replace(/[&<>"']/g, function (c) { return {'&':'&','<':'<','>':'>','"':'"','\'':'''}[c]; }); } window.ChatbotModelSelectorUI = { /** * 渲染模型选择器主界面,并绑定所有交互事件。 * * 主要逻辑: * 1. 隐藏 preset 区和聊天区,显示模型选择器。 * 2. 渲染模型下拉、特殊模型、温度、并发、最大token等参数输入。 * 3. 绑定所有输入、切换、帮助、返回等事件。 * 4. 参数变更自动保存到 localStorage 或调用 saveSettings。 * * @param {HTMLElement} mainContentArea - 主内容区容器。 * @param {HTMLElement} chatBody - 聊天内容区。 * @param {Array} availableModels - 可用模型列表。 * @param {object} currentSettings - 当前设置。 * @param {function} updateChatbotUI - UI刷新回调。 */ render: function(mainContentArea, chatBody, availableModels, currentSettings, updateChatbotUI) { // 隐藏预设和聊天区 const presetContainer = document.getElementById('chatbot-preset-container'); if (presetContainer) presetContainer.style.display = 'none'; if (chatBody) chatBody.style.display = 'none'; // 处理模型列表和默认选中 let models = availableModels; if (!Array.isArray(models) || models.length === 0) models = []; let settings = currentSettings; let defaultModelId = settings.selectedCustomModelId || localStorage.getItem('lastSelectedCustomModel') || (models[0]?.id || models[0] || ''); // 移除已存在的选择器,防止重复渲染 let modelSelectorDiv = document.getElementById('chatbot-model-selector'); if (modelSelectorDiv) modelSelectorDiv.remove(); // 创建主容器 modelSelectorDiv = document.createElement('div'); modelSelectorDiv.id = 'chatbot-model-selector'; modelSelectorDiv.style.margin = '-30px auto 0 auto'; modelSelectorDiv.style.maxWidth = '340px'; modelSelectorDiv.style.background = 'linear-gradient(135deg,#f0f9ff 80%,#e0f2fe 100%)'; modelSelectorDiv.style.border = '2px dashed #93c5fd'; modelSelectorDiv.style.borderRadius = '16px'; modelSelectorDiv.style.padding = '20px 16px 16px 16px'; modelSelectorDiv.style.maxHeight = '100%'; modelSelectorDiv.style.overflowY = 'auto'; // 读取默认参数 let defaultTemperature = 0.5; let defaultMaxTokens = 8000; try { if (settings.customModelSettings) { defaultTemperature = typeof settings.customModelSettings.temperature === 'number' ? settings.customModelSettings.temperature : defaultTemperature; defaultMaxTokens = typeof settings.customModelSettings.max_tokens === 'number' ? settings.customModelSettings.max_tokens : defaultMaxTokens; } } catch (e) { console.error("Error accessing customModelSettings:", e); } const defaultConcurrency = (window.chatbotActiveOptions && Number.isInteger(window.chatbotActiveOptions.segmentConcurrency)) ? window.chatbotActiveOptions.segmentConcurrency : 20; // 主体HTML结构 modelSelectorDiv.innerHTML = `
选择自定义模型
默认模型
温度 (0-1)
并发上限
回复长度
(max_tokens)
`; mainContentArea.insertBefore(modelSelectorDiv, chatBody); // 事件绑定区域 // 展开/收起特殊模型 const addSpecialBtn = modelSelectorDiv.querySelector('#chatbot-add-special-models-btn'); const specialContainer = modelSelectorDiv.querySelector('#chatbot-special-models-container'); if (addSpecialBtn && specialContainer) { addSpecialBtn.onclick = () => { if (specialContainer.style.display === 'none') { specialContainer.style.display = 'flex'; addSpecialBtn.textContent = '-'; } else { specialContainer.style.display = 'none'; addSpecialBtn.textContent = '+'; } }; } // 多模态模型选择 const multiSelect = modelSelectorDiv.querySelector('#chatbot-multimodal-model-select'); if (multiSelect) { multiSelect.onchange = e => { window.chatbotActiveOptions.multimodalModel = e.target.value; let settings = {}; try { settings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch {} settings.multimodalModel = e.target.value; if (typeof saveSettings === 'function') { saveSettings(settings); } else { localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } }; let userSettings = {}; try { userSettings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch {} multiSelect.value = userSettings.multimodalModel || ''; window.chatbotActiveOptions.multimodalModel = userSettings.multimodalModel || ''; } // Batch模型选择 const batchSelect = modelSelectorDiv.querySelector('#chatbot-batch-model-select'); if (batchSelect) { batchSelect.onchange = e => { window.chatbotActiveOptions.batchModel = e.target.value; let settings = {}; try { settings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch {} settings.batchModel = e.target.value; if (typeof saveSettings === 'function') { saveSettings(settings); } else { localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } }; let userSettings = {}; try { userSettings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch {} batchSelect.value = userSettings.batchModel || ''; window.chatbotActiveOptions.batchModel = userSettings.batchModel || ''; } // 并发参数绑定 const concurrencyInput = modelSelectorDiv.querySelector('#chatbot-concurrency-input'); const concurrencyRange = modelSelectorDiv.querySelector('#chatbot-concurrency-range'); function saveConcurrency() { let v = parseInt(concurrencyInput.value); if (isNaN(v) || v < 1) v = 1; if (v > 50) v = 50; concurrencyInput.value = v; concurrencyRange.value = v; window.chatbotActiveOptions.segmentConcurrency = v; let settings = {}; try { settings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch {}; settings.segmentConcurrency = v; if (typeof saveSettings === 'function') saveSettings(settings); else localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } if (concurrencyInput && concurrencyRange) { concurrencyInput.oninput = saveConcurrency; concurrencyRange.oninput = saveConcurrency; // Should be oninput or onchange, not saveConcurrency directly concurrencyRange.oninput = () => { concurrencyInput.value = concurrencyRange.value; saveConcurrency(); }; concurrencyInput.oninput = () => { concurrencyRange.value = concurrencyInput.value; saveConcurrency(); }; } // 帮助按钮 const tempHelpBtn = modelSelectorDiv.querySelector('#chatbot-temp-help-btn'); if (tempHelpBtn) tempHelpBtn.onclick = () => ChatbotUtils.showToast('温度:调节模型生成的随机性,0表示最确定,1表示最随机', 'info', 3000); const concurrencyHelpBtn = modelSelectorDiv.querySelector('#chatbot-concurrency-help-btn'); if (concurrencyHelpBtn) concurrencyHelpBtn.onclick = () => ChatbotUtils.showToast('并发上限:控制同时处理分段的最大并发请求数', 'info', 3000); const maxTokensHelpBtn = modelSelectorDiv.querySelector('#chatbot-maxtokens-help-btn'); if (maxTokensHelpBtn) maxTokensHelpBtn.onclick = () => ChatbotUtils.showToast('回复长度:模型最大输出的token数量', 'info', 3000); // 主模型选择 const select = document.getElementById('chatbot-model-select'); if (select) { select.onchange = function() { localStorage.setItem('lastSelectedCustomModel', this.value); let settings = {}; try { settings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch (e) {} settings.selectedCustomModelId = this.value; if (typeof saveSettings === 'function') { saveSettings(settings); } else { localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } }; } // 工具:将新模型ID列表追加到主下拉 async function appendModelsToMainSelect(ids) { const mainSelect = document.getElementById('chatbot-model-select'); if (!mainSelect || !Array.isArray(ids)) return 0; const existing = new Set(Array.from(mainSelect.options).map(o => o.value)); let added = 0; ids.forEach(id => { const mid = String(id || '').trim(); if (!mid || existing.has(mid)) return; const opt = document.createElement('option'); opt.value = mid; opt.textContent = mid; mainSelect.appendChild(opt); added++; }); return added; } // 检测 Gemini 可用模型并填充列表(仅当当前选择或默认选择是 Gemini 时生效) const detectGeminiBtn = modelSelectorDiv.querySelector('#chatbot-detect-gemini-btn'); if (detectGeminiBtn) { detectGeminiBtn.onclick = async () => { try { // 仅在可用 Key 存在时进行 const keys = (typeof loadModelKeys === 'function') ? (loadModelKeys('gemini') || []) : []; const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value); if (usableKeys.length === 0) { return ChatbotUtils.showToast('请先在模型与Key管理中配置 Gemini 的 API Key', 'warning', 3000); } const apiKey = usableKeys[0].value.trim(); detectGeminiBtn.disabled = true; detectGeminiBtn.textContent = '检测中...'; const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); const data = await resp.json(); const items = Array.isArray(data.models || data.data) ? (data.models || data.data) : []; if (items.length === 0) { return ChatbotUtils.showToast('未返回模型列表', 'info', 3000); } const ids = items.map(m => (m.name ? String(m.name).split('/').pop() : (m.id || ''))).filter(Boolean); const added = await appendModelsToMainSelect(ids); if (added > 0) ChatbotUtils.showToast(`已加入 ${added} 个 Gemini 模型到列表`, 'success', 3000); } catch (e) { ChatbotUtils.showToast(`检测失败:${e.message}`, 'error', 3000); } finally { detectGeminiBtn.disabled = false; detectGeminiBtn.textContent = '检测 Gemini'; } }; } // 检测 DeepSeek 可用模型 const detectDeepseekBtn = modelSelectorDiv.querySelector('#chatbot-detect-deepseek-btn'); if (detectDeepseekBtn) { detectDeepseekBtn.onclick = async () => { try { const keys = (typeof loadModelKeys === 'function') ? (loadModelKeys('deepseek') || []) : []; const usable = keys.filter(k => k.status !== 'invalid' && k.value); if (usable.length === 0) { return ChatbotUtils.showToast('请先配置 DeepSeek 的 API Key', 'warning', 3000); } const apiKey = usable[0].value.trim(); detectDeepseekBtn.disabled = true; detectDeepseekBtn.textContent = '检测中...'; const resp = await fetch('https://api.deepseek.com/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); const data = await resp.json(); const items = Array.isArray(data.data) ? data.data : []; const ids = items.map(m => m.id).filter(Boolean); const added = await appendModelsToMainSelect(ids); ChatbotUtils.showToast(added > 0 ? `已加入 ${added} 个 DeepSeek 模型到列表` : '未返回模型列表', added > 0 ? 'success' : 'info', 3000); } catch (e) { ChatbotUtils.showToast(`检测失败:${e.message}`, 'error', 3000); } finally { detectDeepseekBtn.disabled = false; detectDeepseekBtn.textContent = '检测 DeepSeek'; } }; } // 检测 通义(DashScope)可用模型 const detectTongyiBtn = modelSelectorDiv.querySelector('#chatbot-detect-tongyi-btn'); if (detectTongyiBtn) { detectTongyiBtn.onclick = async () => { try { // 统一使用 'tongyi' 的 Key 列表 let keys = []; if (typeof loadModelKeys === 'function') { keys = loadModelKeys('tongyi') || []; } const usable = keys.filter(k => k.status !== 'invalid' && k.value); if (usable.length === 0) { return ChatbotUtils.showToast('请先配置 通义 的 API Key', 'warning', 3000); } const apiKey = usable[0].value.trim(); detectTongyiBtn.disabled = true; detectTongyiBtn.textContent = '检测中...'; // 使用 OpenAI 兼容模式的模型列表端点(用户指定) const resp = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); const data = await resp.json(); const items = Array.isArray(data.data) ? data.data : (Array.isArray(data.models) ? data.models : (Array.isArray(data?.data?.models) ? data.data.models : [])); const ids = items.map(m => (m.model || m.id || m.name)).filter(Boolean); const added = await appendModelsToMainSelect(ids); ChatbotUtils.showToast(added > 0 ? `已加入 ${added} 个 通义 模型到列表` : '未返回模型列表', added > 0 ? 'success' : 'info', 3000); } catch (e) { ChatbotUtils.showToast(`检测失败:${e.message}`, 'error', 3000); } finally { detectTongyiBtn.disabled = false; detectTongyiBtn.textContent = '检测 通义'; } }; } // 检测 火山(Ark)可用模型 const detectVolcanoBtn = modelSelectorDiv.querySelector('#chatbot-detect-volcano-btn'); if (detectVolcanoBtn) { // 按用户要求:不提供在线检测,改为手动填写 detectVolcanoBtn.style.display = 'none'; } // 温度、最大token参数绑定 const tempInput = document.getElementById('chatbot-temp-input'); const tempRange = document.getElementById('chatbot-temp-range'); const maxTokensInput = document.getElementById('chatbot-maxtokens-input'); const maxTokensRange = document.getElementById('chatbot-maxtokens-range'); function saveCustomModelParams() { let settings = {}; try { settings = typeof loadSettings === 'function' ? loadSettings() : JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch (e) {} if (!settings.customModelSettings) settings.customModelSettings = {}; let t = parseFloat(tempInput.value); if (isNaN(t) || t < 0) t = 0; if (t > 1) t = 1; let m = parseInt(maxTokensInput.value); if (isNaN(m) || m < 256) m = 256; if (m > 32768) m = 32768; tempInput.value = t; tempRange.value = t; maxTokensInput.value = m; maxTokensRange.value = m; settings.customModelSettings.temperature = t; settings.customModelSettings.max_tokens = m; if (typeof saveSettings === 'function') { saveSettings(settings); } else { localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } } if (tempInput && tempRange) { tempInput.oninput = function() { tempRange.value = tempInput.value; saveCustomModelParams(); }; tempRange.oninput = function() { tempInput.value = tempRange.value; saveCustomModelParams(); }; } if (maxTokensInput && maxTokensRange) { maxTokensInput.oninput = function() { maxTokensRange.value = maxTokensInput.value; saveCustomModelParams(); }; maxTokensRange.oninput = function() { maxTokensInput.value = maxTokensRange.value; saveCustomModelParams(); }; } // 返回按钮 const backBtn = document.getElementById('chatbot-model-back-btn'); if (backBtn) { backBtn.onclick = function() { window.isModelSelectorOpen = false; // 清除配置缓存,以便下次获取最新配置 window._cachedChatbotConfig = null; updateChatbotUI(); }; } } };