// js/ui.js // ===================== // UI 相关操作与交互函数 // ===================== // --------------------- // DOM 元素获取(集中管理,便于维护) // --------------------- /** @type {HTMLTextAreaElement | null} mistralApiKeysTextarea - Mistral API 密钥输入框。 */ const mistralApiKeysTextarea = document.getElementById('mistralApiKeys'); /** @type {HTMLInputElement | null} rememberMistralKeyCheckbox - "记住 Mistral 密钥"复选框。 */ const rememberMistralKeyCheckbox = document.getElementById('rememberMistralKey'); /** @type {HTMLTextAreaElement | null} translationApiKeysTextarea - (通用)翻译服务 API 密钥输入框。 */ const translationApiKeysTextarea = document.getElementById('translationApiKeys'); /** @type {HTMLInputElement | null} rememberTranslationKeyCheckbox - "记住翻译密钥"复选框。 */ const rememberTranslationKeyCheckbox = document.getElementById('rememberTranslationKey'); /** @type {HTMLSelectElement | null} translationModelSelect - 翻译模型选择下拉框。 */ const translationModelSelect = document.getElementById('translationModel'); /** @type {HTMLElement | null} customModelSettingsContainer - (旧版)自定义模型设置区域的容器。 */ const customModelSettingsContainer = document.getElementById('customModelSettingsContainer'); /** @type {HTMLElement | null} customModelSettings - (旧版)自定义模型具体设置的容器。 */ const customModelSettings = document.getElementById('customModelSettings'); /** @type {HTMLElement | null} advancedSettingsToggle - 高级设置区域的切换按钮。 */ const advancedSettingsToggle = document.getElementById('advancedSettingsToggle'); /** @type {HTMLElement | null} advancedSettings - 高级设置区域的容器。 */ const advancedSettings = document.getElementById('advancedSettings'); /** @type {HTMLElement | null} advancedSettingsIcon - 高级设置切换按钮中的图标。 */ const advancedSettingsIcon = document.getElementById('advancedSettingsIcon'); /** @type {HTMLInputElement | null} maxTokensPerChunk - 每个文本块最大 Token 数的滑块输入。 */ const maxTokensPerChunk = document.getElementById('maxTokensPerChunk'); /** @type {HTMLElement | null} maxTokensPerChunkValue - 显示当前最大 Token 数的元素。 */ const maxTokensPerChunkValue = document.getElementById('maxTokensPerChunkValue'); /** @type {HTMLInputElement | null} skipProcessedFilesCheckbox - "跳过已处理文件"复选框。 */ const skipProcessedFilesCheckbox = document.getElementById('skipProcessedFiles'); /** @type {HTMLInputElement | null} concurrencyLevelInput - (OCR/通用)并发级别输入框。 */ const concurrencyLevelInput = document.getElementById('concurrencyLevel'); /** @type {HTMLElement | null} dropZone - 文件拖放区域。 */ const dropZone = document.getElementById('dropZone'); /** @type {HTMLInputElement | null} pdfFileInput - 文件选择输入框 (type="file")。 */ const pdfFileInput = document.getElementById('pdfFileInput'); /** @type {HTMLButtonElement | null} browseFilesBtn - "浏览文件"按钮。 */ const browseFilesBtn = document.getElementById('browseFilesBtn'); /** @type {HTMLElement | null} fileListContainer - 文件列表的容器。 */ const fileListContainer = document.getElementById('fileListContainer'); /** @type {HTMLElement | null} fileList - 文件列表的 UL 或 OL 元素。 */ const fileList = document.getElementById('fileList'); /** @type {HTMLButtonElement | null} clearFilesBtn - "清空文件列表"按钮。 */ const clearFilesBtn = document.getElementById('clearFilesBtn'); /** @type {HTMLSelectElement | null} targetLanguage - 目标语言选择下拉框。 */ const targetLanguage = document.getElementById('targetLanguage'); /** @type {HTMLButtonElement | null} processBtn - "开始处理"按钮。 */ const processBtn = document.getElementById('processBtn'); /** @type {HTMLButtonElement | null} downloadAllBtn - "全部下载"按钮。 */ const downloadAllBtn = document.getElementById('downloadAllBtn'); /** @type {HTMLElement | null} batchModeToggleWrapper - 批量模式开关容器。 */ const batchModeToggleWrapper = document.getElementById('batchModeToggleWrapper'); /** @type {HTMLInputElement | null} batchModeToggle - 批量模式开关。 */ const batchModeToggle = document.getElementById('batchModeToggle'); /** @type {HTMLElement | null} batchModeConfigPanel - 批量模式配置面板。 */ const batchModeConfigPanel = document.getElementById('batchModeConfig'); /** @type {HTMLElement | null} resultsSection - 处理结果显示区域。 */ const resultsSection = document.getElementById('resultsSection'); /** @type {HTMLElement | null} resultsSummary - 处理结果总结信息的容器。 */ const resultsSummary = document.getElementById('resultsSummary'); /** @type {HTMLElement | null} progressSection - 进度显示区域。 */ const progressSection = document.getElementById('progressSection'); /** @type {HTMLElement | null} batchProgressText - 批处理整体进度文本显示元素。 */ const batchProgressText = document.getElementById('batchProgressText'); /** @type {HTMLElement | null} concurrentProgressText - 当前并发任务数文本显示元素。 */ const concurrentProgressText = document.getElementById('concurrentProgressText'); /** @type {HTMLElement | null} progressStep - 当前处理步骤文本显示元素。 */ const progressStep = document.getElementById('progressStep'); /** @type {HTMLElement | null} progressPercentage - 进度百分比文本显示元素。 */ const progressPercentage = document.getElementById('progressPercentage'); /** @type {HTMLElement | null} progressBar - 进度条的内部填充元素。 */ const progressBar = document.getElementById('progressBar'); /** @type {HTMLElement | null} progressLog - 详细进度日志的容器。 */ const progressLog = document.getElementById('progressLog'); /** @type {HTMLElement | null} notificationContainer - 通知消息的容器。 */ const notificationContainer = document.getElementById('notification-container'); /** @type {HTMLElement | null} customModelSettingsToggle - (旧版)自定义模型设置的切换按钮。 */ const customModelSettingsToggle = document.getElementById('customModelSettingsToggle'); /** @type {HTMLElement | null} customModelSettingsToggleIcon - (旧版)自定义模型设置切换按钮中的图标。 */ const customModelSettingsToggleIcon = document.getElementById('customModelSettingsToggleIcon'); /** @type {HTMLElement | null} customSourceSiteContainer - 自定义API源站点选择区域的容器。 */ const customSourceSiteContainer = document.getElementById('customSourceSiteContainer'); /** @type {HTMLSelectElement | null} customSourceSiteSelect - 自定义API源站点选择下拉框。 */ const customSourceSiteSelect = document.getElementById('customSourceSiteSelect'); /** @type {HTMLElement | null} customSourceSiteToggleIcon - 自定义源站点设置区域切换按钮的图标 (可能与高级设置共用或独立)。 */ const customSourceSiteToggleIcon = document.getElementById('customSourceSiteToggleIcon'); // 注意:此ID可能与 advancedSettingsIcon 描述冲突,需确认实际HTML结构 /** @type {HTMLButtonElement | null} detectModelsBtn - "检测可用模型"按钮,通常用于自定义源站点。 */ const detectModelsBtn = document.getElementById('detectModelsBtn'); document.addEventListener('DOMContentLoaded', function() { // ... 其它初始化 ... if (customModelSettingsToggle && customModelSettings && customModelSettingsToggleIcon) { customModelSettingsToggle.addEventListener('click', function() { customModelSettings.classList.toggle('hidden'); if (customModelSettings.classList.contains('hidden')) { customModelSettingsToggleIcon.setAttribute('icon', 'carbon:chevron-down'); } else { customModelSettingsToggleIcon.setAttribute('icon', 'carbon:chevron-up'); } }); } // ===== 模型管理器变量声明 ===== const modelKeyManagerBtn = document.getElementById('modelKeyManagerBtn'); const modelKeyManagerModal = document.getElementById('modelKeyManagerModal'); const closeModelKeyManager = document.getElementById('closeModelKeyManager'); const modelListColumn = document.getElementById('modelListColumn'); const modelConfigColumn = document.getElementById('modelConfigColumn'); const keyManagerColumn = document.getElementById('keyManagerColumn'); let currentManagerUI = null; let currentSelectedSourceSiteId = null; // 用于自定义源站选择 let selectedModelForManager = null; const supportedModelsForKeyManager = window.supportedModelsForKeyManager || []; // 渲染模型列表 (委托给模块) function renderModelList() { if (window.modelManager) { window.modelManager.renderModelList(); } } // 选择模型 (委托给模块) function selectModelForManager(modelKey) { if (window.modelManager) { window.modelManager.selectModel(modelKey); selectedModelForManager = window.modelManager.getSelectedModel(); } currentSelectedSourceSiteId = null; } function renderModelConfigSection(modelKey) { modelConfigColumn.innerHTML = ''; const modelDefinition = supportedModelsForKeyManager.find(m => m.key === modelKey); if (!modelDefinition) return; const title = document.createElement('h3'); title.className = 'text-lg font-semibold mb-3 text-gray-800'; modelConfigColumn.appendChild(title); if (modelKey === 'custom') { title.textContent = `自定义源站管理`; const addNewButton = document.createElement('button'); addNewButton.id = 'addNewSourceSiteBtn'; addNewButton.innerHTML = '添加新源站'; addNewButton.className = 'mb-4 px-3 py-1.5 text-sm bg-green-500 hover:bg-green-600 text-white rounded transition-colors flex items-center'; addNewButton.addEventListener('click', () => { currentSelectedSourceSiteId = null; // 清除选中状态,表示新增 renderSourceSitesList(); // 更新列表,移除高亮 renderSourceSiteForm(null); }); modelConfigColumn.appendChild(addNewButton); const sitesListContainer = document.createElement('div'); sitesListContainer.id = 'sourceSitesListContainer'; modelConfigColumn.appendChild(sitesListContainer); const siteConfigFormContainer = document.createElement('div'); siteConfigFormContainer.id = 'sourceSiteConfigFormContainer'; siteConfigFormContainer.className = 'mt-4 p-4 border border-gray-200 rounded-md hidden'; modelConfigColumn.appendChild(siteConfigFormContainer); renderSourceSitesList(); if (!currentSelectedSourceSiteId && Object.keys(loadAllCustomSourceSites()).length === 0) { keyManagerColumn.innerHTML = '

