Stop tracking local one-off patch/ingest scripts.

Keep smoke/ops scripts (imap/cc/retention) and product source.

Co-authored-by: Cursor <cursoragent@cursor.com>
main
你的GitHub用户名 1 month ago
parent 2b29fa7497
commit 7214d32627

19
.gitignore vendored

@ -37,4 +37,23 @@ _tmp*
scripts/_dump-*.ts
scripts/_check-*.ts
scripts/debug-*.ts
scripts/patch-*.ts
scripts/fix-*.ts
scripts/rewrite-*.ts
scripts/ingest-sample-mails.ts
scripts/ingest-mail3.ts
scripts/ingest-forecast-ui.ts
scripts/ingest-do-upload-ui.ts
scripts/ingest-recognize-gold.ts
scripts/scan-mail-samples.ts
scripts/analyze-mail-rounds.ts
scripts/bind-instruction-segment-ui.ts
scripts/check-template-recognize.ts
scripts/extract-mail-recognize-docx.py
scripts/extract-pdf-text.py
scripts/fill-xcz-packing-template.ts
scripts/flatten-mail-attachments.ts
scripts/mark-non-business-ignored.ts
scripts/purge-all-mails.ts
scripts/verify-reimport-rules.ts
docx/_excel_summary.py

@ -1,38 +0,0 @@
import fs from "fs";
import {
extractMailInstructions,
splitBodySegments,
} from "../src/services/parse/split-instructions";
const raw = JSON.parse(
fs.readFileSync("data/logs/mail-pdf-extract.json", "utf8"),
) as Record<string, string[]>;
function bodyOf(keyPart: string): string {
const key = Object.keys(raw).find((k) => k.includes(keyPart));
if (!key) return "";
return raw[key].join("\n");
}
for (const name of ["邮件2", "邮件3", "邮件4"]) {
const body = bodyOf(name);
const segs = splitBodySegments(body);
console.log(`\n==== ${name} segs=${segs.length} bodyLen=${body.length}`);
segs.forEach((s, i) => {
console.log(` ${i}: ${s.replace(/\s+/g, " ").slice(0, 100)}`);
});
const units = extractMailInstructions({
subject: "",
body,
filenames: [],
});
console.log(
" units",
units.map((u) => ({
kind: u.uiKind,
cur: u.isCurrent,
seg: u.segmentIndex,
kw: u.keywords.slice(0, 4),
})),
);
}

@ -1,103 +0,0 @@
/**
* Bind instruction unit.segmentText into MailBusinessSummary form builders.
* ASCII-only edits to avoid Chinese corruption.
*/
import fs from "fs";
const p = "src/components/MailBusinessSummary.tsx";
let t = fs.readFileSync(p, "utf8");
if (!t.includes("segmentText")) {
// work_order block: use unit segment for form
const woOld = `} else if (unit.uiKind === "work_order") {
body = (
<CcWorkOrderForm
mode={canConfirmOps ? "edit" : "readonly"}
value={workOrderValue}
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
/>
);`;
const woNew = `} else if (unit.uiKind === "work_order") {
const woForUnit = buildCcWorkOrderFormValue({
subject: unit.segmentSubject || mail.subject,
body: unit.segmentText || bodyText,
filenames,
containerNo,
actions: record?.work_order_actions,
});
body = (
<CcWorkOrderForm
mode={canConfirmOps ? "edit" : "readonly"}
value={{
...woForUnit,
// keep edits on shared state when confirming primary WO
...(mail.mail_type === "WORK_ORDER" && unit.isCurrent
? workOrderValue
: {}),
}}
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
/>
);`;
if (!t.includes(woOld)) {
console.error("work_order block not found");
process.exit(1);
}
t = t.replace(woOld, woNew);
const doOld = `} else if (unit.uiKind === "do_upload") {
body = (
<CcDoUploadPanel
mode={canConfirmOps ? "edit" : "readonly"}
value={doValue}
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
/>
);`;
const doNew = `} else if (unit.uiKind === "do_upload") {
const doForUnit = buildCcDoUploadFormValue({
subject: unit.segmentSubject || mail.subject,
body: unit.segmentText || bodyText,
containerNo,
filenames:
unit.source === "attachment"
? [unit.segmentText]
: filenames,
});
body = (
<CcDoUploadPanel
mode={canConfirmOps ? "edit" : "readonly"}
value={
mail.mail_type === "DO_UPLOAD" && unit.isCurrent
? doValue
: doForUnit
}
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
/>
);`;
if (!t.includes(doOld)) {
console.error("do_upload block not found");
process.exit(1);
}
t = t.replace(doOld, doNew);
fs.writeFileSync(p, t, "utf8");
}
const check = fs.readFileSync(p, "utf8");
console.log(
JSON.stringify(
{
hasSegmentBind: check.includes("woForUnit") && check.includes("doForUnit"),
titleOk: check.includes("\u8fd9\u5c01\u90ae\u4ef6\u5728\u5e72\u4ec0\u4e48"),
},
null,
2,
),
);
if (!check.includes("\u8fd9\u5c01\u90ae\u4ef6\u5728\u5e72\u4ec0\u4e48")) {
console.error("Chinese title corrupted <20>?restore with fix-mail-business-summary-zh.ts");
process.exit(1);
}

@ -1,43 +0,0 @@
import { classifyMail } from "../src/services/parse/classify";
import { extractMailInstructions } from "../src/services/parse/split-instructions";
import { isDoAttachmentFilename } from "../src/services/parse/do-upload-extract";
import fs from "fs";
import path from "path";
const root = "docx/邮件/模板/附件下载_邮件识别";
const files: string[] = [];
function walk(d: string) {
for (const n of fs.readdirSync(d)) {
const p = path.join(d, n);
if (fs.statSync(p).isDirectory()) walk(p);
else if (!/\.docx$/i.test(n)) files.push(n);
}
}
walk(root);
const subject =
"智鸿2+WHLC027G597465+WHSU8127240+洛杉<EFBFBD>?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆<EFBFBD>?;
const body = "请查收新增预报\nDO也同步上传至附件请注意查收!";
const ev = classifyMail({ subject, body, filenames: files });
const units = extractMailInstructions({ subject, body, filenames: files });
console.log("files", files);
console.log("classify", ev.mail_type, "score", ev.total);
console.log(
"signals",
ev.signals.map((s) => `${s.signal}:${s.score}`),
);
console.log(
"units",
units.map((u) => ({
k: u.uiKind,
cur: u.isCurrent,
src: u.source,
kw: u.keywords.slice(0, 4),
})),
);
console.log(
"DO?",
files.filter((f) => isDoAttachmentFilename(f) || /\bDO\b/i.test(f)),
);

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import zipfile, re, html, os
docx = r"docx/邮件/模板/附件下载_邮件识别/邮件识别.docx"
with zipfile.ZipFile(docx) as z:
xml = z.read("word/document.xml").decode("utf-8")
text = re.sub(r"<w:tab[^/]*/>", "\t", xml)
text = re.sub(r"</w:p>", "\n", text)
text = re.sub(r"<[^>]+>", "", text)
text = html.unescape(text)
text = re.sub(r"\n{3,}", "\n\n", text)
out = r"data/logs/mail-recognize-docx.txt"
os.makedirs("data/logs", exist_ok=True)
open(out, "w", encoding="utf-8").write(text)
print(out, "chars", len(text))
print(text[:4000])

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
"""Extract plain text from a PDF (pypdf). Usage: python scripts/extract-pdf-text.py <pdf>"""
import sys
from pypdf import PdfReader
def main():
path = sys.argv[1]
reader = PdfReader(path)
parts = []
for page in reader.pages:
t = page.extract_text() or ""
if t.strip():
parts.append(t)
sys.stdout.reconfigure(encoding="utf-8")
print("\n".join(parts))
if __name__ == "__main__":
main()

