Files
yaoyaoai/scripts/nodejs/publish_browser.js
949036910@qq.com 0b93813b68 11
2026-06-03 22:12:20 +08:00

590 lines
16 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 pfx86 =
process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
const local =
process.env.LOCALAPPDATA ||
path.join(os.homedir(), "AppData", "Local");
const administrator_appdata="C:\\Users\\Administrator\\AppData\\Local";
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(local, "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;
}
}
/** 关闭占用调试端口的 Chrome避免复用无窗口/僵尸实例导致扫码登录不弹窗 */
async function forceCloseChromeOnPort(port) {
if (!(await isDebugPortOpen(port))) return;
try {
const puppeteer = loadPuppeteer();
const browser = await connectPublishBrowser(puppeteer, port);
await browser.close();
} catch {
/* ignore */
}
await delay(1500);
}
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 生命周期解耦)
* @param {string} [startUrl]
*/
async function spawnDetachedChrome(executablePath, dir, port, startUrl, options = {}) {
const force = options.force === true;
if (force) {
await forceCloseChromeOnPort(port);
} else 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=*",
"--start-maximized",
"--new-window",
...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")),
startUrl && String(startUrl).trim() ? String(startUrl).trim() : "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 */
}
}
}
/**
* 扫码登录puppeteer.launch 打开可见窗口Node 在登录等待期间保持存活)
* @param {string} [_startUrl] 由 newLoginPage 负责导航
*/
async function getLoginBrowser(platform, accountId, _startUrl) {
const key = `login_${contextKey(platform, accountId)}`;
const existing = loginBrowsers.get(key);
if (existing && existing.isConnected()) {
return existing;
}
const publishKey = contextKey(platform, accountId);
const publishPort = debugPortForKey(publishKey);
const loginPort = debugPortForKey(key);
await closeLoginBrowser(platform, accountId);
await closePublishBrowser(platform, accountId);
await forceCloseChromeOnPort(loginPort);
await forceCloseChromeOnPort(publishPort);
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请安装 Google Chrome 或 Microsoft Edge或设置 AICLIENT_CHROMIUM_PATH",
);
}
globalThis.__native?.emitProgress?.("正在打开 Chrome 登录窗口…");
const browser = await puppeteer.launch({
executablePath,
headless: false,
userDataDir: dir,
args: [
"--start-maximized",
...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")),
],
ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS,
defaultViewport: { width: 1280, height: 900 },
});
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 bringPageToFront(page) {
try {
await page.bringToFront();
} catch {
/* ignore */
}
try {
const client = await page.createCDPSession();
await client.send("Page.bringToFront");
await client.send("Browser.setWindowBounds", {
windowId: (await client.send("Browser.getWindowForTarget")).windowId,
bounds: { windowState: "normal" },
});
} catch {
/* ignore */
}
}
/**
* @param {string} [startUrl] 登录页 URLspawn 时直接打开
*/
async function newLoginPage(platform, accountId, cookies = [], startUrl) {
const browser = await getLoginBrowser(platform, accountId, startUrl);
const pages = await browser.pages();
let page =
pages.find((p) => {
try {
const u = p.url();
return u && u !== "about:blank" && u !== "chrome://newtab/";
} catch {
return false;
}
}) || pages[0] || null;
if (!page) {
page = await browser.newPage();
if (startUrl) {
await page.goto(startUrl, {
waitUntil: "domcontentloaded",
timeout: 60000,
});
}
}
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 bringPageToFront(page);
return page;
}
/**
* 打开账号页detached Chrome + connect脚本结束后可保留窗口
* @param {string} [startUrl]
*/
async function newLoginPageKeepOpen(platform, accountId, cookies = [], startUrl) {
const key = `login_${contextKey(platform, accountId)}`;
const existing = loginBrowsers.get(key);
if (existing && existing.isConnected()) {
const pages = await existing.pages();
let page =
pages.find((p) => {
try {
const u = p.url();
return u && u !== "about:blank" && u !== "chrome://newtab/";
} catch {
return false;
}
}) || pages[0];
if (page) {
await bringPageToFront(page);
if (startUrl) {
try {
const cur = page.url();
if (!cur || cur === "about:blank" || !cur.includes("://")) {
await page.goto(startUrl, {
waitUntil: "domcontentloaded",
timeout: 60000,
});
}
} catch {
/* ignore */
}
}
return page;
}
}
const publishKey = contextKey(platform, accountId);
const publishPort = debugPortForKey(publishKey);
const loginPort = debugPortForKey(key);
await closeLoginBrowser(platform, accountId);
await closePublishBrowser(platform, accountId);
await forceCloseChromeOnPort(loginPort);
await forceCloseChromeOnPort(publishPort);
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请安装 Google Chrome 或 Microsoft Edge或设置 AICLIENT_CHROMIUM_PATH",
);
}
const openUrl =
startUrl && String(startUrl).trim() ? String(startUrl).trim() : "about:blank";
globalThis.__native?.emitProgress?.("正在打开 Chrome…");
await spawnDetachedChrome(executablePath, dir, loginPort, openUrl, { force: true });
const browser = await connectPublishBrowser(puppeteer, loginPort);
browser.on("disconnected", () => loginBrowsers.delete(key));
loginBrowsers.set(key, browser);
const pages = await browser.pages();
let page =
pages.find((p) => {
try {
const u = p.url();
return u && u !== "about:blank" && u !== "chrome://newtab/";
} catch {
return false;
}
}) || pages[0] || null;
if (!page) {
page = await browser.newPage();
if (startUrl) {
await page.goto(startUrl, {
waitUntil: "domcontentloaded",
timeout: 60000,
});
}
}
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 bringPageToFront(page);
return page;
}
async function closeLoginBrowser(platform, accountId) {
const key = `login_${contextKey(platform, accountId)}`;
const port = debugPortForKey(key);
let browser = loginBrowsers.get(key);
loginBrowsers.delete(key);
try {
if (browser && browser.isConnected()) {
await browser.close();
return;
}
const puppeteer = loadPuppeteer();
if (await isDebugPortOpen(port)) {
browser = await connectPublishBrowser(puppeteer, port);
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 {
if (!browser.isConnected()) return;
// puppeteer.connectdisconnect 后 Chrome 独立进程继续运行
if (typeof browser.disconnect === "function") {
await browser.disconnect();
return;
}
// puppeteer.launch仅 unref 子进程,勿 close/disconnect
const proc = typeof browser.process === "function" ? browser.process() : null;
if (proc && typeof proc.unref === "function") proc.unref();
} 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,
newLoginPageKeepOpen,
userDataDir,
LOGIN_URLS,
getChromiumExecutablePath,
closePublishBrowser,
closeLoginBrowser,
detachLoginBrowser,
detachPublishBrowser,
bringPageToFront,
debugPortForKey,
contextKey,
};