@@ -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 } )
}
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
}
}