@ -1,119 +0,0 @@
/**
* 按新辰泽填写规范生成卡派表(不就<EFBFBD>?splice,避免残留旧行)<EFBFBD>?
* 用法: pnpm exec tsx scripts/fill-xcz-packing-template.ts
*/
import ExcelJS from "exceljs";
import path from "path";
const SRC = path.join(process.cwd(), "docx", "邮件", "模板", "数据模版.xlsx");
const OUT = path.join(
process.cwd(),
"docx",
"邮件",
"模板",
"数据模版-新辰<E696B0>?WHSU8127240.xlsx",
);
const CN = "WHSU8127240";
const LAS1_ADDR = [
"仓库代码<E4BBA3>?LAS1",
"收件人: LAS1",
"公司名称<E5908D>?AMAZON COM SERVICES INC",
"联系电话<E794B5>?0123456789",
"<22>?<3F>?区: NV / HENDERSON /",
"邮政编码<E7BC96>?89044-8746",
"收货地址<E59CB0>?12300 Bermuda Road",
].join("\n");
const PRIVATE_ADDR = [
"收件人: Erica Fabian",
"公司名称<E5908D>?Rapid Fulfillment",
"联系电话<E794B5>?818-492-2760",
"<22>?<3F>?区: CA / Pacoima /",
"邮政编码<E7BC96>?91331",
"收货地址<E59CB0>?12924 Pierce St",
].join("\n");
type Row = [
string,
string,
number,
string,
number,
number,
string,
string,
string,
string,
string,
string,
string,
string,
];
const ROWS: Row[] = [
[CN, "", 44, "卡派", 5.14, 567, LAS1_ADDR, "FBA16SQQ7PTF", "758SDXPH", "LAS1", "2026-03-29", "2026-04-06", "6268357756", ""],
[CN, "", 14, "卡派", 5.11, 562, LAS1_ADDR, "FBA16SQTM0R1", "39MFHV4O", "LAS1", "2026-03-29", "2026-04-06", "6268357756", ""],
[CN, "", 6, "卡派", 0.45, 98, LAS1_ADDR, "FBA16SRQFXY5", "3ROVWIOM", "LAS1", "2026-03-29", "2026-04-06", "9926386665", ""],
[CN, "", 5, "卡派", 2.56, 756.32, LAS1_ADDR, "FBA16SKZQCBB", "4UWQROSH", "LAS1", "2026-03-29", "2026-04-06", "20220729LAS139", ""],
[CN, "", 34, "卡派", 2.56, 756.32, LAS1_ADDR, "FBA16SL031T3", "5DQ3HYSM", "LAS1", "2026-03-29", "2026-04-06", "20220729LAS139", ""],
[CN, "", 7, "卡派", 0.24, 128, LAS1_ADDR, "FBA16SP4NBQR", "79VUM4FL", "LAS1", "2026-03-29", "2026-04-06", "822072867821", ""],
[CN, "", 5, "卡派", 0.48, 110.85, LAS1_ADDR, "FBA16SRW4F6W", "38JNRT2Q", "LAS1", "2026-03-28", "2026-04-08", "DGG803076", ""],
[CN, "", 8, "卡派", 0.88, 166, LAS1_ADDR, "FBA16SP4GFX0", "8OSM83RO", "LAS1", "2026-03-28", "2026-04-08", "JBHSF072803", ""],
[CN, "", 6, "卡派", 1.02, 249, LAS1_ADDR, "FBA16SJZHL56", "3ME4KJQV", "LAS1", "2026-03-28", "2026-04-08", "YZ22080003", ""],
[CN, "", 28, "卡派", 2.25, 429, PRIVATE_ADDR, "220729YT28NB", "", "", "", "", "/", "三票分开打托"],
[CN, "", 11, "卡派", 0.59, 242, PRIVATE_ADDR, "220730FK11", "", "", "", "", "/", ""],
[CN, "", 10, "卡派", 0.44, 136, PRIVATE_ADDR, "822072968180", "", "", "", "", "/", ""],
[CN, "", 16, "卡派", 1.76, 144.31, PRIVATE_ADDR, "FBA16SCKQQWC", "", "", "", "", "/", ""],
[CN, "", 6, "卡派", 0.4, 13, PRIVATE_ADDR, "XT0729JBSBD171", "", "", "", "", "/", ""],
[CN, "", 3, "UPS", 1.23, 296, "", "1Z4X98060308009900", "", "", "", "", "/", ""],
[CN, "", 5, "UPS", 1.23, 296, "", "1Z4X98060321113067", "", "", "", "", "/", ""],
[CN, "", 8, "UPS", 1.23, 296, "", "1Z4X98060322191230", "", "", "", "", "/", ""],
[CN, "", 24, "UPS", 0.72, 425, "", "1Z4X98060306370433", "", "", "", "", "/", ""],
[CN, "", 7, "UPS", 0.5, 103, "", "1Z4X98060307290787", "", "", "", "", "FBA16ST52H3N", ""],
[CN, "", 40, "FEDEX", 3.43, 595.86, "", "889396190583", "", "", "", "", "FBA16SV9CKD5", ""],
[CN, "", 12, "FEDEX", 1.1, 210, "", "889396190584", "", "", "", "", "/", ""],
[CN, "", 4, "自提", 0.8, 120, PRIVATE_ADDR, "PICKUP-XCZ-001", "", "", "", "", "/", ""],
[CN, "", 2, "存仓", 0.3, 45, "", "STORAGE-XCZ-001", "", "", "", "", "/", ""],
];
async function main() {
const srcWb = new ExcelJS.Workbook();
await srcWb.xlsx.readFile(SRC);
const src = srcWb.worksheets[0];
if (!src) throw new Error("no source sheet");
const outWb = new ExcelJS.Workbook();
const ws = outWb.addWorksheet(src.name || "Sheet1");
// 复制<E5A48D>?3 行表头(<E5A4B4>?+ 合并可忽略)
for (let r = 1; r <= 3; r++) {
const srcRow = src.getRow(r);
const vals: ExcelJS.CellValue[] = [];
srcRow.eachCell({ includeEmpty: true }, (cell, col) => {
vals[col - 1] = cell.value;
});
ws.addRow(vals);
}
for (const r of ROWS) {
const row = ws.addRow([...r]);
row.getCell(3).value = r[2];
row.getCell(5).value = r[4];
row.getCell(6).value = r[5];
}
await outWb.xlsx.writeFile(OUT);
try {
await outWb.xlsx.writeFile(SRC);
console.log(`also updated ${SRC}`);
} catch (e) {
console.warn(`skip overwrite locked template: ${(e as Error).message}`);
}
console.log(`filled ${ROWS.length} rows <20>?${OUT}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

@ -1,5 +0,0 @@
/**
* Fix MailBusinessSummary Chinese corruption <EFBFBD>?delegates to full UTF-8 rewrite.
* Prefer: pnpm fix:ui-zh
*/
import "./rewrite-mail-business-summary";

@ -1,34 +0,0 @@
import fs from "fs";
const p = "src/components/MailBusinessSummary.tsx";
let t = fs.readFileSync(p, "utf8");
const btnWo = "\u786e\u8ba4\u63d0\u4ea4\u5de5\u5355";
t = t.replace(
/(mail\.mail_type === "WORK_ORDER"\) \{\s*actions = \(\s*<Button[\s\S]*?>\s*)\?{2,}(\s*<\/Button>)/,
`$1${btnWo}$2`,
);
// Comments with ? are fine; flag remaining UI ?
const bad = t
.split("\n")
.map((l, i) => ({ i: i + 1, l }))
.filter(
(x) =>
/\?{3,}/.test(x.l) &&
!x.l.includes("eslint") &&
!x.l.trim().startsWith("//"),
);
fs.writeFileSync(p, t, "utf8");
console.log(
JSON.stringify(
{
btnOk: t.includes(btnWo),
badLines: bad.map((x) => `${x.i}: ${x.l.trim()}`),
},
null,
2,
),
);

@ -1,62 +0,0 @@
/**
* 将邮<EFBFBD>?/4 子目录内业务附件平铺到邮件根目录<EFBFBD>? * 用法: pnpm exec tsx scripts/flatten-mail-attachments.ts
*/
import fs from "fs/promises";
import path from "path";
const MAIL_ROOT = path.join(process.cwd(), "docx", "邮件");
async function exists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function moveUp(subdir: string, mailDir: string): Promise<string[]> {
const moved: string[] = [];
if (!(await exists(subdir))) return moved;
const entries = await fs.readdir(subdir, { withFileTypes: true });
for (const e of entries) {
if (!e.isFile()) continue;
const src = path.join(subdir, e.name);
const dest = path.join(mailDir, e.name);
if (await exists(dest)) {
// 同名已在根:删子目录副本
await fs.unlink(src);
moved.push(`${e.name} (root exists, removed nested)`);
continue;
}
await fs.rename(src, dest);
moved.push(e.name);
}
// 清空后删子目<E5AD90>? const left = await fs.readdir(subdir);
if (!left.length) await fs.rmdir(subdir);
return moved;
}
async function main() {
const ops: Array<{ mail: string; nested: string }> = [
{ mail: "邮件3", nested: "WHSU5574991 卡转<EFBFBD>? },
{
mail: "邮件4",
nested: "原箱号YT2604021091=FBA199R49LD6-MDW2=76件换标操作指<EFBFBD>?,
},
];
for (const op of ops) {
const mailDir = path.join(MAIL_ROOT, op.mail);
const nested = path.join(mailDir, op.nested);
const moved = await moveUp(nested, mailDir);
console.log(
JSON.stringify({ mail: op.mail, nested: op.nested, moved }, null, 2),
);
}
}
main().catch((e) => {
console.error(e);
process.exitCode = 1;
});

@ -1,145 +0,0 @@
/**
* 入库「上<EFBFBD>?DO」样例:docx/邮件/模板/WHSU8127240-DO.pdf
* 用法: pnpm sample:do
*/
import { createHash } from "crypto";
import fs from "fs/promises";
import path from "path";
import { prisma } from "@/services/db";
import { ParsePipeline } from "@/services/parse/pipeline";
import { buildCcDoUploadFormValue } from "@/components/CcDoUploadPanel";
import { extractMailInstructions } from "@/services/parse/split-instructions";
import { sha256 } from "@/utils/hash";
const SUBJECT = "请查<E8AFB7>?DO:柜<EFBC9A>?WHSU8127240";
const BODY = "Dear,\n请查收附<EFBFBD>?DO 文件,谢谢<EFBFBD>?;
const MESSAGE_ID = "<sample-do-upload-whsu8127240@local.test>";
const DO_PDF = path.join(
process.cwd(),
"docx",
"邮件",
"模板",
"WHSU8127240-DO.pdf",
);
async function attachDo(mailId: bigint): Promise<string> {
const buf = await fs.readFile(DO_PDF);
const hash = sha256(buf);
const filename = "WHSU8127240-DO.pdf";
const dir = path.join(process.cwd(), "data", "mails", String(mailId));
await fs.mkdir(dir, { recursive: true });
const dest = path.join(dir, filename);
await fs.writeFile(dest, buf);
const rel = path.relative(process.cwd(), dest).replace(/\\/g, "/");
await prisma.mailAttachment.create({
data: {
mailId,
filename,
contentType: "application/pdf",
sha256: hash,
path: rel,
size: buf.length,
rejected: false,
},
});
return filename;
}
async function main() {
await fs.access(DO_PDF);
const rawHash = createHash("sha256")
.update(`do-upload-ui|${MESSAGE_ID}|${SUBJECT}|WHSU8127240-DO.pdf|v1`)
.digest("hex");
const existing = await prisma.mailMessage.findFirst({
where: { OR: [{ messageId: MESSAGE_ID }, { rawHash }] },
});
let mailId: bigint;
if (existing) {
await prisma.importCompensation.deleteMany({
where: { import: { mailId: existing.id } },
});
await prisma.containerImport.deleteMany({ where: { mailId: existing.id } });
await prisma.parseResult.deleteMany({ where: { mailId: existing.id } });
await prisma.mailAttachment.deleteMany({ where: { mailId: existing.id } });
await prisma.mailMessage.update({
where: { id: existing.id },
data: {
subject: SUBJECT.slice(0, 512),
fromAddr: "do-upload@local.test",
bodyText: BODY,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
rawHash,
receivedAt: new Date("2026-04-20T09:00:00.000Z"),
typeEvidence: { note: "do_upload_ui_sample" },
version: { increment: 1 },
},
});
mailId = existing.id;
} else {
const created = await prisma.mailMessage.create({
data: {
messageId: MESSAGE_ID,
subject: SUBJECT.slice(0, 512),
fromAddr: "do-upload@local.test",
bodyText: BODY,
status: "FETCHED",
mailType: "UNKNOWN",
rawHash,
folder: "INBOX",
receivedAt: new Date("2026-04-20T09:00:00.000Z"),
isThreadRoot: true,
typeEvidence: { note: "do_upload_ui_sample" },
},
});
mailId = created.id;
}
const filename = await attachDo(mailId);
await ParsePipeline.run(mailId);
const updated = await prisma.mailMessage.findUniqueOrThrow({
where: { id: mailId },
include: { parseResult: true, attachments: true },
});
const form = buildCcDoUploadFormValue({
subject: updated.subject,
body: updated.bodyText,
filenames: updated.attachments.map((a) => a.filename),
});
const units = extractMailInstructions({
subject: updated.subject,
body: updated.bodyText || "",
filenames: updated.attachments.map((a) => a.filename),
});
console.log(
JSON.stringify(
{
id: String(updated.id),
mail_type: updated.mailType,
status: updated.status,
attached: filename,
form,
ui_kinds: units.map((u) => u.uiKind),
url: `http://localhost:3100/mails/${updated.id}`,
},
null,
2,
),
);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,170 +0,0 @@
/**
* 入库「邮<EFBFBD>? 完整线程<EFBFBD>? 新辰泽卡派模板(历史预报货件表)<EFBFBD>? * 最新指令是换标/贴标工单;主题链路上的新增预报仅作历史只读<EFBFBD>? * 用法: pnpm exec tsx scripts/ingest-forecast-ui.ts
*/
import { createHash } from "crypto";
import fs from "fs/promises";
import path from "path";
import { prisma } from "@/services/db";
import { ParsePipeline } from "@/services/parse/pipeline";
import { sha256 } from "@/utils/hash";
import { loadMailboxPdfBody } from "./lib/load-mailbox-pdf-body";
const 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 FALLBACK_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 MESSAGE_ID = "<sample-forecast-formorder-ui@local.test>";
const MAIL4_DIR = path.join(process.cwd(), "docx", "\u90ae\u4ef6", "\u90ae\u4ef64");
const TEMPLATE = path.join(
process.cwd(),
"docx",
"\u90ae\u4ef6",
"\u6a21\u677f",
"\u6570\u636e\u6a21\u7248-\u65b0\u8fb0\u6cfd-WHSU8127240.xlsx",
);
async function attachTemplate(mailId: bigint): Promise<void> {
const buf = await fs.readFile(TEMPLATE);
const hash = sha256(buf);
const filename = "\u65b0\u8fb0\u6cfd-\u5361\u6d3e\u8d44\u6599-WHSU8127240.xlsx";
const dir = path.join(process.cwd(), "data", "mails", String(mailId));
await fs.mkdir(dir, { recursive: true });
const dest = path.join(dir, filename);
await fs.writeFile(dest, buf);
const rel = path.relative(process.cwd(), dest).replace(/\\/g, "/");
await prisma.mailAttachment.create({
data: {
mailId,
filename,
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
sha256: hash,
path: rel,
size: buf.length,
rejected: false,
},
});
}
async function main() {
const pdfBody = await loadMailboxPdfBody(MAIL4_DIR);
const bodyText =
pdfBody && pdfBody.length > FALLBACK_BODY.length ? pdfBody : FALLBACK_BODY;
console.log(`body chars=${bodyText.length} (pdf=${Boolean(pdfBody)})`);
const rawHash = createHash("sha256")
.update(`forecast-ui|${MESSAGE_ID}|${SUBJECT}|${bodyText}|tpl-v5-full`)
.digest("hex");
const existing = await prisma.mailMessage.findFirst({
where: { OR: [{ messageId: MESSAGE_ID }, { rawHash }] },
});
let mailId: bigint;
if (existing) {
await prisma.importCompensation.deleteMany({
where: { import: { mailId: existing.id } },
});
await prisma.containerImport.deleteMany({ where: { mailId: existing.id } });
await prisma.parseResult.deleteMany({ where: { mailId: existing.id } });
await prisma.mailAttachment.deleteMany({ where: { mailId: existing.id } });
await prisma.mailMessage.update({
where: { id: existing.id },
data: {
subject: SUBJECT.slice(0, 512),
fromAddr: "forecast-ui@local.test",
bodyText,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
rawHash,
receivedAt: new Date("2026-04-20T08:00:00.000Z"),
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
version: { increment: 1 },
},
});
mailId = existing.id;
} else {
// also refresh by messageId if hash changed
const byMid = await prisma.mailMessage.findFirst({
where: { messageId: MESSAGE_ID },
});
if (byMid) {
await prisma.importCompensation.deleteMany({
where: { import: { mailId: byMid.id } },
});
await prisma.containerImport.deleteMany({ where: { mailId: byMid.id } });
await prisma.parseResult.deleteMany({ where: { mailId: byMid.id } });
await prisma.mailAttachment.deleteMany({ where: { mailId: byMid.id } });
await prisma.mailMessage.update({
where: { id: byMid.id },
data: {
subject: SUBJECT.slice(0, 512),
bodyText,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
rawHash,
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
version: { increment: 1 },
},
});
mailId = byMid.id;
} else {
const created = await prisma.mailMessage.create({
data: {
messageId: MESSAGE_ID,
subject: SUBJECT.slice(0, 512),
fromAddr: "forecast-ui@local.test",
bodyText,
status: "FETCHED",
mailType: "UNKNOWN",
rawHash,
folder: "INBOX",
receivedAt: new Date("2026-04-20T08:00:00.000Z"),
isThreadRoot: true,
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
},
});
mailId = created.id;
}
}
await attachTemplate(mailId);
await ParsePipeline.run(mailId);
const updated = await prisma.mailMessage.findUniqueOrThrow({
where: { id: mailId },
include: { parseResult: true, attachments: true },
});
console.log(
JSON.stringify(
{
id: String(updated.id),
mail_type: updated.mailType,
status: updated.status,
body_chars: (updated.bodyText || "").length,
has_label: /原箱号|换标/.test(updated.bodyText || ""),
shipments: ((updated.parseResult?.shipments as unknown[]) || []).length,
url: `http://localhost:3100/mails/${updated.id}`,
},
null,
2,
),
);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,128 +0,0 @@
/**
* å°?docx/é‚®ä»/é‚®ä»3 主题注入本地库å¹è·?ParsePipeline(主é¢?only,无正文/附ä»ï¼‰ã€?
* 用法: pnpm sample:mail3
*
* 完整样例(å<EFBFBD>«å<EFBFBD>¡è½¬æµ?PDF 入库)请用:pnpm sample:mails
*/
import { createHash } from "crypto";
import { prisma } from "@/services/db";
import { ParsePipeline } from "@/services/parse/pipeline";
const MAIL3_SUBJECT =
"Fw: 转å<C2AC>‘:派é€<C3A9>è¦<C3A8>求更æ–? 拆柜清å<E280A6>•æ›´æ–°ï¼?DO请查æ”?拆柜清å<E280A6>•æ›´æ–°: WHSU5574991+WHL063G550810+船å<C2B9><C3A5>航次:OOCL SINGAPORE / 065W+ETAï¼?/28+FedEx-29ä»?UPS-102ä»?亚马逊å<C5A0>¡æ´?289ä»?ç§<C3A7>人地å<C2B0>€-166ä»?拦截-209ä»?拆柜清å<E280A6>•";
const MAIL3_BODY = [
"æ´¾é€<EFBFBD>è¦<EFBFBD>求更新,请查æ”?DO 与拆柜清å<EFBFBD>•ã€?,
"柜å<C593>·ï¼šWHSU5574991",
"æ<><C3A6>å<EFBFBD>•:WHL063G550810",
"船å<C2B9><C3A5>航次:OOCL SINGAPORE / 065W",
"ETAï¼?026-06-28",
].join("\n");
const MESSAGE_ID = "<sample-mail3-whsu5574991@local.test>";
const FROM_ADDR = "clx@cnwally.com.cn";
async function main() {
const rawHash = createHash("sha256")
.update(`mail3|${MAIL3_SUBJECT}|${MAIL3_BODY}|v2`)
.digest("hex");
const existing = await prisma.mailMessage.findFirst({
where: {
OR: [{ messageId: MESSAGE_ID }, { rawHash }],
},
});
let mailId: bigint;
if (existing) {
await prisma.containerImport.deleteMany({ where: { mailId: existing.id } });
await prisma.parseResult.deleteMany({ where: { mailId: existing.id } });
await prisma.mailAttachment.deleteMany({ where: { mailId: existing.id } });
await prisma.mailMessage.update({
where: { id: existing.id },
data: {
subject: MAIL3_SUBJECT.slice(0, 512),
fromAddr: FROM_ADDR,
bodyText: MAIL3_BODY,
ocrText: null,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
typeEvidence: {
sample_dir: "docx/邮件/邮件3",
note: "subject_only_ingest",
},
receivedAt: new Date("2026-07-14T08:02:00.000Z"),
version: { increment: 1 },
},
});
mailId = existing.id;
console.log(`updated existing mail id=${mailId}`);
} else {
const created = await prisma.mailMessage.create({
data: {
messageId: MESSAGE_ID,
subject: MAIL3_SUBJECT.slice(0, 512),
fromAddr: FROM_ADDR,
bodyText: MAIL3_BODY,
status: "FETCHED",
mailType: "UNKNOWN",
rawHash,
folder: "INBOX",
receivedAt: new Date("2026-07-14T08:02:00.000Z"),
isThreadRoot: true,
typeEvidence: {
sample_dir: "docx/邮件/邮件3",
note: "subject_body_ingest",
},
},
});
mailId = created.id;
console.log(`created mail id=${mailId}`);
}
await ParsePipeline.run(mailId);
const mail = await prisma.mailMessage.findUnique({
where: { id: mailId },
include: { parseResult: true },
});
const header = mail?.parseResult?.containerHeader as {
F_ContainerNo?: string;
F_BLCopyCode?: string;
F_ETA?: string;
F_Instruction?: string;
} | null;
const lineage = mail?.parseResult?.lineage as {
mail_record?: { kind?: string; summary?: string };
} | null;
console.log(
JSON.stringify(
{
id: String(mailId),
status: mail?.status,
mail_type: mail?.mailType,
container_no: header?.F_ContainerNo,
bl: header?.F_BLCopyCode,
eta: header?.F_ETA,
instruction: header?.F_Instruction,
mail_record_kind: lineage?.mail_record?.kind,
summary: lineage?.mail_record?.summary,
url: `http://localhost:3100/mails/${mailId}`,
},
null,
2,
),
);
}
main()
.catch((err) => {
console.error(err);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,198 +0,0 @@
/**
* 金样入库:docx/邮件/模板/附件下载_邮件识别
* 四类识别:新增预<EFBFBD>?+ 上传 DO(同封)
* 用法: pnpm sample:recognize
*/
import { createHash } from "crypto";
import fs from "fs/promises";
import path from "path";
import { prisma } from "@/services/db";
import { ParsePipeline } from "@/services/parse/pipeline";
import { extractMailInstructions } from "@/services/parse/split-instructions";
import { classifyMail } from "@/services/parse/classify";
import { sha256 } from "@/utils/hash";
const GOLD_DIR = path.join(
process.cwd(),
"docx",
"邮件",
"模板",
"附件下载_邮件识别",
);
const SUBJECT =
"智鸿2+WHLC027G597465+WHSU8127240+洛杉<EFBFBD>?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆<EFBFBD>?;
const BODY = [
"请查收新增预报:智鸿2+WHLC027G597465+WHSU8127240+洛杉<EFBFBD>?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆<EFBFBD>?,
"DO也同步上传至附件请注意查收!",
].join("\n");
const MESSAGE_ID = "<sample-recognize-gold-whsu8127240@local.test>";
const CONTENT_TYPES: Record<string, string> = {
".xlsx":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xls": "application/vnd.ms-excel",
".pdf": "application/pdf",
".csv": "text/csv",
};
async function collectGoldFiles(dir: string): Promise<string[]> {
const out: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
out.push(...(await collectGoldFiles(p)));
} else if (!/\.docx$/i.test(e.name)) {
out.push(p);
}
}
return out;
}
async function attachAll(mailId: bigint, files: string[]): Promise<string[]> {
const dir = path.join(process.cwd(), "data", "mails", String(mailId));
await fs.mkdir(dir, { recursive: true });
const names: string[] = [];
for (const src of files) {
const buf = await fs.readFile(src);
const filename = path.basename(src);
const dest = path.join(dir, filename);
await fs.writeFile(dest, buf);
const rel = path.relative(process.cwd(), dest).replace(/\\/g, "/");
const ext = path.extname(filename).toLowerCase();
await prisma.mailAttachment.create({
data: {
mailId,
filename,
contentType: CONTENT_TYPES[ext] || "application/octet-stream",
sha256: sha256(buf),
path: rel,
size: buf.length,
rejected: false,
},
});
names.push(filename);
}
return names;
}
async function resetMail(mailId: bigint): Promise<void> {
await prisma.importCompensation.deleteMany({
where: { import: { mailId } },
});
await prisma.containerImport.deleteMany({ where: { mailId } });
await prisma.parseResult.deleteMany({ where: { mailId } });
await prisma.mailAttachment.deleteMany({ where: { mailId } });
}
async function main() {
await fs.access(GOLD_DIR);
const goldFiles = await collectGoldFiles(GOLD_DIR);
if (!goldFiles.length) {
throw new Error(`no attachments under ${GOLD_DIR}`);
}
const rawHash = createHash("sha256")
.update(
`recognize-gold|${MESSAGE_ID}|${SUBJECT}|${goldFiles.map((f) => path.basename(f)).sort().join(",")}|v1`,
)
.digest("hex");
const existing = await prisma.mailMessage.findFirst({
where: { OR: [{ messageId: MESSAGE_ID }, { rawHash }] },
});
let mailId: bigint;
if (existing) {
await resetMail(existing.id);
await prisma.mailMessage.update({
where: { id: existing.id },
data: {
subject: SUBJECT.slice(0, 512),
fromAddr: "recognize-gold@local.test",
bodyText: BODY,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
rawHash,
receivedAt: new Date("2026-04-20T10:00:00.000Z"),
typeEvidence: { note: "recognize_gold_附件下载_邮件识别" },
version: { increment: 1 },
},
});
mailId = existing.id;
} else {
const created = await prisma.mailMessage.create({
data: {
messageId: MESSAGE_ID,
subject: SUBJECT.slice(0, 512),
fromAddr: "recognize-gold@local.test",
bodyText: BODY,
status: "FETCHED",
mailType: "UNKNOWN",
rawHash,
folder: "INBOX",
receivedAt: new Date("2026-04-20T10:00:00.000Z"),
isThreadRoot: true,
typeEvidence: { note: "recognize_gold_附件下载_邮件识别" },
},
});
mailId = created.id;
}
const attached = await attachAll(mailId, goldFiles);
await ParsePipeline.run(mailId);
const updated = await prisma.mailMessage.findUniqueOrThrow({
where: { id: mailId },
include: { parseResult: true, attachments: true },
});
const filenames = updated.attachments.map((a) => a.filename);
const evidence = classifyMail({
subject: updated.subject,
body: updated.bodyText || "",
filenames,
});
const units = extractMailInstructions({
subject: updated.subject,
body: updated.bodyText || "",
filenames,
});
console.log(
JSON.stringify(
{
id: String(updated.id),
mail_type: updated.mailType,
status: updated.status,
attached,
classify: {
mail_type: evidence.mail_type,
scores: {
DO_UPLOAD: evidence.scores.DO_UPLOAD,
NEW_CONTAINER: evidence.scores.NEW_CONTAINER,
},
},
ui_kinds: units.map((u) => ({
kind: u.uiKind,
current: u.isCurrent,
source: u.source,
})),
url: `http://localhost:3100/mails/${updated.id}`,
},
null,
2,
),
);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,341 +0,0 @@
/**
* å°?docx/é‚®ä»/é‚®ä»1~4(å<EFBFBD>Šå<EFBFBD>¯é€‰æ¨¡æ<EFBFBD>¿é¢„报)注入本地库å¹è·?ParsePipelineã€? * 用法: pnpm sample:mails
*
* 目录约定(平铺)ï¼? * docx/é‚®ä»/é‚®ä»N/
* *邮箱.pdf / QQ邮箱.pdf â€?QQ 截图(默认ä¸<EFBFBD>入库ï¼? * *.xlsx / 业务 *.pdf â€?业务附ä»ï¼ˆæŒ‰ SampleDef.attachPolicy 入库ï¼? * docx/é‚®ä»/模æ<EFBFBD>¿/ â€?共享预报模æ<EFBFBD>¿ï¼ˆM_BL / forecast-uiï¼? *
* M1 主题-only(UNKNOWNï¼? * M2 主题+å<EFBFBD>¡æ´¾ xlsx(WORK_ORDER/转仓ï¼? * M3 主题 + å<EFBFBD>¡è½¬æµ?PDF 展示(WORK_ORDER 拆柜清å<EFBFBD>•;解æž<EFBFBD>ä»<EFBFBD>主题短路ï¼? * M4 主题+æ<EFBFBD>¢æ ‡æ­£æ–‡ + æ<EFBFBD>¢æ ‡æŒ‡ä»¤ xlsx/PDF(WORK_ORDER / è´´æ ‡ï¼? */
import { createHash } from "crypto";
import fs from "fs/promises";
import path from "path";
import { prisma } from "@/services/db";
import { ParsePipeline } from "@/services/parse/pipeline";
import {
selectAttachments,
type AttachPolicy,
} from "@/services/sample/mail-attachment-select";
import { sha256 } from "@/utils/hash";
import { loadMailboxPdfBody } from "./lib/load-mailbox-pdf-body";
const DOC_MAIL_ROOT = path.join(process.cwd(), "docx", "邮件");
type SampleDef = {
key: string;
messageId: string;
fromAddr: string;
subject: string;
bodyText: string;
receivedAt: string;
/** 相对 docx/邮件/ 的目录å<E280A2><C3A5>,如 邮件2 */
sampleDir: string;
attachPolicy: AttachPolicy;
/** å<>¯é€‰ï¼šæ–‡ä»¶å<C2B6><C3A5>须包å<E280A6>«çš„关键è¯<C3A8>(全部命中) */
nameIncludes?: string[];
};
const SAMPLES: SampleDef[] = [
{
key: "mail1",
messageId: "<sample-mail1-tiiu8073522@local.test>",
fromAddr: "cs3@xinfenginc.com",
subject: "Fw: 转å<C2AC>‘:TIIU8073522-90022",
bodyText: [
"(样例)主题为柜å<EFBFBD>·çº¿ç´¢ï¼Œå®Œæ•´æ­£æ–‡è§?QQ 邮箱截图附ä»ã€?,
"柜å<C593>·çº¿ç´¢ï¼šTIIU8073522",
].join("\n"),
receivedAt: "2026-07-14T08:02:00.000Z",
sampleDir: "邮件1",
attachPolicy: "none",
},
{
key: "mail2",
messageId: "<sample-mail2-matu2745683@local.test>",
fromAddr: "op7@xinfenginc.com",
subject:
"Fw: 转å<C2AC>‘:LINK EVER INC + MATS4583030000+ 柜å<C593>·ï¼šMATU2745683+ ETA : 7/13",
bodyText: "新增转仓,请留æ„<C3A6>\n柜å<C593>·ï¼šMATU2745683 æ›´æ–°æ´¾é€<C3A9>å<EFBFBD>•,请查收",
receivedAt: "2026-07-14T08:02:00.000Z",
sampleDir: "邮件2",
attachPolicy: "packing_xlsx",
nameIncludes: ["MATU2745683"],
},
{
key: "mail3",
messageId: "<sample-mail3-whsu5574991@local.test>",
fromAddr: "clx@cnwally.com.cn",
subject:
"Fw: 转å<C2AC>‘:派é€<C3A9>è¦<C3A8>求更æ–? 拆柜清å<E280A6>•æ›´æ–°ï¼?DO请查æ”?拆柜清å<E280A6>•æ›´æ–°: WHSU5574991+WHL063G550810+船å<C2B9><C3A5>航次:OOCL SINGAPORE / 065W+ETAï¼?/28+FedEx-29ä»?UPS-102ä»?亚马逊å<C5A0>¡æ´?289ä»?ç§<C3A7>人地å<C2B0>€-166ä»?拦截-209ä»?拆柜清å<E280A6>•",
bodyText: [
"æ´¾é€<EFBFBD>è¦<EFBFBD>求更新,请查æ”?DO 与拆柜清å<EFBFBD>•ã€?,
"柜å<C593>·ï¼šWHSU5574991",
"æ<><C3A6>å<EFBFBD>•:WHL063G550810",
"船å<C2B9><C3A5>航次:OOCL SINGAPORE / 065W",
"ETAï¼?026-06-28",
"FedEx-29ä»?UPS-102ä»?亚马逊å<EFBFBD>¡æ´?289ä»?ç§<EFBFBD>人地å<EFBFBD>€-166ä»?拦截-209ä»?,
].join("\n"),
receivedAt: "2026-07-14T08:02:00.000Z",
sampleDir: "邮件3",
// å<>¡è½¬æµ?PDF 入库供详情展示;解æž<C3A6>ä»<C3A4>走主题短路,ä¸<C3A4>å<EFBFBD>ƒè¡¨æ ? attachPolicy: "business",
},
{
key: "mail4",
messageId: "<sample-mail4-label-yt2604021091@local.test>",
fromAddr: "xinchenze002@126.com",
subject:
"Fw: 转å<C2AC>‘:新增预æŠ?36:新辰泽+WHLC027G597465+WHSU8127240+æ´›æ<E280BA>‰çŸ?40HQ+EDT2026.04-25 ETA2026.05-15船å<C2B9><C3A5>航次HMM EMERALD 013E+æ<><C3A6>拆派组å<E2809E>ˆæŸœ-ä¸<C3A4>带托架",
bodyText: [
"Dear,",
"原箱å<EFBFBD>·YT2604021091=FBA199R49LD6-MDW2=76仿“<EFBFBD>作指ä»?,
"æ<EFBFBD>¢æ ‡å<EFBFBD>Žå<EFBFBD>•å<EFBFBD>·ï¼šFBA19HW52S0L-2G1MCB6X-HIA1=76ä»?,
"1:覆盖贴FBA标签,一箱贴两张",
"2:覆盖贴SKU标签,一�张(FNSKU:X003UHDF7B�04PCS))",
"3:贴好æ‹<C3A6>照回传等国内客户确认å<C2A4>Žï¼Œå†<C3A5>约仓å<E2809C>¡æ´¾äº¤ä»˜ï¼<C3AF>",
"注:回传过æ<EFBFBD>¥çš„照片需è¦<EFBFBD>清晰æ‹<EFBFBD>ç…§SKU上é<EFBFBD>¢çš„字迹,客户确认å<EFBFBD>Žå†<EFBFBD>安排å<EFBFBD>¡æ´¾äº¤ä»˜ï¼Œä¸<EFBFBD>确认ä¸<EFBFBD>予安排ï¼<EFBFBD>ï¼<EFBFBD>ï¼?,
].join("\n"),
receivedAt: "2026-07-14T08:00:00.000Z",
sampleDir: "邮件4",
attachPolicy: "label_instruction",
},
];
async function resolveSampleDir(sampleDir: string): Promise<string | null> {
const direct = path.join(DOC_MAIL_ROOT, sampleDir);
try {
const st = await fs.stat(direct);
if (st.isDirectory()) return direct;
} catch {
/* fall through */
}
// 兜底:在 docx/邮件 下按目录å<E280A2><C3A5>包å<E280A6>«åŒ¹é…? try {
const entries = await fs.readdir(DOC_MAIL_ROOT, { withFileTypes: true });
for (const e of entries) {
if (e.isDirectory() && (e.name === sampleDir || e.name.includes(sampleDir))) {
return path.join(DOC_MAIL_ROOT, e.name);
}
}
} catch {
return null;
}
return null;
}
/** 递归收集目录内文件(å<CB86>«ä¸€å±‚å­<C3A5>目录,兼容未平铺旧结构) */
async function listFilesRecursive(
root: string,
): Promise<Array<{ abs: string; filename: string; rel: string }>> {
const out: Array<{ abs: string; filename: string; rel: string }> = [];
const stack = [root];
while (stack.length) {
const cur = stack.pop()!;
let entries: Awaited<ReturnType<typeof fs.readdir>>;
try {
entries = await fs.readdir(cur, { withFileTypes: true });
} catch {
continue;
}
for (const e of entries) {
const full = path.join(cur, e.name);
if (e.isDirectory()) {
stack.push(full);
continue;
}
out.push({
abs: full,
filename: e.name,
rel: path.relative(root, full).split(path.sep).join("/"),
});
}
}
return out;
}
async function upsertMail(sample: SampleDef): Promise<bigint> {
const rawHash = createHash("sha256")
.update(`sample|${sample.key}|${sample.subject}|${sample.bodyText}|fullbody-v1`)
.digest("hex");
const existing = await prisma.mailMessage.findFirst({
where: {
OR: [{ messageId: sample.messageId }, { rawHash }],
},
});
if (existing) {
await prisma.importCompensation.deleteMany({
where: { import: { mailId: existing.id } },
});
await prisma.containerImport.deleteMany({ where: { mailId: existing.id } });
await prisma.parseResult.deleteMany({ where: { mailId: existing.id } });
await prisma.mailAttachment.deleteMany({ where: { mailId: existing.id } });
await prisma.mailMessage.update({
where: { id: existing.id },
data: {
subject: sample.subject.slice(0, 512),
fromAddr: sample.fromAddr,
bodyText: sample.bodyText,
ocrText: null,
status: "FETCHED",
mailType: "UNKNOWN",
lastError: null,
rawHash,
receivedAt: new Date(sample.receivedAt),
typeEvidence: {
sample_dir: `docx/邮件/${sample.sampleDir}`,
note: "sample_ingest",
sample_key: sample.key,
attach_policy: sample.attachPolicy,
},
version: { increment: 1 },
},
});
return existing.id;
}
const created = await prisma.mailMessage.create({
data: {
messageId: sample.messageId,
subject: sample.subject.slice(0, 512),
fromAddr: sample.fromAddr,
bodyText: sample.bodyText,
status: "FETCHED",
mailType: "UNKNOWN",
rawHash,
folder: "INBOX",
receivedAt: new Date(sample.receivedAt),
isThreadRoot: true,
typeEvidence: {
sample_dir: `docx/邮件/${sample.sampleDir}`,
note: "sample_ingest",
sample_key: sample.key,
attach_policy: sample.attachPolicy,
},
},
});
return created.id;
}
function contentTypeFor(filename: string): string {
if (/\.xlsx?$/i.test(filename)) {
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}
if (/\.csv$/i.test(filename)) return "text/csv";
if (/\.pdf$/i.test(filename)) return "application/pdf";
return "application/octet-stream";
}
async function attachFiles(
mailId: bigint,
files: Array<{ abs: string; filename: string }>,
): Promise<void> {
if (!files.length) return;
const dir = path.join(process.cwd(), "data", "mails", String(mailId));
await fs.mkdir(dir, { recursive: true });
for (const f of files) {
const buf = await fs.readFile(f.abs);
const hash = sha256(buf);
const safe = f.filename.replace(/[<>:"|?*\x00-\x1f]/g, "_");
const dest = path.join(dir, safe);
await fs.writeFile(dest, buf);
const rel = path.relative(process.cwd(), dest).replace(/\\/g, "/");
await prisma.mailAttachment.create({
data: {
mailId,
filename: f.filename.slice(0, 512),
contentType: contentTypeFor(f.filename),
sha256: hash,
path: rel,
size: buf.length,
rejected: false,
},
});
}
}
async function main() {
const results: Array<Record<string, unknown>> = [];
for (const sample of SAMPLES) {
const sampleDir = await resolveSampleDir(sample.sampleDir);
let bodyText = sample.bodyText;
if (sampleDir) {
const pdfBody = await loadMailboxPdfBody(sampleDir);
if (pdfBody && pdfBody.length > bodyText.length) {
bodyText = pdfBody;
console.log(
`${sample.key}: body from mailbox PDF (${pdfBody.length} chars)`,
);
}
}
const mailId = await upsertMail({ ...sample, bodyText });
let attached: string[] = [];
if (!sampleDir) {
console.warn(`${sample.key}: sample dir not found: ${sample.sampleDir}`);
} else if (sample.attachPolicy !== "none") {
const files = await listFilesRecursive(sampleDir);
const selected = selectAttachments(
files,
sample.attachPolicy,
sample.nameIncludes,
);
await attachFiles(mailId, selected);
attached = selected.map((f) => f.filename);
console.log(
`${sample.key}: dir=${sample.sampleDir} policy=${sample.attachPolicy} attached=${attached.length ? attached.join(" | ") : "(none)"}`,
);
} else {
console.log(`${sample.key}: attachPolicy=none`);
}
await ParsePipeline.run(mailId);
const mail = await prisma.mailMessage.findUnique({
where: { id: mailId },
include: { parseResult: true, attachments: true },
});
const header = mail?.parseResult?.containerHeader as {
F_ContainerNo?: string;
F_BLCopyCode?: string;
F_ETA?: string;
} | null;
const lineage = mail?.parseResult?.lineage as {
mail_record?: { kind?: string; summary?: string };
note?: string;
} | null;
const shipments = (mail?.parseResult?.shipments as unknown[]) || [];
const row = {
key: sample.key,
id: String(mailId),
status: mail?.status,
mail_type: mail?.mailType,
container_no: header?.F_ContainerNo,
bl: header?.F_BLCopyCode,
eta: header?.F_ETA,
shipments: shipments.length,
attachments: mail?.attachments.length ?? 0,
attached_names: attached,
record_kind: lineage?.mail_record?.kind,
parse_note: lineage?.note,
summary: lineage?.mail_record?.summary?.slice(0, 160),
url: `http://localhost:3100/mails/${mailId}`,
};
results.push(row);
console.log(JSON.stringify(row, null, 2));
}
console.log("\n=== summary ===");
for (const r of results) {
console.log(
`${r.key}\t#${r.id}\t${r.mail_type}\t${r.status}\tatt=${r.attachments}\t${r.url}`,
);
}
}
main()
.catch((err) => {
console.error(err);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,46 +0,0 @@
/**
* 将库内已拉入的非预报/装箱相关邮件标为 IGNORED(列表默认隐藏)<EFBFBD>?
* Usage: pnpm exec tsx scripts/mark-non-business-ignored.ts
*/
import { PrismaClient } from "@prisma/client";
import { isBusinessRelevantMail } from "../src/services/parse/classify";
import { NON_BUSINESS_SKIP } from "../src/services/imap/poller";
const prisma = new PrismaClient();
async function main() {
const mails = await prisma.mailMessage.findMany({
where: { status: { not: "IGNORED" } },
include: { attachments: { select: { filename: true } } },
});
let marked = 0;
for (const m of mails) {
const relevant = isBusinessRelevantMail({
subject: m.subject,
body: m.bodyText || "",
filenames: m.attachments.map((a) => a.filename),
});
if (relevant) continue;
await prisma.mailMessage.update({
where: { id: m.id },
data: {
status: "IGNORED",
lastError: NON_BUSINESS_SKIP,
version: { increment: 1 },
},
});
marked += 1;
console.log(
`IGNORED id=${m.id} from=${m.fromAddr} subject=${m.subject.slice(0, 80)}`,
);
}
console.log(`done: scanned=${mails.length} marked=${marked}`);
}
main()
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());

@ -1,76 +0,0 @@
/**
* Align classify with 附件下载_邮件识别 gold:
* - 新增预报 / 请查收新增预<EFBFBD>?on subject+body
* - 数据模版 / packing-list xlsx -> NEW_CONTAINER score
*/
import fs from "fs";
const path = "src/services/parse/classify.ts";
let src = fs.readFileSync(path, "utf8");
const nl = src.includes("\r\n") ? "\r\n" : "\n";
const oldForecast = ` if (/\\u65b0\\u589e\\u9884\\u62a5/.test(subject)) {
signals.push({
signal: "\\u65b0\\u589e\\u9884\\u62a5",
score: 50,
matched: "\\u65b0\\u589e\\u9884\\u62a5",
source: "subject",
});
scores.NEW_CONTAINER += 50;
}`;
// Match actual Chinese in file
const oldForecastRe =
/ if \(\/新增预报\/\.test\(subject\)\) \{\r?\n signals\.push\(\{\r?\n signal: "新增预报",\r?\n score: 50,\r?\n matched: "新增预报",\r?\n source: "subject",\r?\n \}\);\r?\n scores\.NEW_CONTAINER \+= 50;\r?\n \}/;
const newForecast = ` if (/新增预报|请查收新增预<E5A29E>?.test(text)) {
signals.push({
signal: "新增预报",
score: 50,
matched: "新增预报",
source: /新增预报|请查收新增预<EFBFBD>?.test(subject) ? "subject" : "body",
});
scores.NEW_CONTAINER += 50;
}
// 金样:数据模<E68DAE>?/ 卡派清单<E6B885>?xlsx <20>?预报<E9A284>?
const forecastPacking = filenames.some(
(f) =>
/数据模版/i.test(f) ||
(/卡派|装箱|packing|货件清单|预报资料/i.test(f) &&
/\\.(xlsx|xls|csv)$/i.test(f)),
);
if (forecastPacking && scores.NEW_CONTAINER < 40) {
signals.push({
signal: "预报清单附件",
score: 45,
matched: filenames.find((f) => /数据模版|卡派|装箱|packing/i.test(f)) || "xlsx",
source: "attachment",
});
scores.NEW_CONTAINER += 45;
} else if (forecastPacking) {
scores.NEW_CONTAINER += 15;
signals.push({
signal: "预报清单附件",
score: 15,
matched: "xlsx",
source: "attachment",
});
}`;
if (!oldForecastRe.test(src)) {
const i = src.indexOf("新增预报");
console.log("near", JSON.stringify(src.slice(i - 30, i + 200)));
throw new Error("forecast block not found");
}
src = src.replace(oldForecastRe, newForecast);
// Also treat 数据模版 as packing boost lead (hasPacking already 卡派资料 only)
const oldHasPacking = ` const hasPacking = /卡派资料/.test(fileText);`;
const newHasPacking = ` const hasPacking = /卡派资料|数据模版/.test(fileText);`;
if (!src.includes(oldHasPacking)) throw new Error("hasPacking missing");
src = src.replace(oldHasPacking, newHasPacking);
fs.writeFileSync(path, src);
console.log("classify patched");

@ -1,37 +0,0 @@
import fs from "fs";
const path = "src/services/parse/classify.ts";
let src = fs.readFileSync(path, "utf8");
const re =
/ \/\/ 金样:[\s\S]*?matched: "xlsx",\r?\n source: "attachment",\r?\n \}\);\r?\n \}/;
const neu = ` // 金样:数据模<E68DAE>?/ 明确预报清单附件 <20>?NEW_CONTAINER(不把单纯「卡派资料」当预报<E9A284>?
const forecastPacking = filenames.some(
(f) =>
/数据模版/i.test(f) ||
(/预报资料|货件清单|装箱清单/i.test(f) &&
/\\.(xlsx|xls|csv)$/i.test(f)),
);
if (forecastPacking) {
const add = scores.NEW_CONTAINER < 40 ? 45 : 15;
signals.push({
signal: "预报清单附件",
score: add,
matched:
filenames.find((f) =>
/数据模版|预报资料|货件清单|装箱清单/i.test(f),
) || "xlsx",
source: "attachment",
});
scores.NEW_CONTAINER += add;
}`;
if (!re.test(src)) {
const i = src.indexOf("金样");
console.log("fail", i, JSON.stringify(src.slice(i, i + 250)));
process.exit(1);
}
src = src.replace(re, neu);
fs.writeFileSync(path, src);
console.log("narrowed ok");

@ -1,21 +0,0 @@
import fs from "fs";
const path = "src/services/parse/classify.ts";
let src = fs.readFileSync(path, "utf8");
// normalize for matching
const nl = src.includes("\r\n") ? "\r\n" : "\n";
const softNeedle = `for (const kw of ["\u62c6\u67dc\u6e05\u5355", "\u5361\u8f6c\u6d77", "\u6d3e\u9001\u8981\u6c42", "\u6539\u81ea\u63d0", "\u7559\u4ed3"]) {`;
const softRepl = `for (const kw of ["\u62c6\u67dc\u6e05\u5355", "\u5361\u8f6c\u6d77", "\u6d3e\u9001\u8981\u6c42", "\u6539\u81ea\u63d0", "\u66f4\u65b0\u6d3e\u9001\u5355", "\u7559\u4ed3"]) {`;
if (!src.includes(softNeedle)) throw new Error("soft list missing");
src = src.replace(softNeedle, softRepl);
const oldTail = ` if (bestScore < 40) best = "UNKNOWN";${nl}${nl} return { total: bestScore, mail_type: best, signals, scores };${nl}}`;
const newTail = ` if (bestScore < 40) best = "UNKNOWN";${nl}${nl} // mail1-like: container / booking only -> WORK_ORDER${nl} if (${nl} best === "UNKNOWN" &&${nl} (/[A-Z]{4}\\d{7}/.test(text) || /\u9884\u7ea6\u7801/.test(text))${nl} ) {${nl} best = "WORK_ORDER";${nl} bestScore = 40;${nl} signals.push({${nl} signal: "\u65e0\u5173\u952e\u8bcd\u515c\u5e95",${nl} score: 40,${nl} matched: iso?.[0] || "\u9884\u7ea6\u7801",${nl} source: "body",${nl} });${nl} }${nl}${nl} return { total: bestScore, mail_type: best, signals, scores };${nl}}`;
if (!src.includes(oldTail)) throw new Error("tail missing: " + JSON.stringify(src.slice(src.indexOf("bestScore < 40"), src.indexOf("bestScore < 40") + 100)));
src = src.replace(oldTail, newTail);
fs.writeFileSync(path, src);
console.log("classify ok");

@ -1,29 +0,0 @@
import fs from "fs";
const path = "src/services/parse/instruction-lexicon.ts";
let src = fs.readFileSync(path, "utf8");
const re =
/\/\/ date[\s\S]*?!BODY_ACTION_RE\.test\(bodyOnly\)\s*\n\s*\) \{\s*\n\s*return true;\s*\n\s*\}/;
if (!re.test(src)) {
// try alternate
const idx = src.indexOf("date / QQ");
console.log("snippet", JSON.stringify(src.slice(idx, idx + 280)));
throw new Error("block not found");
}
src = src.replace(
re,
`// date middle-forward: no Dear = shell (subject may contain \u62c6\u67dc/\u62e6\u622a noise)
if (
/date@usasinogroup\\.com/i.test(compact) &&
/\u5bc4\u4ef6\u4eba|\u4e3b\u65e8/.test(compact) &&
!/Dear[,,]/.test(compact)
) {
return true;
}`,
);
fs.writeFileSync(path, src);
console.log("date shell rule updated");

