H1-H10 优化OCR实时体验闭环
这个提交包含在:
@@ -105,11 +105,11 @@ export function mapApiErrorMessage(
|
||||
}
|
||||
|
||||
if (code === "VALID_006") {
|
||||
return "OCR 重试次数已达上限,请稍后人工处理。";
|
||||
return "票据识别重试次数已达上限,请稍后人工处理。";
|
||||
}
|
||||
|
||||
if (code === "VALID_007") {
|
||||
return "OCR 重试过于频繁,请稍后再试。";
|
||||
return "票据识别重试过于频繁,请稍后再试。";
|
||||
}
|
||||
|
||||
if (code === "VALID_001") {
|
||||
@@ -188,7 +188,7 @@ export function mapApiErrorMessage(
|
||||
}
|
||||
|
||||
if (details?.ocr === "disabled") {
|
||||
return "当前环境未开启 OCR,请联系管理员或切换到 mock/tencent。";
|
||||
return "当前环境未开启票据识别,请联系管理员处理。";
|
||||
}
|
||||
|
||||
if (details?.ocr === "missing_required_fields") {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
export type AppEvent = {
|
||||
id?: string;
|
||||
type: string;
|
||||
invoiceId?: string;
|
||||
status?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
};
|
||||
|
||||
type SubscribeAppEventsOptions = {
|
||||
accessToken: string;
|
||||
onEvent: (event: AppEvent) => void;
|
||||
onError?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
function parseSseMessage(message: string): AppEvent | null {
|
||||
const dataLines = message
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice("data:".length).trimStart());
|
||||
|
||||
if (!dataLines.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(dataLines.join("\n")) as AppEvent;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeAppEvents(options: SubscribeAppEventsOptions) {
|
||||
const controller = new AbortController();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await fetch("/api/events", {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
authorization: `Bearer ${options.accessToken}`,
|
||||
accept: "text/event-stream",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`events_stream_unavailable:${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (!controller.signal.aborted) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const chunks = buffer.split(/\r?\n\r?\n/);
|
||||
buffer = chunks.pop() ?? "";
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const event = parseSseMessage(chunk);
|
||||
if (event) {
|
||||
options.onEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
options.onError?.(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => controller.abort();
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export type InvoiceVerificationRecord = {
|
||||
export type InvoicePoolListItem = {
|
||||
id: string;
|
||||
status: InvoicePoolStatus;
|
||||
availabilityReason: string | null;
|
||||
availabilityMessage: string | null;
|
||||
ocrType: string;
|
||||
ocrProvider: string;
|
||||
ocrAttempts: number;
|
||||
@@ -68,11 +70,15 @@ export type UploadInvoiceResponse = {
|
||||
uploadAction?: "create" | "reuse_existing" | "blocked";
|
||||
unavailableReason?: string | null;
|
||||
unavailableMessage?: string | null;
|
||||
availabilityReason?: string | null;
|
||||
availabilityMessage?: string | null;
|
||||
};
|
||||
|
||||
export type InvoicePoolDetail = {
|
||||
id: string;
|
||||
status: InvoicePoolStatus;
|
||||
availabilityReason: string | null;
|
||||
availabilityMessage: string | null;
|
||||
archivedReason: string | null;
|
||||
archivedAt: string | null;
|
||||
ownerEmployeeId: string;
|
||||
@@ -144,6 +150,8 @@ export type InvoiceStatusItem = {
|
||||
status: InvoicePoolStatus;
|
||||
reason: string;
|
||||
message: string;
|
||||
availabilityReason: string | null;
|
||||
availabilityMessage: string | null;
|
||||
invoiceCode: string | null;
|
||||
invoiceNumber: string | null;
|
||||
invoiceDate: string | null;
|
||||
|
||||
在新工单中引用
屏蔽一个用户