This commit is contained in:
949036910@qq.com
2026-06-03 21:43:46 +08:00
parent bbdee469fc
commit 7d65e3e602
4 changed files with 178 additions and 38 deletions

View File

@@ -41,7 +41,7 @@ function getChromiumExecutablePath() {
path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"),
path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"),
path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), path.join(pf, "Google", "Chrome", "Application", "chrome.exe"),
// path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.exe"),
); );
} else if (process.platform === "darwin") { } else if (process.platform === "darwin") {
candidates.push( candidates.push(

View File

@@ -34,13 +34,20 @@ function getChromiumExecutablePath() {
const candidates = []; const candidates = [];
if (process.platform === "win32") { if (process.platform === "win32") {
const pf = process.env.ProgramFiles || "C:\\Program Files"; const pf = process.env.ProgramFiles || "C:\\Program Files";
const local = process.env.LOCALAPPDATA || ""; 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"; const administrator_appdata="C:\\Users\\Administrator\\AppData\\Local";
candidates.push( candidates.push(
path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), 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(local, "Google", "Chrome", "Application", "chrome.exe"),
path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"),
// path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.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)); return candidates.find((p) => p && fs.existsSync(p));
@@ -107,6 +114,19 @@ async function isDebugPortOpen(port) {
} }
} }
/** 关闭占用调试端口的 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) { async function waitForDebugPort(port, timeoutMs = 25000) {
const start = Date.now(); const start = Date.now();
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
@@ -118,17 +138,25 @@ async function waitForDebugPort(port, timeoutMs = 25000) {
/** /**
* 独立进程启动 Chrome与 Node 生命周期解耦) * 独立进程启动 Chrome与 Node 生命周期解耦)
* @param {string} [startUrl]
*/ */
async function spawnDetachedChrome(executablePath, dir, port) { async function spawnDetachedChrome(executablePath, dir, port, startUrl, options = {}) {
if (await isDebugPortOpen(port)) return; 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 }); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const args = [ const args = [
`--remote-debugging-port=${port}`, `--remote-debugging-port=${port}`,
`--user-data-dir=${dir}`, `--user-data-dir=${dir}`,
"--remote-allow-origins=*", "--remote-allow-origins=*",
"--start-maximized",
"--new-window",
...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")), ...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")),
"about:blank", startUrl && String(startUrl).trim() ? String(startUrl).trim() : "about:blank",
]; ];
const child = spawn(executablePath, args, { const child = spawn(executablePath, args, {
@@ -164,15 +192,24 @@ function cleanupProfileLocks(dir) {
} }
/** /**
* 扫码登录专用:可见 Chrome对齐 Electron launchPersistentContext headless:false * 扫码登录puppeteer.launch 打开可见窗口Node 在登录等待期间保持存活
* @param {string} [_startUrl] 由 newLoginPage 负责导航
*/ */
async function getLoginBrowser(platform, accountId) { async function getLoginBrowser(platform, accountId, _startUrl) {
const key = `login_${contextKey(platform, accountId)}`; const key = `login_${contextKey(platform, accountId)}`;
const existing = loginBrowsers.get(key); const existing = loginBrowsers.get(key);
if (existing && existing.isConnected()) { if (existing && existing.isConnected()) {
return existing; 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 puppeteer = loadPuppeteer();
const dir = userDataDir(platform, accountId); const dir = userDataDir(platform, accountId);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -181,20 +218,22 @@ async function getLoginBrowser(platform, accountId) {
const executablePath = getChromiumExecutablePath(); const executablePath = getChromiumExecutablePath();
if (!executablePath) { if (!executablePath) {
throw new Error( throw new Error(
"未找到 Chrome/Edge请安装浏览器或设置 AICLIENT_CHROMIUM_PATH", "未找到 Chrome/Edge请安装 Google Chrome 或 Microsoft Edge或设置 AICLIENT_CHROMIUM_PATH",
); );
} }
globalThis.__native?.emitProgress?.("正在打开 Chrome 登录窗口…");
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
executablePath, executablePath,
headless: false, headless: false,
userDataDir: dir, userDataDir: dir,
args: [...COMMON_BROWSER_ARGS, "--start-maximized"], args: [
"--start-maximized",
...COMMON_BROWSER_ARGS.filter((a) => !a.startsWith("--user-data-dir")),
],
ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS, ignoreDefaultArgs: BROWSER_IGNORE_DEFAULT_ARGS,
defaultViewport: { width: 1280, height: 900 }, defaultViewport: { width: 1280, height: 900 },
handleSIGINT: false,
handleSIGTERM: false,
handleSIGHUP: false,
}); });
browser.on("disconnected", () => loginBrowsers.delete(key)); browser.on("disconnected", () => loginBrowsers.delete(key));
@@ -251,13 +290,56 @@ async function closeBlankPages(browser, keepPage) {
} }
} }
async function newLoginPage(platform, accountId, cookies = []) { async function bringPageToFront(page) {
const browser = await getLoginBrowser(platform, accountId); try {
const page = await browser.newPage(); 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.setUserAgent(DEFAULT_USER_AGENT);
await page.evaluateOnNewDocument(STEALTH_INIT_SCRIPT); await page.evaluateOnNewDocument(STEALTH_INIT_SCRIPT);
page.setDefaultNavigationTimeout(60000); page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000); page.setDefaultTimeout(60000);
if (Array.isArray(cookies) && cookies.length) { if (Array.isArray(cookies) && cookies.length) {
try { try {
await page.setCookie(...cookies); await page.setCookie(...cookies);
@@ -265,21 +347,26 @@ async function newLoginPage(platform, accountId, cookies = []) {
console.warn("setCookie 失败:", e.message); console.warn("setCookie 失败:", e.message);
} }
} }
try {
await page.bringToFront(); await bringPageToFront(page);
} catch {
/* ignore */
}
return page; return page;
} }
async function closeLoginBrowser(platform, accountId) { async function closeLoginBrowser(platform, accountId) {
const key = `login_${contextKey(platform, accountId)}`; const key = `login_${contextKey(platform, accountId)}`;
const browser = loginBrowsers.get(key); const port = debugPortForKey(key);
if (!browser) return; let browser = loginBrowsers.get(key);
loginBrowsers.delete(key); loginBrowsers.delete(key);
try { try {
if (browser && browser.isConnected()) {
await browser.close(); await browser.close();
return;
}
const puppeteer = loadPuppeteer();
if (await isDebugPortOpen(port)) {
browser = await connectPublishBrowser(puppeteer, port);
await browser.close();
}
} catch { } catch {
/* ignore */ /* ignore */
} }

View File

@@ -1,12 +1,29 @@
"use strict"; "use strict";
const fs = require("fs");
const path = require("path");
const os = require("os");
const { const {
delay, delay,
newLoginPage, newLoginPage,
LOGIN_URLS, LOGIN_URLS,
closeLoginBrowser, closeLoginBrowser,
getChromiumExecutablePath,
userDataDir,
} = require("./publish_browser.js"); } = require("./publish_browser.js");
function appendLoginDebug(line) {
try {
const base = process.env.AICLIENT_DATA_DIR || path.join(os.homedir(), ".aiclient");
const file = path.join(base, "publish-login-debug.log");
fs.mkdirSync(base, { recursive: true });
fs.appendFileSync(file, `${new Date().toISOString()} ${line}\n`, "utf8");
} catch {
/* ignore */
}
}
const DOUYIN_HOME_URL = "https://creator.douyin.com/creator-micro/home"; const DOUYIN_HOME_URL = "https://creator.douyin.com/creator-micro/home";
const LOGIN_SELECTORS = { const LOGIN_SELECTORS = {
@@ -113,13 +130,51 @@ globalThis.__nodejsMain = async function main(params) {
let page = null; let page = null;
globalThis.__native?.emitProgress?.("正在启动浏览器,请扫码登录…"); globalThis.__native?.emitProgress?.("正在启动浏览器,请扫码登录…");
appendLoginDebug(`login start platform=${platform} accountId=${accountId}`);
try { try {
page = await newLoginPage(platform, accountId, []); const chromePath = getChromiumExecutablePath();
if (!chromePath) {
const err = "未找到 Chrome/Edge请安装浏览器或设置 AICLIENT_CHROMIUM_PATH";
appendLoginDebug(`error: ${err}`);
return { success: false, error: err };
}
globalThis.__native?.log?.(
"info",
"启动扫码登录浏览器",
{ chromePath, profileDir: userDataDir(platform, accountId) },
);
globalThis.__native?.emitProgress?.("正在启动 Chrome 登录窗口…");
appendLoginDebug(`chrome=${chromePath} profile=${userDataDir(platform, accountId)}`);
page = await newLoginPage(platform, accountId, [], loginUrl);
try {
appendLoginDebug(`page opened url=${page.url()}`);
} catch {
appendLoginDebug("page opened");
}
globalThis.__native?.emitProgress?.("正在打开登录页…"); globalThis.__native?.emitProgress?.("正在打开登录页…");
try { try {
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60000 }); const cur = page.url();
const host = (() => {
try {
return new URL(loginUrl).hostname;
} catch {
return "";
}
})();
if (
!cur ||
cur === "about:blank" ||
cur === "chrome://newtab/" ||
(host && !cur.includes(host))
) {
await page.goto(loginUrl, {
waitUntil: "domcontentloaded",
timeout: 60000,
});
}
} catch (e) { } catch (e) {
globalThis.__native?.log?.( globalThis.__native?.log?.(
"warn", "warn",
@@ -128,11 +183,6 @@ globalThis.__nodejsMain = async function main(params) {
); );
} }
await delay(3000); await delay(3000);
try {
await page.bringToFront();
} catch {
/* ignore */
}
globalThis.__native?.emitProgress?.( globalThis.__native?.emitProgress?.(
"请在弹出的浏览器窗口中扫码登录(最长等待 5 分钟)", "请在弹出的浏览器窗口中扫码登录(最长等待 5 分钟)",
@@ -148,6 +198,8 @@ globalThis.__nodejsMain = async function main(params) {
} }
if (await checkLoggedIn(page, platform)) { if (await checkLoggedIn(page, platform)) {
appendLoginDebug("detected logged in");
globalThis.__native?.emitProgress?.("检测到已登录,正在保存账号信息…");
const cookies = await page.cookies(); const cookies = await page.cookies();
const resolvedUid = await resolveUid(page); const resolvedUid = await resolveUid(page);
let douyinId = ""; let douyinId = "";
@@ -200,6 +252,8 @@ globalThis.__nodejsMain = async function main(params) {
}; };
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
appendLoginDebug(`exception: ${msg}`);
globalThis.__native?.emitProgress?.(`登录失败:${msg}`);
return { success: false, error: msg || "启动登录浏览器失败" }; return { success: false, error: msg || "启动登录浏览器失败" };
} finally { } finally {
if (page) { if (page) {

View File

@@ -24,12 +24,11 @@ globalThis.__nodejsMain = async function main(params) {
return { success: false, message: `不支持的平台: ${platform}` }; return { success: false, message: `不支持的平台: ${platform}` };
} }
const page = await newLoginPage(platform, accountId, cookies); globalThis.__native?.emitProgress?.("正在打开浏览器…");
const page = await newLoginPage(platform, accountId, cookies, targetUrl);
const cur = page.url();
if (!cur || cur === "about:blank" || !cur.includes("://")) {
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60000 }); await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
try {
await page.bringToFront();
} catch {
/* ignore */
} }
await detachLoginBrowser(platform, accountId); await detachLoginBrowser(platform, accountId);