Files
yaoyaoai/scripts/nodejs/douyin_browser_manager.js
949036910@qq.com 7d65e3e602 11
2026-06-03 21:43:46 +08:00

236 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };