This commit is contained in:
fengchuanhn@gmail.com
2026-05-17 12:54:13 +08:00
parent 1b8ca10ec9
commit 3db327d93b
38 changed files with 2820 additions and 14 deletions

View File

@@ -0,0 +1,216 @@
"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 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")
);
} 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;
this.userDataPath = getRuntimeDataPath("browser-data", "douyin");
}
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);
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000);
return page;
}
async close() {
try {
if (this.browser) {
await this.browser.close();
this.browser = null;
console.log("浏览器管理器已关闭");
}
} 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 };