@ -1,157 +0,0 @@
/**
* Detail page: show full body_text + separate OCR; no silent truncation.
*/
import fs from "fs";
const path = "src/app/(ops)/mails/[id]/page.tsx";
let src = fs.readFileSync(path, "utf8");
const old = ` <Collapse
items={[
{
key: "body",
label: "原始正文(排查用<EFBFBD>?,
children: (
<pre
className="mono"
style={{
maxHeight: 280,
overflow: "auto",
margin: 0,
whiteSpace: "pre-wrap",
lineHeight: 1.5,
fontSize: 12,
}}
>
{mail.body_text ||
mail.ocr_text ||
"(无正<E697A0>?"}
</pre>
),
},
]}
/>`;
const neu = ` <Collapse
defaultActiveKey={
!(mail.body_text || "").trim() && (mail.ocr_text || "").trim()
? ["body"]
: undefined
}
items={[
{
key: "body",
label: \`原始正文(排查用)<EFBFBD>?\${(mail.body_text || "").length} 字\${
(mail.ocr_text || "").trim()
? \` · OCR \${(mail.ocr_text || "").length} 字\`
: ""
}\`,
children: (
<div>
<Typography.Paragraph
type="secondary"
style={{ marginBottom: 8, fontSize: 12 }}
>
入库原文完整保留,解<EFBFBD>?OCR 不会覆盖此字段。可滚动查看全部内容<EFBFBD>?
</Typography.Paragraph>
<pre
className="mono"
style={{
maxHeight: 480,
overflow: "auto",
margin: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
lineHeight: 1.5,
fontSize: 12,
padding: 12,
background: "rgba(0,0,0,0.02)",
borderRadius: 6,
}}
>
{(mail.body_text || "").trim()
? mail.body_text
: "(无正<E697A0>?"}
</pre>
{(mail.ocr_text || "").trim() ? (
<>
<Typography.Text
strong
style={{ display: "block", marginTop: 16, marginBottom: 8 }}
>
OCR 文本(附件识别,与原文分开保存)·{" "}
{(mail.ocr_text || "").length} <EFBFBD>?
</Typography.Text>
<pre
className="mono"
style={{
maxHeight: 320,
overflow: "auto",
margin: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
lineHeight: 1.5,
fontSize: 12,
padding: 12,
background: "rgba(0,0,0,0.02)",
borderRadius: 6,
}}
>
{mail.ocr_text}
</pre>
</>
) : null}
</div>
),
},
]}
/>`;
if (!src.includes('label: "原始正文(排查用<E69FA5>?')) {
console.error("collapse block missing");
process.exit(1);
}
// Replace by finding key markers
const start = src.indexOf('key: "body"');
const labelIdx = src.lastIndexOf("Collapse", start);
const collapseStart = src.lastIndexOf("<Collapse", start);
const endMarker = "TypeOverrideModal";
const end = src.indexOf(endMarker, start);
if (collapseStart < 0 || end < 0) {
console.error("bounds", collapseStart, end);
process.exit(1);
}
// find closing of Collapse before TypeOverrideModal
const collapseEnd = src.lastIndexOf("/>", end);
// better: match from <Collapse to /> that closes it
const slice = src.slice(collapseStart, end);
const closeRel = slice.indexOf("/>");
if (closeRel < 0) {
console.error("no close");
process.exit(1);
}
// Might be wrong close - find ` />\n\n <TypeOverride`
const m = src.slice(collapseStart).match(/^[\s\S]*?\n \/>\n\n <TypeOverrideModal/);
if (!m) {
// try without blank
const m2 = src.slice(collapseStart).match(/^[\s\S]*?\n \/>\r?\n\r?\n?\s*<TypeOverrideModal/);
if (!m2) {
console.error("pattern fail", JSON.stringify(src.slice(collapseStart, collapseStart + 200)));
process.exit(1);
}
src =
src.slice(0, collapseStart) +
neu +
"\n\n <TypeOverrideModal" +
src.slice(collapseStart + m2[0].length - "<TypeOverrideModal".length);
} else {
src =
src.slice(0, collapseStart) +
neu +
"\n\n <TypeOverrideModal" +
src.slice(collapseStart + m[0].length - "<TypeOverrideModal".length);
}
fs.writeFileSync(path, src);
console.log("detail body UI updated");

