{"repo": "callioper/ebook-pdf-downloader", "files": {"frontend/src/components/settings/OCRNetworkSection.tsx": {"0be0544d107358c0fa422cb66ffa7d9d9faf1af0": {"t": 1785747357.4945219, "n": 49257, "h": "57f1b74e63731383", "c": "import React, { useState, useEffect, useRef, useCallback } from 'react'\nimport { SectionProps } from './types'\nimport { LLM_OCR_RECOMMENDED, API_BASE } from '../../constants'\n\nfunction StatusDot({ status }: { status: 'green' | 'red' | 'yellow' | null }) {\n const colors: Record = {\n green: 'bg-green-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500',\n }\n return (\n \n )\n}\n\nconst OCR_ENGINES = [\n { key: 'tesseract', name: 'Tesseract OCR', desc: '内置引擎,需 chi_sim 语言包' },\n { key: 'paddleocr', name: 'PaddleOCR', desc: '百度引擎,需 Python 3.11 虚拟环境' },\n { key: 'llm_ocr', name: 'LLM OCR', desc: '大模型视觉识别,无需本地引擎' },\n { key: 'mineru', name: 'MinerU', desc: '线上 API 精准解析,需 Token' },\n { key: 'paddleocr_online', name: 'PaddleOCR-VL-1.5', desc: '线上 API,需 Token' },\n]\n\nconst OCR_INSTALL_GUIDE = `## 安装 OCR 引擎\n\n**注意**:Python 3.14 与 \\`paddlepaddle\\` 不兼容。建议使用 Python 3.11 创建虚拟环境。\n\n---\n\n### 1. 创建/重建虚拟环境(Python 3.11)\n\n\\`\\`\\`powershell\n# 如果原 venv 不支持 Python 3.11,先删除\nRemove-Item -Recurse -Force backend\\\\venv\n\n# 使用 Python 3.11 创建新 venv\nC:\\\\Python311\\\\python.exe -m venv backend\\\\venv\n\\`\\`\\`\n\n### 2. 激活虚拟环境(PowerShell)\n\n\\`\\`\\`powershell\n# 方法一:PowerShell 执行策略问题\npowershell -ExecutionPolicy Bypass -Command \"backend\\\\venv\\\\Scripts\\\\Activate.ps1\"\n\n# 方法二:直接用 python 运行(无需激活)\nC:\\\\Python311\\\\python.exe -m pip install ...\n\\`\\`\\`\n\n### 3. 安装 OCRmyPDF 核心\n\n\\`\\`\\`powershell\npython -m pip install ocrmypdf\n\\`\\`\\`\n\n### 4. 安装引擎插件\n\n\\`\\`\\`powershell\n# 安装所有 OCR 引擎(推荐,一步到位)\npython -m pip install ocrmypdf-paddleocr paddleocr\n\n# 或分开安装\npython -m pip install paddleocr\npython -m pip install ocrmypdf-paddleocr # 需要先装好 paddleocr\n\\`\\`\\`\n\n### 5. 安装 Tesseract OCR 系统依赖\n\n\\`\\`\\`powershell\nwinget install --id UB-Mannheim.TesseractOCR --accept-package-agreements --accept-source-agreements\n\\`\\`\\`\n\n### 6. 将 Tesseract 添加到 PATH\n\n\\`\\`\\`powershell\n[Environment]::SetEnvironmentVariable(\"Path\", [Environment]::GetEnvironmentVariable(\"Path\", \"User\") + \";C:\\\\Program Files\\\\Tesseract-OCR\", \"User\")\n\\`\\`\\`\n\n> ⚠️ 添加后需**重启终端或IDE**使 PATH 生效,或使用完整路径验证:\n> \\`\\`\\`powershell\n> & \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n> \\`\\`\\`\n\n### 7. 验证安装\n\n\\`\\`\\`powershell\npython -c \"import ocrmypdf; print('ocrmypdf:', ocrmypdf.__version__)\"\npython -c \"from paddle import paddle; print('paddle:', paddle.__version__)\"\n\"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n\\`\\`\\`\n\n### 常见问题\n\n| 问题 | 解决 |\n|------|------|\n| \\`ModuleNotFoundError: No module named 'paddlepaddle'\\` | \\`import paddle\\` 而非 \\`import paddlepaddle\\` |\n| \\`ocrmypdf-paddleocr\\` 依赖冲突 | 先装 \\`paddleocr\\`,再装 \\`ocrmypdf-paddleocr\\` |\n| Tesseract 找不到 | 重启终端,或使用完整路径 \\`C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\\` |`\n\nconst AI_VISION_ENDPOINTS: Record = {\n openai_compatible: 'https://api.openai.com/v1',\n openai_responses: 'https://api.openai.com/v1',\n azure: 'https://RESOURCE_NAME.openai.azure.com',\n anthropic: 'https://api.anthropic.com',\n gemini: 'https://generativelanguage.googleapis.com/v1beta',\n minimax_openai: 'https://api.minimaxi.com/v1',\n minimax_anthropic: 'https://api.minimaxi.com',\n custom: '',\n}\n\nfunction OCRNetworkSection({ form, updateForm, mountedRef }: SectionProps) {\n const [ocrChecking, setOcrChecking] = useState(false)\n const [ocrStatus, setOcrStatus] = useState<'green' | 'red' | 'yellow' | null>(null)\n const [ocrMsg, setOcrMsg] = useState('')\n const [ocrEngines, setOcrEngines] = useState>({})\n const [llmModels, setLlmModels] = useState([])\n const [llmFetchMsg, setLlmFetchMsg] = useState('')\n const [onlineEngineStatus, setOnlineEngineStatus] = useState>({ mineru: '', paddleocr: '' })\n const [onlineTesting, setOnlineTesting] = useState>({ mineru: false, paddleocr: false })\n const [llmFetching, setLlmFetching] = useState(false)\n const [llmTestMsg, setLlmTestMsg] = useState('')\n const [llmTesting, setLlmTesting] = useState(false)\n const [aiModels, setAiModels] = useState<{ id: string; name: string }[]>([])\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchModelsMsg, setFetchModelsMsg] = useState('')\n const [aiVisionTest, setAiVisionTest] = useState<'testing' | 'ok' | 'fail' | null>(null)\n const [aiVisionMsg, setAiVisionMsg] = useState('')\n const [saving, setSaving] = useState(false)\n const [saveMsg, setSaveMsg] = useState('')\n const [updateChecking, setUpdateChecking] = useState(false)\n const [updateResult, setUpdateResult] = useState('')\n\n // --- OCR logic ---\n const checkOcr = useCallback(async (engine?: string) => {\n setOcrChecking(true)\n try {\n const eng = engine || form.ocr_engine || 'tesseract'\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.message || (data.ok ? data.version || '已安装' : '未检测到'))\n } catch (e) {\n if (mountedRef.current) {\n setOcrStatus('red')\n setOcrMsg('检测失败')\n }\n } finally {\n if (mountedRef.current) setOcrChecking(false)\n }\n }, [form.ocr_engine, mountedRef])\n\n const handleDetectOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: false, msg: '检测中...' } }))\n try {\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(engine)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '检测失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleInstallOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: true, msg: '安装中...' } }))\n try {\n const res = await fetch('/api/v1/install-ocr', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ engine }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.message || (data.ok ? '安装成功' : '安装失败'),\n },\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '安装请求失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleTestMineru = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n setOnlineEngineStatus(prev => ({ ...prev, mineru: '' }))\n try {\n const res = await fetch('/api/v1/check-mineru', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).mineru_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, mineru: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }, [form, mountedRef])\n\n const handleTestPaddleocrOnline = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: '' }))\n try {\n const res = await fetch('/api/v1/check-paddleocr-online', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).paddleocr_online_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }, [form, mountedRef])\n\n // Auto-detect OCR engine statuses\n const autoOcrRef = useRef(false)\n useEffect(() => {\n if (autoOcrRef.current) return\n autoOcrRef.current = true\n const engines = ['tesseract', 'paddleocr']\n engines.forEach((eng) => {\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n [eng]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n })\n const defaultEngine = form.ocr_engine || 'tesseract'\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(defaultEngine)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.version || data.message || (data.ok ? '已安装' : '未检测到'))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Auto-detect PaddleOCR availability on mount\n // PaddleOCR already covered by B1's forEach loop\n useEffect(() => {\n fetch(`${API_BASE}/check-ocr?engine=paddleocr`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n paddleocr: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Update OCR header status when engine selection changes\n useEffect(() => {\n const eng = form.ocr_engine || 'tesseract'\n const info = ocrEngines[eng]\n if (info) {\n setOcrStatus(info.installed ? 'green' : 'red')\n setOcrMsg(info.msg || (info.installed ? '已安装' : '未检测到'))\n }\n }, [form.ocr_engine, ocrEngines])\n\n // Auto-detect online API connectivity\n const autoOnlineRef = useRef(false)\n useEffect(() => {\n if (autoOnlineRef.current) return\n autoOnlineRef.current = true\n const check = async () => {\n if ((form as any).mineru_token) {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n try {\n const r = await fetch('/api/v1/check-mineru', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).mineru_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }\n if ((form as any).paddleocr_online_token) {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n try {\n const r = await fetch('/api/v1/check-paddleocr-online', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).paddleocr_online_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }\n }\n check()\n }, [mountedRef])\n\n // --- Save / Update ---\n const handleSave = useCallback(async () => {\n setSaving(true)\n setSaveMsg('')\n try {\n const body = JSON.stringify(form)\n const res = await fetch('/api/v1/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!res.ok) {\n setSaveMsg('保存失败: HTTP ' + res.status)\n setSaving(false)\n return\n }\n setSaveMsg('保存成功')\n setTimeout(() => setSaveMsg(''), 2000)\n } catch (e: any) {\n setSaveMsg('保存失败: ' + (e.message || '未知错误'))\n }\n setSaving(false)\n }, [form, mountedRef])\n\n const handleCheckUpdate = useCallback(async () => {\n setUpdateChecking(true)\n setUpdateResult('')\n try {\n const res = await fetch('/api/v1/check-update')\n const data = await res.json()\n if (!mountedRef.current) return\n if (data.has_update) {\n setUpdateResult(`新版本 v${data.latest} 可用 (当前 v${data.current})`)\n } else {\n setUpdateResult(`已是最新版本 v${data.current}`)\n }\n } catch (e) {\n if (mountedRef.current) setUpdateResult('检查失败')\n } finally {\n if (mountedRef.current) setUpdateChecking(false)\n }\n }, [mountedRef])\n\n return (\n
\n {/* OCRmyPDF 独立状态区 */}\n
\n \n
\n \n \n {(ocrEngines['ocrmypdf'] as any)?.msg || '点击检测'}\n \n handleDetectOcrEngine('ocrmypdf')}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine('ocrmypdf')}\n disabled={(ocrEngines['ocrmypdf'] as any)?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {(ocrEngines['ocrmypdf'] as any)?.installing ? '安装中...' : '一键安装'}\n \n
\n
\n\n {/* 引擎切换区 */}\n
\n 引擎切换\n
\n {OCR_ENGINES.filter(eng => eng.key !== 'llm_ocr' && eng.key !== 'mineru' && eng.key !== 'paddleocr_online').map((eng) => {\n const info = (ocrEngines[eng.key] as any) || { installed: eng.key === 'llm_ocr', msg: '' }\n const isSelected = form.ocr_engine === eng.key\n return (\n \n
\n {eng.name}\n
\n \n
\n
\n

{eng.desc}

\n {eng.key !== 'llm_ocr' && info?.msg && (\n

\n {info.msg}\n

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && !info?.installed && (\n

需要 Python 3.11 虚拟环境,点击安装自动搭建

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && info?.installed && (info as any)?.venv && (\n

运行环境: {(info as any).venv}

\n )}\n
\n {eng.key !== 'llm_ocr' && (\n <>\n handleDetectOcrEngine(eng.key)}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine(eng.key)}\n disabled={info?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {info?.installing ? '安装中...' : '安装'}\n \n \n )}\n {eng.key === form.ocr_engine ? null : (\n updateForm({ ocr_engine: eng.key })}\n className=\"px-2 py-1 text-xs rounded text-blue-600 hover:bg-blue-50 ml-auto\"\n >\n 选用\n \n )}\n
\n
\n )\n })}\n
\n
\n\n {/* Tesseract 语言包状态 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n Tesseract 语言包\n
\n \n \n {(ocrEngines['tesseract'] as any)?.has_chi_sim\n ? 'chi_sim 已安装'\n : (ocrEngines['tesseract'] as any)?.languages\n ? 'chi_sim 未安装'\n : '请先检测 Tesseract'}\n \n {!((ocrEngines['tesseract'] as any)?.has_chi_sim) && (\n {\n try {\n await fetch('/api/v1/install-tesseract-lang?lang=chi_sim')\n handleDetectOcrEngine('tesseract')\n } catch (e) {\n }\n }}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600\"\n >\n 安装语言包\n \n )}\n
\n
\n )}\n\n
\n
\n \n updateForm({ ocr_languages: e.target.value })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n \n
\n
\n \n updateForm({ ocr_jobs: parseInt(e.target.value) || 1 })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n

Tesseract 支持多线程,PaddleOCR 仅单线程

\n
\n
\n \n updateForm({ ocr_timeout: parseInt(e.target.value) || 3600 })}\n min={60}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n
\n
\n \n updateForm({ ocr_oversample: parseInt(e.target.value) } as any)}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n 越低越快,150-400,推荐 200\n
\n
\n\n {/* OCR 安装指引 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n \n \n \n \n OCR 命令行安装指引\n \n
\n

📋 将以下提示词复制并发送给 OpenCode:

\n
{OCR_INSTALL_GUIDE}
\n

安装后返回设置页点击\"检测\"按钮确认状态。

\n
\n
\n )}\n\n {/* LLM OCR */}\n
\n
\n \n {form.ocr_engine === 'llm_ocr' ? (\n LLM OCR 已启用\n ) : (\n LLM OCR\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'llm_ocr' ? 'tesseract' : 'llm_ocr' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'llm_ocr'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'llm_ocr' ? '停用并回到 OCRmyPDF' : '启用 LLM OCR'}\n \n
\n
\n\n {form.ocr_engine === 'llm_ocr' && (\n
\n

\n 使用视觉大模型逐框识别文字层。需要运行 lmstudio / ollama 加载对应模型。\n 推荐模型已验证中文 PDF 可用。\n

\n
\n \n
\n updateForm({ llm_ocr_endpoint: e.target.value } as any)}\n placeholder=\"http://127.0.0.1:1234/v1\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmTestMsg &&

{llmTestMsg}

}\n
\n
\n \n
\n updateForm({ llm_ocr_model: e.target.value } as any)}\n placeholder=\"qwen3-vl-4b-instruct\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmModels.length > 0 && (\n \n )}\n {llmFetchMsg &&

{llmFetchMsg}

}\n
\n
\n \n updateForm({ llm_ocr_concurrency: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 1 (默认)5 (最快)\n
\n
\n
\n \n updateForm({ llm_ocr_detect_batch: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 5 页 (低内存)50 页 (更快)\n
\n

Surya 版面检测每批处理页数。内存不足时降低。

\n
\n
\n \n 推荐模型 (已验证中文 PDF)\n \n
\n {LLM_OCR_RECOMMENDED.map(m => (\n \n ))}\n
\n
\n
\n )}\n\n {/* MinerU */}\n
\n
\n \n {form.ocr_engine === 'mineru' ? (\n MinerU 线上 API 已启用\n ) : (\n MinerU 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'mineru' ? 'tesseract' : 'mineru' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'mineru'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'mineru' ? '停用 MinerU' : '启用 MinerU'}\n \n
\n
\n {form.ocr_engine === 'mineru' && (\n
\n

\n 使用 MinerU v4 精准解析 API 进行文档 OCR。PDF 将上传至 MinerU 服务器处理。\n 仅限中国大陆网络访问,单文件 ≤200MB / ≤200页。\n 实验功能,识别可能会有重复\n

\n
\n \n updateForm({ mineru_token: e.target.value } as any)}\n placeholder=\"输入 MinerU API Token(Bearer 认证)\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n {onlineTesting.mineru ? '测试中...' : '检测'}\n \n {onlineEngineStatus.mineru === 'ok' && (\n 连接正常\n )}\n {onlineEngineStatus.mineru === 'fail' && (\n 连接失败\n )}\n
\n

\n 需要先申请 Token:MinerU API 文档\n

\n
\n )}\n\n {/* PaddleOCR-VL-1.5 */}\n
\n
\n \n {form.ocr_engine === 'paddleocr_online' ? (\n PaddleOCR-VL-1.5 线上 API 已启用\n ) : (\n PaddleOCR-VL-1.5 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'paddleocr_online' ? 'tesseract' : 'paddleocr_online' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'paddleocr_online'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'paddleocr_online' ? '停用 PaddleOCR-VL-1.5' : '启用 PaddleOCR-VL-1.5'}\n \n
\n
\n {form.ocr_engine === 'paddleocr_online' && (\n
\n

\n 使用百度 PaddleOCR-VL-1.5 视觉大模型进行文档版面解析。\n 支持中英文文档,无需本地 GPU。\n 实验功能,识别可能会有重复\n

\n
\n \n
\n {[\n { key: 'spatial', label: '空间分配(快)' },\n { key: 'perbox', label: '逐框识别(慢)' },\n { key: 'hybrid', label: '混合识别' },\n ].map(({ key, label }) => (\n updateForm({ paddleocr_online_mode: key } as any)}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).paddleocr_online_mode || 'spatial') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n

\n {((form as any).paddleocr_online_mode || 'spatial') === 'spatial' && '段落文字精准,行识别不精确'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'perbox' && '行识别精准,有乱码风险'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'hybrid' && '逐框为主、空间填补,可能产生识别重复'}\n

\n
\n
\n \n
\n updateForm({ paddleocr_online_token: e.target.value } as any)}\n placeholder=\"输入 PaddleOCR access token\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n {onlineTesting.paddleocr ? '测试中...' : '检测'}\n \n
\n {onlineEngineStatus.paddleocr === 'ok' && (\n

连接正常

\n )}\n {onlineEngineStatus.paddleocr === 'fail' && (\n

连接失败

\n )}\n
\n

\n 从 PaddleOCR 控制台 获取 Access Token。\n API 文档\n

\n
\n )}\n\n
\n updateForm({ ocr_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n\n {/* ============ AI Vision / 书签 ============ */}\n
\n AI Vision 目录提取\n
\n updateForm({ ai_vision_enabled: e.target.checked } as any)} className=\"rounded\" />\n \n
\n
\n \n updateForm({ ai_vision_endpoint: e.target.value } as any)}\n placeholder={\n (form as any).ai_vision_provider === 'gemini' ? 'https://generativelanguage.googleapis.com/v1beta' :\n (form as any).ai_vision_provider === 'azure' ? 'https://{resource}.openai.azure.com' :\n (form as any).ai_vision_provider === 'anthropic' || (form as any).ai_vision_provider === 'minimax_anthropic' ? 'https://api.anthropic.com' :\n (form as any).ai_vision_provider === 'minimax_openai' ? 'https://api.minimaxi.com/v1' :\n 'http://127.0.0.1:12345/v1'\n }\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n
\n updateForm({ ai_vision_model: e.target.value } as any)}\n placeholder=\"sabafallah/deepseek-ocr\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n {\n setFetchingModels(true)\n setFetchModelsMsg('')\n try {\n const res = await fetch('/api/v1/fetch-models', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n }),\n })\n const data = await res.json()\n if (data.ok && data.models.length > 0) {\n setAiModels(data.models)\n setFetchModelsMsg(`${data.models.length} 个模型`)\n } else {\n setFetchModelsMsg(data.message || '无可用模型')\n }\n } catch (e) {\n setFetchModelsMsg(String(e))\n }\n setFetchingModels(false)\n }}\n disabled={fetchingModels || !(form as any).ai_vision_endpoint}\n className=\"px-2 py-1.5 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 whitespace-nowrap\"\n title=\"从提供商拉取可用模型列表\"\n >\n {fetchingModels ? '...' : '获取模型'}\n \n
\n {aiModels.length > 0 && (\n updateForm({ ai_vision_model: e.target.value } as any)}\n className=\"w-full mt-1 rounded border border-blue-300 px-2 py-1 text-xs font-mono\"\n size={Math.min(aiModels.length + 1, 8)}\n >\n \n {aiModels.map(m => (\n \n ))}\n \n )}\n {fetchModelsMsg && (\n

\n {fetchModelsMsg}\n

\n )}\n
\n
\n \n updateForm({ ai_vision_api_key: e.target.value } as any)}\n placeholder=\"sk-... (支持 {env:VAR_NAME})\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n \n
\n {(form as any).ai_vision_provider === 'custom' && (\n
\n updateForm({ ai_vision_messages_api: e.target.checked } as any)} className=\"rounded\" />\n \n
\n )}\n
\n
\n \n updateForm({ ai_vision_dpi: parseInt(e.target.value) || 150 } as any)}\n min={72} max={300} className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs\" />\n
\n
\n {\n setAiVisionTest('testing');\n try {\n const res = await fetch('/api/v1/check-ai-vision', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n model: (form as any).ai_vision_model,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n messages_api: (form as any).ai_vision_messages_api,\n }),\n });\n const data = await res.json();\n setAiVisionTest(data.ok ? 'ok' : 'fail');\n setAiVisionMsg(data.message || data.error || '');\n } catch (e) {\n setAiVisionTest('fail');\n setAiVisionMsg(String(e));\n }\n }}\n disabled={aiVisionTest === 'testing'}\n className=\"px-3 py-1.5 mt-2 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50\"\n >\n {aiVisionTest === 'testing' ? '测试中...' : '检测'}\n \n {aiVisionMsg && (\n

