You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
5.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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