init
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import readline from 'node:readline'
|
||||
import process from 'node:process'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 60_000
|
||||
|
||||
let workerPromise = null
|
||||
let requestSequence = 0
|
||||
const pendingRequests = new Map()
|
||||
let stderrBuffer = []
|
||||
|
||||
export async function recognizeTencentCaptcha(payload) {
|
||||
return callOcrWorker('recognize', payload)
|
||||
}
|
||||
|
||||
export async function batchRecognizeTencentCaptcha(payload) {
|
||||
return callOcrWorker('batch', payload, { timeoutMs: 180_000 })
|
||||
}
|
||||
|
||||
export async function warmupLocalOcrWorker() {
|
||||
await ensureOcrWorker()
|
||||
}
|
||||
|
||||
export async function closeLocalOcrWorker() {
|
||||
const worker = await workerPromise?.catch(() => null)
|
||||
|
||||
if (!worker) {
|
||||
workerPromise = null
|
||||
return
|
||||
}
|
||||
|
||||
worker.child.kill()
|
||||
workerPromise = null
|
||||
}
|
||||
|
||||
async function callOcrWorker(action, payload, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||
const worker = await ensureOcrWorker()
|
||||
const requestId = `ocr-${Date.now()}-${++requestSequence}`
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pendingRequests.delete(requestId)
|
||||
worker.child.kill()
|
||||
reject(new Error('本地 OCR 识别超时,请确认 uv 环境和 OCR worker 依赖正常'))
|
||||
}, timeoutMs)
|
||||
|
||||
pendingRequests.set(requestId, {
|
||||
resolve: (value) => {
|
||||
clearTimeout(timer)
|
||||
resolve(value)
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
|
||||
worker.child.stdin.write(
|
||||
`${JSON.stringify({ id: requestId, action, payload: payload || {} })}\n`,
|
||||
'utf8',
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureOcrWorker() {
|
||||
if (!workerPromise) {
|
||||
workerPromise = createOcrWorker().catch((error) => {
|
||||
workerPromise = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return workerPromise
|
||||
}
|
||||
|
||||
function createOcrWorker() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const projectRoot = resolveOcrProjectRoot()
|
||||
stderrBuffer = []
|
||||
|
||||
const child = spawn('uv', ['run', 'ocr-worker', 'worker'], {
|
||||
cwd: projectRoot,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: process.env,
|
||||
})
|
||||
child.stdin.setDefaultEncoding('utf8')
|
||||
|
||||
const stdoutReader = readline.createInterface({ input: child.stdout })
|
||||
const worker = { child, stdoutReader }
|
||||
|
||||
const readyTimer = setTimeout(() => {
|
||||
reject(new Error(`本地 OCR worker 启动超时,请检查 ${projectRoot} 下是否已执行 uv sync`))
|
||||
child.kill()
|
||||
}, 15_000)
|
||||
|
||||
let ready = false
|
||||
|
||||
stdoutReader.on('line', (line) => {
|
||||
const message = tryParseJson(line)
|
||||
|
||||
if (!message || typeof message !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type === 'ready') {
|
||||
if (!ready) {
|
||||
ready = true
|
||||
clearTimeout(readyTimer)
|
||||
resolve(worker)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type !== 'response') {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = String(message.id || '')
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingRequests.delete(requestId)
|
||||
pending.resolve(message.payload)
|
||||
})
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
const text = String(chunk || '').trim()
|
||||
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
stderrBuffer.push(text)
|
||||
if (stderrBuffer.length > 20) {
|
||||
stderrBuffer = stderrBuffer.slice(-20)
|
||||
}
|
||||
})
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(readyTimer)
|
||||
const workerError = buildWorkerError(error)
|
||||
failAllPendingRequests(workerError)
|
||||
if (!ready) {
|
||||
reject(workerError)
|
||||
}
|
||||
workerPromise = null
|
||||
})
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
clearTimeout(readyTimer)
|
||||
const workerError = new Error(
|
||||
buildWorkerExitMessage({
|
||||
code,
|
||||
signal,
|
||||
projectRoot,
|
||||
stderrText: stderrBuffer.join('\n'),
|
||||
}),
|
||||
)
|
||||
failAllPendingRequests(workerError)
|
||||
if (!ready) {
|
||||
reject(workerError)
|
||||
}
|
||||
workerPromise = null
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function failAllPendingRequests(error) {
|
||||
for (const [requestId, pending] of pendingRequests.entries()) {
|
||||
pendingRequests.delete(requestId)
|
||||
pending.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOcrProjectRoot() {
|
||||
return String(runtimeConfig.ocr.projectRoot || '').trim()
|
||||
}
|
||||
|
||||
function buildWorkerError(error) {
|
||||
if (error instanceof Error && error.message.includes('spawn uv ENOENT')) {
|
||||
return new Error('未找到 uv 命令,请先安装 uv,并确认它在 PATH 里')
|
||||
}
|
||||
|
||||
return new Error(
|
||||
`本地 OCR worker 启动失败: ${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 = `请先执行: cd ${projectRoot} && uv sync`
|
||||
const stderrHint = stderrText ? `\n${stderrText}` : ''
|
||||
|
||||
return `本地 OCR worker 已退出(${reason})。${installHint}${stderrHint}`
|
||||
}
|
||||
|
||||
function tryParseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user