Files
2026-06-19 18:45:55 +08:00

300 lines
9.9 KiB
TypeScript

/**
* 浏览器管理器 - 单例模式管理浏览器实例
* 避免重复初始化和过早关闭问题
*/
import { chromium, Browser, BrowserContext, Page } from 'playwright';
import path from 'path';
import fs from 'fs';
import { app } from 'electron';
import { getPlaywrightChromiumPath, COMMON_BROWSER_ARGS, BROWSER_IGNORE_DEFAULT_ARGS, STEALTH_INIT_SCRIPT } from '../../lib/browser-path';
import { getRuntimeDataPath } from '../../lib/resource-path';
export interface BrowserManager {
getInstance(): BrowserManager;
initBrowser(): Promise<void>;
newPage(): Promise<Page>;
close(): Promise<void>;
isInitialized(): boolean;
}
class DouyinBrowserManager implements BrowserManager {
private static instance: DouyinBrowserManager;
private browser: Browser | null = null;
private context: BrowserContext | null = null;
private isInitializing: boolean = false;
private userDataPath: string;
private constructor() {
this.userDataPath = getRuntimeDataPath('browser-data', 'douyin');
}
/**
* 获取浏览器可执行文件路径(使用统一工具)
*/
private getChromiumExecutablePath(): string | undefined {
return getPlaywrightChromiumPath();
}
public static getInstance(): DouyinBrowserManager {
if (!DouyinBrowserManager.instance) {
DouyinBrowserManager.instance = new DouyinBrowserManager();
}
return DouyinBrowserManager.instance;
}
public async initBrowser(): Promise<void> {
if (this.isInitializing) {
console.log('浏览器正在初始化中,等待完成...');
while (this.isInitializing) {
await new Promise(resolve => setTimeout(resolve, 500));
}
return;
}
if (this.context) {
console.log('浏览器已初始化,直接返回');
return;
}
this.isInitializing = true;
try {
console.log('开始初始化浏览器实例...');
const executablePath = this.getChromiumExecutablePath();
if (executablePath) {
console.log('✅ [Browser] 使用打包的浏览器:', executablePath);
try {
fs.accessSync(executablePath, fs.constants.R_OK);
console.log('✅ [Browser] 浏览器文件可读');
} catch (e) {
console.error('❌ [Browser] 浏览器文件不可访问:', executablePath, e);
}
} else {
console.log('⚠️ [Browser] 未找到打包的浏览器,将使用 Playwright 默认浏览器');
}
// === 阶段1:正常启动 ===
try {
console.log('[阶段1] 使用现有用户数据目录启动:', this.userDataPath);
await this.launchBrowserContext(executablePath, this.userDataPath);
console.log('✅ [阶段1] 浏览器启动成功');
return;
} catch (error1) {
const errMsg1 = error1 instanceof Error ? error1.message : String(error1);
console.error('❌ [阶段1] 启动失败:', errMsg1);
// === 阶段2:清理锁文件后重试 ===
try {
console.log('[阶段2] 清理锁文件后重试...');
this.cleanupBrowserLocks(this.userDataPath);
await this.launchBrowserContext(executablePath, this.userDataPath);
console.log('✅ [阶段2] 浏览器启动成功(清理锁文件后)');
return;
} catch (error2) {
const errMsg2 = error2 instanceof Error ? error2.message : String(error2);
console.error('❌ [阶段2] 启动失败:', errMsg2);
// === 阶段3:使用全新临时目录 ===
try {
const tempDataPath = getRuntimeDataPath('temp', `douyin-browser-${Date.now()}`);
console.log('[阶段3] 使用全新临时目录启动:', tempDataPath);
fs.mkdirSync(tempDataPath, { recursive: true });
await this.launchBrowserContext(executablePath, tempDataPath);
console.log('✅ [阶段3] 浏览器启动成功(使用临时目录)');
return;
} catch (error3) {
const errMsg3 = error3 instanceof Error ? error3.message : String(error3);
console.error('❌ [阶段3] 所有启动方式均失败:', errMsg3);
// 给出详细的诊断信息
const isDllError = errMsg3.includes('3221225781') || errMsg3.includes('0xC0000135');
const isClosed = errMsg3.includes('has been closed');
if (isDllError || isClosed) {
throw new Error(
'浏览器启动失败(错误码: 0xC0000135)。\n' +
'可能原因:\n' +
'1. 系统缺少 Visual C++ 运行时库,请安装: https://aka.ms/vs/17/release/vc_redist.x64.exe\n' +
'2. 系统版本过低,Chromium 要求 Windows 10 或更高版本\n' +
'3. 杀毒软件拦截了浏览器进程\n' +
'安装 VC++ 运行库后重启应用即可。'
);
}
throw error3;
}
}
}
} finally {
this.isInitializing = false;
}
}
/**
* 启动浏览器上下文(内部方法)
*/
private async launchBrowserContext(executablePath: string | undefined, userDataPath: string): Promise<void> {
if (!fs.existsSync(userDataPath)) {
fs.mkdirSync(userDataPath, { recursive: true });
}
this.context = await chromium.launchPersistentContext(userDataPath, {
executablePath,
headless: true,
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
locale: 'zh-CN',
acceptDownloads: true,
ignoreHTTPSErrors: true,
permissions: ['geolocation', 'notifications'],
serviceWorkers: 'block',
args: COMMON_BROWSER_ARGS,
ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS,
});
// 注入反检测脚本(与自动发布模块一致)
await this.context.addInitScript(STEALTH_INIT_SCRIPT);
this.browser = this.context.browser();
console.log('✅ 浏览器实例初始化成功');
if (this.browser) {
this.browser.on('disconnected', () => {
console.log('浏览器已断开连接');
this.browser = null;
this.context = null;
});
}
}
/**
* 清理浏览器锁文件(修复崩溃后无法重启的问题)
*/
private cleanupBrowserLocks(userDataPath: string): void {
const lockFiles = ['SingletonLock', 'SingletonSocket', 'SingletonCookie', 'Lock'];
for (const lockFile of lockFiles) {
const lockPath = path.join(userDataPath, lockFile);
try {
if (fs.existsSync(lockPath)) {
fs.unlinkSync(lockPath);
console.log('🗑️ 已清理锁文件:', lockFile);
}
} catch (e) {
console.log('清理锁文件失败(忽略):', lockFile, e);
}
}
}
public async newPage(): Promise<Page> {
if (!this.browser || !this.context) {
await this.initBrowser();
}
if (!this.context) {
throw new Error('浏览器上下文未初始化');
}
try {
const page = await this.context.newPage();
// 设置页面配置
page.setDefaultTimeout(60000);
// 监听页面事件
page.on('close', () => {
console.log('页面已关闭');
});
page.on('error', (error) => {
console.error('页面错误:', error);
});
return page;
} catch (error) {
console.error('创建新页面失败:', error);
throw error;
}
}
public async close(): Promise<void> {
try {
console.log('开始关闭浏览器管理器...');
// 保存状态
if (this.context) {
try {
const statePath = path.join(this.userDataPath, 'douyin-state.json');
await this.context.storageState({ path: statePath });
console.log('✅ 浏览器状态已保存');
} catch (saveError) {
console.log('保存浏览器状态失败:', saveError);
}
// 🔧 关键修复:从 context 对象中获取浏览器实例
try {
const browserInstance = (this.context as any).browser?.() || (this.context as any)._browser;
if (browserInstance) {
console.log('找到浏览器实例,准备直接关闭浏览器窗口...');
try {
await browserInstance.close();
console.log('✅ 浏览器窗口已关闭');
} catch (browserCloseError) {
console.log('浏览器关闭失败:', browserCloseError);
}
}
} catch (e) {
console.log('无法获取浏览器实例,尝试通过 context.close() 关闭:', e);
}
// 关闭上下文
try {
await this.context.close();
console.log('✅ 上下文已关闭');
} catch (contextCloseError) {
console.log('上下文关闭失败(忽略):', contextCloseError);
}
this.context = null;
}
// 确保浏览器进程被关闭
if (this.browser) {
try {
console.log('正在关闭浏览器主进程...');
await this.browser.close();
console.log('✅ 浏览器主进程已关闭');
} catch (browserCloseError) {
console.log('浏览器主进程关闭失败:', browserCloseError);
}
this.browser = null;
}
console.log('✅ 浏览器管理器已彻底关闭');
} catch (error) {
console.error('关闭浏览器管理器失败:', error);
}
}
public isInitialized(): boolean {
return this.browser !== null && this.context !== null;
}
public getBrowserInfo(): { browser: Browser | null; context: BrowserContext | null } {
return {
browser: this.browser,
context: this.context
};
}
// 全局清理方法
public static async cleanup(): Promise<void> {
if (DouyinBrowserManager.instance) {
await DouyinBrowserManager.instance.close();
DouyinBrowserManager.instance = null as any;
}
}
}
export { DouyinBrowserManager };