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:
@@ -19,7 +19,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ocr: {
|
ocr: {
|
||||||
projectRoot: path.resolve(__dirname, '../subservices/ocr-worker'),
|
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -128,11 +128,6 @@ function applyEnvOverrides(baseConfig) {
|
|||||||
if (sessionDebug !== null) {
|
if (sessionDebug !== null) {
|
||||||
nextConfig.session.debug = sessionDebug;
|
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();
|
const ocrBaseUrl = String(process.env.OCR_BASE_URL || "").trim();
|
||||||
if (ocrBaseUrl) {
|
if (ocrBaseUrl) {
|
||||||
nextConfig.ocr.baseUrl = ocrBaseUrl;
|
nextConfig.ocr.baseUrl = ocrBaseUrl;
|
||||||
|
|||||||
@@ -1,52 +1,38 @@
|
|||||||
// @ts-nocheck
|
// @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'
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 60_000
|
const DEFAULT_TIMEOUT_MS = 60_000
|
||||||
|
|
||||||
export async function recognizeTencentCaptcha(payload) {
|
export async function recognizeTencentCaptcha(payload) {
|
||||||
return callOcrRequest('recognize', payload)
|
return callOcrViaHttp('recognize', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recognizeImageCaptcha(payload) {
|
export async function recognizeImageCaptcha(payload) {
|
||||||
return callOcrRequest('recognize', payload)
|
return callOcrViaHttp('recognize', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function batchRecognizeTencentCaptcha(payload) {
|
export async function batchRecognizeTencentCaptcha(payload) {
|
||||||
return callOcrRequest('batch', payload, { timeoutMs: 180_000 })
|
return callOcrViaHttp('batch', payload, { timeoutMs: 180_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function warmupLocalOcrWorker() {
|
export async function warmupLocalOcrWorker() {
|
||||||
const mode = resolveOcrMode()
|
await warmupOcrViaHttp()
|
||||||
if (mode === 'http') {
|
|
||||||
await warmupOcrViaHttp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await runOcrCommand('healthcheck', { timeoutMs: 30_000 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeLocalOcrWorker() {
|
export async function closeLocalOcrWorker() {
|
||||||
// OCR 改为按次调用 Python 子进程或 HTTP 服务,这里不再维护常驻 worker。
|
// HTTP 模式无持久连接,无需关闭。
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Mode Resolution ──────────────────────────────────────────────────────────
|
// ── HTTP Call ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function resolveOcrMode() {
|
async function callOcrViaHttp(action, payload, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||||
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 baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '')
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error('OCR_BASE_URL 未配置,无法调用 OCR 服务')
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${baseUrl}/ocr/${action}`
|
const url = `${baseUrl}/ocr/${action}`
|
||||||
|
|
||||||
let response
|
let response
|
||||||
@@ -81,6 +67,11 @@ async function callOcrViaHttp(action, payload, { timeoutMs }) {
|
|||||||
|
|
||||||
async function warmupOcrViaHttp() {
|
async function warmupOcrViaHttp() {
|
||||||
const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '')
|
const baseUrl = String(runtimeConfig.ocr.baseUrl).trim().replace(/\/+$/, '')
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error('OCR_BASE_URL 未配置,无法启动 OCR 健康检查')
|
||||||
|
}
|
||||||
|
|
||||||
let response
|
let response
|
||||||
try {
|
try {
|
||||||
response = await fetch(`${baseUrl}/health`, {
|
response = await fetch(`${baseUrl}/health`, {
|
||||||
@@ -96,185 +87,3 @@ async function warmupOcrViaHttp() {
|
|||||||
throw new Error(`OCR 服务健康检查失败:HTTP ${response.status}`)
|
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
|
* debug: boolean
|
||||||
* }
|
* }
|
||||||
* ocr: {
|
* ocr: {
|
||||||
* projectRoot: string
|
|
||||||
* baseUrl: string
|
* baseUrl: string
|
||||||
* }
|
* }
|
||||||
* data: {
|
* data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user