|
|
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
|
|
|
import { getRpaBrowserLocale } from "@/lib/rpa/env";
|
|
|
import { RpaError } from "@/modules/rpa/errors";
|
|
|
import {
|
|
|
getStorageStatePath,
|
|
|
persistContext,
|
|
|
storageStateExists,
|
|
|
} from "@/lib/rpa/storage-state";
|
|
|
import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter";
|
|
|
|
|
|
const DEFAULT_USER_AGENT =
|
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36";
|
|
|
|
|
|
let bootstrapPromise: Promise<void> | null = null;
|
|
|
|
|
|
/** 缺 .rpa/mothership-storage.json 时是否 headless 自动访问 MotherShip(默认 true) */
|
|
|
export function isAutoBootstrapStorageEnabled(): boolean {
|
|
|
if (process.env.RPA_MOCK_MODE === "true") {
|
|
|
return false;
|
|
|
}
|
|
|
return process.env.RPA_AUTO_BOOTSTRAP_STORAGE?.trim().toLowerCase() !== "false";
|
|
|
}
|
|
|
|
|
|
function missingStorageMessage(): string {
|
|
|
const path = getStorageStatePath();
|
|
|
if (isAutoBootstrapStorageEnabled()) {
|
|
|
return `缺少 MotherShip 会话文件 ${path},且自动获取失败;请检查 VPN/网络与 MOTHERSHIP_QUOTE_URLS,或运行 npm run record:mothership`;
|
|
|
}
|
|
|
return `缺少 MotherShip 会话文件 ${path};请运行 npm run record:mothership 录制 cookies,或设 RPA_AUTO_BOOTSTRAP_STORAGE=true(默认开启)`;
|
|
|
}
|
|
|
|
|
|
export function assertStorageStatePresent(): void {
|
|
|
if (!storageStateExists()) {
|
|
|
throw new Error(missingStorageMessage());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function runStorageBootstrap(): Promise<void> {
|
|
|
const timeoutMs = Number(process.env.RPA_STORAGE_BOOTSTRAP_MS ?? 90_000);
|
|
|
await Promise.race([
|
|
|
runStorageBootstrapInner(),
|
|
|
new Promise<never>((_, reject) =>
|
|
|
setTimeout(
|
|
|
() =>
|
|
|
reject(
|
|
|
new RpaError(
|
|
|
"QUOTE_ENTRY_UNAVAILABLE",
|
|
|
`自动获取 MotherShip 会话超时(${Math.round(timeoutMs / 1000)}s);请检查 VPN/网络与 MOTHERSHIP_QUOTE_URLS,或运行 npm run record:mothership`,
|
|
|
{ retryable: true },
|
|
|
),
|
|
|
),
|
|
|
timeoutMs,
|
|
|
),
|
|
|
),
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
async function runStorageBootstrapInner(): Promise<void> {
|
|
|
if (storageStateExists()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const statePath = getStorageStatePath();
|
|
|
console.log(`[rpa] 缺少 storageState,自动访问 MotherShip 获取会话 → ${statePath}`);
|
|
|
|
|
|
const headless =
|
|
|
process.env.RPA_HEADED === "true"
|
|
|
? false
|
|
|
: process.env.RPA_HEADLESS !== "false";
|
|
|
|
|
|
const browser = await launchRpaBrowser({ headless });
|
|
|
try {
|
|
|
const locale = getRpaBrowserLocale();
|
|
|
const context = await browser.newContext({
|
|
|
viewport: { width: 1280, height: 900 },
|
|
|
locale,
|
|
|
extraHTTPHeaders: { "Accept-Language": `${locale},en;q=0.9` },
|
|
|
userAgent: DEFAULT_USER_AGENT,
|
|
|
});
|
|
|
const page = await context.newPage();
|
|
|
try {
|
|
|
const { activeUrl } = await quotePageAdapter.resolveQuoteEntry(page);
|
|
|
await quotePageAdapter.preCheck(page);
|
|
|
await persistContext(context);
|
|
|
console.log(
|
|
|
`[rpa] MotherShip 会话已自动写入 ${statePath}(入口 ${activeUrl})`,
|
|
|
);
|
|
|
} finally {
|
|
|
await page.close().catch(() => undefined);
|
|
|
await context.close().catch(() => undefined);
|
|
|
}
|
|
|
} finally {
|
|
|
await browser.close().catch(() => undefined);
|
|
|
}
|
|
|
|
|
|
if (!storageStateExists()) {
|
|
|
throw new RpaError(
|
|
|
"QUOTE_ENTRY_UNAVAILABLE",
|
|
|
missingStorageMessage(),
|
|
|
{ retryable: true },
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 确保 MotherShip Playwright storageState 存在(缺失时自动 bootstrap,并发安全) */
|
|
|
export async function ensureMothershipStorageState(): Promise<void> {
|
|
|
if (storageStateExists()) {
|
|
|
return;
|
|
|
}
|
|
|
if (!isAutoBootstrapStorageEnabled()) {
|
|
|
throw new Error(missingStorageMessage());
|
|
|
}
|
|
|
|
|
|
if (!bootstrapPromise) {
|
|
|
bootstrapPromise = runStorageBootstrap()
|
|
|
.catch((error) => {
|
|
|
throw error;
|
|
|
})
|
|
|
.finally(() => {
|
|
|
bootstrapPromise = null;
|
|
|
});
|
|
|
}
|
|
|
await bootstrapPromise;
|
|
|
}
|