// 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)可获得
CrossRef 和
OpenAlex
更高的速率限制(点击链接以了解更多)
`;
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 = '可用服务:';
for (const [service, info] of Object.entries(data.services)) {
const status = info.enabled ? '✓' : '✗';
const apiKeyStatus = info.hasApiKey !== undefined ? (info.hasApiKey ? ' (有密钥)' : ' (无密钥)') : '';
servicesHtml += `- ${status} ${service}${apiKeyStatus}
`;
}
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 = `
点击“检测”从 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 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 += `
`;
}
// 添加键检查信息和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;
}
}
});