paper-burner/js/ui/ui_model_manager_core.js

406 lines
16 KiB
JavaScript
Raw Permalink 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.

/**
* 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 = 'mineru';
let currentOcrConfigured = false;
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
currentOcrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mineru');
} else {
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mineru';
}
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 = '<iconify-icon icon="carbon:export" width="16"></iconify-icon><span class="ml-1">导出全部</span>';
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 = '<iconify-icon icon="carbon:import-export" width="16"></iconify-icon><span class="ml-1">导入全部</span>';
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 = `<iconify-icon icon="carbon:warning" width="14"></iconify-icon><span>当前 OCR 引擎(${engineName})未配置完成,无法进行 PDF 的 OCR 操作。</span>`;
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 = '<iconify-icon icon="carbon:warning" width="14"></iconify-icon><span>当前无有效翻译 Key无法进行翻译操作。</span>';
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]
? '<span class="inline-block w-1.5 h-1.5 mr-2 rounded-full bg-emerald-500"></span>'
: '<span class="inline-block w-1.5 h-1.5 mr-2 rounded-full bg-slate-300"></span>';
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);