11
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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] 登录页 URL,spawn 时直接打开
|
||||||
|
*/
|
||||||
|
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 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user