Files
geo/apps/desktop-client/src/main/account-binder.ts
T
root ddce8b3d8d refactor(desktop): extract qwen auth rules into adapter module
Move the Qwen page-state probe, session detection, and silent probe
helpers out of account-binder into apps/desktop-client/src/main/adapters/qwen,
mirroring the Kimi adapter layout. Generic AI auth no longer special-cases
qwen since the platform now owns its own credential rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:33:40 +08:00

4278 lines
122 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<DetectedAccount | null>
}
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<DesktopAccountInfo>
}
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<string, ActiveBindOperation>()
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<void> {
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, string | number>,
): 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<string, string | number> = {
...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*["']([^"']+)["']/,
/(?:\?|&amp;|&)token=(\d+)(?:&amp;|&|$)/,
/["']?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<WeixinGzhConsoleState | null> {
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<WeixinGzhConsoleState>
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<string> {
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<string> {
if (definition.id === 'weixin_gzh') {
return await resolveWeixinGzhConsoleURL(session)
}
return definition.consoleUrl
}
async function detectToutiaoFromPageState(
webContents: WebContents,
): Promise<Pick<DetectedAccount, 'platformUid' | 'displayName' | 'avatarUrl'> | 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<DetectedAccount> | 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<string> {
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<GenericAIPageState>
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<GenericAIPageState | null> {
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<string | null> {
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<DetectedAccount | null> {
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<DetectedAccount | null> {
return await detectCredentialBackedAIPlatform(platformId, label, context)
}
async function detectYuanbaoPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
return await detectCredentialBackedAIPlatform(platformId, label, context)
}
async function detectWenxinPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
return await detectCredentialBackedAIPlatform(platformId, label, context)
}
async function detectQwenFromSession(
session: Session,
pageState: GenericAIPageState | null,
hints: {
displayName?: string | null
avatarUrl?: string | null
} = {},
): Promise<DetectedAccount | null> {
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<DetectedAccount | null> {
const detected = await detectKimiAccountFromSession(session, pageState, hints)
return detected ? sanitizeDetectedAccount(detected) : null
}
async function detectQwenPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
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<DetectedAccount | null> {
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<DetectedAccount | null> {
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<DetectedAccount | null> {
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<DetectedAccount | null> {
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
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<ToutiaoMediaInfoResponse>(
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<DetectedAccount | null> {
const response = await sessionCookieFetchJson<ToutiaoMediaInfoResponse>(
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<DetectedAccount | null> {
const detected = await detectToutiaoFromSessionOnly(session)
if (detected) {
return sanitizeDetectedAccount(detected)
}
return null
}
async function hasToutiaoAuthCookies(session: Session): Promise<boolean> {
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<WangyiInfoResponse | null> {
const headers = {
accept: 'application/json, text/plain, */*',
referer: WANGYI_API_REFERER,
origin: WANGYI_API_ORIGIN,
}
if (options.allowCookieWrite) {
return await sessionFetchJson<WangyiInfoResponse>(session, WANGYI_INFO_URL, {
credentials: 'include',
headers,
}).catch(() => null)
}
return await sessionCookieFetchJson<WangyiInfoResponse>(session, WANGYI_INFO_URL, {
headers,
}).catch(() => null)
}
async function detectWangyihaoFromSession(session: Session): Promise<DetectedAccount | null> {
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<string>()
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<void> {
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<void> {
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<AuthProbeResult> {
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<boolean> {
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<SessionHandle | null> {
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<SessionHandle | null> {
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<SessionHandle> {
const existing = await findLocalPublishAccountSessionHandle(account)
return existing ?? createSessionHandle(account.id)
}
async function findLocalPublishAccountSessionHandle(
account: PublishAccountIdentity,
): Promise<SessionHandle | null> {
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<PublishAccountProfile | null> {
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<AuthProbeResult> {
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<AuthProbeResult> {
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<boolean> {
const result = await probeAIAccountSession(account, partition).catch(() => null)
return result?.verdict === 'active'
}
export async function silentRefreshPublishAccountSession(
account: PublishAccountIdentity,
partition?: string,
): Promise<boolean> {
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<PublishAccountLocalInspection> {
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<PublishAccountProfile | null> {
const handle = await ensurePublishAccountSessionHandle(account)
return await resolvePublishAccountProfileFromHandle(account, handle)
}
async function finalizeDetectedAccountForBind(
_definition: PublishPlatformBindingDefinition,
_context: DetectContext,
detected: DetectedAccount,
): Promise<DetectedAccount> {
return detected
}
function isToutiaoLoginURL(url: string): boolean {
return url.startsWith(TOUTIAO_LOGIN_URL)
}
async function waitForWindowNavigationSettled(
window: BrowserWindow,
timeoutMs = 8_000,
): Promise<void> {
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<void> {
const maybeFlushStorageData = (
target as Session & {
flushStorageData?: () => Promise<void>
}
).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<boolean> {
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<ToutiaoMediaInfoResponse>(
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<ToutiaoMediaInfoResponse>(
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<DetectedAccount | null> {
const response = await sessionFetchJson<BaijiahaoAppInfoResponse>(
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<DetectedAccount | null> {
const response = await sessionFetchJson<SohuRegisterInfoResponse>(
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<DetectedAccount | null> {
const cookie = await sessionCookieHeader(session, 'om.qq.com')
const response = await sessionFetchJson<QiehaoBasicInfoResponse>(
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<DetectedAccount | null> {
const response = await sessionFetchJson<ZhihuMeResponse>(
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<DetectedAccount | null> {
return await detectWangyihaoFromSession(session)
}
async function detectJianshu({ session }: DetectContext): Promise<DetectedAccount | null> {
const response = await sessionFetchJson<JianshuUser>(
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<string | null> {
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<BilibiliSpaceInfoResponse>(
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<DetectedAccount | null> {
const cookie = await sessionCookieHeader(session, 'bilibili.com')
const response = await sessionFetchJson<BilibiliNavResponse>(
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<DetectedAccount | null> {
const response = await sessionFetchJson<JuejinProfileResponse>(
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<SmzdmPageState | null> {
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<SmzdmPageState> | 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<DetectedAccount | null> {
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<DetectedAccount | null> {
const currentUserUrl = 'https://zhiyou.smzdm.com/user/info/jsonp_get_current'
const pageState = await readSmzdmPageState(webContents).catch(() => null)
const pageResponse = await pageFetchJson<SmzdmCurrentUser>(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<SmzdmCurrentUser>(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<SmzdmCurrentUser>(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<DetectedAccount | null> {
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<Record<string, string>> {
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<DetectedAccount | null> {
const response = await sessionFetchJson<ZolUserInfoResponse>(
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<DetectedAccount | null> {
const response = await sessionFetchJson<DongchediAccountInfoResponse>(
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<string, PublishPlatformBindingDefinition> = {
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<string, PublishPlatformBindingDefinition> = 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<string, PublishPlatformBindingDefinition>
const platformBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = {
...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<DesktopAccountInfo> {
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<DesktopAccountInfo>((resolve, reject) => {
let finished = false
let checking = false
let detectReady = false
let detectDebounceHandle: ReturnType<typeof setTimeout> | 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<void> {
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()
}