@ -1,63 +0,0 @@
/**
* DO_UPLOAD + bl-plus: keep customer_name from 客户+提单+柜号 template
*/
import fs from "fs";
const path = "src/services/parse/work-order-record.ts";
let src = fs.readFileSync(path, "utf8");
const old = ` if (input.mailType === "NEW_CONTAINER" && input.blModules) {
return buildBlForecastRecord({
modules: {
...input.blModules,
container_no:
input.blModules.container_no || input.containerNo || undefined,
},
shipments,
source: shipments.length ? "mixed" : "plus_template",
plusPayload: input.plusPayload || undefined,
});
}`;
const neu = ` // DO \u4e3b\u7c7b\u578b\u65f6\u4ecd\u4fdd\u7559\u63d0\u5355\u6a21\u677f\uff08\u5ba2\u6237\u540d\u7b49\uff09\u2014\u2014\u91d1\u6837\u9884\u62a5+DO \u540c\u5c01
if (
input.blModules &&
(input.mailType === "NEW_CONTAINER" || input.mailType === "DO_UPLOAD")
) {
const record = buildBlForecastRecord({
modules: {
...input.blModules,
container_no:
input.blModules.container_no || input.containerNo || undefined,
},
shipments,
source: shipments.length ? "mixed" : "plus_template",
plusPayload: input.plusPayload || undefined,
});
if (input.mailType === "DO_UPLOAD") {
return {
...record,
summary: record.summary.replace(
"\u65b0\u589e\u9884\u62a5\uff08\u63d0\u5355\u6a21\u677f\uff09",
"\u4e0a\u4f20DO\uff08\u542b\u9884\u62a5\u6a21\u677f\uff09",
),
};
}
return record;
}`;
if (!src.includes(old)) {
// try CRLF
const oldCrlf = old.replace(/\n/g, "\r\n");
if (src.includes(oldCrlf)) {
src = src.replace(oldCrlf, neu.replace(/\n/g, "\r\n"));
} else {
console.error("block not found");
process.exit(1);
}
} else {
src = src.replace(old, neu);
}
fs.writeFileSync(path, src);
console.log("buildMailRecord patched");

@ -1,47 +0,0 @@
import fs from "fs";
const path = "src/components/CcForecastFormOrder.tsx";
let src = fs.readFileSync(path, "utf8");
if (src.includes("PACKING_FILL_TIPS")) {
console.log("already");
process.exit(0);
}
src = src.replace(
/} from "@\/constants\/ui-copy";\r?\n/,
`} from "@/constants/ui-copy";\r\nimport { PACKING_FILL_TIPS } from "@/services/parse/packing-fill-rules";\r\n`,
);
const re =
/(<div className="cc-fo-step2">\r?\n)(\s*<div className="cc-fo-toolbar">)/;
if (!re.test(src)) throw new Error("step2 not found");
src = src.replace(
re,
`$1 <div
className="cc-fo-packing-tips"
style={{
marginBottom: 10,
padding: "8px 10px",
fontSize: 12,
lineHeight: 1.55,
color: "#4b5563",
background: "rgba(37,99,235,0.06)",
borderRadius: 6,
}}
>
<div style={{ fontWeight: 600, marginBottom: 4, color: "#1f2937" }}>
\u8d27\u4ef6\u586b\u5199\u987b\u77e5\uff08\u5df2\u5199\u5165\u7cfb\u7edf\u6821\u9a8c\uff1b\u6a21\u677f\u300c\u6ce8\u610f\u300d\u8bf4\u660e\u884c\u4e0d\u5165\u8868\uff09
</div>
<ol style={{ margin: 0, paddingLeft: 18 }}>
{PACKING_FILL_TIPS.map((t) => (
<li key={t}>{t}</li>
))}
</ol>
</div>
$2`,
);
fs.writeFileSync(path, src);
console.log("ok", src.includes("PACKING_FILL_TIPS.map"));

@ -1,71 +0,0 @@
/**
* Wire sample ingest to use full mailbox PDF text as bodyText.
*/
import fs from "fs";
const path = "scripts/ingest-sample-mails.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("loadMailboxPdfBody")) {
src = src.replace(
`import { sha256 } from "@/utils/hash";`,
`import { sha256 } from "@/utils/hash";
import { loadMailboxPdfBody } from "./lib/load-mailbox-pdf-body";`,
);
}
// bump hash version so re-ingest refreshes body
src = src.replace(
'`.update(`sample|${sample.key}|${sample.subject}|${sample.bodyText}`)',
'`.update(`sample|${sample.key}|${sample.subject}|${sample.bodyText}|fullbody-v1`)',
);
// the above might be wrong quoting - fix:
src = src.replace(
"sample|${sample.key}|${sample.subject}|${sample.bodyText}`",
"sample|${sample.key}|${sample.subject}|${sample.bodyText}|fullbody-v1`",
);
const oldLoop = ` for (const sample of SAMPLES) {
const mailId = await upsertMail(sample);
const sampleDir = await resolveSampleDir(sample.sampleDir);`;
const neuLoop = ` for (const sample of SAMPLES) {
const sampleDir = await resolveSampleDir(sample.sampleDir);
let bodyText = sample.bodyText;
if (sampleDir) {
const pdfBody = await loadMailboxPdfBody(sampleDir);
if (pdfBody && pdfBody.length > bodyText.length) {
bodyText = pdfBody;
console.log(
\`\${sample.key}: body from mailbox PDF (\${pdfBody.length} chars)\`,
);
}
}
const mailId = await upsertMail({ ...sample, bodyText });`;
if (!src.includes(oldLoop)) {
console.error("loop missing");
process.exit(1);
}
src = src.replace(oldLoop, neuLoop);
// remove duplicate sampleDir resolve
src = src.replace(
` const mailId = await upsertMail({ ...sample, bodyText });
let attached: string[] = [];
if (!sampleDir) {
console.warn(\`\${sample.key}: sample dir not found: \${sample.sampleDir}\`);
} else if (sample.attachPolicy !== "none") {
const files = await listFilesRecursive(sampleDir);`,
` const mailId = await upsertMail({ ...sample, bodyText });
let attached: string[] = [];
if (!sampleDir) {
console.warn(\`\${sample.key}: sample dir not found: \${sample.sampleDir}\`);
} else if (sample.attachPolicy !== "none") {
const files = await listFilesRecursive(sampleDir);`,
);
fs.writeFileSync(path, src);
console.log("ingest wired", src.includes("loadMailboxPdfBody"), src.includes("fullbody-v1"));

