|
|
import { randomUUID } from "node:crypto";
|
|
|
import {
|
|
|
SESSION_CONFIRM_POLL_MS,
|
|
|
SESSION_CONFIRM_RESULT_PREFIX,
|
|
|
SESSION_CONFIRM_RESULT_TTL_SECONDS,
|
|
|
SESSION_CONFIRM_TIMEOUT_MS,
|
|
|
} from "@/lib/constants/session-confirm-queue";
|
|
|
import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra";
|
|
|
import {
|
|
|
ensureRedisReady,
|
|
|
getRedis,
|
|
|
resetRedisClient,
|
|
|
} from "@/lib/redis";
|
|
|
import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health";
|
|
|
import type { MothershipAddressCandidate } from "@/modules/address/types";
|
|
|
import type { SessionConfirmJobData } from "@/modules/address/session-confirm-queue-types";
|
|
|
import { RpaError } from "@/modules/rpa/errors";
|
|
|
import { addSessionConfirmJob } from "@/workers/rpa/queue";
|
|
|
|
|
|
type StoredConfirmResult =
|
|
|
| { ok: true }
|
|
|
| { ok: false; code: string; message: string; retryable?: boolean };
|
|
|
|
|
|
function resultKey(jobId: string): string {
|
|
|
return `${SESSION_CONFIRM_RESULT_PREFIX}${jobId}`;
|
|
|
}
|
|
|
|
|
|
async function ensureConfirmRedisReady(): Promise<void> {
|
|
|
try {
|
|
|
await ensureRedisReady(10, 800);
|
|
|
} catch {
|
|
|
ensureNativeInfraIfNeeded();
|
|
|
resetRedisClient();
|
|
|
await ensureRedisReady(15, 1_000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export async function saveSessionConfirmResult(
|
|
|
jobId: string,
|
|
|
payload: StoredConfirmResult,
|
|
|
): Promise<void> {
|
|
|
await ensureConfirmRedisReady();
|
|
|
await getRedis().setex(
|
|
|
resultKey(jobId),
|
|
|
SESSION_CONFIRM_RESULT_TTL_SECONDS,
|
|
|
JSON.stringify(payload),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/** 用户确认 MotherShip 地址后,在同驻留页锁定双地址 */
|
|
|
export async function requestSessionAddressConfirm(input: {
|
|
|
sessionId: string;
|
|
|
pickup: MothershipAddressCandidate;
|
|
|
delivery: MothershipAddressCandidate;
|
|
|
}): Promise<void> {
|
|
|
if (process.env.RPA_QUEUE_ENABLED === "false") {
|
|
|
throw new RpaError(
|
|
|
"PAGE_LOAD_TIMEOUT",
|
|
|
"RPA 队列已禁用,无法在同页确认地址",
|
|
|
{ retryable: false },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const workerOk = await isAnyRpaWorkerHealthy().catch(() => false);
|
|
|
if (!workerOk) {
|
|
|
throw new RpaError(
|
|
|
"PAGE_LOAD_TIMEOUT",
|
|
|
"RPA Worker 未运行,无法复用驻留页(请 npm run worker:rpa)",
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
|
|
|
await ensureConfirmRedisReady();
|
|
|
const jobId = randomUUID();
|
|
|
const payload: SessionConfirmJobData = {
|
|
|
jobId,
|
|
|
sessionId: input.sessionId,
|
|
|
pickup: input.pickup,
|
|
|
delivery: input.delivery,
|
|
|
};
|
|
|
await addSessionConfirmJob(payload);
|
|
|
|
|
|
const deadline = Date.now() + SESSION_CONFIRM_TIMEOUT_MS;
|
|
|
while (Date.now() < deadline) {
|
|
|
const raw = await getRedis().get(resultKey(jobId));
|
|
|
if (raw) {
|
|
|
const stored = JSON.parse(raw) as StoredConfirmResult;
|
|
|
if (stored.ok) {
|
|
|
return;
|
|
|
}
|
|
|
throw new RpaError(
|
|
|
stored.code as "PAGE_LOAD_TIMEOUT",
|
|
|
stored.message,
|
|
|
{ retryable: stored.retryable ?? false },
|
|
|
);
|
|
|
}
|
|
|
await new Promise((resolve) => setTimeout(resolve, SESSION_CONFIRM_POLL_MS));
|
|
|
}
|
|
|
|
|
|
throw new RpaError(
|
|
|
"PAGE_LOAD_TIMEOUT",
|
|
|
"地址确认超时,请重新获取候选",
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|