Files

180 lines
6.5 KiB
JavaScript

import { constants } from 'node:fs';
import { access, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { join, win32 } from 'node:path';
import process from 'node:process';
import puppeteer from 'puppeteer-core';
const SENTINEL_FRAME_URL = 'https://sentinel.openai.com/backend-api/sentinel/frame.html?sv=20260219f9f6';
const DEFAULT_TIMEOUT_MS = 20000;
let browserRuntime = null;
let browserKey = '';
let browserQueue = Promise.resolve();
function browserExecutableCandidates({ env, platform, homeDirectory }) {
const candidates = [env.SENTINEL_BROWSER_PATH, env.CHROME_PATH];
if (platform === 'darwin') {
for (const applicationsDirectory of ['/Applications', join(homeDirectory, 'Applications')]) {
candidates.push(
join(applicationsDirectory, 'Google Chrome.app/Contents/MacOS/Google Chrome'),
join(applicationsDirectory, 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge'),
join(applicationsDirectory, 'Chromium.app/Contents/MacOS/Chromium'),
);
}
} else if (platform === 'win32') {
for (const programFiles of [env.PROGRAMFILES || 'C:/Program Files', env['PROGRAMFILES(X86)'] || 'C:/Program Files (x86)']) {
candidates.push(
win32.join(programFiles, 'Google/Chrome/Application/chrome.exe'),
win32.join(programFiles, 'Microsoft/Edge/Application/msedge.exe'),
);
}
if (env.LOCALAPPDATA) candidates.push(win32.join(env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe'));
} else if (platform === 'linux') {
candidates.push(
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/opt/google/chrome/chrome',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
'/usr/bin/microsoft-edge',
'/usr/bin/microsoft-edge-stable',
);
}
return [...new Set(candidates.filter(Boolean))];
}
async function canExecuteBrowser(candidate) {
if (!(await stat(candidate)).isFile()) return false;
await access(candidate, constants.X_OK);
return true;
}
export async function resolveSentinelExecutablePath({
env = process.env,
platform = process.platform,
homeDirectory = homedir(),
canExecute = canExecuteBrowser,
} = {}) {
for (const candidate of browserExecutableCandidates({ env, platform, homeDirectory })) {
try {
if (await canExecute(candidate)) return candidate;
} catch {
// Continue until a locally installed Chromium browser is found.
}
}
throw new Error('sentinel_browser_unavailable');
}
function normalizeProxy(proxy) {
if (!proxy?.server) return null;
let parsed;
try {
parsed = new URL(String(proxy.server));
} catch {
throw new Error('sentinel_proxy_invalid');
}
const username = String(proxy.username || (parsed.username ? decodeURIComponent(parsed.username) : ''));
const password = String(proxy.password || (parsed.password ? decodeURIComponent(parsed.password) : ''));
if (parsed.protocol.startsWith('socks') && (username || password)) {
throw new Error('sentinel_socks_auth_unsupported');
}
const port = parsed.port || (parsed.protocol.startsWith('socks') ? '1080' : '8080');
// Chromium's --proxy-server flag expects the socks5 spelling; keep the
// socks5h alias at the Node request boundary where it is meaningful.
const browserProtocol = parsed.protocol === 'socks5h:' ? 'socks5:' : parsed.protocol;
return {
server: `${browserProtocol}//${parsed.hostname}:${port}`,
username,
password,
};
}
async function closeBrowser() {
const runtime = browserRuntime;
browserRuntime = null;
browserKey = '';
await runtime?.close().catch(() => undefined);
}
async function browserFor(proxy) {
const executablePath = await resolveSentinelExecutablePath();
const key = JSON.stringify({ executablePath, proxy: proxy?.server || '' });
if (browserRuntime?.connected && browserKey === key) return browserRuntime;
await closeBrowser();
const args = [
'--no-first-run',
'--no-default-browser-check',
'--disable-background-networking',
'--disable-component-update',
'--disable-sync',
'--disable-gpu',
];
if (proxy?.server) args.push(`--proxy-server=${proxy.server}`);
const browser = await puppeteer.launch({ executablePath, headless: true, args });
browserRuntime = browser;
browserKey = key;
browser.once('disconnected', () => {
if (browserRuntime === browser) {
browserRuntime = null;
browserKey = '';
}
});
return browser;
}
async function calculateToken({ flow, deviceId, userAgent, proxy, timeoutMs }) {
const normalizedProxy = normalizeProxy(proxy);
const browser = await browserFor(normalizedProxy);
const context = await browser.createBrowserContext();
const page = await context.newPage();
try {
if (normalizedProxy?.username || normalizedProxy?.password) {
await page.authenticate({ username: normalizedProxy.username, password: normalizedProxy.password });
}
await page.setUserAgent(userAgent);
await page.setViewport({ width: 1365, height: 768, deviceScaleFactor: 1 });
await page.setExtraHTTPHeaders({ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8' });
await page.setCookie({
name: 'oai-did',
value: deviceId,
domain: 'sentinel.openai.com',
path: '/',
secure: true,
httpOnly: false,
sameSite: 'None',
});
await page.goto(SENTINEL_FRAME_URL, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
try {
await page.waitForFunction(() => typeof window.SentinelSDK?.token === 'function', { timeout: timeoutMs });
} catch {
await page.reload({ waitUntil: 'domcontentloaded', timeout: timeoutMs });
await page.waitForFunction(() => typeof window.SentinelSDK?.token === 'function', { timeout: timeoutMs });
}
const token = await page.evaluate(async (runtimeFlow) => window.SentinelSDK.token(runtimeFlow), flow);
if (typeof token !== 'string' || !token.trim()) throw new Error('sentinel_token_empty');
return token.trim();
} finally {
await context.close().catch(() => undefined);
}
}
export function fetchSentinelToken({ flow, deviceId, userAgent, proxy = null, timeoutMs = DEFAULT_TIMEOUT_MS }) {
const task = () => calculateToken({
flow: String(flow || ''),
deviceId: String(deviceId || ''),
userAgent: String(userAgent || ''),
proxy,
timeoutMs: Math.max(5000, Number(timeoutMs) || DEFAULT_TIMEOUT_MS),
});
const result = browserQueue.then(task, task);
browserQueue = result.catch(() => undefined);
return result;
}
export async function closeSentinelBrowser() {
await browserQueue.catch(() => undefined);
await closeBrowser();
}