diff --git a/scripts/nodejs/douyin_browser_manager.js b/scripts/nodejs/douyin_browser_manager.js index 2f5da79..590f2ce 100644 --- a/scripts/nodejs/douyin_browser_manager.js +++ b/scripts/nodejs/douyin_browser_manager.js @@ -41,7 +41,7 @@ function getChromiumExecutablePath() { path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"), path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), - path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.exe"), + // ); } else if (process.platform === "darwin") { candidates.push( diff --git a/scripts/nodejs/publish_browser.js b/scripts/nodejs/publish_browser.js index 37677b9..1d45a12 100644 --- a/scripts/nodejs/publish_browser.js +++ b/scripts/nodejs/publish_browser.js @@ -19,6 +19,8 @@ const { } = require("./douyin_browser_constants.js"); const browsers = new Map(); +/** 扫码登录 / 打开账号:需可见窗口,使用 puppeteer.launch(Node 等待登录期间保持存活) */ +const loginBrowsers = new Map(); function getRuntimeDataPath(...parts) { const base = process.env.AICLIENT_DATA_DIR || path.join(os.homedir(), ".aiclient"); @@ -38,7 +40,7 @@ function getChromiumExecutablePath() { 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"), + // path.join(administrator_appdata, "Google", "Chrome", "bin","chrome.exe"), ); } return candidates.find((p) => p && fs.existsSync(p)); @@ -145,6 +147,61 @@ async function connectPublishBrowser(puppeteer, port) { }); } +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); @@ -194,6 +251,57 @@ async function closeBlankPages(browser, keepPage) { } } +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(); @@ -267,11 +375,15 @@ const LOGIN_URLS = { module.exports = { delay, getPublishBrowser, + getLoginBrowser, newPublishPage, + newLoginPage, userDataDir, LOGIN_URLS, getChromiumExecutablePath, closePublishBrowser, + closeLoginBrowser, + detachLoginBrowser, detachPublishBrowser, debugPortForKey, contextKey, diff --git a/scripts/nodejs/publish_login.js b/scripts/nodejs/publish_login.js index b6c5525..3389e81 100644 --- a/scripts/nodejs/publish_login.js +++ b/scripts/nodejs/publish_login.js @@ -2,10 +2,11 @@ const { delay, - newPublishPage, + newLoginPage, LOGIN_URLS, - closePublishBrowser, + closeLoginBrowser, } = require("./publish_browser.js"); + const DOUYIN_HOME_URL = "https://creator.douyin.com/creator-micro/home"; const LOGIN_SELECTORS = { @@ -104,21 +105,48 @@ async function resolveDouyinLabel(page) { globalThis.__nodejsMain = async function main(params) { const p = params || {}; const platform = String(p.platform || "").trim(); - const accountId = p.accountId || null; + const accountId = p.accountId ?? null; const loginUrl = LOGIN_URLS[platform]; if (!loginUrl) { return { success: false, error: `不支持的平台: ${platform}` }; } - globalThis.__native?.emitProgress?.("正在打开登录页,请在浏览器中完成登录…"); - const page = await newPublishPage(platform, accountId, []); + let page = null; + globalThis.__native?.emitProgress?.("正在启动浏览器,请扫码登录…"); + try { - await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60000 }); + page = await newLoginPage(platform, accountId, []); + + globalThis.__native?.emitProgress?.("正在打开登录页…"); + try { + await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60000 }); + } catch (e) { + globalThis.__native?.log?.( + "warn", + `登录页加载超时,继续等待扫码: ${e.message}`, + null, + ); + } await delay(3000); + try { + await page.bringToFront(); + } catch { + /* ignore */ + } + + globalThis.__native?.emitProgress?.( + "请在弹出的浏览器窗口中扫码登录(最长等待 5 分钟)", + ); const maxMs = 300000; const start = Date.now(); + let lastLog = 0; + while (Date.now() - start < maxMs) { + if (page.isClosed()) { + return { success: false, error: "浏览器窗口已关闭,登录已取消" }; + } + if (await checkLoggedIn(page, platform)) { const cookies = await page.cookies(); const resolvedUid = await resolveUid(page); @@ -155,23 +183,32 @@ globalThis.__nodejsMain = async function main(params) { cookies, }; } - await delay(2000); - globalThis.__native?.emitProgress?.("等待登录完成…"); + + const elapsed = Date.now() - start; + if (elapsed - lastLog >= 15000) { + globalThis.__native?.emitProgress?.( + `等待扫码登录… ${Math.round(elapsed / 1000)} 秒`, + ); + lastLog = elapsed; + } + await delay(3000); } + return { success: false, error: "登录超时(5 分钟),请重试", }; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { success: false, error: msg || "启动登录浏览器失败" }; } finally { - try { - await page.close(); - } catch { - /* ignore */ - } - try { - await closePublishBrowser(platform, accountId); - } catch { - /* ignore */ + if (page) { + try { + await page.close(); + } catch { + /* ignore */ + } } + await closeLoginBrowser(platform, accountId); } }; diff --git a/scripts/nodejs/publish_open.js b/scripts/nodejs/publish_open.js index 006c6cb..6b8d598 100644 --- a/scripts/nodejs/publish_open.js +++ b/scripts/nodejs/publish_open.js @@ -1,6 +1,10 @@ "use strict"; -const { newPublishPage, LOGIN_URLS } = require("./publish_browser.js"); +const { + newLoginPage, + detachLoginBrowser, + LOGIN_URLS, +} = require("./publish_browser.js"); const OPEN_URLS = { douyin: "https://creator.douyin.com/creator-micro/content/upload", @@ -20,8 +24,14 @@ globalThis.__nodejsMain = async function main(params) { return { success: false, message: `不支持的平台: ${platform}` }; } - const page = await newPublishPage(platform, accountId, cookies); + const page = await newLoginPage(platform, accountId, cookies); await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60000 }); + try { + await page.bringToFront(); + } catch { + /* ignore */ + } + await detachLoginBrowser(platform, accountId); return { success: true,