\n {aiVisionMsg}\n

\n )}\n

\n 阶段1 从 OCR 文字层提取目录(免费),阶段2 用 AI Vision 从目录页图片提取。\n 支持 OpenAI 兼容 / Gemini / MiniMax 格式。\n

\n
\n updateForm({ bookmark_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n
\n\n {/* ============ 主题 ============ */}\n
\n \n
\n {[\n { key: 'auto', label: '自动' },\n { key: 'light', label: '白昼' },\n { key: 'dark', label: '黑夜' },\n ].map(({ key, label }) => (\n updateForm({ theme: key })}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).theme || 'auto') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n
\n\n {/* ============ 保存按钮 ============ */}\n
\n \n {saving ? '保存中...' : '保存设置'}\n \n \n {updateChecking ? '检测中...' : '检查更新'}\n \n {saveMsg && (\n \n {saveMsg}\n \n )}\n {updateResult && (\n \n {updateResult}\n \n )}\n
\n \n )\n}\n\nexport default React.memo(OCRNetworkSection)\n"}, "edc250d5d4ab762c3915f5cd33fdce3ac09d9fce": {"t": 1785772779.7189453, "n": 49257, "h": "57f1b74e63731383", "c": "import React, { useState, useEffect, useRef, useCallback } from 'react'\nimport { SectionProps } from './types'\nimport { LLM_OCR_RECOMMENDED, API_BASE } from '../../constants'\n\nfunction StatusDot({ status }: { status: 'green' | 'red' | 'yellow' | null }) {\n const colors: Record = {\n green: 'bg-green-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500',\n }\n return (\n \n )\n}\n\nconst OCR_ENGINES = [\n { key: 'tesseract', name: 'Tesseract OCR', desc: '内置引擎,需 chi_sim 语言包' },\n { key: 'paddleocr', name: 'PaddleOCR', desc: '百度引擎,需 Python 3.11 虚拟环境' },\n { key: 'llm_ocr', name: 'LLM OCR', desc: '大模型视觉识别,无需本地引擎' },\n { key: 'mineru', name: 'MinerU', desc: '线上 API 精准解析,需 Token' },\n { key: 'paddleocr_online', name: 'PaddleOCR-VL-1.5', desc: '线上 API,需 Token' },\n]\n\nconst OCR_INSTALL_GUIDE = `## 安装 OCR 引擎\n\n**注意**:Python 3.14 与 \\`paddlepaddle\\` 不兼容。建议使用 Python 3.11 创建虚拟环境。\n\n---\n\n### 1. 创建/重建虚拟环境(Python 3.11)\n\n\\`\\`\\`powershell\n# 如果原 venv 不支持 Python 3.11,先删除\nRemove-Item -Recurse -Force backend\\\\venv\n\n# 使用 Python 3.11 创建新 venv\nC:\\\\Python311\\\\python.exe -m venv backend\\\\venv\n\\`\\`\\`\n\n### 2. 激活虚拟环境(PowerShell)\n\n\\`\\`\\`powershell\n# 方法一:PowerShell 执行策略问题\npowershell -ExecutionPolicy Bypass -Command \"backend\\\\venv\\\\Scripts\\\\Activate.ps1\"\n\n# 方法二:直接用 python 运行(无需激活)\nC:\\\\Python311\\\\python.exe -m pip install ...\n\\`\\`\\`\n\n### 3. 安装 OCRmyPDF 核心\n\n\\`\\`\\`powershell\npython -m pip install ocrmypdf\n\\`\\`\\`\n\n### 4. 安装引擎插件\n\n\\`\\`\\`powershell\n# 安装所有 OCR 引擎(推荐,一步到位)\npython -m pip install ocrmypdf-paddleocr paddleocr\n\n# 或分开安装\npython -m pip install paddleocr\npython -m pip install ocrmypdf-paddleocr # 需要先装好 paddleocr\n\\`\\`\\`\n\n### 5. 安装 Tesseract OCR 系统依赖\n\n\\`\\`\\`powershell\nwinget install --id UB-Mannheim.TesseractOCR --accept-package-agreements --accept-source-agreements\n\\`\\`\\`\n\n### 6. 将 Tesseract 添加到 PATH\n\n\\`\\`\\`powershell\n[Environment]::SetEnvironmentVariable(\"Path\", [Environment]::GetEnvironmentVariable(\"Path\", \"User\") + \";C:\\\\Program Files\\\\Tesseract-OCR\", \"User\")\n\\`\\`\\`\n\n> ⚠️ 添加后需**重启终端或IDE**使 PATH 生效,或使用完整路径验证:\n> \\`\\`\\`powershell\n> & \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n> \\`\\`\\`\n\n### 7. 验证安装\n\n\\`\\`\\`powershell\npython -c \"import ocrmypdf; print('ocrmypdf:', ocrmypdf.__version__)\"\npython -c \"from paddle import paddle; print('paddle:', paddle.__version__)\"\n\"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n\\`\\`\\`\n\n### 常见问题\n\n| 问题 | 解决 |\n|------|------|\n| \\`ModuleNotFoundError: No module named 'paddlepaddle'\\` | \\`import paddle\\` 而非 \\`import paddlepaddle\\` |\n| \\`ocrmypdf-paddleocr\\` 依赖冲突 | 先装 \\`paddleocr\\`,再装 \\`ocrmypdf-paddleocr\\` |\n| Tesseract 找不到 | 重启终端,或使用完整路径 \\`C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\\` |`\n\nconst AI_VISION_ENDPOINTS: Record = {\n openai_compatible: 'https://api.openai.com/v1',\n openai_responses: 'https://api.openai.com/v1',\n azure: 'https://RESOURCE_NAME.openai.azure.com',\n anthropic: 'https://api.anthropic.com',\n gemini: 'https://generativelanguage.googleapis.com/v1beta',\n minimax_openai: 'https://api.minimaxi.com/v1',\n minimax_anthropic: 'https://api.minimaxi.com',\n custom: '',\n}\n\nfunction OCRNetworkSection({ form, updateForm, mountedRef }: SectionProps) {\n const [ocrChecking, setOcrChecking] = useState(false)\n const [ocrStatus, setOcrStatus] = useState<'green' | 'red' | 'yellow' | null>(null)\n const [ocrMsg, setOcrMsg] = useState('')\n const [ocrEngines, setOcrEngines] = useState>({})\n const [llmModels, setLlmModels] = useState([])\n const [llmFetchMsg, setLlmFetchMsg] = useState('')\n const [onlineEngineStatus, setOnlineEngineStatus] = useState>({ mineru: '', paddleocr: '' })\n const [onlineTesting, setOnlineTesting] = useState>({ mineru: false, paddleocr: false })\n const [llmFetching, setLlmFetching] = useState(false)\n const [llmTestMsg, setLlmTestMsg] = useState('')\n const [llmTesting, setLlmTesting] = useState(false)\n const [aiModels, setAiModels] = useState<{ id: string; name: string }[]>([])\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchModelsMsg, setFetchModelsMsg] = useState('')\n const [aiVisionTest, setAiVisionTest] = useState<'testing' | 'ok' | 'fail' | null>(null)\n const [aiVisionMsg, setAiVisionMsg] = useState('')\n const [saving, setSaving] = useState(false)\n const [saveMsg, setSaveMsg] = useState('')\n const [updateChecking, setUpdateChecking] = useState(false)\n const [updateResult, setUpdateResult] = useState('')\n\n // --- OCR logic ---\n const checkOcr = useCallback(async (engine?: string) => {\n setOcrChecking(true)\n try {\n const eng = engine || form.ocr_engine || 'tesseract'\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.message || (data.ok ? data.version || '已安装' : '未检测到'))\n } catch (e) {\n if (mountedRef.current) {\n setOcrStatus('red')\n setOcrMsg('检测失败')\n }\n } finally {\n if (mountedRef.current) setOcrChecking(false)\n }\n }, [form.ocr_engine, mountedRef])\n\n const handleDetectOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: false, msg: '检测中...' } }))\n try {\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(engine)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '检测失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleInstallOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: true, msg: '安装中...' } }))\n try {\n const res = await fetch('/api/v1/install-ocr', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ engine }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.message || (data.ok ? '安装成功' : '安装失败'),\n },\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '安装请求失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleTestMineru = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n setOnlineEngineStatus(prev => ({ ...prev, mineru: '' }))\n try {\n const res = await fetch('/api/v1/check-mineru', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).mineru_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, mineru: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }, [form, mountedRef])\n\n const handleTestPaddleocrOnline = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: '' }))\n try {\n const res = await fetch('/api/v1/check-paddleocr-online', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).paddleocr_online_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }, [form, mountedRef])\n\n // Auto-detect OCR engine statuses\n const autoOcrRef = useRef(false)\n useEffect(() => {\n if (autoOcrRef.current) return\n autoOcrRef.current = true\n const engines = ['tesseract', 'paddleocr']\n engines.forEach((eng) => {\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n [eng]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n })\n const defaultEngine = form.ocr_engine || 'tesseract'\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(defaultEngine)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.version || data.message || (data.ok ? '已安装' : '未检测到'))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Auto-detect PaddleOCR availability on mount\n // PaddleOCR already covered by B1's forEach loop\n useEffect(() => {\n fetch(`${API_BASE}/check-ocr?engine=paddleocr`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n paddleocr: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Update OCR header status when engine selection changes\n useEffect(() => {\n const eng = form.ocr_engine || 'tesseract'\n const info = ocrEngines[eng]\n if (info) {\n setOcrStatus(info.installed ? 'green' : 'red')\n setOcrMsg(info.msg || (info.installed ? '已安装' : '未检测到'))\n }\n }, [form.ocr_engine, ocrEngines])\n\n // Auto-detect online API connectivity\n const autoOnlineRef = useRef(false)\n useEffect(() => {\n if (autoOnlineRef.current) return\n autoOnlineRef.current = true\n const check = async () => {\n if ((form as any).mineru_token) {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n try {\n const r = await fetch('/api/v1/check-mineru', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).mineru_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }\n if ((form as any).paddleocr_online_token) {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n try {\n const r = await fetch('/api/v1/check-paddleocr-online', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).paddleocr_online_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }\n }\n check()\n }, [mountedRef])\n\n // --- Save / Update ---\n const handleSave = useCallback(async () => {\n setSaving(true)\n setSaveMsg('')\n try {\n const body = JSON.stringify(form)\n const res = await fetch('/api/v1/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!res.ok) {\n setSaveMsg('保存失败: HTTP ' + res.status)\n setSaving(false)\n return\n }\n setSaveMsg('保存成功')\n setTimeout(() => setSaveMsg(''), 2000)\n } catch (e: any) {\n setSaveMsg('保存失败: ' + (e.message || '未知错误'))\n }\n setSaving(false)\n }, [form, mountedRef])\n\n const handleCheckUpdate = useCallback(async () => {\n setUpdateChecking(true)\n setUpdateResult('')\n try {\n const res = await fetch('/api/v1/check-update')\n const data = await res.json()\n if (!mountedRef.current) return\n if (data.has_update) {\n setUpdateResult(`新版本 v${data.latest} 可用 (当前 v${data.current})`)\n } else {\n setUpdateResult(`已是最新版本 v${data.current}`)\n }\n } catch (e) {\n if (mountedRef.current) setUpdateResult('检查失败')\n } finally {\n if (mountedRef.current) setUpdateChecking(false)\n }\n }, [mountedRef])\n\n return (\n
\n {/* OCRmyPDF 独立状态区 */}\n
\n \n
\n \n \n {(ocrEngines['ocrmypdf'] as any)?.msg || '点击检测'}\n \n handleDetectOcrEngine('ocrmypdf')}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine('ocrmypdf')}\n disabled={(ocrEngines['ocrmypdf'] as any)?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {(ocrEngines['ocrmypdf'] as any)?.installing ? '安装中...' : '一键安装'}\n \n
\n
\n\n {/* 引擎切换区 */}\n
\n 引擎切换\n
\n {OCR_ENGINES.filter(eng => eng.key !== 'llm_ocr' && eng.key !== 'mineru' && eng.key !== 'paddleocr_online').map((eng) => {\n const info = (ocrEngines[eng.key] as any) || { installed: eng.key === 'llm_ocr', msg: '' }\n const isSelected = form.ocr_engine === eng.key\n return (\n \n
\n {eng.name}\n
\n \n
\n
\n

{eng.desc}

\n {eng.key !== 'llm_ocr' && info?.msg && (\n

\n {info.msg}\n

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && !info?.installed && (\n

需要 Python 3.11 虚拟环境,点击安装自动搭建

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && info?.installed && (info as any)?.venv && (\n

运行环境: {(info as any).venv}

\n )}\n
\n {eng.key !== 'llm_ocr' && (\n <>\n handleDetectOcrEngine(eng.key)}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine(eng.key)}\n disabled={info?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {info?.installing ? '安装中...' : '安装'}\n \n \n )}\n {eng.key === form.ocr_engine ? null : (\n updateForm({ ocr_engine: eng.key })}\n className=\"px-2 py-1 text-xs rounded text-blue-600 hover:bg-blue-50 ml-auto\"\n >\n 选用\n \n )}\n
\n
\n )\n })}\n
\n
\n\n {/* Tesseract 语言包状态 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n Tesseract 语言包\n
\n \n \n {(ocrEngines['tesseract'] as any)?.has_chi_sim\n ? 'chi_sim 已安装'\n : (ocrEngines['tesseract'] as any)?.languages\n ? 'chi_sim 未安装'\n : '请先检测 Tesseract'}\n \n {!((ocrEngines['tesseract'] as any)?.has_chi_sim) && (\n {\n try {\n await fetch('/api/v1/install-tesseract-lang?lang=chi_sim')\n handleDetectOcrEngine('tesseract')\n } catch (e) {\n }\n }}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600\"\n >\n 安装语言包\n \n )}\n
\n
\n )}\n\n
\n
\n \n updateForm({ ocr_languages: e.target.value })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n \n
\n
\n \n updateForm({ ocr_jobs: parseInt(e.target.value) || 1 })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n

Tesseract 支持多线程,PaddleOCR 仅单线程

\n
\n
\n \n updateForm({ ocr_timeout: parseInt(e.target.value) || 3600 })}\n min={60}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n
\n
\n \n updateForm({ ocr_oversample: parseInt(e.target.value) } as any)}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n 越低越快,150-400,推荐 200\n
\n
\n\n {/* OCR 安装指引 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n \n \n \n \n OCR 命令行安装指引\n \n
\n

📋 将以下提示词复制并发送给 OpenCode:

\n
{OCR_INSTALL_GUIDE}
\n

安装后返回设置页点击\"检测\"按钮确认状态。

\n
\n
\n )}\n\n {/* LLM OCR */}\n
\n
\n \n {form.ocr_engine === 'llm_ocr' ? (\n LLM OCR 已启用\n ) : (\n LLM OCR\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'llm_ocr' ? 'tesseract' : 'llm_ocr' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'llm_ocr'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'llm_ocr' ? '停用并回到 OCRmyPDF' : '启用 LLM OCR'}\n \n
\n
\n\n {form.ocr_engine === 'llm_ocr' && (\n
\n

\n 使用视觉大模型逐框识别文字层。需要运行 lmstudio / ollama 加载对应模型。\n 推荐模型已验证中文 PDF 可用。\n

\n
\n \n
\n updateForm({ llm_ocr_endpoint: e.target.value } as any)}\n placeholder=\"http://127.0.0.1:1234/v1\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmTestMsg &&

{llmTestMsg}

}\n
\n
\n \n
\n updateForm({ llm_ocr_model: e.target.value } as any)}\n placeholder=\"qwen3-vl-4b-instruct\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmModels.length > 0 && (\n \n )}\n {llmFetchMsg &&

{llmFetchMsg}

}\n
\n
\n \n updateForm({ llm_ocr_concurrency: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 1 (默认)5 (最快)\n
\n
\n
\n \n updateForm({ llm_ocr_detect_batch: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 5 页 (低内存)50 页 (更快)\n
\n

Surya 版面检测每批处理页数。内存不足时降低。

\n
\n
\n \n 推荐模型 (已验证中文 PDF)\n \n
\n {LLM_OCR_RECOMMENDED.map(m => (\n \n ))}\n
\n
\n
\n )}\n\n {/* MinerU */}\n
\n
\n \n {form.ocr_engine === 'mineru' ? (\n MinerU 线上 API 已启用\n ) : (\n MinerU 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'mineru' ? 'tesseract' : 'mineru' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'mineru'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'mineru' ? '停用 MinerU' : '启用 MinerU'}\n \n
\n
\n {form.ocr_engine === 'mineru' && (\n
\n

\n 使用 MinerU v4 精准解析 API 进行文档 OCR。PDF 将上传至 MinerU 服务器处理。\n 仅限中国大陆网络访问,单文件 ≤200MB / ≤200页。\n 实验功能,识别可能会有重复\n

\n
\n \n updateForm({ mineru_token: e.target.value } as any)}\n placeholder=\"输入 MinerU API Token(Bearer 认证)\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n {onlineTesting.mineru ? '测试中...' : '检测'}\n \n {onlineEngineStatus.mineru === 'ok' && (\n 连接正常\n )}\n {onlineEngineStatus.mineru === 'fail' && (\n 连接失败\n )}\n
\n

\n 需要先申请 Token:MinerU API 文档\n

\n
\n )}\n\n {/* PaddleOCR-VL-1.5 */}\n
\n
\n \n {form.ocr_engine === 'paddleocr_online' ? (\n PaddleOCR-VL-1.5 线上 API 已启用\n ) : (\n PaddleOCR-VL-1.5 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'paddleocr_online' ? 'tesseract' : 'paddleocr_online' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'paddleocr_online'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'paddleocr_online' ? '停用 PaddleOCR-VL-1.5' : '启用 PaddleOCR-VL-1.5'}\n \n
\n
\n {form.ocr_engine === 'paddleocr_online' && (\n
\n

\n 使用百度 PaddleOCR-VL-1.5 视觉大模型进行文档版面解析。\n 支持中英文文档,无需本地 GPU。\n 实验功能,识别可能会有重复\n

\n
\n \n
\n {[\n { key: 'spatial', label: '空间分配(快)' },\n { key: 'perbox', label: '逐框识别(慢)' },\n { key: 'hybrid', label: '混合识别' },\n ].map(({ key, label }) => (\n updateForm({ paddleocr_online_mode: key } as any)}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).paddleocr_online_mode || 'spatial') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n

\n {((form as any).paddleocr_online_mode || 'spatial') === 'spatial' && '段落文字精准,行识别不精确'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'perbox' && '行识别精准,有乱码风险'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'hybrid' && '逐框为主、空间填补,可能产生识别重复'}\n

\n
\n
\n \n
\n updateForm({ paddleocr_online_token: e.target.value } as any)}\n placeholder=\"输入 PaddleOCR access token\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n {onlineTesting.paddleocr ? '测试中...' : '检测'}\n \n
\n {onlineEngineStatus.paddleocr === 'ok' && (\n

连接正常

\n )}\n {onlineEngineStatus.paddleocr === 'fail' && (\n

连接失败

\n )}\n
\n

\n 从 PaddleOCR 控制台 获取 Access Token。\n API 文档\n

\n
\n )}\n\n
\n updateForm({ ocr_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n\n {/* ============ AI Vision / 书签 ============ */}\n
\n AI Vision 目录提取\n
\n updateForm({ ai_vision_enabled: e.target.checked } as any)} className=\"rounded\" />\n \n
\n
\n \n updateForm({ ai_vision_endpoint: e.target.value } as any)}\n placeholder={\n (form as any).ai_vision_provider === 'gemini' ? 'https://generativelanguage.googleapis.com/v1beta' :\n (form as any).ai_vision_provider === 'azure' ? 'https://{resource}.openai.azure.com' :\n (form as any).ai_vision_provider === 'anthropic' || (form as any).ai_vision_provider === 'minimax_anthropic' ? 'https://api.anthropic.com' :\n (form as any).ai_vision_provider === 'minimax_openai' ? 'https://api.minimaxi.com/v1' :\n 'http://127.0.0.1:12345/v1'\n }\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n
\n updateForm({ ai_vision_model: e.target.value } as any)}\n placeholder=\"sabafallah/deepseek-ocr\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n {\n setFetchingModels(true)\n setFetchModelsMsg('')\n try {\n const res = await fetch('/api/v1/fetch-models', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n }),\n })\n const data = await res.json()\n if (data.ok && data.models.length > 0) {\n setAiModels(data.models)\n setFetchModelsMsg(`${data.models.length} 个模型`)\n } else {\n setFetchModelsMsg(data.message || '无可用模型')\n }\n } catch (e) {\n setFetchModelsMsg(String(e))\n }\n setFetchingModels(false)\n }}\n disabled={fetchingModels || !(form as any).ai_vision_endpoint}\n className=\"px-2 py-1.5 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 whitespace-nowrap\"\n title=\"从提供商拉取可用模型列表\"\n >\n {fetchingModels ? '...' : '获取模型'}\n \n
\n {aiModels.length > 0 && (\n updateForm({ ai_vision_model: e.target.value } as any)}\n className=\"w-full mt-1 rounded border border-blue-300 px-2 py-1 text-xs font-mono\"\n size={Math.min(aiModels.length + 1, 8)}\n >\n \n {aiModels.map(m => (\n \n ))}\n \n )}\n {fetchModelsMsg && (\n

\n {fetchModelsMsg}\n

\n )}\n
\n
\n \n updateForm({ ai_vision_api_key: e.target.value } as any)}\n placeholder=\"sk-... (支持 {env:VAR_NAME})\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n \n
\n {(form as any).ai_vision_provider === 'custom' && (\n
\n updateForm({ ai_vision_messages_api: e.target.checked } as any)} className=\"rounded\" />\n \n
\n )}\n
\n
\n \n updateForm({ ai_vision_dpi: parseInt(e.target.value) || 150 } as any)}\n min={72} max={300} className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs\" />\n
\n
\n {\n setAiVisionTest('testing');\n try {\n const res = await fetch('/api/v1/check-ai-vision', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n model: (form as any).ai_vision_model,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n messages_api: (form as any).ai_vision_messages_api,\n }),\n });\n const data = await res.json();\n setAiVisionTest(data.ok ? 'ok' : 'fail');\n setAiVisionMsg(data.message || data.error || '');\n } catch (e) {\n setAiVisionTest('fail');\n setAiVisionMsg(String(e));\n }\n }}\n disabled={aiVisionTest === 'testing'}\n className=\"px-3 py-1.5 mt-2 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50\"\n >\n {aiVisionTest === 'testing' ? '测试中...' : '检测'}\n \n {aiVisionMsg && (\n

