mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Pre-merge fixes for the CloakBrowser/Playwright extraction into the optional @omniroute/browser-pool workspace package: - Add the @omniroute/browser-pool path mapping to the root tsconfig.json (tsconfig.typecheck-core.json extends the root, not open-sse/tsconfig.json, so typecheck:core was failing with 3x TS2307 in browserPool.ts). - tryBackedChat() in browserBackedChat.ts silently returned the stale httpResult (the unsolved challenge/403 body) when the optional browser-pool package was absent, instead of surfacing an error. Now throws a descriptive error so callers (claude-web, duckduckgo-web) and upstream fallback handling can react correctly instead of treating an unsolved challenge as a definitive response. Restored resolveBrowserContextProxy (dropped during the browserPool.ts merge conflict resolution against origin/release/v3.8.49) which tests/unit/browserPool-proxy.test.ts already depends on. - Fix acquireFreshGrokClearance stub signature in grokClearance.ts to match the real package impl and the grok-web.ts caller: (signal?: AbortSignal | null) => Promise<string | null>, not (prompt: string) => Promise<PooledContext | null>. - Add tests/unit/browserBackedChat-optional-package-absent.test.ts covering the previously-silent "package absent + challenge response" regression, plus a __setBrowserPoolModOverrideForTesting() hook to simulate that path deterministically. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
248 lines
8.7 KiB
TypeScript
248 lines
8.7 KiB
TypeScript
/**
|
|
* browserPool.ts — core stub that delegates to @omniroute/browser-pool.
|
|
*
|
|
* Copyright 2025 OmniRoute. All rights reserved.
|
|
*
|
|
* Lightweight functions (resolvePlaywrightProxy, getBrowserPoolStatus)
|
|
* remain inline to avoid forcing an async package load for trivial lookups.
|
|
* All heavy browser-interaction functions are forwarded to the optional
|
|
* @omniroute/browser-pool package via dynamic import.
|
|
*/
|
|
|
|
import { Buffer } from "node:buffer";
|
|
|
|
type Browser = import("playwright").Browser;
|
|
type BrowserContext = import("playwright").BrowserContext;
|
|
type Page = import("playwright").Page;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types — re-exported so callers never import from @omniroute/browser-pool
|
|
// directly. When the package is installed these would come from there, but by
|
|
// keeping local definitions both the stub and the package agree on shape.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface BrowserPoolContextOptions {
|
|
cookieDomain: string;
|
|
cookieString?: string | null;
|
|
warmupUrl?: string | null;
|
|
userAgent?: string;
|
|
locale?: string;
|
|
timezone?: string;
|
|
preferCloakbrowser?: boolean;
|
|
proxyProviderKey?: string;
|
|
}
|
|
|
|
export interface PooledContext {
|
|
id: string;
|
|
context: BrowserContext;
|
|
warmupPage: Page | null;
|
|
lastUsed: number;
|
|
isStealth: boolean;
|
|
}
|
|
|
|
export interface BrowserPoolMetrics {
|
|
browserLaunches: number;
|
|
browserLaunchFailures: number;
|
|
contextsCreated: number;
|
|
contextsReused: number;
|
|
contextsEvicted: number;
|
|
contextsReleased: number;
|
|
contextCreateFailures: number;
|
|
shutdowns: number;
|
|
lastShutdownReason: string | null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Proxy resolver types — local to resolvePlaywrightProxy
|
|
// ---------------------------------------------------------------------------
|
|
interface ProxyRecord {
|
|
type?: string;
|
|
host: string;
|
|
port: number;
|
|
username?: string | null;
|
|
password?: string | null;
|
|
}
|
|
|
|
interface ResolvePlaywrightProxyDeps {
|
|
resolveProxy?: (providerId: string) => Promise<ProxyRecord | null | undefined>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dynamic import rationale: we keep the import URL as a computed string so
|
|
// that bundlers (Turbopack / webpack / esbuild) do NOT statically resolve
|
|
// it during route compilation. If a literal dynamic import of the module
|
|
// appeared here, Turbopack resolve it during route compilation and error out.
|
|
// By shunting through a join pattern the bundler never sees the literal
|
|
// dependency path and leaves the import as a true runtime dynamic import.
|
|
// return ["cloak", "browser"].join("");
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Resolve the cloakbrowser module name without a literal string —
|
|
* prevents Turbopack from resolving it at build time.
|
|
*/
|
|
function resolveCloakBrowserModule(): string {
|
|
return ["cloak", "browser"].join("");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dynamic import — eager; load starts on module import.
|
|
// setProxyResolver is called once the module resolves, so any subsequent
|
|
// acquireBrowserContext call will have the resolver wired in.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let modPromise: Promise<typeof import("@omniroute/browser-pool")> | null = null;
|
|
|
|
function getMod(): Promise<typeof import("@omniroute/browser-pool")> {
|
|
if (!modPromise) {
|
|
modPromise = import("@omniroute/browser-pool").then((mod) => {
|
|
mod.setProxyResolver(resolvePlaywrightProxy);
|
|
return mod;
|
|
});
|
|
}
|
|
return modPromise;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolvePlaywrightProxy — INLINE (no Playwright dependency at runtime)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve a Playwright-compatible proxy config for a given provider key.
|
|
* Looks up the proxy from the database via resolveProxyForProvider, then
|
|
* builds the { server, username?, password? } object that Playwright expects.
|
|
*
|
|
* May be injected into @omniroute/browser-pool via setProxyResolver so
|
|
* that acquireBrowserContext uses the same proxy resolution as the rest
|
|
* of the application.
|
|
*/
|
|
export async function resolvePlaywrightProxy(
|
|
providerKey: string,
|
|
deps?: ResolvePlaywrightProxyDeps
|
|
): Promise<import("playwright").LaunchOptions["proxy"] | undefined> {
|
|
try {
|
|
const resolver =
|
|
deps?.resolveProxy ??
|
|
(async (id: string) => {
|
|
const { resolveProxyForProvider } = await import("../../src/lib/db/proxies");
|
|
return resolveProxyForProvider(id);
|
|
});
|
|
const p = await resolver(providerKey);
|
|
if (!p?.host) return undefined;
|
|
const scheme = p.type === "socks5" ? "socks5" : "http";
|
|
// Build explicitly instead of a conditional object spread: the spread form
|
|
// widens username/password to `{}` under the LaunchOptions["proxy"] type,
|
|
// tripping typecheck once browserPool.ts is pulled into typecheck-core scope.
|
|
const proxy: NonNullable<import("playwright").LaunchOptions["proxy"]> = {
|
|
server: `${scheme}://${p.host}:${p.port}`,
|
|
};
|
|
if (p.username) {
|
|
proxy.username = String(p.username);
|
|
proxy.password = p.password == null ? "" : String(p.password);
|
|
}
|
|
return proxy;
|
|
} catch (err) {
|
|
console.warn("[BrowserPool] Failed to resolve proxy from DB:", err);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the proxy for a browser-pool context key. Scoped context keys
|
|
* (e.g. "claude-web:account-scope") carry a stable `proxyProviderKey` so the
|
|
* proxy lookup always targets the underlying provider, not the scoped key.
|
|
* Kept inline alongside resolvePlaywrightProxy — trivial wrapper, no
|
|
* playwright/browser-pool dependency.
|
|
*/
|
|
export async function resolveBrowserContextProxy(
|
|
contextKey: string,
|
|
options: Pick<BrowserPoolContextOptions, "proxyProviderKey">,
|
|
deps?: ResolvePlaywrightProxyDeps
|
|
): Promise<import("playwright").LaunchOptions["proxy"] | undefined> {
|
|
return resolvePlaywrightProxy(options.proxyProviderKey ?? contextKey, deps);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getBrowserPoolStatus — INLINE (returns disabled status)
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Return the current status of the browser pool. In the core stub, the pool
|
|
* is always reported as disabled — the real pool lives inside the optional
|
|
* @omniroute/browser-pool package.
|
|
*/
|
|
export function getBrowserPoolStatus(): {
|
|
enabled: boolean;
|
|
contexts: number;
|
|
browserRunning: boolean;
|
|
stealthAvailable: boolean;
|
|
lastActivityAgoMs: number;
|
|
} {
|
|
return {
|
|
enabled: false,
|
|
contexts: 0,
|
|
browserRunning: false,
|
|
stealthAvailable: false,
|
|
lastActivityAgoMs: -1,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Delegated functions — forwarded to @omniroute/browser-pool
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function acquireBrowserContext(
|
|
key: string,
|
|
options: BrowserPoolContextOptions
|
|
): Promise<PooledContext> {
|
|
const mod = await getMod();
|
|
return mod.acquireBrowserContext(key, options);
|
|
}
|
|
/**
|
|
* #3368 PR7 — browser-pool observability. Returns disabled status and empty
|
|
* metrics. Callers get a snapshot; the real pool state lives inside the
|
|
* optional @omniroute/browser-pool package.
|
|
*/
|
|
export function getBrowserPoolMetrics(): {
|
|
status: ReturnType<typeof getBrowserPoolStatus>;
|
|
metrics: BrowserPoolMetrics;
|
|
} {
|
|
return { status: getBrowserPoolStatus(), metrics: createEmptyMetrics() };
|
|
}
|
|
|
|
function createEmptyMetrics(): BrowserPoolMetrics {
|
|
return {
|
|
browserLaunches: 0,
|
|
browserLaunchFailures: 0,
|
|
contextsCreated: 0,
|
|
contextsReused: 0,
|
|
contextsEvicted: 0,
|
|
contextsReleased: 0,
|
|
contextCreateFailures: 0,
|
|
shutdowns: 0,
|
|
lastShutdownReason: null,
|
|
};
|
|
}
|
|
|
|
export async function releaseBrowserContext(key: string): Promise<void> {
|
|
const mod = await getMod();
|
|
return mod.releaseBrowserContext(key);
|
|
}
|
|
|
|
export async function shutdownPool(reason: string): Promise<void> {
|
|
const mod = await getMod();
|
|
return mod.shutdownPool(reason);
|
|
}
|
|
|
|
|
|
/** Test-only: reset cumulative metrics so assertions start from a clean slate. */
|
|
export async function __resetBrowserPoolMetricsForTest(): Promise<void> {
|
|
const mod = await getMod();
|
|
return mod.__resetBrowserPoolMetricsForTest();
|
|
}
|
|
|
|
export async function readPageResponseBody(
|
|
response: import("playwright").Response
|
|
): Promise<{ status: number; headers: Record<string, string>; body: Buffer }> {
|
|
const mod = await getMod();
|
|
return mod.readPageResponseBody(response);
|
|
}
|