269 lines
7.5 KiB
JavaScript
269 lines
7.5 KiB
JavaScript
"use strict";
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const os = require("os");
|
||
|
||
const {
|
||
delay,
|
||
newLoginPage,
|
||
LOGIN_URLS,
|
||
closeLoginBrowser,
|
||
getChromiumExecutablePath,
|
||
userDataDir,
|
||
} = 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 LOGIN_SELECTORS = {
|
||
douyin: ["#header-avatar", ".creator-avatar", 'input[type="file"]'],
|
||
kuaishou: ['[class*="avatar"]', ".user-avatar", '[class*="upload"]'],
|
||
shipin: ['[class*="avatar"]', ".avatar"],
|
||
xiaohongshu: ['[class*="avatar"]', ".user-avatar", '[class*="upload"]'],
|
||
};
|
||
|
||
const URL_EXCLUDE = {
|
||
douyin: ["login", "passport"],
|
||
kuaishou: ["login", "passport"],
|
||
shipin: ["login", "auth", "qr"],
|
||
xiaohongshu: ["login", "passport", "account"],
|
||
};
|
||
|
||
async function checkLoggedIn(page, platform) {
|
||
const url = page.url();
|
||
const excludes = URL_EXCLUDE[platform] || URL_EXCLUDE.douyin;
|
||
if (excludes.some((k) => url.includes(k))) return false;
|
||
const sels = LOGIN_SELECTORS[platform] || LOGIN_SELECTORS.douyin;
|
||
for (const sel of sels) {
|
||
const el = await page.$(sel);
|
||
if (el) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function resolveUid(page) {
|
||
try {
|
||
const uidFromDom = await page.evaluate(() => {
|
||
const node = document.querySelector("[data-user-id], [data-uid]");
|
||
return node?.getAttribute("data-user-id") || node?.getAttribute("data-uid") || "";
|
||
});
|
||
if (uidFromDom) return String(uidFromDom);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
const local = await page.evaluate(() => {
|
||
const keys = Object.keys(localStorage || {});
|
||
for (const k of keys) {
|
||
if (/(uid|user.?id|sec_uid|openid)/i.test(k)) {
|
||
const v = localStorage.getItem(k);
|
||
if (v) return v;
|
||
}
|
||
}
|
||
return "";
|
||
});
|
||
if (local) return String(local).slice(0, 128);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return "";
|
||
}
|
||
|
||
async function resolveDouyinLabel(page) {
|
||
try {
|
||
await page.goto(DOUYIN_HOME_URL, {
|
||
waitUntil: "domcontentloaded",
|
||
timeout: 60000,
|
||
});
|
||
await delay(2000);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
try {
|
||
const text = await page.$eval(
|
||
'[class^="unique_id-"], [class*=" unique_id-"]',
|
||
(el) => (el.textContent || "").trim(),
|
||
);
|
||
const m = String(text).match(/抖音号[::]\s*([a-zA-Z0-9._-]+)/);
|
||
if (m?.[1]) return m[1];
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
try {
|
||
const fallback = await page.evaluate(() => {
|
||
const all = Array.from(document.querySelectorAll("div,span,p"));
|
||
const node = all.find((el) =>
|
||
/抖音号[::]/.test((el.textContent || "").trim()),
|
||
);
|
||
return (node?.textContent || "").trim();
|
||
});
|
||
const m = String(fallback).match(/抖音号[::]\s*([a-zA-Z0-9._-]+)/);
|
||
if (m?.[1]) return m[1];
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
globalThis.__nodejsMain = async function main(params) {
|
||
const p = params || {};
|
||
const platform = String(p.platform || "").trim();
|
||
const accountId = p.accountId ?? null;
|
||
const loginUrl = LOGIN_URLS[platform];
|
||
if (!loginUrl) {
|
||
return { success: false, error: `不支持的平台: ${platform}` };
|
||
}
|
||
|
||
let page = null;
|
||
globalThis.__native?.emitProgress?.("正在启动浏览器,请扫码登录…");
|
||
appendLoginDebug(`login start platform=${platform} accountId=${accountId}`);
|
||
|
||
try {
|
||
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?.("正在打开登录页…");
|
||
try {
|
||
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) {
|
||
globalThis.__native?.log?.(
|
||
"warn",
|
||
`登录页加载超时,继续等待扫码: ${e.message}`,
|
||
null,
|
||
);
|
||
}
|
||
await delay(3000);
|
||
|
||
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)) {
|
||
appendLoginDebug("detected logged in");
|
||
globalThis.__native?.emitProgress?.("检测到已登录,正在保存账号信息…");
|
||
const cookies = await page.cookies();
|
||
const resolvedUid = await resolveUid(page);
|
||
let douyinId = "";
|
||
let nickname = "";
|
||
try {
|
||
nickname = await page.$eval(
|
||
".username, .account-name, .user-name, .nickname",
|
||
(el) => (el.textContent || "").trim(),
|
||
);
|
||
} catch {
|
||
nickname = "已登录用户";
|
||
}
|
||
let avatar = "";
|
||
try {
|
||
avatar =
|
||
(await page.$eval(
|
||
"img.avatar, img[class*='avatar'], .avatar img",
|
||
(el) => el?.src || "",
|
||
)) || "";
|
||
} catch {
|
||
avatar = "";
|
||
}
|
||
if (platform === "douyin") {
|
||
douyinId = await resolveDouyinLabel(page);
|
||
}
|
||
return {
|
||
success: true,
|
||
message: "登录成功",
|
||
nickname: douyinId || nickname || "已登录用户",
|
||
uid: douyinId || resolvedUid || `user_${Date.now()}`,
|
||
label: douyinId || nickname || "已登录用户",
|
||
avatar,
|
||
cookies,
|
||
};
|
||
}
|
||
|
||
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);
|
||
appendLoginDebug(`exception: ${msg}`);
|
||
globalThis.__native?.emitProgress?.(`登录失败:${msg}`);
|
||
return { success: false, error: msg || "启动登录浏览器失败" };
|
||
} finally {
|
||
if (page) {
|
||
try {
|
||
await page.close();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
await closeLoginBrowser(platform, accountId);
|
||
}
|
||
};
|