@ -1,138 +0,0 @@
/**
* Product rules:
* 1) Only latest instruction segment confirmable; history readonly
* 2) Soft keywords stay work_order
* 3) No-keyword mail (mail1) -> work_order fallback
*/
import fs from "fs";
const path = "src/services/parse/split-instructions.ts";
let src = fs.readFileSync(path, "utf8");
const marker = " // 2) ";
const idx = src.indexOf(marker);
if (idx < 0) throw new Error("section 2 marker missing");
const attMarker = " // 3) ";
const attIdx = src.indexOf(attMarker, idx);
if (attIdx < 0) throw new Error("section 3 marker missing");
const sortMarker = " // \u5c55\u793a\u987a\u5e8f\uff1a\u5f53\u524d\u6bb5\u4f18\u5148";
const sortIdx = src.indexOf(sortMarker);
if (sortIdx < 0) throw new Error("sort marker missing");
const section2 = ` // 2) \u6b63\u6587\u6309\u5bf9\u8bdd\u6bb5\u62c6\u5206\uff1b\u4ec5\u300c\u6700\u65b0\u4e00\u6761\u6709\u6307\u4ee4\u7684\u6bb5\u300d\u53ef\u786e\u8ba4\uff0c\u5386\u53f2\u53ea\u8bfb
const segments = splitBodySegments(body);
let currentSegIndex: number | null = null;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (isNonInstructionSegment(seg)) continue;
const kinds = detectKindsForSegment(extractSegmentSubject(seg, ""), seg);
if (kinds.length) {
currentSegIndex = i;
break;
}
}
// \u65e0\u5173\u952e\u8bcd\u6bb5\uff1a\u9996\u4e2a\u975e\u58f3\u6bb5\u4f5c\u4e3a\u515c\u5e95\u5de5\u5355\u8f7d\u4f53
if (currentSegIndex === null) {
for (let i = 0; i < segments.length; i++) {
if (!isNonInstructionSegment(segments[i])) {
currentSegIndex = i;
break;
}
}
}
const kindsInCurrentBody = new Set<InstructionUiKind>();
segments.forEach((seg, i) => {
if (isNonInstructionSegment(seg)) return;
const segSubject = extractSegmentSubject(seg, "");
const kinds = detectKindsForSegment(segSubject, seg);
const isCurrent = currentSegIndex !== null && i === currentSegIndex;
if (!kinds.length) {
// \u90ae\u4ef61 \u7b49\uff1a\u65e0\u56db\u7c7b\u5173\u952e\u8bcd \u2192 \u6700\u65b0\u6bb5\u515c\u5e95\u4e3a\u5de5\u5355
if (isCurrent) {
kindsInCurrentBody.add("work_order");
push(
makeUnit({
uiKind: "work_order",
source: "body_segment",
segmentIndex: i,
isCurrent: true,
text: seg,
segmentSubject: segSubject || undefined,
extraKeywords: ["\u65e0\u5173\u952e\u8bcd\u515c\u5e95"],
}),
);
}
return;
}
for (const kind of kinds) {
if (isCurrent) kindsInCurrentBody.add(kind);
push(
makeUnit({
uiKind: kind,
source: "body_segment",
segmentIndex: i,
isCurrent,
text: seg,
segmentSubject: segSubject || undefined,
}),
);
}
});
`;
// Replace from section 2 through just before section 3
src = src.slice(0, idx) + section2 + src.slice(attIdx);
// Re-find markers after splice
const attIdx2 = src.indexOf(attMarker);
const pushAttRe =
/push\(\s*makeUnit\(\{\s*uiKind: kind,\s*source: "attachment",\s*segmentIndex: -2,\s*isCurrent: true,\s*text: f,\s*extraKeywords: \[f\.slice\(0, 40\)\],\s*\}\),\s*\);/;
if (!pushAttRe.test(src.slice(attIdx2, attIdx2 + 800))) {
throw new Error("attachment push block not found");
}
src = src.replace(
pushAttRe,
`const attachCurrent =
kindsInCurrentBody.size === 0 || !kindsInCurrentBody.has(kind);
push(
makeUnit({
uiKind: kind,
source: "attachment",
segmentIndex: -2,
isCurrent: Boolean(attachCurrent),
text: f,
extraKeywords: [f.slice(0, 40)],
}),
);`,
);
const sortIdx2 = src.indexOf(sortMarker);
if (sortIdx2 < 0) throw new Error("sort marker missing after edit");
const fallback = ` // \u6574\u5c01\u4ecd\u65e0\u5355\u5143 \u2192 \u5de5\u5355\u515c\u5e95
if (!units.length && (subject.trim() || body.trim())) {
push(
makeUnit({
uiKind: "work_order",
source: "record",
segmentIndex: -3,
isCurrent: true,
text: body.trim() || subject,
segmentSubject: subject || undefined,
extraKeywords: ["\u65e0\u5173\u952e\u8bcd\u515c\u5e95"],
}),
);
}
`;
src = src.slice(0, sortIdx2) + fallback + src.slice(sortIdx2);
fs.writeFileSync(path, src);
console.log("patched", path);

@ -1,57 +0,0 @@
import fs from "fs";
// packing-list test
{
const path = "tests/unit/packing-list.test.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("skips \u6ce8\u610f instruction footer")) {
const insert = `
it("skips \u6ce8\u610f instruction footer in \u667a\u9e3f gold xlsx", async () => {
const fs = await import("fs/promises");
const path = await import("path");
const file = path.join(
process.cwd(),
"docx",
"\u90ae\u4ef6",
"\u6a21\u677f",
"\u9644\u4ef6\u4e0b\u8f7d_\u90ae\u4ef6\u8bc6\u522b",
"\u667a\u9e3f2+WHLC027G597465+WHSU8127240+\u6d1b\u6749\u77f6+40HQ+EDT2026.04-25 ETA2026.05-15\u8239\u540d\u822a\u6b21HMM EMERALD 013E+\u63d0\u62c6\u6d3e.xlsx",
);
const buf = await fs.readFile(file);
const parsed = await parsePackingListBuffer(buf, { enableIsoCheck: false });
expect(parsed.errorCode).toBeUndefined();
expect(parsed.shipments.length).toBe(1);
expect(parsed.shipments[0].row_status).toBe("VALID");
expect(parsed.shipments[0].F_Transporter).toBe("\u5b58\u4ed3");
expect(parsed.shipments[0].F_CTNS).toBe(1041);
expect(
parsed.shipments.every((s) => !String(s.F_FBACode || "").includes("\u6ce8\u610f")),
).toBe(true);
});
`;
src = src.replace(/\n\}\);\s*$/, `${insert}\n});\n`);
fs.writeFileSync(path, src);
console.log("packing test added");
}
}
// channel-map test
{
const path = "tests/unit/channel-map.test.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("ignores Truck inside instruction note")) {
src = src.replace(
/\n\}\);\s*$/,
`
it("ignores Truck inside instruction note", () => {
const note =
"\u6ce8\u610f\uff1a1.ups\u548cfedex\u7684\u4ef6... \u5982UPS/FEDEX/USPS/Truck \u6216 \u5361\u6d3e";
expect(mapChannel(note).transporter).toBe("");
});
});
`,
);
fs.writeFileSync(path, src);
console.log("channel test added");
}
}

@ -1,45 +0,0 @@
import fs from "fs";
const path = "src/services/parse/packing-list.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("function isInstructionNoiseRow")) {
const insertAt = src.indexOf("export async function parsePackingListBuffer");
if (insertAt < 0) throw new Error("fn missing");
const helper = `/** Template footer instruction row <20>?not a shipment */
export function isInstructionNoiseRow(raw: Record<string, unknown>): boolean {
const parts = Object.values(raw).map((v) => String(v ?? "").trim());
const blob = parts.join(" ");
if (!blob) return false;
if (/^\u6ce8\u610f\\s*[\uFF1A:]/.test(blob)) return true;
if (parts.some((p) => /^\u6ce8\u610f\\s*[\uFF1A:]/.test(p))) return true;
const channel = String(raw.F_Transporter ?? "").trim();
const fba = String(raw.F_FBACode ?? "").trim();
const addr = String(raw.F_Address ?? "").trim();
if (
(channel.length > 40 || fba.length > 40 || addr.length > 80) &&
/\u6ce8\u610f|\u586b\u5199|\u5355\u5143\u683c|\u5fc5\u987b\u586b\u5199/.test(blob)
) {
return true;
}
return false;
}
`;
src = src.slice(0, insertAt) + helper + src.slice(insertAt);
}
if (!src.includes("isInstructionNoiseRow(raw)")) {
const re = /if \(!hasAny\) continue;/;
if (!re.test(src)) throw new Error("no hasAny continue");
src = src.replace(
re,
"if (!hasAny) continue;\n if (isInstructionNoiseRow(raw)) continue;",
);
}
fs.writeFileSync(path, src);
console.log({
helper: src.includes("function isInstructionNoiseRow"),
call: src.includes("isInstructionNoiseRow(raw)"),
});

@ -1,75 +0,0 @@
/**
* Stop pipeline from overwriting bodyText with HTML-normalized / OCR-merged text.
* Parse still uses a local normalized body; OCR stays in ocrText only.
*/
import fs from "fs";
const path = "src/services/parse/pipeline.ts";
let src = fs.readFileSync(path, "utf8");
const oldNorm = ` const subject = mail.subject?.trim() ? mail.subject : "(无主<E697A0>?";
// 历史库可能存了原<E4BA86>?HTML;解析前先归一成纯文本
let body = plainTextFromMaybeHtml(mail.bodyText || "");
if (body && body !== (mail.bodyText || "").trim()) {
await prisma.mailMessage.update({
where: { id: mailId },
data: { bodyText: body },
});
}`;
const neuNorm = ` const subject = mail.subject?.trim() ? mail.subject : "(无主<E697A0>?";
// 解析用归一化正文;绝不回写 bodyText(排查用原文一字不落)
const bodyStored = mail.bodyText || "";
let body = plainTextFromMaybeHtml(bodyStored) || bodyStored;`;
if (!src.includes(oldNorm)) {
const oldCrlf = oldNorm.replace(/\n/g, "\r\n");
if (!src.includes(oldCrlf)) {
console.error("norm block missing");
process.exit(1);
}
src = src.replace(oldCrlf, neuNorm.replace(/\n/g, "\r\n"));
} else {
src = src.replace(oldNorm, neuNorm);
}
const oldOcr = ` body = body.trim()
? \`\${body}\\n\\n\${ocr.text}\`
: ocr.text;
await prisma.mailMessage.update({
where: { id: mailId },
data: { ocrText: ocr.text, bodyText: body },
});`;
const neuOcr = ` // OCR 只写<E58FAA>?ocrText,不污染 bodyText
if (!body.trim()) body = ocr.text;
else body = \`\${body}\\n\\n\${ocr.text}\`;
await prisma.mailMessage.update({
where: { id: mailId },
data: { ocrText: ocr.text },
});`;
if (!src.includes("ocrText: ocr.text, bodyText: body")) {
// try already patched
if (!src.includes("OCR 只写<E58FAA>?ocrText")) {
console.error("ocr block missing", src.includes("bodyText: body"));
process.exit(1);
}
} else {
src = src.replace(
/body = body\.trim\(\)\s*\?\s*`\$\{body\}\\n\\n\$\{ocr\.text\}`\s*:\s*ocr\.text;\s*await prisma\.mailMessage\.update\(\{\s*where: \{ id: mailId \},\s*data: \{ ocrText: ocr\.text, bodyText: body \},\s*\}\);/s,
`// OCR only in ocrText <20>?never overwrite bodyText
if (!body.trim()) body = ocr.text;
else body = \`\${body}\\n\\n\${ocr.text}\`;
await prisma.mailMessage.update({
where: { id: mailId },
data: { ocrText: ocr.text },
});`,
);
}
fs.writeFileSync(path, src);
console.log("pipeline body preserve ok", {
hasStored: src.includes("bodyStored"),
ocrOnly: src.includes("ocrText: ocr.text }") || src.includes("data: { ocrText: ocr.text }"),
});

@ -1,51 +0,0 @@
import fs from "fs";
const path = "src/utils/mail-body-text.ts";
let src = fs.readFileSync(path, "utf8");
const start = src.indexOf("export function resolveParsedMailBody");
if (start < 0) throw new Error("missing");
const neu = `export function resolveParsedMailBody(parsed: {
text?: string | false | null;
html?: string | false | null;
}): string {
const text =
typeof parsed.text === "string" ? parsed.text.trim() : "";
const htmlRaw =
typeof parsed.html === "string" ? parsed.html : "";
const fromHtml = htmlRaw ? htmlToPlainText(htmlRaw) : "";
if (!text && !fromHtml) return "";
if (!text) return fromHtml;
if (!fromHtml) return text;
// Prefer longer side so troubleshooting body is not truncated
if (fromHtml.length > text.length + 40) return fromHtml;
if (text.length > fromHtml.length + 40) return text;
const textHead = text.slice(0, Math.min(48, text.length));
if (textHead && fromHtml.includes(textHead) && fromHtml.length >= text.length) {
return fromHtml;
}
const htmlHead = fromHtml.slice(0, Math.min(48, fromHtml.length));
if (htmlHead && text.includes(htmlHead) && text.length >= fromHtml.length) {
return text;
}
// Both long and not nested: keep both parts
if (
text.length > 80 &&
fromHtml.length > 80 &&
!fromHtml.includes(textHead) &&
!text.includes(htmlHead)
) {
return \`\${text}\\n\\n---\\n\\n\${fromHtml}\`;
}
return fromHtml.length >= text.length ? fromHtml : text;
}
`;
src = src.slice(0, start) + neu;
fs.writeFileSync(path, src);
console.log("resolve ok, len", src.length);

@ -1,152 +0,0 @@
/**
* Fix: ignore subject/主旨 lines when detecting forward shells & kinds pollution
*/
import fs from "fs";
{
const path = "src/services/parse/instruction-lexicon.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("function stripSubjectLines")) {
const insertAt = src.indexOf("export function isForwardShellSegment");
if (insertAt < 0) throw new Error("isForwardShellSegment missing");
const helper = `/** Drop subject/\\u4e3b\\u65e8 lines so thread titles do not count as body actions */\nfunction stripSubjectLines(text: string): string {\n return text\n .split(/\\n/)\n .filter((line) => !/(?:\\u4e3b\\u9898|\\u4e3b\\u65e8|Subject)\\s*[\\uff1a:]/i.test(line))\n .join("\\n");\n}\n\n`;
// Use real Chinese in helper via unicode escapes already - wait use actual:
const helper2 = `/** Drop subject lines so thread titles do not count as body actions */\nfunction stripSubjectLines(text: string): string {\n return text\n .split(/\\n/)\n .filter((line) => !/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]/i.test(line))\n .join("\\n");\n}\n\n`;
src = src.slice(0, insertAt) + helper2 + src.slice(insertAt);
}
// Rewrite isForwardShellSegment body
const start = src.indexOf("export function isForwardShellSegment");
const end = src.indexOf("export function isNonInstructionSegment");
if (start < 0 || end < 0) throw new Error("shell funcs missing");
const replacement = `export function isForwardShellSegment(text: string): boolean {
const compact = text.replace(/\\s+/g, " ").trim();
if (/\u53d1\u81ea\u6211\u7684iPhone/.test(compact)) return true;
const bodyOnly = stripSubjectLines(text);
const bodyCompact = bodyOnly.replace(/\\s+/g, " ").trim();
// date / QQ pure forward wrapper (keywords only in \u4e3b\u65e8)
if (
/date@usasinogroup\\.com/i.test(compact) &&
!/Dear[,,]/.test(bodyOnly) &&
!BODY_ACTION_RE.test(bodyOnly)
) {
return true;
}
if (
/\u53d1\u81ea\u6211\u7684iPhone|\u5927\u5c0f\\s*\\d/.test(compact) &&
!BODY_ACTION_RE.test(bodyOnly) &&
!/\u65b0\u589e\u9884\u62a5|\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5|\u65b0\u589e\u8f6c\u4ed3|DO\u8bf7\u67e5\u6536/.test(
bodyOnly,
)
) {
return true;
}
// short date forward: only headers + signature
if (
/\u5bc4\u4ef6\u4eba/.test(compact) &&
/date@usasinogroup\\.com/i.test(compact) &&
bodyCompact.length < 120 &&
!/Dear[,,]/.test(bodyOnly)
) {
return true;
}
return false;
}
`;
src = src.slice(0, start) + replacement + src.slice(end);
// Also update isNonInstructionSegment QQ shell to use stripSubjectLines
src = src.replace(
` // QQ \u5916\u58f3\uff1a\u4ec5\u8f6c\u53d1\u5143\u6570\u636e\u3001\u65e0\u4e1a\u52a1\u52a8\u4f5c
if (
/\u53d1\u81ea\u6211\u7684iPhone|\u5927\u5c0f\\s*\\d/.test(compact) &&
!BODY_ACTION_RE.test(compact) &&
!/\u65b0\u589e\u9884\u62a5|\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5|\u65b0\u589e\u8f6c\u4ed3|DO\u8bf7\u67e5\u6536/.test(compact)
) {
return true;
}`,
` // QQ shell without body actions (ignore subject title noise)
{
const bodyOnly = stripSubjectLines(text);
if (
/\u53d1\u81ea\u6211\u7684iPhone|\u5927\u5c0f\\s*\\d/.test(compact) &&
!BODY_ACTION_RE.test(bodyOnly) &&
!/\u65b0\u589e\u9884\u62a5|\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5|\u65b0\u589e\u8f6c\u4ed3|DO\u8bf7\u67e5\u6536/.test(
bodyOnly,
)
) {
return true;
}
}`,
);
fs.writeFileSync(path, src);
console.log("lexicon shell ok");
}
{
const path = "src/services/parse/split-instructions.ts";
let src = fs.readFileSync(path, "utf8");
// Strengthen bodyForKindDetect: strip ALL subject lines + Re:新增预报 pollution lines
const old = `function bodyForKindDetect(segBody: string): string {
if (!BODY_ACTION_RE.test(segBody)) return segBody;
return segBody
.split(/\\n/)
.filter(
(line) =>
!/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:].*(?:\u65b0\u589e\u9884\u62a5|Fw:|\u8f6c\u53d1)/i.test(line),
)
.join("\\n");
}`;
const neu = `function bodyForKindDetect(segBody: string): string {
// Always strip subject/Re title lines; they carry historical \u65b0\u589e\u9884\u62a5
const stripped = segBody
.split(/\\n/)
.filter((line) => {
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]/i.test(line)) return false;
if (/^Re:\\s*Re:/i.test(line.trim()) && /\u65b0\u589e\u9884\u62a5/.test(line))
return false;
return true;
})
.join("\\n");
return stripped;
}`;
if (!src.includes("function bodyForKindDetect")) throw new Error("no bodyForKindDetect");
// replace by function bounds
const a = src.indexOf("function bodyForKindDetect");
const b = src.indexOf("export function detectKindsForSegment");
if (a < 0 || b < 0) throw new Error("bounds");
src = src.slice(0, a) + neu + "\n\n" + src.slice(b);
// useSubject: only when body (stripped) has no action words
src = src.replace(
` const useSubject =
!BODY_ACTION_RE.test(segBody) ||
/\\u8bf7\\u67e5\\u6536\\u65b0\\u589e\\u9884\\u62a5/.test(bodyText);`,
` const useSubject =
!BODY_ACTION_RE.test(bodyText) ||
/\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5/.test(bodyText);`,
);
// Also fix the actual Chinese version if unicode escape didn't match
src = src.replace(
/const useSubject =\s*!BODY_ACTION_RE\.test\(segBody\) \|\|\s*\/请查收新增预报\/\.test\(bodyText\);/,
`const useSubject =
!BODY_ACTION_RE.test(bodyText) ||
/\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5/.test(bodyText);`,
);
fs.writeFileSync(path, src);
console.log("split bodyForKindDetect ok");
}

