import { createHash } from "node:crypto"; import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main"; import type { Cookie, Session, WebContents } from "electron/main"; import { aiPlatformCatalog } from "@geo/shared-types"; import type { DesktopAccountInfo } from "@geo/shared-types"; import { normalizeRemoteUrl, pageFetchJson, sessionCookieFetchJson, sessionCookieHeader, sessionCookieValue, sessionFetchJson, sessionFetchText, } from "./adapters/common"; import { attachSessionHandle, createPendingSessionHandle, createSessionHandle, createSessionHandleForPartition, forgetSessionHandle, getPersistedPartition, getSessionHandle, type SessionHandle, } from "./session-registry"; import { attachWindowDiagnostics, isWindowReadyForDetection, loadWindowURLSafely, } from "./external-window"; import { buildGenericAIPageFingerprintFallback, genericAIPageLooksAuthenticated, type GenericAIPageState, } from "./generic-ai-auth"; import { upsertDesktopAccount } from "./transport/api-client"; import { STANDARD_USER_AGENT } from "./user-agent"; import { normalizeAccountPlatformUid, sameAccountPlatformUid, } from "../shared/account-identity"; import type { AccountHealthProfile, AuthProbeResult } from "./auth-types"; interface DetectedAccount { platformUid: string; displayName: string; avatarUrl: string | null; } interface DetectContext { session: Session; webContents: WebContents; } interface PublishPlatformBindingDefinition { id: string; label: string; loginUrl: string; consoleUrl: string; loginRedirectUrls?: string[]; detect(context: DetectContext): Promise; } export interface PublishAccountIdentity { id: string; platform: string; platformUid: string; displayName: string; } export interface PublishAccountProfile { platformUid: string; displayName: string; avatarUrl: string | null; } export interface PublishAccountLocalInspection { availability: "missing" | "ready" | "expired"; profile: PublishAccountProfile | null; } interface ActiveBindOperation { platformId: string; window: BrowserWindow; promise: Promise; } interface WeixinGzhConsoleState { loggedOut: boolean; title: string; textSnippet: string; } type ToutiaoMediaInfoResponse = { data?: { user?: { id_str?: string | number; screen_name?: string; https_avatar_url?: string; }; user_info?: { id_str?: string | number; user_id_str?: string | number; screen_name?: string; name?: string; https_avatar_url?: string; avatar_url?: string; }; media?: { id?: string | number; media_id?: string | number; media_name?: string; screen_name?: string; https_avatar_url?: string; avatar_url?: string; }; }; }; type BaijiahaoAppInfoResponse = { data?: { user?: { userid?: string | number; name?: string; uname?: string; username?: string; avatar?: string; }; }; }; type SohuRegisterInfoResponse = { data?: { account?: { id?: string | number; nickName?: string; avatar?: string; }; }; }; type QiehaoBasicInfoResponse = { data?: { cpInfo?: { mediaId?: string | number; mediaName?: string; header?: string; }; }; }; type ZhihuMeResponse = { uid?: string; id?: string; name?: string; avatar_url?: string; }; type WangyiInfoResponse = { code?: number | string; message?: string; msg?: string; data?: { mediaInfo?: { userId?: string | number; wemediaId?: string | number; tname?: string; icon?: string; }; }; }; type WangyiInfoFetchOptions = { allowCookieWrite?: boolean; }; type StoredWangyihaoCookie = Pick< Cookie, "expirationDate" | "hostOnly" | "httpOnly" | "name" | "path" | "sameSite" | "secure" | "session" | "value" > & { domain: string; }; type WangyihaoCookieVault = Record; type JianshuUser = { id?: string | number; nickname?: string; avatar?: string; }; type BilibiliNavResponse = { code?: number; data?: { isLogin?: boolean; mid?: string | number; uname?: string; face?: string; wbi_img?: { img_url?: string; sub_url?: string; }; }; }; type BilibiliSpaceInfoResponse = { code?: number; data?: { face?: string; }; }; type JuejinProfileResponse = { data?: { bui_user?: { user_id?: string; screen_name?: string; avatar_large?: string; avatar_url?: string; }; }; }; type SmzdmCurrentUser = { smzdm_id?: string | number; nickname?: string; audit_nickname?: string; avatar?: string; data?: { smzdm_id?: string | number; nickname?: string; audit_nickname?: string; avatar?: string; }; }; type SmzdmPageState = { displayName: string | null; avatarUrl: string | null; loggedInPage: boolean; }; type ZolUserInfoResponse = { data?: { userId?: string | number; nickName?: string; photo?: string; }; }; type DongchediAccountInfoResponse = { data?: { user_id_str?: string; name?: string; avatar_url?: string; }; }; const activeBindOperations = new Map(); const MAX_CONCURRENT_BIND_WINDOWS = 2; const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login"; const TOUTIAO_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index"; const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i; const WEIXIN_GZH_HOME_URL = "https://mp.weixin.qq.com/"; const WEIXIN_GZH_LOGIN_URL = "https://mp.weixin.qq.com/cgi-bin/loginpage"; const WEIXIN_GZH_DRAFT_LIST_URL = "https://mp.weixin.qq.com/cgi-bin/appmsg"; const BILIBILI_API_ORIGIN = "https://api.bilibili.com"; const ZOL_API_ORIGIN = "https://open-api.zol.com.cn"; const ZOL_REFERER = "https://post.zol.com.cn/"; const ZOL_CONSOLE_URL = "https://post.zol.com.cn/v2/manage/works/all"; const ZOL_LOGIN_REDIRECT_URLS = [ "https://passport.zol.com.cn/", "https://service.zol.com.cn/user/login", ]; const WANGYI_API_ORIGIN = "https://mp.163.com"; const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com"; const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`; const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`; const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`; const WANGYI_LOGIN_REDIRECT_URLS = [ `${WANGYI_API_ORIGIN}/login.html`, `${WANGYI_CONSOLE_ORIGIN}/login.html`, "https://reg.163.com/", "https://dl.reg.163.com/", ]; const WANGYI_COOKIE_URLS = [ WANGYI_API_ORIGIN, WANGYI_CONSOLE_ORIGIN, "https://reg.163.com/", "https://dl.reg.163.com/", ]; const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52, ]; function sleep(ms: number): Promise { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function normalizeComparableText(value: string): string { return value.trim().replace(/\s+/g, "").toLowerCase(); } function partitionsRootPath(): string { return join(app.getPath("userData"), "Partitions"); } function listPendingPartitions(platformId: string): string[] { try { return readdirSync(partitionsRootPath(), { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.startsWith(`pending-${platformId}-`)) .map((entry) => { const absolutePath = join(partitionsRootPath(), entry.name); let mtimeMs = 0; try { mtimeMs = statSync(absolutePath).mtimeMs; } catch { mtimeMs = 0; } return { partition: `persist:${entry.name}`, mtimeMs, }; }) .sort((left, right) => right.mtimeMs - left.mtimeMs) .map((item) => item.partition); } catch { return []; } } function normalizeText(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed ? trimmed : null; } function stringId(value: unknown): string { if (typeof value === "number") { return Number.isFinite(value) ? String(value) : ""; } return normalizeText(value) ?? ""; } function decodeCookieText(value: string): string { const source = value.replace(/\+/g, " "); try { return decodeURIComponent(source); } catch { return source; } } function hashText(value: string): string { return createHash("sha1").update(value).digest("hex").slice(0, 20); } function buildBilibiliMixinKey(imgKey: string, subKey: string): string { const raw = imgKey + subKey; return BILIBILI_WBI_MIXIN_KEY_ENC_TAB .map((index) => raw.charAt(index)) .filter(Boolean) .join("") .slice(0, 32); } function bilibiliKeyFromWbiURL(value: string | undefined): string { const source = value?.trim() || ""; if (!source) { return ""; } const slashIndex = source.lastIndexOf("/"); const dotIndex = source.lastIndexOf("."); if (slashIndex < 0 || dotIndex <= slashIndex) { return ""; } return source.slice(slashIndex + 1, dotIndex); } function buildBilibiliWbiQuery( nav: BilibiliNavResponse | null | undefined, params: Record, ): string | null { const imgKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.img_url); const subKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.sub_url); if (!imgKey || !subKey) { return null; } const mixinKey = buildBilibiliMixinKey(imgKey, subKey); const next: Record = { ...params, wts: Math.round(Date.now() / 1000), }; const illegalChars = /[!'()*]/g; const query = Object.keys(next) .sort() .map((key) => { const value = String(next[key]).replace(illegalChars, ""); return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }) .join("&"); const wRid = createHash("md5").update(`${query}${mixinKey}`).digest("hex"); return `${query}&w_rid=${wRid}`; } function normalizeBilibiliAvatarUrl(value?: string | null): string | null { return normalizeRemoteUrl(value)?.replace(/^http:\/\//i, "https://") ?? null; } function boundedIdentity(prefix: string, raw: string): string { const normalizedPrefix = prefix.trim().replace(/:+$/, ""); const normalizedRaw = raw.trim(); if (!normalizedRaw) { return `${normalizedPrefix}:${hashText(prefix)}`; } const candidate = `${normalizedPrefix}:${normalizedRaw}`; if (candidate.length <= 120) { return candidate; } return `${normalizedPrefix}:${hashText(normalizedRaw)}`; } function detectedAccountFromFields(input: { uid?: unknown; name?: unknown; avatar?: unknown; }): DetectedAccount | null { const platformUid = stringId(input.uid); const displayName = normalizeText(input.name); if (!platformUid || !displayName) { return null; } return { platformUid, displayName, avatarUrl: normalizeRemoteUrl(stringId(input.avatar) || normalizeText(input.avatar)), }; } function extractToutiaoAccount(response: ToutiaoMediaInfoResponse | null | undefined): DetectedAccount | null { const candidates = [ detectedAccountFromFields({ uid: response?.data?.user?.id_str, name: response?.data?.user?.screen_name, avatar: response?.data?.user?.https_avatar_url, }), detectedAccountFromFields({ uid: response?.data?.user_info?.id_str ?? response?.data?.user_info?.user_id_str, name: response?.data?.user_info?.screen_name ?? response?.data?.user_info?.name, avatar: response?.data?.user_info?.https_avatar_url ?? response?.data?.user_info?.avatar_url, }), detectedAccountFromFields({ uid: response?.data?.media?.media_id ?? response?.data?.media?.id, name: response?.data?.media?.screen_name ?? response?.data?.media?.media_name, avatar: response?.data?.media?.https_avatar_url ?? response?.data?.media?.avatar_url, }), ]; return candidates.find((item) => item !== null) ?? null; } function extractWeixinGzhToken(html: string): string { const patterns = [ /data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/, /(?:\?|&|&)token=(\d+)(?:&|&|$)/, /["']?token["']?\s*[:=]\s*["']?(\d+)["']?/, ]; for (const pattern of patterns) { const token = html.match(pattern)?.[1]?.trim(); if (token) { return token; } } return ""; } function buildWeixinGzhDraftListURL(token?: string): string { const url = new URL(WEIXIN_GZH_DRAFT_LIST_URL); url.searchParams.set("begin", "0"); url.searchParams.set("count", "10"); url.searchParams.set("type", "77"); url.searchParams.set("action", "list_card"); if (token) { url.searchParams.set("token", token); } url.searchParams.set("lang", "zh_CN"); return url.toString(); } function isWeixinGzhLoggedOutText(text: string): boolean { return /(登录超时|登录态超时|登录已过期|会话已过期|请重新登录|未登录)/.test(text); } async function readWeixinGzhConsoleState( webContents: WebContents | null | undefined, ): Promise { if (!webContents || webContents.isDestroyed()) { return null; } const state = await webContents.executeJavaScript( `(() => { try { const text = String(document.body?.innerText || ""); const title = String(document.title || ""); return { title, textSnippet: text.slice(0, 500), loggedOut: /(登录超时|登录态超时|登录已过期|会话已过期|请重新登录|未登录)/.test(text + "\\n" + title), }; } catch { return null; } })();`, true, ).catch(() => null); if (!state || typeof state !== "object") { return null; } const typed = state as Partial; return { loggedOut: typed.loggedOut === true || isWeixinGzhLoggedOutText(`${typed.title ?? ""}\n${typed.textSnippet ?? ""}`), title: typeof typed.title === "string" ? typed.title : "", textSnippet: typeof typed.textSnippet === "string" ? typed.textSnippet : "", }; } async function resolveWeixinGzhConsoleURL(session: Session): Promise { const html = await sessionFetchText( session, WEIXIN_GZH_HOME_URL, { credentials: "include", headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", referer: WEIXIN_GZH_HOME_URL, }, }, ).catch(() => ""); const token = extractWeixinGzhToken(html); if (!token) { return WEIXIN_GZH_LOGIN_URL; } return buildWeixinGzhDraftListURL(token); } async function resolvePublishAccountConsoleURL( definition: PublishPlatformBindingDefinition, session: Session, ): Promise { if (definition.id === "weixin_gzh") { return await resolveWeixinGzhConsoleURL(session); } return definition.consoleUrl; } async function detectToutiaoFromPageState( webContents: WebContents, ): Promise | null> { if (!webContents || webContents.isDestroyed()) { return null; } const serialized = await webContents.executeJavaScript(`(() => { try { const normalizeText = (value) => typeof value === "string" ? value.trim() : ""; const stringId = (value) => typeof value === "number" ? Number.isFinite(value) ? String(value) : "" : normalizeText(value); const pickAccount = (value) => { if (!value || typeof value !== "object") { return null; } const uid = stringId( value.id_str ?? value.user_id_str ?? value.media_id ?? value.mediaId ?? value.uid ?? value.user_id ?? value.id, ); const displayName = normalizeText( value.screen_name ?? value.nick_name ?? value.nickname ?? value.user_name ?? value.media_name ?? value.mediaName ?? value.name, ); const avatarUrl = normalizeText( value.https_avatar_url ?? value.avatar_url ?? value.avatar ?? value.head_img, ); if (!uid || !displayName) { return null; } return { platformUid: uid, displayName, avatarUrl: avatarUrl || null, }; }; const extractUidFromText = (value) => { if (!value) { return ""; } const source = String(value); const patterns = [ /"id_str":"([^"]+)"/, /"user_id_str":"([^"]+)"/, /"media_id":"?([^",}]+)"?/, /"mediaId":"?([^",}]+)"?/, /\b(?:id_str|user_id_str|media_id|mediaId|uid|user_id)\b[^0-9A-Za-z_-]*([0-9A-Za-z_-]{4,})/i, ]; for (const pattern of patterns) { const match = source.match(pattern); if (match?.[1]) { return match[1]; } } return ""; }; const collectStorageHints = (storage) => { const hints = []; if (!storage) { return hints; } try { for (let index = 0; index < storage.length && index < 80; index += 1) { const key = storage.key(index); if (!key) { continue; } const value = storage.getItem(key) || ""; if ( /(uid|user|media|creator|account|id)/i.test(key) || /(id_str|user_id_str|media_id|mediaId|uid|user_id)/i.test(value) ) { hints.push(key + "=" + value); } } } catch { return hints; } return hints; }; const detectHeaderName = () => { const ignoredPatterns = [ /^消息$/, /^更多$/, /^立即完善$/, /^主页$/, /^创作$/, /^管理$/, /^数据$/, /^第\d+天$/, /^在头条创作/, /^扫码登录$/, /^验证码登录$/, /^密码登录$/, /^手机号$/, /^获取验证码$/, /^登录$/, ]; const candidates = Array.from(document.querySelectorAll("body *")) .map((element) => { const rect = element.getBoundingClientRect(); const text = (element.textContent || "").trim().replace(/\s+/g, ""); return { text, rect }; }) .filter(({ text, rect }) => text && text.length >= 2 && text.length <= 40 && rect.top >= 0 && rect.top <= 220 && rect.left >= window.innerWidth * 0.55 && rect.width <= 320 && rect.height <= 120, ) .map(({ text, rect }) => ({ text, rect })) .sort((left, right) => { if (right.rect.left !== left.rect.left) { return right.rect.left - left.rect.left; } return left.rect.top - right.rect.top; }) .map(({ text }) => text); return candidates.find((text) => !ignoredPatterns.some((pattern) => pattern.test(text))) || ""; }; const detectHeaderAvatar = () => { const images = Array.from(document.images) .map((image) => { const rect = image.getBoundingClientRect(); return { src: image.currentSrc || image.src || "", rect, }; }) .filter(({ src, rect }) => src && rect.top >= 0 && rect.top <= 220 && rect.left >= window.innerWidth * 0.55 && rect.width >= 24 && rect.width <= 120 && rect.height >= 24 && rect.height <= 120, ) .sort((left, right) => { if (right.rect.left !== left.rect.left) { return right.rect.left - left.rect.left; } return left.rect.top - right.rect.top; }); return images[0]?.src || ""; }; const isLoggedInWorkbench = () => { const text = document.body.innerText || ""; return ( location.hostname.includes("mp.toutiao.com") && !/\/auth\/page\/login/.test(location.pathname) && ( /在头条创作的第\s*\d+\s*天/.test(text) || (text.includes("主页") && text.includes("创作") && text.includes("管理")) ) ); }; const roots = [ window, window.__INITIAL_STATE__, window.__INITIAL_DATA__, window.__REDUX_STATE__, window.__NEXT_DATA__, window.__STORE__ && typeof window.__STORE__.getState === "function" ? window.__STORE__.getState() : null, ]; const queue = []; const seen = new WeakSet(); const enqueue = (value) => { if (!value || typeof value !== "object" || seen.has(value)) { return; } seen.add(value); queue.push(value); }; roots.forEach(enqueue); let account = null; while (queue.length && !account) { const current = queue.shift(); account = pickAccount(current); if (account) { break; } const children = Array.isArray(current) ? current : Object.values(current); for (const child of children.slice(0, 120)) { enqueue(child); } } if (!account) { const html = document.documentElement.outerHTML; const patternGroups = [ { uid: /"id_str":"([^"]+)"/, name: /"screen_name":"([^"]+)"/, avatar: /"https_avatar_url":"([^"]+)"/, }, { uid: /"user_id_str":"([^"]+)"/, name: /"name":"([^"]+)"/, avatar: /"avatar_url":"([^"]+)"/, }, { uid: /"media_id":"?([^",}]+)"?/, name: /"media_name":"([^"]+)"/, avatar: /"avatar_url":"([^"]+)"/, }, ]; for (const group of patternGroups) { const uid = html.match(group.uid)?.[1] ?? ""; const name = html.match(group.name)?.[1] ?? ""; if (uid && name) { account = { platformUid: uid, displayName: name, avatarUrl: html.match(group.avatar)?.[1] ?? null, }; break; } } } const isLoginPage = /\/auth\/page\/login/.test(location.pathname); if (!account && !isLoginPage) { const displayName = detectHeaderName(); if (displayName) { const storageHints = [ ...collectStorageHints(window.localStorage), ...collectStorageHints(window.sessionStorage), document.cookie, document.documentElement.outerHTML, ]; const fallbackUid = storageHints.map(extractUidFromText).find(Boolean) || ("toutiao-name:" + displayName); account = { platformUid: fallbackUid, displayName, avatarUrl: detectHeaderAvatar() || null, }; } } if (!account && isLoggedInWorkbench()) { account = { platformUid: "", displayName: detectHeaderName() || "头条号账号", avatarUrl: detectHeaderAvatar() || null, }; } return JSON.stringify(account); } catch { return JSON.stringify(null); } })()`, true).catch(() => null); if (typeof serialized !== "string" || !serialized) { return null; } try { const parsed = JSON.parse(serialized) as Partial | null; if (!parsed?.displayName) { return null; } return { platformUid: stringId(parsed.platformUid), displayName: parsed.displayName, avatarUrl: normalizeRemoteUrl(parsed.avatarUrl), }; } catch { return null; } } async function deriveToutiaoFallbackUid(session: Session, displayName: string): Promise { const cookieCandidates = [ ["mp.toutiao.com", "uid_tt"], ["mp.toutiao.com", "uid_tt_ss"], ["mp.toutiao.com", "sessionid"], ["mp.toutiao.com", "sessionid_ss"], ["mp.toutiao.com", "ttwid"], ["toutiao.com", "uid_tt"], ["toutiao.com", "uid_tt_ss"], ["toutiao.com", "ttwid"], ] as const; for (const [domain, name] of cookieCandidates) { const value = await sessionCookieValue(session, domain, name).catch(() => ""); const normalized = normalizeText(value); if (normalized) { return boundedIdentity(`toutiao-cookie-${name}`, normalized); } } const cookieHeader = await sessionCookieHeader(session, "mp.toutiao.com").catch(() => ""); if (cookieHeader.trim()) { return boundedIdentity("toutiao-cookie-hash", cookieHeader); } return boundedIdentity("toutiao-name", displayName); } function sanitizeDetectedAccount(account: DetectedAccount): DetectedAccount { const displayName = normalizeText(account.displayName) ?? "未命名账号"; const normalizedPlatformUid = normalizeAccountPlatformUid(account.platformUid); const platformUid = normalizedPlatformUid || normalizeAccountPlatformUid(boundedIdentity("platform", displayName)) || displayName; return { platformUid, displayName, avatarUrl: normalizeRemoteUrl(account.avatarUrl), }; } async function readGenericAIPageState( webContents: WebContents | null | undefined, ): Promise { if (!webContents || webContents.isDestroyed()) { return null; } const state = await webContents.executeJavaScript( `(() => { try { const normalize = (value) => { if (typeof value !== "string") return null; const trimmed = value.trim().replace(/\\s+/g, " "); return trimmed || null; }; const isVisible = (node) => { if (!(node instanceof Element)) { return false; } const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const bodyText = normalize(document.body?.innerText || ""); const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i; const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i; const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i; const challengePattern = /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|手机验证码|扫码验证|verify|verification|captcha|security check)/i; const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i; const isInsideAuthSurface = (node) => { if (!(node instanceof Element)) { return false; } let current = node; for (let depth = 0; current && depth < 8; depth += 1) { const descriptor = [ current.getAttribute('role') || '', current.getAttribute('aria-modal') || '', current.getAttribute('data-testid') || '', current.id || '', current.className || '', ].join(' '); if (/(dialog|modal|popup|popover|sheet|drawer|login|signin|auth|scan|qrcode|qrlogin)/i.test(descriptor)) { return true; } const text = normalize(current.textContent || ''); if (text && text.length <= 120 && loggedOutPattern.test(text)) { return true; } current = current.parentElement; } return false; }; const pickGreetingName = () => { if (!bodyText) { return null; } const patterns = [ /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+([^\\n,,]{1,20})/i, /(?:^|\\n)\\s*欢迎回来[,,\\s]*([^\\n]{1,20})/i, ]; for (const pattern of patterns) { const matched = bodyText.match(pattern); const candidate = normalize(matched?.[1] || ""); if (candidate && !stopWords.test(candidate)) { return candidate; } } return null; }; const pickDisplayName = () => { const selectors = [ '[data-testid*="user"]', '[data-testid*="profile"]', '[class*="user-name"]', '[class*="username"]', '[class*="nickname"]', '[class*="profile"] [class*="name"]', '[class*="avatar"] + *', '[class*="account"] [class*="name"]', 'aside [class*="name"]', 'nav [class*="name"]', ]; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; if (isInsideAuthSurface(node)) continue; const text = normalize(node.textContent || ""); if (!text) continue; if (text.length < 2 || text.length > 48) continue; if (stopWords.test(text)) continue; return text; } } return null; }; const pickAvatar = () => { const selectors = [ '[class*="avatar"] img', '[data-testid*="avatar"] img', '[class*="profile"] img', '[class*="account"] img', 'img[alt*="头像"]', ]; for (const selector of selectors) { const node = document.querySelector(selector); if (!node || !isVisible(node)) continue; if (isInsideAuthSurface(node)) continue; const src = normalize(node && 'src' in node ? node.src : null); if (src && /(logo|icon|favicon)/i.test(src)) continue; if (src) return src; } return null; }; const collectLoginSignals = () => { const matches = []; if (bodyText && loginPattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || ""); if (!text || !loginPattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectLoggedOutSignals = () => { const matches = []; if (bodyText && loggedOutPattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]', '[role="dialog"]']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || ""); if (!text || !loggedOutPattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectChallengeSignals = () => { const matches = []; if (bodyText && challengePattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[role="dialog"]', 'input', 'label']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || node.getAttribute?.('aria-label') || ""); if (!text || !challengePattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectCredentialStorage = (storage, prefix) => { const items = []; try { const limit = Math.min(storage.length, 40); for (let index = 0; index < limit; index += 1) { const key = storage.key(index); if (!key || !credentialKeyPattern.test(key)) { continue; } const value = storage.getItem(key); const text = String(value || '').slice(0, 240).trim(); if (!text) { continue; } items.push(prefix + ':' + key + '=' + text); } } catch { return items; } return items; }; const collectStorage = (storage, prefix) => { const items = []; try { const limit = Math.min(storage.length, 40); for (let index = 0; index < limit; index += 1) { const key = storage.key(index); if (!key || !/(user|uid|token|session|auth|account|profile|login|member|nick|name)/i.test(key)) { continue; } const value = storage.getItem(key); items.push(prefix + ':' + key + '=' + String(value || '').slice(0, 160)); } } catch { return items; } return items; }; const displayName = pickGreetingName() || pickDisplayName(); const avatarUrl = pickAvatar(); const strongSignals = []; if (displayName) { strongSignals.push('displayName'); } if (avatarUrl) { strongSignals.push('avatar'); } if (bodyText && /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+/i.test(bodyText)) { strongSignals.push('greeting'); } const loginSignals = collectLoginSignals(); const loggedOutSignals = collectLoggedOutSignals(); const challengeSignals = collectChallengeSignals(); const credentialFingerprintParts = [ ...collectCredentialStorage(window.localStorage, 'local'), ...collectCredentialStorage(window.sessionStorage, 'session'), ]; const fingerprintParts = [ document.cookie || '', window.location.origin || '', document.title || '', ...collectStorage(window.localStorage, 'local'), ...collectStorage(window.sessionStorage, 'session'), ].filter(Boolean); return { displayName, avatarUrl, fingerprint: fingerprintParts.length ? fingerprintParts.join('||') : null, credentialFingerprint: credentialFingerprintParts.length ? credentialFingerprintParts.join('||') : null, currentPath: normalize(window.location.pathname || ''), strongAuthSignalCount: strongSignals.length, loginSignalCount: loginSignals.length, loggedOutSignalCount: loggedOutSignals.length, challengeSignalCount: challengeSignals.length, }; } catch { return null; } })();`, true, ).catch(() => null); if (!state || typeof state !== "object") { return null; } const typed = state as Partial; return { displayName: normalizeText(typed.displayName), avatarUrl: normalizeRemoteUrl(typed.avatarUrl), fingerprint: normalizeText(typed.fingerprint), credentialFingerprint: normalizeText(typed.credentialFingerprint), currentPath: normalizeText(typed.currentPath), strongAuthSignalCount: typeof typed.strongAuthSignalCount === "number" && Number.isFinite(typed.strongAuthSignalCount) ? typed.strongAuthSignalCount : 0, loginSignalCount: typeof typed.loginSignalCount === "number" && Number.isFinite(typed.loginSignalCount) ? typed.loginSignalCount : 0, loggedOutSignalCount: typeof typed.loggedOutSignalCount === "number" && Number.isFinite(typed.loggedOutSignalCount) ? typed.loggedOutSignalCount : 0, challengeSignalCount: typeof typed.challengeSignalCount === "number" && Number.isFinite(typed.challengeSignalCount) ? typed.challengeSignalCount : 0, }; } function isAIPlatformBinding(platformId: string): boolean { return aiPlatformCatalog.some((item) => item.id === platformId); } function normalizeURLPath(pathname: string): string { const normalized = pathname.replace(/\/+$/, ""); return normalized === "/" ? "" : normalized; } function safeParseURL(input: string): URL | null { try { return new URL(input); } catch { return null; } } function isLikelyGenericAICredentialName(name: string): boolean { const normalized = name.trim().toLowerCase(); if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) { return false; } return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name); } function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean { const normalized = normalizeText(value); if (!normalized) { return false; } if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) { return false; } return normalized.length >= 8; } async function buildGenericAISessionFingerprint( platformId: string, session: Session, pageState?: GenericAIPageState | null, ): Promise { const parts: string[] = []; if (pageState?.credentialFingerprint) { parts.push(pageState.credentialFingerprint); } const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (platformMeta) { const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]); for (const url of urls) { try { const cookies = await session.cookies.get({ url }); const authCookies = cookies.filter((cookie) => { return isLikelyGenericAICredentialName(cookie.name) && isLikelyGenericAICredentialValue(cookie.value); }); if (authCookies.length > 0) { const serialized = authCookies .map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`) .sort() .join(";"); parts.push(`${url}:${serialized}`); } } catch { continue; } } } if (parts.length === 0) { return buildGenericAIPageFingerprintFallback(platformId, pageState); } return boundedIdentity(`${platformId}-session`, parts.join("||")); } async function detectGenericAIPlatform( platformId: string, label: string, context: DetectContext, ): Promise { if (!context.webContents || context.webContents.isDestroyed()) { return null; } const currentURL = context.webContents && !context.webContents.isDestroyed() ? context.webContents.getURL() : ""; const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (!platformMeta) { return null; } if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { return null; } const pageState = await readGenericAIPageState(context.webContents).catch(() => null); if ((pageState?.challengeSignalCount ?? 0) > 0) { return null; } if (!genericAIPageLooksAuthenticated(currentURL, pageState)) { return null; } const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState); if (!fingerprint) { return null; } return sanitizeDetectedAccount({ platformUid: fingerprint, displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); } async function detectQwenFromSession( session: Session, hints: { displayName?: string | null; avatarUrl?: string | null; } = {}, ): Promise { const cookies = await session.cookies.get({ url: "https://www.qianwen.com/" }).catch(() => []); if (!cookies.length) { return null; } const cookieMap = new Map(); for (const cookie of cookies) { if (!cookieMap.has(cookie.name)) { cookieMap.set(cookie.name, cookie.value); } } const ticketHash = normalizeText(cookieMap.get("tongyi_sso_ticket_hash")); const ticket = normalizeText(cookieMap.get("tongyi_sso_ticket")); const platformUid = ticketHash ?? ticket; if (!platformUid) { return null; } return sanitizeDetectedAccount({ platformUid, displayName: hints.displayName ?? "通义千问账号", avatarUrl: hints.avatarUrl ?? null, }); } async function detectKimiFromSession( session: Session, hints: { displayName?: string | null; avatarUrl?: string | null; } = {}, ): Promise { const cookies = await session.cookies.get({ url: "https://www.kimi.com/" }).catch(() => []); if (!cookies.length) { return null; } const authCookie = cookies.find((cookie) => cookie.name === "kimi-auth" && isLikelyGenericAICredentialValue(cookie.value), ); if (!authCookie) { return null; } return sanitizeDetectedAccount({ platformUid: boundedIdentity("kimi-auth", authCookie.value), displayName: hints.displayName ?? "Kimi 会话", avatarUrl: hints.avatarUrl ?? null, }); } async function detectQwenPlatform( platformId: string, label: string, context: DetectContext, ): Promise { const genericDetected = await detectGenericAIPlatform(platformId, label, context); if (genericDetected) { return genericDetected; } if (!context.webContents || context.webContents.isDestroyed()) { return null; } const currentURL = context.webContents.getURL(); const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (!platformMeta) { return null; } if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { return null; } const pageState = await readGenericAIPageState(context.webContents).catch(() => null); if ((pageState?.challengeSignalCount ?? 0) > 0) { return null; } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null; } const fromSession = await detectQwenFromSession(context.session, { displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); if (fromSession) { return fromSession; } const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState); if (!fingerprint) { return null; } return sanitizeDetectedAccount({ platformUid: fingerprint, displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); } async function detectKimiPlatform( platformId: string, label: string, context: DetectContext, ): Promise { const genericDetected = await detectGenericAIPlatform(platformId, label, context); if (genericDetected) { return genericDetected; } if (!context.webContents || context.webContents.isDestroyed()) { return null; } const currentURL = context.webContents.getURL(); const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (!platformMeta) { return null; } if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { return null; } const pageState = await readGenericAIPageState(context.webContents).catch(() => null); if ((pageState?.challengeSignalCount ?? 0) > 0) { return null; } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null; } const fromSession = await detectKimiFromSession(context.session, { displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); if (fromSession) { return fromSession; } const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState); if (!fingerprint) { return null; } return sanitizeDetectedAccount({ platformUid: fingerprint, displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); } async function detectAIPlatformAccount( platformId: string, label: string, context: DetectContext, ): Promise { if (platformId === "qwen") { return await detectQwenPlatform(platformId, label, context); } if (platformId === "kimi") { return await detectKimiPlatform(platformId, label, context); } return await detectGenericAIPlatform(platformId, label, context); } async function detectToutiao({ session, webContents }: DetectContext): Promise { const pageResponse = await pageFetchJson( webContents, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); const accountFromPage = extractToutiaoAccount(pageResponse); if (accountFromPage) { return accountFromPage; } const sessionResponse = await sessionCookieFetchJson( session, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); const accountFromSession = extractToutiaoAccount(sessionResponse); if (accountFromSession) { return accountFromSession; } const accountFromPageState = await detectToutiaoFromPageState(webContents); if (!accountFromPageState?.displayName) { const currentURL = webContents.isDestroyed() ? "" : webContents.getURL(); if (/^https:\/\/mp\.toutiao\.com\//i.test(currentURL) && !/\/auth\/page\/login/i.test(currentURL)) { const fallbackDisplayName = normalizeText(webContents.getTitle()) || "头条号账号"; return sanitizeDetectedAccount({ platformUid: await deriveToutiaoFallbackUid(session, fallbackDisplayName), displayName: fallbackDisplayName, avatarUrl: null, }); } return null; } return sanitizeDetectedAccount({ platformUid: accountFromPageState.platformUid || await deriveToutiaoFallbackUid(session, accountFromPageState.displayName), displayName: accountFromPageState.displayName, avatarUrl: accountFromPageState.avatarUrl, }); } async function detectToutiaoFromSessionOnly(session: Session): Promise { const response = await sessionCookieFetchJson( session, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); return extractToutiaoAccount(response); } async function detectToutiaoFromSession(session: Session): Promise { const detected = await detectToutiaoFromSessionOnly(session); if (detected) { return sanitizeDetectedAccount(detected); } return null; } async function hasToutiaoAuthCookies(session: Session): Promise { const candidates = [ ["toutiao.com", "sessionid"], ["toutiao.com", "sessionid_ss"], ["toutiao.com", "sid_tt"], ["toutiao.com", "sid_ucp_v1"], ["mp.toutiao.com", "sessionid"], ["mp.toutiao.com", "sessionid_ss"], ] as const; for (const [domain, name] of candidates) { const value = await sessionCookieValue(session, domain, name).catch(() => ""); if (normalizeText(value)) { return true; } } return false; } function extractWangyihaoAccount(response: WangyiInfoResponse | null | undefined): DetectedAccount | null { const media = response?.data?.mediaInfo; const uid = stringId(media?.userId) || stringId(media?.wemediaId); const displayName = normalizeText(media?.tname); if (!uid || !displayName) { return null; } return { platformUid: uid, displayName, avatarUrl: normalizeRemoteUrl(media?.icon), }; } async function fetchWangyihaoInfo( session: Session, options: WangyiInfoFetchOptions = {}, ): Promise { const headers = { accept: "application/json, text/plain, */*", referer: WANGYI_API_REFERER, origin: WANGYI_API_ORIGIN, }; if (options.allowCookieWrite) { return await sessionFetchJson( session, WANGYI_INFO_URL, { credentials: "include", headers, }, ).catch(() => null); } return await sessionCookieFetchJson( session, WANGYI_INFO_URL, { headers }, ).catch(() => null); } async function detectWangyihaoFromSession(session: Session): Promise { return extractWangyihaoAccount(await fetchWangyihaoInfo(session)); } function wangyihaoCookieVaultPath(): string { return join(app.getPath("userData"), "desktop-wangyihao-cookies.json"); } function readWangyihaoCookieVault(): WangyihaoCookieVault { try { return JSON.parse(readFileSync(wangyihaoCookieVaultPath(), "utf8")) as WangyihaoCookieVault; } catch { return {}; } } function writeWangyihaoCookieVault(vault: WangyihaoCookieVault): void { const target = wangyihaoCookieVaultPath(); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, JSON.stringify(vault, null, 2), "utf8"); } function isWangyihaoCookie(cookie: Cookie): boolean { const domain = cookie.domain?.toLowerCase() ?? ""; return domain.endsWith("163.com") || domain.endsWith("126.net"); } function dedupeWangyihaoCookies(cookies: Cookie[]): StoredWangyihaoCookie[] { const seen = new Set(); const result: StoredWangyihaoCookie[] = []; for (const cookie of cookies) { const domain = cookie.domain; if (!domain || !isWangyihaoCookie(cookie) || !cookie.value) { continue; } const key = [ domain, cookie.path, cookie.name, cookie.secure ? "secure" : "plain", ].join("\n"); if (seen.has(key)) { continue; } seen.add(key); result.push({ domain, expirationDate: cookie.expirationDate, hostOnly: cookie.hostOnly, httpOnly: cookie.httpOnly, name: cookie.name, path: cookie.path, sameSite: cookie.sameSite, secure: cookie.secure, session: cookie.session, value: cookie.value, }); } return result; } function urlForStoredWangyihaoCookie(cookie: StoredWangyihaoCookie): string { const domain = cookie.domain.replace(/^\./, ""); const protocol = cookie.secure ? "https" : "http"; return `${protocol}://${domain}${cookie.path || "/"}`; } async function saveWangyihaoSessionCookies(accountId: string, session: Session): Promise { const collected: Cookie[] = []; for (const url of WANGYI_COOKIE_URLS) { collected.push(...await session.cookies.get({ url }).catch(() => [])); } const cookies = dedupeWangyihaoCookies(collected); if (!cookies.length) { return; } const vault = readWangyihaoCookieVault(); vault[accountId] = { updatedAt: Date.now(), cookies, }; writeWangyihaoCookieVault(vault); } async function restoreWangyihaoSessionCookies(accountId: string, session: Session): Promise { const snapshot = readWangyihaoCookieVault()[accountId]; if (!snapshot?.cookies?.length) { return; } for (const cookie of snapshot.cookies) { await session.cookies.set({ url: urlForStoredWangyihaoCookie(cookie), name: cookie.name, value: cookie.value, domain: cookie.domain, path: cookie.path || "/", secure: cookie.secure, httpOnly: cookie.httpOnly, sameSite: cookie.sameSite, ...(!cookie.session && cookie.expirationDate ? { expirationDate: cookie.expirationDate } : {}), }).catch((error) => { console.warn("[desktop-session] restore wangyihao cookie failed", { accountId, domain: cookie.domain, name: cookie.name, message: error instanceof Error ? error.message : String(error), }); }); } } async function probeWangyihaoSession( account: PublishAccountIdentity, session: Session, options: WangyiInfoFetchOptions = {}, ): Promise { const response = await fetchWangyihaoInfo(session, options).catch(() => null); const detected = extractWangyihaoAccount(response); if (detected) { if (!detectedAccountMatchesIdentity(detected, account)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, evidence: { detectedPlatformUid: detected.platformUid, expectedPlatformUid: account.platformUid, }, }; } return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, }; } if (!response) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, }; } return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { code: response.code ?? null, message: response.message ?? response.msg ?? null, }, }; } async function hasWangyihaoAuthCookies(session: Session): Promise { const authCookieNames = new Set([ "P_INFO", "S_INFO", "NTES_SESS", "l_yd_sign", ]); const isAuthCookie = (name: string) => authCookieNames.has(name) || name.startsWith("l_s_subscribe"); const urls = [ WANGYI_API_ORIGIN, WANGYI_CONSOLE_ORIGIN, "https://reg.163.com/", "https://dl.reg.163.com/", ]; for (const url of urls) { const cookies = await session.cookies.get({ url }).catch(() => []); if (cookies.some((cookie) => isAuthCookie(cookie.name) && normalizeText(cookie.value))) { return true; } } return false; } async function recoverWangyihaoSessionHandle(account: PublishAccountIdentity): Promise { const pendingPartitions = listPendingPartitions("wangyihao"); if (!pendingPartitions.length) { return null; } const expectedUid = normalizeText(account.platformUid) ?? ""; const expectedName = normalizeComparableText(account.displayName); for (const partition of pendingPartitions) { const candidateSession = electronSession.fromPartition(partition); if (!(await hasWangyihaoAuthCookies(candidateSession))) { continue; } const detected = await detectWangyihaoFromSession(candidateSession).catch(() => null); if (!detected) { continue; } if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) { console.info("[desktop-session] recovered wangyihao partition by uid", { accountId: account.id, partition, platformUid: detected.platformUid, }); return createSessionHandleForPartition(account.id, partition); } if (expectedName && normalizeComparableText(detected.displayName) === expectedName) { console.info("[desktop-session] recovered wangyihao partition by name", { accountId: account.id, partition, displayName: detected.displayName, }); return createSessionHandleForPartition(account.id, partition); } } return null; } async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Promise { const pendingPartitions = listPendingPartitions("toutiaohao"); if (!pendingPartitions.length) { return null; } const expectedUid = normalizeText(account.platformUid) ?? ""; const expectedName = normalizeComparableText(account.displayName); let fallbackPartition: string | null = null; for (const partition of pendingPartitions) { const candidateSession = electronSession.fromPartition(partition); if (!(await hasToutiaoAuthCookies(candidateSession))) { continue; } fallbackPartition ??= partition; const detected = await detectToutiaoFromSessionOnly(candidateSession).catch(() => null); if (!detected) { continue; } if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) { console.info("[desktop-session] recovered toutiao partition by uid", { accountId: account.id, partition, platformUid: detected.platformUid, }); return createSessionHandleForPartition(account.id, partition); } if (expectedName && normalizeComparableText(detected.displayName) === expectedName) { console.info("[desktop-session] recovered toutiao partition by name", { accountId: account.id, partition, displayName: detected.displayName, }); return createSessionHandleForPartition(account.id, partition); } } if (!fallbackPartition) { return null; } console.info("[desktop-session] recovered toutiao partition by auth cookie fallback", { accountId: account.id, partition: fallbackPartition, }); return createSessionHandleForPartition(account.id, fallbackPartition); } export async function ensurePublishAccountSessionHandle( account: PublishAccountIdentity, ): Promise { const existing = await findLocalPublishAccountSessionHandle(account); return existing ?? createSessionHandle(account.id); } async function findLocalPublishAccountSessionHandle( account: PublishAccountIdentity, ): Promise { const existing = getSessionHandle(account.id); if (existing) { if (account.platform === "wangyihao") { await restoreWangyihaoSessionCookies(account.id, existing.session); } return existing; } const persisted = getPersistedPartition(account.id); if (persisted) { const handle = createSessionHandle(account.id); if (account.platform === "wangyihao") { await restoreWangyihaoSessionCookies(account.id, handle.session); } return handle; } if (account.platform === "toutiaohao") { const recovered = await recoverToutiaoSessionHandle(account); if (recovered) { return recovered; } } if (account.platform === "wangyihao") { const recovered = await recoverWangyihaoSessionHandle(account); if (recovered) { return recovered; } } return null; } function toPublishAccountProfile(detected: DetectedAccount | null): PublishAccountProfile | null { if (!detected) { return null; } const normalized = sanitizeDetectedAccount(detected); return { platformUid: normalized.platformUid, displayName: normalized.displayName, avatarUrl: normalized.avatarUrl, }; } function toAccountHealthProfile(profile: PublishAccountProfile | null): AccountHealthProfile | null { if (!profile) { return null; } return { subjectUid: profile.platformUid, displayName: profile.displayName, avatarUrl: profile.avatarUrl, }; } function fallbackAccountHealthProfile(account: PublishAccountIdentity, overrides: { displayName?: string | null; avatarUrl?: string | null; } = {}): AccountHealthProfile { return { subjectUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, displayName: overrides.displayName ?? account.displayName, avatarUrl: overrides.avatarUrl ?? null, }; } async function resolvePublishAccountProfileFromHandle( account: PublishAccountIdentity, handle: SessionHandle, ): Promise { if (account.platform === "toutiaohao") { return await detectToutiaoFromSession(handle.session); } const definition = platformBindingDefinitions[account.platform]; if (!definition) { return null; } if (isAIPlatformBinding(account.platform)) { const window = createBoundWindow( `${definition.label} 静默识别`, await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); try { await waitForWindowNavigationSettled(window); if (window.isDestroyed()) { return null; } const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }).catch(() => null); return toPublishAccountProfile(detected); } finally { if (!window.isDestroyed()) { window.destroy(); } } } const detected = await definition.detect({ session: handle.session, webContents: undefined as unknown as WebContents, }).catch(() => null); return toPublishAccountProfile(detected); } export async function probePublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account); if (!handle) { return { verdict: "expired", reason: "missing_partition", profile: null, sessionFingerprint: null, }; } const definition = platformBindingDefinitions[account.platform]; if (!definition) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { platform: account.platform, code: "desktop_platform_not_supported", }, }; } if (account.platform === "wangyihao") { return await probeWangyihaoSession(account, handle.session); } const window = createBoundWindow( `${definition.label} 静默探针`, await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); let mainFrameLoadError: | { code: number; description: string; url: string; } | undefined; window.webContents.on( "did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (isMainFrame && errorCode !== -3) { mainFrameLoadError = { code: errorCode, description: errorDescription, url: validatedURL, }; } }, ); try { await waitForWindowNavigationSettled(window); if (window.isDestroyed()) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, }; } const loadError = mainFrameLoadError; if (loadError) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { errorCode: loadError.code, errorDescription: loadError.description, url: loadError.url, }, }; } const currentURL = window.webContents.getURL(); if (!currentURL || currentURL.startsWith("data:text/html")) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } const isLoginRedirect = definitionLooksLikeLoginRedirect(currentURL, definition); if (isLoginRedirect) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } if (account.platform === "weixin_gzh") { const pageState = await readWeixinGzhConsoleState(window.webContents); if (pageState?.loggedOut) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, title: pageState.title, text: pageState.textSnippet, }, }; } const detected = await detectWeixinGzh({ session: handle.session, webContents: window.webContents, }).catch(() => null); if (!detected) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, title: pageState?.title, text: pageState?.textSnippet, }, }; } if (!detectedAccountMatchesIdentity(detected, account)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, evidence: { url: currentURL, detectedPlatformUid: detected.platformUid, expectedPlatformUid: account.platformUid, }, }; } return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, }; } if (account.platform === "toutiaohao") { const detected = await detectToutiaoFromSession(handle.session); if (detected) { return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: detected.platformUid, }; } const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session); if (consoleReady) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account), sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, }; } return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, }; } if (isAIPlatformBinding(account.platform)) { const pageState = await readGenericAIPageState(window.webContents); if ((pageState?.challengeSignalCount ?? 0) > 0) { return { verdict: "challenge_required", reason: "captcha_gate", profile: null, sessionFingerprint: null, evidence: { url: currentURL, challengeSignalCount: pageState?.challengeSignalCount ?? 0, }, }; } const detected = await detectAIPlatformAccount(account.platform, definition.label, { session: handle.session, webContents: window.webContents, }); if (detected) { return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: detected.platformUid, }; } if (!pageState) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } if (pageState.loggedOutSignalCount > 0 || definitionLooksLikeLoginRedirect(currentURL, definition)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, loggedOutSignalCount: pageState.loggedOutSignalCount, }, }; } if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, loginSignalCount: pageState.loginSignalCount, }, }; } const fingerprint = await buildGenericAISessionFingerprint(account.platform, handle.session, pageState); if (genericAIPageLooksAuthenticated(currentURL, pageState)) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account, { displayName: pageState.displayName ?? account.displayName, avatarUrl: pageState.avatarUrl ?? null, }), sessionFingerprint: fingerprint, }; } return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: fingerprint, evidence: { url: currentURL, strongAuthSignalCount: pageState.strongAuthSignalCount, loginSignalCount: pageState.loginSignalCount, loggedOutSignalCount: pageState.loggedOutSignalCount, }, }; } const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }); if (detected) { if ( platformRequiresDetectedAccountIdentityMatch(account.platform) && !detectedAccountMatchesIdentity(detected, account) ) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, evidence: { detectedPlatformUid: detected.platformUid, expectedPlatformUid: account.platformUid, }, }; } return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, }; } if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, code: "account_api_empty", }, }; } const consoleReady = account.platform === "zol" ? await verifyZolConsoleAccess(window, handle.session, account) : await verifyPublishAccountConsoleAccess(account, handle); if (consoleReady) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account), sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, }; } return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } catch (error) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { message: error instanceof Error ? error.message : String(error), }, }; } finally { if (!window.isDestroyed()) { window.destroy(); } } } export async function silentRefreshPublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account); if (!handle) { return false; } const definition = platformBindingDefinitions[account.platform]; if (!definition) { return false; } if (account.platform === "wangyihao") { const result = await probeWangyihaoSession(account, handle.session, { allowCookieWrite: true }) .catch(() => null); if (result?.verdict === "active") { await saveWangyihaoSessionCookies(account.id, handle.session); } return result?.verdict === "active"; } const window = createBoundWindow( `${definition.label} 静默续期`, await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); try { await waitForWindowNavigationSettled(window, 10_000); if (window.isDestroyed()) { return false; } await sleep(1_200); await flushSessionPersistence(handle.session); if (account.platform === "weixin_gzh") { const pageState = await readWeixinGzhConsoleState(window.webContents); if (pageState?.loggedOut) { return false; } const detected = await detectWeixinGzh({ session: handle.session, webContents: window.webContents, }).catch(() => null); return detected ? detectedAccountMatchesIdentity(detected, account) : false; } if (account.platform === "zol") { return await verifyZolConsoleAccess(window, handle.session, account); } if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }).catch(() => null); return detected ? detectedAccountMatchesIdentity(detected, account) : false; } return !definitionLooksLikeLoginRedirect(window.webContents.getURL(), definition); } catch { return false; } finally { if (!window.isDestroyed()) { window.destroy(); } } } export async function inspectPublishAccountLocalSession( account: PublishAccountIdentity, ): Promise { const handle = await findLocalPublishAccountSessionHandle(account); if (!handle) { return { availability: "missing", profile: null, }; } const profile = await resolvePublishAccountProfileFromHandle(account, handle); if (!profile) { if (account.platform === "wangyihao") { return { availability: "expired", profile: null, }; } const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false); if (consoleReady) { return { availability: "ready", profile: { platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, displayName: account.displayName, avatarUrl: null, }, }; } if (account.platform === "toutiaohao" && await hasToutiaoAuthCookies(handle.session)) { return { availability: "ready", profile: { platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, displayName: account.displayName, avatarUrl: null, }, }; } return { availability: "expired", profile: null, }; } return { availability: "ready", profile, }; } export async function resolvePublishAccountProfile( account: PublishAccountIdentity, ): Promise { const handle = await ensurePublishAccountSessionHandle(account); return await resolvePublishAccountProfileFromHandle(account, handle); } async function finalizeDetectedAccountForBind( _definition: PublishPlatformBindingDefinition, _context: DetectContext, detected: DetectedAccount, ): Promise { return detected; } function isToutiaoLoginURL(url: string): boolean { return url.startsWith(TOUTIAO_LOGIN_URL); } async function waitForWindowNavigationSettled(window: BrowserWindow, timeoutMs = 8_000): Promise { const startedAt = Date.now(); let lastURL = ""; let stableSince = Date.now(); while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) { const currentURL = window.webContents.getURL(); if (currentURL !== lastURL) { lastURL = currentURL; stableSince = Date.now(); } if (!window.webContents.isLoading() && Date.now() - stableSince >= 450) { return; } await sleep(150); } } async function flushSessionPersistence(target: Session): Promise { const maybeFlushStorageData = (target as Session & { flushStorageData?: () => Promise; }).flushStorageData; if (typeof maybeFlushStorageData === "function") { try { await maybeFlushStorageData.call(target); } catch (error) { console.warn("[desktop-session] flushStorageData failed", { message: error instanceof Error ? error.message : String(error), }); } } try { await target.cookies.flushStore(); } catch (error) { console.warn("[desktop-session] flushStore failed", { message: error instanceof Error ? error.message : String(error), }); } } async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Session): Promise { if (window.isDestroyed()) { return false; } await loadWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, "头条号创作台校验"); await waitForWindowNavigationSettled(window); if (window.isDestroyed()) { return false; } const currentURL = window.webContents.getURL(); if (isToutiaoLoginURL(currentURL)) { return false; } if (TOUTIAO_WORKBENCH_URL_PATTERN.test(currentURL)) { return true; } const pageResponse = await pageFetchJson( window.webContents, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); if (extractToutiaoAccount(pageResponse)) { return true; } const sessionResponse = await sessionCookieFetchJson( session, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); return extractToutiaoAccount(sessionResponse) !== null; } function isWangyihaoLoggedOutText(text: string): boolean { return /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i .test(text); } async function readWangyihaoConsoleState( webContents: WebContents | null | undefined, ): Promise<{ loggedOut: boolean; title: string; textSnippet: string } | null> { if (!webContents || webContents.isDestroyed()) { return null; } const state = await webContents.executeJavaScript( `(() => { try { const text = String(document.body?.innerText || ""); const title = String(document.title || ""); const source = title + "\\n" + text.slice(0, 1200); return { title, textSnippet: text.slice(0, 500), loggedOut: /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i.test(source), }; } catch { return null; } })();`, true, ).catch(() => null); if (!state || typeof state !== "object") { return null; } const typed = state as { loggedOut?: unknown; title?: unknown; textSnippet?: unknown }; const title = typeof typed.title === "string" ? typed.title : ""; const textSnippet = typeof typed.textSnippet === "string" ? typed.textSnippet : ""; return { loggedOut: typed.loggedOut === true || isWangyihaoLoggedOutText(`${title}\n${textSnippet}`), title, textSnippet, }; } async function verifyWangyihaoConsoleAccess( window: BrowserWindow, session: Session, account: PublishAccountIdentity, ): Promise { if (window.isDestroyed()) { return false; } const currentURL = window.webContents.getURL(); if (!currentURL || currentURL.startsWith("data:text/html")) { return false; } const definition = platformBindingDefinitions.wangyihao; if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) { return false; } const pageState = await readWangyihaoConsoleState(window.webContents); if (pageState?.loggedOut) { return false; } const detected = await detectWangyihaoFromSession(session).catch(() => null); if (detected) { return detectedAccountMatchesIdentity(detected, account); } return false; } function detectedAccountsLikelyMatch(left: DetectedAccount, right: DetectedAccount): boolean { const leftUid = normalizeAccountPlatformUid(left.platformUid) || left.platformUid; const rightUid = normalizeAccountPlatformUid(right.platformUid) || right.platformUid; if (leftUid && rightUid) { return sameAccountPlatformUid(leftUid, rightUid); } const leftName = normalizeComparableText(left.displayName); const rightName = normalizeComparableText(right.displayName); return Boolean(leftName && rightName && leftName === rightName); } async function verifyWangyihaoBindWindowReady( window: BrowserWindow, session: Session, expected: DetectedAccount, ): Promise { if (window.isDestroyed()) { return false; } const currentURL = window.webContents.getURL(); const definition = platformBindingDefinitions.wangyihao; if (!currentURL || currentURL.startsWith("data:text/html")) { return false; } if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) { return false; } const pageState = await readWangyihaoConsoleState(window.webContents); if (pageState?.loggedOut) { return false; } const detected = await detectWangyihaoFromSession(session).catch(() => null); return detected ? detectedAccountsLikelyMatch(detected, expected) : false; } function detectedAccountMatchesIdentity( detected: DetectedAccount, account: PublishAccountIdentity, ): boolean { const expected = normalizeAccountPlatformUid(account.platformUid) || account.platformUid; if (!expected) { return true; } return sameAccountPlatformUid(detected.platformUid, expected); } function platformRequiresDetectedAccountForConsoleAccess(platformId: string): boolean { return platformId === "qiehao"; } function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean { return platformId === "qiehao" || platformId === "wangyihao" || platformId === "zol"; } async function verifyZolConsoleAccess( window: BrowserWindow, session: Session, account: PublishAccountIdentity, ): Promise { if (window.isDestroyed()) { return false; } const detected = await detectZol({ session, webContents: window.webContents, }).catch(() => null); return detected ? detectedAccountMatchesIdentity(detected, account) : false; } const LOGIN_REDIRECT_TOKEN_RE = /(^|[/?#=&._-])(login|signin|sign-in|sign_in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i; const LOGIN_REDIRECT_HOST_RE = /(^|[.-])(login|passport|auth|sso|reg)(?=$|[.-])/i; function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean { if (!currentURL || !loginURL) { return false; } try { const current = new URL(currentURL); const login = new URL(loginURL); if (current.origin !== login.origin) { return false; } const currentPath = normalizeURLPath(current.pathname); const loginPath = normalizeURLPath(login.pathname); if (!loginPath) { return LOGIN_REDIRECT_TOKEN_RE.test(`${current.pathname}${current.search}${current.hash}`) || LOGIN_REDIRECT_HOST_RE.test(current.hostname); } return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`); } catch { return false; } } function definitionLooksLikeLoginRedirect( currentURL: string, definition: PublishPlatformBindingDefinition, ): boolean { const loginRedirectUrls = definition.loginRedirectUrls ?? [definition.loginUrl]; return loginRedirectUrls.some((loginURL) => looksLikeLoginRedirect(currentURL, loginURL)); } async function verifyPublishAccountConsoleAccess( account: PublishAccountIdentity, handle: SessionHandle, ): Promise { const definition = platformBindingDefinitions[account.platform]; if (!definition) { return false; } const window = createBoundWindow( `${definition.label} 静默校验`, await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); try { if (account.platform === "toutiaohao") { return await verifyToutiaoConsoleAccess(window, handle.session); } await waitForWindowNavigationSettled(window); if (window.isDestroyed()) { return false; } const currentURL = window.webContents.getURL(); if (!currentURL || currentURL.startsWith("data:text/html")) { return false; } if (isAIPlatformBinding(account.platform)) { const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }).catch(() => null); return detected !== null; } if (account.platform === "weixin_gzh") { const pageState = await readWeixinGzhConsoleState(window.webContents); if (pageState?.loggedOut) { return false; } const detected = await detectWeixinGzh({ session: handle.session, webContents: window.webContents, }).catch(() => null); return detected ? detectedAccountMatchesIdentity(detected, account) : false; } if (account.platform === "wangyihao") { return await verifyWangyihaoConsoleAccess(window, handle.session, account); } if (account.platform === "zol") { return await verifyZolConsoleAccess(window, handle.session, account); } if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }).catch(() => null); return detected ? detectedAccountMatchesIdentity(detected, account) : false; } return !definitionLooksLikeLoginRedirect(currentURL, definition); } finally { if (!window.isDestroyed()) { window.destroy(); } } } async function detectBaijiahao({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://baijiahao.baidu.com/builder/app/appinfo", { headers: { accept: "application/json, text/plain, */*", }, }, ).catch(() => null); const user = response?.data?.user; const uid = stringId(user?.userid); const nickname = user?.name || user?.uname || user?.username || ""; if (!uid || !nickname) { return null; } return { platformUid: uid, displayName: nickname, avatarUrl: normalizeRemoteUrl(user?.avatar), }; } async function detectSohu({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://mp.sohu.com/mpbp/bp/account/register-info", ).catch(() => null); const account = response?.data?.account; const uid = stringId(account?.id); if (!uid || !account?.nickName) { return null; } return { platformUid: uid, displayName: account.nickName, avatarUrl: normalizeRemoteUrl(account.avatar), }; } async function detectQiehao({ session }: DetectContext): Promise { const cookie = await sessionCookieHeader(session, "om.qq.com"); const response = await sessionFetchJson( session, "https://om.qq.com/maccountsetting/basicinfo?relogin=1", { headers: { accept: "application/json, text/plain, */*", ...(cookie ? { cookie } : {}), }, }, ).catch(() => null); const cpInfo = response?.data?.cpInfo; const uid = stringId(cpInfo?.mediaId); if (!uid || !cpInfo?.mediaName) { return null; } return { platformUid: uid, displayName: cpInfo.mediaName, avatarUrl: normalizeRemoteUrl(cpInfo.header), }; } async function detectZhihu({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://www.zhihu.com/api/v4/me", { headers: { "x-requested-with": "fetch", }, }, ).catch(() => null); const uid = stringId(response?.uid || response?.id); if (!uid || !response?.name) { return null; } return { platformUid: uid, displayName: response.name, avatarUrl: normalizeRemoteUrl(response.avatar_url), }; } async function detectWangyihao({ session }: DetectContext): Promise { return await detectWangyihaoFromSession(session); } async function detectJianshu({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://www.jianshu.com/author/current_user", { headers: { accept: "application/json", }, }, ).catch(() => null); const uid = stringId(response?.id); if (!uid || !response?.nickname) { return null; } return { platformUid: uid, displayName: response.nickname, avatarUrl: normalizeRemoteUrl(response.avatar), }; } async function fetchBilibiliSpaceAvatar( session: Session, uid: string, nav: BilibiliNavResponse | null, cookie: string, ): Promise { const query = buildBilibiliWbiQuery(nav, { mid: uid, platform: "web", token: "", web_location: 1550101, }); if (!query) { return null; } const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 1800); try { const response = await sessionFetchJson( session, `${BILIBILI_API_ORIGIN}/x/space/wbi/acc/info?${query}`, { signal: controller.signal, headers: { accept: "application/json, text/plain, */*", referer: `https://space.bilibili.com/${encodeURIComponent(uid)}`, ...(cookie ? { cookie } : {}), }, }, ).catch(() => null); if (response?.code !== 0) { return null; } return normalizeBilibiliAvatarUrl(response.data?.face); } finally { clearTimeout(timeout); } } async function detectBilibili({ session }: DetectContext): Promise { const cookie = await sessionCookieHeader(session, "bilibili.com"); const response = await sessionFetchJson( session, `${BILIBILI_API_ORIGIN}/x/web-interface/nav?build=0&mobi_app=web`, { headers: { accept: "application/json, text/plain, */*", ...(cookie ? { cookie } : {}), }, }, ).catch(() => null); const user = response?.data; const uid = stringId(user?.mid); if (response?.code !== 0 || !user?.isLogin || !uid || !user.uname) { return null; } const avatarUrl = normalizeBilibiliAvatarUrl(user.face) ?? await fetchBilibiliSpaceAvatar(session, uid, response, cookie); return { platformUid: uid, displayName: user.uname, avatarUrl, }; } async function detectJuejin({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://api.juejin.cn/user_api/v1/user/get_profile", { method: "POST", headers: { "content-type": "application/json", accept: "application/json", }, body: "{}", }, ).catch(() => null); const user = response?.data?.bui_user; if (!user?.user_id || !user.screen_name) { return null; } return { platformUid: user.user_id, displayName: user.screen_name, avatarUrl: normalizeRemoteUrl(user.avatar_url ?? user.avatar_large), }; } function normalizeSmzdmUid(value: unknown): string { const uid = stringId(value); return uid && uid !== "0" ? uid : ""; } function smzdmAccountFromCurrentUser( response: SmzdmCurrentUser | null | undefined, fallback?: { displayName?: string | null; avatarUrl?: string | null; } | null, ): DetectedAccount | null { const user = response?.data ?? response; const uid = normalizeSmzdmUid(user?.smzdm_id); if (!uid) { return null; } const nickname = normalizeText(user?.nickname) ?? normalizeText(user?.audit_nickname) ?? normalizeText(fallback?.displayName) ?? `值友${uid}`; return { platformUid: uid, displayName: nickname, avatarUrl: normalizeRemoteUrl(user?.avatar) ?? normalizeRemoteUrl(fallback?.avatarUrl), }; } async function readSmzdmPageState( webContents: WebContents | null | undefined, ): Promise { if (!webContents || webContents.isDestroyed()) { return null; } const serialized = await webContents.executeJavaScript(`(() => { try { const normalize = (value) => { if (typeof value !== "string") return null; const trimmed = value.trim().replace(/\\s+/g, " "); return trimmed || null; }; const compact = (value) => normalize(value)?.replace(/\\s+/g, "") || null; const isVisible = (node) => { if (!(node instanceof Element)) return false; const style = window.getComputedStyle(node); if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") { return false; } const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const ignored = /^(首页|好价|社区|百科|集团官网|商业合作|爆料投稿|个人中心|我的消息|兑换记录|账户设置|关注|粉丝|社区达人榜|查看更多|搜索|文章|笔记|视频|商品|众测|评论|收藏|品牌认领|金币|碎银子|经验|登录|注册|扫码登录|手机号登录)$/; const scoreName = (text, rect) => { let score = 0; if (/^值友[0-9A-Za-z_-]{4,}$/.test(text)) score += 80; if (rect.top >= 120 && rect.top <= 360 && rect.left >= 160 && rect.left <= 820) score += 40; if (rect.top >= 40 && rect.top <= 140 && rect.left >= window.innerWidth * 0.72) score += 20; return score; }; const pickDisplayName = () => { const candidates = []; const selectors = [ "[class*=nickname]", "[class*=user] [class*=name]", "[class*=account] [class*=name]", "[class*=profile] [class*=name]", ".name", ]; for (const selector of selectors) { for (const node of document.querySelectorAll(selector)) { if (!isVisible(node)) continue; const text = compact(node.textContent || ""); if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue; const rect = node.getBoundingClientRect(); candidates.push({ text, score: scoreName(text, rect) + 30 }); } } for (const node of document.querySelectorAll("body *")) { if (!isVisible(node)) continue; const text = compact(node.textContent || ""); if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue; const rect = node.getBoundingClientRect(); if (rect.top < 0 || rect.top > 420 || rect.left < 80) continue; const score = scoreName(text, rect); if (score > 0) { candidates.push({ text, score }); } } candidates.sort((left, right) => right.score - left.score); if (candidates[0]?.text) { return candidates[0].text; } const bodyText = document.body?.innerText || ""; return compact(bodyText.match(/值友[0-9A-Za-z_-]{4,}/)?.[0] || ""); }; const pickAvatar = () => { const images = Array.from(document.images) .map((image) => ({ src: image.currentSrc || image.src || "", rect: image.getBoundingClientRect() })) .filter(({ src, rect }) => src && !/(logo|favicon|sprite|blank)/i.test(src) && rect.top >= 100 && rect.top <= 420 && rect.left >= 80 && rect.left <= 520 && rect.width >= 40 && rect.height >= 40 ) .sort((left, right) => { const leftArea = left.rect.width * left.rect.height; const rightArea = right.rect.width * right.rect.height; return rightArea - leftArea; }); return images[0]?.src || null; }; const displayName = pickDisplayName(); const bodyText = document.body?.innerText || ""; const path = window.location.pathname || ""; const loggedInPage = /(?:^|\\.)smzdm\\.com$/i.test(window.location.hostname) && !/\\/user\\/login/i.test(path) && Boolean(displayName) && ( /个人中心/.test(bodyText) || /账户设置/.test(bodyText) || /我的消息/.test(bodyText) || /^值友[0-9A-Za-z_-]{4,}$/.test(displayName) ); return JSON.stringify({ displayName, avatarUrl: pickAvatar(), loggedInPage, }); } catch { return JSON.stringify(null); } })()`, true).catch(() => null); if (typeof serialized !== "string" || !serialized) { return null; } try { const parsed = JSON.parse(serialized) as Partial | null; if (!parsed || typeof parsed !== "object") { return null; } return { displayName: normalizeText(parsed.displayName), avatarUrl: normalizeRemoteUrl(parsed.avatarUrl), loggedInPage: parsed.loggedInPage === true, }; } catch { return null; } } async function deriveSmzdmCookieAccount( session: Session, pageState?: SmzdmPageState | null, ): Promise { const uid = normalizeSmzdmUid(await sessionCookieValue(session, "smzdm.com", "smzdm_id").catch(() => "")); const userCookie = await sessionCookieValue(session, "smzdm.com", "user").catch(() => ""); const sessCookie = await sessionCookieValue(session, "smzdm.com", "sess").catch(() => ""); const decodedUser = normalizeText(decodeCookieText(userCookie)); const displayName = decodedUser ?? normalizeText(pageState?.displayName) ?? (uid ? `值友${uid}` : null); if (uid && displayName) { return { platformUid: uid, displayName, avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl), }; } const authFingerprint = normalizeText(sessCookie); if (authFingerprint && displayName) { return { platformUid: boundedIdentity("smzdm-sess", authFingerprint), displayName, avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl), }; } if (pageState?.loggedInPage && pageState.displayName) { const cookieHeader = await sessionCookieHeader(session, "smzdm.com").catch(() => ""); return { platformUid: boundedIdentity("smzdm-page", cookieHeader || pageState.displayName), displayName: pageState.displayName, avatarUrl: normalizeRemoteUrl(pageState.avatarUrl), }; } return null; } async function detectSmzdm({ session, webContents }: DetectContext): Promise { const currentUserUrl = "https://zhiyou.smzdm.com/user/info/jsonp_get_current"; const pageState = await readSmzdmPageState(webContents).catch(() => null); const pageResponse = await pageFetchJson( webContents, currentUserUrl, { credentials: "include", headers: { accept: "application/json, text/plain, */*", }, }, ).catch(() => null); const fromPage = smzdmAccountFromCurrentUser(pageResponse, pageState); if (fromPage) { return fromPage; } const sessionResponse = await sessionFetchJson( session, currentUserUrl, { credentials: "include", headers: { accept: "application/json, text/plain, */*", referer: "https://zhiyou.smzdm.com/user", origin: "https://zhiyou.smzdm.com", }, }, ).catch(() => null); const fromSession = smzdmAccountFromCurrentUser(sessionResponse, pageState); if (fromSession) { return fromSession; } const cookieResponse = await sessionCookieFetchJson( session, currentUserUrl, { headers: { accept: "application/json, text/plain, */*", }, }, ).catch(() => null); const fromCookieRequest = smzdmAccountFromCurrentUser(cookieResponse, pageState); if (fromCookieRequest) { return fromCookieRequest; } return await deriveSmzdmCookieAccount(session, pageState); } async function detectWeixinGzh({ session }: DetectContext): Promise { const html = await sessionFetchText(session, WEIXIN_GZH_HOME_URL).catch(() => ""); if (!html) { return null; } const token = extractWeixinGzhToken(html); const userName = html.match(/user_name:\s*["']([^"']+)["']/)?.[1] ?? ""; const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? ""; const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? ""; const avatarFromMeta = html.match(/head_img:\s*['"]([^'"]+)['"]/)?.[1] ?? ""; const avatarUrl = (avatarFromHead || avatarFromMeta || "").replace(/^http:\/\//, "https://"); if (!token || !userName || !nickName) { return null; } return { platformUid: userName, displayName: nickName, avatarUrl: normalizeRemoteUrl(avatarUrl), }; } async function zolRequestHeaders(session: Session): Promise> { const cookie = await sessionCookieHeader(session, "zol.com.cn").catch(() => ""); return { accept: "application/json, text/plain, */*", origin: "https://post.zol.com.cn", referer: ZOL_REFERER, ...(cookie ? { cookie } : {}), }; } async function detectZol({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, `${ZOL_API_ORIGIN}/api/v1/creator.user.getinfo`, { credentials: "include", headers: await zolRequestHeaders(session), }, ).catch(() => null); const user = response?.data; const uid = stringId(user?.userId); if (!uid || !user?.nickName) { return null; } return { platformUid: uid, displayName: user.nickName, avatarUrl: normalizeRemoteUrl(user.photo), }; } async function detectDongchedi({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://mp.dcdapp.com/passport/account/info/v2/?aid=2302&account_sdk_source=web", ).catch(() => null); const user = response?.data; if (!user?.user_id_str || !user.name) { return null; } return { platformUid: user.user_id_str, displayName: user.name, avatarUrl: normalizeRemoteUrl(user.avatar_url), }; } const publishBindingDefinitions: Record = { toutiaohao: { id: "toutiaohao", label: "头条号", loginUrl: TOUTIAO_LOGIN_URL, consoleUrl: TOUTIAO_CONSOLE_HOME_URL, detect: detectToutiao, }, baijiahao: { id: "baijiahao", label: "百家号", loginUrl: "https://baijiahao.baidu.com/builder/theme/bjh/login", consoleUrl: "https://baijiahao.baidu.com/builder/rc/edit?type=news", detect: detectBaijiahao, }, sohuhao: { id: "sohuhao", label: "搜狐号", loginUrl: "https://mp.sohu.com/mpfe/v4/login", consoleUrl: "https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle", detect: detectSohu, }, qiehao: { id: "qiehao", label: "企鹅号", loginUrl: "https://om.qq.com", consoleUrl: "https://om.qq.com/main.html#/article/add", detect: detectQiehao, }, zhihu: { id: "zhihu", label: "知乎", loginUrl: "https://www.zhihu.com/signin", consoleUrl: "https://zhuanlan.zhihu.com/write", detect: detectZhihu, }, wangyihao: { id: "wangyihao", label: "网易号", loginUrl: "https://mp.163.com/login.html", consoleUrl: WANGYI_CONSOLE_URL, loginRedirectUrls: WANGYI_LOGIN_REDIRECT_URLS, detect: detectWangyihao, }, jianshu: { id: "jianshu", label: "简书", loginUrl: "https://www.jianshu.com/sign_in", consoleUrl: "https://www.jianshu.com/writer", detect: detectJianshu, }, bilibili: { id: "bilibili", label: "bilibili", loginUrl: "https://www.bilibili.com", consoleUrl: "https://member.bilibili.com/platform/upload/text/edit", detect: detectBilibili, }, juejin: { id: "juejin", label: "稀土掘金", loginUrl: "https://juejin.cn/login", consoleUrl: "https://juejin.cn/editor/drafts/new/v3", detect: detectJuejin, }, smzdm: { id: "smzdm", label: "什么值得买", loginUrl: "https://zhiyou.smzdm.com/user/login", consoleUrl: "https://post.smzdm.com/", detect: detectSmzdm, }, weixin_gzh: { id: "weixin_gzh", label: "微信公众号", loginUrl: WEIXIN_GZH_LOGIN_URL, consoleUrl: buildWeixinGzhDraftListURL(), detect: detectWeixinGzh, }, zol: { id: "zol", label: "中关村在线", loginUrl: ZOL_CONSOLE_URL, consoleUrl: ZOL_CONSOLE_URL, loginRedirectUrls: ZOL_LOGIN_REDIRECT_URLS, detect: detectZol, }, dongchedi: { id: "dongchedi", label: "懂车帝", loginUrl: "https://mp.dcdapp.com/login", consoleUrl: "https://mp.dcdapp.com/profile_v2/publish/article", detect: detectDongchedi, }, }; const aiBindingDefinitions: Record = Object.fromEntries( aiPlatformCatalog.map((platform) => [ platform.id, { id: platform.id, label: platform.label, loginUrl: platform.loginUrl, consoleUrl: platform.consoleUrl, detect: (context: DetectContext) => detectAIPlatformAccount(platform.id, platform.label, context), }, ]), ) as Record; const platformBindingDefinitions: Record = { ...publishBindingDefinitions, ...aiBindingDefinitions, }; function createBoundWindow( title: string, targetURL: string, sessionHandle: Session, options: { show?: boolean } = {}, ): BrowserWindow { const show = options.show ?? true; const window = new BrowserWindow({ show, width: 1320, height: 900, minWidth: 1100, minHeight: 760, title, autoHideMenuBar: true, skipTaskbar: !show, focusable: show, hiddenInMissionControl: !show, backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea", webPreferences: { session: sessionHandle, sandbox: true, contextIsolation: true, nodeIntegration: false, }, }); window.webContents.setUserAgent(STANDARD_USER_AGENT); attachWindowDiagnostics(window, title); window.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:\/\//i.test(url)) { setImmediate(() => { if (!window.isDestroyed()) { void loadWindowURLSafely(window, url, `${title} popup`); } }); } return { action: "deny" }; }); void loadWindowURLSafely(window, targetURL, title); return window; } function focusBindWindow(window: BrowserWindow): void { if (window.isDestroyed()) { return; } if (window.isMinimized()) { window.restore(); } if (!window.isVisible()) { window.show(); } window.focus(); } function cleanupInactiveBindOperations(): void { for (const [platformId, operation] of activeBindOperations.entries()) { if (operation.window.isDestroyed()) { activeBindOperations.delete(platformId); } } } function clearActiveBindOperation(platformId: string, window: BrowserWindow): void { const operation = activeBindOperations.get(platformId); if (operation?.window === window) { activeBindOperations.delete(platformId); } } export async function bindPublishAccount(platformId: string): Promise { const definition = platformBindingDefinitions[platformId]; if (!definition) { throw new Error(`desktop_platform_not_supported:${platformId}`); } cleanupInactiveBindOperations(); const existingOperation = activeBindOperations.get(platformId); if (existingOperation && !existingOperation.window.isDestroyed()) { focusBindWindow(existingOperation.window); return existingOperation.promise; } if (activeBindOperations.size >= MAX_CONCURRENT_BIND_WINDOWS) { throw new Error("desktop_account_bind_limit_reached"); } const handle = createPendingSessionHandle(platformId); let window: BrowserWindow; try { window = createBoundWindow(`绑定 ${definition.label}`, definition.loginUrl, handle.session); } catch (error) { forgetSessionHandle(handle.accountId); throw error instanceof Error ? error : new Error("desktop_account_bind_failed"); } const bindPromise = new Promise((resolve, reject) => { let finished = false; let checking = false; let detectReady = false; let detectDebounceHandle: ReturnType | null = null; const finalizeOperation = () => { clearInterval(intervalHandle); if (detectDebounceHandle) { clearTimeout(detectDebounceHandle); detectDebounceHandle = null; } clearActiveBindOperation(platformId, window); }; const settleSuccess = (account: DesktopAccountInfo) => { if (finished) { return; } finished = true; finalizeOperation(); attachSessionHandle(account.id, handle); resolve(account); if (!window.isDestroyed()) { window.setTitle(`${definition.label}(${account.display_name}) 已绑定`); setTimeout(() => { if (!window.isDestroyed()) { window.close(); } }, 800); } }; const settleFailure = (error: Error) => { if (finished) { return; } finished = true; finalizeOperation(); forgetSessionHandle(handle.accountId); if (!window.isDestroyed()) { window.close(); } reject(error); }; const syncDetectionState = () => { if (window.isDestroyed()) { detectReady = false; return; } const currentURL = window.webContents.getURL(); const fallbackReady = /^https?:\/\//i.test(currentURL) && !currentURL.startsWith("data:text/html") && !window.webContents.isLoading(); detectReady = isWindowReadyForDetection(window) || fallbackReady; }; const scheduleDetectAndBind = (delayMs = 120) => { if (finished || window.isDestroyed()) { return; } if (detectDebounceHandle) { clearTimeout(detectDebounceHandle); } detectDebounceHandle = setTimeout(() => { detectDebounceHandle = null; void detectAndBind(); }, delayMs); }; const handleNavigationReady = () => { syncDetectionState(); scheduleDetectAndBind(); }; window.webContents.on("did-finish-load", handleNavigationReady); window.webContents.on("did-navigate", handleNavigationReady); window.webContents.on("did-navigate-in-page", handleNavigationReady); window.webContents.on("page-title-updated", () => scheduleDetectAndBind(250)); window.webContents.on( "did-fail-load", (_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => { if (isMainFrame && errorCode !== -3) { detectReady = false; } }, ); const detectAndBind = async () => { syncDetectionState(); const currentURL = window.isDestroyed() ? "" : window.webContents.getURL(); const canProbe = detectReady || /^https?:\/\//i.test(currentURL); if (finished || checking || !canProbe || window.isDestroyed()) { return; } checking = true; try { console.info("[desktop-bind] detect tick", { platform: definition.id, detectReady, canProbe, loading: window.webContents.isLoading(), url: currentURL, }); const detected = await definition .detect({ session: handle.session, webContents: window.webContents }) .catch((error) => { console.warn(`[desktop-bind] ${definition.id} detect failed`, { message: error instanceof Error ? error.message : String(error), }); return null; }); if (!detected) { return; } let normalizedDetected = sanitizeDetectedAccount(detected); console.info("[desktop-bind] detect success", { platform: definition.id, displayName: normalizedDetected.displayName, platformUid: normalizedDetected.platformUid, url: currentURL, }); if (definition.id === "toutiaohao") { const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session); console.info("[desktop-bind] toutiao console verification", { platform: definition.id, ready: consoleReady, url: window.isDestroyed() ? "" : window.webContents.getURL(), }); if (!consoleReady) { return; } const detectedFromWorkbench = await definition .detect({ session: handle.session, webContents: window.webContents }) .catch(() => null); if (detectedFromWorkbench) { normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench); } } if (definition.id === "wangyihao") { const consoleReady = await verifyWangyihaoBindWindowReady( window, handle.session, normalizedDetected, ); console.info("[desktop-bind] wangyihao console verification", { platform: definition.id, ready: consoleReady, url: window.isDestroyed() ? "" : window.webContents.getURL(), }); if (!consoleReady) { return; } const detectedFromWorkbench = await definition .detect({ session: handle.session, webContents: window.webContents }) .catch(() => null); if (detectedFromWorkbench) { normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench); } } normalizedDetected = await finalizeDetectedAccountForBind( definition, { session: handle.session, webContents: window.webContents }, normalizedDetected, ); void flushSessionPersistence(handle.session); try { const account = await upsertDesktopAccount({ platform: definition.id, platform_uid: normalizedDetected.platformUid, display_name: normalizedDetected.displayName, avatar_url: normalizedDetected.avatarUrl, account_fingerprint: normalizedDetected.platformUid, health: "live", verified_at: new Date().toISOString(), tags: [], }); if (definition.id === "wangyihao") { await saveWangyihaoSessionCookies(account.id, handle.session); } settleSuccess(account); } catch (error) { console.error("[desktop-bind] upsert failed", { platform: definition.id, url: currentURL, message: error instanceof Error ? error.message : String(error), }); if (!window.isDestroyed()) { window.setTitle(`${definition.label} 绑定保存失败,保留窗口重试`); } return; } } catch (error) { console.error("[desktop-bind] bind failed", { platform: definition.id, url: currentURL, message: error instanceof Error ? error.message : String(error), }); settleFailure(error instanceof Error ? error : new Error("desktop_account_bind_failed")); } finally { checking = false; } }; const intervalHandle = setInterval(() => { void detectAndBind(); }, 650); window.on("closed", () => { clearActiveBindOperation(platformId, window); if (!finished) { clearInterval(intervalHandle); forgetSessionHandle(handle.accountId); reject(new Error("desktop_account_bind_window_closed")); } }); syncDetectionState(); void detectAndBind(); }); activeBindOperations.set(platformId, { platformId, window, promise: bindPromise, }); focusBindWindow(window); return await bindPromise; } export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise { const definition = platformBindingDefinitions[account.platform]; if (!definition) { throw new Error(`desktop_platform_not_supported:${account.platform}`); } const handle = await ensurePublishAccountSessionHandle(account); const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session); if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { const detected = await definition.detect({ session: handle.session, webContents: undefined as unknown as WebContents, }).catch(() => null); if (!detected || !detectedAccountMatchesIdentity(detected, account)) { throw new Error(`desktop_account_session_expired:${account.platform}`); } } const window = createBoundWindow(`${definition.label} 创作台`, consoleURL, handle.session); window.show(); window.focus(); }