refactor(ocr): remove subprocess mode — HTTP only
- ocr.js: Removed all subprocess/spawn logic (spawn, fs, path, process imports, resolveWorkerCommand, buildPythonPath, buildWorkerError, callOcrViaSubprocess, resolveOcrMode, runOcrCommand, etc.) HTTP is now the only mode. Added validation when OCR_BASE_URL is not configured. ~281 lines → ~80 lines. - default.cjs: Removed projectRoot from ocr config (kept path import for data.root) - runtime.js: Removed OCR_PROJECT_ROOT env var parsing - runtime-config.js: Removed projectRoot from ocr type definition All 5 exports preserved: recognizeTencentCaptcha, recognizeImageCaptcha, batchRecognizeTencentCaptcha, warmupLocalOcrWorker, closeLocalOcrWorker
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export {};
|
||||
* debug: boolean
|
||||
* }
|
||||
* ocr: {
|
||||
* projectRoot: string
|
||||
* baseUrl: string
|
||||
* }
|
||||
* data: {
|
||||
|
||||
Reference in New Issue
Block a user