请添加并选择一个源站以管理其 API Keys。

'; } else if (!currentSelectedSourceSiteId) { keyManagerColumn.innerHTML = '

请从上方列表选择一个源站以管理其 API Keys。

'; } } else if (modelKey === 'embedding') { title.textContent = `向量搜索与重排 - 配置`; renderEmbeddingConfig(); } else if (modelKey === 'academicSearch') { title.textContent = `学术搜索与代理 - 配置`; renderAcademicSearchConfig(); } else if (modelKey === 'mistral') { title.textContent = `${modelDefinition.name} - 配置`; renderMistralOcrConfig(); } else if (modelKey === 'mineru') { title.textContent = `${modelDefinition.name} - 配置`; renderMinerUConfig(); } else if (modelKey === 'doc2x') { title.textContent = `${modelDefinition.name} - 配置`; renderDoc2XConfig(); } else { title.textContent = `${modelDefinition.name} - 配置`; } } // 导出到全局,供模块使用 window.renderModelConfigSection = renderModelConfigSection; // ===== 初始化模型管理器模块 (在函数定义之后) ===== if (window.modelManager) { window.modelManager.init({ modelKeyManagerBtn, modelKeyManagerModal, closeModelKeyManager, modelListColumn, modelConfigColumn, keyManagerColumn }); } // 显示嵌入模型选择器的辅助函数 function showEmbeddingModelSelector(models, targetInput) { // 创建一个简单的选择对话框 const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400px; max-width: 90vw; max-height: 60vh; background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); z-index: 100002; padding: 0; overflow: hidden; `; const header = document.createElement('div'); header.style.cssText = 'padding: 16px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;'; header.innerHTML = `

选择嵌入模型

