|
|
/**
|
|
|
* 入库「邮<E3808C>? 完整线程<E7BABF>? 新辰泽卡派模板(历史预报货件表)<E8A1A8>? * 最新指令是换标/贴标工单;主题链路上的新增预报仅作历史只读<E58FAA>? * 用法: pnpm exec tsx scripts/ingest-forecast-ui.ts
|
|
|
*/
|
|
|
import { createHash } from "crypto";
|
|
|
import fs from "fs/promises";
|
|
|
import path from "path";
|
|
|
import { prisma } from "@/services/db";
|
|
|
import { ParsePipeline } from "@/services/parse/pipeline";
|
|
|
import { sha256 } from "@/utils/hash";
|
|
|
import { loadMailboxPdfBody } from "./lib/load-mailbox-pdf-body";
|
|
|
|
|
|
const SUBJECT =
|
|
|
"Fw: \u8f6c\u53d1\uff1a\u65b0\u589e\u9884\u62a5136\uff1a\u65b0\u8fb0\u6cfd+WHLC027G597465+WHSU8127240+\u6d1b\u6749\u77f6+40HQ+EDT2026.04-25 ETA2026.05-15\u8239\u540d\u822a\u6b21HMM EMERALD 013E+\u63d0\u62c6\u6d3e\u7ec4\u5408\u67dc-\u4e0d\u5e26\u6258\u67b6";
|
|
|
const FALLBACK_BODY = [
|
|
|
"Dear,",
|
|
|
"\u539f\u7bb1\u53f7YT2604021091=FBA199R49LD6-MDW2=76\u4ef6\u64cd\u4f5c\u6307\u4ee4",
|
|
|
"\u6362\u6807\u540e\u5355\u53f7\uff1aFBA19HW52S0L-2G1MCB6X-HIA1=76\u4ef6",
|
|
|
"1\uff1a\u8986\u76d6\u8d34FBA\u6807\u7b7e\uff0c\u4e00\u7bb1\u8d34\u4e24\u5f20",
|
|
|
"3\uff1a\u8d34\u597d\u62cd\u7167\u56de\u4f20",
|
|
|
].join("\n");
|
|
|
const MESSAGE_ID = "<sample-forecast-formorder-ui@local.test>";
|
|
|
const MAIL4_DIR = path.join(process.cwd(), "docx", "\u90ae\u4ef6", "\u90ae\u4ef64");
|
|
|
const TEMPLATE = path.join(
|
|
|
process.cwd(),
|
|
|
"docx",
|
|
|
"\u90ae\u4ef6",
|
|
|
"\u6a21\u677f",
|
|
|
"\u6570\u636e\u6a21\u7248-\u65b0\u8fb0\u6cfd-WHSU8127240.xlsx",
|
|
|
);
|
|
|
|
|
|
async function attachTemplate(mailId: bigint): Promise<void> {
|
|
|
const buf = await fs.readFile(TEMPLATE);
|
|
|
const hash = sha256(buf);
|
|
|
const filename = "\u65b0\u8fb0\u6cfd-\u5361\u6d3e\u8d44\u6599-WHSU8127240.xlsx";
|
|
|
const dir = path.join(process.cwd(), "data", "mails", String(mailId));
|
|
|
await fs.mkdir(dir, { recursive: true });
|
|
|
const dest = path.join(dir, filename);
|
|
|
await fs.writeFile(dest, buf);
|
|
|
const rel = path.relative(process.cwd(), dest).replace(/\\/g, "/");
|
|
|
await prisma.mailAttachment.create({
|
|
|
data: {
|
|
|
mailId,
|
|
|
filename,
|
|
|
contentType:
|
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
sha256: hash,
|
|
|
path: rel,
|
|
|
size: buf.length,
|
|
|
rejected: false,
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async function main() {
|
|
|
const pdfBody = await loadMailboxPdfBody(MAIL4_DIR);
|
|
|
const bodyText =
|
|
|
pdfBody && pdfBody.length > FALLBACK_BODY.length ? pdfBody : FALLBACK_BODY;
|
|
|
console.log(`body chars=${bodyText.length} (pdf=${Boolean(pdfBody)})`);
|
|
|
|
|
|
const rawHash = createHash("sha256")
|
|
|
.update(`forecast-ui|${MESSAGE_ID}|${SUBJECT}|${bodyText}|tpl-v5-full`)
|
|
|
.digest("hex");
|
|
|
|
|
|
const existing = await prisma.mailMessage.findFirst({
|
|
|
where: { OR: [{ messageId: MESSAGE_ID }, { rawHash }] },
|
|
|
});
|
|
|
|
|
|
let mailId: bigint;
|
|
|
if (existing) {
|
|
|
await prisma.importCompensation.deleteMany({
|
|
|
where: { import: { mailId: existing.id } },
|
|
|
});
|
|
|
await prisma.containerImport.deleteMany({ where: { mailId: existing.id } });
|
|
|
await prisma.parseResult.deleteMany({ where: { mailId: existing.id } });
|
|
|
await prisma.mailAttachment.deleteMany({ where: { mailId: existing.id } });
|
|
|
await prisma.mailMessage.update({
|
|
|
where: { id: existing.id },
|
|
|
data: {
|
|
|
subject: SUBJECT.slice(0, 512),
|
|
|
fromAddr: "forecast-ui@local.test",
|
|
|
bodyText,
|
|
|
status: "FETCHED",
|
|
|
mailType: "UNKNOWN",
|
|
|
lastError: null,
|
|
|
rawHash,
|
|
|
receivedAt: new Date("2026-04-20T08:00:00.000Z"),
|
|
|
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
|
|
|
version: { increment: 1 },
|
|
|
},
|
|
|
});
|
|
|
mailId = existing.id;
|
|
|
} else {
|
|
|
// also refresh by messageId if hash changed
|
|
|
const byMid = await prisma.mailMessage.findFirst({
|
|
|
where: { messageId: MESSAGE_ID },
|
|
|
});
|
|
|
if (byMid) {
|
|
|
await prisma.importCompensation.deleteMany({
|
|
|
where: { import: { mailId: byMid.id } },
|
|
|
});
|
|
|
await prisma.containerImport.deleteMany({ where: { mailId: byMid.id } });
|
|
|
await prisma.parseResult.deleteMany({ where: { mailId: byMid.id } });
|
|
|
await prisma.mailAttachment.deleteMany({ where: { mailId: byMid.id } });
|
|
|
await prisma.mailMessage.update({
|
|
|
where: { id: byMid.id },
|
|
|
data: {
|
|
|
subject: SUBJECT.slice(0, 512),
|
|
|
bodyText,
|
|
|
status: "FETCHED",
|
|
|
mailType: "UNKNOWN",
|
|
|
lastError: null,
|
|
|
rawHash,
|
|
|
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
|
|
|
version: { increment: 1 },
|
|
|
},
|
|
|
});
|
|
|
mailId = byMid.id;
|
|
|
} else {
|
|
|
const created = await prisma.mailMessage.create({
|
|
|
data: {
|
|
|
messageId: MESSAGE_ID,
|
|
|
subject: SUBJECT.slice(0, 512),
|
|
|
fromAddr: "forecast-ui@local.test",
|
|
|
bodyText,
|
|
|
status: "FETCHED",
|
|
|
mailType: "UNKNOWN",
|
|
|
rawHash,
|
|
|
folder: "INBOX",
|
|
|
receivedAt: new Date("2026-04-20T08:00:00.000Z"),
|
|
|
isThreadRoot: true,
|
|
|
typeEvidence: { note: "forecast_formorder_ui_full_thread" },
|
|
|
},
|
|
|
});
|
|
|
mailId = created.id;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
await attachTemplate(mailId);
|
|
|
await ParsePipeline.run(mailId);
|
|
|
|
|
|
const updated = await prisma.mailMessage.findUniqueOrThrow({
|
|
|
where: { id: mailId },
|
|
|
include: { parseResult: true, attachments: true },
|
|
|
});
|
|
|
|
|
|
console.log(
|
|
|
JSON.stringify(
|
|
|
{
|
|
|
id: String(updated.id),
|
|
|
mail_type: updated.mailType,
|
|
|
status: updated.status,
|
|
|
body_chars: (updated.bodyText || "").length,
|
|
|
has_label: /原箱号|换标/.test(updated.bodyText || ""),
|
|
|
shipments: ((updated.parseResult?.shipments as unknown[]) || []).length,
|
|
|
url: `http://localhost:3100/mails/${updated.id}`,
|
|
|
},
|
|
|
null,
|
|
|
2,
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
main()
|
|
|
.catch((e) => {
|
|
|
console.error(e);
|
|
|
process.exit(1);
|
|
|
})
|
|
|
.finally(async () => {
|
|
|
await prisma.$disconnect();
|
|
|
});
|