11
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
|
||||
/**
|
||||
* 发布用 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,
|
||||
@@ -55,33 +59,115 @@ function userDataDir(platform, accountId) {
|
||||
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);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const port = debugPortForKey(key);
|
||||
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,
|
||||
ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS,
|
||||
defaultViewport: { width: 1280, height: 900 },
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -112,7 +198,8 @@ async function newPublishPage(platform, accountId, cookies) {
|
||||
let page =
|
||||
pages.find((p) => {
|
||||
try {
|
||||
return p.url() === "about:blank";
|
||||
const u = p.url();
|
||||
return !u || u === "about:blank";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -137,16 +224,37 @@ async function newPublishPage(platform, accountId, cookies) {
|
||||
|
||||
async function closePublishBrowser(platform, accountId) {
|
||||
const key = contextKey(platform, accountId);
|
||||
const browser = browsers.get(key);
|
||||
if (!browser) return;
|
||||
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/",
|
||||
@@ -162,4 +270,7 @@ module.exports = {
|
||||
LOGIN_URLS,
|
||||
getChromiumExecutablePath,
|
||||
closePublishBrowser,
|
||||
detachPublishBrowser,
|
||||
debugPortForKey,
|
||||
contextKey,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user