/** * UI OCR 配置渲染模块 * 提取 OCR 引擎(Mistral、MinerU、Doc2X)的配置界面渲染代码 */ (function(window) { 'use strict'; /** * 渲染 Mistral OCR 配置界面 * @param {HTMLElement} container - 配置容器元素(modelConfigColumn) */ function renderMistralOcrConfig(container) { const configDiv = document.createElement('div'); configDiv.className = 'space-y-3'; const noticeDiv = document.createElement('div'); noticeDiv.className = 'bg-purple-50 border border-purple-200 rounded-md p-3 text-sm text-gray-700'; noticeDiv.innerHTML = `

📝 Mistral OCR Keys 管理

`; configDiv.appendChild(noticeDiv); // Base URL 配置 const baseUrlDiv = document.createElement('div'); const currentBaseUrl = localStorage.getItem('ocrMistralBaseUrl') || 'https://api.mistral.ai'; baseUrlDiv.innerHTML = `

默认: https://api.mistral.ai,如需使用第三方代理可在此修改

`; configDiv.appendChild(baseUrlDiv); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存配置'; saveBtn.className = 'px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors'; saveBtn.addEventListener('click', () => { const baseUrlInput = document.getElementById('mistral-base-url-km'); if (baseUrlInput) { const newBaseUrl = baseUrlInput.value.trim() || 'https://api.mistral.ai'; localStorage.setItem('ocrMistralBaseUrl', newBaseUrl); if (typeof showNotification === 'function') { showNotification('Mistral OCR 配置已保存', 'success'); } } }); configDiv.appendChild(saveBtn); container.appendChild(configDiv); } /** * 渲染 MinerU OCR 配置界面 * @param {HTMLElement} container - 配置容器元素(modelConfigColumn) */ function renderMinerUConfig(container) { // 从 localStorage 加载配置 const workerUrl = localStorage.getItem('ocrMinerUWorkerUrl') || ''; const authKey = localStorage.getItem('ocrWorkerAuthKey') || ''; const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'frontend'; const token = localStorage.getItem('ocrMinerUToken') || ''; const configDiv = document.createElement('div'); configDiv.className = 'space-y-4'; // Worker URL const urlDiv = document.createElement('div'); urlDiv.innerHTML = `

Cloudflare Worker 代理地址

`; configDiv.appendChild(urlDiv); // Worker Auth Key (可选) const authKeyDiv = document.createElement('div'); authKeyDiv.innerHTML = `

对应 Worker 环境变量 AUTH_SECRET(如果启用了 ENABLE_AUTH

`; configDiv.appendChild(authKeyDiv); // Token 配置模式 const tokenModeDiv = document.createElement('div'); tokenModeDiv.className = 'border-t pt-4'; tokenModeDiv.innerHTML = `
`; configDiv.appendChild(tokenModeDiv); // 前端透传 Token 输入框 const frontendTokenDiv = document.createElement('div'); frontendTokenDiv.id = 'mineru-frontend-token-div'; frontendTokenDiv.style.display = tokenMode === 'frontend' ? 'block' : 'none'; frontendTokenDiv.innerHTML = `

从 https://mineru.net 获取,格式:JWT(eyJ 开头)

💡 前端透传模式:通过请求头(X-MinerU-Key)传递 Token,Worker 无需配置 MINERU_API_TOKEN
`; configDiv.appendChild(frontendTokenDiv); // Worker 配置模式提示 const workerTokenDiv = document.createElement('div'); workerTokenDiv.id = 'mineru-worker-token-div'; workerTokenDiv.style.display = tokenMode === 'worker' ? 'block' : 'none'; workerTokenDiv.innerHTML = `
💡 Worker 配置模式:MinerU Token 存储在 Worker 环境变量(MINERU_API_TOKEN)中,前端不需要提供
`; configDiv.appendChild(workerTokenDiv); // 选项 const enableOcr = localStorage.getItem('ocrMinerUEnableOcr') !== 'false'; const enableFormula = localStorage.getItem('ocrMinerUEnableFormula') !== 'false'; const enableTable = localStorage.getItem('ocrMinerUEnableTable') !== 'false'; const optionsDiv = document.createElement('div'); optionsDiv.className = 'border-t pt-4'; optionsDiv.innerHTML = `
`; configDiv.appendChild(optionsDiv); // 测试/保存/设为当前引擎按钮 const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'pt-2 grid grid-cols-1 sm:grid-cols-3 gap-2'; buttonsDiv.innerHTML = ` `; configDiv.appendChild(buttonsDiv); // 测试结果显示 const mineruResultDiv = document.createElement('div'); mineruResultDiv.id = 'mineru-test-result-km'; mineruResultDiv.className = 'text-sm mt-2'; mineruResultDiv.style.display = 'none'; configDiv.appendChild(mineruResultDiv); container.appendChild(configDiv); // Token 模式切换事件 document.querySelectorAll('input[name="mineru-token-mode"]').forEach(radio => { radio.addEventListener('change', (e) => { const mode = e.target.value; document.getElementById('mineru-frontend-token-div').style.display = mode === 'frontend' ? 'block' : 'none'; document.getElementById('mineru-worker-token-div').style.display = mode === 'worker' ? 'block' : 'none'; }); }); // Auth Key 显示/隐藏切换 const authKeyToggle = document.getElementById('mineru-auth-key-toggle'); const authKeyInput = document.getElementById('mineru-auth-key-km'); if (authKeyToggle && authKeyInput) { authKeyToggle.addEventListener('click', () => { const isPassword = authKeyInput.type === 'password'; authKeyInput.type = isPassword ? 'text' : 'password'; authKeyToggle.innerHTML = isPassword ? '隐藏' : '显示'; }); } // Token 显示/隐藏切换 const tokenToggle = document.getElementById('mineru-token-toggle'); const tokenInput = document.getElementById('mineru-token-km'); if (tokenToggle && tokenInput) { tokenToggle.addEventListener('click', () => { const isPassword = tokenInput.type === 'password'; tokenInput.type = isPassword ? 'text' : 'password'; tokenToggle.innerHTML = isPassword ? '隐藏' : '显示'; }); } // 保存配置 document.getElementById('mineru-save-km').onclick = () => { const selectedMode = document.querySelector('input[name="mineru-token-mode"]:checked').value; // 去掉末尾斜杠 const workerUrl = document.getElementById('mineru-worker-url-km').value.trim().replace(/\/+$/, ''); localStorage.setItem('ocrMinerUWorkerUrl', workerUrl); localStorage.setItem('ocrWorkerAuthKey', document.getElementById('mineru-auth-key-km').value.trim()); localStorage.setItem('ocrMinerUTokenMode', selectedMode); if (selectedMode === 'frontend') { localStorage.setItem('ocrMinerUToken', document.getElementById('mineru-token-km').value.trim()); } localStorage.setItem('ocrMinerUEnableOcr', document.getElementById('mineru-enable-ocr-km').checked.toString()); localStorage.setItem('ocrMinerUEnableFormula', document.getElementById('mineru-enable-formula-km').checked.toString()); localStorage.setItem('ocrMinerUEnableTable', document.getElementById('mineru-enable-table-km').checked.toString()); if (typeof showNotification === 'function') { showNotification('MinerU OCR 配置已保存', 'success'); } else { alert('配置已保存'); } if (typeof window.renderModelList === 'function') window.renderModelList(); }; // 测试连接 document.getElementById('mineru-test-km').onclick = async () => { const btn = document.getElementById('mineru-test-km'); const result = document.getElementById('mineru-test-result-km'); const wurl = document.getElementById('mineru-worker-url-km').value.trim(); const akey = document.getElementById('mineru-auth-key-km').value.trim(); const selectedMode = document.querySelector('input[name="mineru-token-mode"]:checked').value; const token = selectedMode === 'frontend' ? document.getElementById('mineru-token-km').value.trim() : ''; result.style.display = 'none'; btn.disabled = true; btn.textContent = '测试中...'; try { if (!wurl) throw new Error('请先填写 Worker URL'); const base = wurl.replace(/\/+$/, ''); // 第一步:测试Worker可达性 result.style.display = 'block'; result.style.color = '#3b82f6'; result.textContent = '🔄 正在测试Worker可达性...'; const healthResp = await fetch(base + '/health', { headers: akey ? { 'X-Auth-Key': akey } : {} }); if (!healthResp.ok) { throw new Error(`Worker不可达: ${healthResp.status} ${healthResp.statusText}`); } // 第二步:测试Token有效性(如果是前端模式) if (selectedMode === 'frontend') { if (!token) { throw new Error('前端模式下必须提供MinerU Token'); } result.style.color = '#3b82f6'; result.textContent = '🔄 正在验证Token有效性...'; const tokenTestResp = await fetch(base + '/mineru/result/__health__', { headers: { 'X-Auth-Key': akey || '', 'X-MinerU-Key': token } }); const tokenTestData = await tokenTestResp.json(); if (!tokenTestResp.ok || !tokenTestData.success) { throw new Error(`Token无效: ${tokenTestData.message || tokenTestData.error || '未知错误'}`); } result.style.color = '#059669'; result.textContent = '✅ Worker可达且Token有效'; } else { // Worker模式:只需要验证Worker可达性 result.style.color = '#059669'; result.textContent = '✅ Worker可达(Worker模式,Token由Worker配置)'; } } catch (e) { result.style.display = 'block'; result.style.color = '#dc2626'; result.textContent = `❌ 测试失败: ${e.message}`; } finally { btn.disabled = false; btn.textContent = '测试连接'; } }; // 设为当前 OCR 引擎 document.getElementById('mineru-set-engine-km').onclick = () => { try { localStorage.setItem('ocrEngine', 'mineru'); if (window.ocrSettingsManager && typeof window.ocrSettingsManager.loadSettings === 'function') { window.ocrSettingsManager.loadSettings(); } if (typeof showNotification === 'function') { showNotification('已将 MinerU 设为当前 OCR 引擎', 'success'); } } catch (e) { alert('设为当前引擎失败'); } }; } /** * 渲染 Doc2X OCR 配置界面 * @param {HTMLElement} container - 配置容器元素(modelConfigColumn) */ function renderDoc2XConfig(container) { // 从 localStorage 加载配置 const workerUrl = localStorage.getItem('ocrDoc2XWorkerUrl') || ''; const authKey = localStorage.getItem('ocrWorkerAuthKey') || ''; const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'frontend'; const token = localStorage.getItem('ocrDoc2XToken') || ''; const configDiv = document.createElement('div'); configDiv.className = 'space-y-4'; // Worker URL const urlDiv = document.createElement('div'); urlDiv.innerHTML = `

Cloudflare Worker 代理地址

`; configDiv.appendChild(urlDiv); // Worker Auth Key (可选) const authKeyDiv = document.createElement('div'); authKeyDiv.innerHTML = `

对应 Worker 环境变量 AUTH_SECRET(如果启用了 ENABLE_AUTH

`; configDiv.appendChild(authKeyDiv); // Token 配置模式选择 const tokenModeDiv = document.createElement('div'); tokenModeDiv.className = 'border-t pt-4'; tokenModeDiv.innerHTML = `
`; configDiv.appendChild(tokenModeDiv); // 前端透传模式 - Token 输入 const frontendTokenDiv = document.createElement('div'); frontendTokenDiv.id = 'doc2x-frontend-token-div'; frontendTokenDiv.style.display = tokenMode === 'frontend' ? 'block' : 'none'; frontendTokenDiv.innerHTML = `
💡 前端透传模式:通过请求头(X-Doc2X-Key)传递 Token,Worker 无需配置 DOC2X_API_TOKEN
`; configDiv.appendChild(frontendTokenDiv); // Worker 配置模式 - 提示 const workerTokenDiv = document.createElement('div'); workerTokenDiv.id = 'doc2x-worker-token-div'; workerTokenDiv.style.display = tokenMode === 'worker' ? 'block' : 'none'; workerTokenDiv.innerHTML = `
💡 Worker 配置模式:Doc2X Token 存储在 Worker 环境变量(DOC2X_API_TOKEN)中,前端不需要提供
`; configDiv.appendChild(workerTokenDiv); // 说明 const noticeDiv = document.createElement('div'); noticeDiv.className = 'bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-gray-700'; noticeDiv.innerHTML = `

📝 Doc2X OCR 特性:支持图片和复杂排版识别,公式使用 Dollar 格式 ($...$)

`; configDiv.appendChild(noticeDiv); // 测试/保存/设为当前引擎按钮 const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'pt-2 grid grid-cols-1 sm:grid-cols-3 gap-2'; buttonsDiv.innerHTML = ` `; configDiv.appendChild(buttonsDiv); // 测试结果显示 const doc2xResultDiv = document.createElement('div'); doc2xResultDiv.id = 'doc2x-test-result-km'; doc2xResultDiv.className = 'text-sm mt-2'; doc2xResultDiv.style.display = 'none'; configDiv.appendChild(doc2xResultDiv); container.appendChild(configDiv); // 模式切换事件处理 document.querySelectorAll('input[name="doc2x-token-mode"]').forEach(radio => { radio.addEventListener('change', (e) => { const mode = e.target.value; document.getElementById('doc2x-frontend-token-div').style.display = mode === 'frontend' ? 'block' : 'none'; document.getElementById('doc2x-worker-token-div').style.display = mode === 'worker' ? 'block' : 'none'; }); }); // Auth Key 显示/隐藏切换 const authKeyToggle = document.getElementById('doc2x-auth-key-toggle'); const authKeyInput = document.getElementById('doc2x-auth-key-km'); if (authKeyToggle && authKeyInput) { authKeyToggle.addEventListener('click', () => { const isPassword = authKeyInput.type === 'password'; authKeyInput.type = isPassword ? 'text' : 'password'; authKeyToggle.innerHTML = isPassword ? '隐藏' : '显示'; }); } // Token 显示/隐藏切换 const tokenToggle = document.getElementById('doc2x-token-toggle'); const tokenInput = document.getElementById('doc2x-token-km'); if (tokenToggle && tokenInput) { tokenToggle.addEventListener('click', () => { const isPassword = tokenInput.type === 'password'; tokenInput.type = isPassword ? 'text' : 'password'; tokenToggle.innerHTML = isPassword ? '隐藏' : '显示'; }); } // 保存配置 document.getElementById('doc2x-save-km').onclick = () => { const selectedMode = document.querySelector('input[name="doc2x-token-mode"]:checked').value; // 去掉末尾斜杠 const workerUrl = document.getElementById('doc2x-worker-url-km').value.trim().replace(/\/+$/, ''); localStorage.setItem('ocrDoc2XWorkerUrl', workerUrl); localStorage.setItem('ocrWorkerAuthKey', document.getElementById('doc2x-auth-key-km').value.trim()); localStorage.setItem('ocrDoc2XTokenMode', selectedMode); if (selectedMode === 'frontend') { localStorage.setItem('ocrDoc2XToken', document.getElementById('doc2x-token-km').value.trim()); } if (typeof showNotification === 'function') { showNotification('Doc2X OCR 配置已保存', 'success'); } else { alert('配置已保存'); } if (typeof window.renderModelList === 'function') window.renderModelList(); }; // 测试连接 document.getElementById('doc2x-test-km').onclick = async () => { const btn = document.getElementById('doc2x-test-km'); const result = document.getElementById('doc2x-test-result-km'); const wurl = document.getElementById('doc2x-worker-url-km').value.trim(); const akey = document.getElementById('doc2x-auth-key-km').value.trim(); const selectedMode = document.querySelector('input[name="doc2x-token-mode"]:checked').value; const token = selectedMode === 'frontend' ? document.getElementById('doc2x-token-km').value.trim() : ''; result.style.display = 'none'; btn.disabled = true; btn.textContent = '测试中...'; try { if (!wurl) throw new Error('请先填写 Worker URL'); const base = wurl.replace(/\/+$/, ''); // 第一步:测试Worker可达性 result.style.display = 'block'; result.style.color = '#3b82f6'; result.textContent = '🔄 正在测试Worker可达性...'; const healthResp = await fetch(base + '/health', { headers: akey ? { 'X-Auth-Key': akey } : {} }); if (!healthResp.ok) { throw new Error(`Worker不可达: ${healthResp.status} ${healthResp.statusText}`); } // 第二步:测试Token有效性(如果是前端模式) if (selectedMode === 'frontend') { if (!token) { throw new Error('前端模式下必须提供Doc2X Token'); } result.style.color = '#3b82f6'; result.textContent = '🔄 正在验证Token有效性...'; const tokenTestResp = await fetch(base + '/doc2x/status/__health__', { headers: { 'X-Auth-Key': akey || '', 'X-Doc2X-Key': token } }); const tokenTestData = await tokenTestResp.json(); if (!tokenTestResp.ok || !tokenTestData.success) { throw new Error(`Token无效: ${tokenTestData.message || tokenTestData.error || '未知错误'}`); } result.style.color = '#059669'; result.textContent = '✅ Worker可达且Token有效'; } else { // Worker模式:只需要验证Worker可达性 result.style.color = '#059669'; result.textContent = '✅ Worker可达(Worker模式,Token由Worker配置)'; } } catch (e) { result.style.display = 'block'; result.style.color = '#dc2626'; result.textContent = `❌ 测试失败: ${e.message}`; } finally { btn.disabled = false; btn.textContent = '测试连接'; } }; // 设为当前 OCR 引擎 document.getElementById('doc2x-set-engine-km').onclick = () => { try { localStorage.setItem('ocrEngine', 'doc2x'); if (window.ocrSettingsManager && typeof window.ocrSettingsManager.loadSettings === 'function') { window.ocrSettingsManager.loadSettings(); } if (typeof showNotification === 'function') { showNotification('已将 Doc2X 设为当前 OCR 引擎', 'success'); } } catch (e) { alert('设为当前引擎失败'); } }; } // 导出到全局作用域 window.UIModelOcrConfigRenderer = { renderMistralOcrConfig, renderMinerUConfig, renderDoc2XConfig }; })(window);