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