@ -1,94 +0,0 @@
import fs from "fs";
{
const path = "src/services/parse/packing-list.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("function isInstructionNoiseRow")) {
const insertAt = src.indexOf("export async function parsePackingListBuffer");
if (insertAt < 0) throw new Error("fn missing");
const helper = `/** Template footer instruction row <20>?not a shipment */
export function isInstructionNoiseRow(raw: Record<string, unknown>): boolean {
const parts = Object.values(raw).map((v) => String(v ?? "").trim());
const blob = parts.join(" ");
if (!blob) return false;
if (/^\u6ce8\u610f\\s*[\\uFF1A:]/.test(blob)) return true;
if (parts.some((p) => /^\u6ce8\u610f\\s*[\\uFF1A:]/.test(p))) return true;
const channel = String(raw.F_Transporter ?? "").trim();
const fba = String(raw.F_FBACode ?? "").trim();
const addr = String(raw.F_Address ?? "").trim();
if (
(channel.length > 40 || fba.length > 40 || addr.length > 80) &&
/\u6ce8\u610f|\u586b\u5199|\u5355\u5143\u683c|\u5fc5\u987b\u586b\u5199/.test(blob)
) {
return true;
}
return false;
}
`;
src = src.slice(0, insertAt) + helper + src.slice(insertAt);
}
if (!src.includes("isInstructionNoiseRow(raw)")) {
src = src.replace(
/\/\/ \u8df3\u8fc7\u7a7a\u884c\r?\n\s*const hasAny = Object\.values\(raw\)\.some\(\r?\n\s*\(v\) => v != null && String\(v\)\.trim\(\) !== "",\r?\n\s*\);\r?\n\s*if \(!hasAny\) continue;/,
`// skip empty / instruction footer rows\n const hasAny = Object.values(raw).some(\n (v) => v != null && String(v).trim() !== "",\n );\n if (!hasAny) continue;\n if (isInstructionNoiseRow(raw)) continue;`,
);
}
if (!src.includes("isInstructionNoiseRow(raw)")) {
throw new Error("failed to insert skip call");
}
fs.writeFileSync(path, src);
console.log("packing-list patched");
}
{
const path = "src/services/parse/channel-map.ts";
let src = fs.readFileSync(path, "utf8");
if (src.includes("text.length > 40")) {
console.log("channel-map already ok");
} else {
src = src.replace(
/export function mapChannel\(raw: string\): \{[\s\S]*?return \{ transporter: text\.slice\(0, 30\), unmapped: true \};\n\}/,
`export function mapChannel(raw: string): {
transporter: string;
unmapped: boolean;
} {
const text = (raw || "").trim();
if (!text) return { transporter: "", unmapped: true };
// Long instruction text must not map (e.g. note mentioning Truck)
if (text.length > 40 || /^\\u6ce8\\u610f\\s*[\\uFF1A:]/.test(text)) {
return { transporter: "", unmapped: true };
}
const upper = text.toUpperCase();
for (const rule of CHANNEL_MAP) {
if (
rule.keys.some((k) => {
const key = k.toUpperCase();
if (/^[A-Z0-9]+$/i.test(k)) {
return new RegExp(\`(?:^|[^A-Z0-9])\${key}(?:[^A-Z0-9]|$)\`).test(upper);
}
return upper.includes(key);
})
) {
return { transporter: rule.value, unmapped: false };
}
}
return { transporter: text.slice(0, 30), unmapped: true };
}`,
);
// Fix the broken unicode escape in the written file - use real chars
src = src.replace(
"if (text.length > 40 || /^\\\\u6ce8\\\\u610f\\\\s*[\\\\uFF1A:]/.test(text))",
"if (text.length > 40 || /^\u6ce8\u610f\\s*[\uFF1A:]/.test(text))",
);
// Also if the replace used the regex with single backslash unicode incorrectly
if (!src.includes("text.length > 40")) {
throw new Error("channel map replace failed");
}
fs.writeFileSync(path, src);
console.log("channel-map patched");
}
}

@ -1,96 +0,0 @@
import fs from "fs";
const lexPath = "src/services/parse/instruction-lexicon.ts";
let lex = fs.readFileSync(lexPath, "utf8");
if (!lex.includes("isForwardShellSegment")) {
lex = lex.replace(
`export function isNonInstructionSegment(text: string): boolean {
const compact = text.replace(/\\s+/g, " ").trim();
if (compact.length < 6) return true;`,
`/** QQ/中间人纯转发壳,无客<E697A0>?Dear 正文 */
export function isForwardShellSegment(text: string): boolean {
const compact = text.replace(/\\s+/g, " ").trim();
if (/发自我的iPhone/.test(compact)) return true;
if (
/date@usasinogroup\\.com/i.test(compact) &&
!/Dear[,,]/.test(compact) &&
!BODY_ACTION_RE.test(compact)
) {
return true;
}
return false;
}
export function isNonInstructionSegment(text: string): boolean {
const compact = text.replace(/\\s+/g, " ").trim();
if (compact.length < 6) return true;
if (isForwardShellSegment(text)) return true;`,
);
fs.writeFileSync(lexPath, lex, "utf8");
}
const splitPath = "src/services/parse/split-instructions.ts";
let sp = fs.readFileSync(splitPath, "utf8");
if (!sp.includes("bodyForKindDetect")) {
sp = sp.replace(
`export function detectKindsForSegment(
segSubject: string,
segBody: string,
): InstructionUiKind[] {
if (isNonInstructionSegment(segBody)) return [];
const bodyHits = matchKeywordRules(segBody, "body");
const useSubject =
!BODY_ACTION_RE.test(segBody) ||
/新增预报|请查收新增预<EFBFBD>?.test(segBody);
const subjectHits = useSubject
? matchKeywordRules(segSubject, "subject")
: [];
const kinds = new Set<InstructionUiKind>([
...bodyHits.map((h) => h.uiKind),
...subjectHits.map((h) => h.uiKind),
]);
return UI_KIND_ORDER.filter((k) => kinds.has(k));
}`,
`/** 去掉段内「主<E3808C>? Re:Re:新增预报」历史主题污<E9A298>?*/
function bodyForKindDetect(segBody: string): string {
if (!BODY_ACTION_RE.test(segBody)) return segBody;
return segBody
.split(/\\n/)
.filter(
(line) =>
!/(?:主题|主旨|Subject)\\s*[<5B>?].*(?:新增预报|Fw:|转发)/i.test(line),
)
.join("\\n");
}
export function detectKindsForSegment(
segSubject: string,
segBody: string,
): InstructionUiKind[] {
if (isNonInstructionSegment(segBody)) return [];
const bodyText = bodyForKindDetect(segBody);
const bodyHits = matchKeywordRules(bodyText, "body");
const useSubject =
!BODY_ACTION_RE.test(segBody) ||
/请查收新增预<EFBFBD>?.test(bodyText);
const subjectHits = useSubject
? matchKeywordRules(segSubject, "subject")
: [];
const kinds = new Set<InstructionUiKind>([
...bodyHits.map((h) => h.uiKind),
...subjectHits.map((h) => h.uiKind),
]);
return UI_KIND_ORDER.filter((k) => kinds.has(k));
}`,
);
fs.writeFileSync(splitPath, sp, "utf8");
}
console.log(
JSON.stringify({
lex: fs.readFileSync(lexPath, "utf8").includes("isForwardShellSegment"),
split: fs.readFileSync(splitPath, "utf8").includes("bodyForKindDetect"),
}),
);

@ -1,108 +0,0 @@
import fs from "fs";
const p = "src/services/parse/split-instructions.ts";
let t = fs.readFileSync(p, "utf8");
t = t.replace(
`import {
ATTACHMENT_HINT_RE,
KEYWORD_RULES,
SEGMENT_BOUNDARY_PATTERNS,
SEGMENT_START_LINE_RE,
matchKeywordRules,
type InstructionUiKind,
} from "@/services/parse/instruction-lexicon";`,
`import {
ATTACHMENT_HINT_RE,
BODY_ACTION_RE,
KEYWORD_RULES,
SEGMENT_BOUNDARY_PATTERNS,
SEGMENT_START_LINE_RE,
isNonInstructionSegment,
matchKeywordRules,
type InstructionUiKind,
} from "@/services/parse/instruction-lexicon";`,
);
const oldDetect = `export function detectUiKindsFromText(
subject: string,
body: string,
filenames: string[] = [],
): InstructionUiKind[] {
const kinds = new Set<InstructionUiKind>();
for (const h of matchKeywordRules(subject, "subject")) kinds.add(h.uiKind);
for (const h of matchKeywordRules(body, "body")) kinds.add(h.uiKind);
for (const f of filenames) {
const whereHits = matchKeywordRules(f, "attachment");
for (const h of whereHits) kinds.add(h.uiKind);
if (isDoAttachmentFilename(f)) kinds.add("do_upload");
if (ATTACHMENT_HINT_RE.test(f) && /换标|贴标|覆盖贴|操作指令/.test(f)) {
kinds.add("work_order");
}
}
return UI_KIND_ORDER.filter((k) => kinds.has(k));
}`;
const newDetect = `export function detectUiKindsFromText(
subject: string,
body: string,
filenames: string[] = [],
): InstructionUiKind[] {
const kinds = new Set<InstructionUiKind>();
for (const h of matchKeywordRules(subject, "subject")) kinds.add(h.uiKind);
for (const h of matchKeywordRules(body, "body")) kinds.add(h.uiKind);
for (const f of filenames) {
for (const h of matchKeywordRules(f, "attachment")) kinds.add(h.uiKind);
if (isDoAttachmentFilename(f)) kinds.add("do_upload");
if (ATTACHMENT_HINT_RE.test(f) && /换标|贴标|覆盖贴|操作指令/.test(f)) {
kinds.add("work_order");
}
}
return UI_KIND_ORDER.filter((k) => kinds.has(k));
}
/** 单段:正文有动作词时,忽<EFBC8C>?Re: 链路上的历史「新增预报」主<E3808D>?*/
export function detectKindsForSegment(
segSubject: string,
segBody: string,
): InstructionUiKind[] {
if (isNonInstructionSegment(segBody)) return [];
const bodyHits = matchKeywordRules(segBody, "body");
const useSubject =
!BODY_ACTION_RE.test(segBody) ||
/新增预报|请查收新增预<EFBFBD>?.test(segBody);
const subjectHits = useSubject
? matchKeywordRules(segSubject, "subject")
: [];
const kinds = new Set<InstructionUiKind>([
...bodyHits.map((h) => h.uiKind),
...subjectHits.map((h) => h.uiKind),
]);
return UI_KIND_ORDER.filter((k) => kinds.has(k));
}`;
if (!t.includes(oldDetect)) {
console.error("detectUiKindsFromText block not found");
process.exit(1);
}
t = t.replace(oldDetect, newDetect);
t = t.replace(
`segments.forEach((seg, i) => {
const segSubject = extractSegmentSubject(seg, "");
const kinds = detectUiKindsFromText(segSubject || subject, seg, []);`,
`segments.forEach((seg, i) => {
if (isNonInstructionSegment(seg)) return;
const segSubject = extractSegmentSubject(seg, "");
const kinds = detectKindsForSegment(segSubject, seg);`,
);
fs.writeFileSync(p, t, "utf8");
console.log(
JSON.stringify({
ok:
t.includes("detectKindsForSegment") &&
t.includes("isNonInstructionSegment") &&
t.includes("BODY_ACTION_RE"),
}),
);

@ -1,158 +0,0 @@
/**
* Conservative subject-block strip: stop on body actions / Dear / 新增*
*/
import fs from "fs";
const STRIP_FN = `
function stripSubjectLines(text: string): string {
const lines = text.split(/\\n/);
const out: string[] = [];
let inSubject = false;
let cont = 0;
for (const line of lines) {
// "\\u4e3b\\u9898 Re:..." often has NO colon
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
inSubject = true;
cont = 0;
continue;
}
if (inSubject) {
const t = line.trim();
if (!t) {
inSubject = false;
continue;
}
// real body starts
if (
BODY_ACTION_RE.test(line) ||
/Dear[,,]/.test(t) ||
/(?:\u65b0\u589e\u8f6c\u4ed3|\u65b0\u589e\u9884\u62a5|\u8bf7\u67e5\u6536|\u66f4\u65b0\u6d3e\u9001\u5355)/.test(
t,
) ||
/^\\d+[\\.\\u3001\\uff1a:]/.test(t)
) {
inSubject = false;
out.push(line);
continue;
}
if (
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|----)/i.test(
t,
) ||
/^date@/i.test(t) ||
t === "date"
) {
inSubject = false;
out.push(line);
continue;
}
// wrapped subject: ETA / container / voyage fragments only (max 4 lines)
if (
cont < 4 &&
/Fw:|\u8f6c\u53d1|ETA|ETD|\u8239\u540d|\u822a\u6b21|\u67dc\u53f7|[A-Z]{4}\\d{7}|40HQ|20GP|\u4ef6\\+|\\+\\s*$/i.test(
line,
)
) {
cont += 1;
continue;
}
inSubject = false;
out.push(line);
continue;
}
out.push(line);
}
return out.join("\\n");
}
`.trimStart();
function replaceStripInLexicon() {
const path = "src/services/parse/instruction-lexicon.ts";
let src = fs.readFileSync(path, "utf8");
const a = src.indexOf("function stripSubjectLines");
const b = src.indexOf("export function isForwardShellSegment");
if (a < 0 || b < 0) throw new Error("lexicon markers");
let start = src.lastIndexOf("/**", a);
if (start < 0 || a - start > 200) start = a;
src =
src.slice(0, start) +
"/** Drop subject block; stop early on real body actions */\n" +
STRIP_FN +
"\n" +
src.slice(b);
fs.writeFileSync(path, src);
console.log("lexicon ok");
}
function replaceBodyDetect() {
const path = "src/services/parse/split-instructions.ts";
let src = fs.readFileSync(path, "utf8");
// Import BODY_ACTION already exists from lexicon
const neu = `function bodyForKindDetect(segBody: string): string {
const lines = segBody.split(/\\n/);
const out: string[] = [];
let inSubject = false;
let cont = 0;
for (const line of lines) {
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
inSubject = true;
cont = 0;
continue;
}
if (inSubject) {
const t = line.trim();
if (!t) {
inSubject = false;
continue;
}
if (
BODY_ACTION_RE.test(line) ||
/Dear[,,]/.test(t) ||
/(?:\u65b0\u589e\u8f6c\u4ed3|\u65b0\u589e\u9884\u62a5|\u8bf7\u67e5\u6536|\u66f4\u65b0\u6d3e\u9001\u5355)/.test(
t,
) ||
/^\\d+[\\.\\u3001\\uff1a:]/.test(t)
) {
inSubject = false;
out.push(line);
continue;
}
if (
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|----)/i.test(
t,
) ||
/^date@/i.test(t) ||
t === "date"
) {
inSubject = false;
out.push(line);
continue;
}
if (
cont < 4 &&
/Fw:|\u8f6c\u53d1|ETA|ETD|\u8239\u540d|\u822a\u6b21|\u67dc\u53f7|[A-Z]{4}\\d{7}|40HQ|20GP|\u4ef6\\+|\\+\\s*$/i.test(
line,
)
) {
cont += 1;
continue;
}
inSubject = false;
out.push(line);
continue;
}
out.push(line);
}
return out.join("\\n");
}
`;
const a = src.indexOf("function bodyForKindDetect");
const b = src.indexOf("export function detectKindsForSegment");
if (a < 0 || b < 0) throw new Error("split markers");
src = src.slice(0, a) + neu + "\n" + src.slice(b);
fs.writeFileSync(path, src);
console.log("split ok");
}
replaceStripInLexicon();
replaceBodyDetect();

