"use strict"; /** * 发布用 Puppeteer 浏览器(按平台/账号隔离 userDataDir) * * Windows:Chrome 必须以 detached 子进程启动,否则 Node 被 Tauri 结束后会连带关掉浏览器。 */ const fs = require("fs"); const path = require("path"); const os = require("os"); const http = require("http"); const { spawn } = require("child_process"); const { COMMON_BROWSER_ARGS, BROWSER_IGNORE_DEFAULT_ARGS, STEALTH_INIT_SCRIPT, DEFAULT_USER_AGENT, } = require("./douyin_browser_constants.js"); const browsers = new Map(); 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 local = process.env.LOCALAPPDATA || ""; const administrator_appdata="C:\\Users\\Administrator\\AppData\\Local"; candidates.push( path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), path.join(local, "Google", "Chrome", "Application", "chrome.exe"), path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.exe"), ); } return candidates.find((p) => p && fs.existsSync(p)); } function loadPuppeteer() { try { return require("puppeteer-core"); } catch { return require("puppeteer"); } } function contextKey(platform, accountId) { return accountId ? `publish_${platform}_${accountId}` : `publish_${platform}`; } function userDataDir(platform, accountId) { const sub = accountId ? `${platform}_${accountId}` : platform; return getRuntimeDataPath("browser-data", "publish", sub); } function debugPortForKey(key) { let h = 5381; for (let i = 0; i < key.length; i++) { h = ((h << 5) + h + key.charCodeAt(i)) >>> 0; } return 9300 + (h % 700); } function delay(ms) { return new Promise((r) => setTimeout(r, ms)); } function fetchDebugVersion(port) { return new Promise((resolve, reject) => { const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => { let body = ""; res.on("data", (chunk) => { body += chunk; }); res.on("end", () => { try { resolve(JSON.parse(body)); } catch (e) { reject(e); } }); }); req.on("error", reject); req.setTimeout(2500, () => { req.destroy(); reject(new Error("timeout")); }); }); } async function isDebugPortOpen(port) { try { await fetchDebugVersion(port); return true; } catch { return false; } } async function waitForDebugPort(port, timeoutMs = 25000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (await isDebugPortOpen(port)) return; await delay(400); } throw new Error(`Chrome 调试端口 ${port} 未就绪,请检查是否已安装 Chrome/Edge`); } /** * 独立进程启动 Chrome(与 Node 生命周期解耦) */ async function spawnDetachedChrome(executablePath, dir, port) { if (await isDebugPortOpen(port)) return; if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const args = [ `--remote-debugging-port=${port}`, `--user-data-dir=${dir}`, "--remote-allow-origins=*", ...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")), "about:blank", ]; const child = spawn(executablePath, args, { detached: true, stdio: "ignore", windowsHide: false, }); child.unref(); await waitForDebugPort(port); } async function connectPublishBrowser(puppeteer, port) { return puppeteer.connect({ browserURL: `http://127.0.0.1:${port}`, defaultViewport: { width: 1280, height: 900 }, }); } async function getPublishBrowser(platform, accountId) { const key = contextKey(platform, accountId); const existing = browsers.get(key); if (existing && existing.isConnected()) { return existing; } const puppeteer = loadPuppeteer(); const dir = userDataDir(platform, accountId); const port = debugPortForKey(key); const executablePath = getChromiumExecutablePath(); if (!executablePath) { throw new Error( "未找到 Chrome/Edge,请安装浏览器或设置 AICLIENT_CHROMIUM_PATH", ); } let browser; if (await isDebugPortOpen(port)) { browser = await connectPublishBrowser(puppeteer, port); } else { await spawnDetachedChrome(executablePath, dir, port); browser = await connectPublishBrowser(puppeteer, port); } browser.on("disconnected", () => browsers.delete(key)); browsers.set(key, browser); return browser; } async function closeBlankPages(browser, keepPage) { try { const pages = await browser.pages(); for (const p of pages) { if (keepPage && p === keepPage) continue; const u = p.url(); if (!u || u === "about:blank") { try { await p.close(); } catch { /* ignore */ } } } } catch { /* ignore */ } } async function newPublishPage(platform, accountId, cookies) { const browser = await getPublishBrowser(platform, accountId); const pages = await browser.pages(); let page = pages.find((p) => { try { const u = p.url(); return !u || u === "about:blank"; } catch { return false; } }) || null; if (!page) { page = await browser.newPage(); } await page.setUserAgent(DEFAULT_USER_AGENT); await page.evaluateOnNewDocument(STEALTH_INIT_SCRIPT); page.setDefaultNavigationTimeout(60000); page.setDefaultTimeout(60000); if (Array.isArray(cookies) && cookies.length) { try { await page.setCookie(...cookies); } catch (e) { console.warn("setCookie 失败:", e.message); } } await closeBlankPages(browser, page); return page; } async function closePublishBrowser(platform, accountId) { const key = contextKey(platform, accountId); const port = debugPortForKey(key); let browser = browsers.get(key); browsers.delete(key); try { const puppeteer = loadPuppeteer(); if (!browser || !browser.isConnected()) { if (!(await isDebugPortOpen(port))) return; browser = await connectPublishBrowser(puppeteer, port); } await browser.close(); } catch { /* ignore */ } } /** * 断开 Puppeteer 连接,Chrome 以 detached 进程继续运行 */ async function detachPublishBrowser(platform, accountId) { const key = contextKey(platform, accountId); const browser = browsers.get(key); browsers.delete(key); try { if (browser && browser.isConnected()) { await browser.disconnect(); } } catch (e) { console.warn("detachPublishBrowser:", e.message); } } const LOGIN_URLS = { douyin: "https://creator.douyin.com/", kuaishou: "https://cp.kuaishou.com/", shipin: "https://channels.weixin.qq.com/", xiaohongshu: "https://creator.xiaohongshu.com/", }; module.exports = { delay, getPublishBrowser, newPublishPage, userDataDir, LOGIN_URLS, getChromiumExecutablePath, closePublishBrowser, detachPublishBrowser, debugPortForKey, contextKey, };