`; const list = document.createElement('div'); list.style.cssText = 'max-height: 400px; overflow-y: auto; padding: 8px;'; models.forEach(model => { const item = document.createElement('div'); item.style.cssText = ` padding: 12px 16px; margin: 4px 0; border-radius: 8px; cursor: pointer; transition: all 0.2s; border: 1px solid #e5e7eb; `; item.innerHTML = `
${model.id}
${model.owned_by ? `
by ${model.owned_by}
` : ''} `; item.onmouseover = () => { item.style.background = '#f3f4f6'; item.style.borderColor = '#3b82f6'; }; item.onmouseout = () => { item.style.background = '#fff'; item.style.borderColor = '#e5e7eb'; }; item.onclick = () => { targetInput.value = model.id; document.body.removeChild(overlay); document.body.removeChild(container); }; list.appendChild(item); }); container.appendChild(header); container.appendChild(list); const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 100001; `; const closeHandler = () => { document.body.removeChild(overlay); document.body.removeChild(container); }; overlay.onclick = closeHandler; header.querySelector('.model-selector-close').onclick = closeHandler; document.body.appendChild(overlay); document.body.appendChild(container); } function renderEmbeddingConfig() { // 委托给 ui_embedding_config.js 模块 if (window.UIEmbeddingConfigRenderer) { window.UIEmbeddingConfigRenderer.renderEmbeddingConfig(modelConfigColumn); } } // Rerank 模型选择器(与嵌入选择器风格一致) function showRerankModelSelector(models, targetInput) { const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 420px; max-width: 92vw; max-height: 60vh; background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); z-index: 100002; padding: 0; overflow: hidden; `; const header = document.createElement('div'); header.style.cssText = 'padding: 16px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;'; header.innerHTML = `

选择重排模型

`; const list = document.createElement('div'); list.style.cssText = 'max-height: 400px; overflow-y: auto; padding: 8px;'; (models || []).forEach(model => { const item = document.createElement('div'); item.style.cssText = ` padding: 12px 16px; margin: 4px 0; border-radius: 8px; cursor: pointer; transition: all 0.2s; border: 1px solid #e5e7eb; `; const id = model.id || model.name || ''; item.innerHTML = `
${id}
${model.owned_by ? `
by ${model.owned_by}
` : ''} `; item.onmouseover = () => { item.style.background = '#f3f4f6'; item.style.borderColor = '#737373'; }; item.onmouseout = () => { item.style.background = '#fff'; item.style.borderColor = '#e5e7eb'; }; item.onclick = () => { if (id) targetInput.value = id; document.body.removeChild(overlay); document.body.removeChild(container); }; list.appendChild(item); }); container.appendChild(header); container.appendChild(list); const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 100001; `; const closeHandler = () => { document.body.removeChild(overlay); document.body.removeChild(container); }; overlay.onclick = closeHandler; header.querySelector('.model-selector-close').onclick = closeHandler; document.body.appendChild(overlay); document.body.appendChild(container); } function renderMistralOcrConfig() { // 委托给 ui_model_ocr_config.js 模块 if (window.UIModelOcrConfigRenderer) { window.UIModelOcrConfigRenderer.renderMistralOcrConfig(modelConfigColumn); } } function renderMinerUConfig() { // 委托给 ui_model_ocr_config.js 模块 if (window.UIModelOcrConfigRenderer) { window.UIModelOcrConfigRenderer.renderMinerUConfig(modelConfigColumn); } } function renderDoc2XConfig() { // 委托给 ui_model_ocr_config.js 模块 if (window.UIModelOcrConfigRenderer) { window.UIModelOcrConfigRenderer.renderDoc2XConfig(modelConfigColumn); } } function renderAcademicSearchConfig() { // 从 localStorage 加载配置 const proxyConfig = JSON.parse(localStorage.getItem('academicSearchProxyConfig') || 'null') || { enabled: false, baseUrl: '', semanticScholarApiKey: '', pubmedApiKey: '', authKey: '' }; // 学术搜索源配置 const sourcesConfig = JSON.parse(localStorage.getItem('academicSearchSourcesConfig') || 'null') || { sources: [ { key: 'crossref', name: 'CrossRef', enabled: true, order: 0 }, { key: 'openalex', name: 'OpenAlex', enabled: true, order: 1 }, { key: 'arxiv', name: 'arXiv', enabled: true, order: 2 }, { key: 'pubmed', name: 'PubMed', enabled: true, order: 3 }, { key: 'semanticscholar', name: 'Semantic Scholar', enabled: true, order: 4 } ] }; const container = document.createElement('div'); container.className = 'space-y-4'; // Tab 切换 const tabsDiv = document.createElement('div'); tabsDiv.className = 'border-b border-gray-200'; tabsDiv.innerHTML = ` `; container.appendChild(tabsDiv); // Tab 1: 搜索源管理 const sourcesTab = document.createElement('div'); sourcesTab.id = 'academic-sources-tab-content'; sourcesTab.className = 'pt-4'; sourcesTab.innerHTML = `
拖动调整查询顺序,取消勾选可禁用某个源
`; container.appendChild(sourcesTab); // Tab 2: 代理配置 const proxyTab = document.createElement('div'); proxyTab.id = 'academic-proxy-tab-content'; proxyTab.className = 'pt-4 hidden'; container.appendChild(proxyTab); modelConfigColumn.appendChild(container); // 渲染搜索源列表 renderAcademicSourcesList(sourcesConfig); // 渲染代理配置 renderAcademicProxyConfig(proxyTab, proxyConfig); // Tab 切换逻辑 document.querySelectorAll('.academic-tab').forEach(tab => { tab.addEventListener('click', (e) => { const targetId = e.target.id; // 更新 tab 样式 document.querySelectorAll('.academic-tab').forEach(t => { t.classList.remove('border-blue-600', 'text-blue-600'); t.classList.add('border-transparent', 'text-gray-500'); }); e.target.classList.remove('border-transparent', 'text-gray-500'); e.target.classList.add('border-blue-600', 'text-blue-600'); // 切换内容 if (targetId === 'academic-tab-sources') { sourcesTab.classList.remove('hidden'); proxyTab.classList.add('hidden'); } else { sourcesTab.classList.add('hidden'); proxyTab.classList.remove('hidden'); } }); }); } function renderAcademicSourcesList(config) { const listContainer = document.getElementById('academic-sources-list'); if (!listContainer) return; listContainer.innerHTML = ''; // 按 order 排序 const sortedSources = [...config.sources].sort((a, b) => a.order - b.order); sortedSources.forEach((source, index) => { const item = document.createElement('div'); item.className = 'flex items-center gap-3 p-3 bg-white border border-gray-200 rounded-md hover:shadow-sm transition-shadow cursor-move'; item.draggable = true; item.dataset.sourceKey = source.key; item.innerHTML = ` ${source.name} ${source.key} `; listContainer.appendChild(item); // 拖拽事件 item.addEventListener('dragstart', (e) => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', source.key); item.classList.add('opacity-50'); }); item.addEventListener('dragend', () => { item.classList.remove('opacity-50'); }); item.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; item.classList.add('border-blue-400', 'bg-blue-50'); }); item.addEventListener('dragleave', () => { item.classList.remove('border-blue-400', 'bg-blue-50'); }); item.addEventListener('drop', (e) => { e.preventDefault(); item.classList.remove('border-blue-400', 'bg-blue-50'); const draggedKey = e.dataTransfer.getData('text/plain'); const targetKey = source.key; if (draggedKey !== targetKey) { // 重新排序 const draggedIndex = config.sources.findIndex(s => s.key === draggedKey); const targetIndex = config.sources.findIndex(s => s.key === targetKey); const [draggedItem] = config.sources.splice(draggedIndex, 1); config.sources.splice(targetIndex, 0, draggedItem); // 更新 order config.sources.forEach((s, idx) => s.order = idx); // 保存并重新渲染 localStorage.setItem('academicSearchSourcesConfig', JSON.stringify(config)); renderAcademicSourcesList(config); showNotification && showNotification('搜索源顺序已更新', 'success', 2000); } }); }); // 启用/禁用切换 document.querySelectorAll('.source-enable-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const key = e.target.dataset.key; const source = config.sources.find(s => s.key === key); if (source) { source.enabled = e.target.checked; localStorage.setItem('academicSearchSourcesConfig', JSON.stringify(config)); showNotification && showNotification(`${source.name} 已${source.enabled ? '启用' : '禁用'}`, 'success', 2000); } }); }); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.className = 'w-full mt-3 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700'; saveBtn.textContent = '保存配置'; saveBtn.onclick = () => { localStorage.setItem('academicSearchSourcesConfig', JSON.stringify(config)); showNotification && showNotification('搜索源配置已保存', 'success'); }; listContainer.appendChild(saveBtn); } function renderAcademicProxyConfig(container, config) { // 不要覆盖 className,保留 hidden 类 container.classList.add('space-y-4'); if (!container.classList.contains('pt-4')) { container.classList.add('pt-4'); } // 启用开关 const enableDiv = document.createElement('div'); enableDiv.innerHTML = `

开启后,PubMed、Semantic Scholar 和 arXiv 查询将通过代理服务器

`; container.appendChild(enableDiv); // Worker URL const urlDiv = document.createElement('div'); urlDiv.innerHTML = `

Cloudflare Worker 学术搜索代理地址

`; container.appendChild(urlDiv); // 部署模式说明 const modeInfoDiv = document.createElement('div'); modeInfoDiv.className = 'border-t pt-4'; modeInfoDiv.innerHTML = `
支持两种部署模式
方案一:透传模式(推荐)
• 在下方填写 API Key,通过 X-Api-Key 请求头透传给 Worker
• Worker 可以选择配置密钥作为备用,如果前端没有提供则使用 Worker 配置的密钥
• 适合个人使用或分享给他人
方案二:共享密钥模式
• API Key 存储在 Worker 环境变量中(必需)
• 需要在下方填写 Worker Auth Key(对应 Worker 的 AUTH_SECRET
• 适合团队共享,但需要保护好 Auth Key
`; container.appendChild(modeInfoDiv); // Semantic Scholar API Key(透传模式) const s2KeyDiv = document.createElement('div'); s2KeyDiv.className = 'border-t pt-4'; s2KeyDiv.innerHTML = `

Semantic Scholar 获取,提高请求限额

`; container.appendChild(s2KeyDiv); // PubMed API Key(透传模式) const pubmedKeyDiv = document.createElement('div'); pubmedKeyDiv.innerHTML = `

NCBI 获取,提高请求限额

`; container.appendChild(pubmedKeyDiv); // Worker Auth Key(共享模式) const authKeyDiv = document.createElement('div'); authKeyDiv.className = 'border-t pt-4'; authKeyDiv.innerHTML = `

对应 Worker 环境变量 AUTH_SECRET(仅在共享模式需要)

`; container.appendChild(authKeyDiv); // 联系邮箱(可选,用于 CrossRef 和 OpenAlex 的 polite pool) const emailDiv = document.createElement('div'); emailDiv.innerHTML = `

提供邮箱(Polite Pool)可获得 CrossRefOpenAlex 更高的速率限制(点击链接以了解更多)

`; container.appendChild(emailDiv); // 测试/保存按钮 const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'pt-2 grid grid-cols-1 sm:grid-cols-2 gap-2'; buttonsDiv.innerHTML = ` `; container.appendChild(buttonsDiv); // 测试结果显示 const resultDiv = document.createElement('div'); resultDiv.id = 'academic-search-test-result'; resultDiv.className = 'text-sm mt-2'; resultDiv.style.display = 'none'; container.appendChild(resultDiv); modelConfigColumn.appendChild(container); // 绑定显示/隐藏切换事件 const toggleButtons = [ { btnId: 'academic-search-s2-toggle', inputId: 'academic-search-s2-key' }, { btnId: 'academic-search-pubmed-toggle', inputId: 'academic-search-pubmed-key' }, { btnId: 'academic-search-auth-toggle', inputId: 'academic-search-auth-key' } ]; toggleButtons.forEach(({ btnId, inputId }) => { const btn = document.getElementById(btnId); const input = document.getElementById(inputId); if (btn && input) { btn.addEventListener('click', () => { const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; btn.querySelector('span').textContent = isPassword ? '隐藏' : '显示'; btn.querySelector('iconify-icon').setAttribute('icon', isPassword ? 'carbon:view-off' : 'carbon:view'); }); } }); // 保存配置 document.getElementById('academic-search-save').onclick = () => { try { const newConfig = { enabled: document.getElementById('academic-search-enabled').checked, baseUrl: document.getElementById('academic-search-base-url').value.trim(), semanticScholarApiKey: document.getElementById('academic-search-s2-key').value.trim(), pubmedApiKey: document.getElementById('academic-search-pubmed-key').value.trim(), authKey: document.getElementById('academic-search-auth-key').value.trim(), contactEmail: document.getElementById('academic-search-contact-email').value.trim() }; // 保留已有的 rateLimit 信息(从测试连接获取) const existingConfig = JSON.parse(localStorage.getItem('academicSearchProxyConfig') || '{}'); if (existingConfig.rateLimit) { newConfig.rateLimit = existingConfig.rateLimit; } localStorage.setItem('academicSearchProxyConfig', JSON.stringify(newConfig)); showNotification && showNotification('学术搜索配置已保存', 'success'); // 通知学术搜索设置管理器重新加载(如果存在) if (window.academicSearchSettingsManager && typeof window.academicSearchSettingsManager.loadSettings === 'function') { window.academicSearchSettingsManager.loadSettings(); } } catch (e) { alert('保存配置失败:' + e.message); } }; // 测试连接 document.getElementById('academic-search-test').onclick = async () => { const baseUrl = document.getElementById('academic-search-base-url').value.trim(); const authKey = document.getElementById('academic-search-auth-key').value.trim(); const resultDiv = document.getElementById('academic-search-test-result'); if (!baseUrl) { resultDiv.style.display = 'block'; resultDiv.className = 'text-sm mt-2 p-2 bg-red-50 border border-red-200 text-red-700 rounded'; resultDiv.textContent = '❌ 请填写 Worker URL'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'text-sm mt-2 p-2 bg-blue-50 border border-blue-200 text-blue-700 rounded'; resultDiv.textContent = '⏳ 正在测试连接...'; try { const headers = { 'Content-Type': 'application/json' }; // 如果配置了 Auth Key,加入请求头 if (authKey) { headers['X-Auth-Key'] = authKey; } // 透传 API 密钥(用于密钥状态检测) const proxyConfig = JSON.parse(localStorage.getItem('academicSearchProxyConfig') || '{}'); if (proxyConfig.semanticScholarApiKey) { headers['X-Api-Key'] = proxyConfig.semanticScholarApiKey; } else if (proxyConfig.pubmedApiKey) { headers['X-Api-Key'] = proxyConfig.pubmedApiKey; } const response = await fetch(`${baseUrl}/health`, { method: 'GET', headers: headers }); if (response.ok) { const data = await response.json(); // 保存速率限制信息到配置中 const currentConfig = JSON.parse(localStorage.getItem('academicSearchProxyConfig') || '{}'); currentConfig.rateLimit = data.rateLimit || null; localStorage.setItem('academicSearchProxyConfig', JSON.stringify(currentConfig)); // 格式化输出 let servicesHtml = ''; if (data.services) { servicesHtml = '
可用服务:
'; } let rateLimitHtml = ''; if (data.rateLimit) { if (data.rateLimit.enabled) { rateLimitHtml = `
速率限制: TPS: ${data.rateLimit.tps}, TPM: ${data.rateLimit.tpm}, 每IP TPS: ${data.rateLimit.perIpTps}, 每IP TPM: ${data.rateLimit.perIpTpm}`; // 显示服务级别速率限制 if (data.rateLimit.services) { rateLimitHtml += '
'; if (data.rateLimit.services.pubmed) { rateLimitHtml += `
• PubMed: TPS ${data.rateLimit.services.pubmed.tps}, TPM ${data.rateLimit.services.pubmed.tpm}
`; } if (data.rateLimit.services.semanticscholar) { rateLimitHtml += `
• Semantic Scholar: TPS ${data.rateLimit.services.semanticscholar.tps}, TPM ${data.rateLimit.services.semanticscholar.tpm}
`; } rateLimitHtml += '
'; } rateLimitHtml += '
'; } else { rateLimitHtml = '
速率限制: 未启用
'; } } let authHtml = ''; if (data.authentication) { authHtml = `
认证: ${data.authentication.required ? '必需' : '不需要'}
`; } resultDiv.className = 'text-sm mt-2 p-2 bg-green-50 border border-green-200 text-green-700 rounded'; resultDiv.innerHTML = `✅ 连接成功!速率限制配置已保存${servicesHtml}${rateLimitHtml}${authHtml}`; } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { resultDiv.className = 'text-sm mt-2 p-2 bg-red-50 border border-red-200 text-red-700 rounded'; resultDiv.textContent = `❌ 连接失败:${error.message}`; } }; } function renderSourceSitesList() { const sitesListContainer = document.getElementById('sourceSitesListContainer'); if (!sitesListContainer) return; sitesListContainer.innerHTML = ''; const sites = loadAllCustomSourceSites(); const siteIds = Object.keys(sites); if (siteIds.length === 0) { sitesListContainer.innerHTML = '

还没有自定义源站。请点击上方按钮添加一个。

'; document.getElementById('sourceSiteConfigFormContainer').classList.add('hidden'); if (selectedModelForManager === 'custom') { keyManagerColumn.innerHTML = '

请添加并选择一个源站以管理其 API Keys。

'; } return; } const ul = document.createElement('ul'); ul.className = 'space-y-2'; siteIds.forEach(id => { const site = sites[id]; const li = document.createElement('li'); li.className = `p-3 border rounded-md flex justify-between items-center cursor-pointer hover:bg-gray-100 transition-colors ${currentSelectedSourceSiteId === id ? 'bg-blue-50 border-blue-300 shadow-md' : 'bg-white'}`; li.dataset.siteId = id; li.addEventListener('click', () => { selectSourceSite(id); }); const displayNameSpan = document.createElement('span'); displayNameSpan.textContent = site.displayName || `源站 (ID: ${id.substring(0,8)}...)`; displayNameSpan.className = 'font-medium text-sm text-gray-700 flex-grow'; const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'space-x-2 flex-shrink-0'; const editButton = document.createElement('button'); editButton.innerHTML = ''; editButton.title = '编辑此源站配置'; editButton.className = 'p-1.5 text-gray-500 hover:text-blue-700 rounded hover:bg-blue-100'; editButton.addEventListener('click', (e) => { e.stopPropagation(); currentSelectedSourceSiteId = id; renderSourceSitesList(); renderSourceSiteForm(site); keyManagerColumn.innerHTML = '

编辑源站配置中。保存或取消以管理 Keys。

'; }); const deleteButton = document.createElement('button'); deleteButton.innerHTML = ''; deleteButton.title = '删除此源站'; deleteButton.className = 'p-1.5 text-gray-500 hover:text-red-700 rounded hover:bg-red-100'; deleteButton.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`确定要删除源站 "${site.displayName || id}" 吗?其关联的API Keys也将被删除。`)) { deleteCustomSourceSite(id); if (typeof showNotification === 'function') showNotification(`源站 "${site.displayName || id}" 已删除。`, 'success'); if (currentSelectedSourceSiteId === id) { currentSelectedSourceSiteId = null; keyManagerColumn.innerHTML = '

请选择一个源站以管理其 API Keys。

'; document.getElementById('sourceSiteConfigFormContainer').classList.add('hidden'); } renderSourceSitesList(); } }); buttonsDiv.appendChild(editButton); buttonsDiv.appendChild(deleteButton); li.appendChild(displayNameSpan); li.appendChild(buttonsDiv); ul.appendChild(li); }); sitesListContainer.appendChild(ul); if (!currentSelectedSourceSiteId && siteIds.length > 0) { selectSourceSite(siteIds[0]); } else if (currentSelectedSourceSiteId && sites[currentSelectedSourceSiteId]) { renderKeyManagerForModel(`custom_source_${currentSelectedSourceSiteId}`); } else if (siteIds.length > 0) { // Has sites, but nothing selected (e.g. after a delete) keyManagerColumn.innerHTML = '

请选择一个源站以管理其 API Keys。

'; document.getElementById('sourceSiteConfigFormContainer').classList.add('hidden'); } } function selectSourceSite(siteId) { currentSelectedSourceSiteId = siteId; const sites = loadAllCustomSourceSites(); const site = sites[siteId]; if (site) { renderKeyManagerForModel(`custom_source_${siteId}`); const formContainer = document.getElementById('sourceSiteConfigFormContainer'); if (formContainer) { formContainer.classList.add('hidden'); formContainer.innerHTML = ''; } } renderSourceSitesList(); } function renderSourceSiteForm(siteData) { const formContainer = document.getElementById('sourceSiteConfigFormContainer'); if (!formContainer) return; formContainer.innerHTML = ''; formContainer.classList.remove('hidden'); const isEditing = siteData !== null; const formTitleText = isEditing ? `编辑源站: ${siteData.displayName || '未命名'}` : '添加新源站'; const formTitle = document.createElement('h4'); formTitle.textContent = formTitleText; formTitle.className = 'text-md font-semibold mb-3 text-gray-700'; formContainer.appendChild(formTitle); const form = document.createElement('form'); form.className = 'space-y-3'; const siteIdForForm = isEditing ? siteData.id : _generateUUID_ui(); form.appendChild(createConfigInput(`sourceDisplayName_${siteIdForForm}`, '显示名称 *', isEditing ? siteData.displayName : '', 'text', '例如: 我的备用 OpenAI', () => {})); form.appendChild(createConfigInput(`sourceApiBaseUrl_${siteIdForForm}`, 'API Base URL *', isEditing ? siteData.apiBaseUrl : '', 'url', '例如: https://api.openai.com', () => {})); const endpointModeOptions = [ { value: 'auto', text: '自动补全(必要时追加 /v1/...)' }, { value: 'chat', text: '仅追加 /chat/completions' }, { value: 'manual', text: '已是完整端点(不追加)' } ]; const endpointModeField = createConfigSelect( `sourceEndpointMode_${siteIdForForm}`, '端点补全方式', isEditing ? (siteData.endpointMode || 'auto') : 'auto', endpointModeOptions, () => {} ); const endpointModeHint = document.createElement('p'); endpointModeHint.className = 'mt-1 text-[11px] text-gray-500 leading-4'; endpointModeHint.textContent = '若第三方已提供完整的 /chat/completions 或 /messages 地址,请选择“已是完整端点”。'; endpointModeField.appendChild(endpointModeHint); form.appendChild(endpointModeField); // --- Enhanced Model ID Input with Detection --- const modelIdGroup = document.createElement('div'); modelIdGroup.className = 'mb-3'; const modelIdLabel = document.createElement('label'); modelIdLabel.htmlFor = `sourceModelId_${siteIdForForm}`; modelIdLabel.className = 'block text-xs font-medium text-gray-600 mb-1'; modelIdLabel.textContent = '默认模型 ID *'; modelIdGroup.appendChild(modelIdLabel); const modelIdInputContainer = document.createElement('div'); modelIdInputContainer.id = `sourceModelIdInputContainer_${siteIdForForm}`; // Container to hold input/select modelIdInputContainer.className = 'flex flex-col sm:flex-row sm:items-center sm:space-x-2 space-y-2 sm:space-y-0'; let modelIdEditableElement = document.createElement('input'); modelIdEditableElement.type = 'text'; modelIdEditableElement.id = `sourceModelId_${siteIdForForm}`; modelIdEditableElement.name = `sourceModelId_${siteIdForForm}`; modelIdEditableElement.value = isEditing ? siteData.modelId : ''; modelIdEditableElement.placeholder = '例如: gpt-4-turbo'; modelIdEditableElement.className = 'w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors'; modelIdInputContainer.appendChild(modelIdEditableElement); const detectModelsButton = document.createElement('button'); detectModelsButton.type = 'button'; detectModelsButton.innerHTML = '检测'; detectModelsButton.title = '从此 Base URL 检测可用模型'; detectModelsButton.className = 'px-3 py-1.5 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors flex items-center justify-center w-full sm:w-auto'; modelIdInputContainer.appendChild(detectModelsButton); const searchModelsButton = document.createElement('button'); searchModelsButton.type = 'button'; searchModelsButton.id = `sourceModelSearchBtn_${siteIdForForm}`; searchModelsButton.innerHTML = '搜索模型'; searchModelsButton.className = 'px-3 py-1.5 text-xs border border-gray-300 rounded text-gray-600 hover:text-blue-600 hover:border-blue-400 transition-colors flex-shrink-0 flex items-center disabled:opacity-60 disabled:cursor-not-allowed'; searchModelsButton.disabled = true; modelIdInputContainer.appendChild(searchModelsButton); modelIdGroup.appendChild(modelIdInputContainer); // Temporary API Key for detection const tempApiKeyInput = createConfigInput(`sourceTempApiKey_${siteIdForForm}`, 'API Key (检测时使用,可留空)', '', 'password', '如需临时检测可填写 Key', null, {autocomplete: 'new-password'}); tempApiKeyInput.classList.add('text-xs'); // Smaller label tempApiKeyInput.querySelector('label').classList.add('text-gray-500'); tempApiKeyInput.querySelector('input').classList.add('text-xs', 'py-1'); const tempHint = document.createElement('p'); tempHint.className = 'mt-1 text-[11px] text-slate-400'; tempHint.textContent = '如已在下方“API Key”列表中添加 Key,可留空自动使用。'; tempApiKeyInput.appendChild(tempHint); modelIdGroup.appendChild(tempApiKeyInput); // Add it below the model ID input group form.appendChild(modelIdGroup); // Event listener for detectModelsButton detectModelsButton.addEventListener('click', async () => { const baseUrl = document.getElementById(`sourceApiBaseUrl_${siteIdForForm}`).value.trim(); let tempApiKey = document.getElementById(`sourceTempApiKey_${siteIdForForm}`).value.trim(); let usedStoredKey = false; if (!baseUrl) { showNotification('请输入 API Base URL 以检测模型。', 'warning'); return; } if (!tempApiKey && typeof loadModelKeys === 'function') { const storedKeys = (loadModelKeys(`custom_source_${siteIdForForm}`) || []) .filter(k => k && k.value && k.value.trim() && k.status !== 'invalid'); if (storedKeys.length > 0) { tempApiKey = storedKeys[0].value.trim(); usedStoredKey = true; } } if (!tempApiKey) { showNotification('未找到可用的 API Key,请在下方添加或临时输入再检测。', 'warning'); return; } detectModelsButton.disabled = true; detectModelsButton.innerHTML = '检测中...'; const endpointModeSelect = document.getElementById(`sourceEndpointMode_${siteIdForForm}`); const requestFormatSelect = document.getElementById(`sourceRequestFormat_${siteIdForForm}`); const endpointModeValue = endpointModeSelect ? endpointModeSelect.value : 'auto'; const requestFormatValue = requestFormatSelect ? requestFormatSelect.value : 'openai'; try { const detectedModels = await window.modelDetector.detectModelsForModal(baseUrl, tempApiKey, requestFormatValue, endpointModeValue); if (usedStoredKey) { showNotification && showNotification('已使用已保存的 Key 进行模型检测。', 'info'); } showNotification(`检测到 ${detectedModels.length} 个模型。`, 'success'); const cacheKey = `custom_source_${siteIdForForm}`; if (!detectedModels || detectedModels.length === 0) { setModelSearchCache(cacheKey, []); searchModelsButton.disabled = true; if (typeof showNotification === 'function') { showNotification('未返回模型列表,请检查 Base URL 或 API Key。', 'info'); } return; } const currentModelIdValue = document.getElementById(`sourceModelId_${siteIdForForm}`).value; const newSelect = document.createElement('select'); newSelect.id = `sourceModelId_${siteIdForForm}`; // Keep the same ID for form submission newSelect.name = `sourceModelId_${siteIdForForm}`; newSelect.className = 'w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors'; // Option for manual input const manualOption = document.createElement('option'); manualOption.value = "__manual_input__"; // Special value manualOption.textContent = "-- 手动输入其他模型 --"; newSelect.appendChild(manualOption); const normalized = []; detectedModels.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name || model.id; newSelect.appendChild(option); normalized.push({ value: model.id, label: model.name || model.id, description: model.rawName || '' }); }); // Replace the input with the select const inputContainer = document.getElementById(`sourceModelIdInputContainer_${siteIdForForm}`); const oldInput = document.getElementById(`sourceModelId_${siteIdForForm}`); inputContainer.insertBefore(newSelect, oldInput); // Insert select before old input if(oldInput) oldInput.remove(); // Remove the old text input modelIdEditableElement = newSelect; // Update reference setModelSearchCache(cacheKey, normalized); registerModelSearchIntegration({ key: cacheKey, selectEl: newSelect, buttonEl: searchModelsButton, title: `选择模型(${document.getElementById(`sourceDisplayName_${siteIdForForm}`).value || '自定义源'})`, placeholder: '搜索模型 ID...', emptyMessage: '未找到匹配的模型', onEmpty: () => { if (!detectModelsButton.disabled) detectModelsButton.click(); return true; } }); searchModelsButton.disabled = false; // Try to set the value let modelFoundInSelect = false; if (currentModelIdValue) { const existingOption = Array.from(newSelect.options).find(opt => opt.value === currentModelIdValue); if (existingOption) { newSelect.value = currentModelIdValue; modelFoundInSelect = true; } } if (!modelFoundInSelect && detectedModels.length > 0 && !currentModelIdValue) { newSelect.value = detectedModels[0].id; // Default to first detected if no prior value } else if (!modelFoundInSelect && currentModelIdValue) { newSelect.value = "__manual_input__"; // Fallback to manual if current value not in list // We might need to re-create a text input here if manual is selected and there was a value // For now, this just selects "manual input" in the dropdown. } // If manual input is selected, and there was a value, we might want to show a text input again. // This part can be enhanced later for a smoother UX when switching back to manual from select. } catch (error) { showNotification(`模型检测失败: ${error.message}`, 'error'); console.error("Model detection error in form:", error); setModelSearchCache(`custom_source_${siteIdForForm}`, []); searchModelsButton.disabled = true; } finally { detectModelsButton.disabled = false; detectModelsButton.innerHTML = '检测'; } }); // --- End of Enhanced Model ID Input --- const requestFormatOptions = [ { value: 'openai', text: 'OpenAI 格式' }, { value: 'anthropic', text: 'Anthropic 格式' }, { value: 'gemini', text: 'Google Gemini 格式' } ]; form.appendChild(createConfigSelect(`sourceRequestFormat_${siteIdForForm}`, '请求格式', isEditing ? siteData.requestFormat : 'openai', requestFormatOptions, () => {})); form.appendChild(createConfigInput(`sourceTemperature_${siteIdForForm}`, '温度 (0-2)', isEditing ? siteData.temperature : 0.5, 'number', '0.5', () => {}, {min:0, max:2, step:0.01})); form.appendChild(createConfigInput(`sourceMaxTokens_${siteIdForForm}`, '最大 Tokens', isEditing ? siteData.max_tokens : 8000, 'number', '8000', () => {}, {min:1, step:1})); const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'flex space-x-2 pt-3 border-t mt-2'; const saveButton = document.createElement('button'); saveButton.type = 'submit'; saveButton.innerHTML = '保存'; saveButton.className = 'px-3 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors flex items-center'; const cancelButton = document.createElement('button'); cancelButton.type = 'button'; cancelButton.textContent = '取消'; cancelButton.className = 'px-3 py-1.5 text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 rounded transition-colors'; cancelButton.addEventListener('click', () => { formContainer.classList.add('hidden'); formContainer.innerHTML = ''; if (currentSelectedSourceSiteId) { selectSourceSite(currentSelectedSourceSiteId); } else if (Object.keys(loadAllCustomSourceSites()).length > 0){ selectSourceSite(Object.keys(loadAllCustomSourceSites())[0]); } else { keyManagerColumn.innerHTML = '

请选择或添加一个源站以管理其 API Keys。

'; } }); buttonsDiv.appendChild(saveButton); buttonsDiv.appendChild(cancelButton); form.appendChild(buttonsDiv); form.addEventListener('submit', (e) => { e.preventDefault(); const newSiteData = { id: siteIdForForm, displayName: document.getElementById(`sourceDisplayName_${siteIdForForm}`).value.trim(), apiBaseUrl: document.getElementById(`sourceApiBaseUrl_${siteIdForForm}`).value.trim(), // Get modelId from the input/select, which now shares the same ID modelId: document.getElementById(`sourceModelId_${siteIdForForm}`).value === '__manual_input__' ? '' : document.getElementById(`sourceModelId_${siteIdForForm}`).value.trim(), requestFormat: document.getElementById(`sourceRequestFormat_${siteIdForForm}`).value, temperature: parseFloat(document.getElementById(`sourceTemperature_${siteIdForForm}`).value), max_tokens: parseInt(document.getElementById(`sourceMaxTokens_${siteIdForForm}`).value), availableModels: isEditing && siteData.availableModels ? siteData.availableModels : [], endpointMode: document.getElementById(`sourceEndpointMode_${siteIdForForm}`).value }; if (!newSiteData.displayName || !newSiteData.apiBaseUrl || !newSiteData.modelId) { if (typeof showNotification === 'function') showNotification('显示名称、API Base URL 和模型 ID 不能为空!', 'error'); return; } saveCustomSourceSite(newSiteData); if (typeof showNotification === 'function') showNotification(`源站 "${newSiteData.displayName}" 已${isEditing ? '更新' : '添加'}。`, 'success'); formContainer.classList.add('hidden'); formContainer.innerHTML = ''; currentSelectedSourceSiteId = siteIdForForm; renderSourceSitesList(); selectSourceSite(siteIdForForm); }); formContainer.appendChild(form); document.getElementById(`sourceDisplayName_${siteIdForForm}`).focus(); } function renderKeyManagerForModel(modelKeyOrSourceSiteModelName) { keyManagerColumn.innerHTML = ''; if (currentManagerUI && typeof currentManagerUI.destroy === 'function') { currentManagerUI.destroy(); } currentManagerUI = new KeyManagerUI( modelKeyOrSourceSiteModelName, keyManagerColumn, handleTestKey, handleTestAllKeys, loadModelKeys, saveModelKeys ); // 追加:对于 Gemini 提供“检测可用模型并设为默认”的小面板 if (modelKeyOrSourceSiteModelName === 'gemini') { const panel = document.createElement('div'); panel.className = 'mt-4 p-3 border rounded-md bg-blue-50'; panel.innerHTML = `
Gemini 可用模型检测
点击“检测”从 Google API 拉取模型列表
`; keyManagerColumn.appendChild(panel); const detectBtn = panel.querySelector('#detectGeminiModelsBtn'); const area = panel.querySelector('#geminiModelsArea'); let searchBtn; detectBtn.onclick = async () => { const keys = (loadModelKeys('gemini') || []).filter(k => k.status !== 'invalid' && k.value); if (keys.length === 0) { area.innerHTML = '无可用 Gemini API Key'; return; } const apiKey = keys[0].value.trim(); detectBtn.disabled = true; detectBtn.textContent = '检测中...'; let searchBtn; try { 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) { area.innerHTML = '未返回模型列表'; return; } const select = document.createElement('select'); select.className = 'mt-2 w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors'; const normalized = []; items.forEach(m => { const id = m.name ? String(m.name).split('/').pop() : (m.id || ''); if (!id || normalized.some(n => n.value === id)) return; const opt = document.createElement('option'); opt.value = id; opt.textContent = id; select.appendChild(opt); const display = m.displayName || m.description || ''; normalized.push({ value: id, label: id, description: display }); }); const cacheKey = 'gemini_key_manager_detect_list'; setModelSearchCache(cacheKey, normalized); searchBtn = document.createElement('button'); searchBtn.className = 'mt-2 px-3 py-1.5 text-xs border border-gray-300 rounded text-gray-600 hover:text-blue-600 hover:border-blue-400 transition-colors flex items-center justify-center w-full sm:w-auto'; searchBtn.innerHTML = '搜索模型'; registerModelSearchIntegration({ key: cacheKey, selectEl: select, buttonEl: searchBtn, title: '选择 Gemini 模型', placeholder: '搜索模型 ID 或名称...', emptyMessage: '未找到匹配的模型', onEmpty: () => { detectBtn.click(); return true; }, onSelect: (value) => { if (select.value !== value) { select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); } } }); searchBtn.disabled = normalized.length === 0; const saveBtn = document.createElement('button'); saveBtn.className = 'mt-2 px-3 py-1.5 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded w-full sm:w-auto'; saveBtn.textContent = '设为默认模型'; saveBtn.onclick = () => { saveModelConfig('gemini', { preferredModelId: select.value }); if (typeof showNotification === 'function') showNotification(`Gemini 默认模型已设为 ${select.value}`, 'success'); }; area.innerHTML = ''; const controls = document.createElement('div'); controls.className = 'mt-2 grid grid-cols-1 sm:grid-cols-[minmax(0,1fr)_auto_auto] sm:items-center gap-2'; controls.appendChild(select); controls.appendChild(searchBtn); controls.appendChild(saveBtn); area.appendChild(controls); } catch (e) { console.error(e); area.innerHTML = `检测失败: ${e.message}`; setModelSearchCache('gemini_key_manager_detect_list', []); if (searchBtn) searchBtn.disabled = true; } finally { detectBtn.disabled = false; detectBtn.textContent = '检测'; } }; } // 追加:DeepSeek 检测面板 if (modelKeyOrSourceSiteModelName === 'deepseek') { const panel = document.createElement('div'); panel.className = 'mt-4 p-3 border rounded-md bg-blue-50'; panel.innerHTML = `
DeepSeek 可用模型检测
点击“检测”从 DeepSeek API 拉取模型列表
`; keyManagerColumn.appendChild(panel); const detectBtn = panel.querySelector('#detectDeepseekModelsBtn'); const area = panel.querySelector('#deepseekModelsArea'); detectBtn.onclick = async () => { const keys = (loadModelKeys('deepseek') || []).filter(k => k.status !== 'invalid' && k.value); if (keys.length === 0) { area.innerHTML = '无可用 DeepSeek API Key'; return; } const apiKey = keys[0].value.trim(); detectBtn.disabled = true; detectBtn.textContent = '检测中...'; let searchBtn; try { 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 : []; if (items.length === 0) { area.innerHTML = '未返回模型列表'; setModelSearchCache('deepseek_key_manager_detect_list', []); return; } const select = document.createElement('select'); select.className = 'mt-2 w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors'; const normalized = []; items.forEach(m => { const id = m.id; if (!id || normalized.some(n => n.value === id)) return; const opt = document.createElement('option'); opt.value = id; opt.textContent = id; select.appendChild(opt); normalized.push({ value: id, label: id, description: '' }); }); const cacheKey = 'deepseek_key_manager_detect_list'; setModelSearchCache(cacheKey, normalized); searchBtn = document.createElement('button'); searchBtn.className = 'mt-2 px-3 py-1.5 text-xs border border-gray-300 rounded text-gray-600 hover:text-blue-600 hover:border-blue-400 transition-colors flex items-center justify-center w-full sm:w-auto'; searchBtn.innerHTML = '搜索模型'; registerModelSearchIntegration({ key: cacheKey, selectEl: select, buttonEl: searchBtn, title: '选择 DeepSeek 模型', placeholder: '搜索模型 ID...', emptyMessage: '未找到匹配的模型', onEmpty: () => { detectBtn.click(); return true; }, onSelect: value => { if (select.value !== value) { select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); } } }); searchBtn.disabled = normalized.length === 0; const saveBtn = document.createElement('button'); saveBtn.className = 'mt-2 px-3 py-1.5 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded w-full sm:w-auto'; saveBtn.textContent = '设为默认模型'; saveBtn.onclick = () => { saveModelConfig('deepseek', { preferredModelId: select.value }); showNotification && showNotification(`DeepSeek 默认模型已设为 ${select.value}`, 'success'); }; area.innerHTML = ''; const controls = document.createElement('div'); controls.className = 'mt-2 grid grid-cols-1 sm:grid-cols-[minmax(0,1fr)_auto_auto] sm:items-center gap-2'; controls.appendChild(select); controls.appendChild(searchBtn); controls.appendChild(saveBtn); area.appendChild(controls); } catch (error) { console.error(error); area.innerHTML = `检测失败: ${error.message}`; setModelSearchCache('deepseek_key_manager_detect_list', []); if (searchBtn) searchBtn.disabled = true; } finally { detectBtn.disabled = false; detectBtn.textContent = '检测'; } }; } if (modelKeyOrSourceSiteModelName === 'tongyi') { const panel = document.createElement('div'); panel.className = 'mt-4 p-3 border rounded-md bg-blue-50'; panel.innerHTML = `
通义 可用模型检测
点击“检测”从 DashScope API 拉取模型列表
`; keyManagerColumn.appendChild(panel); const detectBtn = panel.querySelector('#detectTongyiModelsBtn'); const area = panel.querySelector('#tongyiModelsArea'); detectBtn.onclick = async () => { let keys = (loadModelKeys('tongyi') || []); keys = keys.filter(k => k.status !== 'invalid' && k.value); if (keys.length === 0) { area.innerHTML = '无可用 通义 API Key'; return; } const apiKey = keys[0].value.trim(); detectBtn.disabled = true; detectBtn.textContent = '检测中...'; let searchBtn; try { 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 : [])); if (!items || items.length === 0) { area.innerHTML = '未返回模型列表'; setModelSearchCache('tongyi_key_manager_detect_list', []); return; } const select = document.createElement('select'); select.className = 'mt-2 w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors'; const normalized = []; items.forEach(m => { const id = m.model || m.id || m.name; if (!id || normalized.some(n => n.value === id)) return; const opt = document.createElement('option'); opt.value = id; opt.textContent = id; select.appendChild(opt); normalized.push({ value: id, label: id, description: '' }); }); const cacheKey = 'tongyi_key_manager_detect_list'; setModelSearchCache(cacheKey, normalized); searchBtn = document.createElement('button'); searchBtn.className = 'mt-2 px-3 py-1.5 text-xs border border-gray-300 rounded text-gray-600 hover:text-blue-600 hover:border-blue-400 transition-colors flex items-center justify-center w-full sm:w-auto'; searchBtn.innerHTML = '搜索模型'; registerModelSearchIntegration({ key: cacheKey, selectEl: select, buttonEl: searchBtn, title: '选择通义模型', placeholder: '搜索模型 ID...', emptyMessage: '未找到匹配的模型', onEmpty: () => { detectBtn.click(); return true; }, onSelect: value => { if (select.value !== value) { select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); } } }); searchBtn.disabled = normalized.length === 0; const saveBtn = document.createElement('button'); saveBtn.className = 'mt-2 px-3 py-1.5 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded w-full sm:w-auto'; saveBtn.textContent = '设为默认模型'; saveBtn.onclick = () => { saveModelConfig('tongyi', { preferredModelId: select.value }); showNotification && showNotification(`通义 默认模型已设为 ${select.value}`, 'success'); }; area.innerHTML = ''; const controls = document.createElement('div'); controls.className = 'mt-2 grid grid-cols-1 sm:grid-cols-[minmax(0,1fr)_auto_auto] sm:items-center gap-2'; controls.appendChild(select); controls.appendChild(searchBtn); controls.appendChild(saveBtn); area.appendChild(controls); } catch (e) { if (typeof console !== 'undefined') console.error(e); area.innerHTML = `检测失败: ${e.message}`; setModelSearchCache('tongyi_key_manager_detect_list', []); if (searchBtn) searchBtn.disabled = true; } finally { detectBtn.disabled = false; detectBtn.textContent = '检测'; } }; } // 追加:火山 检测面板(两个火山条目使用 'volcano') if (modelKeyOrSourceSiteModelName === 'volcano') { const panel = document.createElement('div'); panel.className = 'mt-4 p-3 border rounded-md bg-blue-50'; panel.innerHTML = `
火山 可用模型检测
点击“检测”从 Ark API 拉取模型列表
`; // 修正误植 panel.innerHTML = panel.innerHTML.replace('之间', 'between').replace('白', 'white'); keyManagerColumn.appendChild(panel); const detectBtn = panel.querySelector('#detectVolcanoModelsBtn'); const area = panel.querySelector('#volcanoModelsArea'); // 按用户要求:不提供在线检测,改为手动输入并保存 if (detectBtn && area) { detectBtn.style.display = 'none'; area.innerHTML = `
不提供在线检测;请手动输入模型ID。
`; try { const cfg = loadModelConfig && loadModelConfig('volcano'); if (cfg && (cfg.preferredModelId || cfg.modelId)) area.querySelector('#volcanoKMManualInput').value = cfg.preferredModelId || cfg.modelId; } catch {} const saveBtn = area.querySelector('#volcanoKMSaveBtn'); saveBtn.onclick = () => { const val = (area.querySelector('#volcanoKMManualInput').value || '').trim(); if (!val) { showNotification && showNotification('请输入模型ID', 'warning'); return; } saveModelConfig && saveModelConfig('volcano', { preferredModelId: val }); showNotification && showNotification(`火山 默认模型已设为 ${val}`, 'success'); }; // 不再绑定在线检测 return; } if (detectBtn) detectBtn.style.display = 'none'; if (area) area.innerHTML = '请手动输入模型ID,或在设置中选择。示例:doubao-1-5-pro-32k-250115 / deepseek-v3-250324'; } if (modelKeyOrSourceSiteModelName === 'deeplx') { const panel = document.createElement('div'); panel.className = 'mt-4 p-3 border rounded-md bg-blue-50'; const placeholderHtml = DEEPLX_DEFAULT_ENDPOINT_TEMPLATE.replace(/&/g, '&').replace(//g, '>'); panel.innerHTML = `
DeepLX 接口模板

模板中的 <api-key> 或 {API_KEY} 会自动替换为当前使用的 Key,可用于自建代理地址。

`; keyManagerColumn.appendChild(panel); const inputEl = panel.querySelector('#deeplxEndpointTemplateInput-manager'); const resetBtn = panel.querySelector('#deeplxEndpointResetBtn-manager'); setupDeeplxEndpointInput(inputEl, resetBtn); } } // 导出到全局,供模块使用 window.renderKeyManagerForModel = renderKeyManagerForModel; async function handleTestKey(modelName, keyObject) { if (!currentManagerUI) return; currentManagerUI.updateKeyStatus(keyObject.id, 'testing'); let modelConfigForTest = {}; let apiEndpointForTest = null; // 获取友好显示名 let modelDisplayNameForNotification = modelName; if (modelName.startsWith('custom_source_')) { const sourceSiteId = modelName.replace('custom_source_', ''); if (typeof loadAllCustomSourceSites === 'function') { const sites = loadAllCustomSourceSites(); const site = sites[sourceSiteId]; if (site && site.displayName) { modelDisplayNameForNotification = `"${site.displayName}"`; } else { modelDisplayNameForNotification = `源站点 (ID: ...${sourceSiteId.slice(-8)})`; } } } if (modelName.startsWith('custom_source_')) { const sourceSiteId = modelName.replace('custom_source_', ''); const allSites = loadAllCustomSourceSites(); const siteConfig = allSites[sourceSiteId]; if (siteConfig && siteConfig.apiBaseUrl && siteConfig.modelId) { apiEndpointForTest = siteConfig.apiBaseUrl; modelConfigForTest = { ...siteConfig, apiEndpoint: siteConfig.apiBaseUrl }; } else { currentManagerUI.updateKeyStatus(keyObject.id, 'untested'); showNotification(`源站配置不完整 (ID: ${sourceSiteId}),缺少 API Base URL 或模型 ID。请在配置区完善。`, 'error'); return; } } else { modelConfigForTest = loadModelConfig(modelName) || {}; apiEndpointForTest = modelConfigForTest.apiEndpoint; } try { let isValid = false; if (modelName === 'mistral') { // 特殊处理:使用 Mistral 的 /v1/models 端点快速测活 try { const resp = await fetch('https://api.mistral.ai/v1/models', { headers: { 'Authorization': `Bearer ${keyObject.value}` } }); isValid = resp.ok; } catch (e) { isValid = false; } } else { const r = await testModelKey(modelName, keyObject.value, modelConfigForTest, apiEndpointForTest); isValid = !!r; } currentManagerUI.updateKeyStatus(keyObject.id, isValid ? 'valid' : 'invalid'); showNotification(`Key (${keyObject.value.substring(0,4)}...) for ${modelDisplayNameForNotification} test: ${isValid ? '有效' : '无效'}`, isValid ? 'success' : 'error'); } catch (error) { console.error("Key test error:", error); currentManagerUI.updateKeyStatus(keyObject.id, 'invalid'); showNotification(`Key test for ${modelDisplayNameForNotification} failed: ${error.message}`, 'error'); } } async function handleTestAllKeys(modelName, keysArray) { // 获取友好显示名 let modelDisplayNameForNotification = modelName; if (modelName.startsWith('custom_source_')) { const sourceSiteId = modelName.replace('custom_source_', ''); if (typeof loadAllCustomSourceSites === 'function') { const sites = loadAllCustomSourceSites(); const site = sites[sourceSiteId]; if (site && site.displayName) { modelDisplayNameForNotification = `"${site.displayName}"`; } else { modelDisplayNameForNotification = `源站点 (ID: ...${sourceSiteId.slice(-8)})`; } } } showNotification(`开始批量测试 ${modelDisplayNameForNotification} 的 ${keysArray.length} 个Key...`, 'info'); for (const keyObj of keysArray) { await handleTestKey(modelName, keyObj); } showNotification(`${modelDisplayNameForNotification} 的所有 Key 测试完毕。`, 'info'); } // 旧的 updateCustomModelConfig, handleDetectModelsInModal might need to be adapted or removed // if their functionality is now part of the source site form. // The functions createConfigInput and createConfigSelect are still useful for the new form. window.refreshKeyManagerForModel = (modelName, keyId, newStatus) => { if (modelKeyManagerModal && !modelKeyManagerModal.classList.contains('hidden') && currentManagerUI && currentManagerUI.modelName === modelName) { currentManagerUI.updateKeyStatus(keyId, newStatus); } }; // 新增: Event-Listener für das customSourceSiteSelect Dropdown-Menü if (customSourceSiteSelect) { customSourceSiteSelect.addEventListener('change', () => { // 保存当前选中的源站点ID到设置 let settings = typeof loadSettings === 'function' ? loadSettings() : {}; settings.selectedCustomSourceSiteId = customSourceSiteSelect.value; if (typeof saveSettings === 'function') { saveSettings(settings); } else { localStorage.setItem('paperBurnerSettings', JSON.stringify(settings)); } // 原有逻辑 saveCurrentSettings && saveCurrentSettings(); // 新增:切换后立即刷新信息面板 if (typeof updateCustomSourceSiteInfo === 'function') { updateCustomSourceSiteInfo(customSourceSiteSelect.value); } if (typeof window.refreshCustomSourceSiteInfo === 'function') { window.refreshCustomSourceSiteInfo({ autoSelect: false }); } }); } // 新增:更新自定义源站点信息函数 function updateCustomSourceSiteInfo(siteId) { const infoContainer = document.getElementById('customSourceSiteInfo'); const manageKeyBtn = document.getElementById('manageSourceSiteKeyBtn'); if (!infoContainer || !manageKeyBtn) return; if (!siteId) { infoContainer.classList.add('hidden'); manageKeyBtn.classList.add('hidden'); return; } try { const sites = typeof loadAllCustomSourceSites === 'function' ? loadAllCustomSourceSites() : {}; const site = sites[siteId]; if (site) { // 显示信息面板和按钮 infoContainer.classList.remove('hidden'); manageKeyBtn.classList.remove('hidden'); // 获取可用API Key数量 - 移到前面以便模板使用 const customSourceKeysCount = typeof loadModelKeys === 'function' ? (loadModelKeys(`custom_source_${siteId}`) || []).filter(k => k.status !== 'invalid').length : 0; const endpointModeLabels = { auto: '自动补全 /v1/... (默认)', chat: '仅追加 /chat/completions', manual: '完整端点(不自动追加)' }; const endpointModeLabel = endpointModeLabels[site.endpointMode] || endpointModeLabels.auto; // 构建HTML以展示站点信息 let infoHtml = `

${site.displayName || '未命名源站点'}

API Base URL: ${site.apiBaseUrl || '未设置'}
端点补全: ${endpointModeLabel}
当前模型: ${site.modelId || '未设置'}
请求格式: ${site.requestFormat || 'openai'}
温度: ${site.temperature || '0.5'}
`; // 如果有可用模型列表,则展示为可选择的下拉框 if (site.availableModels && site.availableModels.length > 0) { infoHtml += `
选择模型:
检测到 ${site.availableModels.length} 个可用模型
`; } else { // 没有可用模型列表时,显示更明确的提示和手动输入选项 infoHtml += `
模型ID:
还未检测模型
点击 ${customSourceKeysCount > 0 ? `${customSourceKeysCount}个可用Key` : `请先添加API Key`}
`; } // 添加键检查信息和API Key管理按钮 infoHtml += `
API Keys: ${customSourceKeysCount > 0 ? `${customSourceKeysCount}个可用Key` : `无可用Key`}
`; infoHtml += `
`; infoContainer.innerHTML = infoHtml; const cacheKey = `custom_source_${siteId}`; const modelSelectEl = document.getElementById(`sourceSiteModelSelect_${siteId}`); const searchBtnEl = document.getElementById(`sourceSiteModelSearchBtn_${siteId}`); if (modelSelectEl) { const normalizedModels = []; const seenModelIds = new Set(); (site.availableModels || []).forEach(model => { const modelId = model && (model.id || model.name); if (!modelId || seenModelIds.has(modelId)) return; seenModelIds.add(modelId); normalizedModels.push({ value: modelId, label: model.name || modelId, description: model.rawName || model.description || '' }); }); setModelSearchCache(cacheKey, normalizedModels); if (searchBtnEl) { registerModelSearchIntegration({ key: cacheKey, selectEl: modelSelectEl, buttonEl: searchBtnEl, title: `选择模型(${site.displayName || '自定义源'})`, placeholder: '搜索模型 ID...', emptyMessage: '未找到匹配的模型', onEmpty: () => { const reDetectBtn = document.getElementById(`reDetectModelsBtn_${siteId}`) || document.getElementById(`infoDetectModelsBtn_${siteId}`); if (reDetectBtn && !reDetectBtn.disabled) { reDetectBtn.click(); } return true; }, onSelect: (value) => { if (!modelSelectEl || !value) return; if (modelSelectEl.value !== value) { modelSelectEl.value = value; modelSelectEl.dispatchEvent(new Event('change', { bubbles: true })); } } }); } } else if (searchBtnEl) { searchBtnEl.disabled = true; } // 隐藏底部的管理按钮 - 因为我们有了内联的按钮 manageKeyBtn.classList.add('hidden'); // 为内联的管理API Key按钮添加点击事件 const infoManageKeyBtn = document.getElementById(`infoManageKeyBtn_${siteId}`); if (infoManageKeyBtn) { infoManageKeyBtn.onclick = function() { // 打开模型Key管理弹窗 document.getElementById('modelKeyManagerBtn').click(); // 等待弹窗打开,然后设置正确的模型和源站点 setTimeout(() => { if (typeof selectModelForManager === 'function') { // 选择custom模型 selectModelForManager('custom'); // 再选择特定源站点 if (typeof selectSourceSite === 'function') { selectSourceSite(siteId); } } }, 100); }; } // 为内联的检测模型按钮添加点击事件 const infoDetectModelsBtn = document.getElementById(`infoDetectModelsBtn_${siteId}`); if (infoDetectModelsBtn) { infoDetectModelsBtn.onclick = function() { // 直接调用外部检测按钮的点击事件 const mainDetectBtn = document.getElementById('detectModelsBtn'); if (mainDetectBtn) { mainDetectBtn.click(); } }; } // 为重新检测按钮添加事件 const reDetectBtn = document.getElementById(`reDetectModelsBtn_${siteId}`); if (reDetectBtn) { reDetectBtn.onclick = function() { const mainDetectBtn = document.getElementById('detectModelsBtn'); if (mainDetectBtn) { mainDetectBtn.click(); } }; } // 添加新功能:绑定模型选择/保存事件 setTimeout(() => { // 1. 如果有可用模型下拉框,绑定保存事件 const modelSelect = document.getElementById(`sourceSiteModelSelect_${siteId}`); // 新增:如果 site.modelId 为空,自动选中第一个并保存 if (modelSelect && (!site.modelId || !site.availableModels.some(m => m.id === site.modelId))) { if (modelSelect.options.length > 0) { const firstModelId = modelSelect.options[0].value; if (!site.modelId || site.modelId !== firstModelId) { site.modelId = firstModelId; if (typeof saveCustomSourceSite === 'function') { saveCustomSourceSite(site); } modelSelect.value = firstModelId; // 同步预览 const previewText = document.getElementById(`currentModelPreview_${siteId}`); if (previewText) { previewText.textContent = modelSelect.options[0].text || firstModelId; } } } } if (modelSelect) { // 下拉框change事件 - 实现即时预览并保存 modelSelect.addEventListener('change', () => { const selectedOption = modelSelect.options[modelSelect.selectedIndex]; const previewText = document.getElementById(`currentModelPreview_${siteId}`); if (previewText) { previewText.textContent = selectedOption.text || selectedOption.value; previewText.classList.add('font-semibold', 'text-blue-600'); setTimeout(() => { previewText.classList.remove('font-semibold', 'text-blue-600'); }, 1500); } // 新增:切换时立即保存 site.modelId = modelSelect.value; // 新增:同步写入 lastSelectedCustomModel localStorage.setItem('lastSelectedCustomModel', modelSelect.value); if (typeof saveCustomSourceSite === 'function') { saveCustomSourceSite(site); } }); } // 2. 如果有手动输入模型ID,绑定保存事件 const saveManualModelBtn = document.getElementById(`saveManualModelBtn_${siteId}`); const manualModelInput = document.getElementById(`manualModelId_${siteId}`); if (saveManualModelBtn && manualModelInput) { saveManualModelBtn.addEventListener('click', () => { if (manualModelInput && manualModelInput.value.trim()) { saveSelectedModelForSite(siteId, manualModelInput.value.trim()); // 更新当前模型显示 const previewText = document.getElementById(`currentModelPreview_${siteId}`); if (previewText) { previewText.textContent = manualModelInput.value.trim(); previewText.classList.add('font-semibold', 'text-blue-600'); setTimeout(() => { previewText.classList.remove('font-semibold', 'text-blue-600'); }, 1500); } } else { showNotification('请输入有效的模型ID', 'warning'); } }); // 添加Enter键保存功能 manualModelInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && manualModelInput.value.trim()) { saveManualModelBtn.click(); } }); } }, 100); } else { infoContainer.classList.add('hidden'); manageKeyBtn.classList.add('hidden'); } } catch (e) { console.error("Error updating custom source site info:", e); infoContainer.classList.add('hidden'); manageKeyBtn.classList.add('hidden'); } } /** * 保存选定的模型ID到源站点配置 * @param {string} siteId - 源站点ID * @param {string} modelId - 要保存的模型ID */ function saveSelectedModelForSite(siteId, modelId) { if (!siteId || !modelId) return; try { const sites = typeof loadAllCustomSourceSites === 'function' ? loadAllCustomSourceSites() : {}; const site = sites[siteId]; if (site) { // 更新模型ID site.modelId = modelId; // 保存更新后的配置 if (typeof saveCustomSourceSite === 'function') { saveCustomSourceSite(site); showNotification(`已将模型 "${modelId}" 设为源站 "${site.displayName || siteId}" 的默认模型`, 'success'); // 刷新信息显示 updateCustomSourceSiteInfo(siteId); } else { showNotification('保存失败:saveCustomSourceSite 函数不可用', 'error'); } } else { showNotification(`保存失败:未找到ID为 "${siteId}" 的源站点配置`, 'error'); } } catch (e) { console.error('Error saving selected model for site:', e); showNotification('保存模型ID时发生错误', 'error'); } } // 新增:自定义事件监听,用于外部调用源站点选择 window.addEventListener('selectCustomSourceSiteForKeyManager', function(e) { if (e.detail && typeof e.detail === 'string') { if (typeof selectModelForManager === 'function') { selectModelForManager('custom'); if (typeof selectSourceSite === 'function') { selectSourceSite(e.detail); } } } }); // 新增:选择源站点完毕后首次加载信息的钩子 // 新增:把选择源站和显示信息函数暴露给全局 window.updateCustomSourceSiteInfo = updateCustomSourceSiteInfo; // 新增:使管理函数可全局访问 window.selectModelForManager = selectModelForManager; window.selectSourceSite = selectSourceSite; // 新增:检测可用模型按钮事件 if (detectModelsBtn) { detectModelsBtn.addEventListener('click', function() { const selectedSiteId = customSourceSiteSelect.value; if (!selectedSiteId) { showNotification('请先选择一个源站点', 'warning'); return; } // 先检查该源站点是否已有API Key const keysForSite = typeof loadModelKeys === 'function' ? loadModelKeys(`custom_source_${selectedSiteId}`) : []; const validKeys = keysForSite.filter(key => key.status === 'valid' || key.status === 'untested'); if (validKeys.length === 0) { // 没有可用的Key,提示用户先添加Key if (confirm(`源站点没有可用的API Key。是否立即添加Key?`)) { // 打开模型管理器并直接跳到Key管理界面 document.getElementById('modelKeyManagerBtn').click(); setTimeout(() => { if (typeof selectModelForManager === 'function') { selectModelForManager('custom'); if (typeof selectSourceSite === 'function') { selectSourceSite(selectedSiteId); } } }, 100); } return; } // 有可用的Key,则直接开始检测模型 const sites = typeof loadAllCustomSourceSites === 'function' ? loadAllCustomSourceSites() : {}; const site = sites[selectedSiteId]; if (!site || !site.apiBaseUrl) { showNotification('源站点配置不完整,缺少API Base URL', 'error'); return; } // 这里使用已有的Key进行检测,而不是要求用户重新输入 showNotification('开始使用现有API Key检测可用模型,请稍候...', 'info'); // 修改为直接使用现有Key检测 if (typeof window.modelDetector === 'object' && typeof window.modelDetector.detectModelsForSite === 'function') { detectModelsWithExistingKeys(selectedSiteId, site, validKeys); } else { showNotification('模型检测器不可用,请刷新页面重试', 'error'); } }); } /** * 使用现有API Key检测源站点的可用模型 * @param {string} siteId - 源站点ID * @param {object} site - 源站点配置对象 * @param {array} validKeys - 可用的API Key列表 */ async function detectModelsWithExistingKeys(siteId, site, validKeys) { let detectBtn = document.getElementById('detectModelsBtn'); let originalBtnText = detectBtn.innerHTML; try { // 修改按钮状态 detectBtn.disabled = true; detectBtn.innerHTML = '检测中...'; // 尝试每个Key,直到成功检测到模型 let modelsDetected = []; let successfulKey = null; const requestFormat = site.requestFormat || 'openai'; const endpointMode = site.endpointMode || 'auto'; for (const key of validKeys) { try { showNotification(`正在尝试使用Key (${key.value.substring(0, 4)}...) 检测模型`, 'info'); modelsDetected = await window.modelDetector.detectModelsForSite( site.apiBaseUrl, key.value, requestFormat, endpointMode ); if (modelsDetected && modelsDetected.length > 0) { successfulKey = key; break; // 成功检测到模型,跳出循环 } } catch (keyError) { console.warn(`Key (${key.value.substring(0, 4)}...) 检测模型失败:`, keyError); // 继续尝试下一个Key } } if (modelsDetected.length === 0) { throw new Error('所有Key都无法成功检测到模型'); } // 更新源站点的可用模型列表 site.availableModels = modelsDetected; // 如果源站点还没有设置默认模型,则设置为第一个检测到的模型 if (!site.modelId && modelsDetected.length > 0) { site.modelId = modelsDetected[0].id; } // 保存更新后的源站点配置 if (typeof saveCustomSourceSite === 'function') { saveCustomSourceSite(site); showNotification(`已检测到 ${modelsDetected.length} 个可用模型,并已保存到源站点配置`, 'success'); // 刷新源站点信息显示 updateCustomSourceSiteInfo(siteId); } else { throw new Error('保存配置失败:saveCustomSourceSite 函数不可用'); } // 更新使用成功的Key状态 if (successfulKey && typeof window.refreshKeyManagerForModel === 'function') { // 标记为有效状态 window.refreshKeyManagerForModel(`custom_source_${siteId}`, successfulKey.id, 'valid'); } } catch (error) { console.error('检测模型失败:', error); showNotification(`检测模型失败: ${error.message}`, 'error'); } finally { // 恢复按钮状态 detectBtn.disabled = false; detectBtn.innerHTML = originalBtnText; } } });