\n {aiVisionMsg}\n

\n )}\n

\n 阶段1 从 OCR 文字层提取目录(免费),阶段2 用 AI Vision 从目录页图片提取。\n 支持 OpenAI 兼容 / Gemini / MiniMax 格式。\n

\n
\n updateForm({ bookmark_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n
\n\n {/* ============ 主题 ============ */}\n
\n \n
\n {[\n { key: 'auto', label: '自动' },\n { key: 'light', label: '白昼' },\n { key: 'dark', label: '黑夜' },\n ].map(({ key, label }) => (\n updateForm({ theme: key })}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).theme || 'auto') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n
\n\n {/* ============ 保存按钮 ============ */}\n
\n \n {saving ? '保存中...' : '保存设置'}\n \n \n {updateChecking ? '检测中...' : '检查更新'}\n \n {saveMsg && (\n \n {saveMsg}\n \n )}\n {updateResult && (\n \n {updateResult}\n \n )}\n
\n \n )\n}\n\nexport default React.memo(OCRNetworkSection)\n"}, "82e44dcd228c35b00a7ff5d95587319cc6f55f04": {"t": 1785772781.3517742, "n": 49236, "h": "d1a56a20ffac91c5", "c": "import React, { useState, useEffect, useRef, useCallback } from 'react'\nimport { SectionProps } from './types'\nimport { LLM_OCR_RECOMMENDED, API_BASE } from '../../constants'\n\nfunction StatusDot({ status }: { status: 'green' | 'red' | 'yellow' | null }) {\n const colors: Record = {\n green: 'bg-green-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500',\n }\n return (\n \n )\n}\n\nconst OCR_ENGINES = [\n { key: 'tesseract', name: 'Tesseract OCR', desc: '内置引擎,需 chi_sim 语言包' },\n { key: 'paddleocr', name: 'PaddleOCR', desc: '百度引擎,需 Python 3.11 虚拟环境' },\n { key: 'llm_ocr', name: 'LLM OCR', desc: '大模型视觉识别,无需本地引擎' },\n { key: 'mineru', name: 'MinerU', desc: '线上 API 精准解析,需 Token' },\n { key: 'paddleocr_online', name: 'PaddleOCR-VL-1.5', desc: '线上 API,需 Token' },\n]\n\nconst OCR_INSTALL_GUIDE = `## 安装 OCR 引擎\n\n**注意**:Python 3.14 与 \\`paddlepaddle\\` 不兼容。建议使用 Python 3.11 创建虚拟环境。\n\n---\n\n### 1. 创建/重建虚拟环境(Python 3.11)\n\n\\`\\`\\`powershell\n# 如果原 venv 不支持 Python 3.11,先删除\nRemove-Item -Recurse -Force backend\\\\venv\n\n# 使用 Python 3.11 创建新 venv\nC:\\\\Python311\\\\python.exe -m venv backend\\\\venv\n\\`\\`\\`\n\n### 2. 激活虚拟环境(PowerShell)\n\n\\`\\`\\`powershell\n# 方法一:PowerShell 执行策略问题\npowershell -ExecutionPolicy Bypass -Command \"backend\\\\venv\\\\Scripts\\\\Activate.ps1\"\n\n# 方法二:直接用 python 运行(无需激活)\nC:\\\\Python311\\\\python.exe -m pip install ...\n\\`\\`\\`\n\n### 3. 安装 OCRmyPDF 核心\n\n\\`\\`\\`powershell\npython -m pip install ocrmypdf\n\\`\\`\\`\n\n### 4. 安装引擎插件\n\n\\`\\`\\`powershell\n# 安装所有 OCR 引擎(推荐,一步到位)\npython -m pip install ocrmypdf-paddleocr paddleocr\n\n# 或分开安装\npython -m pip install paddleocr\npython -m pip install ocrmypdf-paddleocr # 需要先装好 paddleocr\n\\`\\`\\`\n\n### 5. 安装 Tesseract OCR 系统依赖\n\n\\`\\`\\`powershell\nwinget install --id UB-Mannheim.TesseractOCR --accept-package-agreements --accept-source-agreements\n\\`\\`\\`\n\n### 6. 将 Tesseract 添加到 PATH\n\n\\`\\`\\`powershell\n[Environment]::SetEnvironmentVariable(\"Path\", [Environment]::GetEnvironmentVariable(\"Path\", \"User\") + \";C:\\\\Program Files\\\\Tesseract-OCR\", \"User\")\n\\`\\`\\`\n\n> ⚠️ 添加后需**重启终端或IDE**使 PATH 生效,或使用完整路径验证:\n> \\`\\`\\`powershell\n> & \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n> \\`\\`\\`\n\n### 7. 验证安装\n\n\\`\\`\\`powershell\npython -c \"import ocrmypdf; print('ocrmypdf:', ocrmypdf.__version__)\"\npython -c \"from paddle import paddle; print('paddle:', paddle.__version__)\"\n\"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n\\`\\`\\`\n\n### 常见问题\n\n| 问题 | 解决 |\n|------|------|\n| \\`ModuleNotFoundError: No module named 'paddlepaddle'\\` | \\`import paddle\\` 而非 \\`import paddlepaddle\\` |\n| \\`ocrmypdf-paddleocr\\` 依赖冲突 | 先装 \\`paddleocr\\`,再装 \\`ocrmypdf-paddleocr\\` |\n| Tesseract 找不到 | 重启终端,或使用完整路径 \\`C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\\` |`\n\nconst AI_VISION_ENDPOINTS: Record = {\n openai_compatible: 'https://api.openai.com/v1',\n openai_responses: 'https://api.openai.com/v1',\n azure: 'https://RESOURCE_NAME.openai.azure.com',\n anthropic: 'https://api.anthropic.com',\n gemini: 'https://generativelanguage.googleapis.com/v1beta',\n minimax_openai: 'https://api.minimaxi.com/v1',\n minimax_anthropic: 'https://api.minimaxi.com',\n custom: '',\n}\n\nfunction OCRNetworkSection({ form, updateForm, mountedRef }: SectionProps) {\n const [ocrChecking, setOcrChecking] = useState(false)\n const [ocrStatus, setOcrStatus] = useState<'green' | 'red' | 'yellow' | null>(null)\n const [ocrMsg, setOcrMsg] = useState('')\n const [ocrEngines, setOcrEngines] = useState>({})\n const [llmModels, setLlmModels] = useState([])\n const [llmFetchMsg, setLlmFetchMsg] = useState('')\n const [onlineEngineStatus, setOnlineEngineStatus] = useState>({ mineru: '', paddleocr: '' })\n const [onlineTesting, setOnlineTesting] = useState>({ mineru: false, paddleocr: false })\n const [llmFetching, setLlmFetching] = useState(false)\n const [llmTestMsg, setLlmTestMsg] = useState('')\n const [llmTesting, setLlmTesting] = useState(false)\n const [aiModels, setAiModels] = useState<{ id: string; name: string }[]>([])\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchModelsMsg, setFetchModelsMsg] = useState('')\n const [aiVisionTest, setAiVisionTest] = useState<'testing' | 'ok' | 'fail' | null>(null)\n const [aiVisionMsg, setAiVisionMsg] = useState('')\n const [saving, setSaving] = useState(false)\n const [saveMsg, setSaveMsg] = useState('')\n const [updateChecking, setUpdateChecking] = useState(false)\n const [updateResult, setUpdateResult] = useState('')\n\n // --- OCR logic ---\n const checkOcr = useCallback(async (engine?: string) => {\n setOcrChecking(true)\n try {\n const eng = engine || form.ocr_engine || 'tesseract'\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.message || (data.ok ? data.version || '已安装' : '未检测到'))\n } catch (e) {\n if (mountedRef.current) {\n setOcrStatus('red')\n setOcrMsg('检测失败')\n }\n } finally {\n if (mountedRef.current) setOcrChecking(false)\n }\n }, [form.ocr_engine, mountedRef])\n\n const handleDetectOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: false, msg: '检测中...' } }))\n try {\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(engine)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '检测失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleInstallOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: true, msg: '安装中...' } }))\n try {\n const res = await fetch('/api/v1/install-ocr', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ engine }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.message || (data.ok ? '安装成功' : '安装失败'),\n },\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '安装请求失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleTestMineru = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n setOnlineEngineStatus(prev => ({ ...prev, mineru: '' }))\n try {\n const res = await fetch('/api/v1/check-mineru', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).mineru_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, mineru: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }, [form, mountedRef])\n\n const handleTestPaddleocrOnline = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: '' }))\n try {\n const res = await fetch('/api/v1/check-paddleocr-online', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).paddleocr_online_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }, [form, mountedRef])\n\n // Auto-detect OCR engine statuses\n const autoOcrRef = useRef(false)\n useEffect(() => {\n if (autoOcrRef.current) return\n autoOcrRef.current = true\n const engines = ['tesseract', 'paddleocr']\n engines.forEach((eng) => {\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n [eng]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n })\n const defaultEngine = form.ocr_engine || 'tesseract'\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(defaultEngine)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.version || data.message || (data.ok ? '已安装' : '未检测到'))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Auto-detect PaddleOCR availability on mount\n useEffect(() => {\n fetch(`${API_BASE}/check-ocr?engine=paddleocr`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n paddleocr: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Update OCR header status when engine selection changes\n useEffect(() => {\n const eng = form.ocr_engine || 'tesseract'\n const info = ocrEngines[eng]\n if (info) {\n setOcrStatus(info.installed ? 'green' : 'red')\n setOcrMsg(info.msg || (info.installed ? '已安装' : '未检测到'))\n }\n }, [form.ocr_engine, ocrEngines])\n\n // Auto-detect online API connectivity\n const autoOnlineRef = useRef(false)\n useEffect(() => {\n if (autoOnlineRef.current) return\n autoOnlineRef.current = true\n const check = async () => {\n if ((form as any).mineru_token) {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n try {\n const r = await fetch('/api/v1/check-mineru', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).mineru_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }\n if ((form as any).paddleocr_online_token) {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n try {\n const r = await fetch('/api/v1/check-paddleocr-online', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).paddleocr_online_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }\n }\n check()\n }, [mountedRef])\n\n // --- Save / Update ---\n const handleSave = useCallback(async () => {\n setSaving(true)\n setSaveMsg('')\n try {\n const body = JSON.stringify(form)\n const res = await fetch('/api/v1/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!res.ok) {\n setSaveMsg('保存失败: HTTP ' + res.status)\n setSaving(false)\n return\n }\n setSaveMsg('保存成功')\n setTimeout(() => setSaveMsg(''), 2000)\n } catch (e: any) {\n setSaveMsg('保存失败: ' + (e.message || '未知错误'))\n }\n setSaving(false)\n }, [form, mountedRef])\n\n const handleCheckUpdate = useCallback(async () => {\n setUpdateChecking(true)\n setUpdateResult('')\n try {\n const res = await fetch('/api/v1/check-update')\n const data = await res.json()\n if (!mountedRef.current) return\n if (data.has_update) {\n setUpdateResult(`新版本 v${data.latest} 可用 (当前 v${data.current})`)\n } else {\n setUpdateResult(`已是最新版本 v${data.current}`)\n }\n } catch (e) {\n if (mountedRef.current) setUpdateResult('检查失败')\n } finally {\n if (mountedRef.current) setUpdateChecking(false)\n }\n }, [mountedRef])\n\n return (\n
\n {/* OCRmyPDF 独立状态区 */}\n
\n \n
\n \n \n {(ocrEngines['ocrmypdf'] as any)?.msg || '点击检测'}\n \n handleDetectOcrEngine('ocrmypdf')}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine('ocrmypdf')}\n disabled={(ocrEngines['ocrmypdf'] as any)?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {(ocrEngines['ocrmypdf'] as any)?.installing ? '安装中...' : '一键安装'}\n \n
\n
\n\n {/* 引擎切换区 */}\n
\n 引擎切换\n
\n {OCR_ENGINES.filter(eng => eng.key !== 'llm_ocr' && eng.key !== 'mineru' && eng.key !== 'paddleocr_online').map((eng) => {\n const info = (ocrEngines[eng.key] as any) || { installed: eng.key === 'llm_ocr', msg: '' }\n const isSelected = form.ocr_engine === eng.key\n return (\n \n
\n {eng.name}\n
\n \n
\n
\n

{eng.desc}

\n {eng.key !== 'llm_ocr' && info?.msg && (\n

\n {info.msg}\n

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && !info?.installed && (\n

需要 Python 3.11 虚拟环境,点击安装自动搭建

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && info?.installed && (info as any)?.venv && (\n

运行环境: {(info as any).venv}

\n )}\n
\n {eng.key !== 'llm_ocr' && (\n <>\n handleDetectOcrEngine(eng.key)}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine(eng.key)}\n disabled={info?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {info?.installing ? '安装中...' : '安装'}\n \n \n )}\n {eng.key === form.ocr_engine ? null : (\n updateForm({ ocr_engine: eng.key })}\n className=\"px-2 py-1 text-xs rounded text-blue-600 hover:bg-blue-50 ml-auto\"\n >\n 选用\n \n )}\n
\n
\n )\n })}\n
\n
\n\n {/* Tesseract 语言包状态 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n Tesseract 语言包\n
\n \n \n {(ocrEngines['tesseract'] as any)?.has_chi_sim\n ? 'chi_sim 已安装'\n : (ocrEngines['tesseract'] as any)?.languages\n ? 'chi_sim 未安装'\n : '请先检测 Tesseract'}\n \n {!((ocrEngines['tesseract'] as any)?.has_chi_sim) && (\n {\n try {\n await fetch('/api/v1/install-tesseract-lang?lang=chi_sim')\n handleDetectOcrEngine('tesseract')\n } catch (e) {\n }\n }}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600\"\n >\n 安装语言包\n \n )}\n
\n
\n )}\n\n
\n
\n \n updateForm({ ocr_languages: e.target.value })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n \n
\n
\n \n updateForm({ ocr_jobs: parseInt(e.target.value) || 1 })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n

Tesseract 支持多线程,PaddleOCR 仅单线程

\n
\n
\n \n updateForm({ ocr_timeout: parseInt(e.target.value) || 3600 })}\n min={60}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n
\n
\n \n updateForm({ ocr_oversample: parseInt(e.target.value) } as any)}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n 越低越快,150-400,推荐 200\n
\n
\n\n {/* OCR 安装指引 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n \n \n \n \n OCR 命令行安装指引\n \n
\n

📋 将以下提示词复制并发送给 OpenCode:

\n
{OCR_INSTALL_GUIDE}
\n

安装后返回设置页点击\"检测\"按钮确认状态。

\n
\n
\n )}\n\n {/* LLM OCR */}\n
\n
\n \n {form.ocr_engine === 'llm_ocr' ? (\n LLM OCR 已启用\n ) : (\n LLM OCR\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'llm_ocr' ? 'tesseract' : 'llm_ocr' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'llm_ocr'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'llm_ocr' ? '停用并回到 OCRmyPDF' : '启用 LLM OCR'}\n \n
\n
\n\n {form.ocr_engine === 'llm_ocr' && (\n
\n

\n 使用视觉大模型逐框识别文字层。需要运行 lmstudio / ollama 加载对应模型。\n 推荐模型已验证中文 PDF 可用。\n

\n
\n \n
\n updateForm({ llm_ocr_endpoint: e.target.value } as any)}\n placeholder=\"http://127.0.0.1:1234/v1\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmTestMsg &&

{llmTestMsg}

}\n
\n
\n \n
\n updateForm({ llm_ocr_model: e.target.value } as any)}\n placeholder=\"qwen3-vl-4b-instruct\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmModels.length > 0 && (\n \n )}\n {llmFetchMsg &&

{llmFetchMsg}

}\n
\n
\n \n updateForm({ llm_ocr_concurrency: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 1 (默认)5 (最快)\n
\n
\n
\n \n updateForm({ llm_ocr_detect_batch: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 5 页 (低内存)50 页 (更快)\n
\n

Surya 版面检测每批处理页数。内存不足时降低。

\n
\n
\n \n 推荐模型 (已验证中文 PDF)\n \n
\n {LLM_OCR_RECOMMENDED.map(m => (\n \n ))}\n
\n
\n
\n )}\n\n {/* MinerU */}\n
\n
\n \n {form.ocr_engine === 'mineru' ? (\n MinerU 线上 API 已启用\n ) : (\n MinerU 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'mineru' ? 'tesseract' : 'mineru' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'mineru'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'mineru' ? '停用 MinerU' : '启用 MinerU'}\n \n
\n
\n {form.ocr_engine === 'mineru' && (\n
\n

\n 使用 MinerU v4 精准解析 API 进行文档 OCR。PDF 将上传至 MinerU 服务器处理。\n 仅限中国大陆网络访问,单文件 ≤200MB / ≤200页。\n 实验功能,识别可能会有重复\n

\n
\n \n updateForm({ mineru_token: e.target.value } as any)}\n placeholder=\"输入 MinerU API Token(Bearer 认证)\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n {onlineTesting.mineru ? '测试中...' : '检测'}\n \n {onlineEngineStatus.mineru === 'ok' && (\n 连接正常\n )}\n {onlineEngineStatus.mineru === 'fail' && (\n 连接失败\n )}\n
\n

\n 需要先申请 Token:MinerU API 文档\n

\n
\n )}\n\n {/* PaddleOCR-VL-1.5 */}\n
\n
\n \n {form.ocr_engine === 'paddleocr_online' ? (\n PaddleOCR-VL-1.5 线上 API 已启用\n ) : (\n PaddleOCR-VL-1.5 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'paddleocr_online' ? 'tesseract' : 'paddleocr_online' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'paddleocr_online'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'paddleocr_online' ? '停用 PaddleOCR-VL-1.5' : '启用 PaddleOCR-VL-1.5'}\n \n
\n
\n {form.ocr_engine === 'paddleocr_online' && (\n
\n

\n 使用百度 PaddleOCR-VL-1.5 视觉大模型进行文档版面解析。\n 支持中英文文档,无需本地 GPU。\n 实验功能,识别可能会有重复\n

\n
\n \n
\n {[\n { key: 'spatial', label: '空间分配(快)' },\n { key: 'perbox', label: '逐框识别(慢)' },\n { key: 'hybrid', label: '混合识别' },\n ].map(({ key, label }) => (\n updateForm({ paddleocr_online_mode: key } as any)}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).paddleocr_online_mode || 'spatial') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n

\n {((form as any).paddleocr_online_mode || 'spatial') === 'spatial' && '段落文字精准,行识别不精确'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'perbox' && '行识别精准,有乱码风险'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'hybrid' && '逐框为主、空间填补,可能产生识别重复'}\n

\n
\n
\n \n
\n updateForm({ paddleocr_online_token: e.target.value } as any)}\n placeholder=\"输入 PaddleOCR access token\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n {onlineTesting.paddleocr ? '测试中...' : '检测'}\n \n
\n {onlineEngineStatus.paddleocr === 'ok' && (\n

连接正常

\n )}\n {onlineEngineStatus.paddleocr === 'fail' && (\n

连接失败

\n )}\n
\n

\n 从 PaddleOCR 控制台 获取 Access Token。\n API 文档\n

\n
\n )}\n\n
\n updateForm({ ocr_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n\n {/* ============ AI Vision / 书签 ============ */}\n
\n AI Vision 目录提取\n
\n updateForm({ ai_vision_enabled: e.target.checked } as any)} className=\"rounded\" />\n \n
\n
\n \n updateForm({ ai_vision_endpoint: e.target.value } as any)}\n placeholder={\n (form as any).ai_vision_provider === 'gemini' ? 'https://generativelanguage.googleapis.com/v1beta' :\n (form as any).ai_vision_provider === 'azure' ? 'https://{resource}.openai.azure.com' :\n (form as any).ai_vision_provider === 'anthropic' || (form as any).ai_vision_provider === 'minimax_anthropic' ? 'https://api.anthropic.com' :\n (form as any).ai_vision_provider === 'minimax_openai' ? 'https://api.minimaxi.com/v1' :\n 'http://127.0.0.1:12345/v1'\n }\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n
\n updateForm({ ai_vision_model: e.target.value } as any)}\n placeholder=\"sabafallah/deepseek-ocr\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n {\n setFetchingModels(true)\n setFetchModelsMsg('')\n try {\n const res = await fetch('/api/v1/fetch-models', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n }),\n })\n const data = await res.json()\n if (data.ok && data.models.length > 0) {\n setAiModels(data.models)\n setFetchModelsMsg(`${data.models.length} 个模型`)\n } else {\n setFetchModelsMsg(data.message || '无可用模型')\n }\n } catch (e) {\n setFetchModelsMsg(String(e))\n }\n setFetchingModels(false)\n }}\n disabled={fetchingModels || !(form as any).ai_vision_endpoint}\n className=\"px-2 py-1.5 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 whitespace-nowrap\"\n title=\"从提供商拉取可用模型列表\"\n >\n {fetchingModels ? '...' : '获取模型'}\n \n
\n {aiModels.length > 0 && (\n updateForm({ ai_vision_model: e.target.value } as any)}\n className=\"w-full mt-1 rounded border border-blue-300 px-2 py-1 text-xs font-mono\"\n size={Math.min(aiModels.length + 1, 8)}\n >\n \n {aiModels.map(m => (\n \n ))}\n \n )}\n {fetchModelsMsg && (\n

\n {fetchModelsMsg}\n

\n )}\n
\n
\n \n updateForm({ ai_vision_api_key: e.target.value } as any)}\n placeholder=\"sk-... (支持 {env:VAR_NAME})\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n \n
\n {(form as any).ai_vision_provider === 'custom' && (\n
\n updateForm({ ai_vision_messages_api: e.target.checked } as any)} className=\"rounded\" />\n \n
\n )}\n
\n
\n \n updateForm({ ai_vision_dpi: parseInt(e.target.value) || 150 } as any)}\n min={72} max={300} className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs\" />\n
\n
\n {\n setAiVisionTest('testing');\n try {\n const res = await fetch('/api/v1/check-ai-vision', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n model: (form as any).ai_vision_model,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n messages_api: (form as any).ai_vision_messages_api,\n }),\n });\n const data = await res.json();\n setAiVisionTest(data.ok ? 'ok' : 'fail');\n setAiVisionMsg(data.message || data.error || '');\n } catch (e) {\n setAiVisionTest('fail');\n setAiVisionMsg(String(e));\n }\n }}\n disabled={aiVisionTest === 'testing'}\n className=\"px-3 py-1.5 mt-2 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50\"\n >\n {aiVisionTest === 'testing' ? '测试中...' : '检测'}\n \n {aiVisionMsg && (\n

