Keep smoke/ops scripts (imap/cc/retention) and product source. Co-authored-by: Cursor <cursoragent@cursor.com>main
parent
2b29fa7497
commit
7214d32627
@ -1,38 +0,0 @@
|
||||
import fs from "fs";
|
||||
import {
|
||||
extractMailInstructions,
|
||||
splitBodySegments,
|
||||
} from "../src/services/parse/split-instructions";
|
||||
|
||||
const raw = JSON.parse(
|
||||
fs.readFileSync("data/logs/mail-pdf-extract.json", "utf8"),
|
||||
) as Record<string, string[]>;
|
||||
|
||||
function bodyOf(keyPart: string): string {
|
||||
const key = Object.keys(raw).find((k) => k.includes(keyPart));
|
||||
if (!key) return "";
|
||||
return raw[key].join("\n");
|
||||
}
|
||||
|
||||
for (const name of ["邮件2", "邮件3", "邮件4"]) {
|
||||
const body = bodyOf(name);
|
||||
const segs = splitBodySegments(body);
|
||||
console.log(`\n==== ${name} segs=${segs.length} bodyLen=${body.length}`);
|
||||
segs.forEach((s, i) => {
|
||||
console.log(` ${i}: ${s.replace(/\s+/g, " ").slice(0, 100)}`);
|
||||
});
|
||||
const units = extractMailInstructions({
|
||||
subject: "",
|
||||
body,
|
||||
filenames: [],
|
||||
});
|
||||
console.log(
|
||||
" units",
|
||||
units.map((u) => ({
|
||||
kind: u.uiKind,
|
||||
cur: u.isCurrent,
|
||||
seg: u.segmentIndex,
|
||||
kw: u.keywords.slice(0, 4),
|
||||
})),
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile, re, html, os
|
||||
|
||||
docx = r"docx/邮件/模板/附件下载_邮件识别/邮件识别.docx"
|
||||
with zipfile.ZipFile(docx) as z:
|
||||
xml = z.read("word/document.xml").decode("utf-8")
|
||||
text = re.sub(r"<w:tab[^/]*/>", "\t", xml)
|
||||
text = re.sub(r"</w:p>", "\n", text)
|
||||
text = re.sub(r"<[^>]+>", "", text)
|
||||
text = html.unescape(text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
out = r"data/logs/mail-recognize-docx.txt"
|
||||
os.makedirs("data/logs", exist_ok=True)
|
||||
open(out, "w", encoding="utf-8").write(text)
|
||||
print(out, "chars", len(text))
|
||||
print(text[:4000])
|
||||
@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Extract plain text from a PDF (pypdf). Usage: python scripts/extract-pdf-text.py <pdf>"""
|
||||
import sys
|
||||
from pypdf import PdfReader
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
reader = PdfReader(path)
|
||||
parts = []
|
||||
for page in reader.pages:
|
||||
t = page.extract_text() or ""
|
||||
if t.strip():
|
||||
parts.append(t)
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
print("\n".join(parts))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,34 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
const p = "src/components/MailBusinessSummary.tsx";
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
|
||||
const btnWo = "\u786e\u8ba4\u63d0\u4ea4\u5de5\u5355";
|
||||
|
||||
t = t.replace(
|
||||
/(mail\.mail_type === "WORK_ORDER"\) \{\s*actions = \(\s*<Button[\s\S]*?>\s*)\?{2,}(\s*<\/Button>)/,
|
||||
`$1${btnWo}$2`,
|
||||
);
|
||||
|
||||
// Comments with ? are fine; flag remaining UI ?
|
||||
const bad = t
|
||||
.split("\n")
|
||||
.map((l, i) => ({ i: i + 1, l }))
|
||||
.filter(
|
||||
(x) =>
|
||||
/\?{3,}/.test(x.l) &&
|
||||
!x.l.includes("eslint") &&
|
||||
!x.l.trim().startsWith("//"),
|
||||
);
|
||||
|
||||
fs.writeFileSync(p, t, "utf8");
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
btnOk: t.includes(btnWo),
|
||||
badLines: bad.map((x) => `${x.i}: ${x.l.trim()}`),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
@ -1,21 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/services/parse/classify.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
// normalize for matching
|
||||
const nl = src.includes("\r\n") ? "\r\n" : "\n";
|
||||
|
||||
const softNeedle = `for (const kw of ["\u62c6\u67dc\u6e05\u5355", "\u5361\u8f6c\u6d77", "\u6d3e\u9001\u8981\u6c42", "\u6539\u81ea\u63d0", "\u7559\u4ed3"]) {`;
|
||||
const softRepl = `for (const kw of ["\u62c6\u67dc\u6e05\u5355", "\u5361\u8f6c\u6d77", "\u6d3e\u9001\u8981\u6c42", "\u6539\u81ea\u63d0", "\u66f4\u65b0\u6d3e\u9001\u5355", "\u7559\u4ed3"]) {`;
|
||||
if (!src.includes(softNeedle)) throw new Error("soft list missing");
|
||||
src = src.replace(softNeedle, softRepl);
|
||||
|
||||
const oldTail = ` if (bestScore < 40) best = "UNKNOWN";${nl}${nl} return { total: bestScore, mail_type: best, signals, scores };${nl}}`;
|
||||
const newTail = ` if (bestScore < 40) best = "UNKNOWN";${nl}${nl} // mail1-like: container / booking only -> WORK_ORDER${nl} if (${nl} best === "UNKNOWN" &&${nl} (/[A-Z]{4}\\d{7}/.test(text) || /\u9884\u7ea6\u7801/.test(text))${nl} ) {${nl} best = "WORK_ORDER";${nl} bestScore = 40;${nl} signals.push({${nl} signal: "\u65e0\u5173\u952e\u8bcd\u515c\u5e95",${nl} score: 40,${nl} matched: iso?.[0] || "\u9884\u7ea6\u7801",${nl} source: "body",${nl} });${nl} }${nl}${nl} return { total: bestScore, mail_type: best, signals, scores };${nl}}`;
|
||||
|
||||
if (!src.includes(oldTail)) throw new Error("tail missing: " + JSON.stringify(src.slice(src.indexOf("bestScore < 40"), src.indexOf("bestScore < 40") + 100)));
|
||||
src = src.replace(oldTail, newTail);
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("classify ok");
|
||||
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* DO_UPLOAD + bl-plus: keep customer_name from 客户+提单+柜号 template
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/services/parse/work-order-record.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
const old = ` if (input.mailType === "NEW_CONTAINER" && input.blModules) {
|
||||
return buildBlForecastRecord({
|
||||
modules: {
|
||||
...input.blModules,
|
||||
container_no:
|
||||
input.blModules.container_no || input.containerNo || undefined,
|
||||
},
|
||||
shipments,
|
||||
source: shipments.length ? "mixed" : "plus_template",
|
||||
plusPayload: input.plusPayload || undefined,
|
||||
});
|
||||
}`;
|
||||
|
||||
const neu = ` // DO \u4e3b\u7c7b\u578b\u65f6\u4ecd\u4fdd\u7559\u63d0\u5355\u6a21\u677f\uff08\u5ba2\u6237\u540d\u7b49\uff09\u2014\u2014\u91d1\u6837\u9884\u62a5+DO \u540c\u5c01
|
||||
if (
|
||||
input.blModules &&
|
||||
(input.mailType === "NEW_CONTAINER" || input.mailType === "DO_UPLOAD")
|
||||
) {
|
||||
const record = buildBlForecastRecord({
|
||||
modules: {
|
||||
...input.blModules,
|
||||
container_no:
|
||||
input.blModules.container_no || input.containerNo || undefined,
|
||||
},
|
||||
shipments,
|
||||
source: shipments.length ? "mixed" : "plus_template",
|
||||
plusPayload: input.plusPayload || undefined,
|
||||
});
|
||||
if (input.mailType === "DO_UPLOAD") {
|
||||
return {
|
||||
...record,
|
||||
summary: record.summary.replace(
|
||||
"\u65b0\u589e\u9884\u62a5\uff08\u63d0\u5355\u6a21\u677f\uff09",
|
||||
"\u4e0a\u4f20DO\uff08\u542b\u9884\u62a5\u6a21\u677f\uff09",
|
||||
),
|
||||
};
|
||||
}
|
||||
return record;
|
||||
}`;
|
||||
|
||||
if (!src.includes(old)) {
|
||||
// try CRLF
|
||||
const oldCrlf = old.replace(/\n/g, "\r\n");
|
||||
if (src.includes(oldCrlf)) {
|
||||
src = src.replace(oldCrlf, neu.replace(/\n/g, "\r\n"));
|
||||
} else {
|
||||
console.error("block not found");
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
src = src.replace(old, neu);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("buildMailRecord patched");
|
||||
@ -1,47 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/components/CcForecastFormOrder.tsx";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
if (src.includes("PACKING_FILL_TIPS")) {
|
||||
console.log("already");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
src = src.replace(
|
||||
/} from "@\/constants\/ui-copy";\r?\n/,
|
||||
`} from "@/constants/ui-copy";\r\nimport { PACKING_FILL_TIPS } from "@/services/parse/packing-fill-rules";\r\n`,
|
||||
);
|
||||
|
||||
const re =
|
||||
/(<div className="cc-fo-step2">\r?\n)(\s*<div className="cc-fo-toolbar">)/;
|
||||
if (!re.test(src)) throw new Error("step2 not found");
|
||||
|
||||
src = src.replace(
|
||||
re,
|
||||
`$1 <div
|
||||
className="cc-fo-packing-tips"
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
padding: "8px 10px",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.55,
|
||||
color: "#4b5563",
|
||||
background: "rgba(37,99,235,0.06)",
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4, color: "#1f2937" }}>
|
||||
\u8d27\u4ef6\u586b\u5199\u987b\u77e5\uff08\u5df2\u5199\u5165\u7cfb\u7edf\u6821\u9a8c\uff1b\u6a21\u677f\u300c\u6ce8\u610f\u300d\u8bf4\u660e\u884c\u4e0d\u5165\u8868\uff09
|
||||
</div>
|
||||
<ol style={{ margin: 0, paddingLeft: 18 }}>
|
||||
{PACKING_FILL_TIPS.map((t) => (
|
||||
<li key={t}>{t}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
$2`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("ok", src.includes("PACKING_FILL_TIPS.map"));
|
||||
@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Wire sample ingest to use full mailbox PDF text as bodyText.
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
const path = "scripts/ingest-sample-mails.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
if (!src.includes("loadMailboxPdfBody")) {
|
||||
src = src.replace(
|
||||
`import { sha256 } from "@/utils/hash";`,
|
||||
`import { sha256 } from "@/utils/hash";
|
||||
import { loadMailboxPdfBody } from "./lib/load-mailbox-pdf-body";`,
|
||||
);
|
||||
}
|
||||
|
||||
// bump hash version so re-ingest refreshes body
|
||||
src = src.replace(
|
||||
'`.update(`sample|${sample.key}|${sample.subject}|${sample.bodyText}`)',
|
||||
'`.update(`sample|${sample.key}|${sample.subject}|${sample.bodyText}|fullbody-v1`)',
|
||||
);
|
||||
// the above might be wrong quoting - fix:
|
||||
src = src.replace(
|
||||
"sample|${sample.key}|${sample.subject}|${sample.bodyText}`",
|
||||
"sample|${sample.key}|${sample.subject}|${sample.bodyText}|fullbody-v1`",
|
||||
);
|
||||
|
||||
const oldLoop = ` for (const sample of SAMPLES) {
|
||||
const mailId = await upsertMail(sample);
|
||||
const sampleDir = await resolveSampleDir(sample.sampleDir);`;
|
||||
|
||||
const neuLoop = ` for (const sample of SAMPLES) {
|
||||
const sampleDir = await resolveSampleDir(sample.sampleDir);
|
||||
let bodyText = sample.bodyText;
|
||||
if (sampleDir) {
|
||||
const pdfBody = await loadMailboxPdfBody(sampleDir);
|
||||
if (pdfBody && pdfBody.length > bodyText.length) {
|
||||
bodyText = pdfBody;
|
||||
console.log(
|
||||
\`\${sample.key}: body from mailbox PDF (\${pdfBody.length} chars)\`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const mailId = await upsertMail({ ...sample, bodyText });`;
|
||||
|
||||
if (!src.includes(oldLoop)) {
|
||||
console.error("loop missing");
|
||||
process.exit(1);
|
||||
}
|
||||
src = src.replace(oldLoop, neuLoop);
|
||||
|
||||
// remove duplicate sampleDir resolve
|
||||
src = src.replace(
|
||||
` const mailId = await upsertMail({ ...sample, bodyText });
|
||||
|
||||
let attached: string[] = [];
|
||||
if (!sampleDir) {
|
||||
console.warn(\`\${sample.key}: sample dir not found: \${sample.sampleDir}\`);
|
||||
} else if (sample.attachPolicy !== "none") {
|
||||
const files = await listFilesRecursive(sampleDir);`,
|
||||
` const mailId = await upsertMail({ ...sample, bodyText });
|
||||
|
||||
let attached: string[] = [];
|
||||
if (!sampleDir) {
|
||||
console.warn(\`\${sample.key}: sample dir not found: \${sample.sampleDir}\`);
|
||||
} else if (sample.attachPolicy !== "none") {
|
||||
const files = await listFilesRecursive(sampleDir);`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("ingest wired", src.includes("loadMailboxPdfBody"), src.includes("fullbody-v1"));
|
||||
@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Product rules:
|
||||
* 1) Only latest instruction segment confirmable; history readonly
|
||||
* 2) Soft keywords stay work_order
|
||||
* 3) No-keyword mail (mail1) -> work_order fallback
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/services/parse/split-instructions.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
const marker = " // 2) ";
|
||||
const idx = src.indexOf(marker);
|
||||
if (idx < 0) throw new Error("section 2 marker missing");
|
||||
|
||||
const attMarker = " // 3) ";
|
||||
const attIdx = src.indexOf(attMarker, idx);
|
||||
if (attIdx < 0) throw new Error("section 3 marker missing");
|
||||
|
||||
const sortMarker = " // \u5c55\u793a\u987a\u5e8f\uff1a\u5f53\u524d\u6bb5\u4f18\u5148";
|
||||
const sortIdx = src.indexOf(sortMarker);
|
||||
if (sortIdx < 0) throw new Error("sort marker missing");
|
||||
|
||||
const section2 = ` // 2) \u6b63\u6587\u6309\u5bf9\u8bdd\u6bb5\u62c6\u5206\uff1b\u4ec5\u300c\u6700\u65b0\u4e00\u6761\u6709\u6307\u4ee4\u7684\u6bb5\u300d\u53ef\u786e\u8ba4\uff0c\u5386\u53f2\u53ea\u8bfb
|
||||
const segments = splitBodySegments(body);
|
||||
let currentSegIndex: number | null = null;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const seg = segments[i];
|
||||
if (isNonInstructionSegment(seg)) continue;
|
||||
const kinds = detectKindsForSegment(extractSegmentSubject(seg, ""), seg);
|
||||
if (kinds.length) {
|
||||
currentSegIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// \u65e0\u5173\u952e\u8bcd\u6bb5\uff1a\u9996\u4e2a\u975e\u58f3\u6bb5\u4f5c\u4e3a\u515c\u5e95\u5de5\u5355\u8f7d\u4f53
|
||||
if (currentSegIndex === null) {
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (!isNonInstructionSegment(segments[i])) {
|
||||
currentSegIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kindsInCurrentBody = new Set<InstructionUiKind>();
|
||||
segments.forEach((seg, i) => {
|
||||
if (isNonInstructionSegment(seg)) return;
|
||||
const segSubject = extractSegmentSubject(seg, "");
|
||||
const kinds = detectKindsForSegment(segSubject, seg);
|
||||
const isCurrent = currentSegIndex !== null && i === currentSegIndex;
|
||||
if (!kinds.length) {
|
||||
// \u90ae\u4ef61 \u7b49\uff1a\u65e0\u56db\u7c7b\u5173\u952e\u8bcd \u2192 \u6700\u65b0\u6bb5\u515c\u5e95\u4e3a\u5de5\u5355
|
||||
if (isCurrent) {
|
||||
kindsInCurrentBody.add("work_order");
|
||||
push(
|
||||
makeUnit({
|
||||
uiKind: "work_order",
|
||||
source: "body_segment",
|
||||
segmentIndex: i,
|
||||
isCurrent: true,
|
||||
text: seg,
|
||||
segmentSubject: segSubject || undefined,
|
||||
extraKeywords: ["\u65e0\u5173\u952e\u8bcd\u515c\u5e95"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const kind of kinds) {
|
||||
if (isCurrent) kindsInCurrentBody.add(kind);
|
||||
push(
|
||||
makeUnit({
|
||||
uiKind: kind,
|
||||
source: "body_segment",
|
||||
segmentIndex: i,
|
||||
isCurrent,
|
||||
text: seg,
|
||||
segmentSubject: segSubject || undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
`;
|
||||
|
||||
// Replace from section 2 through just before section 3
|
||||
src = src.slice(0, idx) + section2 + src.slice(attIdx);
|
||||
|
||||
// Re-find markers after splice
|
||||
const attIdx2 = src.indexOf(attMarker);
|
||||
const pushAttRe =
|
||||
/push\(\s*makeUnit\(\{\s*uiKind: kind,\s*source: "attachment",\s*segmentIndex: -2,\s*isCurrent: true,\s*text: f,\s*extraKeywords: \[f\.slice\(0, 40\)\],\s*\}\),\s*\);/;
|
||||
|
||||
if (!pushAttRe.test(src.slice(attIdx2, attIdx2 + 800))) {
|
||||
throw new Error("attachment push block not found");
|
||||
}
|
||||
|
||||
src = src.replace(
|
||||
pushAttRe,
|
||||
`const attachCurrent =
|
||||
kindsInCurrentBody.size === 0 || !kindsInCurrentBody.has(kind);
|
||||
push(
|
||||
makeUnit({
|
||||
uiKind: kind,
|
||||
source: "attachment",
|
||||
segmentIndex: -2,
|
||||
isCurrent: Boolean(attachCurrent),
|
||||
text: f,
|
||||
extraKeywords: [f.slice(0, 40)],
|
||||
}),
|
||||
);`,
|
||||
);
|
||||
|
||||
const sortIdx2 = src.indexOf(sortMarker);
|
||||
if (sortIdx2 < 0) throw new Error("sort marker missing after edit");
|
||||
|
||||
const fallback = ` // \u6574\u5c01\u4ecd\u65e0\u5355\u5143 \u2192 \u5de5\u5355\u515c\u5e95
|
||||
if (!units.length && (subject.trim() || body.trim())) {
|
||||
push(
|
||||
makeUnit({
|
||||
uiKind: "work_order",
|
||||
source: "record",
|
||||
segmentIndex: -3,
|
||||
isCurrent: true,
|
||||
text: body.trim() || subject,
|
||||
segmentSubject: subject || undefined,
|
||||
extraKeywords: ["\u65e0\u5173\u952e\u8bcd\u515c\u5e95"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
src = src.slice(0, sortIdx2) + fallback + src.slice(sortIdx2);
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("patched", path);
|
||||
@ -1,57 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
// packing-list test
|
||||
{
|
||||
const path = "tests/unit/packing-list.test.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
if (!src.includes("skips \u6ce8\u610f instruction footer")) {
|
||||
const insert = `
|
||||
it("skips \u6ce8\u610f instruction footer in \u667a\u9e3f gold xlsx", async () => {
|
||||
const fs = await import("fs/promises");
|
||||
const path = await import("path");
|
||||
const file = path.join(
|
||||
process.cwd(),
|
||||
"docx",
|
||||
"\u90ae\u4ef6",
|
||||
"\u6a21\u677f",
|
||||
"\u9644\u4ef6\u4e0b\u8f7d_\u90ae\u4ef6\u8bc6\u522b",
|
||||
"\u667a\u9e3f2+WHLC027G597465+WHSU8127240+\u6d1b\u6749\u77f6+40HQ+EDT2026.04-25 ETA2026.05-15\u8239\u540d\u822a\u6b21HMM EMERALD 013E+\u63d0\u62c6\u6d3e.xlsx",
|
||||
);
|
||||
const buf = await fs.readFile(file);
|
||||
const parsed = await parsePackingListBuffer(buf, { enableIsoCheck: false });
|
||||
expect(parsed.errorCode).toBeUndefined();
|
||||
expect(parsed.shipments.length).toBe(1);
|
||||
expect(parsed.shipments[0].row_status).toBe("VALID");
|
||||
expect(parsed.shipments[0].F_Transporter).toBe("\u5b58\u4ed3");
|
||||
expect(parsed.shipments[0].F_CTNS).toBe(1041);
|
||||
expect(
|
||||
parsed.shipments.every((s) => !String(s.F_FBACode || "").includes("\u6ce8\u610f")),
|
||||
).toBe(true);
|
||||
});
|
||||
`;
|
||||
src = src.replace(/\n\}\);\s*$/, `${insert}\n});\n`);
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("packing test added");
|
||||
}
|
||||
}
|
||||
|
||||
// channel-map test
|
||||
{
|
||||
const path = "tests/unit/channel-map.test.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
if (!src.includes("ignores Truck inside instruction note")) {
|
||||
src = src.replace(
|
||||
/\n\}\);\s*$/,
|
||||
`
|
||||
it("ignores Truck inside instruction note", () => {
|
||||
const note =
|
||||
"\u6ce8\u610f\uff1a1.ups\u548cfedex\u7684\u4ef6... \u5982UPS/FEDEX/USPS/Truck \u6216 \u5361\u6d3e";
|
||||
expect(mapChannel(note).transporter).toBe("");
|
||||
});
|
||||
});
|
||||
`,
|
||||
);
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("channel test added");
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/utils/mail-body-text.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
const start = src.indexOf("export function resolveParsedMailBody");
|
||||
if (start < 0) throw new Error("missing");
|
||||
|
||||
const neu = `export function resolveParsedMailBody(parsed: {
|
||||
text?: string | false | null;
|
||||
html?: string | false | null;
|
||||
}): string {
|
||||
const text =
|
||||
typeof parsed.text === "string" ? parsed.text.trim() : "";
|
||||
const htmlRaw =
|
||||
typeof parsed.html === "string" ? parsed.html : "";
|
||||
const fromHtml = htmlRaw ? htmlToPlainText(htmlRaw) : "";
|
||||
|
||||
if (!text && !fromHtml) return "";
|
||||
if (!text) return fromHtml;
|
||||
if (!fromHtml) return text;
|
||||
|
||||
// Prefer longer side so troubleshooting body is not truncated
|
||||
if (fromHtml.length > text.length + 40) return fromHtml;
|
||||
if (text.length > fromHtml.length + 40) return text;
|
||||
|
||||
const textHead = text.slice(0, Math.min(48, text.length));
|
||||
if (textHead && fromHtml.includes(textHead) && fromHtml.length >= text.length) {
|
||||
return fromHtml;
|
||||
}
|
||||
const htmlHead = fromHtml.slice(0, Math.min(48, fromHtml.length));
|
||||
if (htmlHead && text.includes(htmlHead) && text.length >= fromHtml.length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Both long and not nested: keep both parts
|
||||
if (
|
||||
text.length > 80 &&
|
||||
fromHtml.length > 80 &&
|
||||
!fromHtml.includes(textHead) &&
|
||||
!text.includes(htmlHead)
|
||||
) {
|
||||
return \`\${text}\\n\\n---\\n\\n\${fromHtml}\`;
|
||||
}
|
||||
|
||||
return fromHtml.length >= text.length ? fromHtml : text;
|
||||
}
|
||||
`;
|
||||
|
||||
src = src.slice(0, start) + neu;
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("resolve ok, len", src.length);
|
||||
@ -1,114 +0,0 @@
|
||||
/**
|
||||
* Fix subject-line stripping: optional colon + multi-line subject block
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
const SUBJECT_LINE_RE =
|
||||
"/(?:\\u4e3b\\u9898|\\u4e3b\\u65e8|Subject)\\s*[\\uFF1A:]?/i";
|
||||
|
||||
const helper = `/** Drop subject / \\u4e3b\\u65e8 block (optional colon, multi-line wrap) */\nfunction stripSubjectLines(text: string): string {\n const lines = text.split(/\\n/);\n const out: string[] = [];\n let inSubject = false;\n for (const line of lines) {\n if (/(?:\\u4e3b\\u9898|\\u4e3b\\u65e8|Subject)\\s*[\\uFF1A:]?/i.test(line)) {\n inSubject = true;\n continue;\n }\n if (inSubject) {\n const t = line.trim();\n if (!t) {\n inSubject = false;\n continue;\n }\n if (\n /^(?:\\u5bc4\\u4ef6\\u4eba|\\u53d1\\u4ef6\\u4eba|\\u6536\\u4ef6\\u4eba|\\u6284\\u9001|\\u65e5\\u671f|\\u53d1\\u9001\\u65f6\\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(\n t,\n ) ||\n /^date@/i.test(t) ||\n t === "date" ||\n /^[\\u5728\\\\s]*\\d{4}/.test(t)\n ) {\n inSubject = false;\n out.push(line);\n continue;\n }\n continue;\n }\n out.push(line);\n }\n return out.join("\\n");\n}\n`;
|
||||
|
||||
// Simpler: write file with real unicode via \u in the script string that becomes Chinese
|
||||
const stripFn = `
|
||||
function stripSubjectLines(text: string): string {
|
||||
const lines = text.split(/\\n/);
|
||||
const out: string[] = [];
|
||||
let inSubject = false;
|
||||
for (const line of lines) {
|
||||
// "\\u4e3b\\u9898 Re:..." often has NO colon after \\u4e3b\\u9898
|
||||
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
|
||||
inSubject = true;
|
||||
continue;
|
||||
}
|
||||
if (inSubject) {
|
||||
const t = line.trim();
|
||||
if (!t) {
|
||||
inSubject = false;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(
|
||||
t,
|
||||
) ||
|
||||
/^date@/i.test(t) ||
|
||||
t === "date" ||
|
||||
/^\u5728\\s*\\d{4}/.test(t)
|
||||
) {
|
||||
inSubject = false;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join("\\n");
|
||||
}
|
||||
`.trimStart();
|
||||
|
||||
{
|
||||
const path = "src/services/parse/instruction-lexicon.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
const a = src.indexOf("function stripSubjectLines");
|
||||
const b = src.indexOf("export function isForwardShellSegment");
|
||||
if (a < 0 || b < 0) throw new Error("markers");
|
||||
// keep any comment before function - find from /** Drop or function
|
||||
let start = src.lastIndexOf("/** Drop subject", a);
|
||||
if (start < 0) start = a;
|
||||
src =
|
||||
src.slice(0, start) +
|
||||
"/** Drop subject/\u4e3b\u65e8 block (optional colon; multi-line wrap) */\n" +
|
||||
stripFn +
|
||||
"\n" +
|
||||
src.slice(b);
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("lexicon stripSubjectLines updated");
|
||||
}
|
||||
|
||||
{
|
||||
const path = "src/services/parse/split-instructions.ts";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
const neu = `function bodyForKindDetect(segBody: string): string {
|
||||
// reuse same multi-line subject strip as lexicon (inline copy to avoid circular import)
|
||||
const lines = segBody.split(/\\n/);
|
||||
const out: string[] = [];
|
||||
let inSubject = false;
|
||||
for (const line of lines) {
|
||||
if (/(?:\u4e3b\u9898|\u4e3b\u65e8|Subject)\\s*[\uFF1A:]?/i.test(line)) {
|
||||
inSubject = true;
|
||||
continue;
|
||||
}
|
||||
if (inSubject) {
|
||||
const t = line.trim();
|
||||
if (!t) {
|
||||
inSubject = false;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/^(?:\u5bc4\u4ef6\u4eba|\u53d1\u4ef6\u4eba|\u6536\u4ef6\u4eba|\u6284\u9001|\u65e5\u671f|\u53d1\u9001\u65f6\u95f4|From|To|Cc|Date|Sent|Dear|----)/i.test(
|
||||
t,
|
||||
) ||
|
||||
/^date@/i.test(t) ||
|
||||
t === "date" ||
|
||||
/^\\u5728\\s*\\d{4}/.test(t) ||
|
||||
/^\u5728\\s*\\d{4}/.test(t)
|
||||
) {
|
||||
inSubject = false;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join("\\n");
|
||||
}
|
||||
`;
|
||||
|
||||
const a = src.indexOf("function bodyForKindDetect");
|
||||
const b = src.indexOf("export function detectKindsForSegment");
|
||||
if (a < 0 || b < 0) throw new Error("bounds");
|
||||
src = src.slice(0, a) + neu + "\n" + src.slice(b);
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("split bodyForKindDetect updated");
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
/**
|
||||
* UI fallback: if mail_record missing customer, extract from subject bl-plus
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
const path = "src/components/MailBusinessSummary.tsx";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
if (!src.includes("extractBlPlusFromMail")) {
|
||||
const importNeedle = `import {
|
||||
extractForecastInstructionText,
|
||||
extractMailInstructions,
|
||||
} from "@/services/parse/split-instructions";`;
|
||||
const importRepl = `import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template";
|
||||
import {
|
||||
extractForecastInstructionText,
|
||||
extractMailInstructions,
|
||||
} from "@/services/parse/split-instructions";`;
|
||||
if (!src.includes(importNeedle)) throw new Error("import block missing");
|
||||
src = src.replace(importNeedle, importRepl);
|
||||
}
|
||||
|
||||
const oldForecast = ` const forecastValue = useMemo(() => {
|
||||
const base = buildCcForecastFormValue({
|
||||
customerName: record?.modules.customer_name,
|
||||
header,
|
||||
modules: record?.modules,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
F_Instruction: forecastInstruction || base.F_Instruction,
|
||||
};
|
||||
}, [
|
||||
header,
|
||||
record?.modules,
|
||||
record?.modules.customer_name,
|
||||
forecastInstruction,
|
||||
]);`;
|
||||
|
||||
const neuForecast = ` const blPlusModules = useMemo(
|
||||
() => extractBlPlusFromMail(mail.subject, bodyText)?.modules ?? null,
|
||||
[mail.subject, bodyText],
|
||||
);
|
||||
|
||||
const forecastValue = useMemo(() => {
|
||||
const modules = {
|
||||
...(blPlusModules || {}),
|
||||
...(record?.modules || {}),
|
||||
};
|
||||
const base = buildCcForecastFormValue({
|
||||
customerName:
|
||||
record?.modules.customer_name ||
|
||||
blPlusModules?.customer_name ||
|
||||
undefined,
|
||||
header,
|
||||
modules,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
F_Instruction: forecastInstruction || base.F_Instruction,
|
||||
};
|
||||
}, [
|
||||
header,
|
||||
record?.modules,
|
||||
record?.modules.customer_name,
|
||||
blPlusModules,
|
||||
forecastInstruction,
|
||||
]);`;
|
||||
|
||||
if (!src.includes(oldForecast)) {
|
||||
// CRLF
|
||||
const oldC = oldForecast.replace(/\n/g, "\r\n");
|
||||
if (!src.includes(oldC)) {
|
||||
console.error("forecastValue block missing");
|
||||
process.exit(1);
|
||||
}
|
||||
src = src.replace(oldC, neuForecast.replace(/\n/g, "\r\n"));
|
||||
} else {
|
||||
src = src.replace(oldForecast, neuForecast);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("MailBusinessSummary customer fallback ok");
|
||||
@ -1,148 +0,0 @@
|
||||
/**
|
||||
* UI: only unit.isCurrent confirmable; history readonly + tag
|
||||
*/
|
||||
import fs from "fs";
|
||||
|
||||
// --- Shell ---
|
||||
{
|
||||
const path = "src/components/MailInstructionUnitShell.tsx";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
const oldExtra = ` extra={
|
||||
unit.keywords.length ? (
|
||||
<Space wrap size={[4, 4]}>
|
||||
{unit.keywords.slice(0, 6).map((k) => (
|
||||
<Tag key={k}>{k}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : null
|
||||
}`;
|
||||
const newExtra = ` extra={
|
||||
<Space wrap size={[4, 4]}>
|
||||
<Tag color={unit.isCurrent ? "processing" : "default"}>
|
||||
{unit.isCurrent ? "\u53ef\u786e\u8ba4" : "\u5386\u53f2\u53ea\u8bfb"}
|
||||
</Tag>
|
||||
{unit.keywords.slice(0, 5).map((k) => (
|
||||
<Tag key={k}>{k}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
}`;
|
||||
if (!src.includes(oldExtra)) throw new Error("shell extra block missing");
|
||||
src = src.replace(oldExtra, newExtra);
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("shell ok");
|
||||
}
|
||||
|
||||
// --- MailBusinessSummary confirm gates ---
|
||||
{
|
||||
const path = "src/components/MailBusinessSummary.tsx";
|
||||
let src = fs.readFileSync(path, "utf8");
|
||||
|
||||
// transfer: only current can confirm / edit WO fallback
|
||||
src = src.replace(
|
||||
` {blocked ? (
|
||||
<CcWorkOrderForm
|
||||
mode={canConfirmOps ? "edit" : "readonly"}`,
|
||||
` {blocked ? (
|
||||
<CcWorkOrderForm
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}`,
|
||||
);
|
||||
|
||||
src = src.replace(
|
||||
` if (canConfirmOps) {
|
||||
actions = blocked ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitWorkOrder()}
|
||||
>
|
||||
\u8f6c\u4ed3\u4e0d\u53ef\u7528\uff0c\u63d0\u4ea4\u5de5\u5355
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitTransfer()}
|
||||
>
|
||||
\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3
|
||||
</Button>
|
||||
);
|
||||
}`,
|
||||
` if (canConfirmOps && unit.isCurrent) {
|
||||
actions = blocked ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitWorkOrder()}
|
||||
>
|
||||
\u8f6c\u4ed3\u4e0d\u53ef\u7528\uff0c\u63d0\u4ea4\u5de5\u5355
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitTransfer()}
|
||||
>
|
||||
\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3
|
||||
</Button>
|
||||
);
|
||||
}`,
|
||||
);
|
||||
|
||||
// work_order mode
|
||||
src = src.replace(
|
||||
` <CcWorkOrderForm
|
||||
mode={canConfirmOps ? "edit" : "readonly"}
|
||||
value={{
|
||||
...woForUnit,
|
||||
// keep edits on shared state when confirming primary WO
|
||||
...(mail.mail_type === "WORK_ORDER" && unit.isCurrent
|
||||
? workOrderValue
|
||||
: {}),
|
||||
}}
|
||||
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (canConfirmOps && mail.mail_type === "WORK_ORDER") {`,
|
||||
` <CcWorkOrderForm
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
||||
value={{
|
||||
...woForUnit,
|
||||
...(unit.isCurrent ? workOrderValue : {}),
|
||||
}}
|
||||
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (canConfirmOps && unit.isCurrent) {`,
|
||||
);
|
||||
|
||||
// do_upload
|
||||
src = src.replace(
|
||||
` <CcDoUploadPanel
|
||||
mode={canConfirmOps ? "edit" : "readonly"}
|
||||
value={
|
||||
mail.mail_type === "DO_UPLOAD" && unit.isCurrent
|
||||
? doValue
|
||||
: doForUnit
|
||||
}
|
||||
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
canConfirmOps &&
|
||||
(mail.mail_type === "DO_UPLOAD" || unit.isCurrent)
|
||||
) {`,
|
||||
` <CcDoUploadPanel
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
||||
value={unit.isCurrent ? doValue : doForUnit}
|
||||
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (canConfirmOps && unit.isCurrent) {`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(path, src);
|
||||
console.log("summary ok", {
|
||||
transferCur: src.includes("canConfirmOps && unit.isCurrent"),
|
||||
woMode: src.includes('mode={canConfirmOps && unit.isCurrent ? "edit"'),
|
||||
});
|
||||
}
|
||||
@ -1,679 +0,0 @@
|
||||
/**
|
||||
* Rewrite MailBusinessSummary.tsx fully in UTF-8.
|
||||
* ASCII-only script source; Chinese via \\u escapes only.
|
||||
* Run: pnpm exec tsx scripts/rewrite-mail-business-summary.ts
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const out = path.join(process.cwd(), "src", "components", "MailBusinessSummary.tsx");
|
||||
|
||||
const ZH = {
|
||||
cardTitle: "\u8fd9\u5c01\u90ae\u4ef6\u5728\u5e72\u4ec0\u4e48",
|
||||
splitPrefix: "\u5df2\u62c6\u51fa ",
|
||||
splitSuffix: " \u6761\u6307\u4ee4\uff1a",
|
||||
from: "\u53d1\u4ef6\u4eba",
|
||||
action: "\u4e1a\u52a1\u52a8\u4f5c",
|
||||
hint: "\u90ae\u4ef6\u8868\u8ff0",
|
||||
next: "\u7cfb\u7edf\u4e0b\u4e00\u6b65",
|
||||
vessel: "\u8239\u540d\u822a\u6b21 ",
|
||||
woMock: "\u5de5\u5355\u5df2\u63d0\u4ea4\uff08mock\uff09",
|
||||
woOk: "\u5de5\u5355\u5df2\u63d0\u4ea4",
|
||||
doMock: "DO \u5df2\u767b\u8bb0\uff08mock\uff09",
|
||||
doOk: "DO \u5df2\u4e0a\u4f20",
|
||||
needFba: "\u8bf7\u586b\u5199\u76ee\u6807 FBACode",
|
||||
xferMock: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4\uff08mock\uff09",
|
||||
xferOk: "\u6279\u91cf\u8f6c\u4ed3\u5df2\u63d0\u4ea4",
|
||||
msgType: "\u62c6\u67dc\u65f6\u81ea\u52a8\u8f6c\u4ed3",
|
||||
gate: "\u95f8\u95e8\uff1a",
|
||||
semi: "\uff1b",
|
||||
actXfer: "\u8f6c\u4ed3",
|
||||
btnWo: "\u786e\u8ba4\u63d0\u4ea4\u5de5\u5355",
|
||||
btnXfer: "\u786e\u8ba4\u6279\u91cf\u8f6c\u4ed3",
|
||||
btnDo: "\u786e\u8ba4\u4e0a\u4f20 DO",
|
||||
};
|
||||
|
||||
const file = `"use client";
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
CcDoUploadPanel,
|
||||
buildCcDoUploadFormValue,
|
||||
} from "@/components/CcDoUploadPanel";
|
||||
import {
|
||||
CcForecastFormOrder,
|
||||
buildCcForecastFormValue,
|
||||
} from "@/components/CcForecastFormOrder";
|
||||
import { CcCustomerInstructionPanel } from "@/components/CcCustomerInstructionPanel";
|
||||
import { InstructionUnitShell } from "@/components/MailInstructionUnitShell";
|
||||
import {
|
||||
CcTransferIndexTruck,
|
||||
buildCcTransferFormValue,
|
||||
} from "@/components/CcTransferIndexTruck";
|
||||
import {
|
||||
CcWorkOrderForm,
|
||||
buildCcWorkOrderFormValue,
|
||||
} from "@/components/CcWorkOrderForm";
|
||||
import { TypeTag } from "@/components/TypeTag";
|
||||
import {
|
||||
confirmDoUploadApi,
|
||||
confirmTransferApi,
|
||||
confirmWorkOrderApi,
|
||||
getTransferGateApi,
|
||||
listShippingLinesApi,
|
||||
} from "@/lib/client-api";
|
||||
import {
|
||||
buildMailIntent,
|
||||
extractTransferPairs,
|
||||
filterTransferShipments,
|
||||
} from "@/services/parse/mail-intent";
|
||||
import { extractBlPlusFromMail } from "@/services/parse/bl-plus-template";
|
||||
import {
|
||||
extractForecastInstructionText,
|
||||
extractMailInstructions,
|
||||
} from "@/services/parse/split-instructions";
|
||||
import type { MailMessage } from "@/types";
|
||||
import type {
|
||||
ContainerHeader,
|
||||
MailRecord,
|
||||
MailType,
|
||||
TypeEvidence,
|
||||
} from "@/types/mail";
|
||||
import type { TransferGateDecision } from "@/services/cc/container-status";
|
||||
|
||||
function asHeader(mail: MailMessage): ContainerHeader | null {
|
||||
return mail.parse_result?.container_header ?? null;
|
||||
}
|
||||
|
||||
function asMailRecord(mail: MailMessage): MailRecord | null {
|
||||
const direct = mail.parse_result?.mail_record;
|
||||
if (direct) return direct;
|
||||
const lineage = mail.parse_result?.lineage as
|
||||
| { mail_record?: MailRecord }
|
||||
| undefined;
|
||||
return lineage?.mail_record ?? null;
|
||||
}
|
||||
|
||||
type EvidenceExt = TypeEvidence & {
|
||||
mailbox?: { from?: string; inbox?: string; extracted?: string[] };
|
||||
};
|
||||
|
||||
export function MailBusinessSummary({
|
||||
mail,
|
||||
onConfirmed,
|
||||
}: {
|
||||
mail: MailMessage;
|
||||
onConfirmed?: () => void;
|
||||
}) {
|
||||
const { message } = App.useApp();
|
||||
const header = asHeader(mail);
|
||||
const record = asMailRecord(mail);
|
||||
const evidence = (mail.type_evidence || {
|
||||
total: 0,
|
||||
mail_type: mail.mail_type,
|
||||
signals: [],
|
||||
}) as EvidenceExt;
|
||||
|
||||
// Stable deps for useEffect
|
||||
const shipmentsRaw = mail.parse_result?.shipments;
|
||||
const attachmentsRaw = mail.attachments;
|
||||
const shipmentsKey = JSON.stringify(shipmentsRaw ?? []);
|
||||
const filenamesKey = (attachmentsRaw || []).map((a) => a.filename).join("?");
|
||||
const shipments = useMemo(
|
||||
() => shipmentsRaw ?? [],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by shipmentsKey
|
||||
[shipmentsKey],
|
||||
);
|
||||
const filenames = useMemo(
|
||||
() => (attachmentsRaw || []).map((a) => a.filename),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by filenamesKey
|
||||
[filenamesKey],
|
||||
);
|
||||
const bodyText = mail.body_text || mail.ocr_text || "";
|
||||
const containerNo = header?.F_ContainerNo || mail.container_no || "";
|
||||
const workOrderActionsKey = (record?.work_order_actions || []).join("|");
|
||||
const intentSignalsKey = JSON.stringify(evidence.signals ?? []);
|
||||
|
||||
const intent = useMemo(
|
||||
() =>
|
||||
evidence.intent ||
|
||||
buildMailIntent({
|
||||
mailType: mail.mail_type as MailType,
|
||||
subject: mail.subject,
|
||||
body: bodyText,
|
||||
filenames,
|
||||
shipments,
|
||||
evidence,
|
||||
containerNo,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- evidence default object unstable
|
||||
[
|
||||
evidence.intent,
|
||||
evidence.mail_type,
|
||||
evidence.total,
|
||||
intentSignalsKey,
|
||||
mail.mail_type,
|
||||
mail.subject,
|
||||
bodyText,
|
||||
filenames,
|
||||
shipments,
|
||||
containerNo,
|
||||
],
|
||||
);
|
||||
|
||||
const summaryText = record?.summary || intent.narrative;
|
||||
const transferPairs = useMemo(
|
||||
() =>
|
||||
intent.transfer_pairs?.length
|
||||
? intent.transfer_pairs
|
||||
: extractTransferPairs(shipments),
|
||||
[intent.transfer_pairs, shipments],
|
||||
);
|
||||
const transferShipments = useMemo(
|
||||
() => filterTransferShipments(shipments),
|
||||
[shipments],
|
||||
);
|
||||
|
||||
const instructions = useMemo(
|
||||
() =>
|
||||
extractMailInstructions({
|
||||
subject: mail.subject,
|
||||
body: bodyText,
|
||||
filenames,
|
||||
forceForecast: record?.kind === "BL_FORECAST",
|
||||
workOrderActions: record?.work_order_actions,
|
||||
}),
|
||||
[
|
||||
mail.subject,
|
||||
bodyText,
|
||||
filenames,
|
||||
record?.kind,
|
||||
workOrderActionsKey,
|
||||
],
|
||||
);
|
||||
|
||||
const forecastInstruction = useMemo(() => {
|
||||
const vessel = record?.modules.vessel_voyage;
|
||||
return extractForecastInstructionText({
|
||||
subject: mail.subject,
|
||||
body: bodyText,
|
||||
modulesInstruction: vessel
|
||||
? \`${ZH.vessel}\${vessel}\`
|
||||
: header?.F_Instruction || null,
|
||||
});
|
||||
}, [
|
||||
mail.subject,
|
||||
bodyText,
|
||||
header?.F_Instruction,
|
||||
record?.modules.vessel_voyage,
|
||||
]);
|
||||
|
||||
const hasForecastUi = instructions.some((u) => u.uiKind === "forecast");
|
||||
const hasTransferUi = instructions.some((u) => u.uiKind === "transfer");
|
||||
const canConfirmOps =
|
||||
mail.status === "PENDING_CONFIRM" || mail.status === "FAILED";
|
||||
|
||||
const blPlusModules = useMemo(
|
||||
() => extractBlPlusFromMail(mail.subject, bodyText)?.modules ?? null,
|
||||
[mail.subject, bodyText],
|
||||
);
|
||||
|
||||
const forecastValue = useMemo(() => {
|
||||
const modules = {
|
||||
...(blPlusModules || {}),
|
||||
...(record?.modules || {}),
|
||||
};
|
||||
const base = buildCcForecastFormValue({
|
||||
customerName:
|
||||
record?.modules.customer_name ||
|
||||
blPlusModules?.customer_name ||
|
||||
undefined,
|
||||
header,
|
||||
modules,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
F_Instruction: forecastInstruction || base.F_Instruction,
|
||||
};
|
||||
}, [
|
||||
header,
|
||||
record?.modules,
|
||||
record?.modules.customer_name,
|
||||
blPlusModules,
|
||||
forecastInstruction,
|
||||
]);
|
||||
|
||||
const transferValue = useMemo(
|
||||
() =>
|
||||
buildCcTransferFormValue({
|
||||
customerName: record?.modules.customer_name,
|
||||
containerNo,
|
||||
modules: record?.modules,
|
||||
shipments: transferShipments,
|
||||
transferPairs,
|
||||
}),
|
||||
[
|
||||
record?.modules.customer_name,
|
||||
containerNo,
|
||||
record?.modules,
|
||||
transferShipments,
|
||||
transferPairs,
|
||||
],
|
||||
);
|
||||
|
||||
const currentWorkOrderUnit = useMemo(
|
||||
() => instructions.find((u) => u.uiKind === "work_order" && u.isCurrent),
|
||||
[instructions],
|
||||
);
|
||||
|
||||
const [workOrderValue, setWorkOrderValue] = useState(() =>
|
||||
buildCcWorkOrderFormValue({
|
||||
subject: currentWorkOrderUnit?.segmentSubject || mail.subject,
|
||||
body: currentWorkOrderUnit?.segmentText || bodyText,
|
||||
filenames,
|
||||
containerNo,
|
||||
actions: record?.work_order_actions,
|
||||
}),
|
||||
);
|
||||
|
||||
const [doValue, setDoValue] = useState(() =>
|
||||
buildCcDoUploadFormValue({
|
||||
subject: mail.subject,
|
||||
body: bodyText,
|
||||
containerNo,
|
||||
filenames,
|
||||
}),
|
||||
);
|
||||
|
||||
const [gate, setGate] = useState<TransferGateDecision | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Reset forms when mail / current work-order segment changes
|
||||
useEffect(() => {
|
||||
setWorkOrderValue(
|
||||
buildCcWorkOrderFormValue({
|
||||
subject: currentWorkOrderUnit?.segmentSubject || mail.subject,
|
||||
body: currentWorkOrderUnit?.segmentText || bodyText,
|
||||
filenames,
|
||||
containerNo,
|
||||
actions: record?.work_order_actions,
|
||||
}),
|
||||
);
|
||||
setDoValue(
|
||||
buildCcDoUploadFormValue({
|
||||
subject: mail.subject,
|
||||
body: bodyText,
|
||||
containerNo,
|
||||
filenames,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
mail.id,
|
||||
mail.version,
|
||||
mail.subject,
|
||||
bodyText,
|
||||
containerNo,
|
||||
workOrderActionsKey,
|
||||
filenames,
|
||||
currentWorkOrderUnit?.id,
|
||||
currentWorkOrderUnit?.segmentText,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mail.mail_type !== "TRANSFER" && !hasTransferUi) return;
|
||||
let cancelled = false;
|
||||
void getTransferGateApi(mail.id).then((res) => {
|
||||
if (!cancelled && res.ok) setGate(res.data.gate);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mail.id, mail.mail_type, hasTransferUi]);
|
||||
|
||||
const [shippingLines, setShippingLines] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
if (!hasForecastUi) return;
|
||||
let cancelled = false;
|
||||
void listShippingLinesApi().then((res) => {
|
||||
if (!cancelled && res.ok) {
|
||||
setShippingLines(
|
||||
res.data.items.map((item) => ({ value: item.id, label: item.name })),
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [hasForecastUi]);
|
||||
|
||||
const submitWorkOrder = async () => {
|
||||
setSubmitting(true);
|
||||
const res = await confirmWorkOrderApi(mail.id, {
|
||||
version: mail.version,
|
||||
entity: workOrderValue,
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (!res.ok) {
|
||||
message.error(res.error.message);
|
||||
return;
|
||||
}
|
||||
message.success(
|
||||
res.data.mode === "mock" ? "${ZH.woMock}" : "${ZH.woOk}",
|
||||
);
|
||||
onConfirmed?.();
|
||||
};
|
||||
|
||||
const submitDo = async () => {
|
||||
setSubmitting(true);
|
||||
const res = await confirmDoUploadApi(mail.id, {
|
||||
version: mail.version,
|
||||
container_no: doValue.F_ContainerNo,
|
||||
container_task_id: doValue.F_ContainerTaskId || undefined,
|
||||
filenames: doValue.filenames,
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (!res.ok) {
|
||||
message.error(res.error.message);
|
||||
return;
|
||||
}
|
||||
message.success(
|
||||
res.data.mode === "mock" ? "${ZH.doMock}" : "${ZH.doOk}",
|
||||
);
|
||||
onConfirmed?.();
|
||||
};
|
||||
|
||||
const submitTransfer = async () => {
|
||||
const target =
|
||||
transferValue.targetFBACode || transferValue.F_ConvertFBACode || "";
|
||||
if (!target) {
|
||||
message.error("${ZH.needFba}");
|
||||
return;
|
||||
}
|
||||
const rows = transferShipments.length
|
||||
? transferShipments
|
||||
: shipments.slice(0, 1);
|
||||
setSubmitting(true);
|
||||
const res = await confirmTransferApi(mail.id, {
|
||||
version: mail.version,
|
||||
container_no:
|
||||
transferValue.F_ContainerNo ||
|
||||
header?.F_ContainerNo ||
|
||||
mail.container_no ||
|
||||
"",
|
||||
payload: {
|
||||
keyValue:
|
||||
gate && "containerTaskId" in gate ? gate.containerTaskId || "" : "",
|
||||
strDetialList: rows.map((r) => ({
|
||||
F_FBACode: r.F_FBACode || "",
|
||||
F_ConvertFBACode: target,
|
||||
})),
|
||||
F_ConvertChargeMode: transferValue.F_ConvertChargeMode || "2",
|
||||
F_RemovePlanDetail: transferValue.F_RemovePlanDetail ? "1" : "0",
|
||||
},
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (!res.ok) {
|
||||
message.error(res.error.message);
|
||||
return;
|
||||
}
|
||||
message.success(
|
||||
res.data.mode === "mock" ? "${ZH.xferMock}" : "${ZH.xferOk}",
|
||||
);
|
||||
onConfirmed?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="${ZH.cardTitle}">
|
||||
<Typography.Paragraph style={{ marginBottom: 12, fontSize: 15 }}>
|
||||
{summaryText}
|
||||
</Typography.Paragraph>
|
||||
{instructions.length > 1 ? (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Typography.Text type="secondary" style={{ marginRight: 8 }}>
|
||||
${ZH.splitPrefix}{instructions.length}${ZH.splitSuffix}
|
||||
</Typography.Text>
|
||||
<Space wrap size={[4, 4]}>
|
||||
{instructions.map((u) => (
|
||||
<Tag key={u.id}>{u.label}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={{ xs: 1, sm: 2, md: 2 }}
|
||||
items={[
|
||||
{
|
||||
key: "from",
|
||||
label: "${ZH.from}",
|
||||
children: (
|
||||
<span className="mono">
|
||||
{evidence.mailbox?.from || mail.from_addr || "-"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
label: "${ZH.action}",
|
||||
children: (
|
||||
<Space>
|
||||
<TypeTag type={mail.mail_type} />
|
||||
<Typography.Text>{intent.action}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "hint",
|
||||
label: "${ZH.hint}",
|
||||
children: intent.content_hint || "-",
|
||||
},
|
||||
{
|
||||
key: "next",
|
||||
label: "${ZH.next}",
|
||||
span: { xs: 1, sm: 2, md: 2 },
|
||||
children: intent.next_step || "-",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{instructions.map((unit, index) => {
|
||||
let body: ReactNode = null;
|
||||
let actions: ReactNode = null;
|
||||
|
||||
if (unit.uiKind === "forecast") {
|
||||
body = (
|
||||
<CcForecastFormOrder
|
||||
mode="readonly"
|
||||
value={forecastValue}
|
||||
shipments={shipments}
|
||||
shippingLines={shippingLines}
|
||||
/>
|
||||
);
|
||||
} else if (unit.uiKind === "transfer") {
|
||||
const blocked = gate && !gate.canTransfer;
|
||||
body = (
|
||||
<>
|
||||
{gate ? (
|
||||
<Alert
|
||||
style={{ marginBottom: 12 }}
|
||||
type={gate.canTransfer ? "success" : "warning"}
|
||||
showIcon
|
||||
message={gate.reason}
|
||||
/>
|
||||
) : null}
|
||||
{blocked ? (
|
||||
<CcWorkOrderForm
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
||||
value={{
|
||||
...workOrderValue,
|
||||
F_MessageType:
|
||||
workOrderValue.F_MessageType || "${ZH.msgType}",
|
||||
F_Remark: [
|
||||
workOrderValue.F_Remark,
|
||||
gate.reason ? \`${ZH.gate}\${gate.reason}\` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("${ZH.semi}"),
|
||||
mailActions: workOrderValue.mailActions?.length
|
||||
? workOrderValue.mailActions
|
||||
: ["${ZH.actXfer}"],
|
||||
}}
|
||||
onChange={(p) =>
|
||||
setWorkOrderValue((v) => ({ ...v, ...p }))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CcTransferIndexTruck
|
||||
mode="readonly"
|
||||
value={transferValue}
|
||||
shipments={transferShipments}
|
||||
transferPairs={transferPairs}
|
||||
totalShipmentCount={shipments.length}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (canConfirmOps && unit.isCurrent) {
|
||||
actions = blocked ? (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitWorkOrder()}
|
||||
>
|
||||
${ZH.btnWo}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitTransfer()}
|
||||
>
|
||||
${ZH.btnXfer}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else if (unit.uiKind === "work_order") {
|
||||
const woForUnit = unit.isCurrent
|
||||
? workOrderValue
|
||||
: buildCcWorkOrderFormValue({
|
||||
subject: unit.segmentSubject || mail.subject,
|
||||
body: unit.segmentText || bodyText,
|
||||
filenames,
|
||||
containerNo,
|
||||
});
|
||||
body = (
|
||||
<CcWorkOrderForm
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
||||
value={woForUnit}
|
||||
onChange={(p) => setWorkOrderValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (canConfirmOps && unit.isCurrent) {
|
||||
actions = (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitWorkOrder()}
|
||||
>
|
||||
${ZH.btnWo}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else if (unit.uiKind === "do_upload") {
|
||||
const doForUnit = buildCcDoUploadFormValue({
|
||||
subject: unit.segmentSubject || mail.subject,
|
||||
body: unit.segmentText || bodyText,
|
||||
containerNo,
|
||||
filenames:
|
||||
unit.source === "attachment"
|
||||
? [unit.segmentText]
|
||||
: filenames,
|
||||
});
|
||||
body = (
|
||||
<CcDoUploadPanel
|
||||
mode={canConfirmOps && unit.isCurrent ? "edit" : "readonly"}
|
||||
value={unit.isCurrent ? doValue : doForUnit}
|
||||
onChange={(p) => setDoValue((v) => ({ ...v, ...p }))}
|
||||
/>
|
||||
);
|
||||
if (canConfirmOps && unit.isCurrent) {
|
||||
actions = (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => void submitDo()}
|
||||
>
|
||||
${ZH.btnDo}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else if (unit.uiKind === "customer_instruction") {
|
||||
body = (
|
||||
<CcCustomerInstructionPanel
|
||||
value={{
|
||||
roundAt: unit.roundAt,
|
||||
roundFrom: unit.roundFrom,
|
||||
subject: unit.segmentSubject,
|
||||
body: unit.segmentText || bodyText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InstructionUnitShell
|
||||
key={unit.id}
|
||||
unit={unit}
|
||||
index={index}
|
||||
total={instructions.length}
|
||||
>
|
||||
{body}
|
||||
{actions ? (
|
||||
<div style={{ marginTop: 12, textAlign: "right" }}>{actions}</div>
|
||||
) : null}
|
||||
</InstructionUnitShell>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(out, file, "utf8");
|
||||
|
||||
const check = fs.readFileSync(out, "utf8");
|
||||
const bad = [...check.matchAll(/\?{3,}/g)].filter((m) => {
|
||||
const i = m.index || 0;
|
||||
const line = check.slice(check.lastIndexOf("\n", i) + 1, check.indexOf("\n", i));
|
||||
return !line.includes('join("?")');
|
||||
});
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
bytes: Buffer.byteLength(check, "utf8"),
|
||||
hasTitle: check.includes(ZH.cardTitle),
|
||||
hasFrom: check.includes(ZH.from),
|
||||
hasBtnWo: check.includes(ZH.btnWo),
|
||||
hasDearLogic: check.includes("currentWorkOrderUnit"),
|
||||
leftoverQ: bad.length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (!check.includes(ZH.cardTitle) || bad.length) process.exit(1);
|
||||
@ -1,27 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const root = path.join(process.cwd(), "docx", "邮件");
|
||||
|
||||
type Entry = { rel: string; kind: "DIR" | "FILE"; size?: number };
|
||||
|
||||
function walk(dir: string, out: Entry[]) {
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, e.name);
|
||||
const rel = path.relative(root, full).split(path.sep).join("/");
|
||||
if (e.isDirectory()) {
|
||||
out.push({ rel, kind: "DIR" });
|
||||
walk(full, out);
|
||||
} else {
|
||||
out.push({ rel, kind: "FILE", size: fs.statSync(full).size });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const out: Entry[] = [];
|
||||
if (!fs.existsSync(root)) {
|
||||
console.error("MISSING", root);
|
||||
process.exit(1);
|
||||
}
|
||||
walk(root, out);
|
||||
console.log(JSON.stringify({ root, count: out.length, entries: out }, null, 2));
|
||||
Loading…
Reference in new issue