#!/usr/bin/env node import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; process.noDeprecation = true; const require = createRequire(import.meta.url); const { OCR_PDF_MAX_BYTES, buildMissingRequiredOcrError, runOcrForInvoice, sanitizeOcrResultForPersistence, } = require("../../server/src/services/ocr"); const PDF_MIME_TYPE = "application/pdf"; const PDF_EOF_MARKER = Buffer.from("%%EOF", "ascii"); const SAFE_ALIAS_PATTERN = /^SAMPLE-PDF-[A-Z0-9_-]{1,48}$/u; const SAFE_TOKEN_PATTERN = /^[A-Za-z0-9_.-]{1,96}$/u; const SAFE_FIELD_NAMES = [ "invoiceCode", "invoiceNumber", "invoiceDate", "amount", "taxAmount", "totalAmount", "sellerName", "buyerName", ]; class LiveOcrSampleError extends Error { constructor(code) { super(code); this.name = "LiveOcrSampleError"; this.code = code; } } function parseArgs(argv) { let alias = ""; for (const arg of argv) { if (arg.startsWith("--alias=")) { alias = arg.slice("--alias=".length).trim().toUpperCase(); continue; } throw new LiveOcrSampleError("unsupported_argument"); } if (!SAFE_ALIAS_PATTERN.test(alias)) { throw new LiveOcrSampleError("sample_alias_invalid"); } return { alias }; } function getTencentConfig() { return { ocrProvider: "tencent", tencentSecretId: process.env.TENCENT_SECRET_ID || "", tencentSecretKey: process.env.TENCENT_SECRET_KEY || "", tencentOcrRegion: process.env.TENCENT_OCR_REGION || "ap-beijing", ocrTimeoutMs: Number(process.env.OCR_TIMEOUT_MS || 5000), }; } function validateTencentConfig(config) { if (!config?.tencentSecretId || !config?.tencentSecretKey) { throw new LiveOcrSampleError("ocr_credentials_missing"); } if (config.ocrProvider !== "tencent") { throw new LiveOcrSampleError("ocr_provider_invalid"); } } async function readBoundedStdin(input, maxBytes = OCR_PDF_MAX_BYTES) { const chunks = []; let totalBytes = 0; for await (const chunk of input) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); totalBytes += buffer.length; if (totalBytes > maxBytes) { throw new LiveOcrSampleError("sample_too_large"); } chunks.push(buffer); } if (totalBytes === 0) { throw new LiveOcrSampleError("sample_empty"); } return Buffer.concat(chunks, totalBytes); } function validatePdfBuffer(buffer) { if (!Buffer.isBuffer(buffer) || buffer.length === 0) { throw new LiveOcrSampleError("sample_empty"); } if (buffer.length > OCR_PDF_MAX_BYTES) { throw new LiveOcrSampleError("sample_too_large"); } const header = buffer.toString("ascii", 0, Math.min(buffer.length, 16)); if (!/^%PDF-(?:1\.[0-7]|2\.0)(?:\r\n|\r|\n)/u.test(header)) { throw new LiveOcrSampleError("sample_pdf_invalid"); } let lastContentIndex = buffer.length - 1; while ( lastContentIndex >= 0 && [0x09, 0x0a, 0x0c, 0x0d, 0x20].includes(buffer[lastContentIndex]) ) { lastContentIndex -= 1; } const markerStart = lastContentIndex - PDF_EOF_MARKER.length + 1; if ( markerStart < 0 || !buffer.subarray(markerStart, lastContentIndex + 1).equals(PDF_EOF_MARKER) ) { throw new LiveOcrSampleError("sample_pdf_invalid"); } } function safeToken(value) { return typeof value === "string" && SAFE_TOKEN_PATTERN.test(value) ? value : null; } function safeErrorCode(error) { const code = typeof error?.code === "string" ? error.code : "provider_failed"; return SAFE_TOKEN_PATTERN.test(code) ? code : "provider_failed"; } function buildFieldHits(normalized) { return Object.fromEntries( SAFE_FIELD_NAMES.map((field) => [field, normalized?.[field] != null && normalized[field] !== ""]), ); } function buildErrorSummary(alias, error) { return { alias: SAFE_ALIAS_PATTERN.test(alias) ? alias : "SAMPLE-PDF-UNKNOWN", provider: "tencent", status: "HOLD", transportMode: "pdf_native", errorCode: safeErrorCode(error), }; } async function runLiveSampleCheck({ alias, buffer, config, runOcr = runOcrForInvoice, now = () => Date.now(), }) { if (!SAFE_ALIAS_PATTERN.test(alias)) { throw new LiveOcrSampleError("sample_alias_invalid"); } validatePdfBuffer(buffer); validateTencentConfig(config); const startedAt = now(); const providerResult = await runOcr({ config, ocrType: "vat", imageBase64: buffer.toString("base64"), sourceMimeType: PDF_MIME_TYPE, }); const normalized = sanitizeOcrResultForPersistence("tencent", providerResult); const qualityError = buildMissingRequiredOcrError("vat", normalized); return { alias, provider: "tencent", status: qualityError ? "HOLD" : "PASS", transportMode: normalized.transportMode === "pdf_native" ? "pdf_native" : null, elapsedMs: Math.max(0, now() - startedAt), documentCategory: safeToken(normalized.documentCategory), documentSubtype: safeToken(normalized.documentSubtype), documentCount: Number.isInteger(normalized.documentCount) ? normalized.documentCount : null, fieldHits: buildFieldHits(normalized), missingRequiredFields: qualityError?.startsWith("missing_required_fields:") ? qualityError.slice("missing_required_fields:".length).split(",").filter(Boolean) : [], errorCode: qualityError && !qualityError.startsWith("missing_required_fields:") ? safeToken(qualityError) || "invoice_quality_failed" : null, }; } async function main() { let alias = "SAMPLE-PDF-UNKNOWN"; try { ({ alias } = parseArgs(process.argv.slice(2))); const buffer = await readBoundedStdin(process.stdin); const summary = await runLiveSampleCheck({ alias, buffer, config: getTencentConfig(), }); process.stdout.write(`${JSON.stringify(summary)}\n`); if (summary.status !== "PASS") { process.exitCode = 1; } } catch (error) { process.stdout.write(`${JSON.stringify(buildErrorSummary(alias, error))}\n`); process.exitCode = 1; } } const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; if (import.meta.url === invokedPath) { await main(); } export { LiveOcrSampleError, buildErrorSummary, parseArgs, readBoundedStdin, runLiveSampleCheck, validatePdfBuffer, };