import { createHash } from "node:crypto"; import { readdirSync, statSync } from "node:fs"; import { join } from "node:path"; import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main"; import type { Session, WebContents } from "electron/main"; import { aiPlatformCatalog } from "@geo/shared-types"; import type { DesktopAccountInfo } from "@geo/shared-types"; import { normalizeRemoteUrl, pageFetchJson, sessionCookieFetchJson, sessionCookieHeader, sessionCookieValue, sessionFetchJson, sessionFetchText, } from "./adapters/common"; import { attachSessionHandle, createPendingSessionHandle, createSessionHandle, createSessionHandleForPartition, forgetSessionHandle, getPersistedPartition, getSessionHandle, type SessionHandle, } from "./session-registry"; import { attachWindowDiagnostics, isWindowReadyForDetection, loadWindowURLSafely, } from "./external-window"; import { upsertDesktopAccount } from "./transport/api-client"; import { STANDARD_USER_AGENT } from "./user-agent"; import { normalizeAccountPlatformUid, sameAccountPlatformUid, } from "../shared/account-identity"; import type { AccountHealthProfile, AuthProbeResult } from "./auth-types"; interface DetectedAccount { platformUid: string; displayName: string; avatarUrl: string | null; } interface DetectContext { session: Session; webContents: WebContents; } interface PublishPlatformBindingDefinition { id: string; label: string; loginUrl: string; consoleUrl: string; 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; } 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 = { data?: { mediaInfo?: { userId?: string | number; tname?: string; icon?: string; }; }; }; type JianshuUser = { id?: string | number; nickname?: string; avatar?: string; }; type BilibiliNavResponse = { code?: number; data?: { isLogin?: boolean; mid?: string | number; uname?: string; face?: string; }; }; type JuejinProfileResponse = { data?: { bui_user?: { user_id?: string; screen_name?: string; avatar_large?: string; avatar_url?: string; }; }; }; type SmzdmCurrentUser = { smzdm_id?: string | number; nickname?: string; audit_nickname?: string; avatar?: string; }; 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; 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 hashText(value: string): string { return createHash("sha1").update(value).digest("hex").slice(0, 20); } 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; } 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), }; } interface GenericAIPageState { displayName: string | null; avatarUrl: string | null; fingerprint: string | null; credentialFingerprint: string | null; currentPath: string | null; strongAuthSignalCount: number; loginSignalCount: number; loggedOutSignalCount: number; challengeSignalCount: number; } async function readGenericAIPageState( webContents: WebContents | null | undefined, ): Promise { if (!webContents || webContents.isDestroyed()) { return null; } const state = await webContents.executeJavaScript( `(() => { try { const normalize = (value) => { if (typeof value !== "string") return null; const trimmed = value.trim().replace(/\\s+/g, " "); return trimmed || null; }; const isVisible = (node) => { if (!(node instanceof Element)) { return false; } const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const bodyText = normalize(document.body?.innerText || ""); const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i; const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i; const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i; const challengePattern = /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|手机验证码|扫码验证|verify|verification|captcha|security check)/i; const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i; const isInsideAuthSurface = (node) => { if (!(node instanceof Element)) { return false; } let current = node; for (let depth = 0; current && depth < 8; depth += 1) { const descriptor = [ current.getAttribute('role') || '', current.getAttribute('aria-modal') || '', current.getAttribute('data-testid') || '', current.id || '', current.className || '', ].join(' '); if (/(dialog|modal|popup|popover|sheet|drawer|login|signin|auth|scan|qrcode|qrlogin)/i.test(descriptor)) { return true; } const text = normalize(current.textContent || ''); if (text && text.length <= 120 && loggedOutPattern.test(text)) { return true; } current = current.parentElement; } return false; }; const pickGreetingName = () => { if (!bodyText) { return null; } const patterns = [ /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+([^\\n,,]{1,20})/i, /(?:^|\\n)\\s*欢迎回来[,,\\s]*([^\\n]{1,20})/i, ]; for (const pattern of patterns) { const matched = bodyText.match(pattern); const candidate = normalize(matched?.[1] || ""); if (candidate && !stopWords.test(candidate)) { return candidate; } } return null; }; const pickDisplayName = () => { const selectors = [ '[data-testid*="user"]', '[data-testid*="profile"]', '[class*="user-name"]', '[class*="username"]', '[class*="nickname"]', '[class*="profile"] [class*="name"]', '[class*="avatar"] + *', '[class*="account"] [class*="name"]', 'aside [class*="name"]', 'nav [class*="name"]', ]; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; if (isInsideAuthSurface(node)) continue; const text = normalize(node.textContent || ""); if (!text) continue; if (text.length < 2 || text.length > 48) continue; if (stopWords.test(text)) continue; return text; } } return null; }; const pickAvatar = () => { const selectors = [ '[class*="avatar"] img', '[data-testid*="avatar"] img', '[class*="profile"] img', '[class*="account"] img', 'img[alt*="头像"]', ]; for (const selector of selectors) { const node = document.querySelector(selector); if (!node || !isVisible(node)) continue; if (isInsideAuthSurface(node)) continue; const src = normalize(node && 'src' in node ? node.src : null); if (src && /(logo|icon|favicon)/i.test(src)) continue; if (src) return src; } return null; }; const collectLoginSignals = () => { const matches = []; if (bodyText && loginPattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || ""); if (!text || !loginPattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectLoggedOutSignals = () => { const matches = []; if (bodyText && loggedOutPattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]', '[role="dialog"]']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || ""); if (!text || !loggedOutPattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectChallengeSignals = () => { const matches = []; if (bodyText && challengePattern.test(bodyText)) { matches.push('body-text'); } const selectors = ['button', 'a', '[role="button"]', '[role="dialog"]', 'input', 'label']; for (const selector of selectors) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!isVisible(node)) continue; const text = normalize(node.textContent || node.getAttribute?.('aria-label') || ""); if (!text || !challengePattern.test(text)) continue; matches.push(text); if (matches.length >= 4) { return matches; } } } return matches; }; const collectCredentialStorage = (storage, prefix) => { const items = []; try { const limit = Math.min(storage.length, 40); for (let index = 0; index < limit; index += 1) { const key = storage.key(index); if (!key || !credentialKeyPattern.test(key)) { continue; } const value = storage.getItem(key); const text = String(value || '').slice(0, 240).trim(); if (!text) { continue; } items.push(prefix + ':' + key + '=' + text); } } catch { return items; } return items; }; const collectStorage = (storage, prefix) => { const items = []; try { const limit = Math.min(storage.length, 40); for (let index = 0; index < limit; index += 1) { const key = storage.key(index); if (!key || !/(user|uid|token|session|auth|account|profile|login|member|nick|name)/i.test(key)) { continue; } const value = storage.getItem(key); items.push(prefix + ':' + key + '=' + String(value || '').slice(0, 160)); } } catch { return items; } return items; }; const displayName = pickGreetingName() || pickDisplayName(); const avatarUrl = pickAvatar(); const strongSignals = []; if (displayName) { strongSignals.push('displayName'); } if (avatarUrl) { strongSignals.push('avatar'); } if (bodyText && /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+/i.test(bodyText)) { strongSignals.push('greeting'); } const loginSignals = collectLoginSignals(); const loggedOutSignals = collectLoggedOutSignals(); const challengeSignals = collectChallengeSignals(); const credentialFingerprintParts = [ ...collectCredentialStorage(window.localStorage, 'local'), ...collectCredentialStorage(window.sessionStorage, 'session'), ]; const fingerprintParts = [ document.cookie || '', window.location.origin || '', document.title || '', ...collectStorage(window.localStorage, 'local'), ...collectStorage(window.sessionStorage, 'session'), ].filter(Boolean); return { displayName, avatarUrl, fingerprint: fingerprintParts.length ? fingerprintParts.join('||') : null, credentialFingerprint: credentialFingerprintParts.length ? credentialFingerprintParts.join('||') : null, currentPath: normalize(window.location.pathname || ''), strongAuthSignalCount: strongSignals.length, loginSignalCount: loginSignals.length, loggedOutSignalCount: loggedOutSignals.length, challengeSignalCount: challengeSignals.length, }; } catch { return null; } })();`, true, ).catch(() => null); if (!state || typeof state !== "object") { return null; } const typed = state as Partial; return { displayName: normalizeText(typed.displayName), avatarUrl: normalizeRemoteUrl(typed.avatarUrl), fingerprint: normalizeText(typed.fingerprint), credentialFingerprint: normalizeText(typed.credentialFingerprint), currentPath: normalizeText(typed.currentPath), strongAuthSignalCount: typeof typed.strongAuthSignalCount === "number" && Number.isFinite(typed.strongAuthSignalCount) ? typed.strongAuthSignalCount : 0, loginSignalCount: typeof typed.loginSignalCount === "number" && Number.isFinite(typed.loginSignalCount) ? typed.loginSignalCount : 0, loggedOutSignalCount: typeof typed.loggedOutSignalCount === "number" && Number.isFinite(typed.loggedOutSignalCount) ? typed.loggedOutSignalCount : 0, challengeSignalCount: typeof typed.challengeSignalCount === "number" && Number.isFinite(typed.challengeSignalCount) ? typed.challengeSignalCount : 0, }; } function isAIPlatformBinding(platformId: string): boolean { return aiPlatformCatalog.some((item) => item.id === platformId); } function normalizeURLPath(pathname: string): string { const normalized = pathname.replace(/\/+$/, ""); return normalized === "/" ? "" : normalized; } function safeParseURL(input: string): URL | null { try { return new URL(input); } catch { return null; } } function genericAIConversationPath(pathname: string): boolean { return /^\/(?:chat|c|conversation)(?:\/|$)/i.test(pathname); } function isLikelyGenericAICredentialName(name: string): boolean { const normalized = name.trim().toLowerCase(); if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) { return false; } return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name); } function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean { const normalized = normalizeText(value); if (!normalized) { return false; } if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) { return false; } return normalized.length >= 8; } function genericAIPageLooksAuthenticated(currentURL: string, pageState: GenericAIPageState | null): boolean { const current = safeParseURL(currentURL); const currentPath = normalizeURLPath(current?.pathname ?? pageState?.currentPath ?? ""); const strongAuthSignalCount = pageState?.strongAuthSignalCount ?? 0; const loginSignalCount = pageState?.loginSignalCount ?? 0; const loggedOutSignalCount = pageState?.loggedOutSignalCount ?? 0; const onConversationPath = genericAIConversationPath(currentPath); if (loggedOutSignalCount > 0) { return false; } if (loginSignalCount > 0 && strongAuthSignalCount === 0 && !onConversationPath) { return false; } if (loginSignalCount > 0) { return strongAuthSignalCount >= 2 && !onConversationPath; } return strongAuthSignalCount > 0 || onConversationPath; } async function buildGenericAISessionFingerprint( platformId: string, session: Session, pageState?: GenericAIPageState | null, ): Promise { const parts: string[] = []; if (pageState?.credentialFingerprint) { parts.push(pageState.credentialFingerprint); } const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (platformMeta) { const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]); for (const url of urls) { try { const cookies = await session.cookies.get({ url }); const authCookies = cookies.filter((cookie) => { return isLikelyGenericAICredentialName(cookie.name) && isLikelyGenericAICredentialValue(cookie.value); }); if (authCookies.length > 0) { const serialized = authCookies .map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`) .sort() .join(";"); parts.push(`${url}:${serialized}`); } } catch { continue; } } } if (parts.length === 0) { return null; } return boundedIdentity(`${platformId}-session`, parts.join("||")); } async function detectGenericAIPlatform( platformId: string, label: string, context: DetectContext, ): Promise { if (!context.webContents || context.webContents.isDestroyed()) { return null; } const currentURL = context.webContents && !context.webContents.isDestroyed() ? context.webContents.getURL() : ""; const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (!platformMeta) { return null; } if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { return null; } const pageState = await readGenericAIPageState(context.webContents).catch(() => null); if (!genericAIPageLooksAuthenticated(currentURL, pageState)) { return null; } const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState); if (!fingerprint) { return null; } return sanitizeDetectedAccount({ platformUid: fingerprint, displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); } async function detectQwenFromSession( session: Session, hints: { displayName?: string | null; avatarUrl?: string | null; } = {}, ): Promise { const cookies = await session.cookies.get({ url: "https://www.qianwen.com/" }).catch(() => []); if (!cookies.length) { return null; } const cookieMap = new Map(); for (const cookie of cookies) { if (!cookieMap.has(cookie.name)) { cookieMap.set(cookie.name, cookie.value); } } const ticketHash = normalizeText(cookieMap.get("tongyi_sso_ticket_hash")); const ticket = normalizeText(cookieMap.get("tongyi_sso_ticket")); const platformUid = ticketHash ?? ticket; if (!platformUid) { return null; } return sanitizeDetectedAccount({ platformUid, displayName: hints.displayName ?? "通义千问账号", avatarUrl: hints.avatarUrl ?? null, }); } async function detectQwenPlatform( platformId: string, label: string, context: DetectContext, ): Promise { const genericDetected = await detectGenericAIPlatform(platformId, label, context); if (genericDetected) { return genericDetected; } if (!context.webContents || context.webContents.isDestroyed()) { return null; } const currentURL = context.webContents.getURL(); const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null; if (!platformMeta) { return null; } if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) { return null; } const pageState = await readGenericAIPageState(context.webContents).catch(() => null); if ((pageState?.challengeSignalCount ?? 0) > 0) { return null; } if ((pageState?.loggedOutSignalCount ?? 0) > 0) { return null; } const fromSession = await detectQwenFromSession(context.session, { displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); if (fromSession) { return fromSession; } const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState); if (!fingerprint) { return null; } return sanitizeDetectedAccount({ platformUid: fingerprint, displayName: pageState?.displayName ?? `${label} 会话`, avatarUrl: pageState?.avatarUrl ?? null, }); } async function detectAIPlatformAccount( platformId: string, label: string, context: DetectContext, ): Promise { if (platformId === "qwen") { return await detectQwenPlatform(platformId, label, context); } return await detectGenericAIPlatform(platformId, label, context); } async function detectToutiao({ session, webContents }: DetectContext): Promise { const pageResponse = await pageFetchJson( webContents, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); const accountFromPage = extractToutiaoAccount(pageResponse); if (accountFromPage) { return accountFromPage; } const sessionResponse = await sessionCookieFetchJson( session, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); const accountFromSession = extractToutiaoAccount(sessionResponse); if (accountFromSession) { return accountFromSession; } const accountFromPageState = await detectToutiaoFromPageState(webContents); if (!accountFromPageState?.displayName) { const currentURL = webContents.isDestroyed() ? "" : webContents.getURL(); if (/^https:\/\/mp\.toutiao\.com\//i.test(currentURL) && !/\/auth\/page\/login/i.test(currentURL)) { const fallbackDisplayName = normalizeText(webContents.getTitle()) || "头条号账号"; return sanitizeDetectedAccount({ platformUid: await deriveToutiaoFallbackUid(session, fallbackDisplayName), displayName: fallbackDisplayName, avatarUrl: null, }); } return null; } return sanitizeDetectedAccount({ platformUid: accountFromPageState.platformUid || await deriveToutiaoFallbackUid(session, accountFromPageState.displayName), displayName: accountFromPageState.displayName, avatarUrl: accountFromPageState.avatarUrl, }); } async function detectToutiaoFromSessionOnly(session: Session): Promise { const response = await sessionCookieFetchJson( session, "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ); return extractToutiaoAccount(response); } async function detectToutiaoFromSession(session: Session): Promise { const detected = await detectToutiaoFromSessionOnly(session); if (detected) { return sanitizeDetectedAccount(detected); } return null; } async function hasToutiaoAuthCookies(session: Session): Promise { const candidates = [ ["toutiao.com", "sessionid"], ["toutiao.com", "sessionid_ss"], ["toutiao.com", "sid_tt"], ["toutiao.com", "sid_ucp_v1"], ["mp.toutiao.com", "sessionid"], ["mp.toutiao.com", "sessionid_ss"], ] as const; for (const [domain, name] of candidates) { const value = await sessionCookieValue(session, domain, name).catch(() => ""); if (normalizeText(value)) { return true; } } return false; } 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) { return existing; } const persisted = getPersistedPartition(account.id); if (persisted) { return createSessionHandle(account.id); } if (account.platform === "toutiaohao") { const recovered = await recoverToutiaoSessionHandle(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} 静默识别`, definition.consoleUrl, handle.session, { show: false }, ); try { await waitForWindowNavigationSettled(window); if (window.isDestroyed()) { return null; } const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }).catch(() => null); return toPublishAccountProfile(detected); } finally { if (!window.isDestroyed()) { window.destroy(); } } } const detected = await definition.detect({ session: handle.session, webContents: undefined as unknown as WebContents, }).catch(() => null); return toPublishAccountProfile(detected); } export async function probePublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account); if (!handle) { return { verdict: "expired", reason: "missing_partition", profile: null, sessionFingerprint: null, }; } const definition = platformBindingDefinitions[account.platform]; if (!definition) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { platform: account.platform, code: "desktop_platform_not_supported", }, }; } const window = createBoundWindow( `${definition.label} 静默探针`, definition.consoleUrl, 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 (looksLikeLoginRedirect(currentURL, definition.loginUrl)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } if (account.platform === "toutiaohao") { const detected = await detectToutiaoFromSession(handle.session); if (detected) { return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: detected.platformUid, }; } const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session); if (consoleReady) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account), sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, }; } return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, }; } if (isAIPlatformBinding(account.platform)) { const pageState = await readGenericAIPageState(window.webContents); if ((pageState?.challengeSignalCount ?? 0) > 0) { return { verdict: "challenge_required", reason: "captcha_gate", profile: null, sessionFingerprint: null, evidence: { url: currentURL, challengeSignalCount: pageState?.challengeSignalCount ?? 0, }, }; } const detected = await detectAIPlatformAccount(account.platform, definition.label, { session: handle.session, webContents: window.webContents, }); if (detected) { return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: detected.platformUid, }; } if (!pageState) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } if (pageState.loggedOutSignalCount > 0 || looksLikeLoginRedirect(currentURL, definition.loginUrl)) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, loggedOutSignalCount: pageState.loggedOutSignalCount, }, }; } if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) { return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, loginSignalCount: pageState.loginSignalCount, }, }; } const fingerprint = await buildGenericAISessionFingerprint(account.platform, handle.session, pageState); if (genericAIPageLooksAuthenticated(currentURL, pageState)) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account, { displayName: pageState.displayName ?? account.displayName, avatarUrl: pageState.avatarUrl ?? null, }), sessionFingerprint: fingerprint, }; } return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: fingerprint, evidence: { url: currentURL, strongAuthSignalCount: pageState.strongAuthSignalCount, loginSignalCount: pageState.loginSignalCount, loggedOutSignalCount: pageState.loggedOutSignalCount, }, }; } const detected = await definition.detect({ session: handle.session, webContents: window.webContents, }); if (detected) { return { verdict: "active", reason: "probe_success", profile: toAccountHealthProfile(toPublishAccountProfile(detected)), sessionFingerprint: sanitizeDetectedAccount(detected).platformUid, }; } const consoleReady = await verifyPublishAccountConsoleAccess(account, handle); if (consoleReady) { return { verdict: "active", reason: "probe_success", profile: fallbackAccountHealthProfile(account), sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid, }; } return { verdict: "expired", reason: "login_redirect", profile: null, sessionFingerprint: null, evidence: { url: currentURL, }, }; } catch (error) { return { verdict: "network_error", reason: "network_error", profile: null, sessionFingerprint: null, evidence: { message: error instanceof Error ? error.message : String(error), }, }; } finally { if (!window.isDestroyed()) { window.destroy(); } } } export async function silentRefreshPublishAccountSession( account: PublishAccountIdentity, partition?: string, ): Promise { const handle = partition ? createSessionHandleForPartition(account.id, partition) : await findLocalPublishAccountSessionHandle(account); if (!handle) { return false; } const definition = platformBindingDefinitions[account.platform]; if (!definition) { return false; } const window = createBoundWindow( `${definition.label} 静默续期`, definition.consoleUrl, handle.session, { show: false }, ); try { await waitForWindowNavigationSettled(window, 10_000); if (window.isDestroyed()) { return false; } await sleep(1_200); await flushSessionPersistence(handle.session); return !looksLikeLoginRedirect(window.webContents.getURL(), definition.loginUrl); } 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 { if (detected.avatarUrl) { return detected; } let resolved = detected; for (let attempt = 0; attempt < 3; attempt += 1) { await sleep(400 + attempt * 250); const retried = await definition.detect(context).catch(() => null); if (!retried) { continue; } const sanitized = sanitizeDetectedAccount(retried); resolved = sanitized; if (sanitized.avatarUrl) { break; } } return resolved; } 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 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|signin|sign-in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i .test(`${current.pathname}${current.search}${current.hash}`); } return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`); } catch { return false; } } async function verifyPublishAccountConsoleAccess( account: PublishAccountIdentity, handle: SessionHandle, ): Promise { const definition = platformBindingDefinitions[account.platform]; if (!definition) { return false; } const window = createBoundWindow( `${definition.label} 静默校验`, definition.consoleUrl, 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; } return !looksLikeLoginRedirect(currentURL, definition.loginUrl); } 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 { const response = await sessionFetchJson( session, "https://mp.163.com/wemedia/info.do", ).catch(() => null); const media = response?.data?.mediaInfo; const uid = stringId(media?.userId); if (!uid || !media?.tname) { return null; } return { platformUid: uid, displayName: media.tname, avatarUrl: normalizeRemoteUrl(media.icon), }; } 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 detectBilibili({ session }: DetectContext): Promise { const cookie = await sessionCookieHeader(session, "bilibili.com"); const response = await sessionFetchJson( session, "https://api.bilibili.com/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; } return { platformUid: uid, displayName: user.uname, avatarUrl: normalizeRemoteUrl(user.face), }; } async function detectJuejin({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://api.juejin.cn/user_api/v1/user/get_profile", { method: "POST", headers: { "content-type": "application/json", accept: "application/json", }, body: "{}", }, ).catch(() => null); const user = response?.data?.bui_user; if (!user?.user_id || !user.screen_name) { return null; } return { platformUid: user.user_id, displayName: user.screen_name, avatarUrl: normalizeRemoteUrl(user.avatar_url ?? user.avatar_large), }; } async function detectSmzdm({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://zhiyou.smzdm.com/user/info/jsonp_get_current", ).catch(() => null); const uid = stringId(response?.smzdm_id); const nickname = response?.nickname || response?.audit_nickname || ""; if (!uid || !nickname) { return null; } return { platformUid: uid, displayName: nickname, avatarUrl: normalizeRemoteUrl(response?.avatar), }; } async function detectWeixinGzh({ session }: DetectContext): Promise { const html = await sessionFetchText(session, "https://mp.weixin.qq.com/").catch(() => ""); if (!html) { return null; } const token = html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? ""; 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 detectZol({ session }: DetectContext): Promise { const response = await sessionFetchJson( session, "https://open-api.zol.com.cn/api/v1/creator.user.getinfo", { headers: { accept: "application/json, text/plain, */*", }, }, ).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/news/create", 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: "https://mp.163.com/index.html#/article/create", 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: "https://mp.weixin.qq.com/cgi-bin/loginpage", consoleUrl: "https://mp.weixin.qq.com/cgi-bin/home", detect: detectWeixinGzh, }, zol: { id: "zol", label: "中关村在线", loginUrl: "https://post.zol.com.cn/v2/manage/works/all", consoleUrl: "https://post.zol.com.cn/v2/manage/works/all", detect: detectZol, }, dongchedi: { id: "dongchedi", label: "懂车帝", loginUrl: "https://mp.dcdapp.com/login", consoleUrl: "https://mp.dcdapp.com/content/article/create", 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 window = new BrowserWindow({ show: options.show ?? true, width: 1320, height: 900, minWidth: 1100, minHeight: 760, title, autoHideMenuBar: true, 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; const finalizeOperation = () => { clearInterval(intervalHandle); 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; }; window.webContents.on("did-finish-load", syncDetectionState); window.webContents.on("did-navigate", syncDetectionState); window.webContents.on("did-navigate-in-page", syncDetectionState); window.webContents.on( "did-fail-load", (_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => { if (isMainFrame && errorCode !== -3) { detectReady = false; } }, ); const detectAndBind = async () => { 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); } } normalizedDetected = await finalizeDetectedAccountForBind( definition, { session: handle.session, webContents: window.webContents }, normalizedDetected, ); await 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: [], }); 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(); }, 1800); window.on("closed", () => { clearActiveBindOperation(platformId, window); if (!finished) { clearInterval(intervalHandle); forgetSessionHandle(handle.accountId); reject(new Error("desktop_account_bind_window_closed")); } }); syncDetectionState(); void detectAndBind(); }); activeBindOperations.set(platformId, { platformId, window, promise: bindPromise, }); focusBindWindow(window); return await bindPromise; } export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise { const definition = platformBindingDefinitions[account.platform]; if (!definition) { throw new Error(`desktop_platform_not_supported:${account.platform}`); } const handle = await ensurePublishAccountSessionHandle(account); const window = createBoundWindow(`${definition.label} 创作台`, definition.consoleUrl, handle.session); window.show(); window.focus(); }