From cc6a162bbe9a614fba7069b0f8691444c1133156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84GitHub=E7=94=A8=E6=88=B7=E5=90=8D?= <3457189498@qq.com> Date: Fri, 28 Aug 2026 18:20:29 +0800 Subject: [PATCH] Harden work-order confirm after CC success and clean instruction body display. Avoids false 500 when local sync/log fails post-CC, and strips email/quote noise from customer instruction panels. Co-authored-by: Cursor --- src/components/CcCustomerInstructionPanel.tsx | 4 +- src/components/MailBusinessSummary.tsx | 8 +- .../confirm/OpsInstructionConfirmView.tsx | 8 +- src/services/import/confirm-work-order.ts | 86 +++++++++++++------ .../import/instruction-import-state.ts | 11 ++- .../parse/instruction-execution-state.ts | 12 ++- src/services/parse/work-order-fields.ts | 29 +++++++ tests/unit/work-order-fields.test.ts | 26 ++++++ 8 files changed, 140 insertions(+), 44 deletions(-) diff --git a/src/components/CcCustomerInstructionPanel.tsx b/src/components/CcCustomerInstructionPanel.tsx index 29d5fb6..9ac9691 100644 --- a/src/components/CcCustomerInstructionPanel.tsx +++ b/src/components/CcCustomerInstructionPanel.tsx @@ -1,6 +1,7 @@ "use client"; import { Descriptions } from "antd"; +import { cleanCustomerInstructionDisplayBody } from "@/services/parse/work-order-fields"; export type CcCustomerInstructionValue = { roundAt?: string; @@ -14,6 +15,7 @@ export function CcCustomerInstructionPanel({ }: { value: CcCustomerInstructionValue; }) { + const body = cleanCustomerInstructionDisplayBody(value.body || ""); return (
- {value.body || "(无正文)"} + {body || "(无正文)"}
); diff --git a/src/components/MailBusinessSummary.tsx b/src/components/MailBusinessSummary.tsx index a7b50fb..52f1a70 100644 --- a/src/components/MailBusinessSummary.tsx +++ b/src/components/MailBusinessSummary.tsx @@ -594,11 +594,7 @@ export function MailBusinessSummary({ shipments: unitShipments, transferPairs: unitPairs, }); - const roundBody = (unit.segmentText || "") - .replace(/ /gi, " ") - .replace(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi, "") - .replace(/\s+/g, " ") - .trim(); + const roundBody = unit.segmentText || ""; body = ( <> { const woDraft = buildCcWorkOrderFormValue({ @@ -345,7 +341,7 @@ export function TransferConfirmView({ roundAt: unit.roundAt?.replace(/ /gi, " "), roundFrom: unit.roundFrom, subject: unit.segmentSubject, - body: roundBody || unit.segmentText || "", + body: roundBody, }} /> a.filename), lineage, }); - assertTransition("IMPORTING", status); + const nextStatus: MailStatus = canTransition("IMPORTING", status) + ? status + : "PARTIAL_SUCCESS"; + assertTransition("IMPORTING", nextStatus); await prisma.$transaction(async (tx) => { await tx.parseResult.update({ where: { mailId: input.mailId }, @@ -109,11 +112,11 @@ export async function markInstructionImportedAndResolveMail(input: { await tx.mailMessage.update({ where: { id: input.mailId }, data: { - status, + status: nextStatus, lastError: input.successLastError ?? null, version: { increment: 1 }, }, }); }); - return status; + return nextStatus; } diff --git a/src/services/parse/instruction-execution-state.ts b/src/services/parse/instruction-execution-state.ts index 5384f12..99262af 100644 --- a/src/services/parse/instruction-execution-state.ts +++ b/src/services/parse/instruction-execution-state.ts @@ -117,11 +117,19 @@ export function resolveMailStatusAfterInstructionImport(input: { lineage?: unknown; }): MailStatus { const units = listActionableInstructionUnits(input); - if (!units.length) return input.currentStatus; + if (!units.length) { + // 从 IMPORTING 收尾时不得原样返回 IMPORTING / PENDING_CONFIRM(状态机会炸) + if (input.currentStatus === "IMPORTING") return "SUCCESS"; + return input.currentStatus; + } const imported = units.filter((u) => isInstructionImported(input.lineage, u.id), ); - if (imported.length <= 0) return "PENDING_CONFIRM"; + if (imported.length <= 0) { + // 指令 id 漂移时可能暂时对不上;IMPORTING 收尾一律按部分成功,避免 IllegalTransition → 500 + if (input.currentStatus === "IMPORTING") return "PARTIAL_SUCCESS"; + return "PENDING_CONFIRM"; + } if (imported.length >= units.length) { // 全部只是 mock 成功 → PARTIAL,允许再进 IMPORTING 写真实 CC const allMock = imported.every((u) => diff --git a/src/services/parse/work-order-fields.ts b/src/services/parse/work-order-fields.ts index 6f8a065..7b278b1 100644 --- a/src/services/parse/work-order-fields.ts +++ b/src/services/parse/work-order-fields.ts @@ -183,6 +183,35 @@ export function stripWorkOrderBodyNoise(text: string): string { .trim(); } +const INLINE_EMAIL_RE = + /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g; + +/** + * 客户指令 / 时间轮正文展示:去掉「在…写道」引用头、邮箱与签名噪声。 + * 供详情面板回显,避免把邮件壳层当业务内容。 + */ +export function cleanCustomerInstructionDisplayBody(text: string): string { + let s = stripWorkOrderBodyNoise(text || ""); + // 再扫一遍:引用头若与正文同行、或残留邮箱 + s = s + .replace(/^(?:>|>)?\s*在\s*\d{4}[-/.年][^\n]{0,160}写道[::]?\s*/gim, "") + .replace(/在\s*\d{4}[-/.年][^\n]{0,160}写道[::]?/g, "") + .replace(INLINE_EMAIL_RE, "") + .replace(/<\s*>/g, "") + .replace(/[""\u201c\u201d]\s*[""\u201c\u201d]/g, "") + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{3,}/g, "\n\n") + .replace(/[ \t]{2,}/g, " ") + .trim(); + // 去掉因删邮箱留下的空行 / 纯符号行 + return s + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !isWorkOrderNoiseLine(line) && !/^[,,;;.。]+$/.test(line)) + .join("\n") + .trim(); +} + function isNumberedOpsLine(text: string): boolean { const t = (text || "").trim(); if (!t) return false; diff --git a/tests/unit/work-order-fields.test.ts b/tests/unit/work-order-fields.test.ts index b3cf492..ac1c7f6 100644 --- a/tests/unit/work-order-fields.test.ts +++ b/tests/unit/work-order-fields.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + cleanCustomerInstructionDisplayBody, extractWorkOrderFormFields, parseWorkOrderReferCode, parseWorkOrderRefNo, @@ -143,6 +144,31 @@ describe("stripWorkOrderBodyNoise 邮箱/附件栏", () => { expect(cleaned).not.toMatch(/km16|网易灵犀|签名由/i); }); + it("cleanCustomerInstructionDisplayBody drops quote header and emails", () => { + const raw = [ + '在 2026-05-09 18:00:16, "luo" 写道:', + "好的", + "luo", + "luo@usasinogroup.com", + ].join("\n"); + const cleaned = cleanCustomerInstructionDisplayBody(raw); + expect(cleaned).toBe("好的"); + expect(cleaned).not.toMatch(/写道|@usasinogroup|luo@/i); + }); + + it("cleanCustomerInstructionDisplayBody keeps reject reason without shell", () => { + const raw = [ + '在 2026-05-09 10:52:25, "luo" 写道:', + "抱歉 仓库产能有限 该柜子我们目前无法接收 请您谅解", + "luo", + "luo@usasinogroup.com", + ].join("\n"); + const cleaned = cleanCustomerInstructionDisplayBody(raw); + expect(cleaned).toContain("产能有限"); + expect(cleaned).toContain("无法接收"); + expect(cleaned).not.toMatch(/写道|@usasinogroup/i); + }); + it("keeps 注意 remark but strips trailing signature email", () => { const { content, remark } = splitWorkOrderContentAndRemark( [