@ -1,114 +0,0 @@
/**
* Fix subject-line stripping: optional colon + multi-line subject block
*/
import fs from "fs";
const SUBJECT_LINE_RE =
"/(?:\\u4e3b\\u9898|\\u4e3b\\u65e8|Subject)\\s*[\\uFF1A:]?/i";
const helper = `/** Drop subject / \\u4e3b\\u65e8 block (optional colon, multi-line wrap) */\nfunction stripSubjectLines(text: string): string {\n const lines = text.split(/\\n/);\n const out: string[] = [];\n let inSubject = false;\n for (const line of lines) {\n if (/(?:\\u4e3b\\u9898|\\u4e3b\\u65e8|Subject)\\s*[\\uFF1A:]?/i.test(line)) {\n inSubject = true;\n continue;\n }\n if (inSubject) {\n const t = line.trim();\n if (!t) {\n inSubject = false;\n continue;\n }\n if (\n /^(?:\\u5bc4\\u4ef6\\u4eba|\\u53d1\\u4ef6\\u4eba|\\u6536\\u4ef6\\u4eba|\\u6284\\u9001|\\u65e5\\u671f|\\u53d1\\u9001\\u65f6\\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(\n t,\n ) ||\n /^date@/i.test(t) ||\n t === "date" ||\n /^[\\u5728\\\\s]*\\d{4}/.test(t)\n ) {\n inSubject = false;\n out.push(line);\n continue;\n }\n continue;\n }\n out.push(line);\n }\n return out.join("\\n");\n}\n`;
// Simpler: write file with real unicode via \u in the script string that becomes Chinese
const stripFn = `
function stripSubjectLines(text: string): string {
const lines = text.split(/\\n/);
const out: string[] = [];
let inSubject = false;
for (const line of lines) {
// "\\u4e3b\\u9898 Re:..." often has NO colon after \\u4e3b\\u9898
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
inSubject = true;
continue;
}
if (inSubject) {
const t = line.trim();
if (!t) {
inSubject = false;
continue;
}
if (
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(
t,
) ||
/^date@/i.test(t) ||
t === "date" ||
/^\u5728\\s*\\d{4}/.test(t)
) {
inSubject = false;
out.push(line);
continue;
}
continue;
}
out.push(line);
}
return out.join("\\n");
}
`.trimStart();
{
const path = "src/services/parse/instruction-lexicon.ts";
let src = fs.readFileSync(path, "utf8");
const a = src.indexOf("function stripSubjectLines");
const b = src.indexOf("export function isForwardShellSegment");
if (a < 0 || b < 0) throw new Error("markers");
// keep any comment before function - find from /** Drop or function
let start = src.lastIndexOf("/** Drop subject", a);
if (start < 0) start = a;
src =
src.slice(0, start) +
"/** Drop subject/\u4e3b\u65e8 block (optional colon; multi-line wrap) */\n" +
stripFn +
"\n" +
src.slice(b);
fs.writeFileSync(path, src);
console.log("lexicon stripSubjectLines updated");
}
{
const path = "src/services/parse/split-instructions.ts";
let src = fs.readFileSync(path, "utf8");
const neu = `function bodyForKindDetect(segBody: string): string {
// reuse same multi-line subject strip as lexicon (inline copy to avoid circular import)
const lines = segBody.split(/\\n/);
const out: string[] = [];
let inSubject = false;
for (const line of lines) {
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
inSubject = true;
continue;
}
if (inSubject) {
const t = line.trim();
if (!t) {
inSubject = false;
continue;
}
if (
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(
t,
) ||
/^date@/i.test(t) ||
t === "date" ||
/^\\u5728\\s*\\d{4}/.test(t) ||
/^\u5728\\s*\\d{4}/.test(t)
) {
inSubject = false;
out.push(line);
continue;
}
continue;
}
out.push(line);
}
return out.join("\\n");
}
`;
const a = src.indexOf("function bodyForKindDetect");
const b = src.indexOf("export function detectKindsForSegment");
if (a < 0 || b < 0) throw new Error("bounds");
src = src.slice(0, a) + neu + "\n" + src.slice(b);
fs.writeFileSync(path, src);
console.log("split bodyForKindDetect updated");
}

@ -1,83 +0,0 @@
/**
* UI fallback: if mail_record missing customer, extract from subject bl-plus
*/
import fs from "fs";
const path = "src/components/MailBusinessSummary.tsx";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("extractBlPlusFromMail")) {
const importNeedle = `import {
extractForecastInstructionText,
extractMailInstructions,
} from "@/services/parse/split-instructions";`;
const importRepl = `import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template";
import {
extractForecastInstructionText,
extractMailInstructions,
} from "@/services/parse/split-instructions";`;
if (!src.includes(importNeedle)) throw new Error("import block missing");
src = src.replace(importNeedle, importRepl);
}
const oldForecast = ` const forecastValue = useMemo(() => {
const base = buildCcForecastFormValue({
customerName: record?.modules.customer_name,
header,
modules: record?.modules,
});
return {
...base,
F_Instruction: forecastInstruction || base.F_Instruction,
};
}, [
header,
record?.modules,
record?.modules.customer_name,
forecastInstruction,
]);`;
const neuForecast = ` const blPlusModules = useMemo(
() => extractBlPlusFromMail(mail.subject, bodyText)?.modules ?? null,
[mail.subject, bodyText],
);
const forecastValue = useMemo(() => {
const modules = {
...(blPlusModules || {}),
...(record?.modules || {}),
};
const base = buildCcForecastFormValue({
customerName:
record?.modules.customer_name ||
blPlusModules?.customer_name ||
undefined,
header,
modules,
});
return {
...base,
F_Instruction: forecastInstruction || base.F_Instruction,
};
}, [
header,
record?.modules,
record?.modules.customer_name,
blPlusModules,
forecastInstruction,
]);`;
if (!src.includes(oldForecast)) {
// CRLF
const oldC = oldForecast.replace(/\n/g, "\r\n");
if (!src.includes(oldC)) {
console.error("forecastValue block missing");
process.exit(1);
}
src = src.replace(oldC, neuForecast.replace(/\n/g, "\r\n"));
} else {
src = src.replace(oldForecast, neuForecast);
}
fs.writeFileSync(path, src);
console.log("MailBusinessSummary customer fallback ok");

@ -1,148 +0,0 @@
/**
* UI: only unit.isCurrent confirmable; history readonly + tag
*/
import fs from "fs";
// --- Shell ---
{
const path = "src/components/MailInstructionUnitShell.tsx";
let src = fs.readFileSync(path, "utf8");
const oldExtra = ` extra={
unit.keywords.length ? (
<Space wrap size={[4, 4]}>
{unit.keywords.slice(0, 6).map((k) => (
<Tag key={k}>{k}</Tag>
))}
</Space>
) : null
}`;
const newExtra = ` extra={
<Space wrap size={[4, 4]}>
<Tag color={unit.isCurrent ? "processing" : "default"}>
{unit.isCurrent ? "\u53ef\u786e\u8ba4" : "\u5386\u53f2\u53ea\u8bfb"}
</Tag>
{unit.keywords.slice(0, 5).map((k) => (
<Tag key={k}>{k}</Tag>
))}
</Space>
}`;
if (!src.includes(oldExtra)) throw new Error("shell extra block missing");
src = src.replace(oldExtra, newExtra);
fs.writeFileSync(path, src);
console.log("shell ok");
}
// --- MailBusinessSummary confirm gates ---
{
const path = "src/components/MailBusinessSummary.tsx";
let src = fs.readFileSync(path, "utf8");
// transfer: only current can confirm / edit WO fallback
src = src.replace(
` {blocked ? (
<CcWorkOrderForm
mode={canConfirmOps ? "edit" : "readonly"}`,
` {blocked ? (
<CcWorkOrderForm
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}`,
);
src = src.replace(
` if (canConfirmOps) {
actions = blocked ? (
<Button
type="primary"
loading={submitting}
onClick={() => void submitWorkOrder()}
>
\u8f6c\u4ed3\u4e0d\u53ef\u7528\uff0c\u63d0\u4ea4\u5de5\u5355
</Button>
) : (
<Button
type="primary"
loading={submitting}
onClick={() => void submitTransfer()}
>
\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3
</Button>
);
}`,
` if (canConfirmOps && unit.isCurrent) {
actions = blocked ? (
<Button
type="primary"
loading={submitting}
onClick={() => void submitWorkOrder()}
>
\u8f6c\u4ed3\u4e0d\u53ef\u7528\uff0c\u63d0\u4ea4\u5de5\u5355
</Button>
) : (
<Button
type="primary"
loading={submitting}
onClick={() => void submitTransfer()}
>
\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3
</Button>
);
}`,
);
// work_order mode
src = src.replace(
` <CcWorkOrderForm
mode={canConfirmOps ? "edit" : "readonly"}
value={{
...woForUnit,
// keep edits on shared state when confirming primary WO
...(mail.mail_type === "WORK_ORDER" && unit.isCurrent
? workOrderValue
: {}),
}}
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
/>
);
if (canConfirmOps && mail.mail_type === "WORK_ORDER") {`,
` <CcWorkOrderForm
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
value={{
...woForUnit,
...(unit.isCurrent ? workOrderValue : {}),
}}
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
/>
);
if (canConfirmOps && unit.isCurrent) {`,
);
// do_upload
src = src.replace(
` <CcDoUploadPanel
mode={canConfirmOps ? "edit" : "readonly"}
value={
mail.mail_type === "DO_UPLOAD" && unit.isCurrent
? doValue
: doForUnit
}
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
/>
);
if (
canConfirmOps &&
(mail.mail_type === "DO_UPLOAD" || unit.isCurrent)
) {`,
` <CcDoUploadPanel
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
value={unit.isCurrent ? doValue : doForUnit}
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
/>
);
if (canConfirmOps && unit.isCurrent) {`,
);
fs.writeFileSync(path, src);
console.log("summary ok", {
transferCur: src.includes("canConfirmOps && unit.isCurrent"),
woMode: src.includes('mode={canConfirmOps && unit.isCurrent ? "edit"'),
});
}

@ -1,105 +0,0 @@
import fs from "fs";
const path = "src/services/parse/packing-list.ts";
let src = fs.readFileSync(path, "utf8");
if (!src.includes("validatePackingFill")) {
src = src.replace(
`import { mapChannel } from "./channel-map";`,
`import { mapChannel } from "./channel-map";
import { validatePackingFill } from "./packing-fill-rules";`,
);
}
const oldBlock = ` const channelRaw = String(raw.F_Transporter ?? "").trim();
const mapped = mapChannel(channelRaw);
const fba = String(raw.F_FBACode ?? "").trim();
const ctnsNum = parseNumber(raw.F_CTNS);
const ctns = ctnsNum == null ? 0 : Math.trunc(ctnsNum);
const cbm = parseNumber(raw.F_CBM);
const weight = parseNumber(raw.F_Weight);
const addr = raw.F_Address != null ? String(raw.F_Address).trim() : "";
const express = /^(UPS|FEDEX|USPS|DHL)$/i.test(
mapped.transporter || channelRaw,
);
const invalid_reasons: string[] = [];
// 亚马逊仓必填仓库代码;快<EFBC9B>?私人地址可空
if (!fba && !express && !addr) invalid_reasons.push("仓库ID缺失");
if (!mapped.transporter) invalid_reasons.push("渠道缺失");
if (!ctns || ctns <= 0) invalid_reasons.push("件数无效");
if (cbm == null) invalid_reasons.push("总体积缺<EFBFBD>?);
if (weight == null) invalid_reasons.push("毛重缺失");
const warnings: string[] = [];
if (mapped.unmapped && mapped.transporter) warnings.push("CHANNEL_UNMAPPED");
let shipmentId =
raw.F_ShipmentID != null ? String(raw.F_ShipmentID).trim() : "";
if (!shipmentId) shipmentId = "/";
const fbaIdRaw =
raw.F_FBAID != null ? String(raw.F_FBAID).trim() : "";
const refRaw =
raw.F_ReferenceId != null ? String(raw.F_ReferenceId).trim() : "";
// 一格多 ID:警告(不自动拆行)
if (/[\\s,<2C>?/;]+/.test(fbaIdRaw) && /FBA/i.test(fbaIdRaw)) {
warnings.push("MULTI_FBAID_IN_CELL");
}
if (/[\\s,<2C>?/;]{2,}/.test(refRaw)) {
warnings.push("MULTI_REFERENCE_IN_CELL");
}`;
const neuBlock = ` const channelRaw = String(raw.F_Transporter ?? "").trim();
const mapped = mapChannel(channelRaw);
const fbaRaw = String(raw.F_FBACode ?? "").trim();
const ctnsNum = parseNumber(raw.F_CTNS);
const ctns = ctnsNum == null ? 0 : Math.trunc(ctnsNum);
const cbm = parseNumber(raw.F_CBM);
const weight = parseNumber(raw.F_Weight);
const addr = raw.F_Address != null ? String(raw.F_Address).trim() : "";
const fbaIdRaw =
raw.F_FBAID != null ? String(raw.F_FBAID).trim() : "";
const refRaw =
raw.F_ReferenceId != null ? String(raw.F_ReferenceId).trim() : "";
const shipmentIdRaw =
raw.F_ShipmentID != null ? String(raw.F_ShipmentID).trim() : "";
const fill = validatePackingFill({
channelRaw,
transporter: mapped.transporter,
warehouseId: fbaRaw,
ctns,
cbm,
weight,
address: addr,
shipmentId: shipmentIdRaw,
fbaId: fbaIdRaw,
referenceId: refRaw,
rawCtns: raw.F_CTNS != null ? String(raw.F_CTNS) : undefined,
rawCbm: raw.F_CBM != null ? String(raw.F_CBM) : undefined,
rawWeight: raw.F_Weight != null ? String(raw.F_Weight) : undefined,
});
const fba = fill.warehouseId;
const shipmentId = fill.shipmentId;
const invalid_reasons = [...fill.invalid_reasons];
const warnings = [...fill.warnings];
if (mapped.unmapped && mapped.transporter) warnings.push("CHANNEL_UNMAPPED");`;
if (!src.includes(oldBlock)) {
// try without double escapes for regex in character class
const old2 = oldBlock.replace(/\\s/g, "\\s").replace(/\\\\/g, "\\");
// Just find by markers
const a = src.indexOf("const channelRaw = String(raw.F_Transporter");
const b = src.indexOf("const dateB = toShanghaiDateString");
if (a < 0 || b < 0) {
console.error("markers", a, b);
process.exit(1);
}
src = src.slice(0, a) + neuBlock + "\n\n " + src.slice(b);
} else {
src = src.replace(oldBlock, neuBlock);
}
// Fix shipments.push F_FBACode: fba
fs.writeFileSync(path, src);
console.log("wired", src.includes("validatePackingFill"), src.includes("fill.warehouseId"));

