/**
* 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 管理
- 请在下方"Key 管理器"中添加/测试 Mistral API Keys(每个 Key 独立管理)。
- 系统会在 OCR 时按顺序轮询可用 Key,实现负载均衡与容错。
`;
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 defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
const workerUrl = localStorage.getItem('ocrMinerUWorkerUrl') || defaultUrl;
const authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'backend';
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 defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
const workerUrl = localStorage.getItem('ocrDoc2XWorkerUrl') || defaultUrl;
const authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'backend';
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);