paper-burner/js/ui/ui.js

2237 lines
123 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = '<iconify-icon icon="carbon:add-filled" class="mr-2"></iconify-icon>添加新源站';
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 = '<p class="text-sm text-gray-500">请添加并选择一个源站以管理其 API Keys。</p>';
} else if (!currentSelectedSourceSiteId) {
keyManagerColumn.innerHTML = '<p class="text-sm text-gray-500">请从上方列表选择一个源站以管理其 API Keys。</p>';
}
} 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 = `
<h4 style="margin: 0; font-size: 16px; font-weight: 600; color: #111827;">选择嵌入模型</h4>
<button class="model-selector-close" style="border: none; background: none; font-size: 24px; color: #6b7280; cursor: pointer; line-height: 1;">&times;</button>
`;
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 = `
<div style="font-weight: 500; color: #111827;">${model.id}</div>
${model.owned_by ? `<div style="font-size: 12px; color: #6b7280; margin-top: 2px;">by ${model.owned_by}</div>` : ''}
`;
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 = `
<h4 style="margin: 0; font-size: 16px; font-weight: 600; color: #111827;">选择重排模型</h4>
<button class="model-selector-close" style="border: none; background: none; font-size: 24px; color: #6b7280; cursor: pointer; line-height: 1;">&times;</button>
`;
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 = `
<div style="font-weight: 500; color: #111827;">${id}</div>
${model.owned_by ? `<div style=\"font-size: 12px; color: #6b7280; margin-top: 2px;\">by ${model.owned_by}</div>` : ''}
`;
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 = `
<nav class="flex -mb-px space-x-4">
<button id="academic-tab-sources" class="academic-tab px-4 py-2 text-sm font-medium border-b-2 border-blue-600 text-blue-600">
搜索源管理
</button>
<button id="academic-tab-proxy" class="academic-tab px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
代理配置
</button>
</nav>
`;
container.appendChild(tabsDiv);
// Tab 1: 搜索源管理
const sourcesTab = document.createElement('div');
sourcesTab.id = 'academic-sources-tab-content';
sourcesTab.className = 'pt-4';
sourcesTab.innerHTML = `
<div class="text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded p-2 mb-3 flex items-center gap-1">
<iconify-icon icon="carbon:information" width="14"></iconify-icon>
<span>拖动调整查询顺序,取消勾选可禁用某个源</span>
</div>
<div id="academic-sources-list" class="space-y-2"></div>
`;
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 = `
<iconify-icon icon="carbon:draggable" width="16" class="text-gray-400"></iconify-icon>
<input type="checkbox" ${source.enabled ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 source-enable-checkbox" data-key="${source.key}">
<span class="flex-grow text-sm text-gray-700 font-medium">${source.name}</span>
<span class="text-xs text-gray-400">${source.key}</span>
`;
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 = `
<label class="flex items-center cursor-pointer">
<input type="checkbox" id="academic-search-enabled" ${config.enabled ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-2 text-sm font-medium text-gray-700">启用学术搜索代理</span>
</label>
<p class="mt-1 text-xs text-gray-500 ml-6">开启后PubMed、Semantic Scholar 和 arXiv 查询将通过代理服务器</p>
`;
container.appendChild(enableDiv);
// Worker URL
const urlDiv = document.createElement('div');
urlDiv.innerHTML = `
<label class="block text-sm font-medium text-gray-700 mb-1">Worker URL</label>
<input type="text" id="academic-search-base-url" value="${config.baseUrl}" placeholder="https://your-worker.workers.dev" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">Cloudflare Worker 学术搜索代理地址</p>
`;
container.appendChild(urlDiv);
// 部署模式说明
const modeInfoDiv = document.createElement('div');
modeInfoDiv.className = 'border-t pt-4';
modeInfoDiv.innerHTML = `
<div class="text-xs bg-blue-50 border border-blue-200 rounded p-3 space-y-2">
<div class="font-semibold text-blue-800 flex items-center gap-1">
<iconify-icon icon="carbon:information" width="14"></iconify-icon>
<span>支持两种部署模式</span>
</div>
<div class="text-blue-700">
<strong>方案一:透传模式(推荐)</strong><br>
• 在下方填写 API Key通过 <code class="bg-blue-100 px-1 rounded">X-Api-Key</code> 请求头透传给 Worker<br>
• Worker 可以选择配置密钥作为备用,如果前端没有提供则使用 Worker 配置的密钥<br>
• 适合个人使用或分享给他人
</div>
<div class="text-blue-700">
<strong>方案二:共享密钥模式</strong><br>
• API Key 存储在 Worker 环境变量中(必需)<br>
• 需要在下方填写 Worker Auth Key对应 Worker 的 <code class="bg-blue-100 px-1 rounded">AUTH_SECRET</code><br>
• 适合团队共享,但需要保护好 Auth Key
</div>
</div>
`;
container.appendChild(modeInfoDiv);
// Semantic Scholar API Key透传模式
const s2KeyDiv = document.createElement('div');
s2KeyDiv.className = 'border-t pt-4';
s2KeyDiv.innerHTML = `
<label class="block text-sm font-medium text-gray-700 mb-1">Semantic Scholar API Key可选透传模式</label>
<div class="flex items-center gap-2">
<input type="password" id="academic-search-s2-key" value="${config.semanticScholarApiKey || ''}" placeholder="留空则使用免费额度" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
<button type="button" id="academic-search-s2-toggle" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors flex items-center gap-1">
<iconify-icon icon="carbon:view" width="16"></iconify-icon>
<span>显示</span>
</button>
</div>
<p class="mt-1 text-xs text-gray-500">从 <a href="https://www.semanticscholar.org/product/api" target="_blank" class="text-blue-600 hover:underline">Semantic Scholar</a> 获取,提高请求限额</p>
`;
container.appendChild(s2KeyDiv);
// PubMed API Key透传模式
const pubmedKeyDiv = document.createElement('div');
pubmedKeyDiv.innerHTML = `
<label class="block text-sm font-medium text-gray-700 mb-1">PubMed API Key可选透传模式</label>
<div class="flex items-center gap-2">
<input type="password" id="academic-search-pubmed-key" value="${config.pubmedApiKey || ''}" placeholder="留空则使用免费额度" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
<button type="button" id="academic-search-pubmed-toggle" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors flex items-center gap-1">
<iconify-icon icon="carbon:view" width="16"></iconify-icon>
<span>显示</span>
</button>
</div>
<p class="mt-1 text-xs text-gray-500">从 <a href="https://www.ncbi.nlm.nih.gov/account/" target="_blank" class="text-blue-600 hover:underline">NCBI</a> 获取,提高请求限额</p>
`;
container.appendChild(pubmedKeyDiv);
// Worker Auth Key共享模式
const authKeyDiv = document.createElement('div');
authKeyDiv.className = 'border-t pt-4';
authKeyDiv.innerHTML = `
<label class="block text-sm font-medium text-gray-700 mb-1">Worker Auth Key共享模式</label>
<div class="flex items-center gap-2">
<input type="password" id="academic-search-auth-key" value="${config.authKey || ''}" placeholder="如果 Worker 启用了 ENABLE_AUTH填写这里" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
<button type="button" id="academic-search-auth-toggle" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors flex items-center gap-1">
<iconify-icon icon="carbon:view" width="16"></iconify-icon>
<span>显示</span>
</button>
</div>
<p class="mt-1 text-xs text-gray-500">对应 Worker 环境变量 <code class="bg-gray-100 px-1 rounded">AUTH_SECRET</code>(仅在共享模式需要)</p>
`;
container.appendChild(authKeyDiv);
// 联系邮箱(可选,用于 CrossRef 和 OpenAlex 的 polite pool
const emailDiv = document.createElement('div');
emailDiv.innerHTML = `
<label class="block text-sm font-medium text-gray-700 mb-1">联系邮箱(可选)</label>
<input type="email" id="academic-search-contact-email" value="${config.contactEmail || ''}" placeholder="your-email@example.com" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">
提供邮箱(<span class="font-semibold">Polite Pool</span>)可获得
<a href="https://www.crossref.org/documentation/retrieve-metadata/rest-api/tips-for-using-the-crossref-rest-api/#00831" target="_blank" class="font-semibold text-blue-600 hover:underline">CrossRef</a> 和
<a href="https://docs.openalex.org/how-to-use-the-api/rate-limits-and-authentication#the-polite-pool" target="_blank" class="font-semibold text-blue-600 hover:underline">OpenAlex</a>
更高的速率限制(点击链接以了解更多)
</p>
`;
container.appendChild(emailDiv);
// 测试/保存按钮
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'pt-2 grid grid-cols-1 sm:grid-cols-2 gap-2';
buttonsDiv.innerHTML = `
<button id="academic-search-test" class="px-4 py-2 border border-gray-300 bg-white text-gray-700 rounded-md hover:bg-gray-50">测试连接</button>
<button id="academic-search-save" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">保存配置</button>
`;
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 = '<div class="mt-2"><strong>可用服务:</strong><ul class="list-disc list-inside text-xs mt-1">';
for (const [service, info] of Object.entries(data.services)) {
const status = info.enabled ? '✓' : '✗';
const apiKeyStatus = info.hasApiKey !== undefined ? (info.hasApiKey ? ' (有密钥)' : ' (无密钥)') : '';
servicesHtml += `<li>${status} ${service}${apiKeyStatus}</li>`;
}
servicesHtml += '</ul></div>';
}
let rateLimitHtml = '';
if (data.rateLimit) {
if (data.rateLimit.enabled) {
rateLimitHtml = `<div class="mt-2 text-xs">
<strong>速率限制:</strong> TPS: ${data.rateLimit.tps}, TPM: ${data.rateLimit.tpm}, 每IP TPS: ${data.rateLimit.perIpTps}, 每IP TPM: ${data.rateLimit.perIpTpm}`;
// 显示服务级别速率限制
if (data.rateLimit.services) {
rateLimitHtml += '<div class="ml-4 mt-1 text-xs opacity-80">';
if (data.rateLimit.services.pubmed) {
rateLimitHtml += `<div>• PubMed: TPS ${data.rateLimit.services.pubmed.tps}, TPM ${data.rateLimit.services.pubmed.tpm}</div>`;
}
if (data.rateLimit.services.semanticscholar) {
rateLimitHtml += `<div>• Semantic Scholar: TPS ${data.rateLimit.services.semanticscholar.tps}, TPM ${data.rateLimit.services.semanticscholar.tpm}</div>`;
}
rateLimitHtml += '</div>';
}
rateLimitHtml += '</div>';
} else {
rateLimitHtml = '<div class="mt-2 text-xs"><strong>速率限制:</strong> 未启用</div>';
}
}
let authHtml = '';
if (data.authentication) {
authHtml = `<div class="mt-1 text-xs"><strong>认证:</strong> ${data.authentication.required ? '必需' : '不需要'}</div>`;
}
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 = '<p class="text-sm text-gray-500">还没有自定义源站。请点击上方按钮添加一个。</p>';
document.getElementById('sourceSiteConfigFormContainer').classList.add('hidden');
if (selectedModelForManager === 'custom') {
keyManagerColumn.innerHTML = '<p class="text-sm text-gray-500">请添加并选择一个源站以管理其 API Keys。</p>';
}
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 = '<iconify-icon icon="carbon:edit" width="16"></iconify-icon>';
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 = '<p class="text-sm text-gray-500">编辑源站配置中。保存或取消以管理 Keys。</p>';
});
const deleteButton = document.createElement('button');
deleteButton.innerHTML = '<iconify-icon icon="carbon:trash-can" width="16"></iconify-icon>';
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 = '<p class="text-sm text-gray-500">请选择一个源站以管理其 API Keys。</p>';
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 = '<p class="text-sm text-gray-500">请选择一个源站以管理其 API Keys。</p>';
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 = '<iconify-icon icon="carbon:search-locate" class="mr-1"></iconify-icon>检测';
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 = '<iconify-icon icon="carbon:search" class="mr-1"></iconify-icon>搜索模型';
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 = '<iconify-icon icon="carbon:circle-dash" class="animate-spin mr-1"></iconify-icon>检测中...';
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 = '<iconify-icon icon="carbon:search-locate" class="mr-1"></iconify-icon>检测';
}
});
// --- 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 = '<iconify-icon icon="carbon:save" class="mr-1"></iconify-icon>保存';
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 = '<p class="text-sm text-gray-500">请选择或添加一个源站以管理其 API Keys。</p>';
}
});
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 = `
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="text-sm text-blue-800 font-medium">Gemini 可用模型检测</div>
<button id="detectGeminiModelsBtn" class="px-2 py-1 text-xs border rounded hover:bg-white w-full sm:w-auto">检测</button>
</div>
<div id="geminiModelsArea" class="mt-2 text-sm text-gray-700 space-y-2">
<span class="text-gray-500">点击“检测”从 Google API 拉取模型列表</span>
</div>
`;
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 = '<span class="text-red-600">无可用 Gemini API Key</span>'; 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 = '<span class="text-gray-500">未返回模型列表</span>'; 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 = '<iconify-icon icon="carbon:search" class="mr-1" width="14"></iconify-icon>搜索模型';
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 = `<span class="text-red-600">检测失败: ${e.message}</span>`;
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 = `
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="text-sm text-blue-800 font-medium">DeepSeek 可用模型检测</div>
<button id="detectDeepseekModelsBtn" class="px-2 py-1 text-xs border rounded hover:bg-white w-full sm:w-auto">检测</button>
</div>
<div id="deepseekModelsArea" class="mt-2 text-sm text-gray-700 space-y-2">
<span class="text-gray-500">点击“检测”从 DeepSeek API 拉取模型列表</span>
</div>
`;
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 = '<span class="text-red-600">无可用 DeepSeek API Key</span>';
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 = '<span class="text-gray-500">未返回模型列表</span>';
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 = '<iconify-icon icon="carbon:search" class="mr-1" width="14"></iconify-icon>搜索模型';
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 = `<span class="text-red-600">检测失败: ${error.message}</span>`;
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 = `
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="text-sm text-blue-800 font-medium">通义 可用模型检测</div>
<button id="detectTongyiModelsBtn" class="px-2 py-1 text-xs border rounded hover:bg-white w-full sm:w-auto">检测</button>
</div>
<div id="tongyiModelsArea" class="mt-2 text-sm text-gray-700 space-y-2">
<span class="text-gray-500">点击“检测”从 DashScope API 拉取模型列表</span>
</div>
`;
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 = '<span class="text-red-600">无可用 通义 API Key</span>';
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 = '<span class="text-gray-500">未返回模型列表</span>';
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 = '<iconify-icon icon="carbon:search" class="mr-1" width="14"></iconify-icon>搜索模型';
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 = `<span class="text-red-600">检测失败: ${e.message}</span>`;
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 = `
<div class="flex items-center justify之间">
<div class="text-sm text-blue-800 font-medium">火山 可用模型检测</div>
<button id="detectVolcanoModelsBtn" class="px-2 py-1 text-xs border rounded hover:bg白">检测</button>
</div>
<div id="volcanoModelsArea" class="mt-2 text-sm text-gray-700">
<span class="text-gray-500">点击“检测”从 Ark API 拉取模型列表</span>
</div>
`;
// 修正误植
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 = `
<div class="flex items-center gap-2">
<input id="volcanoKMManualInput" type="text" class="w-full sm:flex-grow px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500" placeholder="例如doubao-1-5-pro-32k-250115">
<button id="volcanoKMSaveBtn" class="px-4 py-1.5 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded whitespace-nowrap">设为默认</button>
</div>
<div class="mt-1 text-xs text-gray-600">不提供在线检测请手动输入模型ID。</div>
`;
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 = '<span class="text-gray-600">请手动输入模型ID或在设置中选择。示例<code>doubao-1-5-pro-32k-250115</code> / <code>deepseek-v3-250324</code></span>';
}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
panel.innerHTML = `
<div class="text-sm text-blue-800 font-medium mb-2">DeepLX 接口模板</div>
<div class="flex items-center gap-2">
<input id="deeplxEndpointTemplateInput-manager" type="text" class="flex-1 px-3 py-1.5 border border-blue-200 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors" placeholder="${placeholderHtml}">
<button id="deeplxEndpointResetBtn-manager" type="button" class="px-2 py-1 text-xs border border-blue-300 rounded hover:bg-white">恢复默认</button>
</div>
<p class="mt-2 text-xs text-blue-900 leading-5">模板中的 <code>&lt;api-key&gt;</code> 或 {API_KEY} 会自动替换为当前使用的 Key可用于自建代理地址。</p>
`;
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 = `
<div class="p-3">
<h3 class="font-bold text-gray-800 text-xl mt-1 mb-2">${site.displayName || '未命名源站点'}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div><span class="font-medium">API Base URL:</span> <span class="text-gray-600">${site.apiBaseUrl || '未设置'}</span></div>
<div><span class="font-medium">端点补全:</span> <span class="text-gray-600">${endpointModeLabel}</span></div>
<div><span class="font-medium">当前模型:</span> <span id="currentModelPreview_${siteId}" class="text-gray-600">${site.modelId || '未设置'}</span></div>
<div><span class="font-medium">请求格式:</span> <span class="text-gray-600">${site.requestFormat || 'openai'}</span></div>
<div><span class="font-medium">温度:</span> <span class="text-gray-600">${site.temperature || '0.5'}</span></div>
</div>`;
// 如果有可用模型列表,则展示为可选择的下拉框
if (site.availableModels && site.availableModels.length > 0) {
infoHtml += `
<div class="mt-2 border-t border-dashed pt-2">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="font-medium mb-1">选择模型:</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:space-x-2 gap-2">
<span class="text-xs text-green-600 flex items-center">
<iconify-icon icon="carbon:checkmark-filled" class="mr-1" width="14"></iconify-icon>
检测到 ${site.availableModels.length} 个可用模型
</span>
<button id="reDetectModelsBtn_${siteId}" class="ml-1 px-1.5 py-0.5 bg-gray-100 hover:bg-blue-100 text-blue-600 rounded flex items-center" title="重新检测模型">
<iconify-icon icon="carbon:renew" class="animate-spin-slow" width="16"></iconify-icon>
</button>
</div>
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:space-x-2 space-y-2 sm:space-y-0 mt-2">
<select id="sourceSiteModelSelect_${siteId}" class="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">`;
site.availableModels.forEach(model => {
const modelName = model.name || model.id;
const modelId = model.id;
const isSelected = modelId === site.modelId;
infoHtml += `<option value="${modelId}" ${isSelected ? 'selected' : ''}>${modelName}</option>`;
});
// 添加当前使用的模型(如果不在列表中)
if (site.modelId && !site.availableModels.some(m => m.id === site.modelId)) {
infoHtml += `<option value="${site.modelId}" selected>${site.modelId} (当前使用)</option>`;
}
infoHtml += `</select>
<button id="sourceSiteModelSearchBtn_${siteId}" class="px-3 py-1.5 border border-gray-300 rounded text-xs text-gray-600 hover:text-blue-600 hover:border-blue-400 transition-colors flex items-center whitespace-nowrap">
<iconify-icon icon="carbon:search" class="mr-1" width="14"></iconify-icon>
搜索模型
</button>
</div>
</div>`;
} else {
// 没有可用模型列表时,显示更明确的提示和手动输入选项
infoHtml += `<div class="mt-2 pt-2 border-t">
<div class="flex justify-between items-center">
<div class="font-medium mb-1">模型ID:</div>
<div class="text-xs text-gray-500">
<iconify-icon icon="carbon:information" class="mr-1" width="14"></iconify-icon>
<span>还未检测模型</span>
</div>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2 w-full">
<input type="text" id="manualModelId_${siteId}" class="w-full sm:flex-1 px-3 py-1.5 border border-gray-300 rounded-l-md text-sm" value="${site.modelId || ''}" placeholder="例如: gpt-4-turbo">
<button id="saveManualModelBtn_${siteId}" class="px-2 py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded sm:rounded-r-md text-xs flex items-center justify-center w-full sm:w-auto">
<iconify-icon icon="carbon:save" class="mr-1" width="14"></iconify-icon>
保存
</button>
</div>
<div class="mt-2 text-xs flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<span class="text-blue-600 inline-flex items-center">
<iconify-icon icon="carbon:arrow-right" class="mr-1" width="14"></iconify-icon>
点击
<button id="infoDetectModelsBtn_${siteId}" class="mx-1 px-1.5 py-0.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs flex items-center">
<iconify-icon icon="carbon:model-alt" class="mr-1" width="12"></iconify-icon>
检测可用模型
</button>
</span>
<!-- 检查是否有可用的API Key -->
<span class="${customSourceKeysCount > 0 ? 'text-green-600' : 'text-red-600'} inline-flex items-center">
${customSourceKeysCount > 0 ?
`<iconify-icon icon="carbon:checkmark" class="mr-1" width="14"></iconify-icon>${customSourceKeysCount}个可用Key` :
`<iconify-icon icon="carbon:warning" class="mr-1" width="14"></iconify-icon>请先添加API Key`}
</span>
</div>
</div>`;
}
// 添加键检查信息和API Key管理按钮
infoHtml += `
<div class="mt-6 pt-3 border-t border-dashed ">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="flex flex-col sm:flex-row sm:items-center gap-3 h-full">
<span class="font-medium">API Keys:</span>
<span class="text-sm ${customSourceKeysCount > 0 ? 'text-green-600' : 'text-red-600'} flex items-center">
${customSourceKeysCount > 0 ?
`<iconify-icon icon="carbon:checkmark-filled" class="mr-1" width="14"></iconify-icon>${customSourceKeysCount}个可用Key` :
`<iconify-icon icon="carbon:warning-filled" class="mr-1" width="14"></iconify-icon>无可用Key`}
</span>
</div>
<button id="infoManageKeyBtn_${siteId}" class="px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs flex items-center justify-center w-full sm:w-auto" style="min-height:2.4em;">
<iconify-icon icon="carbon:api" class="mr-1" width="14"></iconify-icon>
管理API Key
</button>
</div>
</div>`;
infoHtml += `</div>`;
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 = '<iconify-icon icon="carbon:circle-dash" class="animate-spin mr-1"></iconify-icon>检测中...';
// 尝试每个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;
}
}
});