import { prisma } from "@/services/db"; import { getEnv } from "@/lib/env"; import { runWithMailCcOwner } from "@/lib/cc-owner"; import { logger } from "@/lib/logger"; import { writeAudit } from "@/services/audit"; import { assertTransition } from "@/services/state-machine"; import { executeInstructionWrite } from "@/services/cc/instruction-write"; import type { MailStatus, ShipmentRow } from "@/types/mail"; function kindForMailType( mailType: string, ): "transfer" | "hold_split" | "label" | null { if (mailType === "TRANSFER") return "transfer"; if (mailType === "INSTRUCTION_HOLD_SPLIT") return "hold_split"; if (mailType === "INSTRUCTION_LABEL") return "label"; return null; } function autoExecEnabled(mailType: string): boolean { const env = getEnv(); if (mailType === "TRANSFER") return env.ENABLE_AUTO_EXEC_TRANSFER; if (mailType === "INSTRUCTION_HOLD_SPLIT") return env.ENABLE_AUTO_EXEC_HOLD_SPLIT; if (mailType === "INSTRUCTION_LABEL") return env.ENABLE_AUTO_EXEC_LABEL; return false; } export function isAutoExecEligible(mailType: string): boolean { // 四类主业务走人工确认,不再自动写 if ( mailType === "WORK_ORDER" || mailType === "DO_UPLOAD" || mailType === "TRANSFER" || mailType === "NEW_CONTAINER" ) { return false; } if (getEnv().AUTO_EXEC_REQUIRE_CONFIRM) return false; return autoExecEnabled(mailType) && kindForMailType(mailType) != null; } /** 解析后或 due 调度:对 AUTO_EXECUTING 邮件调 CC 写接口 */ export async function runAutoExec(mailId: bigint): Promise<{ ok: boolean; status: string; error?: string; }> { return runWithMailCcOwner(mailId, () => runAutoExecInner(mailId)); } async function runAutoExecInner(mailId: bigint): Promise<{ ok: boolean; status: string; error?: string; }> { const mail = await prisma.mailMessage.findUnique({ where: { id: mailId }, include: { parseResult: true }, }); if (!mail) return { ok: false, status: "MISSING", error: "NOT_FOUND" }; const kind = kindForMailType(mail.mailType); if (!kind || !autoExecEnabled(mail.mailType)) { if (mail.status === "AUTO_EXECUTING") { await prisma.mailMessage.update({ where: { id: mailId }, data: { status: "PARSED", lastError: "AUTO_EXEC_DISABLED" }, }); } return { ok: false, status: "SKIPPED", error: "DISABLED" }; } const from = mail.status as MailStatus; if (from !== "AUTO_EXECUTING") { assertTransition(from, "AUTO_EXECUTING"); await prisma.mailMessage.update({ where: { id: mailId }, data: { status: "AUTO_EXECUTING", version: { increment: 1 } }, }); } const header = mail.parseResult?.containerHeader as | { F_ContainerNo?: string; F_Instruction?: string } | undefined; const shipments = ((mail.parseResult?.shipments || []) as unknown) as ShipmentRow[]; const result = await executeInstructionWrite(kind, { mailId: mailId.toString(), containerNo: header?.F_ContainerNo, subject: mail.subject, shipments, remark: header?.F_Instruction || null, }); const action = kind === "transfer" ? "AUTO_EXEC_TRANSFER" : kind === "hold_split" ? "AUTO_EXEC_HOLD_SPLIT" : "AUTO_EXEC_LABEL"; await writeAudit({ actor: "system", action, mailId, payload: { mode: result.mode, ok: result.ok, request: result.request, response: result.response, error: result.error, }, }); if (result.ok) { assertTransition("AUTO_EXECUTING", "SUCCESS"); await prisma.mailMessage.update({ where: { id: mailId }, data: { status: "SUCCESS", lastError: result.mode === "mock" ? "AUTO_EXEC_MOCK_OK" : null, version: { increment: 1 }, }, }); if (header?.F_ContainerNo) { await prisma.containerImport.create({ data: { mailId, containerNo: header.F_ContainerNo, externalId: result.externalId || null, requestBody: JSON.stringify(result.request), responseBody: JSON.stringify(result.response), status: "SUCCESS", shipmentsHash: "auto-exec", importKind: "forecast", }, }).catch(() => undefined); } logger.info({ mailId: String(mailId), kind, mode: result.mode }, "auto_exec.ok"); return { ok: true, status: "SUCCESS" }; } assertTransition("AUTO_EXECUTING", "FAILED"); await prisma.mailMessage.update({ where: { id: mailId }, data: { status: "FAILED", lastError: (result.error || "AUTO_EXEC_FAILED").slice(0, 1000), version: { increment: 1 }, }, }); return { ok: false, status: "FAILED", error: result.error }; } export async function processDueAutoExec(limit = 10): Promise<{ processed: number; ok: number; fail: number; }> { const rows = await prisma.mailMessage.findMany({ where: { status: "AUTO_EXECUTING" }, orderBy: { updatedAt: "asc" }, take: limit, select: { id: true }, }); let ok = 0; let fail = 0; for (const r of rows) { const res = await runAutoExec(r.id); if (res.ok) ok += 1; else fail += 1; } return { processed: rows.length, ok, fail }; }