Files
geo/apps/desktop-client/src/main/account-binder.ts
T
root 1ef7b872e3 fix(publish/dongchedi): route imagex traffic through session.fetch
Use the account session for imagex apply/upload/commit so the requests carry
the same cookies/UA the bind console saw, instead of falling out of the
session via a bare Node fetch — bytedance imagex was rejecting these as
unauthenticated under the bound session. Also align the binding console URL
with the v2 publish article path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:34:15 +08:00

3265 lines
94 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 { readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
import type { Session, WebContents } from "electron/main";
import { aiPlatformCatalog } from "@geo/shared-types";
import type { DesktopAccountInfo } from "@geo/shared-types";
import {
normalizeRemoteUrl,
pageFetchJson,
sessionCookieFetchJson,
sessionCookieHeader,
sessionCookieValue,
sessionFetchJson,
sessionFetchText,
} from "./adapters/common";
import {
attachSessionHandle,
createPendingSessionHandle,
createSessionHandle,
createSessionHandleForPartition,
forgetSessionHandle,
getPersistedPartition,
getSessionHandle,
type SessionHandle,
} from "./session-registry";
import {
attachWindowDiagnostics,
isWindowReadyForDetection,
loadWindowURLSafely,
} from "./external-window";
import {
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>;
}
type ToutiaoMediaInfoResponse = {
data?: {
user?: {
id_str?: string | number;
screen_name?: string;
https_avatar_url?: string;
};
user_info?: {
id_str?: string | number;
user_id_str?: string | number;
screen_name?: string;
name?: string;
https_avatar_url?: string;
avatar_url?: string;
};
media?: {
id?: string | number;
media_id?: string | number;
media_name?: string;
screen_name?: string;
https_avatar_url?: string;
avatar_url?: string;
};
};
};
type BaijiahaoAppInfoResponse = {
data?: {
user?: {
userid?: string | number;
name?: string;
uname?: string;
username?: string;
avatar?: string;
};
};
};
type SohuRegisterInfoResponse = {
data?: {
account?: {
id?: string | number;
nickName?: string;
avatar?: string;
};
};
};
type QiehaoBasicInfoResponse = {
data?: {
cpInfo?: {
mediaId?: string | number;
mediaName?: string;
header?: string;
};
};
};
type ZhihuMeResponse = {
uid?: string;
id?: string;
name?: string;
avatar_url?: string;
};
type WangyiInfoResponse = {
data?: {
mediaInfo?: {
userId?: string | number;
tname?: string;
icon?: string;
};
};
};
type JianshuUser = {
id?: string | number;
nickname?: string;
avatar?: string;
};
type BilibiliNavResponse = {
code?: number;
data?: {
isLogin?: boolean;
mid?: string | number;
uname?: string;
face?: string;
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 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 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;
}
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;
}
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) {
return existing;
}
const persisted = getPersistedPartition(account.id);
if (persisted) {
return createSessionHandle(account.id);
}
if (account.platform === "toutiaohao") {
const recovered = await recoverToutiaoSessionHandle(account);
if (recovered) {
return recovered;
}
}
return null;
}
function toPublishAccountProfile(detected: DetectedAccount | null): PublishAccountProfile | null {
if (!detected) {
return null;
}
const normalized = sanitizeDetectedAccount(detected);
return {
platformUid: normalized.platformUid,
displayName: normalized.displayName,
avatarUrl: normalized.avatarUrl,
};
}
function toAccountHealthProfile(profile: PublishAccountProfile | null): AccountHealthProfile | null {
if (!profile) {
return null;
}
return {
subjectUid: profile.platformUid,
displayName: profile.displayName,
avatarUrl: profile.avatarUrl,
};
}
function fallbackAccountHealthProfile(account: PublishAccountIdentity, overrides: {
displayName?: string | null;
avatarUrl?: string | null;
} = {}): AccountHealthProfile {
return {
subjectUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: overrides.displayName ?? account.displayName,
avatarUrl: overrides.avatarUrl ?? null,
};
}
async function resolvePublishAccountProfileFromHandle(
account: PublishAccountIdentity,
handle: SessionHandle,
): Promise<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} 静默识别`,
definition.consoleUrl,
handle.session,
{ show: false },
);
try {
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return null;
}
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return toPublishAccountProfile(detected);
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
const detected = await definition.detect({
session: handle.session,
webContents: undefined as unknown as WebContents,
}).catch(() => null);
return toPublishAccountProfile(detected);
}
export async function probePublishAccountSession(
account: PublishAccountIdentity,
partition?: string,
): Promise<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",
},
};
}
const window = createBoundWindow(
`${definition.label} 静默探针`,
definition.consoleUrl,
handle.session,
{ show: false },
);
let mainFrameLoadError:
| {
code: number;
description: string;
url: string;
}
| undefined;
window.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (isMainFrame && errorCode !== -3) {
mainFrameLoadError = {
code: errorCode,
description: errorDescription,
url: validatedURL,
};
}
},
);
try {
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
};
}
const loadError = mainFrameLoadError;
if (loadError) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
errorCode: loadError.code,
errorDescription: loadError.description,
url: loadError.url,
},
};
}
const currentURL = window.webContents.getURL();
if (!currentURL || currentURL.startsWith("data:text/html")) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (definitionLooksLikeLoginRedirect(currentURL, definition)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (account.platform === "toutiaohao") {
const detected = await detectToutiaoFromSession(handle.session);
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
if (consoleReady) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account),
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
};
}
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
};
}
if (isAIPlatformBinding(account.platform)) {
const pageState = await readGenericAIPageState(window.webContents);
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return {
verdict: "challenge_required",
reason: "captcha_gate",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
},
};
}
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
session: handle.session,
webContents: window.webContents,
});
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
if (!pageState) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (pageState.loggedOutSignalCount > 0 || 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 (account.platform === "zol" && !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,
};
}
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;
}
const window = createBoundWindow(
`${definition.label} 静默续期`,
definition.consoleUrl,
handle.session,
{ show: false },
);
try {
await waitForWindowNavigationSettled(window, 10_000);
if (window.isDestroyed()) {
return false;
}
await sleep(1_200);
await flushSessionPersistence(handle.session);
if (account.platform === "zol") {
return await verifyZolConsoleAccess(window, handle.session, account);
}
return !definitionLooksLikeLoginRedirect(window.webContents.getURL(), definition);
} catch {
return false;
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
export async function inspectPublishAccountLocalSession(
account: PublishAccountIdentity,
): Promise<PublishAccountLocalInspection> {
const handle = await findLocalPublishAccountSessionHandle(account);
if (!handle) {
return {
availability: "missing",
profile: null,
};
}
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
if (!profile) {
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
if (consoleReady) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
if (account.platform === "toutiaohao" && await hasToutiaoAuthCookies(handle.session)) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
return {
availability: "expired",
profile: null,
};
}
return {
availability: "ready",
profile,
};
}
export async function resolvePublishAccountProfile(
account: PublishAccountIdentity,
): Promise<PublishAccountProfile | null> {
const handle = await ensurePublishAccountSessionHandle(account);
return await resolvePublishAccountProfileFromHandle(account, handle);
}
async function finalizeDetectedAccountForBind(
_definition: PublishPlatformBindingDefinition,
_context: DetectContext,
detected: DetectedAccount,
): Promise<DetectedAccount> {
return detected;
}
function isToutiaoLoginURL(url: string): boolean {
return url.startsWith(TOUTIAO_LOGIN_URL);
}
async function waitForWindowNavigationSettled(window: BrowserWindow, timeoutMs = 8_000): Promise<void> {
const startedAt = Date.now();
let lastURL = "";
let stableSince = Date.now();
while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) {
const currentURL = window.webContents.getURL();
if (currentURL !== lastURL) {
lastURL = currentURL;
stableSince = Date.now();
}
if (!window.webContents.isLoading() && Date.now() - stableSince >= 450) {
return;
}
await sleep(150);
}
}
async function flushSessionPersistence(target: Session): Promise<void> {
const maybeFlushStorageData = (target as Session & {
flushStorageData?: () => Promise<void>;
}).flushStorageData;
if (typeof maybeFlushStorageData === "function") {
try {
await maybeFlushStorageData.call(target);
} catch (error) {
console.warn("[desktop-session] flushStorageData failed", {
message: error instanceof Error ? error.message : String(error),
});
}
}
try {
await target.cookies.flushStore();
} catch (error) {
console.warn("[desktop-session] flushStore failed", {
message: error instanceof Error ? error.message : String(error),
});
}
}
async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Session): Promise<boolean> {
if (window.isDestroyed()) {
return false;
}
await loadWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, "头条号创作台校验");
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return false;
}
const currentURL = window.webContents.getURL();
if (isToutiaoLoginURL(currentURL)) {
return false;
}
if (TOUTIAO_WORKBENCH_URL_PATTERN.test(currentURL)) {
return true;
}
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
window.webContents,
"https://mp.toutiao.com/mp/agw/media/get_media_info",
{
headers: {
accept: "application/json, text/plain, */*",
},
},
);
if (extractToutiaoAccount(pageResponse)) {
return true;
}
const sessionResponse = await sessionCookieFetchJson<ToutiaoMediaInfoResponse>(
session,
"https://mp.toutiao.com/mp/agw/media/get_media_info",
{
headers: {
accept: "application/json, text/plain, */*",
},
},
);
return extractToutiaoAccount(sessionResponse) !== null;
}
function detectedAccountMatchesIdentity(
detected: DetectedAccount,
account: PublishAccountIdentity,
): boolean {
const expected = normalizeAccountPlatformUid(account.platformUid) || account.platformUid;
if (!expected) {
return true;
}
return sameAccountPlatformUid(detected.platformUid, expected);
}
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)(?=$|[.-])/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} 静默校验`,
definition.consoleUrl,
handle.session,
{ show: false },
);
try {
if (account.platform === "toutiaohao") {
return await verifyToutiaoConsoleAccess(window, handle.session);
}
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return false;
}
const currentURL = window.webContents.getURL();
if (!currentURL || currentURL.startsWith("data:text/html")) {
return false;
}
if (isAIPlatformBinding(account.platform)) {
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
}).catch(() => null);
return detected !== null;
}
if (account.platform === "zol") {
return await verifyZolConsoleAccess(window, handle.session, account);
}
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> {
const response = await sessionFetchJson<WangyiInfoResponse>(
session,
"https://mp.163.com/wemedia/info.do",
).catch(() => null);
const media = response?.data?.mediaInfo;
const uid = stringId(media?.userId);
if (!uid || !media?.tname) {
return null;
}
return {
platformUid: uid,
displayName: media.tname,
avatarUrl: normalizeRemoteUrl(media.icon),
};
}
async function detectJianshu({ session }: DetectContext): Promise<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, "https://mp.weixin.qq.com/").catch(() => "");
if (!html) {
return null;
}
const token = html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? "";
const userName = html.match(/user_name:\s*["']([^"']+)["']/)?.[1] ?? "";
const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? "";
const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? "";
const avatarFromMeta = html.match(/head_img:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
const avatarUrl = (avatarFromHead || avatarFromMeta || "").replace(/^http:\/\//, "https://");
if (!token || !userName || !nickName) {
return null;
}
return {
platformUid: userName,
displayName: nickName,
avatarUrl: normalizeRemoteUrl(avatarUrl),
};
}
async function 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: "https://mp.163.com/index.html#/article/create",
detect: detectWangyihao,
},
jianshu: {
id: "jianshu",
label: "简书",
loginUrl: "https://www.jianshu.com/sign_in",
consoleUrl: "https://www.jianshu.com/writer",
detect: detectJianshu,
},
bilibili: {
id: "bilibili",
label: "bilibili",
loginUrl: "https://www.bilibili.com",
consoleUrl: "https://member.bilibili.com/platform/upload/text/edit",
detect: detectBilibili,
},
juejin: {
id: "juejin",
label: "稀土掘金",
loginUrl: "https://juejin.cn/login",
consoleUrl: "https://juejin.cn/editor/drafts/new/v3",
detect: detectJuejin,
},
smzdm: {
id: "smzdm",
label: "什么值得买",
loginUrl: "https://zhiyou.smzdm.com/user/login",
consoleUrl: "https://post.smzdm.com/",
detect: detectSmzdm,
},
weixin_gzh: {
id: "weixin_gzh",
label: "微信公众号",
loginUrl: "https://mp.weixin.qq.com/cgi-bin/loginpage",
consoleUrl: "https://mp.weixin.qq.com/cgi-bin/home",
detect: detectWeixinGzh,
},
zol: {
id: "zol",
label: "中关村在线",
loginUrl: 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);
}
}
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: [],
});
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 window = createBoundWindow(`${definition.label} 创作台`, definition.consoleUrl, handle.session);
window.show();
window.focus();
}