Files
yaoyaoai/scripts/nodejs/publish_browser.js
949036910@qq.com bbdee469fc 1
2026-06-03 21:21:42 +08:00

391 lines
10 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 浏览器(按平台/账号隔离 userDataDir
*
* WindowsChrome 必须以 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();
/** 扫码登录 / 打开账号:需可见窗口,使用 puppeteer.launchNode 等待登录期间保持存活) */
const loginBrowsers = 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 },
});
}
function cleanupProfileLocks(dir) {
for (const name of [
"SingletonLock",
"SingletonSocket",
"SingletonCookie",
"Lock",
]) {
try {
const p = path.join(dir, name);
if (fs.existsSync(p)) fs.unlinkSync(p);
} catch {
/* ignore */
}
}
}
/**
* 扫码登录专用:可见 Chrome对齐 Electron launchPersistentContext headless:false
*/
async function getLoginBrowser(platform, accountId) {
const key = `login_${contextKey(platform, accountId)}`;
const existing = loginBrowsers.get(key);
if (existing && existing.isConnected()) {
return existing;
}
const puppeteer = loadPuppeteer();
const dir = userDataDir(platform, accountId);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
cleanupProfileLocks(dir);
const executablePath = getChromiumExecutablePath();
if (!executablePath) {
throw new Error(
"未找到 Chrome/Edge请安装浏览器或设置 AICLIENT_CHROMIUM_PATH",
);
}
const browser = await puppeteer.launch({
executablePath,
headless: false,
userDataDir: dir,
args: [...COMMON_BROWSER_ARGS, "--start-maximized"],
ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS,
defaultViewport: { width: 1280, height: 900 },
handleSIGINT: false,
handleSIGTERM: false,
handleSIGHUP: false,
});
browser.on("disconnected", () => loginBrowsers.delete(key));
loginBrowsers.set(key, browser);
return browser;
}
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 newLoginPage(platform, accountId, cookies = []) {
const browser = await getLoginBrowser(platform, accountId);
const 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);
}
}
try {
await page.bringToFront();
} catch {
/* ignore */
}
return page;
}
async function closeLoginBrowser(platform, accountId) {
const key = `login_${contextKey(platform, accountId)}`;
const browser = loginBrowsers.get(key);
if (!browser) return;
loginBrowsers.delete(key);
try {
await browser.close();
} catch {
/* ignore */
}
}
/**
* 打开账号页后保留窗口disconnect避免 Node 退出时关窗)
*/
async function detachLoginBrowser(platform, accountId) {
const key = `login_${contextKey(platform, accountId)}`;
const browser = loginBrowsers.get(key);
if (!browser) return;
loginBrowsers.delete(key);
try {
const proc = typeof browser.process === "function" ? browser.process() : null;
if (proc && typeof proc.unref === "function") proc.unref();
if (browser.isConnected()) await browser.disconnect();
} catch (e) {
console.warn("detachLoginBrowser:", e.message);
}
}
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,
getLoginBrowser,
newPublishPage,
newLoginPage,
userDataDir,
LOGIN_URLS,
getChromiumExecutablePath,
closePublishBrowser,
closeLoginBrowser,
detachLoginBrowser,
detachPublishBrowser,
debugPortForKey,
contextKey,
};