chore: release v0.8.2

This commit is contained in:
Jim Liu 宝玉
2026-01-17 19:00:27 -06:00
parent 688d1760ed
commit 3b13b25f1a
37 changed files with 2557 additions and 1983 deletions
@@ -0,0 +1,82 @@
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolveGeminiWebCookiePath } from './paths.js';
export type CookieMap = Record<string, string>;
export type CookieFileData =
| {
cookies: CookieMap;
updated_at: number;
source?: string;
}
| {
version: number;
updatedAt: string;
cookieMap: CookieMap;
source?: string;
};
export async function read_cookie_file(p: string = resolveGeminiWebCookiePath()): Promise<CookieMap | null> {
try {
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) return null;
const raw = await readFile(p, 'utf8');
const data = JSON.parse(raw) as unknown;
if (data && typeof data === 'object' && 'cookies' in (data as any)) {
const cookies = (data as any).cookies as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return out;
}
}
if (data && typeof data === 'object' && 'cookieMap' in (data as any)) {
const cookies = (data as any).cookieMap as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
}
if (data && typeof data === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
return null;
} catch {
return null;
}
}
export async function write_cookie_file(
cookies: CookieMap,
p: string = resolveGeminiWebCookiePath(),
source?: string,
): Promise<void> {
const dir = path.dirname(p);
await mkdir(dir, { recursive: true });
const payload: CookieFileData = {
version: 1,
updatedAt: new Date().toISOString(),
cookieMap: cookies,
source,
};
await writeFile(p, JSON.stringify(payload, null, 2), 'utf8');
}
export const readCookieFile = read_cookie_file;
export const writeCookieFile = write_cookie_file;
@@ -0,0 +1,34 @@
import { APIError, ImageGenerationError } from '../exceptions.js';
import { sleep } from './http.js';
export function running(retry: number = 0) {
return <TArgs extends unknown[], TResult>(
fn: (client: any, ...args: TArgs) => Promise<TResult>,
): ((client: any, ...args: TArgs) => Promise<TResult>) => {
const wrap = async (client: any, ...args: TArgs): Promise<TResult> => {
try {
if (!client?._running) {
await client.init?.({
timeout: client.timeout,
auto_close: client.auto_close,
close_delay: client.close_delay,
auto_refresh: client.auto_refresh,
refresh_interval: client.refresh_interval,
verbose: false,
});
}
return await fn(client, ...args);
} catch (e) {
let r = retry;
if (e instanceof ImageGenerationError) r = Math.min(1, r);
if (e instanceof APIError && r > 0) {
await sleep(1000);
return await running(r - 1)(fn)(client, ...args);
}
throw e;
}
};
return wrap;
};
}
@@ -0,0 +1,192 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { Endpoint, Headers } from '../constants.js';
import { AuthError } from '../exceptions.js';
import { cookie_header, extract_set_cookie_value, fetch_with_timeout } from './http.js';
import { logger } from './logger.js';
import { read_cookie_file, write_cookie_file } from './cookie-file.js';
import { resolveGeminiWebDataDir, resolveGeminiWebCookiePath } from './paths.js';
import { load_browser_cookies } from './load-browser-cookies.js';
async function send_request(cookies: Record<string, string>, verbose: boolean): Promise<[string, Record<string, string>]> {
const res = await fetch_with_timeout(Endpoint.INIT, {
method: 'GET',
headers: { ...Headers.GEMINI, Cookie: cookie_header(cookies) },
redirect: 'follow',
timeout_ms: 30_000,
});
if (!res.ok) throw new Error(`Init failed: ${res.status} ${res.statusText}`);
const text = await res.text();
const m = text.match(/\"SNlM0e\":\"(.*?)\"/);
if (!m) throw new Error('Missing SNlM0e in response');
if (verbose) logger.debug('Init succeeded. Initializing client...');
return [m[1]!, cookies];
}
function merge_cookie_maps(...maps: Array<Record<string, string> | null | undefined>): Record<string, string> {
const out: Record<string, string> = {};
for (const m of maps) {
if (!m) continue;
for (const [k, v] of Object.entries(m)) {
if (typeof v === 'string' && v.length > 0) out[k] = v;
}
}
return out;
}
function read_cached_1psidts_file(dir: string, sid: string): string | null {
try {
const p = path.join(dir, `.cached_1psidts_${sid}.txt`);
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) return null;
const v = fs.readFileSync(p, 'utf8').trim();
return v || null;
} catch {
return null;
}
}
function list_cached_1psidts(dir: string): Array<{ sid: string; sidts: string }> {
const out: Array<{ sid: string; sidts: string }> = [];
try {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return out;
for (const f of fs.readdirSync(dir)) {
if (!f.startsWith('.cached_1psidts_') || !f.endsWith('.txt')) continue;
const sid = f.slice('.cached_1psidts_'.length, -'.txt'.length);
if (!sid) continue;
const sidts = read_cached_1psidts_file(dir, sid);
if (sidts) out.push({ sid, sidts });
}
} catch {}
return out;
}
async function fetch_google_extra_cookies(proxy: string | null, verbose: boolean): Promise<Record<string, string>> {
void proxy;
try {
const res = await fetch_with_timeout(Endpoint.GOOGLE, { timeout_ms: 15_000 });
const setCookie = res.headers.get('set-cookie');
const nid = extract_set_cookie_value(setCookie, 'NID');
if (nid) return { NID: nid };
} catch (e) {
if (verbose) logger.debug(`Skipping google.com preflight: ${e instanceof Error ? e.message : String(e)}`);
}
return {};
}
export async function get_access_token(
base_cookies: Record<string, string>,
proxy: string | null = null,
verbose: boolean = false,
): Promise<[string, Record<string, string>]> {
const extra = await fetch_google_extra_cookies(proxy, verbose);
const cacheDir = resolveGeminiWebDataDir();
const candidates: Record<string, string>[] = [];
const cookieFilePath = resolveGeminiWebCookiePath();
const cachedFile = await read_cookie_file(cookieFilePath);
const forceLogin = !!(process.env.GEMINI_WEB_LOGIN?.trim() || process.env.GEMINI_WEB_FORCE_LOGIN?.trim());
const shouldUseChromeFirst = forceLogin || (!cachedFile && !base_cookies['__Secure-1PSID'] && !base_cookies['__Secure-1PSIDTS']);
if (shouldUseChromeFirst) {
try {
const browser = await load_browser_cookies('google.com', verbose);
for (const cookies of Object.values(browser)) {
candidates.push(merge_cookie_maps(extra, cookies));
}
} catch (e) {
if (verbose) logger.warning(`Failed to load cookies via Chrome CDP: ${e instanceof Error ? e.message : String(e)}`);
}
}
if (base_cookies['__Secure-1PSID'] && base_cookies['__Secure-1PSIDTS']) {
candidates.push(merge_cookie_maps(extra, base_cookies));
} else if (verbose) {
logger.debug('Skipping loading base cookies. Either __Secure-1PSID or __Secure-1PSIDTS is not provided.');
}
if (cachedFile) {
candidates.push(merge_cookie_maps(extra, cachedFile));
}
if (base_cookies['__Secure-1PSID'] && !base_cookies['__Secure-1PSIDTS']) {
const sid = base_cookies['__Secure-1PSID'];
const sidts = read_cached_1psidts_file(cacheDir, sid);
if (sidts) {
candidates.push(merge_cookie_maps(extra, base_cookies, { '__Secure-1PSIDTS': sidts }));
} else if (verbose) {
logger.debug('Skipping loading cached cookies. Cache file not found or empty.');
}
} else if (!base_cookies['__Secure-1PSID']) {
const caches = list_cached_1psidts(cacheDir);
for (const c of caches) {
candidates.push(merge_cookie_maps(extra, { '__Secure-1PSID': c.sid, '__Secure-1PSIDTS': c.sidts }));
}
if (caches.length === 0 && verbose) {
logger.debug('Skipping loading cached cookies. Cookies will be cached after successful initialization.');
}
}
const unique: Record<string, string>[] = [];
const seen = new Set<string>();
for (const c of candidates) {
const key = `${c['__Secure-1PSID'] ?? ''}:${c['__Secure-1PSIDTS'] ?? ''}:${c.NID ?? ''}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push(c);
}
const try_candidates = async (): Promise<[string, Record<string, string>]> => {
if (unique.length === 0) throw new Error('no candidates');
const attempts = unique.map(async (c, i) => {
try {
if (verbose) logger.debug(`Init attempt (${i + 1}/${unique.length})...`);
return await send_request(c, verbose);
} catch (e) {
if (verbose) logger.debug(`Init attempt (${i + 1}/${unique.length}) failed: ${e instanceof Error ? e.message : String(e)}`);
throw e;
}
});
return (await Promise.any(attempts)) as [string, Record<string, string>];
};
try {
const [token, cookies] = await try_candidates();
await write_cookie_file(cookies, resolveGeminiWebCookiePath(), 'init').catch(() => {});
return [token, cookies];
} catch {
if (verbose) logger.debug('Cookie attempts failed. Falling back to Chrome CDP cookie load...');
}
const browser = await load_browser_cookies('google.com', verbose);
let valid = 0;
for (const cookies of Object.values(browser)) {
if (cookies['__Secure-1PSID']) valid++;
if (base_cookies['__Secure-1PSID'] && cookies['__Secure-1PSID'] && cookies['__Secure-1PSID'] !== base_cookies['__Secure-1PSID']) {
if (verbose) logger.debug('Skipping loaded browser cookies: __Secure-1PSID does not match the one provided.');
continue;
}
unique.push(merge_cookie_maps(extra, cookies));
}
if (valid === 0) {
throw new AuthError(
'No valid cookies available for initialization. Please pass __Secure-1PSID and __Secure-1PSIDTS manually.',
);
}
try {
const [token, cookies] = await try_candidates();
await write_cookie_file(cookies, resolveGeminiWebCookiePath(), 'init').catch(() => {});
return [token, cookies];
} catch {
throw new AuthError(
`Failed to initialize client. SECURE_1PSIDTS could get expired frequently, please make sure cookie values are up to date. (Failed initialization attempts: ${unique.length})`,
);
}
}
export const getAccessToken = get_access_token;
@@ -0,0 +1,57 @@
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve) => {
const t = setTimeout(() => {
if (signal) signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(t);
if (signal) signal.removeEventListener('abort', onAbort);
resolve();
};
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort, { once: true });
}
}
});
}
export function cookie_header(cookies: Record<string, string>): string {
return Object.entries(cookies)
.filter(([, v]) => typeof v === 'string' && v.length > 0)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
}
export const cookieHeader = cookie_header;
export function extract_set_cookie_value(setCookie: string | null, name: string): string | null {
if (!setCookie) return null;
const re = new RegExp(`(?:^|[;,\\s])${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}=([^;]+)`, 'i');
const m = setCookie.match(re);
if (!m) return null;
return m[1] ?? null;
}
export async function fetch_with_timeout(
url: string,
init: RequestInit & { timeout_ms?: number } = {},
): Promise<Response> {
const { timeout_ms, ...rest } = init;
if (!timeout_ms || timeout_ms <= 0) return fetch(url, rest);
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), timeout_ms);
try {
return await fetch(url, { ...rest, signal: ctl.signal });
} finally {
clearTimeout(t);
}
}
export const fetchWithTimeout = fetch_with_timeout;
@@ -0,0 +1,20 @@
export { running } from './decorators.js';
export { get_access_token, getAccessToken } from './get-access-token.js';
export { load_browser_cookies, loadBrowserCookies } from './load-browser-cookies.js';
export { logger, set_log_level, setLogLevel } from './logger.js';
export { extract_json_from_response, extractJsonFromResponse, get_nested_value, getNestedValue } from './parsing.js';
export { rotate_1psidts, rotate1psidts } from './rotate-1psidts.js';
export { upload_file, uploadFile, parse_file_name, parseFileName } from './upload-file.js';
export { read_cookie_file, readCookieFile, write_cookie_file, writeCookieFile } from './cookie-file.js';
export {
resolveUserDataRoot,
resolveGeminiWebChromeProfileDir,
resolveGeminiWebCookiePath,
resolveGeminiWebDataDir,
resolveGeminiWebSessionPath,
resolveGeminiWebSessionsDir,
} from './paths.js';
export { cookie_header, cookieHeader, fetch_with_timeout, fetchWithTimeout, sleep } from './http.js';
export const rotate_tasks = new Map<string, unknown>();
@@ -0,0 +1,271 @@
import { spawn, type ChildProcess } from 'node:child_process';
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import net from 'node:net';
import process from 'node:process';
import { logger } from './logger.js';
import { fetch_with_timeout, sleep } from './http.js';
import { read_cookie_file, type CookieMap, write_cookie_file } from './cookie-file.js';
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js';
type CdpSendOptions = { sessionId?: string; timeoutMs?: number };
class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<
number,
{ resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> | null }
>();
private constructor(ws: WebSocket) {
this.ws = ws;
this.ws.addEventListener('message', (event) => {
try {
const data = typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; result?: unknown; error?: { message?: string } };
if (msg.id) {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
if (p.timer) clearTimeout(p.timer);
if (msg.error?.message) p.reject(new Error(msg.error.message));
else p.resolve(msg.result);
}
}
} catch {}
});
this.ws.addEventListener('close', () => {
for (const [id, p] of this.pending.entries()) {
this.pending.delete(id);
if (p.timer) clearTimeout(p.timer);
p.reject(new Error('CDP connection closed.'));
}
});
}
static async connect(url: string, timeoutMs: number): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const t = setTimeout(() => reject(new Error('CDP connection timeout.')), timeoutMs);
ws.addEventListener('open', () => {
clearTimeout(t);
resolve();
});
ws.addEventListener('error', () => {
clearTimeout(t);
reject(new Error('CDP connection failed.'));
});
});
return new CdpConnection(ws);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, opts?: CdpSendOptions): Promise<T> {
const id = ++this.nextId;
const msg: Record<string, unknown> = { id, method };
if (params) msg.params = params;
if (opts?.sessionId) msg.sessionId = opts.sessionId;
const timeoutMs = opts?.timeoutMs ?? 15_000;
const out = await new Promise<unknown>((resolve, reject) => {
const t =
timeoutMs > 0
? setTimeout(() => {
this.pending.delete(id);
reject(new Error(`CDP timeout: ${method}`));
}, timeoutMs)
: null;
this.pending.set(id, { resolve, reject, timer: t });
this.ws.send(JSON.stringify(msg));
});
return out as T;
}
close(): void {
try {
this.ws.close();
} catch {}
}
}
async function get_free_port(): Promise<number> {
return await new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
srv.on('error', reject);
srv.listen(0, '127.0.0.1', () => {
const addr = srv.address();
if (!addr || typeof addr === 'string') {
srv.close(() => reject(new Error('Unable to allocate a free TCP port.')));
return;
}
const port = addr.port;
srv.close((err) => (err ? reject(err) : resolve(port)));
});
});
}
function find_chrome_executable(): string | null {
const override = process.env.GEMINI_WEB_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
const candidates: string[] = [];
switch (process.platform) {
case 'darwin':
candidates.push(
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
);
break;
case 'win32':
candidates.push(
'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',
'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',
'C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',
'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',
);
break;
default:
candidates.push(
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
'/usr/bin/microsoft-edge',
);
break;
}
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}
async function wait_for_chrome_debug_port(port: number, timeoutMs: number): Promise<string> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const res = await fetch_with_timeout(`http://127.0.0.1:${port}/json/version`, { timeout_ms: 5_000 });
if (!res.ok) throw new Error(`status=${res.status}`);
const j = (await res.json()) as { webSocketDebuggerUrl?: string };
if (j.webSocketDebuggerUrl) return j.webSocketDebuggerUrl;
} catch {}
await sleep(200);
}
throw new Error('Chrome debug port not ready');
}
async function launch_chrome(profileDir: string, port: number): Promise<ChildProcess> {
const chrome = find_chrome_executable();
if (!chrome) throw new Error('Chrome executable not found.');
const args = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-popup-blocking',
'https://gemini.google.com/app',
];
return spawn(chrome, args, { stdio: 'ignore' });
}
async function fetch_google_cookies_via_cdp(
profileDir: string,
timeoutMs: number,
verbose: boolean,
): Promise<CookieMap> {
await mkdir(profileDir, { recursive: true });
const port = await get_free_port();
const chrome = await launch_chrome(profileDir, port);
let cdp: CdpConnection | null = null;
try {
const wsUrl = await wait_for_chrome_debug_port(port, 30_000);
cdp = await CdpConnection.connect(wsUrl, 15_000);
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', {
url: 'https://gemini.google.com/app',
newWindow: true,
});
const { sessionId } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId, flatten: true });
await cdp.send('Network.enable', {}, { sessionId });
if (verbose) {
logger.info('Chrome opened. If needed, complete Google login in the window. Waiting for cookies...');
}
const start = Date.now();
let last: CookieMap = {};
while (Date.now() - start < timeoutMs) {
const { cookies } = await cdp.send<{ cookies: Array<{ name: string; value: string }> }>(
'Network.getCookies',
{ urls: ['https://gemini.google.com/', 'https://accounts.google.com/', 'https://www.google.com/'] },
{ sessionId, timeoutMs: 10_000 },
);
const m: CookieMap = {};
for (const c of cookies) {
if (c?.name && typeof c.value === 'string') m[c.name] = c.value;
}
last = m;
if (m['__Secure-1PSID'] && (m['__Secure-1PSIDTS'] || Date.now() - start > 10_000)) {
return m;
}
await sleep(1000);
}
throw new Error(`Timed out waiting for Google cookies. Last keys: ${Object.keys(last).join(', ')}`);
} finally {
if (cdp) {
try {
await cdp.send('Browser.close', {}, { timeoutMs: 5_000 });
} catch {}
cdp.close();
}
try {
chrome.kill('SIGTERM');
} catch {}
setTimeout(() => {
if (!chrome.killed) {
try {
chrome.kill('SIGKILL');
} catch {}
}
}, 2_000).unref?.();
}
}
export async function load_browser_cookies(domain_name: string = '', verbose: boolean = true): Promise<Record<string, CookieMap>> {
const force = process.env.GEMINI_WEB_LOGIN?.trim() || process.env.GEMINI_WEB_FORCE_LOGIN?.trim();
if (!force) {
const cached = await read_cookie_file();
if (cached) return { chrome: cached };
}
const profileDir = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || resolveGeminiWebChromeProfileDir();
const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose);
const filtered: CookieMap = {};
for (const [k, v] of Object.entries(cookies)) {
if (typeof v === 'string' && v.length > 0) filtered[k] = v;
}
await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp');
void domain_name;
return { chrome: filtered };
}
export const loadBrowserCookies = load_browser_cookies;
@@ -0,0 +1,42 @@
export type LogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | number;
const lvl: Record<Exclude<LogLevel, number>, number> = {
TRACE: 0,
DEBUG: 1,
INFO: 2,
WARNING: 3,
ERROR: 4,
CRITICAL: 5,
};
let cur = lvl.INFO;
function toNum(level: LogLevel): number {
if (typeof level === 'number') return level;
return lvl[level] ?? lvl.INFO;
}
export function set_log_level(level: LogLevel): void {
cur = toNum(level);
}
export const setLogLevel = set_log_level;
function emit(level: Exclude<LogLevel, number>, args: unknown[]): void {
if (lvl[level] < cur) return;
const prefix = `[gemini_webapi] ${level}:`;
if (level === 'WARNING') console.warn(prefix, ...args);
else if (level === 'ERROR' || level === 'CRITICAL') console.error(prefix, ...args);
else console.log(prefix, ...args);
}
export const logger = {
trace: (...args: unknown[]) => emit('TRACE', args),
debug: (...args: unknown[]) => emit('DEBUG', args),
info: (...args: unknown[]) => emit('INFO', args),
warning: (...args: unknown[]) => emit('WARNING', args),
error: (...args: unknown[]) => emit('ERROR', args),
success: (...args: unknown[]) => emit('INFO', args),
};
@@ -0,0 +1,44 @@
import { logger } from './logger.js';
export function get_nested_value<T = unknown>(data: unknown, path: number[], def?: T): T {
let cur: unknown = data;
for (let i = 0; i < path.length; i++) {
const k = path[i]!;
if (!Array.isArray(cur)) {
logger.debug(`Safe navigation: path ${JSON.stringify(path)} ended at index ${i} (key '${k}'), returning default.`);
return def as T;
}
cur = cur[k];
if (cur === undefined) {
logger.debug(`Safe navigation: path ${JSON.stringify(path)} ended at index ${i} (key '${k}'), returning default.`);
return def as T;
}
}
if (cur == null && def !== undefined) return def as T;
return cur as T;
}
export function extract_json_from_response(text: string): unknown {
if (typeof text !== 'string') {
throw new TypeError(`Input text is expected to be a string, got ${typeof text} instead.`);
}
let last: unknown = undefined;
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
last = JSON.parse(trimmed) as unknown;
} catch {}
}
if (last === undefined) {
throw new Error('Could not find a valid JSON object or array in the response.');
}
return last;
}
export const extractJsonFromResponse = extract_json_from_response;
export const getNestedValue = get_nested_value;
@@ -0,0 +1,46 @@
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
const APP_DATA_DIR = 'baoyu-skills';
const GEMINI_DATA_DIR = 'gemini-web';
const COOKIE_FILE_NAME = 'cookies.json';
const PROFILE_DIR_NAME = 'chrome-profile';
export function resolveUserDataRoot(): string {
if (process.platform === 'win32') {
return process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
}
if (process.platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support');
}
return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), '.local', 'share');
}
export function resolveGeminiWebDataDir(): string {
const override = process.env.GEMINI_WEB_DATA_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveUserDataRoot(), APP_DATA_DIR, GEMINI_DATA_DIR);
}
export function resolveGeminiWebCookiePath(): string {
const override = process.env.GEMINI_WEB_COOKIE_PATH?.trim();
if (override) return path.resolve(override);
return path.join(resolveGeminiWebDataDir(), COOKIE_FILE_NAME);
}
export function resolveGeminiWebChromeProfileDir(): string {
const override = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveGeminiWebDataDir(), PROFILE_DIR_NAME);
}
export function resolveGeminiWebSessionsDir(): string {
return path.join(resolveGeminiWebDataDir(), 'sessions');
}
export function resolveGeminiWebSessionPath(name: string): string {
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_');
return path.join(resolveGeminiWebSessionsDir(), `${sanitized}.json`);
}
@@ -0,0 +1,46 @@
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, writeFile } from 'node:fs/promises';
import { Endpoint, Headers } from '../constants.js';
import { AuthError } from '../exceptions.js';
import { cookie_header, extract_set_cookie_value, fetch_with_timeout } from './http.js';
import { resolveGeminiWebDataDir } from './paths.js';
export async function rotate_1psidts(cookies: Record<string, string>, _proxy?: string | null): Promise<string | null> {
const p = resolveGeminiWebDataDir();
await mkdir(p, { recursive: true });
const sid = cookies['__Secure-1PSID'];
if (!sid) throw new Error('Missing __Secure-1PSID cookie.');
const cachePath = path.join(p, `.cached_1psidts_${sid}.txt`);
try {
const st = fs.statSync(cachePath);
if (Date.now() - st.mtimeMs <= 60_000) return null;
} catch {}
const res = await fetch_with_timeout(Endpoint.ROTATE_COOKIES, {
method: 'POST',
headers: { ...Headers.ROTATE_COOKIES, Cookie: cookie_header(cookies) },
body: '[000,"-0000000000000000000"]',
redirect: 'follow',
timeout_ms: 30_000,
});
if (res.status === 401) throw new AuthError('Failed to refresh cookies (401).');
if (!res.ok) throw new Error(`RotateCookies failed: ${res.status} ${res.statusText}`);
const setCookie = res.headers.get('set-cookie');
const v = extract_set_cookie_value(setCookie, '__Secure-1PSIDTS');
if (v) {
await writeFile(cachePath, v, 'utf8');
return v;
}
return null;
}
export const rotate1psidts = rotate_1psidts;
@@ -0,0 +1,41 @@
import fs from 'node:fs';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { Endpoint, Headers } from '../constants.js';
export async function upload_file(file: string, _proxy?: string | null): Promise<string> {
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
throw new Error(`${file} is not a valid file.`);
}
const filename = path.basename(file);
const content = await readFile(file);
const form = new FormData();
form.append('file', new Blob([content]), filename);
const res = await fetch(Endpoint.UPLOAD, {
method: 'POST',
headers: { ...Headers.UPLOAD },
body: form,
redirect: 'follow',
});
if (!res.ok) {
throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
}
return await res.text();
}
export function parse_file_name(file: string): string {
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
throw new Error(`${file} is not a valid file.`);
}
return path.basename(file);
}
export const uploadFile = upload_file;
export const parseFileName = parse_file_name;