From 7214d32627cc3051fe3a958263f9eefc4e244122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84GitHub=E7=94=A8=E6=88=B7=E5=90=8D?= <3457189498@qq.com> Date: Wed, 12 Aug 2026 10:21:29 +0800 Subject: [PATCH] Stop tracking local one-off patch/ingest scripts. Keep smoke/ops scripts (imap/cc/retention) and product source. Co-authored-by: Cursor --- .gitignore | 19 + scripts/analyze-mail-rounds.ts | 38 -- scripts/bind-instruction-segment-ui.ts | 103 ---- scripts/check-template-recognize.ts | 43 -- scripts/extract-mail-recognize-docx.py | 16 - scripts/extract-pdf-text.py | 18 - scripts/fill-xcz-packing-template.ts | 119 ---- scripts/fix-mail-business-summary-zh.ts | 5 - scripts/fix-wo-btn.ts | 34 -- scripts/flatten-mail-attachments.ts | 62 -- scripts/ingest-do-upload-ui.ts | 145 ----- scripts/ingest-forecast-ui.ts | 170 ------ scripts/ingest-mail3.ts | 128 ---- scripts/ingest-recognize-gold.ts | 198 ------- scripts/ingest-sample-mails.ts | 341 ----------- scripts/mark-non-business-ignored.ts | 46 -- scripts/patch-classify-gold.ts | 76 --- scripts/patch-classify-packing-narrow.ts | 37 -- scripts/patch-classify-wo-fallback.ts | 21 - scripts/patch-date-shell-rule.ts | 29 - scripts/patch-detail-full-body-ui.ts | 157 ----- scripts/patch-do-keep-customer.ts | 63 -- scripts/patch-forecast-packing-tips-ui.ts | 47 -- scripts/patch-ingest-full-body.ts | 71 --- scripts/patch-latest-readonly.ts | 138 ----- scripts/patch-packing-note-tests.ts | 57 -- scripts/patch-packing-skip-notes.ts | 45 -- scripts/patch-preserve-body-text.ts | 75 --- scripts/patch-resolve-fuller-body.ts | 51 -- scripts/patch-shell-subject-noise.ts | 152 ----- scripts/patch-skip-instruction-rows.ts | 94 --- scripts/patch-skip-shell-segments.ts | 96 --- scripts/patch-split-segment-kinds.ts | 108 ---- scripts/patch-strip-conservative.ts | 158 ----- scripts/patch-strip-subject-block.ts | 114 ---- scripts/patch-ui-customer-fallback.ts | 83 --- scripts/patch-ui-latest-readonly.ts | 148 ----- scripts/patch-wire-packing-fill-rules.ts | 105 ---- scripts/purge-all-mails.ts | 48 -- scripts/rewrite-mail-business-summary.ts | 679 ---------------------- scripts/scan-mail-samples.ts | 27 - scripts/verify-reimport-rules.ts | 71 --- 42 files changed, 19 insertions(+), 4216 deletions(-) delete mode 100644 scripts/analyze-mail-rounds.ts delete mode 100644 scripts/bind-instruction-segment-ui.ts delete mode 100644 scripts/check-template-recognize.ts delete mode 100644 scripts/extract-mail-recognize-docx.py delete mode 100644 scripts/extract-pdf-text.py delete mode 100644 scripts/fill-xcz-packing-template.ts delete mode 100644 scripts/fix-mail-business-summary-zh.ts delete mode 100644 scripts/fix-wo-btn.ts delete mode 100644 scripts/flatten-mail-attachments.ts delete mode 100644 scripts/ingest-do-upload-ui.ts delete mode 100644 scripts/ingest-forecast-ui.ts delete mode 100644 scripts/ingest-mail3.ts delete mode 100644 scripts/ingest-recognize-gold.ts delete mode 100644 scripts/ingest-sample-mails.ts delete mode 100644 scripts/mark-non-business-ignored.ts delete mode 100644 scripts/patch-classify-gold.ts delete mode 100644 scripts/patch-classify-packing-narrow.ts delete mode 100644 scripts/patch-classify-wo-fallback.ts delete mode 100644 scripts/patch-date-shell-rule.ts delete mode 100644 scripts/patch-detail-full-body-ui.ts delete mode 100644 scripts/patch-do-keep-customer.ts delete mode 100644 scripts/patch-forecast-packing-tips-ui.ts delete mode 100644 scripts/patch-ingest-full-body.ts delete mode 100644 scripts/patch-latest-readonly.ts delete mode 100644 scripts/patch-packing-note-tests.ts delete mode 100644 scripts/patch-packing-skip-notes.ts delete mode 100644 scripts/patch-preserve-body-text.ts delete mode 100644 scripts/patch-resolve-fuller-body.ts delete mode 100644 scripts/patch-shell-subject-noise.ts delete mode 100644 scripts/patch-skip-instruction-rows.ts delete mode 100644 scripts/patch-skip-shell-segments.ts delete mode 100644 scripts/patch-split-segment-kinds.ts delete mode 100644 scripts/patch-strip-conservative.ts delete mode 100644 scripts/patch-strip-subject-block.ts delete mode 100644 scripts/patch-ui-customer-fallback.ts delete mode 100644 scripts/patch-ui-latest-readonly.ts delete mode 100644 scripts/patch-wire-packing-fill-rules.ts delete mode 100644 scripts/purge-all-mails.ts delete mode 100644 scripts/rewrite-mail-business-summary.ts delete mode 100644 scripts/scan-mail-samples.ts delete mode 100644 scripts/verify-reimport-rules.ts diff --git a/.gitignore b/.gitignore index 26456a7..defb4c2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/scripts/analyze-mail-rounds.ts b/scripts/analyze-mail-rounds.ts deleted file mode 100644 index 6f79a64..0000000 --- a/scripts/analyze-mail-rounds.ts +++ /dev/null @@ -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; - -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), - })), - ); -} diff --git a/scripts/bind-instruction-segment-ui.ts b/scripts/bind-instruction-segment-ui.ts deleted file mode 100644 index d9929a9..0000000 --- a/scripts/bind-instruction-segment-ui.ts +++ /dev/null @@ -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 = ( - 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 = ( - 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 = ( - 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 = ( - 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 �?restore with fix-mail-business-summary-zh.ts"); - process.exit(1); -} diff --git a/scripts/check-template-recognize.ts b/scripts/check-template-recognize.ts deleted file mode 100644 index c6e7d40..0000000 --- a/scripts/check-template-recognize.ts +++ /dev/null @@ -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+洛杉�?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆�?; -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)), -); diff --git a/scripts/extract-mail-recognize-docx.py b/scripts/extract-mail-recognize-docx.py deleted file mode 100644 index 545213c..0000000 --- a/scripts/extract-mail-recognize-docx.py +++ /dev/null @@ -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"", "\t", xml) -text = re.sub(r"", "\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]) diff --git a/scripts/extract-pdf-text.py b/scripts/extract-pdf-text.py deleted file mode 100644 index 5707545..0000000 --- a/scripts/extract-pdf-text.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -"""Extract plain text from a PDF (pypdf). Usage: python scripts/extract-pdf-text.py """ -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() diff --git a/scripts/fill-xcz-packing-template.ts b/scripts/fill-xcz-packing-template.ts deleted file mode 100644 index 9b6b8bb..0000000 --- a/scripts/fill-xcz-packing-template.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * 按新辰泽填写规范生成卡派表(不就�?splice,避免残留旧行)�? - * 用法: 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", - "邮件", - "模板", - "数据模版-新辰�?WHSU8127240.xlsx", -); -const CN = "WHSU8127240"; - -const LAS1_ADDR = [ - "仓库代码�?LAS1", - "收件人: LAS1", - "公司名称�?AMAZON COM SERVICES INC", - "联系电话�?0123456789", - "�?�?区: NV / HENDERSON /", - "邮政编码�?89044-8746", - "收货地址�?12300 Bermuda Road", -].join("\n"); - -const PRIVATE_ADDR = [ - "收件人: Erica Fabian", - "公司名称�?Rapid Fulfillment", - "联系电话�?818-492-2760", - "�?�?区: CA / Pacoima /", - "邮政编码�?91331", - "收货地址�?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"); - - // 复制�?3 行表头(�?+ 合并可忽略) - 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 �?${OUT}`); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/fix-mail-business-summary-zh.ts b/scripts/fix-mail-business-summary-zh.ts deleted file mode 100644 index 0665941..0000000 --- a/scripts/fix-mail-business-summary-zh.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Fix MailBusinessSummary Chinese corruption �?delegates to full UTF-8 rewrite. - * Prefer: pnpm fix:ui-zh - */ -import "./rewrite-mail-business-summary"; diff --git a/scripts/fix-wo-btn.ts b/scripts/fix-wo-btn.ts deleted file mode 100644 index 09842ad..0000000 --- a/scripts/fix-wo-btn.ts +++ /dev/null @@ -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*\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, - ), -); diff --git a/scripts/flatten-mail-attachments.ts b/scripts/flatten-mail-attachments.ts deleted file mode 100644 index fa08605..0000000 --- a/scripts/flatten-mail-attachments.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 将邮�?/4 子目录内业务附件平铺到邮件根目录�? * 用法: 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 { - try { - await fs.access(p); - return true; - } catch { - return false; - } -} - -async function moveUp(subdir: string, mailDir: string): Promise { - 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); - } - // 清空后删子目�? 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 卡转�? }, - { - mail: "邮件4", - nested: "原箱号YT2604021091=FBA199R49LD6-MDW2=76件换标操作指�?, - }, - ]; - - 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; -}); diff --git a/scripts/ingest-do-upload-ui.ts b/scripts/ingest-do-upload-ui.ts deleted file mode 100644 index beea5d2..0000000 --- a/scripts/ingest-do-upload-ui.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * 入库「上�?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 = "请查�?DO:柜�?WHSU8127240"; -const BODY = "Dear,\n请查收附�?DO 文件,谢谢�?; -const MESSAGE_ID = ""; -const DO_PDF = path.join( - process.cwd(), - "docx", - "邮件", - "模板", - "WHSU8127240-DO.pdf", -); - -async function attachDo(mailId: bigint): Promise { - 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(); - }); diff --git a/scripts/ingest-forecast-ui.ts b/scripts/ingest-forecast-ui.ts deleted file mode 100644 index 4cc12c2..0000000 --- a/scripts/ingest-forecast-ui.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * 入库「邮�? 完整线程�? 新辰泽卡派模板(历史预报货件表)�? * 最新指令是换标/贴标工单;主题链路上的新增预报仅作历史只读�? * 用法: 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 = ""; -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 { - 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(); - }); diff --git a/scripts/ingest-mail3.ts b/scripts/ingest-mail3.ts deleted file mode 100644 index 7ef5dbc..0000000 --- a/scripts/ingest-mail3.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * å°?docx/邮件/邮件3 主题注入本地库并è·?ParsePipeline(主é¢?only,无正文/附件)ã€? - * 用法: pnpm sample:mail3 - * - * 完整样例(含卡转æµ?PDF 入库)请用:pnpm sample:mails - */ -import { createHash } from "crypto"; -import { prisma } from "@/services/db"; -import { ParsePipeline } from "@/services/parse/pipeline"; - -const MAIL3_SUBJECT = - "Fw: 转发:派送要求更æ–? 拆柜清单更新ï¼?DO请查æ”?拆柜清单更新: WHSU5574991+WHL063G550810+船名航次:OOCL SINGAPORE / 065W+ETAï¼?/28+FedEx-29ä»?UPS-102ä»?亚马逊卡æ´?289ä»?私人地址-166ä»?拦截-209ä»?拆柜清单"; - -const MAIL3_BODY = [ - "派送要求更新,请查æ”?DO 与拆柜清单ã€?, - "柜号:WHSU5574991", - "提单:WHL063G550810", - "船名航次:OOCL SINGAPORE / 065W", - "ETAï¼?026-06-28", -].join("\n"); - -const MESSAGE_ID = ""; -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(); - }); diff --git a/scripts/ingest-recognize-gold.ts b/scripts/ingest-recognize-gold.ts deleted file mode 100644 index 6d506f7..0000000 --- a/scripts/ingest-recognize-gold.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * 金样入库:docx/邮件/模板/附件下载_邮件识别 - * 四类识别:新增预�?+ 上传 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+洛杉�?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆�?; -const BODY = [ - "请查收新增预报:智鸿2+WHLC027G597465+WHSU8127240+洛杉�?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆�?, - "DO也同步上传至附件请注意查收!", -].join("\n"); -const MESSAGE_ID = ""; - -const CONTENT_TYPES: Record = { - ".xlsx": - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ".xls": "application/vnd.ms-excel", - ".pdf": "application/pdf", - ".csv": "text/csv", -}; - -async function collectGoldFiles(dir: string): Promise { - 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 { - 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 { - 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(); - }); diff --git a/scripts/ingest-sample-mails.ts b/scripts/ingest-sample-mails.ts deleted file mode 100644 index d8127c4..0000000 --- a/scripts/ingest-sample-mails.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * å°?docx/邮件/邮件1~4(及可选模板预报)注入本地库并è·?ParsePipelineã€? * 用法: pnpm sample:mails - * - * 目录约定(平铺)ï¼? * docx/邮件/邮件N/ - * *邮箱.pdf / QQ邮箱.pdf â€?QQ 截图(默认不入库ï¼? * *.xlsx / 业务 *.pdf â€?业务附件(按 SampleDef.attachPolicy 入库ï¼? * docx/邮件/模板/ â€?共享预报模板(M_BL / forecast-uiï¼? * - * M1 主题-only(UNKNOWNï¼? * M2 主题+卡派 xlsx(WORK_ORDER/转仓ï¼? * M3 主题 + 卡转æµ?PDF 展示(WORK_ORDER 拆柜清单;解析仍主题短路ï¼? * M4 主题+换标正文 + 换标指令 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/邮件/ 的目录名,如 邮件2 */ - sampleDir: string; - attachPolicy: AttachPolicy; - /** 可选:文件名须包含的关键词(全部命中) */ - nameIncludes?: string[]; -}; - -const SAMPLES: SampleDef[] = [ - { - key: "mail1", - messageId: "", - fromAddr: "cs3@xinfenginc.com", - subject: "Fw: 转发:TIIU8073522-90022", - bodyText: [ - "(样例)主题为柜号线索,完整正文è§?QQ 邮箱截图附件ã€?, - "柜号线索:TIIU8073522", - ].join("\n"), - receivedAt: "2026-07-14T08:02:00.000Z", - sampleDir: "邮件1", - attachPolicy: "none", - }, - { - key: "mail2", - messageId: "", - fromAddr: "op7@xinfenginc.com", - subject: - "Fw: 转发:LINK EVER INC + MATS4583030000+ 柜号:MATU2745683+ ETA : 7/13", - bodyText: "新增转仓,请留意\n柜号:MATU2745683 更新派送单,请查收", - receivedAt: "2026-07-14T08:02:00.000Z", - sampleDir: "邮件2", - attachPolicy: "packing_xlsx", - nameIncludes: ["MATU2745683"], - }, - { - key: "mail3", - messageId: "", - fromAddr: "clx@cnwally.com.cn", - subject: - "Fw: 转发:派送要求更æ–? 拆柜清单更新ï¼?DO请查æ”?拆柜清单更新: WHSU5574991+WHL063G550810+船名航次:OOCL SINGAPORE / 065W+ETAï¼?/28+FedEx-29ä»?UPS-102ä»?亚马逊卡æ´?289ä»?私人地址-166ä»?拦截-209ä»?拆柜清单", - bodyText: [ - "派送要求更新,请查æ”?DO 与拆柜清单ã€?, - "柜号:WHSU5574991", - "提单:WHL063G550810", - "船名航次:OOCL SINGAPORE / 065W", - "ETAï¼?026-06-28", - "FedEx-29ä»?UPS-102ä»?亚马逊卡æ´?289ä»?私人地址-166ä»?拦截-209ä»?, - ].join("\n"), - receivedAt: "2026-07-14T08:02:00.000Z", - sampleDir: "邮件3", - // 卡转æµ?PDF 入库供详情展示;解析仍走主题短路,不吃表æ ? attachPolicy: "business", - }, - { - key: "mail4", - messageId: "", - fromAddr: "xinchenze002@126.com", - subject: - "Fw: 转发:新增预æŠ?36:新辰泽+WHLC027G597465+WHSU8127240+洛杉çŸ?40HQ+EDT2026.04-25 ETA2026.05-15船名航次HMM EMERALD 013E+提拆派组合柜-不带托架", - bodyText: [ - "Dear,", - "原箱号YT2604021091=FBA199R49LD6-MDW2=76件操作指ä»?, - "换标后单号:FBA19HW52S0L-2G1MCB6X-HIA1=76ä»?, - "1:覆盖贴FBA标签,一箱贴两张", - "2:覆盖贴SKU标签,一ç®?张(FNSKU:X003UHDF7Bï¼?04PCS))", - "3:贴好拍照回传等国内客户确认后,再约仓卡派交付!", - "注:回传过来的照片需要清晰拍照SKU上面的字迹,客户确认后再安排卡派交付,不确认不予安排!!ï¼?, - ].join("\n"), - receivedAt: "2026-07-14T08:00:00.000Z", - sampleDir: "邮件4", - attachPolicy: "label_instruction", - }, -]; - -async function resolveSampleDir(sampleDir: string): Promise { - const direct = path.join(DOC_MAIL_ROOT, sampleDir); - try { - const st = await fs.stat(direct); - if (st.isDirectory()) return direct; - } catch { - /* fall through */ - } - // 兜底:在 docx/邮件 下按目录名包含匹é…? 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; -} - -/** 递归收集目录内文件(含一层子目录,兼容未平铺旧结构) */ -async function listFilesRecursive( - root: string, -): Promise> { - const out: Array<{ abs: string; filename: string; rel: string }> = []; - const stack = [root]; - while (stack.length) { - const cur = stack.pop()!; - let entries: Awaited>; - 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 { - 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 { - 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> = []; - - 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(); - }); diff --git a/scripts/mark-non-business-ignored.ts b/scripts/mark-non-business-ignored.ts deleted file mode 100644 index 4c099c3..0000000 --- a/scripts/mark-non-business-ignored.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 将库内已拉入的非预报/装箱相关邮件标为 IGNORED(列表默认隐藏)�? - * 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()); diff --git a/scripts/patch-classify-gold.ts b/scripts/patch-classify-gold.ts deleted file mode 100644 index de4aa09..0000000 --- a/scripts/patch-classify-gold.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Align classify with 附件下载_邮件识别 gold: - * - 新增预报 / 请查收新增预�?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 (/新增预报|请查收新增预�?.test(text)) { - signals.push({ - signal: "新增预报", - score: 50, - matched: "新增预报", - source: /新增预报|请查收新增预�?.test(subject) ? "subject" : "body", - }); - scores.NEW_CONTAINER += 50; - } - - // 金样:数据模�?/ 卡派清单�?xlsx �?预报�? - 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"); diff --git a/scripts/patch-classify-packing-narrow.ts b/scripts/patch-classify-packing-narrow.ts deleted file mode 100644 index af84117..0000000 --- a/scripts/patch-classify-packing-narrow.ts +++ /dev/null @@ -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 = ` // 金样:数据模�?/ 明确预报清单附件 �?NEW_CONTAINER(不把单纯「卡派资料」当预报�? - 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"); diff --git a/scripts/patch-classify-wo-fallback.ts b/scripts/patch-classify-wo-fallback.ts deleted file mode 100644 index bb9b922..0000000 --- a/scripts/patch-classify-wo-fallback.ts +++ /dev/null @@ -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"); diff --git a/scripts/patch-date-shell-rule.ts b/scripts/patch-date-shell-rule.ts deleted file mode 100644 index 5f3b095..0000000 --- a/scripts/patch-date-shell-rule.ts +++ /dev/null @@ -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"); diff --git a/scripts/patch-detail-full-body-ui.ts b/scripts/patch-detail-full-body-ui.ts deleted file mode 100644 index 7233917..0000000 --- a/scripts/patch-detail-full-body-ui.ts +++ /dev/null @@ -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 = ` - {mail.body_text || - mail.ocr_text || - "(无正�?"} - - ), - }, - ]} - />`; - -const neu = ` - - 入库原文完整保留,解�?OCR 不会覆盖此字段。可滚动查看全部内容�? - -
-                  {(mail.body_text || "").trim()
-                    ? mail.body_text
-                    : "(无正�?"}
-                
- {(mail.ocr_text || "").trim() ? ( - <> - - OCR 文本(附件识别,与原文分开保存)·{" "} - {(mail.ocr_text || "").length} �? - -
-                      {mail.ocr_text}
-                    
- - ) : null} - - ), - }, - ]} - />`; - -if (!src.includes('label: "原始正文(排查用�?')) { - 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("", end); -// better: match from 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 \n\n \r?\n\r?\n?\s*\r?\n)(\s*
)/; -if (!re.test(src)) throw new Error("step2 not found"); - -src = src.replace( - re, - `$1
-
- \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 -
-
    - {PACKING_FILL_TIPS.map((t) => ( -
  1. {t}
  2. - ))} -
