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(
[