feat(backend): refactor OCR client to support HTTP + subprocess dual mode

- runtime-config.js: add baseUrl field to ocr type definition
- default.cjs: add ocr.baseUrl default (empty string = subprocess mode)
- runtime.js: add OCR_BASE_URL env var parsing
- ocr.js: add HTTP mode (when OCR_BASE_URL set) alongside existing subprocess mode
  - resolveOcrMode() detects mode from runtimeConfig.ocr.baseUrl
  - callOcrViaHttp() calls FastAPI endpoints directly with payload
  - warmupOcrViaHttp() calls /health endpoint for HTTP mode warmup
  - callOcrRequest() routes between HTTP and subprocess based on mode
  - All 5 original exports preserved (recognizeTencentCaptcha, etc.)
  - Subprocess mode remains backward compatible (default when no OCR_BASE_URL)
This commit is contained in:
yml
2026-05-19 23:09:30 +08:00
parent f87238b6c0
commit bc5fd4212d
4 changed files with 437 additions and 216 deletions
+78 -1
View File
@@ -22,14 +22,89 @@ export async function batchRecognizeTencentCaptcha(payload) {
}
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 子进程,这里不再维护常驻 worker。
// OCR 改为按次调用 Python 子进程或 HTTP 服务,这里不再维护常驻 worker。
}
// ── Mode Resolution ──────────────────────────────────────────────────────────
function resolveOcrMode() {
if (String(runtimeConfig.ocr.baseUrl || '').trim()) {
return 'http'
}
return 'subprocess'
}
// ── HTTP Mode ─────────────────────────────────────────────────────────────────
async function callOcrViaHttp(action, payload, { timeoutMs }) {
const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '')
const url = `${baseUrl}/ocr/${action}`
let response
try {
response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(timeoutMs),
})
} catch (err) {
if (err instanceof Error && err.name === 'TimeoutError') {
throw new Error(`OCR 请求超时(${timeoutMs}ms),请检查 OCR 服务状态`)
}
throw new Error(`OCR 服务不可用(${baseUrl}),请检查 ocr-worker 容器是否正常运行`)
}
if (!response.ok) {
throw new Error(`OCR 服务返回错误:HTTP ${response.status}`)
}
const result = await response.json()
if (!result || typeof result !== 'object' || Array.isArray(result)) {
throw new Error('OCR 服务返回了无效响应')
}
if (result.code !== 0) {
throw new Error(`OCR 服务返回错误:${result.msg || '未知错误'}`)
}
return result
}
async function warmupOcrViaHttp() {
const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '')
let response
try {
response = await fetch(`${baseUrl}/health`, {
signal: AbortSignal.timeout(30_000),
})
} catch (err) {
if (err instanceof Error && err.name === 'TimeoutError') {
throw new Error(`OCR 服务健康检查超时,请检查 ${baseUrl} 是否可访问`)
}
throw new Error(`OCR 服务不可用(${baseUrl}),请检查 ocr-worker 容器是否正常运行`)
}
if (!response.ok) {
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({
@@ -45,6 +120,8 @@ async function callOcrRequest(action, payload, { timeoutMs = DEFAULT_TIMEOUT_MS
return response
}
// ── Subprocess Mode ───────────────────────────────────────────────────────────
async function runOcrCommand(command, { timeoutMs = DEFAULT_TIMEOUT_MS, input = '' } = {}) {
const projectRoot = resolveOcrProjectRoot()
const workerCommand = resolveWorkerCommand(projectRoot)