-
-$2`, -); - -fs.writeFileSync(path, src); -console.log("ok", src.includes("PACKING_FILL_TIPS.map")); diff --git a/scripts/patch-ingest-full-body.ts b/scripts/patch-ingest-full-body.ts deleted file mode 100644 index b6b5d6a..0000000 --- a/scripts/patch-ingest-full-body.ts +++ /dev/null @@ -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")); diff --git a/scripts/patch-latest-readonly.ts b/scripts/patch-latest-readonly.ts deleted file mode 100644 index 01285a5..0000000 --- a/scripts/patch-latest-readonly.ts +++ /dev/null @@ -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(); - 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); diff --git a/scripts/patch-packing-note-tests.ts b/scripts/patch-packing-note-tests.ts deleted file mode 100644 index 8fec9a4..0000000 --- a/scripts/patch-packing-note-tests.ts +++ /dev/null @@ -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"); - } -} diff --git a/scripts/patch-packing-skip-notes.ts b/scripts/patch-packing-skip-notes.ts deleted file mode 100644 index d18616c..0000000 --- a/scripts/patch-packing-skip-notes.ts +++ /dev/null @@ -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 �?not a shipment */ -export function isInstructionNoiseRow(raw: Record): 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)"), -}); diff --git a/scripts/patch-preserve-body-text.ts b/scripts/patch-preserve-body-text.ts deleted file mode 100644 index c5cb6f1..0000000 --- a/scripts/patch-preserve-body-text.ts +++ /dev/null @@ -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 : "(无主�?"; - // 历史库可能存了原�?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 : "(无主�?"; - // 解析用归一化正文;绝不回写 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 只写�?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 只写�?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 �?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 }"), -}); diff --git a/scripts/patch-resolve-fuller-body.ts b/scripts/patch-resolve-fuller-body.ts deleted file mode 100644 index 1307eb7..0000000 --- a/scripts/patch-resolve-fuller-body.ts +++ /dev/null @@ -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); diff --git a/scripts/patch-shell-subject-noise.ts b/scripts/patch-shell-subject-noise.ts deleted file mode 100644 index 326bc76..0000000 --- a/scripts/patch-shell-subject-noise.ts +++ /dev/null @@ -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"); -} diff --git a/scripts/patch-skip-instruction-rows.ts b/scripts/patch-skip-instruction-rows.ts deleted file mode 100644 index 7174799..0000000 --- a/scripts/patch-skip-instruction-rows.ts +++ /dev/null @@ -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 �?not a shipment */ -export function isInstructionNoiseRow(raw: Record): 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"); - } -} diff --git a/scripts/patch-skip-shell-segments.ts b/scripts/patch-skip-shell-segments.ts deleted file mode 100644 index b841ad1..0000000 --- a/scripts/patch-skip-shell-segments.ts +++ /dev/null @@ -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/中间人纯转发壳,无客�?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) || - /新增预报|请查收新增预�?.test(segBody); - const subjectHits = useSubject - ? matchKeywordRules(segSubject, "subject") - : []; - const kinds = new Set([ - ...bodyHits.map((h) => h.uiKind), - ...subjectHits.map((h) => h.uiKind), - ]); - return UI_KIND_ORDER.filter((k) => kinds.has(k)); -}`, - `/** 去掉段内「主�? Re:Re:新增预报」历史主题污�?*/ -function bodyForKindDetect(segBody: string): string { - if (!BODY_ACTION_RE.test(segBody)) return segBody; - return segBody - .split(/\\n/) - .filter( - (line) => - !/(?:主题|主旨|Subject)\\s*[�?].*(?:新增预报|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) || - /请查收新增预�?.test(bodyText); - const subjectHits = useSubject - ? matchKeywordRules(segSubject, "subject") - : []; - const kinds = new Set([ - ...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"), - }), -); diff --git a/scripts/patch-split-segment-kinds.ts b/scripts/patch-split-segment-kinds.ts deleted file mode 100644 index 964f41e..0000000 --- a/scripts/patch-split-segment-kinds.ts +++ /dev/null @@ -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(); - 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(); - 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)); -} - -/** 单段:正文有动作词时,忽�?Re: 链路上的历史「新增预报」主�?*/ -export function detectKindsForSegment( - segSubject: string, - segBody: string, -): InstructionUiKind[] { - if (isNonInstructionSegment(segBody)) return []; - const bodyHits = matchKeywordRules(segBody, "body"); - const useSubject = - !BODY_ACTION_RE.test(segBody) || - /新增预报|请查收新增预�?.test(segBody); - const subjectHits = useSubject - ? matchKeywordRules(segSubject, "subject") - : []; - const kinds = new Set([ - ...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"), - }), -); diff --git a/scripts/patch-strip-conservative.ts b/scripts/patch-strip-conservative.ts deleted file mode 100644 index cbe45ae..0000000 --- a/scripts/patch-strip-conservative.ts +++ /dev/null @@ -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(); diff --git a/scripts/patch-strip-subject-block.ts b/scripts/patch-strip-subject-block.ts deleted file mode 100644 index 9b240a5..0000000 --- a/scripts/patch-strip-subject-block.ts +++ /dev/null @@ -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"); -} diff --git a/scripts/patch-ui-customer-fallback.ts b/scripts/patch-ui-customer-fallback.ts deleted file mode 100644 index e83b90b..0000000 --- a/scripts/patch-ui-customer-fallback.ts +++ /dev/null @@ -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"); diff --git a/scripts/patch-ui-latest-readonly.ts b/scripts/patch-ui-latest-readonly.ts deleted file mode 100644 index 0d95541..0000000 --- a/scripts/patch-ui-latest-readonly.ts +++ /dev/null @@ -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 ? ( - - {unit.keywords.slice(0, 6).map((k) => ( - {k} - ))} - - ) : null - }`; - const newExtra = ` extra={ - - - {unit.isCurrent ? "\u53ef\u786e\u8ba4" : "\u5386\u53f2\u53ea\u8bfb"} - - {unit.keywords.slice(0, 5).map((k) => ( - {k} - ))} - - }`; - 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 ? ( - void submitWorkOrder()} - > - \u8f6c\u4ed3\u4e0d\u53ef\u7528\uff0c\u63d0\u4ea4\u5de5\u5355 - - ) : ( - - ); - }`, - ` if (canConfirmOps && unit.isCurrent) { - actions = blocked ? ( - - ) : ( - - ); - }`, - ); - - // work_order mode - src = src.replace( - ` setWorkOrderValue((v) => ({ ...v, ...p }))} - /> - ); - if (canConfirmOps && mail.mail_type === "WORK_ORDER") {`, - ` setWorkOrderValue((v) => ({ ...v, ...p }))} - /> - ); - if (canConfirmOps && unit.isCurrent) {`, - ); - - // do_upload - src = src.replace( - ` setDoValue((v) => ({ ...v, ...p }))} - /> - ); - if ( - canConfirmOps && - (mail.mail_type === "DO_UPLOAD" || unit.isCurrent) - ) {`, - ` 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"'), - }); -} diff --git a/scripts/patch-wire-packing-fill-rules.ts b/scripts/patch-wire-packing-fill-rules.ts deleted file mode 100644 index 30c3d36..0000000 --- a/scripts/patch-wire-packing-fill-rules.ts +++ /dev/null @@ -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[] = []; - // 亚马逊仓必填仓库代码;快�?私人地址可空 - 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("总体积缺�?); - 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,�?/;]+/.test(fbaIdRaw) && /FBA/i.test(fbaIdRaw)) { - warnings.push("MULTI_FBAID_IN_CELL"); - } - if (/[\\s,�?/;]{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")); diff --git a/scripts/purge-all-mails.ts b/scripts/purge-all-mails.ts deleted file mode 100644 index 2b2658f..0000000 --- a/scripts/purge-all-mails.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 清空全部邮件及相关解�?导入/附件记录(保留邮箱账号等设置)�? - * 用法: 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(); - }); diff --git a/scripts/rewrite-mail-business-summary.ts b/scripts/rewrite-mail-business-summary.ts deleted file mode 100644 index 013bc77..0000000 --- a/scripts/rewrite-mail-business-summary.ts +++ /dev/null @@ -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(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 ( - <> - - - {summaryText} - - {instructions.length > 1 ? ( -
- - ${ZH.splitPrefix}{instructions.length}${ZH.splitSuffix} - - - {instructions.map((u) => ( - {u.label} - ))} - -
- ) : null} - - {evidence.mailbox?.from || mail.from_addr || "-"} - - ), - }, - { - key: "action", - label: "${ZH.action}", - children: ( - - - {intent.action} - - ), - }, - { - 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 || "-", - }, - ]} - /> -
- - {instructions.map((unit, index) => { - let body: ReactNode = null; - let actions: ReactNode = null; - - if (unit.uiKind === "forecast") { - body = ( - - ); - } else if (unit.uiKind === "transfer") { - const blocked = gate && !gate.canTransfer; - body = ( - <> - {gate ? ( - - ) : null} - {blocked ? ( - - setWorkOrderValue((v) => ({ ...v, ...p })) - } - /> - ) : ( - - )} - - ); - if (canConfirmOps && unit.isCurrent) { - actions = blocked ? ( - - ) : ( - - ); - } - } else if (unit.uiKind === "work_order") { - const woForUnit = unit.isCurrent - ? workOrderValue - : buildCcWorkOrderFormValue({ - subject: unit.segmentSubject || mail.subject, - body: unit.segmentText || bodyText, - filenames, - containerNo, - }); - body = ( - setWorkOrderValue((v) => ({ ...v, ...p }))} - /> - ); - if (canConfirmOps && unit.isCurrent) { - actions = ( - - ); - } - } 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 = ( - setDoValue((v) => ({ ...v, ...p }))} - /> - ); - if (canConfirmOps && unit.isCurrent) { - actions = ( - - ); - } - } else if (unit.uiKind === "customer_instruction") { - body = ( - - ); - } - - return ( - - {body} - {actions ? ( -
{actions}
- ) : null} -
- ); - })} - - ); -} -`; - -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); diff --git a/scripts/scan-mail-samples.ts b/scripts/scan-mail-samples.ts deleted file mode 100644 index d170fd4..0000000 --- a/scripts/scan-mail-samples.ts +++ /dev/null @@ -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)); diff --git a/scripts/verify-reimport-rules.ts b/scripts/verify-reimport-rules.ts deleted file mode 100644 index 719e9e3..0000000 --- a/scripts/verify-reimport-rules.ts +++ /dev/null @@ -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: "注意�?.ups和fedex Truck", - F_FBACode: "注意�?.ups", - }); - console.log("\nisInstructionNoiseRow smoke:", noise); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(() => prisma.$disconnect());