@ -1,48 +0,0 @@
/**
* 清空全部邮件及相关解<EFBFBD>?导入/附件记录(保留邮箱账号等设置)<EFBFBD>?
* 用法: pnpm exec tsx scripts/purge-all-mails.ts
*/
import fs from "fs/promises";
import path from "path";
import { prisma } from "@/services/db";
async function rmMailDataDir() {
const root = path.join(process.cwd(), "data", "mails");
try {
await fs.rm(root, { recursive: true, force: true });
await fs.mkdir(root, { recursive: true });
console.log("cleared data/mails/");
} catch (e) {
console.warn("data/mails cleanup skipped", e);
}
}
async function main() {
const before = await prisma.mailMessage.count();
console.log("mails before:", before);
// FK-safe order
await prisma.importCompensation.deleteMany({});
await prisma.containerImport.deleteMany({});
await prisma.containerActiveLock.deleteMany({});
await prisma.parseResult.deleteMany({});
await prisma.mailAttachment.deleteMany({});
await prisma.imapPullLog.deleteMany({});
const deleted = await prisma.mailMessage.deleteMany({});
console.log("deleted mail_message:", deleted.count);
await rmMailDataDir();
const after = await prisma.mailMessage.count();
console.log("mails after:", after);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

@ -1,679 +0,0 @@
/**
* Rewrite MailBusinessSummary.tsx fully in UTF-8.
* ASCII-only script source; Chinese via \\u escapes only.
* Run: pnpm exec tsx scripts/rewrite-mail-business-summary.ts
*/
import fs from "fs";
import path from "path";
const out = path.join(process.cwd(), "src", "components", "MailBusinessSummary.tsx");
const ZH = {
cardTitle: "\u8fd9\u5c01\u90ae\u4ef6\u5728\u5e72\u4ec0\u4e48",
splitPrefix: "\u5df2\u62c6\u51fa ",
splitSuffix: " \u6761\u6307\u4ee4\uff1a",
from: "\u53d1\u4ef6\u4eba",
action: "\u4e1a\u52a1\u52a8\u4f5c",
hint: "\u90ae\u4ef6\u8868\u8ff0",
next: "\u7cfb\u7edf\u4e0b\u4e00\u6b65",
vessel: "\u8239\u540d\u822a\u6b21 ",
woMock: "\u5de5\u5355\u5df2\u63d0\u4ea4\uff08mock\uff09",
woOk: "\u5de5\u5355\u5df2\u63d0\u4ea4",
doMock: "DO \u5df2\u767b\u8bb0\uff08mock\uff09",
doOk: "DO \u5df2\u4e0a\u4f20",
needFba: "\u8bf7\u586b\u5199\u76ee\u6807 FBACode",
xferMock: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4\uff08mock\uff09",
xferOk: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4",
msgType: "\u62c6\u67dc\u65f6\u81ea\u52a8\u8f6c\u4ed3",
gate: "\u95f8\u95e8\uff1a",
semi: "\uff1b",
actXfer: "\u8f6c\u4ed3",
btnWo: "\u786e\u8ba4\u63d0\u4ea4\u5de5\u5355",
btnXfer: "\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3",
btnDo: "\u786e\u8ba4\u4e0a\u4f20 DO",
};
const file = `"use client";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import {
Alert,
App,
Button,
Card,
Descriptions,
Space,
Tag,
Typography,
} from "antd";
import {
CcDoUploadPanel,
buildCcDoUploadFormValue,
} from "@/components/CcDoUploadPanel";
import {
CcForecastFormOrder,
buildCcForecastFormValue,
} from "@/components/CcForecastFormOrder";
import { CcCustomerInstructionPanel } from "@/components/CcCustomerInstructionPanel";
import { InstructionUnitShell } from "@/components/MailInstructionUnitShell";
import {
CcTransferIndexTruck,
buildCcTransferFormValue,
} from "@/components/CcTransferIndexTruck";
import {
CcWorkOrderForm,
buildCcWorkOrderFormValue,
} from "@/components/CcWorkOrderForm";
import { TypeTag } from "@/components/TypeTag";
import {
confirmDoUploadApi,
confirmTransferApi,
confirmWorkOrderApi,
getTransferGateApi,
listShippingLinesApi,
} from "@/lib/client-api";
import {
buildMailIntent,
extractTransferPairs,
filterTransferShipments,
} from "@/services/parse/mail-intent";
import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template";
import {
extractForecastInstructionText,
extractMailInstructions,
} from "@/services/parse/split-instructions";
import type { MailMessage } from "@/types";
import type {
ContainerHeader,
MailRecord,
MailType,
TypeEvidence,
} from "@/types/mail";
import type { TransferGateDecision } from "@/services/cc/container-status";
function asHeader(mail: MailMessage): ContainerHeader | null {
return mail.parse_result?.container_header ?? null;
}
function asMailRecord(mail: MailMessage): MailRecord | null {
const direct = mail.parse_result?.mail_record;
if (direct) return direct;
const lineage = mail.parse_result?.lineage as
| { mail_record?: MailRecord }
| undefined;
return lineage?.mail_record ?? null;
}
type EvidenceExt = TypeEvidence & {
mailbox?: { from?: string; inbox?: string; extracted?: string[] };
};
export function MailBusinessSummary({
mail,
onConfirmed,
}: {
mail: MailMessage;
onConfirmed?: () => void;
}) {
const { message } = App.useApp();
const header = asHeader(mail);
const record = asMailRecord(mail);
const evidence = (mail.type_evidence || {
total: 0,
mail_type: mail.mail_type,
signals: [],
}) as EvidenceExt;
// Stable deps for useEffect
const shipmentsRaw = mail.parse_result?.shipments;
const attachmentsRaw = mail.attachments;
const shipmentsKey = JSON.stringify(shipmentsRaw ?? []);
const filenamesKey = (attachmentsRaw || []).map((a) => a.filename).join("?");
const shipments = useMemo(
() => shipmentsRaw ?? [],
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by shipmentsKey
[shipmentsKey],
);
const filenames = useMemo(
() => (attachmentsRaw || []).map((a) => a.filename),
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by filenamesKey
[filenamesKey],
);
const bodyText = mail.body_text || mail.ocr_text || "";
const containerNo = header?.F_ContainerNo || mail.container_no || "";
const workOrderActionsKey = (record?.work_order_actions || []).join("|");
const intentSignalsKey = JSON.stringify(evidence.signals ?? []);
const intent = useMemo(
() =>
evidence.intent ||
buildMailIntent({
mailType: mail.mail_type as MailType,
subject: mail.subject,
body: bodyText,
filenames,
shipments,
evidence,
containerNo,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps -- evidence default object unstable
[
evidence.intent,
evidence.mail_type,
evidence.total,
intentSignalsKey,
mail.mail_type,
mail.subject,
bodyText,
filenames,
shipments,
containerNo,
],
);
const summaryText = record?.summary || intent.narrative;
const transferPairs = useMemo(
() =>
intent.transfer_pairs?.length
? intent.transfer_pairs
: extractTransferPairs(shipments),
[intent.transfer_pairs, shipments],
);
const transferShipments = useMemo(
() => filterTransferShipments(shipments),
[shipments],
);
const instructions = useMemo(
() =>
extractMailInstructions({
subject: mail.subject,
body: bodyText,
filenames,
forceForecast: record?.kind === "BL_FORECAST",
workOrderActions: record?.work_order_actions,
}),
[
mail.subject,
bodyText,
filenames,
record?.kind,
workOrderActionsKey,
],
);
const forecastInstruction = useMemo(() => {
const vessel = record?.modules.vessel_voyage;
return extractForecastInstructionText({
subject: mail.subject,
body: bodyText,
modulesInstruction: vessel
? \`${ZH.vessel}\${vessel}\`
: header?.F_Instruction || null,
});
}, [
mail.subject,
bodyText,
header?.F_Instruction,
record?.modules.vessel_voyage,
]);
const hasForecastUi = instructions.some((u) => u.uiKind === "forecast");
const hasTransferUi = instructions.some((u) => u.uiKind === "transfer");
const canConfirmOps =
mail.status === "PENDING_CONFIRM" || mail.status === "FAILED";
const blPlusModules = useMemo(
() => extractBlPlusFromMail(mail.subject, bodyText)?.modules ?? null,
[mail.subject, bodyText],
);
const forecastValue = useMemo(() => {
const modules = {
...(blPlusModules || {}),
...(record?.modules || {}),
};
const base = buildCcForecastFormValue({
customerName:
record?.modules.customer_name ||
blPlusModules?.customer_name ||
undefined,
header,
modules,
});
return {
...base,
F_Instruction: forecastInstruction || base.F_Instruction,
};
}, [
header,
record?.modules,
record?.modules.customer_name,
blPlusModules,
forecastInstruction,
]);
const transferValue = useMemo(
() =>
buildCcTransferFormValue({
customerName: record?.modules.customer_name,
containerNo,
modules: record?.modules,
shipments: transferShipments,
transferPairs,
}),
[
record?.modules.customer_name,
containerNo,
record?.modules,
transferShipments,
transferPairs,
],
);
const currentWorkOrderUnit = useMemo(
() => instructions.find((u) => u.uiKind === "work_order" && u.isCurrent),
[instructions],
);
const [workOrderValue, setWorkOrderValue] = useState(() =>
buildCcWorkOrderFormValue({
subject: currentWorkOrderUnit?.segmentSubject || mail.subject,
body: currentWorkOrderUnit?.segmentText || bodyText,
filenames,
containerNo,
actions: record?.work_order_actions,
}),
);
const [doValue, setDoValue] = useState(() =>
buildCcDoUploadFormValue({
subject: mail.subject,
body: bodyText,
containerNo,
filenames,
}),
);
const [gate, setGate] = useState<TransferGateDecision | null>(null);
const [submitting, setSubmitting] = useState(false);
// Reset forms when mail / current work-order segment changes
useEffect(() => {
setWorkOrderValue(
buildCcWorkOrderFormValue({
subject: currentWorkOrderUnit?.segmentSubject || mail.subject,
body: currentWorkOrderUnit?.segmentText || bodyText,
filenames,
containerNo,
actions: record?.work_order_actions,
}),
);
setDoValue(
buildCcDoUploadFormValue({
subject: mail.subject,
body: bodyText,
containerNo,
filenames,
}),
);
}, [
mail.id,
mail.version,
mail.subject,
bodyText,
containerNo,
workOrderActionsKey,
filenames,
currentWorkOrderUnit?.id,
currentWorkOrderUnit?.segmentText,
]);
useEffect(() => {
if (mail.mail_type !== "TRANSFER" && !hasTransferUi) return;
let cancelled = false;
void getTransferGateApi(mail.id).then((res) => {
if (!cancelled && res.ok) setGate(res.data.gate);
});
return () => {
cancelled = true;
};
}, [mail.id, mail.mail_type, hasTransferUi]);
const [shippingLines, setShippingLines] = useState<
{ value: string; label: string }[]
>([]);
useEffect(() => {
if (!hasForecastUi) return;
let cancelled = false;
void listShippingLinesApi().then((res) => {
if (!cancelled && res.ok) {
setShippingLines(
res.data.items.map((item) => ({ value: item.id, label: item.name })),
);
}
});
return () => {
cancelled = true;
};
}, [hasForecastUi]);
const submitWorkOrder = async () => {
setSubmitting(true);
const res = await confirmWorkOrderApi(mail.id, {
version: mail.version,
entity: workOrderValue,
});
setSubmitting(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
message.success(
res.data.mode === "mock" ? "${ZH.woMock}" : "${ZH.woOk}",
);
onConfirmed?.();
};
const submitDo = async () => {
setSubmitting(true);
const res = await confirmDoUploadApi(mail.id, {
version: mail.version,
container_no: doValue.F_ContainerNo,
container_task_id: doValue.F_ContainerTaskId || undefined,
filenames: doValue.filenames,
});
setSubmitting(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
message.success(
res.data.mode === "mock" ? "${ZH.doMock}" : "${ZH.doOk}",
);
onConfirmed?.();
};
const submitTransfer = async () => {
const target =
transferValue.targetFBACode || transferValue.F_ConvertFBACode || "";
if (!target) {
message.error("${ZH.needFba}");
return;
}
const rows = transferShipments.length
? transferShipments
: shipments.slice(0, 1);
setSubmitting(true);
const res = await confirmTransferApi(mail.id, {
version: mail.version,
container_no:
transferValue.F_ContainerNo ||
header?.F_ContainerNo ||
mail.container_no ||
"",
payload: {
keyValue:
gate && "containerTaskId" in gate ? gate.containerTaskId || "" : "",
strDetialList: rows.map((r) => ({
F_FBACode: r.F_FBACode || "",
F_ConvertFBACode: target,
})),
F_ConvertChargeMode: transferValue.F_ConvertChargeMode || "2",
F_RemovePlanDetail: transferValue.F_RemovePlanDetail ? "1" : "0",
},
});
setSubmitting(false);
if (!res.ok) {
message.error(res.error.message);
return;
}
message.success(
res.data.mode === "mock" ? "${ZH.xferMock}" : "${ZH.xferOk}",
);
onConfirmed?.();
};
return (
<>
<Card title="${ZH.cardTitle}">
<Typography.Paragraph style={{ marginBottom: 12, fontSize: 15 }}>
{summaryText}
</Typography.Paragraph>
{instructions.length > 1 ? (
<div style={{ marginBottom: 12 }}>
<Typography.Text type="secondary" style={{ marginRight: 8 }}>
${ZH.splitPrefix}{instructions.length}${ZH.splitSuffix}
</Typography.Text>
<Space wrap size={[4, 4]}>
{instructions.map((u) => (
<Tag key={u.id}>{u.label}</Tag>
))}
</Space>
</div>
) : null}
<Descriptions
bordered
size="small"
column={{ xs: 1, sm: 2, md: 2 }}
items={[
{
key: "from",
label: "${ZH.from}",
children: (
<span className="mono">
{evidence.mailbox?.from || mail.from_addr || "-"}
</span>
),
},
{
key: "action",
label: "${ZH.action}",
children: (
<Space>
<TypeTag type={mail.mail_type} />
<Typography.Text>{intent.action}</Typography.Text>
</Space>
),
},
{
key: "hint",
label: "${ZH.hint}",
children: intent.content_hint || "-",
},
{
key: "next",
label: "${ZH.next}",
span: { xs: 1, sm: 2, md: 2 },
children: intent.next_step || "-",
},
]}
/>
</Card>
{instructions.map((unit, index) => {
let body: ReactNode = null;
let actions: ReactNode = null;
if (unit.uiKind === "forecast") {
body = (
<CcForecastFormOrder
mode="readonly"
value={forecastValue}
shipments={shipments}
shippingLines={shippingLines}
/>
);
} else if (unit.uiKind === "transfer") {
const blocked = gate && !gate.canTransfer;
body = (
<>
{gate ? (
<Alert
style={{ marginBottom: 12 }}
type={gate.canTransfer ? "success" : "warning"}
showIcon
message={gate.reason}
/>
) : null}
{blocked ? (
<CcWorkOrderForm
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
value={{
...workOrderValue,
F_MessageType:
workOrderValue.F_MessageType || "${ZH.msgType}",
F_Remark: [
workOrderValue.F_Remark,
gate.reason ? \`${ZH.gate}\${gate.reason}\` : "",
]
.filter(Boolean)
.join("${ZH.semi}"),
mailActions: workOrderValue.mailActions?.length
? workOrderValue.mailActions
: ["${ZH.actXfer}"],
}}
onChange={(p) =>
setWorkOrderValue((v) => ({ ...v, ...p }))
}
/>
) : (
<CcTransferIndexTruck
mode="readonly"
value={transferValue}
shipments={transferShipments}
transferPairs={transferPairs}
totalShipmentCount={shipments.length}
/>
)}
</>
);
if (canConfirmOps && unit.isCurrent) {
actions = blocked ? (
<Button
type="primary"
loading={submitting}
onClick={() => void submitWorkOrder()}
>
${ZH.btnWo}
</Button>
) : (
<Button
type="primary"
loading={submitting}
onClick={() => void submitTransfer()}
>
${ZH.btnXfer}
</Button>
);
}
} else if (unit.uiKind === "work_order") {
const woForUnit = unit.isCurrent
? workOrderValue
: buildCcWorkOrderFormValue({
subject: unit.segmentSubject || mail.subject,
body: unit.segmentText || bodyText,
filenames,
containerNo,
});
body = (
<CcWorkOrderForm
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
value={woForUnit}
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
/>
);
if (canConfirmOps && unit.isCurrent) {
actions = (
<Button
type="primary"
loading={submitting}
onClick={() => void submitWorkOrder()}
>
${ZH.btnWo}
</Button>
);
}
} else if (unit.uiKind === "do_upload") {
const doForUnit = buildCcDoUploadFormValue({
subject: unit.segmentSubject || mail.subject,
body: unit.segmentText || bodyText,
containerNo,
filenames:
unit.source === "attachment"
? [unit.segmentText]
: filenames,
});
body = (
<CcDoUploadPanel
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
value={unit.isCurrent ? doValue : doForUnit}
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
/>
);
if (canConfirmOps && unit.isCurrent) {
actions = (
<Button
type="primary"
loading={submitting}
onClick={() => void submitDo()}
>
${ZH.btnDo}
</Button>
);
}
} else if (unit.uiKind === "customer_instruction") {
body = (
<CcCustomerInstructionPanel
value={{
roundAt: unit.roundAt,
roundFrom: unit.roundFrom,
subject: unit.segmentSubject,
body: unit.segmentText || bodyText,
}}
/>
);
}
return (
<InstructionUnitShell
key={unit.id}
unit={unit}
index={index}
total={instructions.length}
>
{body}
{actions ? (
<div style={{ marginTop: 12, textAlign: "right" }}>{actions}</div>
) : null}
</InstructionUnitShell>
);
})}
</>
);
}
`;
fs.writeFileSync(out, file, "utf8");
const check = fs.readFileSync(out, "utf8");
const bad = [...check.matchAll(/\?{3,}/g)].filter((m) => {
const i = m.index || 0;
const line = check.slice(check.lastIndexOf("\n", i) + 1, check.indexOf("\n", i));
return !line.includes('join("?")');
});
console.log(
JSON.stringify(
{
bytes: Buffer.byteLength(check, "utf8"),
hasTitle: check.includes(ZH.cardTitle),
hasFrom: check.includes(ZH.from),
hasBtnWo: check.includes(ZH.btnWo),
hasDearLogic: check.includes("currentWorkOrderUnit"),
leftoverQ: bad.length,
},
null,
2,
),
);
if (!check.includes(ZH.cardTitle) || bad.length) process.exit(1);

@ -1,27 +0,0 @@
import fs from "fs";
import path from "path";
const root = path.join(process.cwd(), "docx", "邮件");
type Entry = { rel: string; kind: "DIR" | "FILE"; size?: number };
function walk(dir: string, out: Entry[]) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
const rel = path.relative(root, full).split(path.sep).join("/");
if (e.isDirectory()) {
out.push({ rel, kind: "DIR" });
walk(full, out);
} else {
out.push({ rel, kind: "FILE", size: fs.statSync(full).size });
}
}
}
const out: Entry[] = [];
if (!fs.existsSync(root)) {
console.error("MISSING", root);
process.exit(1);
}
walk(root, out);
console.log(JSON.stringify({ root, count: out.length, entries: out }, null, 2));

@ -1,71 +0,0 @@
/**
* After purge+reimport: print rule-check summary for each mail.
*/
import { prisma } from "@/services/db";
import { extractMailInstructions } from "@/services/parse/split-instructions";
import { isInstructionNoiseRow } from "@/services/parse/packing-list";
async function main() {
const mails = await prisma.mailMessage.findMany({
orderBy: { id: "asc" },
include: { parseResult: true, attachments: true },
});
console.log(`\n=== reimport check: ${mails.length} mails ===\n`);
for (const m of mails) {
const ships = (m.parseResult?.shipments as any[]) || [];
const invalid = ships.filter((s) => s.row_status === "INVALID");
const noteLike = ships.filter(
(s) =>
String(s.F_FBACode || "").includes("注意") ||
String(s.F_Address || "").includes("注意") ||
String(s.F_Transporter || "").includes("注意"),
);
const units = extractMailInstructions({
subject: m.subject,
body: m.bodyText || "",
filenames: m.attachments.map((a) => a.filename),
});
const lineage = m.parseResult?.lineage as { mail_record?: { modules?: { customer_name?: string } } } | null;
const customer =
lineage?.mail_record?.modules?.customer_name ||
(m.parseResult?.containerHeader as { F_MemoRemark?: string } | null)
?.F_MemoRemark ||
"";
console.log(
JSON.stringify(
{
id: String(m.id),
type: m.mailType,
status: m.status,
subject: m.subject.slice(0, 48),
body_chars: (m.bodyText || "").length,
att: m.attachments.length,
shipments: ships.length,
invalid: invalid.length,
note_rows_in_shipments: noteLike.length,
ui_kinds: units.map((u) => `${u.uiKind}${u.isCurrent ? "*" : ""}`),
customer_hint: String(customer).slice(0, 40),
url: `http://localhost:3100/mails/${m.id}`,
},
null,
2,
),
);
}
// sanity: instruction noise helper still works
const noise = isInstructionNoiseRow({
F_Transporter: "注意<E6B3A8>?.ups和fedex Truck",
F_FBACode: "注意<E6B3A8>?.ups",
});
console.log("\nisInstructionNoiseRow smoke:", noise);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());
Loading…
Cancel
Save