Files
geo/apps/desktop-client/src/main/account-binder.ts
T
root b89892be6e feat(desktop-client/wangyihao): persist session cookies and recover bound partitions
Snapshot 163.com/126.net cookies on bind/refresh to a userData vault and
restore them when reattaching a session, recover orphan partitions by
matching the detected mediaInfo, verify the bind window via the console
state instead of a generic profile probe, and route token/draft fetches
through the https API origin while opening console pages over http to
match the live editor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:18:03 +08:00

3986 lines
116 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 { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
import type { Cookie, Session, WebContents } from "electron/main";
import { aiPlatformCatalog } from "@geo/shared-types";
import type { DesktopAccountInfo } from "@geo/shared-types";
import {
normalizeRemoteUrl,
pageFetchJson,
sessionCookieFetchJson,
sessionCookieHeader,
sessionCookieValue,
sessionFetchJson,
sessionFetchText,
} from "./adapters/common";
import {
attachSessionHandle,
createPendingSessionHandle,
createSessionHandle,
createSessionHandleForPartition,
forgetSessionHandle,
getPersistedPartition,
getSessionHandle,
type SessionHandle,
} from "./session-registry";
import {
attachWindowDiagnostics,
isWindowReadyForDetection,
loadWindowURLSafely,
} from "./external-window";
import {
buildGenericAIPageFingerprintFallback,
genericAIPageLooksAuthenticated,
type GenericAIPageState,
} from "./generic-ai-auth";
import { upsertDesktopAccount } from "./transport/api-client";
import { STANDARD_USER_AGENT } from "./user-agent";
import {
normalizeAccountPlatformUid,
sameAccountPlatformUid,
} from "../shared/account-identity";
import type { AccountHealthProfile, AuthProbeResult } from "./auth-types";
interface DetectedAccount {
platformUid: string;
displayName: string;
avatarUrl: string | null;
}
interface DetectContext {
session: Session;
webContents: WebContents;
}
interface PublishPlatformBindingDefinition {
id: string;
label: string;
loginUrl: string;
consoleUrl: string;
loginRedirectUrls?: string[];
detect(context: DetectContext): Promise<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 = {
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 = "http://mp.163.com";
const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`;
const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
const WANGYI_LOGIN_REDIRECT_URLS = [
`${WANGYI_API_ORIGIN}/login.html`,
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
"https://reg.163.com/",
"https://dl.reg.163.com/",
];
const WANGYI_COOKIE_URLS = [
WANGYI_API_ORIGIN,
WANGYI_CONSOLE_ORIGIN,
"https://reg.163.com/",
"https://dl.reg.163.com/",
];
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32,
15, 50, 10, 31, 58, 3, 45, 35,
27, 43, 5, 49, 33, 9, 42, 19,
29, 28, 14, 39, 12, 38, 41, 13,
37, 48, 7, 16, 24, 55, 40, 61,
26, 17, 0, 1, 60, 51, 30, 4,
22, 25, 54, 21, 56, 59, 6, 63,
57, 62, 11, 36, 20, 34, 44, 52,
];
function sleep(ms: number): Promise<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),
};
}
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);
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,
};
}
function isAIPlatformBinding(platformId: string): boolean {
return aiPlatformCatalog.some((item) => item.id === platformId);
}
function normalizeURLPath(pathname: string): string {
const normalized = pathname.replace(/\/+$/, "");
return normalized === "/" ? "" : normalized;
}
function safeParseURL(input: string): URL | null {
try {
return new URL(input);
} catch {
return null;
}
}
function isLikelyGenericAICredentialName(name: string): boolean {
const normalized = name.trim().toLowerCase();
if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) {
return false;
}
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name);
}
function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean {
const normalized = normalizeText(value);
if (!normalized) {
return false;
}
if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) {
return false;
}
return normalized.length >= 8;
}
async function buildGenericAISessionFingerprint(
platformId: string,
session: Session,
pageState?: GenericAIPageState | null,
): Promise<string | null> {
const parts: string[] = [];
if (pageState?.credentialFingerprint) {
parts.push(pageState.credentialFingerprint);
}
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
if (platformMeta) {
const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]);
for (const url of urls) {
try {
const cookies = await session.cookies.get({ url });
const authCookies = cookies.filter((cookie) => {
return isLikelyGenericAICredentialName(cookie.name)
&& isLikelyGenericAICredentialValue(cookie.value);
});
if (authCookies.length > 0) {
const serialized = authCookies
.map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`)
.sort()
.join(";");
parts.push(`${url}:${serialized}`);
}
} catch {
continue;
}
}
}
if (parts.length === 0) {
return buildGenericAIPageFingerprintFallback(platformId, pageState);
}
return boundedIdentity(`${platformId}-session`, parts.join("||"));
}
async function detectGenericAIPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<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 (!genericAIPageLooksAuthenticated(currentURL, pageState)) {
return null;
}
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
if (!fingerprint) {
return null;
}
return sanitizeDetectedAccount({
platformUid: fingerprint,
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
}
async function detectQwenFromSession(
session: Session,
hints: {
displayName?: string | null;
avatarUrl?: string | null;
} = {},
): Promise<DetectedAccount | null> {
const cookies = await session.cookies.get({ url: "https://www.qianwen.com/" }).catch(() => []);
if (!cookies.length) {
return null;
}
const cookieMap = new Map<string, string>();
for (const cookie of cookies) {
if (!cookieMap.has(cookie.name)) {
cookieMap.set(cookie.name, cookie.value);
}
}
const ticketHash = normalizeText(cookieMap.get("tongyi_sso_ticket_hash"));
const ticket = normalizeText(cookieMap.get("tongyi_sso_ticket"));
const platformUid = ticketHash ?? ticket;
if (!platformUid) {
return null;
}
return sanitizeDetectedAccount({
platformUid,
displayName: hints.displayName ?? "通义千问账号",
avatarUrl: hints.avatarUrl ?? null,
});
}
async function detectKimiFromSession(
session: Session,
hints: {
displayName?: string | null;
avatarUrl?: string | null;
} = {},
): Promise<DetectedAccount | null> {
const cookies = await session.cookies.get({ url: "https://www.kimi.com/" }).catch(() => []);
if (!cookies.length) {
return null;
}
const authCookie = cookies.find((cookie) =>
cookie.name === "kimi-auth" && isLikelyGenericAICredentialValue(cookie.value),
);
if (!authCookie) {
return null;
}
return sanitizeDetectedAccount({
platformUid: boundedIdentity("kimi-auth", authCookie.value),
displayName: hints.displayName ?? "Kimi 会话",
avatarUrl: hints.avatarUrl ?? null,
});
}
async function detectQwenPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
const genericDetected = await detectGenericAIPlatform(platformId, label, context);
if (genericDetected) {
return genericDetected;
}
if (!context.webContents || context.webContents.isDestroyed()) {
return null;
}
const currentURL = context.webContents.getURL();
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
if (!platformMeta) {
return null;
}
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
return null;
}
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return null;
}
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
return null;
}
const fromSession = await detectQwenFromSession(context.session, {
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
if (fromSession) {
return fromSession;
}
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
if (!fingerprint) {
return null;
}
return sanitizeDetectedAccount({
platformUid: fingerprint,
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
}
async function detectKimiPlatform(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
const genericDetected = await detectGenericAIPlatform(platformId, label, context);
if (genericDetected) {
return genericDetected;
}
if (!context.webContents || context.webContents.isDestroyed()) {
return null;
}
const currentURL = context.webContents.getURL();
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
if (!platformMeta) {
return null;
}
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
return null;
}
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return null;
}
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
return null;
}
const fromSession = await detectKimiFromSession(context.session, {
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
if (fromSession) {
return fromSession;
}
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
if (!fingerprint) {
return null;
}
return sanitizeDetectedAccount({
platformUid: fingerprint,
displayName: pageState?.displayName ?? `${label} 会话`,
avatarUrl: pageState?.avatarUrl ?? null,
});
}
async function detectAIPlatformAccount(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
if (platformId === "qwen") {
return await detectQwenPlatform(platformId, label, context);
}
if (platformId === "kimi") {
return await detectKimiPlatform(platformId, label, context);
}
return await detectGenericAIPlatform(platformId, label, context);
}
async function detectToutiao({ session, webContents }: DetectContext): Promise<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 probePublishAccountSession(
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 definition = platformBindingDefinitions[account.platform];
if (!definition) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
platform: account.platform,
code: "desktop_platform_not_supported",
},
};
}
if (account.platform === "wangyihao") {
return await probeWangyihaoSession(account, handle.session);
}
const window = createBoundWindow(
`${definition.label} 静默探针`,
await resolvePublishAccountConsoleURL(definition, handle.session),
handle.session,
{ show: false },
);
let mainFrameLoadError:
| {
code: number;
description: string;
url: string;
}
| undefined;
window.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (isMainFrame && errorCode !== -3) {
mainFrameLoadError = {
code: errorCode,
description: errorDescription,
url: validatedURL,
};
}
},
);
try {
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
};
}
const loadError = mainFrameLoadError;
if (loadError) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
errorCode: loadError.code,
errorDescription: loadError.description,
url: loadError.url,
},
};
}
const currentURL = window.webContents.getURL();
if (!currentURL || currentURL.startsWith("data:text/html")) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
const isLoginRedirect = definitionLooksLikeLoginRedirect(currentURL, definition);
if (isLoginRedirect) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (account.platform === "weixin_gzh") {
const pageState = await readWeixinGzhConsoleState(window.webContents);
if (pageState?.loggedOut) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
title: pageState.title,
text: pageState.textSnippet,
},
};
}
const detected = await detectWeixinGzh({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
if (!detected) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
title: pageState?.title,
text: pageState?.textSnippet,
},
};
}
if (!detectedAccountMatchesIdentity(detected, account)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
evidence: {
url: currentURL,
detectedPlatformUid: detected.platformUid,
expectedPlatformUid: account.platformUid,
},
};
}
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
};
}
if (account.platform === "toutiaohao") {
const detected = await detectToutiaoFromSession(handle.session);
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
if (consoleReady) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account),
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
};
}
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
};
}
if (isAIPlatformBinding(account.platform)) {
const pageState = await readGenericAIPageState(window.webContents);
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return {
verdict: "challenge_required",
reason: "captcha_gate",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
},
};
}
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
session: handle.session,
webContents: window.webContents,
});
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
if (!pageState) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (pageState.loggedOutSignalCount > 0 || definitionLooksLikeLoginRedirect(currentURL, definition)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
loggedOutSignalCount: pageState.loggedOutSignalCount,
},
};
}
if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
loginSignalCount: pageState.loginSignalCount,
},
};
}
const fingerprint = await buildGenericAISessionFingerprint(account.platform, handle.session, pageState);
if (genericAIPageLooksAuthenticated(currentURL, pageState)) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account, {
displayName: pageState.displayName ?? account.displayName,
avatarUrl: pageState.avatarUrl ?? null,
}),
sessionFingerprint: fingerprint,
};
}
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: fingerprint,
evidence: {
url: currentURL,
strongAuthSignalCount: pageState.strongAuthSignalCount,
loginSignalCount: pageState.loginSignalCount,
loggedOutSignalCount: pageState.loggedOutSignalCount,
},
};
}
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
});
if (detected) {
if (
platformRequiresDetectedAccountIdentityMatch(account.platform)
&& !detectedAccountMatchesIdentity(detected, account)
) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
evidence: {
detectedPlatformUid: detected.platformUid,
expectedPlatformUid: account.platformUid,
},
};
}
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
};
}
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
code: "account_api_empty",
},
};
}
const consoleReady = account.platform === "zol"
? await verifyZolConsoleAccess(window, handle.session, account)
: await verifyPublishAccountConsoleAccess(account, handle);
if (consoleReady) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account),
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
};
}
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
} catch (error) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
message: error instanceof Error ? error.message : String(error),
},
};
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
export async function silentRefreshPublishAccountSession(
account: PublishAccountIdentity,
partition?: string,
): Promise<boolean> {
const handle = partition
? createSessionHandleForPartition(account.id, partition)
: await findLocalPublishAccountSessionHandle(account);
if (!handle) {
return false;
}
const definition = platformBindingDefinitions[account.platform];
if (!definition) {
return false;
}
if (account.platform === "wangyihao") {
const result = await probeWangyihaoSession(account, handle.session, { allowCookieWrite: true })
.catch(() => null);
if (result?.verdict === "active") {
await saveWangyihaoSessionCookies(account.id, handle.session);
}
return result?.verdict === "active";
}
const window = createBoundWindow(
`${definition.label} 静默续期`,
await resolvePublishAccountConsoleURL(definition, handle.session),
handle.session,
{ show: false },
);
try {
await waitForWindowNavigationSettled(window, 10_000);
if (window.isDestroyed()) {
return false;
}
await sleep(1_200);
await flushSessionPersistence(handle.session);
if (account.platform === "weixin_gzh") {
const pageState = await readWeixinGzhConsoleState(window.webContents);
if (pageState?.loggedOut) {
return false;
}
const detected = await detectWeixinGzh({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
}
if (account.platform === "zol") {
return await verifyZolConsoleAccess(window, handle.session, account);
}
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
}
return !definitionLooksLikeLoginRedirect(window.webContents.getURL(), definition);
} catch {
return false;
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
export async function inspectPublishAccountLocalSession(
account: PublishAccountIdentity,
): Promise<PublishAccountLocalInspection> {
const handle = await findLocalPublishAccountSessionHandle(account);
if (!handle) {
return {
availability: "missing",
profile: null,
};
}
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
if (!profile) {
if (account.platform === "wangyihao") {
return {
availability: "expired",
profile: null,
};
}
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
if (consoleReady) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
if (account.platform === "toutiaohao" && await hasToutiaoAuthCookies(handle.session)) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
return {
availability: "expired",
profile: null,
};
}
return {
availability: "ready",
profile,
};
}
export async function resolvePublishAccountProfile(
account: PublishAccountIdentity,
): Promise<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";
}
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 (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);
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 await deriveSmzdmCookieAccount(session, pageState);
}
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 ensurePublishAccountSessionHandle(account);
const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session);
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
const detected = await definition.detect({
session: handle.session,
webContents: undefined as unknown as WebContents,
}).catch(() => null);
if (!detected || !detectedAccountMatchesIdentity(detected, account)) {
throw new Error(`desktop_account_session_expired:${account.platform}`);
}
}
const window = createBoundWindow(`${definition.label} 创作台`, consoleURL, handle.session);
window.show();
window.focus();
}