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.
144 lines
4.1 KiB
144 lines
4.1 KiB
import { spawn } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { ADDRESS_CANDIDATES_TIMEOUT_MS } from "@/lib/constants/rpa";
|
|
import { readInfraEnvOverrides } from "@/lib/dev-env";
|
|
import type {
|
|
AddressLookupInput,
|
|
MothershipCandidatesResult,
|
|
} from "@/modules/address/types";
|
|
import { RpaError, type RpaErrorCode } from "@/modules/rpa/errors";
|
|
|
|
const CHILD_STARTUP_BUFFER_MS = 15_000;
|
|
|
|
type ChildSuccess = { ok: true; data: MothershipCandidatesResult };
|
|
type ChildFailure = {
|
|
ok: false;
|
|
code: RpaErrorCode | "INTERNAL_ERROR";
|
|
message: string;
|
|
retryable?: boolean;
|
|
};
|
|
|
|
function resolveTsxCommand(): { command: string; args: string[]; shell: boolean } {
|
|
const root = process.cwd();
|
|
const script = path.join(root, "scripts/rpa/fetch-address-candidates.ts");
|
|
const tsxCmd = path.join(root, "node_modules", ".bin", "tsx.cmd");
|
|
const tsxBin = path.join(root, "node_modules", ".bin", "tsx");
|
|
|
|
if (process.platform === "win32" && fs.existsSync(tsxCmd)) {
|
|
return { command: tsxCmd, args: [script], shell: true };
|
|
}
|
|
if (fs.existsSync(tsxBin)) {
|
|
return { command: tsxBin, args: [script], shell: false };
|
|
}
|
|
return {
|
|
command: process.platform === "win32" ? "npx.cmd" : "npx",
|
|
args: ["tsx", script],
|
|
shell: process.platform === "win32",
|
|
};
|
|
}
|
|
|
|
function parseChildPayload(stdout: string): ChildSuccess | ChildFailure | null {
|
|
const lines = stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
try {
|
|
const parsed = JSON.parse(lines[i]!) as ChildSuccess | ChildFailure;
|
|
if (typeof parsed === "object" && parsed !== null && "ok" in parsed) {
|
|
return parsed;
|
|
}
|
|
} catch {
|
|
/* try previous line */
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 在独立 Node 子进程运行 RPA 地址候选,规避 Next.js 打包 Playwright */
|
|
export async function spawnFetchMothershipAddressCandidates(input: {
|
|
pickup: AddressLookupInput;
|
|
delivery: AddressLookupInput;
|
|
}): Promise<MothershipCandidatesResult> {
|
|
const { command, args, shell } = resolveTsxCommand();
|
|
const timeoutMs = ADDRESS_CANDIDATES_TIMEOUT_MS + CHILD_STARTUP_BUFFER_MS;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, ...readInfraEnvOverrides() },
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
shell,
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let settled = false;
|
|
|
|
const finish = (fn: () => void) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
fn();
|
|
};
|
|
|
|
const timer = setTimeout(() => {
|
|
child.kill("SIGTERM");
|
|
finish(() => {
|
|
reject(
|
|
new RpaError(
|
|
"PAGE_LOAD_TIMEOUT",
|
|
"MotherShip 地址联想超时,请检查网络或稍后重试",
|
|
{ retryable: true },
|
|
),
|
|
);
|
|
});
|
|
}, timeoutMs);
|
|
|
|
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
|
|
child.on("error", (error) => {
|
|
finish(() => reject(error));
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
const payload = parseChildPayload(stdout);
|
|
if (payload?.ok) {
|
|
finish(() => resolve(payload.data));
|
|
return;
|
|
}
|
|
|
|
if (payload && !payload.ok) {
|
|
finish(() => {
|
|
reject(
|
|
new RpaError(payload.code as RpaErrorCode, payload.message, {
|
|
retryable: payload.retryable ?? false,
|
|
}),
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
|
|
const detail = stderr.trim() || stdout.trim() || `exit code ${code ?? "unknown"}`;
|
|
finish(() => {
|
|
if (code === 0) {
|
|
reject(new Error(`地址候选子进程输出无效:${detail.slice(0, 240)}`));
|
|
return;
|
|
}
|
|
reject(new Error(`地址候选 RPA 子进程失败:${detail.slice(0, 240)}`));
|
|
});
|
|
});
|
|
|
|
child.stdin.write(JSON.stringify(input));
|
|
child.stdin.end();
|
|
});
|
|
}
|