mirror of
https://github.com/steel-dev/cli.git
synced 2026-09-14 20:47:34 +08:00
259 lines
6.0 KiB
TypeScript
259 lines
6.0 KiB
TypeScript
import {resolveBrowserAuth} from '../auth.js';
|
|
import {BrowserAdapterError} from '../errors.js';
|
|
import {DEFAULT_API_PATH} from './constants.js';
|
|
import type {
|
|
BrowserSessionMode,
|
|
BrowserSessionSummary,
|
|
StartBrowserSessionOptions,
|
|
UnknownRecord,
|
|
} from './types.js';
|
|
|
|
const CLOSED_SESSION_STATUSES = new Set([
|
|
'closed',
|
|
'completed',
|
|
'ended',
|
|
'failed',
|
|
'released',
|
|
'stopped',
|
|
'terminated',
|
|
]);
|
|
|
|
type SessionRestartOptions = Pick<
|
|
StartBrowserSessionOptions,
|
|
| 'local'
|
|
| 'apiUrl'
|
|
| 'sessionName'
|
|
| 'timeoutMs'
|
|
| 'headless'
|
|
| 'region'
|
|
| 'solveCaptcha'
|
|
> & {apiUrl: string | null; sessionName: string | null};
|
|
|
|
function getConnectUrl(session: UnknownRecord): string | null {
|
|
const candidateKeys = [
|
|
'websocketUrl',
|
|
'wsUrl',
|
|
'connectUrl',
|
|
'cdpUrl',
|
|
'browserWSEndpoint',
|
|
'wsEndpoint',
|
|
];
|
|
|
|
for (const key of candidateKeys) {
|
|
const value = session[key];
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getViewerUrl(
|
|
session: UnknownRecord,
|
|
mode: BrowserSessionMode,
|
|
sessionId: string,
|
|
): string | null {
|
|
const candidateKeys = ['sessionViewerUrl', 'viewerUrl', 'liveViewUrl'];
|
|
|
|
for (const key of candidateKeys) {
|
|
const value = session[key];
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
}
|
|
|
|
if (mode === 'cloud') {
|
|
return `https://app.steel.dev/sessions/${sessionId}`;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getSessionStatus(session: UnknownRecord): string | null {
|
|
const status = session['status'];
|
|
if (typeof status === 'string' && status.trim()) {
|
|
return status.trim();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function getSessionId(session: UnknownRecord): string | null {
|
|
const rawId = session['id'] ?? session['sessionId'];
|
|
if (typeof rawId === 'string' && rawId.trim()) {
|
|
return rawId.trim();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isSessionLive(session: UnknownRecord): boolean {
|
|
const liveFlags = ['isLive', 'live', 'active'];
|
|
|
|
for (const key of liveFlags) {
|
|
const value = session[key];
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
const endedAt = session['endedAt'];
|
|
if (typeof endedAt === 'string' && endedAt.trim()) {
|
|
return false;
|
|
}
|
|
|
|
const status = getSessionStatus(session);
|
|
if (status) {
|
|
return !CLOSED_SESSION_STATUSES.has(status.toLowerCase());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function toSessionSummary(
|
|
session: UnknownRecord,
|
|
mode: BrowserSessionMode,
|
|
name: string | null,
|
|
environment: NodeJS.ProcessEnv,
|
|
): BrowserSessionSummary {
|
|
const sessionId = getSessionId(session);
|
|
if (!sessionId) {
|
|
throw new BrowserAdapterError(
|
|
'API_ERROR',
|
|
'Session response did not contain an id.',
|
|
);
|
|
}
|
|
|
|
const auth = resolveBrowserAuth(environment);
|
|
let resolvedConnectUrl =
|
|
getConnectUrl(session) ||
|
|
(mode === 'cloud' && auth.apiKey
|
|
? `wss://connect.steel.dev?apiKey=${auth.apiKey}&sessionId=${sessionId}`
|
|
: null);
|
|
|
|
if (resolvedConnectUrl && mode === 'cloud' && auth.apiKey) {
|
|
const parsed = new URL(resolvedConnectUrl);
|
|
if (!parsed.searchParams.has('apiKey')) {
|
|
parsed.searchParams.set('apiKey', auth.apiKey);
|
|
resolvedConnectUrl = parsed.toString();
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: sessionId,
|
|
mode,
|
|
name,
|
|
live: isSessionLive(session),
|
|
status: getSessionStatus(session),
|
|
connectUrl: resolvedConnectUrl,
|
|
viewerUrl: getViewerUrl(session, mode, sessionId),
|
|
raw: session,
|
|
};
|
|
}
|
|
|
|
function getApiErrorStatusCode(error: BrowserAdapterError): number | null {
|
|
if (error.cause && typeof error.cause === 'object') {
|
|
const status = (error.cause as UnknownRecord)['status'];
|
|
if (typeof status === 'number' && Number.isInteger(status)) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
const statusCodeMatch = error.message.match(/\((\d{3})\)/);
|
|
if (!statusCodeMatch?.[1]) {
|
|
return null;
|
|
}
|
|
|
|
const parsedStatusCode = Number.parseInt(statusCodeMatch[1], 10);
|
|
return Number.isInteger(parsedStatusCode) ? parsedStatusCode : null;
|
|
}
|
|
|
|
export function isNotFoundApiError(error: unknown): boolean {
|
|
if (!(error instanceof BrowserAdapterError) || error.code !== 'API_ERROR') {
|
|
return false;
|
|
}
|
|
|
|
const statusCode = getApiErrorStatusCode(error);
|
|
return statusCode === 404 || statusCode === 410;
|
|
}
|
|
|
|
function isCloudApiUrl(
|
|
apiUrl: string,
|
|
environment: NodeJS.ProcessEnv,
|
|
): boolean {
|
|
try {
|
|
const explicitUrl = new URL(apiUrl);
|
|
const configuredCloudApiUrl = new URL(
|
|
environment.STEEL_API_URL?.trim() || DEFAULT_API_PATH,
|
|
);
|
|
return explicitUrl.origin === configuredCloudApiUrl.origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function resolveSessionMode(
|
|
localOption: boolean | undefined,
|
|
apiUrl: string | null,
|
|
activeSessionMode: BrowserSessionMode | null,
|
|
environment: NodeJS.ProcessEnv,
|
|
): BrowserSessionMode {
|
|
if (localOption) {
|
|
return 'local';
|
|
}
|
|
|
|
if (apiUrl) {
|
|
return isCloudApiUrl(apiUrl, environment) ? 'cloud' : 'local';
|
|
}
|
|
|
|
return activeSessionMode === 'local' ? 'local' : 'cloud';
|
|
}
|
|
|
|
function formatCliArgument(value: string): string {
|
|
return /^[a-zA-Z0-9_./:=-]+$/.test(value) ? value : JSON.stringify(value);
|
|
}
|
|
|
|
function buildSessionRestartCommand(options: SessionRestartOptions): string {
|
|
const args = ['steel browser start'];
|
|
|
|
if (options.apiUrl) {
|
|
args.push(`--api-url ${formatCliArgument(options.apiUrl)}`);
|
|
} else if (options.local) {
|
|
args.push('--local');
|
|
}
|
|
|
|
if (options.sessionName) {
|
|
args.push(`--session ${formatCliArgument(options.sessionName)}`);
|
|
}
|
|
|
|
if (typeof options.timeoutMs === 'number') {
|
|
args.push(`--session-timeout ${String(options.timeoutMs)}`);
|
|
}
|
|
|
|
if (options.headless) {
|
|
args.push('--session-headless');
|
|
}
|
|
|
|
if (options.region) {
|
|
args.push(`--session-region ${formatCliArgument(options.region)}`);
|
|
}
|
|
|
|
if (options.solveCaptcha) {
|
|
args.push('--session-solve-captcha');
|
|
}
|
|
|
|
return args.join(' ');
|
|
}
|
|
|
|
export function buildDeadSessionMessage(
|
|
sessionId: string,
|
|
options: SessionRestartOptions,
|
|
): string {
|
|
const sessionLabel = options.sessionName
|
|
? `Mapped session "${options.sessionName}" (${sessionId})`
|
|
: `Active session ${sessionId}`;
|
|
const restartCommand = buildSessionRestartCommand(options);
|
|
return `${sessionLabel} is no longer live. Run \`${restartCommand}\` to create a new session.`;
|
|
}
|