"use strict"; /** * Puppeteer 浏览器单例(参考 zhenqianba DouyinBrowserManager / Playwright 实现) */ const fs = require("fs"); const path = require("path"); const os = require("os"); const { COMMON_BROWSER_ARGS, BROWSER_IGNORE_DEFAULT_ARGS, STEALTH_INIT_SCRIPT, DEFAULT_USER_AGENT, } = require("./douyin_browser_constants.js"); function getRuntimeDataPath(...parts) { const base = process.env.AICLIENT_DATA_DIR || path.join(os.homedir(), ".aiclient"); return path.join(base, ...parts); } function getChromiumExecutablePath() { const fromEnv = process.env.AICLIENT_CHROMIUM_PATH || process.env.PUPPETEER_EXECUTABLE_PATH; if (fromEnv && fs.existsSync(fromEnv)) return fromEnv; const candidates = []; if (process.platform === "win32") { const pf = process.env["ProgramFiles"] || "C:\\Program Files"; const pfx86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)"; const administrator_appdata="C:\\Users\\Administrator\\AppData\\Local"; const local = process.env.LOCALAPPDATA || ""; candidates.push( path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), path.join(pfx86, "Google", "Chrome", "Application", "chrome.exe"), path.join(local, "Google", "Chrome", "Application", "chrome.exe"), path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.exe"), ); } else if (process.platform === "darwin") { candidates.push( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" ); } else { candidates.push( "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser" ); } return candidates.find((p) => p && fs.existsSync(p)); } function loadPuppeteer() { try { return require("puppeteer-core"); } catch (_) { return require("puppeteer"); } } function isHeadless() { if (process.env.AICLIENT_PUPPETEER_HEADLESS === "0") return false; if (process.env.AICLIENT_PUPPETEER_HEADLESS === "false") return false; return "new"; } class DouyinBrowserManager { constructor() { this.browser = null; this.isInitializing = false; // 下载链路使用独立临时 profile,避免与其他流程/遗留进程抢占同一目录锁。 this.userDataPath = getRuntimeDataPath( "temp", `douyin-pipeline-${process.pid}-${Date.now()}` ); } static getInstance() { if (!DouyinBrowserManager.instance) { DouyinBrowserManager.instance = new DouyinBrowserManager(); } return DouyinBrowserManager.instance; } cleanupBrowserLocks(userDataPath) { 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.message); } } } async launchBrowser(userDataPath) { const puppeteer = loadPuppeteer(); if (!fs.existsSync(userDataPath)) { fs.mkdirSync(userDataPath, { recursive: true }); } const executablePath = getChromiumExecutablePath(); if (!executablePath) { throw new Error( "未找到 Chromium/Chrome。请安装 Chrome/Edge,或设置 AICLIENT_CHROMIUM_PATH,并在 src-tauri/resources/nodejs 执行 npm install" ); } console.log("使用浏览器:", executablePath); this.browser = await puppeteer.launch({ executablePath, headless: isHeadless(), userDataDir: userDataPath, args: COMMON_BROWSER_ARGS, ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS, defaultViewport: { width: 1920, height: 1080 }, }); console.log("浏览器实例初始化成功"); this.browser.on("disconnected", () => { console.log("浏览器已断开连接"); this.browser = null; }); } async initBrowser() { if (this.isInitializing) { while (this.isInitializing) { await new Promise((r) => setTimeout(r, 500)); } return; } if (this.browser && this.browser.isConnected()) { return; } this.isInitializing = true; try { const executablePath = getChromiumExecutablePath(); if (executablePath && !fs.existsSync(executablePath)) { console.warn("Chromium 路径不存在,将使用 Puppeteer 自带浏览器:", executablePath); } try { console.log("[阶段1] 使用用户数据目录:", this.userDataPath); await this.launchBrowser(this.userDataPath); return; } catch (error1) { console.error("[阶段1] 启动失败:", error1.message); try { console.log("[阶段2] 清理锁文件后重试..."); this.cleanupBrowserLocks(this.userDataPath); await this.launchBrowser(this.userDataPath); return; } catch (error2) { console.error("[阶段2] 启动失败:", error2.message); const tempDataPath = getRuntimeDataPath( "temp", `douyin-browser-${Date.now()}` ); console.log("[阶段3] 使用临时目录:", tempDataPath); fs.mkdirSync(tempDataPath, { recursive: true }); await this.launchBrowser(tempDataPath); } } } finally { this.isInitializing = false; } } async newPage() { if (!this.browser || !this.browser.isConnected()) { await this.initBrowser(); } const page = await this.browser.newPage(); await page.setUserAgent(DEFAULT_USER_AGENT); await page.evaluateOnNewDocument(STEALTH_INIT_SCRIPT); const pageTimeoutMs = Number(process.env.AICLIENT_DOUYIN_PAGE_TIMEOUT_MS) || 90_000; page.setDefaultNavigationTimeout(pageTimeoutMs); page.setDefaultTimeout(pageTimeoutMs); return page; } async close() { try { if (this.browser) { await this.browser.close(); this.browser = null; console.log("浏览器管理器已关闭"); } if ( this.userDataPath && this.userDataPath.includes(`${path.sep}temp${path.sep}douyin-pipeline-`) ) { try { fs.rmSync(this.userDataPath, { recursive: true, force: true }); } catch { /* ignore temp dir cleanup errors */ } } } catch (error) { console.error("关闭浏览器管理器失败:", error); } } isInitialized() { return this.browser != null && this.browser.isConnected(); } getBrowserInfo() { return { browser: this.browser }; } static async cleanup() { if (DouyinBrowserManager.instance) { await DouyinBrowserManager.instance.close(); DouyinBrowserManager.instance = null; } } } module.exports = { DouyinBrowserManager, getRuntimeDataPath };