|
|
import { describe, expect, it } from "vitest";
|
|
|
import {
|
|
|
detectUiKindsFromText,
|
|
|
extractForecastInstructionText,
|
|
|
extractMailInstructions,
|
|
|
parseInstructionRoundAtMs,
|
|
|
splitBodySegments,
|
|
|
stripMobileMailChrome,
|
|
|
extractForwardChromeMeta,
|
|
|
} from "@/services/parse/split-instructions";
|
|
|
import { matchKeywordRules } from "@/services/parse/instruction-lexicon";
|
|
|
|
|
|
const MAIL4_SUBJECT =
|
|
|
"Fw: \u8f6c\u53d1\uff1a\u65b0\u589e\u9884\u62a5136\uff1a\u65b0\u8fb0\u6cfd+WHLC027G597465+WHSU8127240+\u6d1b\u6749\u77f6+40HQ+EDT2026.04-25 ETA2026.05-15\u8239\u540d\u822a\u6b21HMM EMERALD 013E+\u63d0\u62c6\u6d3e\u7ec4\u5408\u67dc-\u4e0d\u5e26\u6258\u67b6";
|
|
|
|
|
|
const MAIL4_BODY = [
|
|
|
"Dear,",
|
|
|
"\u539f\u7bb1\u53f7YT2604021091=FBA199R49LD6-MDW2=76\u4ef6\u64cd\u4f5c\u6307\u4ee4",
|
|
|
"\u6362\u6807\u540e\u5355\u53f7\uff1aFBA19HW52S0L-2G1MCB6X-HIA1=76\u4ef6",
|
|
|
"1\uff1a\u8986\u76d6\u8d34FBA\u6807\u7b7e\uff0c\u4e00\u7bb1\u8d34\u4e24\u5f20",
|
|
|
"3\uff1a\u8d34\u597d\u62cd\u7167\u56de\u4f20",
|
|
|
].join("\n");
|
|
|
|
|
|
const MAIL4_FILES = [
|
|
|
"YT2604021091=76\u4ef6\u6362\u6807\u8d70HIA1\u5361\u6d3e\u51fa\u5e93\u64cd\u4f5c\u6307\u4ee4.xlsx",
|
|
|
"FBA19HW52S0L-2G1MCB6X-HIA1.pdf",
|
|
|
];
|
|
|
|
|
|
const MULTI_DATE_BODY = [
|
|
|
"\u65b0\u589e\u8f6c\u4ed3\uff0c\u8bf7\u7559\u610f",
|
|
|
"\u67dc\u53f7\uff1aMATU2745683",
|
|
|
"------------------ \u539f\u59cb\u7535\u5b50\u90ae\u4ef6 ------------------",
|
|
|
"\u53d1\u4ef6\u4eba\uff1aa@b.com",
|
|
|
"\u4e3b\u9898\uff1a\u65b0\u589e\u9884\u62a5136\uff1a\u65b0\u8fb0\u6cfd+WHSU8127240",
|
|
|
"\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5",
|
|
|
].join("\n");
|
|
|
|
|
|
describe("splitBodySegments", () => {
|
|
|
it("splits on quote header", () => {
|
|
|
const segs = splitBodySegments(MULTI_DATE_BODY);
|
|
|
expect(segs.length).toBeGreaterThanOrEqual(2);
|
|
|
expect(segs[0]).toContain("\u65b0\u589e\u8f6c\u4ed3");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("matchKeywordRules", () => {
|
|
|
it("maps hard words to ui kinds", () => {
|
|
|
expect(matchKeywordRules("\u8bf7\u8d34\u6807\u5e76\u62cd\u7167", "body").map((h) => h.uiKind)).toEqual(
|
|
|
expect.arrayContaining(["work_order"]),
|
|
|
);
|
|
|
expect(matchKeywordRules("\u65b0\u589e\u9884\u62a5", "subject").map((h) => h.uiKind)).toContain(
|
|
|
"forecast",
|
|
|
);
|
|
|
expect(matchKeywordRules("\u65b0\u589e\u8f6c\u4ed3", "body").map((h) => h.uiKind)).toContain(
|
|
|
"transfer",
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("detectUiKindsFromText", () => {
|
|
|
it("subject forecast", () => {
|
|
|
expect(detectUiKindsFromText(MAIL4_SUBJECT, "", [])).toContain("forecast");
|
|
|
});
|
|
|
it("body work_order", () => {
|
|
|
expect(detectUiKindsFromText("", MAIL4_BODY, [])).toContain("work_order");
|
|
|
});
|
|
|
it("DO filename", () => {
|
|
|
expect(detectUiKindsFromText("", "", ["WHSU8127240-DO.pdf"])).toContain(
|
|
|
"do_upload",
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("do_upload dedupe", () => {
|
|
|
it("does not emit two DO cards when forecast body + DO.pdf both hit", () => {
|
|
|
const body = [
|
|
|
"Dear,",
|
|
|
"原箱号YT2604021091=76件操作指令",
|
|
|
"换标后单号:FBA19HW52S0L=76件",
|
|
|
"3:贴好拍照回传",
|
|
|
"------------------ 原始邮件邮件 ------------------",
|
|
|
"发件人:a@b.com",
|
|
|
"日期:2026-05-08 19:15:53",
|
|
|
"主题:新增预报136:新辰泽+WHSU8127240",
|
|
|
"Dear,",
|
|
|
"请查收新增预报: 新辰泽 +WHLC027G597465+WHSU8127240+ 洛杉矶",
|
|
|
"+40HQ+EDT2026.04-25 ETA2026.05-15 船名航次 HMM EMERALD 013E+",
|
|
|
"提拆派组合柜 - 不带托架DO也同步上传至附件请注意查收!",
|
|
|
].join("\n");
|
|
|
const units = extractMailInstructions({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body,
|
|
|
filenames: ["WHSU8127240-DO.pdf", ...MAIL4_FILES],
|
|
|
});
|
|
|
const dos = units.filter((u) => u.uiKind === "do_upload");
|
|
|
expect(dos).toHaveLength(1);
|
|
|
expect(dos[0].isCurrent).toBe(true);
|
|
|
expect(dos[0].source).toBe("body_segment");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("extractMailInstructions display order", () => {
|
|
|
it("sorts units newest round first by roundAt", () => {
|
|
|
const body = [
|
|
|
"更新派送单 地址更新 麻烦按此份为准!!!",
|
|
|
"---- 转发的邮件 ----",
|
|
|
"发件人江江<1817055288@qq.com>",
|
|
|
"日期 2026年3月28日 下午 5:56",
|
|
|
"主题 更新地址",
|
|
|
"在 2026-03-20 10:00:00 , \"op\" <op@x.com> 写道:",
|
|
|
"Dear,",
|
|
|
"DO请查收见附件",
|
|
|
"在 2026-03-15 09:00:00 , \"op\" <op@x.com> 写道:",
|
|
|
"Dear,",
|
|
|
"请查收新增预报: 新辰泽 +EGLV143661466958+TRHU8524770",
|
|
|
].join("\n");
|
|
|
const units = extractMailInstructions({
|
|
|
subject:
|
|
|
"Fw: 新增预报136:TRHU8524770+EGLV143661466958+40HQ",
|
|
|
body,
|
|
|
filenames: ["TRHU8524770-DO-1.pdf"],
|
|
|
});
|
|
|
const kinds = units.map((u) => u.uiKind);
|
|
|
const ci = kinds.indexOf("work_order");
|
|
|
const doIdx = kinds.indexOf("do_upload");
|
|
|
const fc = kinds.indexOf("forecast");
|
|
|
expect(ci).toBeGreaterThanOrEqual(0);
|
|
|
expect(doIdx).toBeGreaterThanOrEqual(0);
|
|
|
expect(fc).toBeGreaterThanOrEqual(0);
|
|
|
expect(ci).toBeLessThan(doIdx);
|
|
|
expect(doIdx).toBeLessThan(fc);
|
|
|
});
|
|
|
|
|
|
it("mail4 work_order rounds newest first", () => {
|
|
|
const body = [
|
|
|
"Dear,",
|
|
|
"原箱号YT2604021091=76件操作指令",
|
|
|
"在 2026-05-25 13:45:03 , \"新辰泽\" <a@b.com> 写道:",
|
|
|
"Dear,",
|
|
|
"WHSU8127240= 原箱号 换标",
|
|
|
"在 2026-05-20 15:55:44 , \"新辰泽\" <a@b.com> 写道:",
|
|
|
"Dear,",
|
|
|
"换标 167件操作指令",
|
|
|
].join("\n");
|
|
|
const units = extractMailInstructions({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body,
|
|
|
filenames: [],
|
|
|
});
|
|
|
const wo = units.filter((u) => u.uiKind === "work_order");
|
|
|
const times = wo.map((u) => parseInstructionRoundAtMs(u.roundAt) ?? 0);
|
|
|
for (let i = 1; i < times.length; i++) {
|
|
|
expect(times[i - 1]).toBeGreaterThanOrEqual(times[i]);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("parseInstructionRoundAtMs", () => {
|
|
|
it("parses Chinese date", () => {
|
|
|
expect(parseInstructionRoundAtMs("2026年3月28日 17:56")).toBeTruthy();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("extractMailInstructions multi-date", () => {
|
|
|
it("mail4: forecast + work_order as separate units", () => {
|
|
|
const units = extractMailInstructions({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body: [
|
|
|
MAIL4_BODY,
|
|
|
"在 2026-05-01 09:00:00 , \"op\" <op@x.com> 写道:",
|
|
|
"Dear,",
|
|
|
"请查收新增预报: 新辰泽 +WHSU8127240",
|
|
|
].join("\n"),
|
|
|
filenames: MAIL4_FILES,
|
|
|
});
|
|
|
const kinds = units.map((u) => u.uiKind);
|
|
|
expect(kinds).toContain("forecast");
|
|
|
expect(kinds).toContain("work_order");
|
|
|
const wo = units.find((u) => u.uiKind === "work_order");
|
|
|
expect(wo?.segmentText).toMatch(/操作指令|换标/);
|
|
|
});
|
|
|
|
|
|
it("keeps transfer(current) + forecast(history) as two units", () => {
|
|
|
const units = extractMailInstructions({
|
|
|
subject: "MATU2745683",
|
|
|
body: MULTI_DATE_BODY,
|
|
|
filenames: [],
|
|
|
});
|
|
|
expect(units.some((u) => u.uiKind === "transfer" && u.isCurrent)).toBe(true);
|
|
|
expect(units.some((u) => u.uiKind === "forecast")).toBe(true);
|
|
|
expect(units.length).toBeGreaterThanOrEqual(2);
|
|
|
});
|
|
|
|
|
|
it("does not duplicate subject forecast when body already has forecast", () => {
|
|
|
const units = extractMailInstructions({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body: "\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5\uff1a\u65b0\u8fb0\u6cfd+WHSU8127240+\u63d0\u62c6\u6d3e",
|
|
|
filenames: [],
|
|
|
});
|
|
|
const forecasts = units.filter((u) => u.uiKind === "forecast");
|
|
|
expect(forecasts).toHaveLength(1);
|
|
|
expect(forecasts[0].source).toBe("body_segment");
|
|
|
});
|
|
|
|
|
|
it("mail4 current unit is work_order not forecast", () => {
|
|
|
const units = extractMailInstructions({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body: MAIL4_BODY,
|
|
|
filenames: MAIL4_FILES,
|
|
|
});
|
|
|
const current = units.filter((u) => u.isCurrent);
|
|
|
expect(current.some((u) => u.uiKind === "work_order")).toBe(true);
|
|
|
expect(current.every((u) => u.uiKind !== "forecast")).toBe(true);
|
|
|
});
|
|
|
|
|
|
it("splits nested QQ inline quote headers on mail98-style thread", () => {
|
|
|
const body = [
|
|
|
"Forwarded message:",
|
|
|
"From:朱甜甜<ztt@cnwally.com.cn>",
|
|
|
"Date:2026-07-22 23:18:44(美国山区 (GMT-07:00))",
|
|
|
"TCNU4412465 WL003 FBA19DYHCDLS-XPB2-37件",
|
|
|
"安排卡转海,麻烦安排贴标操作。",
|
|
|
"朱甜甜<ztt@cnwally.com.cn> 在 2026年7月10日 周五 17:07 写道:",
|
|
|
"TCNU4412465",
|
|
|
"WL103262 50433。6件",
|
|
|
"这两票客户需要美时7/10 下午2点前送达",
|
|
|
"朱甜甜<ztt@cnwally.com.cn> 在 2026年7月1日 周三 17:15 写道:",
|
|
|
"TCNU4412465",
|
|
|
"这两票总共31件记得叫仓库拍一下照片过来哈。",
|
|
|
"朱甜甜<ztt@cnwally.com.cn> 在 2026年6月13日 周六 15:47 写道:",
|
|
|
"TCNU4412465+ ONEYNB3BW9236300+船名航次:WAN HAI 511-V.E110+ETA:6/22",
|
|
|
"拆柜清单",
|
|
|
"派送要求: 1,分别按票打板 千万不要混打",
|
|
|
].join("\n");
|
|
|
const segs = splitBodySegments(body);
|
|
|
expect(segs.length).toBeGreaterThanOrEqual(4);
|
|
|
const units = extractMailInstructions({
|
|
|
subject:
|
|
|
"Fw: Fw:贴标操作:派送指令:拍照指令:do请查收:TCNU4412465+ ONEYNB3BW9236300+船名航次:WAN HAI 511-V.E110+ETA:6/22+UPS-6件+亚马逊卡派-1275件+私人地址-30件+拆柜清单",
|
|
|
body,
|
|
|
filenames: [],
|
|
|
});
|
|
|
const wo = units.filter((u) => u.uiKind === "work_order");
|
|
|
expect(wo.length).toBeGreaterThanOrEqual(2);
|
|
|
expect(units.some((u) => u.uiKind === "work_order")).toBe(true);
|
|
|
expect(units.some((u) => u.uiKind === "forecast")).toBe(true);
|
|
|
expect(wo.some((u) => /贴标/.test(u.segmentText))).toBe(true);
|
|
|
expect(wo.some((u) => /拍一下照片|拍照/.test(u.segmentText))).toBe(true);
|
|
|
const forecast = units.find((u) => u.uiKind === "forecast");
|
|
|
expect(forecast?.isCurrent).toBe(false);
|
|
|
expect(forecast?.roundAt).toMatch(/2026年7月1日|2026年6月13日/);
|
|
|
});
|
|
|
|
|
|
it("adds forecast from ops subject when current body is work_order", () => {
|
|
|
const subject =
|
|
|
"Fw: Fw:贴标操作:派送指令:拍照指令:do请查收:TCNU4412465+ ONEYNB3BW9236300+船名航次:WAN HAI 511-V.E110+ETA:6/22+UPS-6件+亚马逊卡派-1275件+私人地址-30件+拆柜清单";
|
|
|
const body = [
|
|
|
"Forwarded message:",
|
|
|
"From:朱甜甜<ztt@cnwally.com.cn>",
|
|
|
"Date:2026-07-22 23:18:44(美国山区 (GMT-07:00))",
|
|
|
"Subject:贴标操作:派送指令:拍照指令:do请查收:TCNU4412465+ ONEYNB3BW9236300+船名航次:WAN HAI 511-V.E110+ETA:6/22+UPS-6件+亚马逊卡派-1275件+私人地址-30件+拆柜清单",
|
|
|
"TCNU4412465 WL003 FBA19DYHCDLS-XPB2-37件",
|
|
|
"安排卡转海,麻烦安排贴标操作。",
|
|
|
"注意:ups默认一件贴2张面单",
|
|
|
].join("\n");
|
|
|
const units = extractMailInstructions({
|
|
|
subject,
|
|
|
body,
|
|
|
filenames: ["WL100244-37-XPB2(1).pdf"],
|
|
|
});
|
|
|
expect(units.some((u) => u.uiKind === "work_order")).toBe(true);
|
|
|
expect(units.some((u) => u.uiKind === "forecast")).toBe(true);
|
|
|
expect(units.find((u) => u.uiKind === "forecast")?.isCurrent).toBe(false);
|
|
|
});
|
|
|
|
|
|
it("splits reply-original boundary into forecast + redirect-delivery", () => {
|
|
|
const body = [
|
|
|
"Forwarded message:",
|
|
|
"From:km16@kmfba.com<km16@kmfba.com>",
|
|
|
"Date:2026-07-21 02:44:04(美国山区 (GMT-07:00))",
|
|
|
"Subject:回复:ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送",
|
|
|
" FCIU7089474 MR03002964 改地址派送到 7400 E. Slauson Ave commerce. CA 90040",
|
|
|
"",
|
|
|
"km16",
|
|
|
"km16@kmfba.com",
|
|
|
"",
|
|
|
"---- 回复的原邮件 ----",
|
|
|
"发送日期2026年6月26日 14:09",
|
|
|
"主题ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送",
|
|
|
"",
|
|
|
"FCIU7089474 提柜 COSCO 40HQ ETA:7.10 卡派拆柜派送",
|
|
|
"km16",
|
|
|
"",
|
|
|
"km16@kmfba.com",
|
|
|
].join("\n");
|
|
|
const units = extractMailInstructions({
|
|
|
subject: "Fw: Fw:回复:ZJYC COSU6504130784 FCIU7089474 7.10 派拆柜派送",
|
|
|
body,
|
|
|
filenames: [],
|
|
|
});
|
|
|
expect(units.some((u) => u.uiKind === "work_order")).toBe(true);
|
|
|
expect(units.some((u) => u.uiKind === "forecast")).toBe(true);
|
|
|
expect(
|
|
|
units.some(
|
|
|
(u) =>
|
|
|
/改地址派送到/.test(u.segmentText) &&
|
|
|
u.uiKind === "work_order" &&
|
|
|
u.keywords.includes("改地址派送"),
|
|
|
),
|
|
|
).toBe(true);
|
|
|
expect(
|
|
|
units.some(
|
|
|
(u) =>
|
|
|
u.uiKind === "forecast" &&
|
|
|
/提柜 COSCO 40HQ ETA:7.10 卡派拆柜派送/.test(u.segmentText),
|
|
|
),
|
|
|
).toBe(true);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("extractForecastInstructionText", () => {
|
|
|
it("isolates forecast from label body", () => {
|
|
|
const t = extractForecastInstructionText({
|
|
|
subject: MAIL4_SUBJECT,
|
|
|
body: MAIL4_BODY,
|
|
|
});
|
|
|
expect(t).not.toMatch(/\u8986\u76d6\u8d34/);
|
|
|
});
|
|
|
|
|
|
it("extracts 时间/发件人/主题 from QQ forward chrome, ignores 收件人", () => {
|
|
|
const body = [
|
|
|
"Dear, 请处理。",
|
|
|
"",
|
|
|
"---- 转发的邮件 ----",
|
|
|
"发件人cs3<cs3@xinfenginc.com>",
|
|
|
"日期 2026年07月09日 15:32",
|
|
|
"收件人fancy<fancy@link-ever.com>、receiving<receiving@usasinogroup.com>、hui",
|
|
|
"<hui@usasinogroup.com>、luo<luo@usasinogroup.com>",
|
|
|
"主题 TIIU8073522-90022",
|
|
|
"",
|
|
|
"贴标后拍照回传",
|
|
|
].join("\n");
|
|
|
const meta = extractForwardChromeMeta(body);
|
|
|
expect(meta.from).toMatch(/cs3.*cs3@xinfenginc\.com/);
|
|
|
expect(meta.at).toMatch(/2026年07月09日\s*15:32/);
|
|
|
expect(meta.subject).toBe("TIIU8073522-90022");
|
|
|
// 收件人不得进 from/subject
|
|
|
expect(meta.from).not.toMatch(/fancy|receiving|hui@/);
|
|
|
expect(meta.subject).not.toMatch(/fancy|收件人/);
|
|
|
|
|
|
const units = extractMailInstructions({
|
|
|
subject: "Fw: TIIU8073522-90022",
|
|
|
body,
|
|
|
});
|
|
|
const cur = units.find((u) => u.isCurrent) || units[0];
|
|
|
expect(cur?.segmentSubject).toBe("TIIU8073522-90022");
|
|
|
expect(cur?.roundFrom).toMatch(/cs3/);
|
|
|
expect(cur?.roundAt).toMatch(/2026年07月09日/);
|
|
|
expect(cur?.segmentText || "").not.toMatch(/收件人fancy|转发的邮件/);
|
|
|
});
|
|
|
|
|
|
it("strips QQ desktop 转发的邮件 header block", () => {
|
|
|
const body = [
|
|
|
"Dear,",
|
|
|
"请贴标后拍照回传,谢谢。",
|
|
|
"",
|
|
|
"---- 转发的邮件 ----",
|
|
|
"发件人cs3<cs3@xinfenginc.com>",
|
|
|
"日期 2026年07月09日 15:32",
|
|
|
"收件人fancy<fancy@link-ever.com>、receiving<receiving@usasinogroup.com>、hui",
|
|
|
"<hui@usasinogroup.com>、luo<luo@usasinogroup.com>、date<date@usasi",
|
|
|
"nogroup.com>、op10<op10@xinfenginc.com>、cs1<cs1@xinfenginc.com>",
|
|
|
"主题 TIIU8073522-90022",
|
|
|
"",
|
|
|
"原业务说明可在此继续",
|
|
|
].join("\n");
|
|
|
const t = stripMobileMailChrome(body);
|
|
|
expect(t).not.toMatch(/转发的邮件/);
|
|
|
expect(t).not.toMatch(/发件人cs3|收件人fancy|usasinogroup|主题 TIIU|日期 2026/);
|
|
|
expect(t).toMatch(/请贴标后拍照回传/);
|
|
|
expect(t).toMatch(/原业务说明可在此继续/);
|
|
|
});
|
|
|
|
|
|
it("strips QQ/iPhone mobile forward chrome from instruction", () => {
|
|
|
const body = [
|
|
|
"Fw: 转发:新增预报136:新辰泽+WHLC027G597465+WHSU8127240+洛杉矶+40HQ",
|
|
|
"船名航次HMM EMERALD 013E+提拆派组合柜-不带托架",
|
|
|
"【发件人75440098<75440098@qq.com>",
|
|
|
"收件人3457189498<3457189498@qq.com>",
|
|
|
"大小 1.4M",
|
|
|
"时间 2026年7月14日 16:00",
|
|
|
"发自我的iPhone】",
|
|
|
"",
|
|
|
"请查收新增预报资料",
|
|
|
].join("\n");
|
|
|
const t = extractForecastInstructionText({
|
|
|
subject: "Fw: 转发:新增预报136:新辰泽+WHSU8127240",
|
|
|
body,
|
|
|
});
|
|
|
expect(t).not.toMatch(/发件人75440098|收件人3457189498|1\.4M|发自我的iPhone|2026年7月14日/);
|
|
|
expect(t).toMatch(/新增预报|船名航次|WHSU8127240|请查收新增预报/);
|
|
|
});
|
|
|
});
|