/**
* 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 = `
选择自定义模型
默认模型
多模态模型
Batch模型
`;
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();
};
}
}
};