OINV1 加固真实发票OCR脱敏报告
这个提交包含在:
@@ -17,6 +17,20 @@ export type LiveInvoiceOcrResult = {
|
||||
invoiceDate: string | null;
|
||||
};
|
||||
|
||||
export type SafeLiveInvoiceOcrEvidence = {
|
||||
alias: string;
|
||||
status: LiveInvoiceOcrStatus;
|
||||
durationMs: number;
|
||||
reasonCategory: "available" | "duplicate_or_same_invoice" | "already_used" | "ocr_failed" | "timeout_or_pending" | "unknown";
|
||||
fieldHits: {
|
||||
invoiceId: boolean;
|
||||
invoiceCodeOrNumber: boolean;
|
||||
sellerName: boolean;
|
||||
totalAmount: boolean;
|
||||
invoiceDate: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type UploadInvoiceResponse = {
|
||||
data?: {
|
||||
invoicePoolId?: string | null;
|
||||
@@ -35,6 +49,35 @@ function extractField(detailText: string, label: string) {
|
||||
return match?.[1]?.trim() || null;
|
||||
}
|
||||
|
||||
function categorizeText(text: string) {
|
||||
if (/重复|相同发票|duplicate/i.test(text)) return "duplicate_or_same_invoice";
|
||||
if (/已用于报销|已使用|used/i.test(text)) return "already_used";
|
||||
if (/超时|timeout|pending/i.test(text)) return "timeout_or_pending";
|
||||
if (/失败|识别失败|ocr_failed|failed/i.test(text)) return "ocr_failed";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function categorizeLiveInvoiceOcrResult(result: LiveInvoiceOcrResult): SafeLiveInvoiceOcrEvidence["reasonCategory"] {
|
||||
if (result.status === "available") return "available";
|
||||
return categorizeText(`${result.bannerText}\n${result.detailText}`);
|
||||
}
|
||||
|
||||
export function createSafeLiveInvoiceOcrEvidence(result: LiveInvoiceOcrResult, alias: string): SafeLiveInvoiceOcrEvidence {
|
||||
return {
|
||||
alias,
|
||||
status: result.status,
|
||||
durationMs: result.durationMs,
|
||||
reasonCategory: categorizeLiveInvoiceOcrResult(result),
|
||||
fieldHits: {
|
||||
invoiceId: Boolean(result.invoiceId),
|
||||
invoiceCodeOrNumber: Boolean(result.invoiceCode || result.invoiceNumber),
|
||||
sellerName: Boolean(result.sellerName),
|
||||
totalAmount: Boolean(result.totalAmount && result.totalAmount !== "-"),
|
||||
invoiceDate: Boolean(result.invoiceDate),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveLiveOcrUser(fallback: E2EUser): E2EUser {
|
||||
const identifier =
|
||||
process.env.PLAYWRIGHT_OCR_LIVE_IDENTIFIER?.trim() ||
|
||||
@@ -124,14 +167,13 @@ export async function uploadLiveInvoiceAndWait(page: Page, invoiceFilePath: stri
|
||||
} catch (error) {
|
||||
const detailText = await detailPanel.innerText().catch(() => "");
|
||||
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "");
|
||||
const invoiceId = uploadedInvoiceId ?? extractField(detailText, "票据ID:");
|
||||
const reasonCategory = categorizeText(`${bannerText}\n${detailText}`);
|
||||
throw new Error(
|
||||
[
|
||||
"live 腾讯 OCR 等待终态超时",
|
||||
`invoiceId=${invoiceId ?? "unknown"}`,
|
||||
`invoiceIdPresent=${Boolean(uploadedInvoiceId ?? extractField(detailText, "票据ID:"))}`,
|
||||
`timeoutMs=${timeout}`,
|
||||
`detail=${detailText || "<empty>"}`,
|
||||
`banner=${bannerText || "<empty>"}`,
|
||||
`reasonCategory=${reasonCategory}`,
|
||||
`cause=${error instanceof Error ? error.message : String(error)}`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
在新工单中引用
屏蔽一个用户