Files
geo/apps/desktop-client/src/main/account-binder.ts
T
root 7abac1e9c4 feat: Implement Qwen adapter and enhance AI platform detection
- Added a new Qwen adapter to facilitate monitoring through Playwright, leveraging internal text/chat managers.
- Updated account detection logic to recognize Qwen sessions based on persisted cookies, improving binding reliability.
- Relaxed authentication requirements for monitor tasks, allowing anonymous execution for certain AI platforms.
- Enhanced the generic AI platform detection to include Qwen-specific logic, ensuring accurate session identification.
- Modified the monitoring dashboard to include runtime state indicating if the current user's desktop client is online.
- Updated various files including `account-binder.ts`, `runtime-controller.ts`, and `monitoring_service.go` to support new features and improvements.
2026-04-20 19:10:35 +08:00

2743 lines
78 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 { upsertDesktopAccount } from "./transport/api-client";
import { STANDARD_USER_AGENT } from "./user-agent";
import {
normalizeAccountPlatformUid,
sameAccountPlatformUid,
} from "../shared/account-identity";
import type { AccountHealthProfile, AuthProbeResult } from "./auth-types";
interface DetectedAccount {
platformUid: string;
displayName: string;
avatarUrl: string | null;
}
interface DetectContext {
session: Session;
webContents: WebContents;
}
interface PublishPlatformBindingDefinition {
id: string;
label: string;
loginUrl: string;
consoleUrl: string;
detect(context: DetectContext): Promise<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;
};
};
type JuejinProfileResponse = {
data?: {
bui_user?: {
user_id?: string;
screen_name?: string;
avatar_large?: string;
avatar_url?: string;
};
};
};
type SmzdmCurrentUser = {
smzdm_id?: string | number;
nickname?: string;
audit_nickname?: string;
avatar?: string;
};
type ZolUserInfoResponse = {
data?: {
userId?: string | number;
nickName?: string;
photo?: string;
};
};
type DongchediAccountInfoResponse = {
data?: {
user_id_str?: string;
name?: string;
avatar_url?: string;
};
};
const activeBindOperations = new Map<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;
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 hashText(value: string): string {
return createHash("sha1").update(value).digest("hex").slice(0, 20);
}
function boundedIdentity(prefix: string, raw: string): string {
const normalizedPrefix = prefix.trim().replace(/:+$/, "");
const normalizedRaw = raw.trim();
if (!normalizedRaw) {
return `${normalizedPrefix}:${hashText(prefix)}`;
}
const candidate = `${normalizedPrefix}:${normalizedRaw}`;
if (candidate.length <= 120) {
return candidate;
}
return `${normalizedPrefix}:${hashText(normalizedRaw)}`;
}
function detectedAccountFromFields(input: {
uid?: unknown;
name?: unknown;
avatar?: unknown;
}): DetectedAccount | null {
const platformUid = stringId(input.uid);
const displayName = normalizeText(input.name);
if (!platformUid || !displayName) {
return null;
}
return {
platformUid,
displayName,
avatarUrl: normalizeRemoteUrl(stringId(input.avatar) || normalizeText(input.avatar)),
};
}
function extractToutiaoAccount(response: ToutiaoMediaInfoResponse | null | undefined): DetectedAccount | null {
const candidates = [
detectedAccountFromFields({
uid: response?.data?.user?.id_str,
name: response?.data?.user?.screen_name,
avatar: response?.data?.user?.https_avatar_url,
}),
detectedAccountFromFields({
uid: response?.data?.user_info?.id_str ?? response?.data?.user_info?.user_id_str,
name: response?.data?.user_info?.screen_name ?? response?.data?.user_info?.name,
avatar: response?.data?.user_info?.https_avatar_url ?? response?.data?.user_info?.avatar_url,
}),
detectedAccountFromFields({
uid: response?.data?.media?.media_id ?? response?.data?.media?.id,
name: response?.data?.media?.screen_name ?? response?.data?.media?.media_name,
avatar: response?.data?.media?.https_avatar_url ?? response?.data?.media?.avatar_url,
}),
];
return candidates.find((item) => item !== null) ?? null;
}
async function detectToutiaoFromPageState(
webContents: WebContents,
): Promise<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),
};
}
interface GenericAIPageState {
displayName: string | null;
avatarUrl: string | null;
fingerprint: string | null;
credentialFingerprint: string | null;
currentPath: string | null;
strongAuthSignalCount: number;
loginSignalCount: number;
loggedOutSignalCount: number;
challengeSignalCount: number;
}
async function readGenericAIPageState(
webContents: WebContents | null | undefined,
): Promise<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 genericAIConversationPath(pathname: string): boolean {
return /^\/(?:chat|c|conversation)(?:\/|$)/i.test(pathname);
}
function isLikelyGenericAICredentialName(name: string): boolean {
const normalized = name.trim().toLowerCase();
if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) {
return false;
}
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name);
}
function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean {
const normalized = normalizeText(value);
if (!normalized) {
return false;
}
if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) {
return false;
}
return normalized.length >= 8;
}
function genericAIPageLooksAuthenticated(currentURL: string, pageState: GenericAIPageState | null): boolean {
const current = safeParseURL(currentURL);
const currentPath = normalizeURLPath(current?.pathname ?? pageState?.currentPath ?? "");
const strongAuthSignalCount = pageState?.strongAuthSignalCount ?? 0;
const loginSignalCount = pageState?.loginSignalCount ?? 0;
const loggedOutSignalCount = pageState?.loggedOutSignalCount ?? 0;
const onConversationPath = genericAIConversationPath(currentPath);
if (loggedOutSignalCount > 0) {
return false;
}
if (loginSignalCount > 0 && strongAuthSignalCount === 0 && !onConversationPath) {
return false;
}
if (loginSignalCount > 0) {
return strongAuthSignalCount >= 2 && !onConversationPath;
}
return strongAuthSignalCount > 0 || onConversationPath;
}
async function buildGenericAISessionFingerprint(
platformId: string,
session: Session,
pageState?: GenericAIPageState | null,
): Promise<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 null;
}
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 (!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 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 detectAIPlatformAccount(
platformId: string,
label: string,
context: DetectContext,
): Promise<DetectedAccount | null> {
if (platformId === "qwen") {
return await detectQwenPlatform(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 (looksLikeLoginRedirect(currentURL, definition.loginUrl)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (account.platform === "toutiaohao") {
const detected = await detectToutiaoFromSession(handle.session);
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
if (consoleReady) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account),
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
};
}
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
};
}
if (isAIPlatformBinding(account.platform)) {
const pageState = await readGenericAIPageState(window.webContents);
if ((pageState?.challengeSignalCount ?? 0) > 0) {
return {
verdict: "challenge_required",
reason: "captcha_gate",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
},
};
}
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
session: handle.session,
webContents: window.webContents,
});
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: detected.platformUid,
};
}
if (!pageState) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
}
if (pageState.loggedOutSignalCount > 0 || looksLikeLoginRedirect(currentURL, definition.loginUrl)) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
loggedOutSignalCount: pageState.loggedOutSignalCount,
},
};
}
if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) {
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
loginSignalCount: pageState.loginSignalCount,
},
};
}
const fingerprint = await buildGenericAISessionFingerprint(account.platform, handle.session, pageState);
if (genericAIPageLooksAuthenticated(currentURL, pageState)) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account, {
displayName: pageState.displayName ?? account.displayName,
avatarUrl: pageState.avatarUrl ?? null,
}),
sessionFingerprint: fingerprint,
};
}
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: fingerprint,
evidence: {
url: currentURL,
strongAuthSignalCount: pageState.strongAuthSignalCount,
loginSignalCount: pageState.loginSignalCount,
loggedOutSignalCount: pageState.loggedOutSignalCount,
},
};
}
const detected = await definition.detect({
session: handle.session,
webContents: window.webContents,
});
if (detected) {
return {
verdict: "active",
reason: "probe_success",
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
};
}
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle);
if (consoleReady) {
return {
verdict: "active",
reason: "probe_success",
profile: fallbackAccountHealthProfile(account),
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
};
}
return {
verdict: "expired",
reason: "login_redirect",
profile: null,
sessionFingerprint: null,
evidence: {
url: currentURL,
},
};
} catch (error) {
return {
verdict: "network_error",
reason: "network_error",
profile: null,
sessionFingerprint: null,
evidence: {
message: error instanceof Error ? error.message : String(error),
},
};
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
export async function silentRefreshPublishAccountSession(
account: PublishAccountIdentity,
partition?: string,
): Promise<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);
return !looksLikeLoginRedirect(window.webContents.getURL(), definition.loginUrl);
} 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> {
if (detected.avatarUrl) {
return detected;
}
let resolved = detected;
for (let attempt = 0; attempt < 3; attempt += 1) {
await sleep(400 + attempt * 250);
const retried = await definition.detect(context).catch(() => null);
if (!retried) {
continue;
}
const sanitized = sanitizeDetectedAccount(retried);
resolved = sanitized;
if (sanitized.avatarUrl) {
break;
}
}
return resolved;
}
function isToutiaoLoginURL(url: string): boolean {
return url.startsWith(TOUTIAO_LOGIN_URL);
}
async function waitForWindowNavigationSettled(window: BrowserWindow, timeoutMs = 8_000): Promise<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 looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
if (!currentURL || !loginURL) {
return false;
}
try {
const current = new URL(currentURL);
const login = new URL(loginURL);
if (current.origin !== login.origin) {
return false;
}
const currentPath = normalizeURLPath(current.pathname);
const loginPath = normalizeURLPath(login.pathname);
if (!loginPath) {
return /(^|[/?#=&._-])(login|signin|sign-in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i
.test(`${current.pathname}${current.search}${current.hash}`);
}
return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`);
} catch {
return false;
}
}
async function verifyPublishAccountConsoleAccess(
account: PublishAccountIdentity,
handle: SessionHandle,
): Promise<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;
}
return !looksLikeLoginRedirect(currentURL, definition.loginUrl);
} 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 detectBilibili({ session }: DetectContext): Promise<DetectedAccount | null> {
const cookie = await sessionCookieHeader(session, "bilibili.com");
const response = await sessionFetchJson<BilibiliNavResponse>(
session,
"https://api.bilibili.com/x/web-interface/nav?build=0&mobi_app=web",
{
headers: {
accept: "application/json, text/plain, */*",
...(cookie ? { cookie } : {}),
},
},
).catch(() => null);
const user = response?.data;
const uid = stringId(user?.mid);
if (response?.code !== 0 || !user?.isLogin || !uid || !user.uname) {
return null;
}
return {
platformUid: uid,
displayName: user.uname,
avatarUrl: normalizeRemoteUrl(user.face),
};
}
async function detectJuejin({ session }: DetectContext): Promise<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),
};
}
async function detectSmzdm({ session }: DetectContext): Promise<DetectedAccount | null> {
const response = await sessionFetchJson<SmzdmCurrentUser>(
session,
"https://zhiyou.smzdm.com/user/info/jsonp_get_current",
).catch(() => null);
const uid = stringId(response?.smzdm_id);
const nickname = response?.nickname || response?.audit_nickname || "";
if (!uid || !nickname) {
return null;
}
return {
platformUid: uid,
displayName: nickname,
avatarUrl: normalizeRemoteUrl(response?.avatar),
};
}
async function detectWeixinGzh({ session }: DetectContext): Promise<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 detectZol({ session }: DetectContext): Promise<DetectedAccount | null> {
const response = await sessionFetchJson<ZolUserInfoResponse>(
session,
"https://open-api.zol.com.cn/api/v1/creator.user.getinfo",
{
headers: {
accept: "application/json, text/plain, */*",
},
},
).catch(() => null);
const user = response?.data;
const uid = stringId(user?.userId);
if (!uid || !user?.nickName) {
return null;
}
return {
platformUid: uid,
displayName: user.nickName,
avatarUrl: normalizeRemoteUrl(user.photo),
};
}
async function detectDongchedi({ session }: DetectContext): Promise<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/news/create",
detect: detectSohu,
},
qiehao: {
id: "qiehao",
label: "企鹅号",
loginUrl: "https://om.qq.com",
consoleUrl: "https://om.qq.com/main.html#/article/add",
detect: detectQiehao,
},
zhihu: {
id: "zhihu",
label: "知乎",
loginUrl: "https://www.zhihu.com/signin",
consoleUrl: "https://zhuanlan.zhihu.com/write",
detect: detectZhihu,
},
wangyihao: {
id: "wangyihao",
label: "网易号",
loginUrl: "https://mp.163.com/login.html",
consoleUrl: "https://mp.163.com/index.html#/article/create",
detect: detectWangyihao,
},
jianshu: {
id: "jianshu",
label: "简书",
loginUrl: "https://www.jianshu.com/sign_in",
consoleUrl: "https://www.jianshu.com/writer",
detect: detectJianshu,
},
bilibili: {
id: "bilibili",
label: "bilibili",
loginUrl: "https://www.bilibili.com",
consoleUrl: "https://member.bilibili.com/platform/upload/text/edit",
detect: detectBilibili,
},
juejin: {
id: "juejin",
label: "稀土掘金",
loginUrl: "https://juejin.cn/login",
consoleUrl: "https://juejin.cn/editor/drafts/new/v3",
detect: detectJuejin,
},
smzdm: {
id: "smzdm",
label: "什么值得买",
loginUrl: "https://zhiyou.smzdm.com/user/login",
consoleUrl: "https://post.smzdm.com/",
detect: detectSmzdm,
},
weixin_gzh: {
id: "weixin_gzh",
label: "微信公众号",
loginUrl: "https://mp.weixin.qq.com/cgi-bin/loginpage",
consoleUrl: "https://mp.weixin.qq.com/cgi-bin/home",
detect: detectWeixinGzh,
},
zol: {
id: "zol",
label: "中关村在线",
loginUrl: "https://post.zol.com.cn/v2/manage/works/all",
consoleUrl: "https://post.zol.com.cn/v2/manage/works/all",
detect: detectZol,
},
dongchedi: {
id: "dongchedi",
label: "懂车帝",
loginUrl: "https://mp.dcdapp.com/login",
consoleUrl: "https://mp.dcdapp.com/content/article/create",
detect: detectDongchedi,
},
};
const aiBindingDefinitions: Record<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 window = new BrowserWindow({
show: options.show ?? true,
width: 1320,
height: 900,
minWidth: 1100,
minHeight: 760,
title,
autoHideMenuBar: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
webPreferences: {
session: sessionHandle,
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
});
window.webContents.setUserAgent(STANDARD_USER_AGENT);
attachWindowDiagnostics(window, title);
window.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:\/\//i.test(url)) {
setImmediate(() => {
if (!window.isDestroyed()) {
void loadWindowURLSafely(window, url, `${title} popup`);
}
});
}
return { action: "deny" };
});
void loadWindowURLSafely(window, targetURL, title);
return window;
}
function focusBindWindow(window: BrowserWindow): void {
if (window.isDestroyed()) {
return;
}
if (window.isMinimized()) {
window.restore();
}
if (!window.isVisible()) {
window.show();
}
window.focus();
}
function cleanupInactiveBindOperations(): void {
for (const [platformId, operation] of activeBindOperations.entries()) {
if (operation.window.isDestroyed()) {
activeBindOperations.delete(platformId);
}
}
}
function clearActiveBindOperation(platformId: string, window: BrowserWindow): void {
const operation = activeBindOperations.get(platformId);
if (operation?.window === window) {
activeBindOperations.delete(platformId);
}
}
export async function bindPublishAccount(platformId: string): Promise<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;
const finalizeOperation = () => {
clearInterval(intervalHandle);
clearActiveBindOperation(platformId, window);
};
const settleSuccess = (account: DesktopAccountInfo) => {
if (finished) {
return;
}
finished = true;
finalizeOperation();
attachSessionHandle(account.id, handle);
resolve(account);
if (!window.isDestroyed()) {
window.setTitle(`${definition.label}${account.display_name} 已绑定`);
setTimeout(() => {
if (!window.isDestroyed()) {
window.close();
}
}, 800);
}
};
const settleFailure = (error: Error) => {
if (finished) {
return;
}
finished = true;
finalizeOperation();
forgetSessionHandle(handle.accountId);
if (!window.isDestroyed()) {
window.close();
}
reject(error);
};
const syncDetectionState = () => {
if (window.isDestroyed()) {
detectReady = false;
return;
}
const currentURL = window.webContents.getURL();
const fallbackReady =
/^https?:\/\//i.test(currentURL)
&& !currentURL.startsWith("data:text/html")
&& !window.webContents.isLoading();
detectReady = isWindowReadyForDetection(window) || fallbackReady;
};
window.webContents.on("did-finish-load", syncDetectionState);
window.webContents.on("did-navigate", syncDetectionState);
window.webContents.on("did-navigate-in-page", syncDetectionState);
window.webContents.on(
"did-fail-load",
(_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => {
if (isMainFrame && errorCode !== -3) {
detectReady = false;
}
},
);
const detectAndBind = async () => {
const currentURL = window.isDestroyed() ? "" : window.webContents.getURL();
const canProbe = detectReady || /^https?:\/\//i.test(currentURL);
if (finished || checking || !canProbe || window.isDestroyed()) {
return;
}
checking = true;
try {
console.info("[desktop-bind] detect tick", {
platform: definition.id,
detectReady,
canProbe,
loading: window.webContents.isLoading(),
url: currentURL,
});
const detected = await definition
.detect({ session: handle.session, webContents: window.webContents })
.catch((error) => {
console.warn(`[desktop-bind] ${definition.id} detect failed`, {
message: error instanceof Error ? error.message : String(error),
});
return null;
});
if (!detected) {
return;
}
let normalizedDetected = sanitizeDetectedAccount(detected);
console.info("[desktop-bind] detect success", {
platform: definition.id,
displayName: normalizedDetected.displayName,
platformUid: normalizedDetected.platformUid,
url: currentURL,
});
if (definition.id === "toutiaohao") {
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
console.info("[desktop-bind] toutiao console verification", {
platform: definition.id,
ready: consoleReady,
url: window.isDestroyed() ? "" : window.webContents.getURL(),
});
if (!consoleReady) {
return;
}
const detectedFromWorkbench = await definition
.detect({ session: handle.session, webContents: window.webContents })
.catch(() => null);
if (detectedFromWorkbench) {
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench);
}
}
normalizedDetected = await finalizeDetectedAccountForBind(
definition,
{ session: handle.session, webContents: window.webContents },
normalizedDetected,
);
await flushSessionPersistence(handle.session);
try {
const account = await upsertDesktopAccount({
platform: definition.id,
platform_uid: normalizedDetected.platformUid,
display_name: normalizedDetected.displayName,
avatar_url: normalizedDetected.avatarUrl,
account_fingerprint: normalizedDetected.platformUid,
health: "live",
verified_at: new Date().toISOString(),
tags: [],
});
settleSuccess(account);
} catch (error) {
console.error("[desktop-bind] upsert failed", {
platform: definition.id,
url: currentURL,
message: error instanceof Error ? error.message : String(error),
});
if (!window.isDestroyed()) {
window.setTitle(`${definition.label} 绑定保存失败,保留窗口重试`);
}
return;
}
} catch (error) {
console.error("[desktop-bind] bind failed", {
platform: definition.id,
url: currentURL,
message: error instanceof Error ? error.message : String(error),
});
settleFailure(error instanceof Error ? error : new Error("desktop_account_bind_failed"));
} finally {
checking = false;
}
};
const intervalHandle = setInterval(() => {
void detectAndBind();
}, 1800);
window.on("closed", () => {
clearActiveBindOperation(platformId, window);
if (!finished) {
clearInterval(intervalHandle);
forgetSessionHandle(handle.accountId);
reject(new Error("desktop_account_bind_window_closed"));
}
});
syncDetectionState();
void detectAndBind();
});
activeBindOperations.set(platformId, {
platformId,
window,
promise: bindPromise,
});
focusBindWindow(window);
return await bindPromise;
}
export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise<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();
}