\n {aiVisionMsg}\n

\n )}\n

\n 阶段1 从 OCR 文字层提取目录(免费),阶段2 用 AI Vision 从目录页图片提取。\n 支持 OpenAI 兼容 / Gemini / MiniMax 格式。\n

\n
\n updateForm({ bookmark_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n
\n\n {/* ============ 主题 ============ */}\n
\n \n
\n {[\n { key: 'auto', label: '自动' },\n { key: 'light', label: '白昼' },\n { key: 'dark', label: '黑夜' },\n ].map(({ key, label }) => (\n updateForm({ theme: key })}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).theme || 'auto') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n
\n\n {/* ============ 保存按钮 ============ */}\n
\n \n {saving ? '保存中...' : '保存设置'}\n \n \n {updateChecking ? '检测中...' : '检查更新'}\n \n {saveMsg && (\n \n {saveMsg}\n \n )}\n {updateResult && (\n \n {updateResult}\n \n )}\n
\n \n )\n}\n\nexport default React.memo(OCRNetworkSection)\n"}, "f52fa61576070eef83250d867ac8ce14ab5406c2": {"t": 1785772783.7645154, "n": 48543, "h": "3e5e5070f2570bc2", "c": "import React, { useState, useEffect, useRef, useCallback } from 'react'\nimport { SectionProps } from './types'\nimport { LLM_OCR_RECOMMENDED } from '../../constants'\n\nfunction StatusDot({ status }: { status: 'green' | 'red' | 'yellow' | null }) {\n const colors: Record = {\n green: 'bg-green-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500',\n }\n return (\n \n )\n}\n\nconst OCR_ENGINES = [\n { key: 'tesseract', name: 'Tesseract OCR', desc: '内置引擎,需 chi_sim 语言包' },\n { key: 'paddleocr', name: 'PaddleOCR', desc: '百度引擎,需 Python 3.11 虚拟环境' },\n { key: 'llm_ocr', name: 'LLM OCR', desc: '大模型视觉识别,无需本地引擎' },\n { key: 'mineru', name: 'MinerU', desc: '线上 API 精准解析,需 Token' },\n { key: 'paddleocr_online', name: 'PaddleOCR-VL-1.5', desc: '线上 API,需 Token' },\n]\n\nconst OCR_INSTALL_GUIDE = `## 安装 OCR 引擎\n\n**注意**:Python 3.14 与 \\`paddlepaddle\\` 不兼容。建议使用 Python 3.11 创建虚拟环境。\n\n---\n\n### 1. 创建/重建虚拟环境(Python 3.11)\n\n\\`\\`\\`powershell\n# 如果原 venv 不支持 Python 3.11,先删除\nRemove-Item -Recurse -Force backend\\\\venv\n\n# 使用 Python 3.11 创建新 venv\nC:\\\\Python311\\\\python.exe -m venv backend\\\\venv\n\\`\\`\\`\n\n### 2. 激活虚拟环境(PowerShell)\n\n\\`\\`\\`powershell\n# 方法一:PowerShell 执行策略问题\npowershell -ExecutionPolicy Bypass -Command \"backend\\\\venv\\\\Scripts\\\\Activate.ps1\"\n\n# 方法二:直接用 python 运行(无需激活)\nC:\\\\Python311\\\\python.exe -m pip install ...\n\\`\\`\\`\n\n### 3. 安装 OCRmyPDF 核心\n\n\\`\\`\\`powershell\npython -m pip install ocrmypdf\n\\`\\`\\`\n\n### 4. 安装引擎插件\n\n\\`\\`\\`powershell\n# 安装所有 OCR 引擎(推荐,一步到位)\npython -m pip install ocrmypdf-paddleocr paddleocr\n\n# 或分开安装\npython -m pip install paddleocr\npython -m pip install ocrmypdf-paddleocr # 需要先装好 paddleocr\n\\`\\`\\`\n\n### 5. 安装 Tesseract OCR 系统依赖\n\n\\`\\`\\`powershell\nwinget install --id UB-Mannheim.TesseractOCR --accept-package-agreements --accept-source-agreements\n\\`\\`\\`\n\n### 6. 将 Tesseract 添加到 PATH\n\n\\`\\`\\`powershell\n[Environment]::SetEnvironmentVariable(\"Path\", [Environment]::GetEnvironmentVariable(\"Path\", \"User\") + \";C:\\\\Program Files\\\\Tesseract-OCR\", \"User\")\n\\`\\`\\`\n\n> ⚠️ 添加后需**重启终端或IDE**使 PATH 生效,或使用完整路径验证:\n> \\`\\`\\`powershell\n> & \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n> \\`\\`\\`\n\n### 7. 验证安装\n\n\\`\\`\\`powershell\npython -c \"import ocrmypdf; print('ocrmypdf:', ocrmypdf.__version__)\"\npython -c \"from paddle import paddle; print('paddle:', paddle.__version__)\"\n\"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\" --version\n\\`\\`\\`\n\n### 常见问题\n\n| 问题 | 解决 |\n|------|------|\n| \\`ModuleNotFoundError: No module named 'paddlepaddle'\\` | \\`import paddle\\` 而非 \\`import paddlepaddle\\` |\n| \\`ocrmypdf-paddleocr\\` 依赖冲突 | 先装 \\`paddleocr\\`,再装 \\`ocrmypdf-paddleocr\\` |\n| Tesseract 找不到 | 重启终端,或使用完整路径 \\`C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\\` |`\n\nconst AI_VISION_ENDPOINTS: Record = {\n openai_compatible: 'https://api.openai.com/v1',\n openai_responses: 'https://api.openai.com/v1',\n azure: 'https://RESOURCE_NAME.openai.azure.com',\n anthropic: 'https://api.anthropic.com',\n gemini: 'https://generativelanguage.googleapis.com/v1beta',\n minimax_openai: 'https://api.minimaxi.com/v1',\n minimax_anthropic: 'https://api.minimaxi.com',\n custom: '',\n}\n\nfunction OCRNetworkSection({ form, updateForm, mountedRef }: SectionProps) {\n const [ocrChecking, setOcrChecking] = useState(false)\n const [ocrStatus, setOcrStatus] = useState<'green' | 'red' | 'yellow' | null>(null)\n const [ocrMsg, setOcrMsg] = useState('')\n const [ocrEngines, setOcrEngines] = useState>({})\n const [llmModels, setLlmModels] = useState([])\n const [llmFetchMsg, setLlmFetchMsg] = useState('')\n const [onlineEngineStatus, setOnlineEngineStatus] = useState>({ mineru: '', paddleocr: '' })\n const [onlineTesting, setOnlineTesting] = useState>({ mineru: false, paddleocr: false })\n const [llmFetching, setLlmFetching] = useState(false)\n const [llmTestMsg, setLlmTestMsg] = useState('')\n const [llmTesting, setLlmTesting] = useState(false)\n const [aiModels, setAiModels] = useState<{ id: string; name: string }[]>([])\n const [fetchingModels, setFetchingModels] = useState(false)\n const [fetchModelsMsg, setFetchModelsMsg] = useState('')\n const [aiVisionTest, setAiVisionTest] = useState<'testing' | 'ok' | 'fail' | null>(null)\n const [aiVisionMsg, setAiVisionMsg] = useState('')\n const [saving, setSaving] = useState(false)\n const [saveMsg, setSaveMsg] = useState('')\n const [updateChecking, setUpdateChecking] = useState(false)\n const [updateResult, setUpdateResult] = useState('')\n\n // --- OCR logic ---\n const checkOcr = useCallback(async (engine?: string) => {\n setOcrChecking(true)\n try {\n const eng = engine || form.ocr_engine || 'tesseract'\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.message || (data.ok ? data.version || '已安装' : '未检测到'))\n } catch (e) {\n if (mountedRef.current) {\n setOcrStatus('red')\n setOcrMsg('检测失败')\n }\n } finally {\n if (mountedRef.current) setOcrChecking(false)\n }\n }, [form.ocr_engine, mountedRef])\n\n const handleDetectOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: false, msg: '检测中...' } }))\n try {\n const res = await fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(engine)}`)\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '检测失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleInstallOcrEngine = useCallback(async (engine: string) => {\n setOcrEngines(prev => ({ ...prev, [engine]: { ...prev[engine], installing: true, msg: '安装中...' } }))\n try {\n const res = await fetch('/api/v1/install-ocr', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ engine }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOcrEngines(prev => ({\n ...prev,\n [engine]: {\n installed: data.ok || false,\n installing: false,\n msg: data.message || (data.ok ? '安装成功' : '安装失败'),\n },\n }))\n } catch (e) {\n if (mountedRef.current) {\n setOcrEngines(prev => ({\n ...prev,\n [engine]: { installed: false, installing: false, msg: '安装请求失败' },\n }))\n }\n }\n }, [mountedRef])\n\n const handleTestMineru = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n setOnlineEngineStatus(prev => ({ ...prev, mineru: '' }))\n try {\n const res = await fetch('/api/v1/check-mineru', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).mineru_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, mineru: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }, [form, mountedRef])\n\n const handleTestPaddleocrOnline = useCallback(async () => {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: '' }))\n try {\n const res = await fetch('/api/v1/check-paddleocr-online', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: (form as any).paddleocr_online_token || '' }),\n })\n const data = await res.json()\n if (!mountedRef.current) return\n setOnlineEngineStatus(prev => ({ ...prev, paddleocr: data.ok ? 'ok' : 'fail' }))\n } catch {\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' }))\n }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }, [form, mountedRef])\n\n // Auto-detect OCR engine statuses\n const autoOcrRef = useRef(false)\n useEffect(() => {\n if (autoOcrRef.current) return\n autoOcrRef.current = true\n const engines = ['tesseract', 'paddleocr']\n engines.forEach((eng) => {\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(eng)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrEngines((prev) => ({\n ...prev,\n [eng]: {\n installed: data.ok || false,\n installing: false,\n msg: data.version || data.message || (data.ok ? '已安装' : '未检测到'),\n has_chi_sim: data.has_chi_sim,\n languages: data.languages,\n venv: data.venv,\n } as any,\n }))\n })\n .catch(() => {})\n })\n const defaultEngine = form.ocr_engine || 'tesseract'\n fetch(`/api/v1/check-ocr?engine=${encodeURIComponent(defaultEngine)}`)\n .then((r) => r.json())\n .then((data) => {\n if (!mountedRef.current) return\n setOcrStatus(data.ok ? 'green' : 'red')\n setOcrMsg(data.version || data.message || (data.ok ? '已安装' : '未检测到'))\n })\n .catch(() => {})\n }, [form.ocr_engine, mountedRef])\n\n // Update OCR header status when engine selection changes\n useEffect(() => {\n const eng = form.ocr_engine || 'tesseract'\n const info = ocrEngines[eng]\n if (info) {\n setOcrStatus(info.installed ? 'green' : 'red')\n setOcrMsg(info.msg || (info.installed ? '已安装' : '未检测到'))\n }\n }, [form.ocr_engine, ocrEngines])\n\n // Auto-detect online API connectivity\n const autoOnlineRef = useRef(false)\n useEffect(() => {\n if (autoOnlineRef.current) return\n autoOnlineRef.current = true\n const check = async () => {\n if ((form as any).mineru_token) {\n setOnlineTesting(prev => ({ ...prev, mineru: true }))\n try {\n const r = await fetch('/api/v1/check-mineru', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).mineru_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, mineru: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, mineru: false }))\n }\n if ((form as any).paddleocr_online_token) {\n setOnlineTesting(prev => ({ ...prev, paddleocr: true }))\n try {\n const r = await fetch('/api/v1/check-paddleocr-online', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: (form as any).paddleocr_online_token }) })\n const d = await r.json()\n if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: d.ok ? 'ok' : 'fail' }))\n } catch { if (mountedRef.current) setOnlineEngineStatus(prev => ({ ...prev, paddleocr: 'fail' })) }\n if (mountedRef.current) setOnlineTesting(prev => ({ ...prev, paddleocr: false }))\n }\n }\n check()\n }, [mountedRef])\n\n // --- Save / Update ---\n const handleSave = useCallback(async () => {\n setSaving(true)\n setSaveMsg('')\n try {\n const body = JSON.stringify(form)\n const res = await fetch('/api/v1/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!res.ok) {\n setSaveMsg('保存失败: HTTP ' + res.status)\n setSaving(false)\n return\n }\n setSaveMsg('保存成功')\n setTimeout(() => setSaveMsg(''), 2000)\n } catch (e: any) {\n setSaveMsg('保存失败: ' + (e.message || '未知错误'))\n }\n setSaving(false)\n }, [form, mountedRef])\n\n const handleCheckUpdate = useCallback(async () => {\n setUpdateChecking(true)\n setUpdateResult('')\n try {\n const res = await fetch('/api/v1/check-update')\n const data = await res.json()\n if (!mountedRef.current) return\n if (data.has_update) {\n setUpdateResult(`新版本 v${data.latest} 可用 (当前 v${data.current})`)\n } else {\n setUpdateResult(`已是最新版本 v${data.current}`)\n }\n } catch (e) {\n if (mountedRef.current) setUpdateResult('检查失败')\n } finally {\n if (mountedRef.current) setUpdateChecking(false)\n }\n }, [mountedRef])\n\n return (\n
\n {/* OCRmyPDF 独立状态区 */}\n
\n \n
\n \n \n {(ocrEngines['ocrmypdf'] as any)?.msg || '点击检测'}\n \n handleDetectOcrEngine('ocrmypdf')}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine('ocrmypdf')}\n disabled={(ocrEngines['ocrmypdf'] as any)?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {(ocrEngines['ocrmypdf'] as any)?.installing ? '安装中...' : '一键安装'}\n \n
\n
\n\n {/* 引擎切换区 */}\n
\n 引擎切换\n
\n {OCR_ENGINES.filter(eng => eng.key !== 'llm_ocr' && eng.key !== 'mineru' && eng.key !== 'paddleocr_online').map((eng) => {\n const info = (ocrEngines[eng.key] as any) || { installed: eng.key === 'llm_ocr', msg: '' }\n const isSelected = form.ocr_engine === eng.key\n return (\n \n
\n {eng.name}\n
\n \n
\n
\n

{eng.desc}

\n {eng.key !== 'llm_ocr' && info?.msg && (\n

\n {info.msg}\n

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && !info?.installed && (\n

需要 Python 3.11 虚拟环境,点击安装自动搭建

\n )}\n {eng.key !== 'llm_ocr' && eng.key === 'paddleocr' && info?.installed && (info as any)?.venv && (\n

运行环境: {(info as any).venv}

\n )}\n
\n {eng.key !== 'llm_ocr' && (\n <>\n handleDetectOcrEngine(eng.key)}\n className=\"px-2 py-1 text-xs rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500\"\n >\n 检测\n \n handleInstallOcrEngine(eng.key)}\n disabled={info?.installing}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50\"\n >\n {info?.installing ? '安装中...' : '安装'}\n \n \n )}\n {eng.key === form.ocr_engine ? null : (\n updateForm({ ocr_engine: eng.key })}\n className=\"px-2 py-1 text-xs rounded text-blue-600 hover:bg-blue-50 ml-auto\"\n >\n 选用\n \n )}\n
\n
\n )\n })}\n
\n
\n\n {/* Tesseract 语言包状态 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n Tesseract 语言包\n
\n \n \n {(ocrEngines['tesseract'] as any)?.has_chi_sim\n ? 'chi_sim 已安装'\n : (ocrEngines['tesseract'] as any)?.languages\n ? 'chi_sim 未安装'\n : '请先检测 Tesseract'}\n \n {!((ocrEngines['tesseract'] as any)?.has_chi_sim) && (\n {\n try {\n await fetch('/api/v1/install-tesseract-lang?lang=chi_sim')\n handleDetectOcrEngine('tesseract')\n } catch (e) {\n }\n }}\n className=\"px-2 py-1 text-xs rounded bg-orange-500 text-white hover:bg-orange-600\"\n >\n 安装语言包\n \n )}\n
\n
\n )}\n\n
\n
\n \n updateForm({ ocr_languages: e.target.value })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n \n
\n
\n \n updateForm({ ocr_jobs: parseInt(e.target.value) || 1 })}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n >\n \n \n \n \n

Tesseract 支持多线程,PaddleOCR 仅单线程

\n
\n
\n \n updateForm({ ocr_timeout: parseInt(e.target.value) || 3600 })}\n min={60}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n
\n
\n \n updateForm({ ocr_oversample: parseInt(e.target.value) } as any)}\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500\"\n />\n 越低越快,150-400,推荐 200\n
\n
\n\n {/* OCR 安装指引 */}\n {form.ocr_engine !== 'llm_ocr' && (\n
\n \n \n \n \n OCR 命令行安装指引\n \n
\n

📋 将以下提示词复制并发送给 OpenCode:

\n
{OCR_INSTALL_GUIDE}
\n

安装后返回设置页点击\"检测\"按钮确认状态。

\n
\n
\n )}\n\n {/* LLM OCR */}\n
\n
\n \n {form.ocr_engine === 'llm_ocr' ? (\n LLM OCR 已启用\n ) : (\n LLM OCR\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'llm_ocr' ? 'tesseract' : 'llm_ocr' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'llm_ocr'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'llm_ocr' ? '停用并回到 OCRmyPDF' : '启用 LLM OCR'}\n \n
\n
\n\n {form.ocr_engine === 'llm_ocr' && (\n
\n

\n 使用视觉大模型逐框识别文字层。需要运行 lmstudio / ollama 加载对应模型。\n 推荐模型已验证中文 PDF 可用。\n

\n
\n \n
\n updateForm({ llm_ocr_endpoint: e.target.value } as any)}\n placeholder=\"http://127.0.0.1:1234/v1\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmTestMsg &&

{llmTestMsg}

}\n
\n
\n \n
\n updateForm({ llm_ocr_model: e.target.value } as any)}\n placeholder=\"qwen3-vl-4b-instruct\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n
\n {llmModels.length > 0 && (\n \n )}\n {llmFetchMsg &&

{llmFetchMsg}

}\n
\n
\n \n updateForm({ llm_ocr_concurrency: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 1 (默认)5 (最快)\n
\n
\n
\n \n updateForm({ llm_ocr_detect_batch: parseInt(e.target.value) } as any)}\n className=\"w-full\" />\n
\n 5 页 (低内存)50 页 (更快)\n
\n

Surya 版面检测每批处理页数。内存不足时降低。

\n
\n
\n \n 推荐模型 (已验证中文 PDF)\n \n
\n {LLM_OCR_RECOMMENDED.map(m => (\n \n ))}\n
\n
\n
\n )}\n\n {/* MinerU */}\n
\n
\n \n {form.ocr_engine === 'mineru' ? (\n MinerU 线上 API 已启用\n ) : (\n MinerU 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'mineru' ? 'tesseract' : 'mineru' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'mineru'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'mineru' ? '停用 MinerU' : '启用 MinerU'}\n \n
\n
\n {form.ocr_engine === 'mineru' && (\n
\n

\n 使用 MinerU v4 精准解析 API 进行文档 OCR。PDF 将上传至 MinerU 服务器处理。\n 仅限中国大陆网络访问,单文件 ≤200MB / ≤200页。\n 实验功能,识别可能会有重复\n

\n
\n \n updateForm({ mineru_token: e.target.value } as any)}\n placeholder=\"输入 MinerU API Token(Bearer 认证)\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n {onlineTesting.mineru ? '测试中...' : '检测'}\n \n {onlineEngineStatus.mineru === 'ok' && (\n 连接正常\n )}\n {onlineEngineStatus.mineru === 'fail' && (\n 连接失败\n )}\n
\n

\n 需要先申请 Token:MinerU API 文档\n

\n
\n )}\n\n {/* PaddleOCR-VL-1.5 */}\n
\n
\n \n {form.ocr_engine === 'paddleocr_online' ? (\n PaddleOCR-VL-1.5 线上 API 已启用\n ) : (\n PaddleOCR-VL-1.5 线上 API\n )}\n \n updateForm({ ocr_engine: form.ocr_engine === 'paddleocr_online' ? 'tesseract' : 'paddleocr_online' })}\n className={`px-3 py-1 text-xs rounded ${\n form.ocr_engine === 'paddleocr_online'\n ? 'bg-gray-200 text-gray-600 hover:bg-gray-300'\n : 'bg-blue-500 text-white hover:bg-blue-600'\n }`}\n >\n {form.ocr_engine === 'paddleocr_online' ? '停用 PaddleOCR-VL-1.5' : '启用 PaddleOCR-VL-1.5'}\n \n
\n
\n {form.ocr_engine === 'paddleocr_online' && (\n
\n

\n 使用百度 PaddleOCR-VL-1.5 视觉大模型进行文档版面解析。\n 支持中英文文档,无需本地 GPU。\n 实验功能,识别可能会有重复\n

\n
\n \n
\n {[\n { key: 'spatial', label: '空间分配(快)' },\n { key: 'perbox', label: '逐框识别(慢)' },\n { key: 'hybrid', label: '混合识别' },\n ].map(({ key, label }) => (\n updateForm({ paddleocr_online_mode: key } as any)}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).paddleocr_online_mode || 'spatial') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n

\n {((form as any).paddleocr_online_mode || 'spatial') === 'spatial' && '段落文字精准,行识别不精确'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'perbox' && '行识别精准,有乱码风险'}\n {((form as any).paddleocr_online_mode || 'spatial') === 'hybrid' && '逐框为主、空间填补,可能产生识别重复'}\n

\n
\n
\n \n
\n updateForm({ paddleocr_online_token: e.target.value } as any)}\n placeholder=\"输入 PaddleOCR access token\"\n className=\"flex-1 rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n \n {onlineTesting.paddleocr ? '测试中...' : '检测'}\n \n
\n {onlineEngineStatus.paddleocr === 'ok' && (\n

连接正常

\n )}\n {onlineEngineStatus.paddleocr === 'fail' && (\n

连接失败

\n )}\n
\n

\n 从 PaddleOCR 控制台 获取 Access Token。\n API 文档\n

\n
\n )}\n\n
\n updateForm({ ocr_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n\n {/* ============ AI Vision / 书签 ============ */}\n
\n AI Vision 目录提取\n
\n updateForm({ ai_vision_enabled: e.target.checked } as any)} className=\"rounded\" />\n \n
\n
\n \n updateForm({ ai_vision_endpoint: e.target.value } as any)}\n placeholder={\n (form as any).ai_vision_provider === 'gemini' ? 'https://generativelanguage.googleapis.com/v1beta' :\n (form as any).ai_vision_provider === 'azure' ? 'https://{resource}.openai.azure.com' :\n (form as any).ai_vision_provider === 'anthropic' || (form as any).ai_vision_provider === 'minimax_anthropic' ? 'https://api.anthropic.com' :\n (form as any).ai_vision_provider === 'minimax_openai' ? 'https://api.minimaxi.com/v1' :\n 'http://127.0.0.1:12345/v1'\n }\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n
\n updateForm({ ai_vision_model: e.target.value } as any)}\n placeholder=\"sabafallah/deepseek-ocr\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n {\n setFetchingModels(true)\n setFetchModelsMsg('')\n try {\n const res = await fetch('/api/v1/fetch-models', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n }),\n })\n const data = await res.json()\n if (data.ok && data.models.length > 0) {\n setAiModels(data.models)\n setFetchModelsMsg(`${data.models.length} 个模型`)\n } else {\n setFetchModelsMsg(data.message || '无可用模型')\n }\n } catch (e) {\n setFetchModelsMsg(String(e))\n }\n setFetchingModels(false)\n }}\n disabled={fetchingModels || !(form as any).ai_vision_endpoint}\n className=\"px-2 py-1.5 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 whitespace-nowrap\"\n title=\"从提供商拉取可用模型列表\"\n >\n {fetchingModels ? '...' : '获取模型'}\n \n
\n {aiModels.length > 0 && (\n updateForm({ ai_vision_model: e.target.value } as any)}\n className=\"w-full mt-1 rounded border border-blue-300 px-2 py-1 text-xs font-mono\"\n size={Math.min(aiModels.length + 1, 8)}\n >\n \n {aiModels.map(m => (\n \n ))}\n \n )}\n {fetchModelsMsg && (\n

\n {fetchModelsMsg}\n

\n )}\n
\n
\n \n updateForm({ ai_vision_api_key: e.target.value } as any)}\n placeholder=\"sk-... (支持 {env:VAR_NAME})\"\n className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs font-mono\" />\n
\n
\n \n \n
\n {(form as any).ai_vision_provider === 'custom' && (\n
\n updateForm({ ai_vision_messages_api: e.target.checked } as any)} className=\"rounded\" />\n \n
\n )}\n
\n
\n \n updateForm({ ai_vision_dpi: parseInt(e.target.value) || 150 } as any)}\n min={72} max={300} className=\"w-full rounded border border-gray-300 px-2 py-1.5 text-xs\" />\n
\n
\n {\n setAiVisionTest('testing');\n try {\n const res = await fetch('/api/v1/check-ai-vision', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n endpoint: (form as any).ai_vision_endpoint,\n model: (form as any).ai_vision_model,\n api_key: (form as any).ai_vision_api_key,\n provider: (form as any).ai_vision_provider,\n messages_api: (form as any).ai_vision_messages_api,\n }),\n });\n const data = await res.json();\n setAiVisionTest(data.ok ? 'ok' : 'fail');\n setAiVisionMsg(data.message || data.error || '');\n } catch (e) {\n setAiVisionTest('fail');\n setAiVisionMsg(String(e));\n }\n }}\n disabled={aiVisionTest === 'testing'}\n className=\"px-3 py-1.5 mt-2 text-xs rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50\"\n >\n {aiVisionTest === 'testing' ? '测试中...' : '检测'}\n \n {aiVisionMsg && (\n

