Files
WebAI2API/lib/browser/launcher.js
T

370 lines
12 KiB
JavaScript

import { Camoufox } from 'camoufox-js';
import { FingerprintGenerator } from 'fingerprint-generator';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { createCursor } from 'ghost-cursor-playwright-port';
import { getRealViewport, clamp, random, sleep } from './utils.js';
import { logger } from '../utils/logger.js';
import { getBrowserProxy, cleanupProxy } from '../utils/proxy.js';
// 全局状态跟踪
let globalBrowserProcess = null;
let globalContext = null; // 替代 globalBrowser
/**
* 清理浏览器资源和进程
* 实现三级退出机制: Playwright close -> SIGTERM -> SIGKILL
* @returns {Promise<void>}
*/
export async function cleanup() {
// Level 1: 通过 Playwright 协议优雅关闭 Context,保存 Profile
if (globalContext) {
try {
logger.debug('浏览器', '正在断开远程调试连接并保存 Profile...');
await globalContext.close();
globalContext = null;
logger.debug('浏览器', '已关闭浏览器上下文');
} catch (e) {
logger.warn('浏览器', `关闭上下文失败: ${e.message}`);
}
}
// Level 2 & 3: 处理残留进程 (主要用于登录模式)
if (globalBrowserProcess && !globalBrowserProcess.killed) {
logger.info('浏览器', '正在终止浏览器进程...');
try {
// Level 2: 发送 SIGTERM (软杀)
globalBrowserProcess.kill('SIGTERM');
// 等待进程退出
const start = Date.now();
while (Date.now() - start < 2000) {
try {
process.kill(globalBrowserProcess.pid, 0);
await new Promise(r => setTimeout(r, 200));
} catch (e) {
break;
}
}
} catch (e) { }
// Level 3: 强制查杀 (SIGKILL)
try {
process.kill(globalBrowserProcess.pid, 0);
logger.debug('浏览器', '浏览器进程无响应,执行强制终止 (SIGKILL)...');
process.kill(-globalBrowserProcess.pid, 'SIGKILL');
} catch (e) { }
globalBrowserProcess = null;
logger.info('浏览器', '浏览器进程已终止');
}
// 清理代理
await cleanupProxy();
}
// 防止重复注册
let signalHandlersRegistered = false;
/**
* 注册进程退出信号处理
* @private
*/
function registerCleanupHandlers() {
if (signalHandlersRegistered) return;
process.on('exit', () => {
if (globalBrowserProcess) globalBrowserProcess.kill();
});
process.on('SIGINT', async () => {
await cleanup();
process.exit();
});
process.on('SIGTERM', async () => {
await cleanup();
process.exit();
});
signalHandlersRegistered = true;
}
/**
* 获取当前操作系统名称
* 将 Node.js 的 platform 转换为 Camoufox/FingerprintGenerator 支持的格式
*/
function getCurrentOS() {
const platform = os.platform();
if (platform === 'win32') return 'windows';
if (platform === 'darwin') return 'macos';
// 其他情况默认为 linux
return 'linux';
}
/**
* 获取或生成持久化指纹
* @param {string} filePath - JSON文件保存路径
*/
function getPersistentFingerprint(filePath) {
// 确保 data 目录存在
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 尝试读取现有指纹
if (fs.existsSync(filePath)) {
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
const savedData = JSON.parse(fileContent);
// 简单校验:确保读取的是一个对象
if (savedData && typeof savedData === 'object') {
logger.info('浏览器', '已加载本地持久化指纹');
return savedData;
}
} catch (e) {
logger.warn('浏览器', `读取指纹文件失败,将重新生成: ${e.message}`);
}
}
// 生成新指纹
const currentOS = getCurrentOS();
logger.info('浏览器', `正在为系统 [${currentOS}] 生成新指纹...`);
// 为不同系统使用不同的配置策略
const generatorOptions = {
browsers: ['firefox'],
operatingSystems: [currentOS],
devices: ['desktop'],
locales: ['en-US'],
screen: {
minWidth: 1280, maxWidth: 1366,
minHeight: 720, maxHeight: 768
}
};
const generator = new FingerprintGenerator(generatorOptions);
const result = generator.getFingerprint();
// 关键点:我们只需要 result.fingerprint 部分
const fingerprintToSave = result.fingerprint;
// 保存到文件
fs.writeFileSync(filePath, JSON.stringify(fingerprintToSave, null, 2));
logger.info('浏览器', `新指纹已保存至: ${filePath}`);
return fingerprintToSave;
}
/**
* 初始化浏览器 (统一启动逻辑)
* @param {object} config - 配置对象
* @param {object} [config.browser] - Browser 配置
* @param {boolean} [config.browser.headless] - 是否开启 Headless 模式
* @param {string} [config.browser.path] - Camoufox 可执行文件路径
* @param {object} [config.browser.proxy] - 代理配置
* @param {object} options - 启动选项
* @param {string} options.userDataDir - 用户数据目录路径
* @param {string} options.targetUrl - 目标 URL
* @param {string} options.productName - 产品名称(用于日志)
* @param {boolean} [options.reuseExistingTab=false] - 是否复用已有特定域名的 tab
* @param {Function} [options.waitInputValidator] - 自定义输入框等待验证函数
* @param {Function} [options.navigationHandler] - 全局导航处理器,用于自动处理登录等跳转
* @returns {Promise<{browser: object, page: object, client: object}>}
*/
export async function initBrowserBase(config, options) {
const {
userDataDir,
targetUrl,
productName,
waitInputValidator = null,
navigationHandler = null
} = options;
// 检测登录模式和 Xvfb 模式
const isLoginMode = process.argv.includes('-login');
const isXvfbMode = process.env.XVFB_RUNNING === 'true';
const ENABLE_AUTOMATION_MODE = !isLoginMode;
logger.info('浏览器', `开始初始化浏览器 (${productName})`);
logger.info('浏览器', `自动化模式: ${ENABLE_AUTOMATION_MODE ? '开启' : '关闭'}`);
if (isLoginMode) {
logger.warn('浏览器', '当前为登录模式,请手动完成登录后关闭登录模式以继续自动化程序!');
}
if (isXvfbMode) {
logger.info('浏览器', '检测到 Xvfb 环境,强制禁用无头模式');
}
const browserConfig = config?.browser || {};
// 获取指纹对象
const fingerprintPath = path.join(process.cwd(), 'data', 'camoufoxFingerprints.json');
const myFingerprint = getPersistentFingerprint(fingerprintPath);
// 构造 Camoufox 启动选项
logger.info('浏览器', '正在启动 Camoufox 浏览器...');
const currentOS = getCurrentOS();
const camoufoxLaunchOptions = {
// 基础选项 (snake_case)
executable_path: browserConfig.path || undefined,
headless: browserConfig.headless && !isLoginMode && !isXvfbMode,
user_data_dir: userDataDir,
window: [1366, 768],
ff_version: 135,
fingerprint: myFingerprint,
os: currentOS,
i_know_what_im_doing: true,
block_webrtc: true,
exclude_addons: ['UBO'],
geoip: false
};
// Headless 模式配置
if (browserConfig.headless && !isLoginMode && !isXvfbMode) {
logger.info('浏览器', 'Headless 模式: 启用');
} else {
const reasons = [];
if (isLoginMode) reasons.push('登录模式');
if (isXvfbMode) reasons.push('Xvfb 模式');
if (!browserConfig.headless) reasons.push('配置禁用');
logger.info('浏览器', 'Headless 模式: 禁用' + (reasons.length > 0 ? ` (${reasons.join(', ')})` : ''));
}
// 代理配置适配
const proxyObject = await getBrowserProxy(browserConfig.proxy);
if (proxyObject) {
camoufoxLaunchOptions.proxy = proxyObject;
}
// 启动 Camoufox
const context = await Camoufox(camoufoxLaunchOptions);
globalContext = context; // 存储全局 Context
logger.info('浏览器', 'Camoufox 浏览器已启动');
// 注册清理处理器
registerCleanupHandlers();
// 注册断开连接事件
context.on('close', async () => {
logger.warn('浏览器', 'Camoufox 浏览器已断开连接');
await cleanup();
process.exit(0);
});
// 获取或创建 Page
let page;
const existingPages = context.pages(); // 获取启动时自动打开的页面
if (!page) {
if (existingPages.length > 0) {
page = existingPages[0];
logger.debug('浏览器', '复用浏览器启动时的默认标签页');
} else {
page = await context.newPage();
logger.debug('浏览器', '浏览器没有标签,已创建新标签页');
}
}
// 强制刷新一下视口大小,防止复用默认窗口时尺寸不对
if (camoufoxLaunchOptions.viewport) {
await page.setViewportSize(camoufoxLaunchOptions.viewport);
}
// 注册全局导航处理器(用于自动处理登录等跳转)
if (navigationHandler) {
page.on('framenavigated', async () => {
try {
await navigationHandler(page);
} catch (e) {
logger.warn('浏览器', `全局导航处理器出错: ${e.message}`);
}
});
logger.debug('浏览器', '已注册全局导航处理器');
}
// 登录模式挂起逻辑
if (isLoginMode) {
// 尝试导航到目标页面方便用户登录
try {
logger.info('浏览器', `正在连接 ${productName}...`);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
} catch (e) {
logger.warn('浏览器', `打开页面失败: ${e.message}`);
}
logger.info('浏览器', '请在弹出的浏览器窗口中手动完成登录操作');
logger.info('浏览器', '完成后可直接关闭浏览器窗口或在终端结束程序');
await new Promise((resolve) => {
context.on('close', () => {
logger.info('浏览器', '检测到浏览器窗口关闭,程序即将退出');
resolve();
});
});
await cleanup();
process.exit(0);
}
// 初始化 ghost-cursor
page.cursor = createCursor(page);
// --- 行为预热建立人机检测信任 ---
const urlDomain = new URL(targetUrl).hostname;
if (!page.url().includes(urlDomain)) {
logger.info('浏览器', `正在连接 ${productName}...`);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
} else {
logger.info('浏览器', `页面已在 ${productName},跳过跳转`);
}
logger.info('浏览器', '正在随机浏览页面以建立信任...');
// 计算屏幕中心点 (动态获取视口大小)
const vp = await getRealViewport(page);
// 计算动态中心点
const centerX = vp.width / 2;
const centerY = vp.height / 2;
// 第一次移动:从左上角移动到中心附近
if (page.cursor) {
// 使用 clamp 确保随机偏移后仍在屏幕内
const targetX = clamp(centerX + random(-200, 200), 10, vp.safeWidth);
const targetY = clamp(centerY + random(-200, 200), 10, vp.safeHeight);
// 移动鼠标 (增加拟人化)
await page.cursor.moveTo({ x: targetX, y: targetY });
}
await sleep(500, 1000);
// 模拟滚动行为
try {
await page.mouse.wheel({ deltaY: random(100, 300) });
await sleep(800, 1500);
await page.mouse.wheel({ deltaY: -random(50, 100) });
} catch (e) { }
// 如果提供了自定义输入框验证函数,使用它
if (waitInputValidator && typeof waitInputValidator === 'function') {
await waitInputValidator(page);
}
logger.info('浏览器', '浏览器初始化完成,系统就绪');
logger.warn('浏览器', '当任务运行时请勿随意调节窗口大小,以免鼠标轨迹错位!');
// 返回对象 (兼容性处理)
return {
browser: context,
page
};
}