/** * 浏览器管理器 - 单例模式管理浏览器实例 * 避免重复初始化和过早关闭问题 */ 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; newPage(): Promise; close(): Promise; 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 { 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 { 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 { 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 { 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 { if (DouyinBrowserManager.instance) { await DouyinBrowserManager.instance.close(); DouyinBrowserManager.instance = null as any; } } } export { DouyinBrowserManager };