\n {aiVisionMsg}\n

\n )}\n

\n 阶段1 从 OCR 文字层提取目录(免费),阶段2 用 AI Vision 从目录页图片提取。\n 支持 OpenAI 兼容 / Gemini / MiniMax 格式。\n

\n
\n updateForm({ bookmark_confirm_enabled: e.target.checked } as any)}\n className=\"rounded\" />\n \n
\n
\n\n {/* ============ 主题 ============ */}\n
\n \n
\n {[\n { key: 'auto', label: '自动' },\n { key: 'light', label: '白昼' },\n { key: 'dark', label: '黑夜' },\n ].map(({ key, label }) => (\n updateForm({ theme: key })}\n className={`flex-1 py-1.5 text-center transition-colors ${\n ((form as any).theme || 'auto') === key\n ? 'bg-blue-500 text-white'\n : 'bg-white text-gray-600 hover:bg-gray-50'\n }`}\n >\n {label}\n \n ))}\n
\n
\n\n {/* ============ 保存按钮 ============ */}\n
\n \n {saving ? '保存中...' : '保存设置'}\n \n \n {updateChecking ? '检测中...' : '检查更新'}\n \n {saveMsg && (\n \n {saveMsg}\n \n )}\n {updateResult && (\n \n {updateResult}\n \n )}\n
\n \n )\n}\n\nexport default React.memo(OCRNetworkSection)\n"}}}}