/**
* UI 模型管理器核心模块
* 负责模型列表渲染、模型选择、模型配置界面等核心逻辑
* 从 ui.js 中提取,减少主文件大小
*/
(function(window) {
'use strict';
// 支持的模型列表 - 翻译功能强制使用通义百炼(通过后端代理)
const SUPPORTED_MODELS = [
{ key: 'mistral', name: 'Mistral OCR', group: 'ocr' },
{ key: 'mineru', name: 'MinerU OCR', group: 'ocr' },
{ key: 'doc2x', name: 'Doc2X OCR', group: 'ocr' },
{ key: 'tongyi', name: '通义百炼(后端代理)', group: 'translation' },
{ key: 'custom', name: '自定义翻译模型', group: 'translation' },
{ key: 'embedding', name: '向量搜索与重排', group: 'search' },
{ key: 'academicSearch', name: '学术搜索与代理', group: 'search' }
];
// 模型配置章节
const MODEL_SECTIONS = [
{ title: '所有 OCR 方式', group: 'ocr', className: 'mt-4 mb-2' },
{ title: '翻译和分析 API', group: 'translation', className: 'mt-5 mb-2' },
{ title: '搜索和检索', group: 'search', className: 'mt-5 mb-2' }
];
/**
* 模型管理器类
* 管理模型列表、模型配置界面的渲染和交互
*/
class ModelManager {
constructor() {
this.currentManagerUI = null;
this.selectedModelForManager = null;
this.currentSelectedSourceSiteId = null;
this.modelListColumn = null;
this.modelConfigColumn = null;
this.keyManagerColumn = null;
}
/**
* 初始化模型管理器
* @param {Object} elements - DOM 元素对象
*/
init(elements) {
this.modelListColumn = elements.modelListColumn;
this.modelConfigColumn = elements.modelConfigColumn;
this.keyManagerColumn = elements.keyManagerColumn;
const { modelKeyManagerBtn, modelKeyManagerModal, closeModelKeyManager } = elements;
if (!modelKeyManagerBtn || !modelKeyManagerModal || !closeModelKeyManager) {
console.warn('[ModelManager] Required elements not found');
return;
}
// 后端模式下禁用模型与Key管理器弹窗
const isBackendMode = (typeof window !== 'undefined' && window.storageAdapter && window.storageAdapter.isFrontendMode === false);
if (isBackendMode) {
modelKeyManagerBtn.setAttribute('title', '后端模式:模型与Key管理已禁用');
modelKeyManagerBtn.classList.add('opacity-50', 'cursor-not-allowed');
modelKeyManagerBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof window.showNotification === 'function') {
window.showNotification('后端模式下无法打开模型/Key设置,请在首页仅选择要使用的模型。', 'info');
} else {
alert('后端模式下无法打开模型/Key设置,请在首页仅选择要使用的模型。');
}
});
// 直接返回,不绑定打开弹窗的逻辑
return;
}
// 打开模型管理器(前端模式)
modelKeyManagerBtn.addEventListener('click', () => {
if (typeof migrateLegacyCustomConfig === 'function') {
migrateLegacyCustomConfig();
}
this.renderModelList();
if (!this.selectedModelForManager && SUPPORTED_MODELS.length > 0) {
this.selectModel(SUPPORTED_MODELS[0].key);
} else if (this.selectedModelForManager) {
this.selectModel(this.selectedModelForManager);
}
modelKeyManagerModal.classList.remove('hidden');
});
// 关闭模型管理器
closeModelKeyManager.addEventListener('click', () => {
modelKeyManagerModal.classList.add('hidden');
this.currentSelectedSourceSiteId = null;
setTimeout(() => {
if (confirm('已关闭模型与Key管理。是否刷新验证状态以更新配置检查?')) {
if (typeof window.refreshValidationState === 'function') {
window.refreshValidationState();
}
}
}, 100);
});
}
/**
* 检查模型是否有可用的 Key
* @param {string} modelKey - 模型键名
* @returns {boolean} 是否有可用 Key
*/
checkModelHasValidKey(modelKey) {
const hasUsableKey = (keys = []) => keys.some(k => k && k.value && k.value.trim() && k.status !== 'invalid');
// Embedding
if (modelKey === 'embedding') {
return !!(window.EmbeddingClient?.config?.enabled && window.EmbeddingClient?.config?.apiKey);
}
// Academic Search
if (modelKey === 'academicSearch') {
try {
const config = JSON.parse(localStorage.getItem('academicSearchProxyConfig') || 'null');
return !!(config && config.enabled && config.baseUrl);
} catch (e) {
return false;
}
}
// 自定义源站
if (modelKey === 'custom') {
let anyCustomKey = false;
const sites = typeof loadAllCustomSourceSites === 'function' ? loadAllCustomSourceSites() : {};
if (typeof loadModelKeys === 'function') {
Object.keys(sites || {}).forEach(siteId => {
const siteKeys = loadModelKeys(`custom_source_${siteId}`) || [];
if (hasUsableKey(siteKeys)) anyCustomKey = true;
});
}
return anyCustomKey;
}
// OCR 引擎
if (modelKey === 'local') {
return true; // 本地解析不需要配置
} else if (modelKey === 'mistral') {
if (typeof loadModelKeys === 'function') {
const keys = loadModelKeys('mistral') || [];
return hasUsableKey(keys);
} else {
const legacy = (localStorage.getItem('ocrMistralKeys') || '').split('\n').map(s => s.trim()).filter(Boolean);
return legacy.length > 0;
}
} else if (modelKey === 'mineru') {
const workerUrl = (localStorage.getItem('ocrMinerUWorkerUrl') || '').trim();
const mode = localStorage.getItem('ocrMinerUTokenMode') || 'frontend';
const token = (localStorage.getItem('ocrMinerUToken') || '').trim();
return !!workerUrl && (mode === 'worker' || !!token);
} else if (modelKey === 'doc2x') {
const workerUrl = (localStorage.getItem('ocrDoc2XWorkerUrl') || '').trim();
const mode = localStorage.getItem('ocrDoc2XTokenMode') || 'frontend';
const token = (localStorage.getItem('ocrDoc2XToken') || '').trim();
return !!workerUrl && (mode === 'worker' || !!token);
}
// 其他预设翻译模型
if (typeof loadModelKeys === 'function') {
const keys = loadModelKeys(modelKey) || [];
return hasUsableKey(keys);
}
return false;
}
/**
* 渲染模型列表
*/
renderModelList() {
if (!this.modelListColumn) return;
this.modelListColumn.innerHTML = '';
// 检查所有模型的配置状态
const modelHasValidKey = {};
SUPPORTED_MODELS.forEach(model => {
modelHasValidKey[model.key] = this.checkModelHasValidKey(model.key);
});
// 检查当前 OCR 引擎配置
let currentOcrEngine = 'mistral';
let currentOcrConfigured = false;
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
currentOcrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mistral');
} else {
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mistral';
}
if (currentOcrEngine === 'none' || currentOcrEngine === 'local') {
currentOcrConfigured = true;
} else {
currentOcrConfigured = modelHasValidKey[currentOcrEngine] || false;
}
} catch (e) {
console.warn('[ModelManager] Failed to check OCR config:', e);
}
const translationHasKey = SUPPORTED_MODELS
.filter(m => m.group === 'translation')
.some(m => modelHasValidKey[m.key]);
// 导入/导出按钮区域
this._renderImportExportSection();
const divider = document.createElement('div');
divider.className = 'border-t border-dashed border-slate-200 my-3';
this.modelListColumn.appendChild(divider);
// 警告信息
if (!currentOcrConfigured && currentOcrEngine !== 'none' && currentOcrEngine !== 'local') {
this._renderOcrWarning(currentOcrEngine);
}
if (!translationHasKey) {
this._renderTranslationWarning();
}
// 渲染各个章节的模型
MODEL_SECTIONS.forEach((section, idx) => {
this._renderModelSection(section, modelHasValidKey, idx === MODEL_SECTIONS.length - 1);
});
}
/**
* 渲染导入/导出区域
* @private
*/
_renderImportExportSection() {
const headerSection = document.createElement('div');
headerSection.className = 'mb-3 space-y-1';
const importExportRow = document.createElement('div');
importExportRow.className = 'flex items-center gap-2 px-1';
const exportIconBtn = document.createElement('button');
exportIconBtn.type = 'button';
exportIconBtn.innerHTML = '导出全部';
exportIconBtn.className = 'px-2 py-1 text-xs rounded-md border border-slate-200 hover:border-blue-300 text-slate-600 transition-colors flex items-center';
exportIconBtn.addEventListener('click', () => {
if (typeof KeyManagerUI !== 'undefined' && KeyManagerUI.exportAllModelData) {
KeyManagerUI.exportAllModelData();
}
});
const importIconBtn = document.createElement('button');
importIconBtn.type = 'button';
importIconBtn.innerHTML = '导入全部';
importIconBtn.className = 'px-2 py-1 text-xs rounded-md border border-slate-200 hover:border-blue-300 text-slate-600 transition-colors flex items-center';
importIconBtn.addEventListener('click', () => {
if (typeof KeyManagerUI !== 'undefined' && KeyManagerUI.importAllModelData) {
KeyManagerUI.importAllModelData(() => {
this.renderModelList();
if (this.selectedModelForManager) {
this.renderKeyManager(this.selectedModelForManager);
}
});
}
});
importExportRow.appendChild(exportIconBtn);
importExportRow.appendChild(importIconBtn);
headerSection.appendChild(importExportRow);
const importExportHint = document.createElement('div');
importExportHint.className = 'text-[11px] text-slate-500 px-1';
importExportHint.textContent = '配置文件为 Paper Burner X 专用 JSON。';
headerSection.appendChild(importExportHint);
this.modelListColumn.appendChild(headerSection);
}
/**
* 渲染 OCR 警告
* @private
*/
_renderOcrWarning(currentOcrEngine) {
const ocrWarning = document.createElement('div');
ocrWarning.className = 'mb-3 text-xs text-rose-600 bg-rose-50 border border-rose-200 rounded px-3 py-2 flex items-start gap-2';
const engineNames = { mistral: 'Mistral OCR', mineru: 'MinerU', doc2x: 'Doc2X' };
const engineName = engineNames[currentOcrEngine] || currentOcrEngine;
ocrWarning.innerHTML = `当前 OCR 引擎(${engineName})未配置完成,无法进行 PDF 的 OCR 操作。`;
this.modelListColumn.appendChild(ocrWarning);
}
/**
* 渲染翻译警告
* @private
*/
_renderTranslationWarning() {
const translationWarning = document.createElement('div');
translationWarning.className = 'mb-3 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-2 flex items-start gap-2';
translationWarning.innerHTML = '当前无有效翻译 Key,无法进行翻译操作。';
this.modelListColumn.appendChild(translationWarning);
}
/**
* 渲染模型章节
* @private
*/
_renderModelSection(section, modelHasValidKey, isLast) {
const header = document.createElement('div');
header.className = `text-xs font-semibold text-slate-500 uppercase tracking-wide px-1 ${section.className || ''}`;
header.textContent = section.title;
this.modelListColumn.appendChild(header);
SUPPORTED_MODELS
.filter(model => model.group === section.group)
.forEach(model => {
const button = document.createElement('button');
button.dataset.modelKey = model.key;
button.className = 'w-full text-left px-3 py-2 text-sm rounded-md transition-colors ';
const indicator = modelHasValidKey[model.key]
? ''
: '';
button.innerHTML = indicator + model.name;
if (model.key === this.selectedModelForManager) {
button.classList.add('bg-blue-100', 'text-blue-700', 'font-semibold');
} else {
button.classList.add('hover:bg-gray-200', 'text-gray-700');
}
button.addEventListener('click', () => this.selectModel(model.key));
this.modelListColumn.appendChild(button);
});
if (!isLast) {
const sectionDivider = document.createElement('div');
sectionDivider.className = 'border-t border-dashed border-slate-200 my-3';
this.modelListColumn.appendChild(sectionDivider);
}
}
/**
* 选择模型
* @param {string} modelKey - 模型键名
*/
selectModel(modelKey) {
this.selectedModelForManager = modelKey;
this.currentSelectedSourceSiteId = null;
this.renderModelList();
// 渲染模型配置(由主 ui.js 中的 renderModelConfigSection 处理)
if (typeof window.renderModelConfigSection === 'function') {
window.renderModelConfigSection(modelKey);
}
// 渲染 Key 管理器
if (modelKey === 'embedding' || modelKey === 'academicSearch' || modelKey === 'mineru' || modelKey === 'doc2x') {
if (this.keyManagerColumn) {
this.keyManagerColumn.innerHTML = '';
}
} else if (modelKey !== 'custom') {
this.renderKeyManager(modelKey);
}
}
/**
* 渲染 Key 管理器
* @param {string} modelKey - 模型键名
*/
renderKeyManager(modelKey) {
// 由主 ui.js 中的 renderKeyManagerForModel 处理
if (typeof window.renderKeyManagerForModel === 'function') {
window.renderKeyManagerForModel(modelKey);
}
}
/**
* 获取支持的模型列表
* @returns {Array} 模型列表
*/
getSupportedModels() {
return [...SUPPORTED_MODELS];
}
/**
* 获取当前选中的模型
* @returns {string|null} 当前选中的模型键名
*/
getSelectedModel() {
return this.selectedModelForManager;
}
}
// 创建全局实例
const modelManager = new ModelManager();
// 导出到全局
window.ModelManager = ModelManager;
window.modelManager = modelManager;
// 向后兼容:导出常量和函数
window.supportedModelsForKeyManager = SUPPORTED_MODELS;
})(window);