import { createHash } from 'node:crypto' import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import type { DesktopAccountInfo } from '@geo/shared-types' import { aiPlatformCatalog } from '@geo/shared-types' import type { Cookie, Session, WebContents } from 'electron/main' import { BrowserWindow, app, session as electronSession, nativeTheme } from 'electron/main' import { normalizeAccountPlatformUid, sameAccountPlatformUid } from '../shared/account-identity' import { normalizeRemoteUrl, pageFetchJson, sessionCookieFetchJson, sessionCookieHeader, sessionCookieValue, sessionFetchJson, sessionFetchText, } from './adapters/common' import { buildDoubaoSessionCredentialEvidence, DOUBAO_AUTH_PROBE_URL, doubaoHasBoundAccountPageSignal, probeDoubaoAccountSession, readDoubaoAuthPageState, } from './adapters/doubao/auth-rules' import { detectKimiAccountFromSession, KIMI_AUTH_PROBE_URL, probeKimiAccountSession, readKimiAuthPageState, } from './adapters/kimi/auth-rules' import { detectQwenAccountFromSession, probeQwenAccountSession, QWEN_AUTH_PROBE_URL, readQwenAuthPageState, } from './adapters/qwen/auth-rules' import type { AccountHealthProfile, AuthProbeResult } from './auth-types' import { attachWindowDiagnostics, isWindowReadyForDetection, loadWindowURLSafely, } from './external-window' import { genericAIHasVisibleAccountSignal, genericAIPlatformRequiresVisibleCredentialSignal, isLikelyPlatformAICredentialCookie, type GenericAIPageState, } from './generic-ai-auth' import { attachSessionHandle, createPendingSessionHandle, createSessionHandle, createSessionHandleForPartition, forgetSessionHandle, getPersistedPartition, getSessionHandle, type SessionHandle, } from './session-registry' import { upsertDesktopAccount } from './transport/api-client' import { STANDARD_USER_AGENT } from './user-agent' 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< string, { updatedAt: number cookies: StoredWangyihaoCookie[] } > 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 = { err_no?: number err_msg?: string 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 = 'https://mp.163.com' const WANGYI_LEGACY_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`, `${WANGYI_LEGACY_CONSOLE_ORIGIN}/login.html`, 'https://reg.163.com/', 'https://dl.reg.163.com/', ] const WANGYI_COOKIE_URLS = [ WANGYI_API_ORIGIN, WANGYI_CONSOLE_ORIGIN, WANGYI_LEGACY_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), } } function normalizeGenericAIPageStateSnapshot(state: unknown): GenericAIPageState | 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, } } 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) return normalizeGenericAIPageStateSnapshot(state) } function isAIPlatformBinding(platformId: string): boolean { return aiPlatformCatalog.some((item) => item.id === platformId) } function normalizeURLPath(pathname: string): string { const normalized = pathname.replace(/\/+$/, '') return normalized === '/' ? '' : normalized } async function buildGenericAISessionFingerprint( platformId: string, session: Session, pageState?: GenericAIPageState | null, ): Promise { const parts: string[] = [] if (pageState?.credentialFingerprint) { if ( genericAIPlatformRequiresVisibleCredentialSignal(platformId) && !genericAIHasVisibleAccountSignal(pageState) ) { return null } 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 isLikelyPlatformAICredentialCookie( platformId, cookie.name, cookie.value, pageState, ) }) 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 null } return boundedIdentity(`${platformId}-session`, parts.join('||')) } async function detectCredentialBackedAIPlatform( 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 ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null } if ((pageState?.loginSignalCount ?? 0) > 0 && !genericAIHasVisibleAccountSignal(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 detectDeepSeekPlatform( platformId: string, label: string, context: DetectContext, ): Promise { return await detectCredentialBackedAIPlatform(platformId, label, context) } async function detectYuanbaoPlatform( platformId: string, label: string, context: DetectContext, ): Promise { return await detectCredentialBackedAIPlatform(platformId, label, context) } async function detectWenxinPlatform( platformId: string, label: string, context: DetectContext, ): Promise { return await detectCredentialBackedAIPlatform(platformId, label, context) } async function detectQwenFromSession( session: Session, pageState: GenericAIPageState | null, hints: { displayName?: string | null avatarUrl?: string | null } = {}, ): Promise { const detected = await detectQwenAccountFromSession(session, pageState, hints) return detected ? sanitizeDetectedAccount(detected) : null } async function detectKimiFromSession( session: Session, pageState: GenericAIPageState | null, hints: { displayName?: string | null avatarUrl?: string | null } = {}, ): Promise { const detected = await detectKimiAccountFromSession(session, pageState, hints) return detected ? sanitizeDetectedAccount(detected) : null } async function detectQwenPlatform( platformId: string, label: string, context: DetectContext, ): Promise { 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 readQwenAuthPageState(context.webContents).catch(() => null) if ((pageState?.challengeSignalCount ?? 0) > 0) { return null } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null } const fromSession = await detectQwenFromSession(context.session, pageState, { displayName: pageState?.displayName ?? null, avatarUrl: pageState?.avatarUrl ?? null, }) if (fromSession) { return fromSession } return null } async function detectDoubaoPlatform( platformId: string, label: string, context: DetectContext, ): Promise { 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 readDoubaoAuthPageState(context.webContents).catch(() => null) if ((pageState?.challengeSignalCount ?? 0) > 0) { return null } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null } // Doubao is intentionally platform-specific. It allows anonymous chat, but // our desktop binding requires the real sidebar account menu plus login cookies. if (!doubaoHasBoundAccountPageSignal(pageState)) { return null } const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]) const credentialEvidence = await buildDoubaoSessionCredentialEvidence( context.session, urls, pageState, ) if (!credentialEvidence) { return null } return sanitizeDetectedAccount({ platformUid: boundedIdentity('doubao-session', credentialEvidence), displayName: pageState?.displayName ?? `${label} 账号`, avatarUrl: pageState?.avatarUrl ?? null, }) } async function detectKimiPlatform( platformId: string, label: string, context: DetectContext, ): Promise { 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 readKimiAuthPageState(context.webContents).catch(() => null) if ((pageState?.challengeSignalCount ?? 0) > 0) { return null } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null } const fromSession = await detectKimiFromSession(context.session, pageState, { displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }) if (fromSession) { return fromSession } return null } async function detectAIPlatformAccount( platformId: string, label: string, context: DetectContext, ): Promise { if (platformId === 'qwen') { return await detectQwenPlatform(platformId, label, context) } if (platformId === 'doubao') { return await detectDoubaoPlatform(platformId, label, context) } if (platformId === 'kimi') { return await detectKimiPlatform(platformId, label, context) } if (platformId === 'deepseek') { return await detectDeepSeekPlatform(platformId, label, context) } if (platformId === 'yuanbao') { return await detectYuanbaoPlatform(platformId, label, context) } if (platformId === 'wenxin') { return await detectWenxinPlatform(platformId, label, context) } return null } 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 probeAIAccountSession( 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 platformMeta = aiPlatformCatalog.find((item) => item.id === account.platform) ?? null if (!platformMeta) { return { verdict: 'network_error', reason: 'network_error', profile: null, sessionFingerprint: null, evidence: { platform: account.platform, code: 'desktop_ai_platform_not_supported', }, } } const aiProbeUrl = account.platform === 'doubao' ? DOUBAO_AUTH_PROBE_URL : account.platform === 'kimi' ? KIMI_AUTH_PROBE_URL : account.platform === 'qwen' ? QWEN_AUTH_PROBE_URL : platformMeta.consoleUrl const window = createBoundWindow( `${platformMeta.label} AI 静默探针`, aiProbeUrl, 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, }, } } if (account.platform === 'doubao') { return await probeDoubaoAccountSession({ session: handle.session, webContents: window.webContents, urls: new Set([platformMeta.loginUrl, platformMeta.consoleUrl]), currentURL, fallbackDisplayName: account.displayName, expectedPlatformUid: account.platformUid, }) } if (account.platform === 'kimi') { return await probeKimiAccountSession({ session: handle.session, webContents: window.webContents, currentURL, fallbackDisplayName: account.displayName, expectedPlatformUid: account.platformUid, }) } if (account.platform === 'qwen') { return await probeQwenAccountSession({ session: handle.session, webContents: window.webContents, currentURL, fallbackDisplayName: account.displayName, expectedPlatformUid: account.platformUid, }) } if (definitionLooksLikeLoginRedirect(currentURL, { id: platformMeta.id, label: platformMeta.label, loginUrl: platformMeta.loginUrl, consoleUrl: platformMeta.consoleUrl, detect: (context: DetectContext) => detectAIPlatformAccount(platformMeta.id, platformMeta.label, context), })) { return { verdict: 'expired', reason: 'login_redirect', profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, } } 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, platformMeta.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) { 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, }, } } return { verdict: 'expired', reason: 'login_redirect', profile: null, sessionFingerprint: null, evidence: { url: currentURL, strongAuthSignalCount: pageState.strongAuthSignalCount, loginSignalCount: pageState.loginSignalCount, loggedOutSignalCount: pageState.loggedOutSignalCount, }, } } 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 probePublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { if (isAIPlatformBinding(account.platform)) { return await probeAIAccountSession(account, partition) } const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account) if (!handle) { return { verdict: 'expired', reason: 'missing_partition', profile: null, sessionFingerprint: null, } } if (account.platform === 'wangyihao') { await restoreWangyihaoSessionCookies(account.id, handle.session) } 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, } } 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 silentRefreshAIAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { const result = await probeAIAccountSession(account, partition).catch(() => null) return result?.verdict === 'active' } export async function silentRefreshPublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { if (isAIPlatformBinding(account.platform)) { return await silentRefreshAIAccountSession(account, partition) } const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account) if (!handle) { return false } if (account.platform === 'wangyihao') { await restoreWangyihaoSessionCookies(account.id, handle.session) } 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 (platformRequiresDetectedAccountIdentityMatch(account.platform)) { const detected = await definition .detect({ session: handle.session, webContents: window.webContents, }) .catch(() => null) return detected ? detectedAccountMatchesIdentity(detected, account) : false } 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) { 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' || platformId === 'bilibili' || platformId === 'juejin' || platformId === 'smzdm' ) } 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 (platformRequiresDetectedAccountIdentityMatch(account.platform)) { const detected = await definition .detect({ session: handle.session, webContents: window.webContents, }) .catch(() => null) return detected ? detectedAccountMatchesIdentity(detected, account) : false } 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) if (!response || (typeof response.err_no === 'number' && response.err_no !== 0)) { return 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 null } 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 findLocalPublishAccountSessionHandle(account) if (!handle) { throw new Error(`desktop_account_session_expired:${account.platform}`) } const consoleURL = account.platform === 'doubao' ? DOUBAO_AUTH_PROBE_URL : 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() }