diff --git a/apps/backend/config/default.cjs b/apps/backend/config/default.cjs index 251e516c..2fb818a8 100644 --- a/apps/backend/config/default.cjs +++ b/apps/backend/config/default.cjs @@ -19,7 +19,6 @@ module.exports = { }, ocr: { - projectRoot: path.resolve(__dirname, '../subservices/ocr-worker'), baseUrl: '', }, diff --git a/apps/backend/src/config/runtime.js b/apps/backend/src/config/runtime.js index 215cf5d1..b680fdaf 100644 --- a/apps/backend/src/config/runtime.js +++ b/apps/backend/src/config/runtime.js @@ -128,11 +128,6 @@ function applyEnvOverrides(baseConfig) { if (sessionDebug !== null) { nextConfig.session.debug = sessionDebug; } - const ocrProjectRoot = String(process.env.OCR_PROJECT_ROOT || "").trim(); - if (ocrProjectRoot) { - nextConfig.ocr.projectRoot = ocrProjectRoot; - } - const ocrBaseUrl = String(process.env.OCR_BASE_URL || "").trim(); if (ocrBaseUrl) { nextConfig.ocr.baseUrl = ocrBaseUrl; diff --git a/apps/backend/src/services/session/ocr.js b/apps/backend/src/services/session/ocr.js index cb6f4914..48c485f3 100644 --- a/apps/backend/src/services/session/ocr.js +++ b/apps/backend/src/services/session/ocr.js @@ -1,52 +1,38 @@ // @ts-nocheck -import { spawn } from 'node:child_process' -import fs from 'node:fs' -import path from 'node:path' -import process from 'node:process' - import { runtimeConfig } from '../../config/runtime.js' const DEFAULT_TIMEOUT_MS = 60_000 export async function recognizeTencentCaptcha(payload) { - return callOcrRequest('recognize', payload) + return callOcrViaHttp('recognize', payload) } export async function recognizeImageCaptcha(payload) { - return callOcrRequest('recognize', payload) + return callOcrViaHttp('recognize', payload) } export async function batchRecognizeTencentCaptcha(payload) { - return callOcrRequest('batch', payload, { timeoutMs: 180_000 }) + return callOcrViaHttp('batch', payload, { timeoutMs: 180_000 }) } export async function warmupLocalOcrWorker() { - const mode = resolveOcrMode() - if (mode === 'http') { - await warmupOcrViaHttp() - return - } - await runOcrCommand('healthcheck', { timeoutMs: 30_000 }) + await warmupOcrViaHttp() } export async function closeLocalOcrWorker() { - // OCR 改为按次调用 Python 子进程或 HTTP 服务,这里不再维护常驻 worker。 + // HTTP 模式无持久连接,无需关闭。 } -// ── Mode Resolution ────────────────────────────────────────────────────────── +// ── HTTP Call ─────────────────────────────────────────────────────────────── -function resolveOcrMode() { - if (String(runtimeConfig.ocr.baseUrl || '').trim()) { - return 'http' - } - return 'subprocess' -} - -// ── HTTP Mode ───────────────────────────────────────────────────────────────── - -async function callOcrViaHttp(action, payload, { timeoutMs }) { +async function callOcrViaHttp(action, payload, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '') + + if (!baseUrl) { + throw new Error('OCR_BASE_URL 未配置,无法调用 OCR 服务') + } + const url = `${baseUrl}/ocr/${action}` let response @@ -81,6 +67,11 @@ async function callOcrViaHttp(action, payload, { timeoutMs }) { async function warmupOcrViaHttp() { const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '') + + if (!baseUrl) { + throw new Error('OCR_BASE_URL 未配置,无法启动 OCR 健康检查') + } + let response try { response = await fetch(`${baseUrl}/health`, { @@ -96,185 +87,3 @@ async function warmupOcrViaHttp() { throw new Error(`OCR 服务健康检查失败:HTTP ${response.status}`) } } - -// ── Request Router ──────────────────────────────────────────────────────────── - -async function callOcrRequest(action, payload, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { - const mode = resolveOcrMode() - if (mode === 'http') { - return await callOcrViaHttp(action, payload, { timeoutMs }) - } - // subprocess mode - const response = await runOcrCommand('request', { - timeoutMs, - input: JSON.stringify({ - action, - payload: payload || {}, - }), - }) - - if (!response || typeof response !== 'object' || Array.isArray(response)) { - throw new Error('本地 OCR 返回了无效响应') - } - - return response -} - -// ── Subprocess Mode ─────────────────────────────────────────────────────────── - -async function runOcrCommand(command, { timeoutMs = DEFAULT_TIMEOUT_MS, input = '' } = {}) { - const projectRoot = resolveOcrProjectRoot() - const workerCommand = resolveWorkerCommand(projectRoot) - const env = { - ...process.env, - PYTHONUNBUFFERED: '1', - PYTHONPATH: buildPythonPath(path.join(projectRoot, 'src')), - } - - return await new Promise((resolve, reject) => { - const child = spawn(workerCommand.command, [...workerCommand.args, command], { - cwd: projectRoot, - stdio: ['pipe', 'pipe', 'pipe'], - env, - }) - - let stdout = '' - let stderr = '' - let settled = false - let timedOut = false - - const timer = setTimeout(() => { - timedOut = true - child.kill() - }, timeoutMs) - - child.stdout.on('data', (chunk) => { - stdout += String(chunk || '') - }) - - child.stderr.on('data', (chunk) => { - stderr += String(chunk || '') - }) - - child.on('error', (error) => { - if (settled) { - return - } - - settled = true - clearTimeout(timer) - reject(buildWorkerError(error)) - }) - - child.on('close', (code, signal) => { - if (settled) { - return - } - - settled = true - clearTimeout(timer) - - if (timedOut) { - reject(new Error(`本地 OCR 执行超时,请检查 ${projectRoot} 的 Python 依赖是否已经准备完成`)) - return - } - - if (signal || code !== 0) { - reject( - new Error( - buildWorkerExitMessage({ - code, - signal, - projectRoot, - stderrText: stderr.trim(), - }), - ), - ) - return - } - - if (command === 'healthcheck') { - resolve(null) - return - } - - const parsed = tryParseJson(stdout.trim()) - if (!parsed || typeof parsed !== 'object') { - reject(new Error(`本地 OCR 返回无法解析的结果${stderr.trim() ? `\n${stderr.trim()}` : ''}`)) - return - } - - resolve(parsed) - }) - - if (input) { - child.stdin.end(`${input}\n`, 'utf8') - return - } - - child.stdin.end() - }) -} - -function resolveOcrProjectRoot() { - return String(runtimeConfig.ocr.projectRoot || '').trim() -} - -function resolveWorkerCommand(projectRoot) { - const venvPython = path.join(projectRoot, '.venv', 'bin', 'python') - - if (fs.existsSync(venvPython)) { - return { - command: venvPython, - args: ['-u', '-m', 'ocr_worker.cli'], - } - } - - return { - command: 'python3', - args: ['-u', '-m', 'ocr_worker.cli'], - } -} - -function buildPythonPath(srcPath) { - const current = String(process.env.PYTHONPATH || '').trim() - return current ? `${srcPath}${path.delimiter}${current}` : srcPath -} - -function buildWorkerError(error) { - if ( - error instanceof Error && - (error.message.includes('spawn python3 ENOENT') || - error.message.includes('spawn ') && error.message.includes('python')) - ) { - const projectRoot = resolveOcrProjectRoot() - return new Error( - `无法启动本地 OCR。请检查 Python 是否可用,并确认 OCR_PROJECT_ROOT 指向的目录存在:${projectRoot}`, - ) - } - - return new Error( - `本地 OCR 启动失败: ${error instanceof Error ? error.message : String(error || '未知错误')}`, - ) -} - -function buildWorkerExitMessage({ code, signal, projectRoot, stderrText }) { - const reason = signal - ? `signal ${signal}` - : typeof code === 'number' - ? `exit code ${code}` - : 'unknown reason' - - const installHint = `请检查 ${projectRoot} 的 Python 依赖是否已安装;本地源码运行请先执行 uv sync,Docker 请重建 backend 镜像` - const stderrHint = stderrText ? `\n${stderrText}` : '' - - return `本地 OCR 已退出(${reason})。${installHint}${stderrHint}` -} - -function tryParseJson(text) { - try { - return JSON.parse(text) - } catch { - return null - } -} diff --git a/apps/backend/src/types/runtime-config.js b/apps/backend/src/types/runtime-config.js index 64c1f0bc..6a68bbce 100644 --- a/apps/backend/src/types/runtime-config.js +++ b/apps/backend/src/types/runtime-config.js @@ -45,7 +45,6 @@ export {}; * debug: boolean * } * ocr: { - * projectRoot: string * baseUrl: string * } * data: {