217 lines
6.3 KiB
JavaScript
217 lines
6.3 KiB
JavaScript
"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 };
|