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 { try { await ensureRedisReady(10, 800); } catch { ensureNativeInfraIfNeeded(); resetRedisClient(); await ensureRedisReady(15, 1_000); } } export async function saveSessionConfirmResult( jobId: string, payload: StoredConfirmResult, ): Promise { 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 { 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 }, ); }