b89892be6e
Snapshot 163.com/126.net cookies on bind/refresh to a userData vault and restore them when reattaching a session, recover orphan partitions by matching the detected mediaInfo, verify the bind window via the console state instead of a generic profile probe, and route token/draft fetches through the https API origin while opening console pages over http to match the live editor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3986 lines
116 KiB
TypeScript
3986 lines
116 KiB
TypeScript
import { createHash } from "node:crypto";
|
||
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||
import { dirname, join } from "node:path";
|
||
|
||
import { BrowserWindow, app, nativeTheme, session as electronSession } from "electron/main";
|
||
import type { Cookie, Session, WebContents } from "electron/main";
|
||
import { aiPlatformCatalog } from "@geo/shared-types";
|
||
import type { DesktopAccountInfo } from "@geo/shared-types";
|
||
|
||
import {
|
||
normalizeRemoteUrl,
|
||
pageFetchJson,
|
||
sessionCookieFetchJson,
|
||
sessionCookieHeader,
|
||
sessionCookieValue,
|
||
sessionFetchJson,
|
||
sessionFetchText,
|
||
} from "./adapters/common";
|
||
import {
|
||
attachSessionHandle,
|
||
createPendingSessionHandle,
|
||
createSessionHandle,
|
||
createSessionHandleForPartition,
|
||
forgetSessionHandle,
|
||
getPersistedPartition,
|
||
getSessionHandle,
|
||
type SessionHandle,
|
||
} from "./session-registry";
|
||
import {
|
||
attachWindowDiagnostics,
|
||
isWindowReadyForDetection,
|
||
loadWindowURLSafely,
|
||
} from "./external-window";
|
||
import {
|
||
buildGenericAIPageFingerprintFallback,
|
||
genericAIPageLooksAuthenticated,
|
||
type GenericAIPageState,
|
||
} from "./generic-ai-auth";
|
||
import { upsertDesktopAccount } from "./transport/api-client";
|
||
import { STANDARD_USER_AGENT } from "./user-agent";
|
||
import {
|
||
normalizeAccountPlatformUid,
|
||
sameAccountPlatformUid,
|
||
} from "../shared/account-identity";
|
||
import type { AccountHealthProfile, AuthProbeResult } from "./auth-types";
|
||
|
||
interface DetectedAccount {
|
||
platformUid: string;
|
||
displayName: string;
|
||
avatarUrl: string | null;
|
||
}
|
||
|
||
interface DetectContext {
|
||
session: Session;
|
||
webContents: WebContents;
|
||
}
|
||
|
||
interface PublishPlatformBindingDefinition {
|
||
id: string;
|
||
label: string;
|
||
loginUrl: string;
|
||
consoleUrl: string;
|
||
loginRedirectUrls?: string[];
|
||
detect(context: DetectContext): Promise<DetectedAccount | null>;
|
||
}
|
||
|
||
export interface PublishAccountIdentity {
|
||
id: string;
|
||
platform: string;
|
||
platformUid: string;
|
||
displayName: string;
|
||
}
|
||
|
||
export interface PublishAccountProfile {
|
||
platformUid: string;
|
||
displayName: string;
|
||
avatarUrl: string | null;
|
||
}
|
||
|
||
export interface PublishAccountLocalInspection {
|
||
availability: "missing" | "ready" | "expired";
|
||
profile: PublishAccountProfile | null;
|
||
}
|
||
|
||
interface ActiveBindOperation {
|
||
platformId: string;
|
||
window: BrowserWindow;
|
||
promise: Promise<DesktopAccountInfo>;
|
||
}
|
||
|
||
interface WeixinGzhConsoleState {
|
||
loggedOut: boolean;
|
||
title: string;
|
||
textSnippet: string;
|
||
}
|
||
|
||
type ToutiaoMediaInfoResponse = {
|
||
data?: {
|
||
user?: {
|
||
id_str?: string | number;
|
||
screen_name?: string;
|
||
https_avatar_url?: string;
|
||
};
|
||
user_info?: {
|
||
id_str?: string | number;
|
||
user_id_str?: string | number;
|
||
screen_name?: string;
|
||
name?: string;
|
||
https_avatar_url?: string;
|
||
avatar_url?: string;
|
||
};
|
||
media?: {
|
||
id?: string | number;
|
||
media_id?: string | number;
|
||
media_name?: string;
|
||
screen_name?: string;
|
||
https_avatar_url?: string;
|
||
avatar_url?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type BaijiahaoAppInfoResponse = {
|
||
data?: {
|
||
user?: {
|
||
userid?: string | number;
|
||
name?: string;
|
||
uname?: string;
|
||
username?: string;
|
||
avatar?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type SohuRegisterInfoResponse = {
|
||
data?: {
|
||
account?: {
|
||
id?: string | number;
|
||
nickName?: string;
|
||
avatar?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type QiehaoBasicInfoResponse = {
|
||
data?: {
|
||
cpInfo?: {
|
||
mediaId?: string | number;
|
||
mediaName?: string;
|
||
header?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type ZhihuMeResponse = {
|
||
uid?: string;
|
||
id?: string;
|
||
name?: string;
|
||
avatar_url?: string;
|
||
};
|
||
|
||
type WangyiInfoResponse = {
|
||
code?: number | string;
|
||
message?: string;
|
||
msg?: string;
|
||
data?: {
|
||
mediaInfo?: {
|
||
userId?: string | number;
|
||
wemediaId?: string | number;
|
||
tname?: string;
|
||
icon?: string;
|
||
};
|
||
};
|
||
};
|
||
type WangyiInfoFetchOptions = {
|
||
allowCookieWrite?: boolean;
|
||
};
|
||
type StoredWangyihaoCookie = Pick<
|
||
Cookie,
|
||
"expirationDate" | "hostOnly" | "httpOnly" | "name" | "path" | "sameSite" | "secure" | "session" | "value"
|
||
> & {
|
||
domain: string;
|
||
};
|
||
type WangyihaoCookieVault = Record<string, {
|
||
updatedAt: number;
|
||
cookies: StoredWangyihaoCookie[];
|
||
}>;
|
||
|
||
type JianshuUser = {
|
||
id?: string | number;
|
||
nickname?: string;
|
||
avatar?: string;
|
||
};
|
||
|
||
type BilibiliNavResponse = {
|
||
code?: number;
|
||
data?: {
|
||
isLogin?: boolean;
|
||
mid?: string | number;
|
||
uname?: string;
|
||
face?: string;
|
||
wbi_img?: {
|
||
img_url?: string;
|
||
sub_url?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type BilibiliSpaceInfoResponse = {
|
||
code?: number;
|
||
data?: {
|
||
face?: string;
|
||
};
|
||
};
|
||
|
||
type JuejinProfileResponse = {
|
||
data?: {
|
||
bui_user?: {
|
||
user_id?: string;
|
||
screen_name?: string;
|
||
avatar_large?: string;
|
||
avatar_url?: string;
|
||
};
|
||
};
|
||
};
|
||
|
||
type SmzdmCurrentUser = {
|
||
smzdm_id?: string | number;
|
||
nickname?: string;
|
||
audit_nickname?: string;
|
||
avatar?: string;
|
||
data?: {
|
||
smzdm_id?: string | number;
|
||
nickname?: string;
|
||
audit_nickname?: string;
|
||
avatar?: string;
|
||
};
|
||
};
|
||
|
||
type SmzdmPageState = {
|
||
displayName: string | null;
|
||
avatarUrl: string | null;
|
||
loggedInPage: boolean;
|
||
};
|
||
|
||
type ZolUserInfoResponse = {
|
||
data?: {
|
||
userId?: string | number;
|
||
nickName?: string;
|
||
photo?: string;
|
||
};
|
||
};
|
||
|
||
type DongchediAccountInfoResponse = {
|
||
data?: {
|
||
user_id_str?: string;
|
||
name?: string;
|
||
avatar_url?: string;
|
||
};
|
||
};
|
||
|
||
const activeBindOperations = new Map<string, ActiveBindOperation>();
|
||
const MAX_CONCURRENT_BIND_WINDOWS = 2;
|
||
|
||
const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login";
|
||
const TOUTIAO_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index";
|
||
const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i;
|
||
const WEIXIN_GZH_HOME_URL = "https://mp.weixin.qq.com/";
|
||
const WEIXIN_GZH_LOGIN_URL = "https://mp.weixin.qq.com/cgi-bin/loginpage";
|
||
const WEIXIN_GZH_DRAFT_LIST_URL = "https://mp.weixin.qq.com/cgi-bin/appmsg";
|
||
const BILIBILI_API_ORIGIN = "https://api.bilibili.com";
|
||
const ZOL_API_ORIGIN = "https://open-api.zol.com.cn";
|
||
const ZOL_REFERER = "https://post.zol.com.cn/";
|
||
const ZOL_CONSOLE_URL = "https://post.zol.com.cn/v2/manage/works/all";
|
||
const ZOL_LOGIN_REDIRECT_URLS = [
|
||
"https://passport.zol.com.cn/",
|
||
"https://service.zol.com.cn/user/login",
|
||
];
|
||
const WANGYI_API_ORIGIN = "https://mp.163.com";
|
||
const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com";
|
||
const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`;
|
||
const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
|
||
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
|
||
const WANGYI_LOGIN_REDIRECT_URLS = [
|
||
`${WANGYI_API_ORIGIN}/login.html`,
|
||
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
|
||
"https://reg.163.com/",
|
||
"https://dl.reg.163.com/",
|
||
];
|
||
const WANGYI_COOKIE_URLS = [
|
||
WANGYI_API_ORIGIN,
|
||
WANGYI_CONSOLE_ORIGIN,
|
||
"https://reg.163.com/",
|
||
"https://dl.reg.163.com/",
|
||
];
|
||
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
|
||
46, 47, 18, 2, 53, 8, 23, 32,
|
||
15, 50, 10, 31, 58, 3, 45, 35,
|
||
27, 43, 5, 49, 33, 9, 42, 19,
|
||
29, 28, 14, 39, 12, 38, 41, 13,
|
||
37, 48, 7, 16, 24, 55, 40, 61,
|
||
26, 17, 0, 1, 60, 51, 30, 4,
|
||
22, 25, 54, 21, 56, 59, 6, 63,
|
||
57, 62, 11, 36, 20, 34, 44, 52,
|
||
];
|
||
|
||
function sleep(ms: number): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
setTimeout(resolve, ms);
|
||
});
|
||
}
|
||
|
||
function normalizeComparableText(value: string): string {
|
||
return value.trim().replace(/\s+/g, "").toLowerCase();
|
||
}
|
||
|
||
function partitionsRootPath(): string {
|
||
return join(app.getPath("userData"), "Partitions");
|
||
}
|
||
|
||
function listPendingPartitions(platformId: string): string[] {
|
||
try {
|
||
return readdirSync(partitionsRootPath(), { withFileTypes: true })
|
||
.filter((entry) => entry.isDirectory() && entry.name.startsWith(`pending-${platformId}-`))
|
||
.map((entry) => {
|
||
const absolutePath = join(partitionsRootPath(), entry.name);
|
||
let mtimeMs = 0;
|
||
try {
|
||
mtimeMs = statSync(absolutePath).mtimeMs;
|
||
} catch {
|
||
mtimeMs = 0;
|
||
}
|
||
return {
|
||
partition: `persist:${entry.name}`,
|
||
mtimeMs,
|
||
};
|
||
})
|
||
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
||
.map((item) => item.partition);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function normalizeText(value: unknown): string | null {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
return trimmed ? trimmed : null;
|
||
}
|
||
|
||
function stringId(value: unknown): string {
|
||
if (typeof value === "number") {
|
||
return Number.isFinite(value) ? String(value) : "";
|
||
}
|
||
return normalizeText(value) ?? "";
|
||
}
|
||
|
||
function decodeCookieText(value: string): string {
|
||
const source = value.replace(/\+/g, " ");
|
||
try {
|
||
return decodeURIComponent(source);
|
||
} catch {
|
||
return source;
|
||
}
|
||
}
|
||
|
||
function hashText(value: string): string {
|
||
return createHash("sha1").update(value).digest("hex").slice(0, 20);
|
||
}
|
||
|
||
function buildBilibiliMixinKey(imgKey: string, subKey: string): string {
|
||
const raw = imgKey + subKey;
|
||
return BILIBILI_WBI_MIXIN_KEY_ENC_TAB
|
||
.map((index) => raw.charAt(index))
|
||
.filter(Boolean)
|
||
.join("")
|
||
.slice(0, 32);
|
||
}
|
||
|
||
function bilibiliKeyFromWbiURL(value: string | undefined): string {
|
||
const source = value?.trim() || "";
|
||
if (!source) {
|
||
return "";
|
||
}
|
||
const slashIndex = source.lastIndexOf("/");
|
||
const dotIndex = source.lastIndexOf(".");
|
||
if (slashIndex < 0 || dotIndex <= slashIndex) {
|
||
return "";
|
||
}
|
||
return source.slice(slashIndex + 1, dotIndex);
|
||
}
|
||
|
||
function buildBilibiliWbiQuery(
|
||
nav: BilibiliNavResponse | null | undefined,
|
||
params: Record<string, string | number>,
|
||
): string | null {
|
||
const imgKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.img_url);
|
||
const subKey = bilibiliKeyFromWbiURL(nav?.data?.wbi_img?.sub_url);
|
||
if (!imgKey || !subKey) {
|
||
return null;
|
||
}
|
||
|
||
const mixinKey = buildBilibiliMixinKey(imgKey, subKey);
|
||
const next: Record<string, string | number> = {
|
||
...params,
|
||
wts: Math.round(Date.now() / 1000),
|
||
};
|
||
const illegalChars = /[!'()*]/g;
|
||
const query = Object.keys(next)
|
||
.sort()
|
||
.map((key) => {
|
||
const value = String(next[key]).replace(illegalChars, "");
|
||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||
})
|
||
.join("&");
|
||
const wRid = createHash("md5").update(`${query}${mixinKey}`).digest("hex");
|
||
return `${query}&w_rid=${wRid}`;
|
||
}
|
||
|
||
function normalizeBilibiliAvatarUrl(value?: string | null): string | null {
|
||
return normalizeRemoteUrl(value)?.replace(/^http:\/\//i, "https://") ?? null;
|
||
}
|
||
|
||
function boundedIdentity(prefix: string, raw: string): string {
|
||
const normalizedPrefix = prefix.trim().replace(/:+$/, "");
|
||
const normalizedRaw = raw.trim();
|
||
if (!normalizedRaw) {
|
||
return `${normalizedPrefix}:${hashText(prefix)}`;
|
||
}
|
||
|
||
const candidate = `${normalizedPrefix}:${normalizedRaw}`;
|
||
if (candidate.length <= 120) {
|
||
return candidate;
|
||
}
|
||
|
||
return `${normalizedPrefix}:${hashText(normalizedRaw)}`;
|
||
}
|
||
|
||
function detectedAccountFromFields(input: {
|
||
uid?: unknown;
|
||
name?: unknown;
|
||
avatar?: unknown;
|
||
}): DetectedAccount | null {
|
||
const platformUid = stringId(input.uid);
|
||
const displayName = normalizeText(input.name);
|
||
if (!platformUid || !displayName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid,
|
||
displayName,
|
||
avatarUrl: normalizeRemoteUrl(stringId(input.avatar) || normalizeText(input.avatar)),
|
||
};
|
||
}
|
||
|
||
function extractToutiaoAccount(response: ToutiaoMediaInfoResponse | null | undefined): DetectedAccount | null {
|
||
const candidates = [
|
||
detectedAccountFromFields({
|
||
uid: response?.data?.user?.id_str,
|
||
name: response?.data?.user?.screen_name,
|
||
avatar: response?.data?.user?.https_avatar_url,
|
||
}),
|
||
detectedAccountFromFields({
|
||
uid: response?.data?.user_info?.id_str ?? response?.data?.user_info?.user_id_str,
|
||
name: response?.data?.user_info?.screen_name ?? response?.data?.user_info?.name,
|
||
avatar: response?.data?.user_info?.https_avatar_url ?? response?.data?.user_info?.avatar_url,
|
||
}),
|
||
detectedAccountFromFields({
|
||
uid: response?.data?.media?.media_id ?? response?.data?.media?.id,
|
||
name: response?.data?.media?.screen_name ?? response?.data?.media?.media_name,
|
||
avatar: response?.data?.media?.https_avatar_url ?? response?.data?.media?.avatar_url,
|
||
}),
|
||
];
|
||
|
||
return candidates.find((item) => item !== null) ?? null;
|
||
}
|
||
|
||
function extractWeixinGzhToken(html: string): string {
|
||
const patterns = [
|
||
/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/,
|
||
/(?:\?|&|&)token=(\d+)(?:&|&|$)/,
|
||
/["']?token["']?\s*[:=]\s*["']?(\d+)["']?/,
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
const token = html.match(pattern)?.[1]?.trim();
|
||
if (token) {
|
||
return token;
|
||
}
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
function buildWeixinGzhDraftListURL(token?: string): string {
|
||
const url = new URL(WEIXIN_GZH_DRAFT_LIST_URL);
|
||
url.searchParams.set("begin", "0");
|
||
url.searchParams.set("count", "10");
|
||
url.searchParams.set("type", "77");
|
||
url.searchParams.set("action", "list_card");
|
||
if (token) {
|
||
url.searchParams.set("token", token);
|
||
}
|
||
url.searchParams.set("lang", "zh_CN");
|
||
return url.toString();
|
||
}
|
||
|
||
function isWeixinGzhLoggedOutText(text: string): boolean {
|
||
return /(登录超时|登录态超时|登录已过期|会话已过期|请重新登录|未登录)/.test(text);
|
||
}
|
||
|
||
async function readWeixinGzhConsoleState(
|
||
webContents: WebContents | null | undefined,
|
||
): Promise<WeixinGzhConsoleState | null> {
|
||
if (!webContents || webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const state = await webContents.executeJavaScript(
|
||
`(() => {
|
||
try {
|
||
const text = String(document.body?.innerText || "");
|
||
const title = String(document.title || "");
|
||
return {
|
||
title,
|
||
textSnippet: text.slice(0, 500),
|
||
loggedOut: /(登录超时|登录态超时|登录已过期|会话已过期|请重新登录|未登录)/.test(text + "\\n" + title),
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();`,
|
||
true,
|
||
).catch(() => null);
|
||
|
||
if (!state || typeof state !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const typed = state as Partial<WeixinGzhConsoleState>;
|
||
return {
|
||
loggedOut:
|
||
typed.loggedOut === true
|
||
|| isWeixinGzhLoggedOutText(`${typed.title ?? ""}\n${typed.textSnippet ?? ""}`),
|
||
title: typeof typed.title === "string" ? typed.title : "",
|
||
textSnippet: typeof typed.textSnippet === "string" ? typed.textSnippet : "",
|
||
};
|
||
}
|
||
|
||
async function resolveWeixinGzhConsoleURL(session: Session): Promise<string> {
|
||
const html = await sessionFetchText(
|
||
session,
|
||
WEIXIN_GZH_HOME_URL,
|
||
{
|
||
credentials: "include",
|
||
headers: {
|
||
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||
referer: WEIXIN_GZH_HOME_URL,
|
||
},
|
||
},
|
||
).catch(() => "");
|
||
|
||
const token = extractWeixinGzhToken(html);
|
||
if (!token) {
|
||
return WEIXIN_GZH_LOGIN_URL;
|
||
}
|
||
|
||
return buildWeixinGzhDraftListURL(token);
|
||
}
|
||
|
||
async function resolvePublishAccountConsoleURL(
|
||
definition: PublishPlatformBindingDefinition,
|
||
session: Session,
|
||
): Promise<string> {
|
||
if (definition.id === "weixin_gzh") {
|
||
return await resolveWeixinGzhConsoleURL(session);
|
||
}
|
||
return definition.consoleUrl;
|
||
}
|
||
|
||
async function detectToutiaoFromPageState(
|
||
webContents: WebContents,
|
||
): Promise<Pick<DetectedAccount, "platformUid" | "displayName" | "avatarUrl"> | null> {
|
||
if (!webContents || webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const serialized = await webContents.executeJavaScript(`(() => {
|
||
try {
|
||
const normalizeText = (value) => typeof value === "string" ? value.trim() : "";
|
||
const stringId = (value) => typeof value === "number"
|
||
? Number.isFinite(value) ? String(value) : ""
|
||
: normalizeText(value);
|
||
const pickAccount = (value) => {
|
||
if (!value || typeof value !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const uid = stringId(
|
||
value.id_str
|
||
?? value.user_id_str
|
||
?? value.media_id
|
||
?? value.mediaId
|
||
?? value.uid
|
||
?? value.user_id
|
||
?? value.id,
|
||
);
|
||
const displayName = normalizeText(
|
||
value.screen_name
|
||
?? value.nick_name
|
||
?? value.nickname
|
||
?? value.user_name
|
||
?? value.media_name
|
||
?? value.mediaName
|
||
?? value.name,
|
||
);
|
||
const avatarUrl = normalizeText(
|
||
value.https_avatar_url
|
||
?? value.avatar_url
|
||
?? value.avatar
|
||
?? value.head_img,
|
||
);
|
||
|
||
if (!uid || !displayName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName,
|
||
avatarUrl: avatarUrl || null,
|
||
};
|
||
};
|
||
const extractUidFromText = (value) => {
|
||
if (!value) {
|
||
return "";
|
||
}
|
||
|
||
const source = String(value);
|
||
const patterns = [
|
||
/"id_str":"([^"]+)"/,
|
||
/"user_id_str":"([^"]+)"/,
|
||
/"media_id":"?([^",}]+)"?/,
|
||
/"mediaId":"?([^",}]+)"?/,
|
||
/\b(?:id_str|user_id_str|media_id|mediaId|uid|user_id)\b[^0-9A-Za-z_-]*([0-9A-Za-z_-]{4,})/i,
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
const match = source.match(pattern);
|
||
if (match?.[1]) {
|
||
return match[1];
|
||
}
|
||
}
|
||
|
||
return "";
|
||
};
|
||
const collectStorageHints = (storage) => {
|
||
const hints = [];
|
||
if (!storage) {
|
||
return hints;
|
||
}
|
||
|
||
try {
|
||
for (let index = 0; index < storage.length && index < 80; index += 1) {
|
||
const key = storage.key(index);
|
||
if (!key) {
|
||
continue;
|
||
}
|
||
|
||
const value = storage.getItem(key) || "";
|
||
if (
|
||
/(uid|user|media|creator|account|id)/i.test(key)
|
||
|| /(id_str|user_id_str|media_id|mediaId|uid|user_id)/i.test(value)
|
||
) {
|
||
hints.push(key + "=" + value);
|
||
}
|
||
}
|
||
} catch {
|
||
return hints;
|
||
}
|
||
|
||
return hints;
|
||
};
|
||
const detectHeaderName = () => {
|
||
const ignoredPatterns = [
|
||
/^消息$/,
|
||
/^更多$/,
|
||
/^立即完善$/,
|
||
/^主页$/,
|
||
/^创作$/,
|
||
/^管理$/,
|
||
/^数据$/,
|
||
/^第\d+天$/,
|
||
/^在头条创作/,
|
||
/^扫码登录$/,
|
||
/^验证码登录$/,
|
||
/^密码登录$/,
|
||
/^手机号$/,
|
||
/^获取验证码$/,
|
||
/^登录$/,
|
||
];
|
||
|
||
const candidates = Array.from(document.querySelectorAll("body *"))
|
||
.map((element) => {
|
||
const rect = element.getBoundingClientRect();
|
||
const text = (element.textContent || "").trim().replace(/\s+/g, "");
|
||
return { text, rect };
|
||
})
|
||
.filter(({ text, rect }) =>
|
||
text
|
||
&& text.length >= 2
|
||
&& text.length <= 40
|
||
&& rect.top >= 0
|
||
&& rect.top <= 220
|
||
&& rect.left >= window.innerWidth * 0.55
|
||
&& rect.width <= 320
|
||
&& rect.height <= 120,
|
||
)
|
||
.map(({ text, rect }) => ({ text, rect }))
|
||
.sort((left, right) => {
|
||
if (right.rect.left !== left.rect.left) {
|
||
return right.rect.left - left.rect.left;
|
||
}
|
||
return left.rect.top - right.rect.top;
|
||
})
|
||
.map(({ text }) => text);
|
||
|
||
return candidates.find((text) => !ignoredPatterns.some((pattern) => pattern.test(text))) || "";
|
||
};
|
||
const detectHeaderAvatar = () => {
|
||
const images = Array.from(document.images)
|
||
.map((image) => {
|
||
const rect = image.getBoundingClientRect();
|
||
return {
|
||
src: image.currentSrc || image.src || "",
|
||
rect,
|
||
};
|
||
})
|
||
.filter(({ src, rect }) =>
|
||
src
|
||
&& rect.top >= 0
|
||
&& rect.top <= 220
|
||
&& rect.left >= window.innerWidth * 0.55
|
||
&& rect.width >= 24
|
||
&& rect.width <= 120
|
||
&& rect.height >= 24
|
||
&& rect.height <= 120,
|
||
)
|
||
.sort((left, right) => {
|
||
if (right.rect.left !== left.rect.left) {
|
||
return right.rect.left - left.rect.left;
|
||
}
|
||
return left.rect.top - right.rect.top;
|
||
});
|
||
|
||
return images[0]?.src || "";
|
||
};
|
||
const isLoggedInWorkbench = () => {
|
||
const text = document.body.innerText || "";
|
||
return (
|
||
location.hostname.includes("mp.toutiao.com")
|
||
&& !/\/auth\/page\/login/.test(location.pathname)
|
||
&& (
|
||
/在头条创作的第\s*\d+\s*天/.test(text)
|
||
|| (text.includes("主页") && text.includes("创作") && text.includes("管理"))
|
||
)
|
||
);
|
||
};
|
||
|
||
const roots = [
|
||
window,
|
||
window.__INITIAL_STATE__,
|
||
window.__INITIAL_DATA__,
|
||
window.__REDUX_STATE__,
|
||
window.__NEXT_DATA__,
|
||
window.__STORE__ && typeof window.__STORE__.getState === "function"
|
||
? window.__STORE__.getState()
|
||
: null,
|
||
];
|
||
const queue = [];
|
||
const seen = new WeakSet();
|
||
const enqueue = (value) => {
|
||
if (!value || typeof value !== "object" || seen.has(value)) {
|
||
return;
|
||
}
|
||
seen.add(value);
|
||
queue.push(value);
|
||
};
|
||
|
||
roots.forEach(enqueue);
|
||
|
||
let account = null;
|
||
while (queue.length && !account) {
|
||
const current = queue.shift();
|
||
account = pickAccount(current);
|
||
if (account) {
|
||
break;
|
||
}
|
||
|
||
const children = Array.isArray(current) ? current : Object.values(current);
|
||
for (const child of children.slice(0, 120)) {
|
||
enqueue(child);
|
||
}
|
||
}
|
||
|
||
if (!account) {
|
||
const html = document.documentElement.outerHTML;
|
||
const patternGroups = [
|
||
{
|
||
uid: /"id_str":"([^"]+)"/,
|
||
name: /"screen_name":"([^"]+)"/,
|
||
avatar: /"https_avatar_url":"([^"]+)"/,
|
||
},
|
||
{
|
||
uid: /"user_id_str":"([^"]+)"/,
|
||
name: /"name":"([^"]+)"/,
|
||
avatar: /"avatar_url":"([^"]+)"/,
|
||
},
|
||
{
|
||
uid: /"media_id":"?([^",}]+)"?/,
|
||
name: /"media_name":"([^"]+)"/,
|
||
avatar: /"avatar_url":"([^"]+)"/,
|
||
},
|
||
];
|
||
|
||
for (const group of patternGroups) {
|
||
const uid = html.match(group.uid)?.[1] ?? "";
|
||
const name = html.match(group.name)?.[1] ?? "";
|
||
if (uid && name) {
|
||
account = {
|
||
platformUid: uid,
|
||
displayName: name,
|
||
avatarUrl: html.match(group.avatar)?.[1] ?? null,
|
||
};
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const isLoginPage = /\/auth\/page\/login/.test(location.pathname);
|
||
|
||
if (!account && !isLoginPage) {
|
||
const displayName = detectHeaderName();
|
||
if (displayName) {
|
||
const storageHints = [
|
||
...collectStorageHints(window.localStorage),
|
||
...collectStorageHints(window.sessionStorage),
|
||
document.cookie,
|
||
document.documentElement.outerHTML,
|
||
];
|
||
const fallbackUid = storageHints.map(extractUidFromText).find(Boolean) || ("toutiao-name:" + displayName);
|
||
account = {
|
||
platformUid: fallbackUid,
|
||
displayName,
|
||
avatarUrl: detectHeaderAvatar() || null,
|
||
};
|
||
}
|
||
}
|
||
|
||
if (!account && isLoggedInWorkbench()) {
|
||
account = {
|
||
platformUid: "",
|
||
displayName: detectHeaderName() || "头条号账号",
|
||
avatarUrl: detectHeaderAvatar() || null,
|
||
};
|
||
}
|
||
|
||
return JSON.stringify(account);
|
||
} catch {
|
||
return JSON.stringify(null);
|
||
}
|
||
})()`, true).catch(() => null);
|
||
|
||
if (typeof serialized !== "string" || !serialized) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(serialized) as Partial<DetectedAccount> | null;
|
||
if (!parsed?.displayName) {
|
||
return null;
|
||
}
|
||
return {
|
||
platformUid: stringId(parsed.platformUid),
|
||
displayName: parsed.displayName,
|
||
avatarUrl: normalizeRemoteUrl(parsed.avatarUrl),
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function deriveToutiaoFallbackUid(session: Session, displayName: string): Promise<string> {
|
||
const cookieCandidates = [
|
||
["mp.toutiao.com", "uid_tt"],
|
||
["mp.toutiao.com", "uid_tt_ss"],
|
||
["mp.toutiao.com", "sessionid"],
|
||
["mp.toutiao.com", "sessionid_ss"],
|
||
["mp.toutiao.com", "ttwid"],
|
||
["toutiao.com", "uid_tt"],
|
||
["toutiao.com", "uid_tt_ss"],
|
||
["toutiao.com", "ttwid"],
|
||
] as const;
|
||
|
||
for (const [domain, name] of cookieCandidates) {
|
||
const value = await sessionCookieValue(session, domain, name).catch(() => "");
|
||
const normalized = normalizeText(value);
|
||
if (normalized) {
|
||
return boundedIdentity(`toutiao-cookie-${name}`, normalized);
|
||
}
|
||
}
|
||
|
||
const cookieHeader = await sessionCookieHeader(session, "mp.toutiao.com").catch(() => "");
|
||
if (cookieHeader.trim()) {
|
||
return boundedIdentity("toutiao-cookie-hash", cookieHeader);
|
||
}
|
||
|
||
return boundedIdentity("toutiao-name", displayName);
|
||
}
|
||
|
||
function sanitizeDetectedAccount(account: DetectedAccount): DetectedAccount {
|
||
const displayName = normalizeText(account.displayName) ?? "未命名账号";
|
||
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platformUid);
|
||
const platformUid =
|
||
normalizedPlatformUid
|
||
|| normalizeAccountPlatformUid(boundedIdentity("platform", displayName))
|
||
|| displayName;
|
||
|
||
return {
|
||
platformUid,
|
||
displayName,
|
||
avatarUrl: normalizeRemoteUrl(account.avatarUrl),
|
||
};
|
||
}
|
||
|
||
async function readGenericAIPageState(
|
||
webContents: WebContents | null | undefined,
|
||
): Promise<GenericAIPageState | null> {
|
||
if (!webContents || webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const state = await webContents.executeJavaScript(
|
||
`(() => {
|
||
try {
|
||
const normalize = (value) => {
|
||
if (typeof value !== "string") return null;
|
||
const trimmed = value.trim().replace(/\\s+/g, " ");
|
||
return trimmed || null;
|
||
};
|
||
const isVisible = (node) => {
|
||
if (!(node instanceof Element)) {
|
||
return false;
|
||
}
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||
return false;
|
||
}
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
};
|
||
const bodyText = normalize(document.body?.innerText || "");
|
||
const stopWords = /^(登录|注册|设置|帮助|历史|聊天|新建|菜单|账户|账号|个人中心|联网搜索|深度思考|工具|更多|安装电脑版|下载电脑版|DeepSeek|元宝|混元|豆包|Kimi|Qwen|通义千问)$/i;
|
||
const loginPattern = /(登录|注册|立即登录|扫码登录|微信登录|QQ登录|手机号登录|请先登录)/i;
|
||
const loggedOutPattern = /(未登录|扫码登录|快捷登录|使用其他头像、昵称或账号|登录后继续|请先登录|立即登录|微信快捷登录)/i;
|
||
const challengePattern = /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|手机验证码|扫码验证|verify|verification|captcha|security check)/i;
|
||
const credentialKeyPattern = /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i;
|
||
const isInsideAuthSurface = (node) => {
|
||
if (!(node instanceof Element)) {
|
||
return false;
|
||
}
|
||
let current = node;
|
||
for (let depth = 0; current && depth < 8; depth += 1) {
|
||
const descriptor = [
|
||
current.getAttribute('role') || '',
|
||
current.getAttribute('aria-modal') || '',
|
||
current.getAttribute('data-testid') || '',
|
||
current.id || '',
|
||
current.className || '',
|
||
].join(' ');
|
||
if (/(dialog|modal|popup|popover|sheet|drawer|login|signin|auth|scan|qrcode|qrlogin)/i.test(descriptor)) {
|
||
return true;
|
||
}
|
||
|
||
const text = normalize(current.textContent || '');
|
||
if (text && text.length <= 120 && loggedOutPattern.test(text)) {
|
||
return true;
|
||
}
|
||
|
||
current = current.parentElement;
|
||
}
|
||
return false;
|
||
};
|
||
const pickGreetingName = () => {
|
||
if (!bodyText) {
|
||
return null;
|
||
}
|
||
const patterns = [
|
||
/(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+([^\\n,,]{1,20})/i,
|
||
/(?:^|\\n)\\s*欢迎回来[,,\\s]*([^\\n]{1,20})/i,
|
||
];
|
||
for (const pattern of patterns) {
|
||
const matched = bodyText.match(pattern);
|
||
const candidate = normalize(matched?.[1] || "");
|
||
if (candidate && !stopWords.test(candidate)) {
|
||
return candidate;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const pickDisplayName = () => {
|
||
const selectors = [
|
||
'[data-testid*="user"]',
|
||
'[data-testid*="profile"]',
|
||
'[class*="user-name"]',
|
||
'[class*="username"]',
|
||
'[class*="nickname"]',
|
||
'[class*="profile"] [class*="name"]',
|
||
'[class*="avatar"] + *',
|
||
'[class*="account"] [class*="name"]',
|
||
'aside [class*="name"]',
|
||
'nav [class*="name"]',
|
||
];
|
||
for (const selector of selectors) {
|
||
const nodes = document.querySelectorAll(selector);
|
||
for (const node of nodes) {
|
||
if (!isVisible(node)) continue;
|
||
if (isInsideAuthSurface(node)) continue;
|
||
const text = normalize(node.textContent || "");
|
||
if (!text) continue;
|
||
if (text.length < 2 || text.length > 48) continue;
|
||
if (stopWords.test(text)) continue;
|
||
return text;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const pickAvatar = () => {
|
||
const selectors = [
|
||
'[class*="avatar"] img',
|
||
'[data-testid*="avatar"] img',
|
||
'[class*="profile"] img',
|
||
'[class*="account"] img',
|
||
'img[alt*="头像"]',
|
||
];
|
||
for (const selector of selectors) {
|
||
const node = document.querySelector(selector);
|
||
if (!node || !isVisible(node)) continue;
|
||
if (isInsideAuthSurface(node)) continue;
|
||
const src = normalize(node && 'src' in node ? node.src : null);
|
||
if (src && /(logo|icon|favicon)/i.test(src)) continue;
|
||
if (src) return src;
|
||
}
|
||
return null;
|
||
};
|
||
const collectLoginSignals = () => {
|
||
const matches = [];
|
||
if (bodyText && loginPattern.test(bodyText)) {
|
||
matches.push('body-text');
|
||
}
|
||
const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]'];
|
||
for (const selector of selectors) {
|
||
const nodes = document.querySelectorAll(selector);
|
||
for (const node of nodes) {
|
||
if (!isVisible(node)) continue;
|
||
const text = normalize(node.textContent || "");
|
||
if (!text || !loginPattern.test(text)) continue;
|
||
matches.push(text);
|
||
if (matches.length >= 4) {
|
||
return matches;
|
||
}
|
||
}
|
||
}
|
||
return matches;
|
||
};
|
||
const collectLoggedOutSignals = () => {
|
||
const matches = [];
|
||
if (bodyText && loggedOutPattern.test(bodyText)) {
|
||
matches.push('body-text');
|
||
}
|
||
const selectors = ['button', 'a', '[role="button"]', '[aria-haspopup="dialog"]', '[role="dialog"]'];
|
||
for (const selector of selectors) {
|
||
const nodes = document.querySelectorAll(selector);
|
||
for (const node of nodes) {
|
||
if (!isVisible(node)) continue;
|
||
const text = normalize(node.textContent || "");
|
||
if (!text || !loggedOutPattern.test(text)) continue;
|
||
matches.push(text);
|
||
if (matches.length >= 4) {
|
||
return matches;
|
||
}
|
||
}
|
||
}
|
||
return matches;
|
||
};
|
||
const collectChallengeSignals = () => {
|
||
const matches = [];
|
||
if (bodyText && challengePattern.test(bodyText)) {
|
||
matches.push('body-text');
|
||
}
|
||
const selectors = ['button', 'a', '[role="button"]', '[role="dialog"]', 'input', 'label'];
|
||
for (const selector of selectors) {
|
||
const nodes = document.querySelectorAll(selector);
|
||
for (const node of nodes) {
|
||
if (!isVisible(node)) continue;
|
||
const text = normalize(node.textContent || node.getAttribute?.('aria-label') || "");
|
||
if (!text || !challengePattern.test(text)) continue;
|
||
matches.push(text);
|
||
if (matches.length >= 4) {
|
||
return matches;
|
||
}
|
||
}
|
||
}
|
||
return matches;
|
||
};
|
||
const collectCredentialStorage = (storage, prefix) => {
|
||
const items = [];
|
||
try {
|
||
const limit = Math.min(storage.length, 40);
|
||
for (let index = 0; index < limit; index += 1) {
|
||
const key = storage.key(index);
|
||
if (!key || !credentialKeyPattern.test(key)) {
|
||
continue;
|
||
}
|
||
const value = storage.getItem(key);
|
||
const text = String(value || '').slice(0, 240).trim();
|
||
if (!text) {
|
||
continue;
|
||
}
|
||
items.push(prefix + ':' + key + '=' + text);
|
||
}
|
||
} catch {
|
||
return items;
|
||
}
|
||
return items;
|
||
};
|
||
const collectStorage = (storage, prefix) => {
|
||
const items = [];
|
||
try {
|
||
const limit = Math.min(storage.length, 40);
|
||
for (let index = 0; index < limit; index += 1) {
|
||
const key = storage.key(index);
|
||
if (!key || !/(user|uid|token|session|auth|account|profile|login|member|nick|name)/i.test(key)) {
|
||
continue;
|
||
}
|
||
const value = storage.getItem(key);
|
||
items.push(prefix + ':' + key + '=' + String(value || '').slice(0, 160));
|
||
}
|
||
} catch {
|
||
return items;
|
||
}
|
||
return items;
|
||
};
|
||
const displayName = pickGreetingName() || pickDisplayName();
|
||
const avatarUrl = pickAvatar();
|
||
const strongSignals = [];
|
||
if (displayName) {
|
||
strongSignals.push('displayName');
|
||
}
|
||
if (avatarUrl) {
|
||
strongSignals.push('avatar');
|
||
}
|
||
if (bodyText && /(?:^|\\n)\\s*(?:hi|hello|你好|嗨)[,,\\s]+/i.test(bodyText)) {
|
||
strongSignals.push('greeting');
|
||
}
|
||
const loginSignals = collectLoginSignals();
|
||
const loggedOutSignals = collectLoggedOutSignals();
|
||
const challengeSignals = collectChallengeSignals();
|
||
const credentialFingerprintParts = [
|
||
...collectCredentialStorage(window.localStorage, 'local'),
|
||
...collectCredentialStorage(window.sessionStorage, 'session'),
|
||
];
|
||
const fingerprintParts = [
|
||
document.cookie || '',
|
||
window.location.origin || '',
|
||
document.title || '',
|
||
...collectStorage(window.localStorage, 'local'),
|
||
...collectStorage(window.sessionStorage, 'session'),
|
||
].filter(Boolean);
|
||
return {
|
||
displayName,
|
||
avatarUrl,
|
||
fingerprint: fingerprintParts.length ? fingerprintParts.join('||') : null,
|
||
credentialFingerprint: credentialFingerprintParts.length
|
||
? credentialFingerprintParts.join('||')
|
||
: null,
|
||
currentPath: normalize(window.location.pathname || ''),
|
||
strongAuthSignalCount: strongSignals.length,
|
||
loginSignalCount: loginSignals.length,
|
||
loggedOutSignalCount: loggedOutSignals.length,
|
||
challengeSignalCount: challengeSignals.length,
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();`,
|
||
true,
|
||
).catch(() => null);
|
||
|
||
if (!state || typeof state !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const typed = state as Partial<GenericAIPageState>;
|
||
return {
|
||
displayName: normalizeText(typed.displayName),
|
||
avatarUrl: normalizeRemoteUrl(typed.avatarUrl),
|
||
fingerprint: normalizeText(typed.fingerprint),
|
||
credentialFingerprint: normalizeText(typed.credentialFingerprint),
|
||
currentPath: normalizeText(typed.currentPath),
|
||
strongAuthSignalCount:
|
||
typeof typed.strongAuthSignalCount === "number" && Number.isFinite(typed.strongAuthSignalCount)
|
||
? typed.strongAuthSignalCount
|
||
: 0,
|
||
loginSignalCount:
|
||
typeof typed.loginSignalCount === "number" && Number.isFinite(typed.loginSignalCount)
|
||
? typed.loginSignalCount
|
||
: 0,
|
||
loggedOutSignalCount:
|
||
typeof typed.loggedOutSignalCount === "number" && Number.isFinite(typed.loggedOutSignalCount)
|
||
? typed.loggedOutSignalCount
|
||
: 0,
|
||
challengeSignalCount:
|
||
typeof typed.challengeSignalCount === "number" && Number.isFinite(typed.challengeSignalCount)
|
||
? typed.challengeSignalCount
|
||
: 0,
|
||
};
|
||
}
|
||
|
||
function isAIPlatformBinding(platformId: string): boolean {
|
||
return aiPlatformCatalog.some((item) => item.id === platformId);
|
||
}
|
||
|
||
function normalizeURLPath(pathname: string): string {
|
||
const normalized = pathname.replace(/\/+$/, "");
|
||
return normalized === "/" ? "" : normalized;
|
||
}
|
||
|
||
function safeParseURL(input: string): URL | null {
|
||
try {
|
||
return new URL(input);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function isLikelyGenericAICredentialName(name: string): boolean {
|
||
const normalized = name.trim().toLowerCase();
|
||
if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) {
|
||
return false;
|
||
}
|
||
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name);
|
||
}
|
||
|
||
function isLikelyGenericAICredentialValue(value: string | null | undefined): boolean {
|
||
const normalized = normalizeText(value);
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
|
||
if (/^(true|false|null|undefined|0|1)$/i.test(normalized)) {
|
||
return false;
|
||
}
|
||
|
||
return normalized.length >= 8;
|
||
}
|
||
|
||
async function buildGenericAISessionFingerprint(
|
||
platformId: string,
|
||
session: Session,
|
||
pageState?: GenericAIPageState | null,
|
||
): Promise<string | null> {
|
||
const parts: string[] = [];
|
||
|
||
if (pageState?.credentialFingerprint) {
|
||
parts.push(pageState.credentialFingerprint);
|
||
}
|
||
|
||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
|
||
if (platformMeta) {
|
||
const urls = new Set([platformMeta.loginUrl, platformMeta.consoleUrl]);
|
||
for (const url of urls) {
|
||
try {
|
||
const cookies = await session.cookies.get({ url });
|
||
const authCookies = cookies.filter((cookie) => {
|
||
return isLikelyGenericAICredentialName(cookie.name)
|
||
&& isLikelyGenericAICredentialValue(cookie.value);
|
||
});
|
||
if (authCookies.length > 0) {
|
||
const serialized = authCookies
|
||
.map((cookie) => `${cookie.domain}|${cookie.name}=${cookie.value}`)
|
||
.sort()
|
||
.join(";");
|
||
parts.push(`${url}:${serialized}`);
|
||
}
|
||
} catch {
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (parts.length === 0) {
|
||
return buildGenericAIPageFingerprintFallback(platformId, pageState);
|
||
}
|
||
|
||
return boundedIdentity(`${platformId}-session`, parts.join("||"));
|
||
}
|
||
|
||
async function detectGenericAIPlatform(
|
||
platformId: string,
|
||
label: string,
|
||
context: DetectContext,
|
||
): Promise<DetectedAccount | null> {
|
||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const currentURL = context.webContents && !context.webContents.isDestroyed()
|
||
? context.webContents.getURL()
|
||
: "";
|
||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
|
||
if (!platformMeta) {
|
||
return null;
|
||
}
|
||
|
||
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
|
||
return null;
|
||
}
|
||
|
||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
|
||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
if (!genericAIPageLooksAuthenticated(currentURL, pageState)) {
|
||
return null;
|
||
}
|
||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
|
||
if (!fingerprint) {
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid: fingerprint,
|
||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||
avatarUrl: pageState?.avatarUrl ?? null,
|
||
});
|
||
}
|
||
|
||
async function detectQwenFromSession(
|
||
session: Session,
|
||
hints: {
|
||
displayName?: string | null;
|
||
avatarUrl?: string | null;
|
||
} = {},
|
||
): Promise<DetectedAccount | null> {
|
||
const cookies = await session.cookies.get({ url: "https://www.qianwen.com/" }).catch(() => []);
|
||
if (!cookies.length) {
|
||
return null;
|
||
}
|
||
|
||
const cookieMap = new Map<string, string>();
|
||
for (const cookie of cookies) {
|
||
if (!cookieMap.has(cookie.name)) {
|
||
cookieMap.set(cookie.name, cookie.value);
|
||
}
|
||
}
|
||
|
||
const ticketHash = normalizeText(cookieMap.get("tongyi_sso_ticket_hash"));
|
||
const ticket = normalizeText(cookieMap.get("tongyi_sso_ticket"));
|
||
const platformUid = ticketHash ?? ticket;
|
||
if (!platformUid) {
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid,
|
||
displayName: hints.displayName ?? "通义千问账号",
|
||
avatarUrl: hints.avatarUrl ?? null,
|
||
});
|
||
}
|
||
|
||
async function detectKimiFromSession(
|
||
session: Session,
|
||
hints: {
|
||
displayName?: string | null;
|
||
avatarUrl?: string | null;
|
||
} = {},
|
||
): Promise<DetectedAccount | null> {
|
||
const cookies = await session.cookies.get({ url: "https://www.kimi.com/" }).catch(() => []);
|
||
if (!cookies.length) {
|
||
return null;
|
||
}
|
||
|
||
const authCookie = cookies.find((cookie) =>
|
||
cookie.name === "kimi-auth" && isLikelyGenericAICredentialValue(cookie.value),
|
||
);
|
||
if (!authCookie) {
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid: boundedIdentity("kimi-auth", authCookie.value),
|
||
displayName: hints.displayName ?? "Kimi 会话",
|
||
avatarUrl: hints.avatarUrl ?? null,
|
||
});
|
||
}
|
||
|
||
async function detectQwenPlatform(
|
||
platformId: string,
|
||
label: string,
|
||
context: DetectContext,
|
||
): Promise<DetectedAccount | null> {
|
||
const genericDetected = await detectGenericAIPlatform(platformId, label, context);
|
||
if (genericDetected) {
|
||
return genericDetected;
|
||
}
|
||
|
||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const currentURL = context.webContents.getURL();
|
||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
|
||
if (!platformMeta) {
|
||
return null;
|
||
}
|
||
|
||
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
|
||
return null;
|
||
}
|
||
|
||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
|
||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
|
||
const fromSession = await detectQwenFromSession(context.session, {
|
||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||
avatarUrl: pageState?.avatarUrl ?? null,
|
||
});
|
||
if (fromSession) {
|
||
return fromSession;
|
||
}
|
||
|
||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
|
||
if (!fingerprint) {
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid: fingerprint,
|
||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||
avatarUrl: pageState?.avatarUrl ?? null,
|
||
});
|
||
}
|
||
|
||
async function detectKimiPlatform(
|
||
platformId: string,
|
||
label: string,
|
||
context: DetectContext,
|
||
): Promise<DetectedAccount | null> {
|
||
const genericDetected = await detectGenericAIPlatform(platformId, label, context);
|
||
if (genericDetected) {
|
||
return genericDetected;
|
||
}
|
||
|
||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const currentURL = context.webContents.getURL();
|
||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
|
||
if (!platformMeta) {
|
||
return null;
|
||
}
|
||
|
||
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
|
||
return null;
|
||
}
|
||
|
||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
|
||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
|
||
return null;
|
||
}
|
||
|
||
const fromSession = await detectKimiFromSession(context.session, {
|
||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||
avatarUrl: pageState?.avatarUrl ?? null,
|
||
});
|
||
if (fromSession) {
|
||
return fromSession;
|
||
}
|
||
|
||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
|
||
if (!fingerprint) {
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid: fingerprint,
|
||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||
avatarUrl: pageState?.avatarUrl ?? null,
|
||
});
|
||
}
|
||
|
||
async function detectAIPlatformAccount(
|
||
platformId: string,
|
||
label: string,
|
||
context: DetectContext,
|
||
): Promise<DetectedAccount | null> {
|
||
if (platformId === "qwen") {
|
||
return await detectQwenPlatform(platformId, label, context);
|
||
}
|
||
|
||
if (platformId === "kimi") {
|
||
return await detectKimiPlatform(platformId, label, context);
|
||
}
|
||
|
||
return await detectGenericAIPlatform(platformId, label, context);
|
||
}
|
||
|
||
async function detectToutiao({ session, webContents }: DetectContext): Promise<DetectedAccount | null> {
|
||
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
|
||
webContents,
|
||
"https://mp.toutiao.com/mp/agw/media/get_media_info",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
);
|
||
const accountFromPage = extractToutiaoAccount(pageResponse);
|
||
if (accountFromPage) {
|
||
return accountFromPage;
|
||
}
|
||
|
||
const sessionResponse = await sessionCookieFetchJson<ToutiaoMediaInfoResponse>(
|
||
session,
|
||
"https://mp.toutiao.com/mp/agw/media/get_media_info",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
);
|
||
const accountFromSession = extractToutiaoAccount(sessionResponse);
|
||
if (accountFromSession) {
|
||
return accountFromSession;
|
||
}
|
||
|
||
const accountFromPageState = await detectToutiaoFromPageState(webContents);
|
||
if (!accountFromPageState?.displayName) {
|
||
const currentURL = webContents.isDestroyed() ? "" : webContents.getURL();
|
||
if (/^https:\/\/mp\.toutiao\.com\//i.test(currentURL) && !/\/auth\/page\/login/i.test(currentURL)) {
|
||
const fallbackDisplayName = normalizeText(webContents.getTitle()) || "头条号账号";
|
||
return sanitizeDetectedAccount({
|
||
platformUid: await deriveToutiaoFallbackUid(session, fallbackDisplayName),
|
||
displayName: fallbackDisplayName,
|
||
avatarUrl: null,
|
||
});
|
||
}
|
||
return null;
|
||
}
|
||
|
||
return sanitizeDetectedAccount({
|
||
platformUid:
|
||
accountFromPageState.platformUid
|
||
|| await deriveToutiaoFallbackUid(session, accountFromPageState.displayName),
|
||
displayName: accountFromPageState.displayName,
|
||
avatarUrl: accountFromPageState.avatarUrl,
|
||
});
|
||
}
|
||
|
||
async function detectToutiaoFromSessionOnly(session: Session): Promise<DetectedAccount | null> {
|
||
const response = await sessionCookieFetchJson<ToutiaoMediaInfoResponse>(
|
||
session,
|
||
"https://mp.toutiao.com/mp/agw/media/get_media_info",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
);
|
||
return extractToutiaoAccount(response);
|
||
}
|
||
|
||
async function detectToutiaoFromSession(session: Session): Promise<DetectedAccount | null> {
|
||
const detected = await detectToutiaoFromSessionOnly(session);
|
||
if (detected) {
|
||
return sanitizeDetectedAccount(detected);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function hasToutiaoAuthCookies(session: Session): Promise<boolean> {
|
||
const candidates = [
|
||
["toutiao.com", "sessionid"],
|
||
["toutiao.com", "sessionid_ss"],
|
||
["toutiao.com", "sid_tt"],
|
||
["toutiao.com", "sid_ucp_v1"],
|
||
["mp.toutiao.com", "sessionid"],
|
||
["mp.toutiao.com", "sessionid_ss"],
|
||
] as const;
|
||
|
||
for (const [domain, name] of candidates) {
|
||
const value = await sessionCookieValue(session, domain, name).catch(() => "");
|
||
if (normalizeText(value)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function extractWangyihaoAccount(response: WangyiInfoResponse | null | undefined): DetectedAccount | null {
|
||
const media = response?.data?.mediaInfo;
|
||
const uid = stringId(media?.userId) || stringId(media?.wemediaId);
|
||
const displayName = normalizeText(media?.tname);
|
||
if (!uid || !displayName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName,
|
||
avatarUrl: normalizeRemoteUrl(media?.icon),
|
||
};
|
||
}
|
||
|
||
async function fetchWangyihaoInfo(
|
||
session: Session,
|
||
options: WangyiInfoFetchOptions = {},
|
||
): Promise<WangyiInfoResponse | null> {
|
||
const headers = {
|
||
accept: "application/json, text/plain, */*",
|
||
referer: WANGYI_API_REFERER,
|
||
origin: WANGYI_API_ORIGIN,
|
||
};
|
||
|
||
if (options.allowCookieWrite) {
|
||
return await sessionFetchJson<WangyiInfoResponse>(
|
||
session,
|
||
WANGYI_INFO_URL,
|
||
{
|
||
credentials: "include",
|
||
headers,
|
||
},
|
||
).catch(() => null);
|
||
}
|
||
|
||
return await sessionCookieFetchJson<WangyiInfoResponse>(
|
||
session,
|
||
WANGYI_INFO_URL,
|
||
{ headers },
|
||
).catch(() => null);
|
||
}
|
||
|
||
async function detectWangyihaoFromSession(session: Session): Promise<DetectedAccount | null> {
|
||
return extractWangyihaoAccount(await fetchWangyihaoInfo(session));
|
||
}
|
||
|
||
function wangyihaoCookieVaultPath(): string {
|
||
return join(app.getPath("userData"), "desktop-wangyihao-cookies.json");
|
||
}
|
||
|
||
function readWangyihaoCookieVault(): WangyihaoCookieVault {
|
||
try {
|
||
return JSON.parse(readFileSync(wangyihaoCookieVaultPath(), "utf8")) as WangyihaoCookieVault;
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function writeWangyihaoCookieVault(vault: WangyihaoCookieVault): void {
|
||
const target = wangyihaoCookieVaultPath();
|
||
mkdirSync(dirname(target), { recursive: true });
|
||
writeFileSync(target, JSON.stringify(vault, null, 2), "utf8");
|
||
}
|
||
|
||
function isWangyihaoCookie(cookie: Cookie): boolean {
|
||
const domain = cookie.domain?.toLowerCase() ?? "";
|
||
return domain.endsWith("163.com") || domain.endsWith("126.net");
|
||
}
|
||
|
||
function dedupeWangyihaoCookies(cookies: Cookie[]): StoredWangyihaoCookie[] {
|
||
const seen = new Set<string>();
|
||
const result: StoredWangyihaoCookie[] = [];
|
||
for (const cookie of cookies) {
|
||
const domain = cookie.domain;
|
||
if (!domain || !isWangyihaoCookie(cookie) || !cookie.value) {
|
||
continue;
|
||
}
|
||
|
||
const key = [
|
||
domain,
|
||
cookie.path,
|
||
cookie.name,
|
||
cookie.secure ? "secure" : "plain",
|
||
].join("\n");
|
||
if (seen.has(key)) {
|
||
continue;
|
||
}
|
||
seen.add(key);
|
||
result.push({
|
||
domain,
|
||
expirationDate: cookie.expirationDate,
|
||
hostOnly: cookie.hostOnly,
|
||
httpOnly: cookie.httpOnly,
|
||
name: cookie.name,
|
||
path: cookie.path,
|
||
sameSite: cookie.sameSite,
|
||
secure: cookie.secure,
|
||
session: cookie.session,
|
||
value: cookie.value,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function urlForStoredWangyihaoCookie(cookie: StoredWangyihaoCookie): string {
|
||
const domain = cookie.domain.replace(/^\./, "");
|
||
const protocol = cookie.secure ? "https" : "http";
|
||
return `${protocol}://${domain}${cookie.path || "/"}`;
|
||
}
|
||
|
||
async function saveWangyihaoSessionCookies(accountId: string, session: Session): Promise<void> {
|
||
const collected: Cookie[] = [];
|
||
for (const url of WANGYI_COOKIE_URLS) {
|
||
collected.push(...await session.cookies.get({ url }).catch(() => []));
|
||
}
|
||
|
||
const cookies = dedupeWangyihaoCookies(collected);
|
||
if (!cookies.length) {
|
||
return;
|
||
}
|
||
|
||
const vault = readWangyihaoCookieVault();
|
||
vault[accountId] = {
|
||
updatedAt: Date.now(),
|
||
cookies,
|
||
};
|
||
writeWangyihaoCookieVault(vault);
|
||
}
|
||
|
||
async function restoreWangyihaoSessionCookies(accountId: string, session: Session): Promise<void> {
|
||
const snapshot = readWangyihaoCookieVault()[accountId];
|
||
if (!snapshot?.cookies?.length) {
|
||
return;
|
||
}
|
||
|
||
for (const cookie of snapshot.cookies) {
|
||
await session.cookies.set({
|
||
url: urlForStoredWangyihaoCookie(cookie),
|
||
name: cookie.name,
|
||
value: cookie.value,
|
||
domain: cookie.domain,
|
||
path: cookie.path || "/",
|
||
secure: cookie.secure,
|
||
httpOnly: cookie.httpOnly,
|
||
sameSite: cookie.sameSite,
|
||
...(!cookie.session && cookie.expirationDate ? { expirationDate: cookie.expirationDate } : {}),
|
||
}).catch((error) => {
|
||
console.warn("[desktop-session] restore wangyihao cookie failed", {
|
||
accountId,
|
||
domain: cookie.domain,
|
||
name: cookie.name,
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
async function probeWangyihaoSession(
|
||
account: PublishAccountIdentity,
|
||
session: Session,
|
||
options: WangyiInfoFetchOptions = {},
|
||
): Promise<AuthProbeResult> {
|
||
const response = await fetchWangyihaoInfo(session, options).catch(() => null);
|
||
const detected = extractWangyihaoAccount(response);
|
||
if (detected) {
|
||
if (!detectedAccountMatchesIdentity(detected, account)) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
evidence: {
|
||
detectedPlatformUid: detected.platformUid,
|
||
expectedPlatformUid: account.platformUid,
|
||
},
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
};
|
||
}
|
||
|
||
if (!response) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
code: response.code ?? null,
|
||
message: response.message ?? response.msg ?? null,
|
||
},
|
||
};
|
||
}
|
||
|
||
async function hasWangyihaoAuthCookies(session: Session): Promise<boolean> {
|
||
const authCookieNames = new Set([
|
||
"P_INFO",
|
||
"S_INFO",
|
||
"NTES_SESS",
|
||
"l_yd_sign",
|
||
]);
|
||
const isAuthCookie = (name: string) => authCookieNames.has(name) || name.startsWith("l_s_subscribe");
|
||
const urls = [
|
||
WANGYI_API_ORIGIN,
|
||
WANGYI_CONSOLE_ORIGIN,
|
||
"https://reg.163.com/",
|
||
"https://dl.reg.163.com/",
|
||
];
|
||
|
||
for (const url of urls) {
|
||
const cookies = await session.cookies.get({ url }).catch(() => []);
|
||
if (cookies.some((cookie) => isAuthCookie(cookie.name) && normalizeText(cookie.value))) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
async function recoverWangyihaoSessionHandle(account: PublishAccountIdentity): Promise<SessionHandle | null> {
|
||
const pendingPartitions = listPendingPartitions("wangyihao");
|
||
if (!pendingPartitions.length) {
|
||
return null;
|
||
}
|
||
|
||
const expectedUid = normalizeText(account.platformUid) ?? "";
|
||
const expectedName = normalizeComparableText(account.displayName);
|
||
for (const partition of pendingPartitions) {
|
||
const candidateSession = electronSession.fromPartition(partition);
|
||
if (!(await hasWangyihaoAuthCookies(candidateSession))) {
|
||
continue;
|
||
}
|
||
|
||
const detected = await detectWangyihaoFromSession(candidateSession).catch(() => null);
|
||
if (!detected) {
|
||
continue;
|
||
}
|
||
|
||
if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) {
|
||
console.info("[desktop-session] recovered wangyihao partition by uid", {
|
||
accountId: account.id,
|
||
partition,
|
||
platformUid: detected.platformUid,
|
||
});
|
||
return createSessionHandleForPartition(account.id, partition);
|
||
}
|
||
|
||
if (expectedName && normalizeComparableText(detected.displayName) === expectedName) {
|
||
console.info("[desktop-session] recovered wangyihao partition by name", {
|
||
accountId: account.id,
|
||
partition,
|
||
displayName: detected.displayName,
|
||
});
|
||
return createSessionHandleForPartition(account.id, partition);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Promise<SessionHandle | null> {
|
||
const pendingPartitions = listPendingPartitions("toutiaohao");
|
||
if (!pendingPartitions.length) {
|
||
return null;
|
||
}
|
||
|
||
const expectedUid = normalizeText(account.platformUid) ?? "";
|
||
const expectedName = normalizeComparableText(account.displayName);
|
||
let fallbackPartition: string | null = null;
|
||
|
||
for (const partition of pendingPartitions) {
|
||
const candidateSession = electronSession.fromPartition(partition);
|
||
if (!(await hasToutiaoAuthCookies(candidateSession))) {
|
||
continue;
|
||
}
|
||
|
||
fallbackPartition ??= partition;
|
||
|
||
const detected = await detectToutiaoFromSessionOnly(candidateSession).catch(() => null);
|
||
if (!detected) {
|
||
continue;
|
||
}
|
||
|
||
if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) {
|
||
console.info("[desktop-session] recovered toutiao partition by uid", {
|
||
accountId: account.id,
|
||
partition,
|
||
platformUid: detected.platformUid,
|
||
});
|
||
return createSessionHandleForPartition(account.id, partition);
|
||
}
|
||
|
||
if (expectedName && normalizeComparableText(detected.displayName) === expectedName) {
|
||
console.info("[desktop-session] recovered toutiao partition by name", {
|
||
accountId: account.id,
|
||
partition,
|
||
displayName: detected.displayName,
|
||
});
|
||
return createSessionHandleForPartition(account.id, partition);
|
||
}
|
||
}
|
||
|
||
if (!fallbackPartition) {
|
||
return null;
|
||
}
|
||
|
||
console.info("[desktop-session] recovered toutiao partition by auth cookie fallback", {
|
||
accountId: account.id,
|
||
partition: fallbackPartition,
|
||
});
|
||
return createSessionHandleForPartition(account.id, fallbackPartition);
|
||
}
|
||
|
||
export async function ensurePublishAccountSessionHandle(
|
||
account: PublishAccountIdentity,
|
||
): Promise<SessionHandle> {
|
||
const existing = await findLocalPublishAccountSessionHandle(account);
|
||
return existing ?? createSessionHandle(account.id);
|
||
}
|
||
|
||
async function findLocalPublishAccountSessionHandle(
|
||
account: PublishAccountIdentity,
|
||
): Promise<SessionHandle | null> {
|
||
const existing = getSessionHandle(account.id);
|
||
if (existing) {
|
||
if (account.platform === "wangyihao") {
|
||
await restoreWangyihaoSessionCookies(account.id, existing.session);
|
||
}
|
||
return existing;
|
||
}
|
||
|
||
const persisted = getPersistedPartition(account.id);
|
||
if (persisted) {
|
||
const handle = createSessionHandle(account.id);
|
||
if (account.platform === "wangyihao") {
|
||
await restoreWangyihaoSessionCookies(account.id, handle.session);
|
||
}
|
||
return handle;
|
||
}
|
||
|
||
if (account.platform === "toutiaohao") {
|
||
const recovered = await recoverToutiaoSessionHandle(account);
|
||
if (recovered) {
|
||
return recovered;
|
||
}
|
||
}
|
||
|
||
if (account.platform === "wangyihao") {
|
||
const recovered = await recoverWangyihaoSessionHandle(account);
|
||
if (recovered) {
|
||
return recovered;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function toPublishAccountProfile(detected: DetectedAccount | null): PublishAccountProfile | null {
|
||
if (!detected) {
|
||
return null;
|
||
}
|
||
|
||
const normalized = sanitizeDetectedAccount(detected);
|
||
return {
|
||
platformUid: normalized.platformUid,
|
||
displayName: normalized.displayName,
|
||
avatarUrl: normalized.avatarUrl,
|
||
};
|
||
}
|
||
|
||
function toAccountHealthProfile(profile: PublishAccountProfile | null): AccountHealthProfile | null {
|
||
if (!profile) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
subjectUid: profile.platformUid,
|
||
displayName: profile.displayName,
|
||
avatarUrl: profile.avatarUrl,
|
||
};
|
||
}
|
||
|
||
function fallbackAccountHealthProfile(account: PublishAccountIdentity, overrides: {
|
||
displayName?: string | null;
|
||
avatarUrl?: string | null;
|
||
} = {}): AccountHealthProfile {
|
||
return {
|
||
subjectUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
|
||
displayName: overrides.displayName ?? account.displayName,
|
||
avatarUrl: overrides.avatarUrl ?? null,
|
||
};
|
||
}
|
||
|
||
async function resolvePublishAccountProfileFromHandle(
|
||
account: PublishAccountIdentity,
|
||
handle: SessionHandle,
|
||
): Promise<PublishAccountProfile | null> {
|
||
if (account.platform === "toutiaohao") {
|
||
return await detectToutiaoFromSession(handle.session);
|
||
}
|
||
|
||
const definition = platformBindingDefinitions[account.platform];
|
||
if (!definition) {
|
||
return null;
|
||
}
|
||
|
||
if (isAIPlatformBinding(account.platform)) {
|
||
const window = createBoundWindow(
|
||
`${definition.label} 静默识别`,
|
||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||
handle.session,
|
||
{ show: false },
|
||
);
|
||
|
||
try {
|
||
await waitForWindowNavigationSettled(window);
|
||
if (window.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return toPublishAccountProfile(detected);
|
||
} finally {
|
||
if (!window.isDestroyed()) {
|
||
window.destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: undefined as unknown as WebContents,
|
||
}).catch(() => null);
|
||
|
||
return toPublishAccountProfile(detected);
|
||
}
|
||
|
||
export async function probePublishAccountSession(
|
||
account: PublishAccountIdentity,
|
||
partition?: string,
|
||
): Promise<AuthProbeResult> {
|
||
const handle = partition
|
||
? createSessionHandleForPartition(account.id, partition)
|
||
: await findLocalPublishAccountSessionHandle(account);
|
||
|
||
if (!handle) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "missing_partition",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
};
|
||
}
|
||
|
||
const definition = platformBindingDefinitions[account.platform];
|
||
if (!definition) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
platform: account.platform,
|
||
code: "desktop_platform_not_supported",
|
||
},
|
||
};
|
||
}
|
||
|
||
if (account.platform === "wangyihao") {
|
||
return await probeWangyihaoSession(account, handle.session);
|
||
}
|
||
|
||
const window = createBoundWindow(
|
||
`${definition.label} 静默探针`,
|
||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||
handle.session,
|
||
{ show: false },
|
||
);
|
||
|
||
let mainFrameLoadError:
|
||
| {
|
||
code: number;
|
||
description: string;
|
||
url: string;
|
||
}
|
||
| undefined;
|
||
window.webContents.on(
|
||
"did-fail-load",
|
||
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||
if (isMainFrame && errorCode !== -3) {
|
||
mainFrameLoadError = {
|
||
code: errorCode,
|
||
description: errorDescription,
|
||
url: validatedURL,
|
||
};
|
||
}
|
||
},
|
||
);
|
||
|
||
try {
|
||
await waitForWindowNavigationSettled(window);
|
||
if (window.isDestroyed()) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
};
|
||
}
|
||
|
||
const loadError = mainFrameLoadError;
|
||
if (loadError) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
errorCode: loadError.code,
|
||
errorDescription: loadError.description,
|
||
url: loadError.url,
|
||
},
|
||
};
|
||
}
|
||
|
||
const currentURL = window.webContents.getURL();
|
||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
},
|
||
};
|
||
}
|
||
|
||
const isLoginRedirect = definitionLooksLikeLoginRedirect(currentURL, definition);
|
||
|
||
if (isLoginRedirect) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
},
|
||
};
|
||
}
|
||
|
||
if (account.platform === "weixin_gzh") {
|
||
const pageState = await readWeixinGzhConsoleState(window.webContents);
|
||
if (pageState?.loggedOut) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
title: pageState.title,
|
||
text: pageState.textSnippet,
|
||
},
|
||
};
|
||
}
|
||
|
||
const detected = await detectWeixinGzh({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
if (!detected) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
title: pageState?.title,
|
||
text: pageState?.textSnippet,
|
||
},
|
||
};
|
||
}
|
||
|
||
if (!detectedAccountMatchesIdentity(detected, account)) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
evidence: {
|
||
url: currentURL,
|
||
detectedPlatformUid: detected.platformUid,
|
||
expectedPlatformUid: account.platformUid,
|
||
},
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
};
|
||
}
|
||
|
||
if (account.platform === "toutiaohao") {
|
||
const detected = await detectToutiaoFromSession(handle.session);
|
||
if (detected) {
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||
sessionFingerprint: detected.platformUid,
|
||
};
|
||
}
|
||
|
||
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
|
||
if (consoleReady) {
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: fallbackAccountHealthProfile(account),
|
||
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
};
|
||
}
|
||
|
||
if (isAIPlatformBinding(account.platform)) {
|
||
const pageState = await readGenericAIPageState(window.webContents);
|
||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||
return {
|
||
verdict: "challenge_required",
|
||
reason: "captcha_gate",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
challengeSignalCount: pageState?.challengeSignalCount ?? 0,
|
||
},
|
||
};
|
||
}
|
||
|
||
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
});
|
||
if (detected) {
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||
sessionFingerprint: detected.platformUid,
|
||
};
|
||
}
|
||
|
||
if (!pageState) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
},
|
||
};
|
||
}
|
||
|
||
if (pageState.loggedOutSignalCount > 0 || definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||
},
|
||
};
|
||
}
|
||
|
||
if ((pageState.loginSignalCount ?? 0) > 0 && (pageState.strongAuthSignalCount ?? 0) === 0) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
loginSignalCount: pageState.loginSignalCount,
|
||
},
|
||
};
|
||
}
|
||
|
||
const fingerprint = await buildGenericAISessionFingerprint(account.platform, handle.session, pageState);
|
||
if (genericAIPageLooksAuthenticated(currentURL, pageState)) {
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: fallbackAccountHealthProfile(account, {
|
||
displayName: pageState.displayName ?? account.displayName,
|
||
avatarUrl: pageState.avatarUrl ?? null,
|
||
}),
|
||
sessionFingerprint: fingerprint,
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: fingerprint,
|
||
evidence: {
|
||
url: currentURL,
|
||
strongAuthSignalCount: pageState.strongAuthSignalCount,
|
||
loginSignalCount: pageState.loginSignalCount,
|
||
loggedOutSignalCount: pageState.loggedOutSignalCount,
|
||
},
|
||
};
|
||
}
|
||
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
});
|
||
if (detected) {
|
||
if (
|
||
platformRequiresDetectedAccountIdentityMatch(account.platform)
|
||
&& !detectedAccountMatchesIdentity(detected, account)
|
||
) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
evidence: {
|
||
detectedPlatformUid: detected.platformUid,
|
||
expectedPlatformUid: account.platformUid,
|
||
},
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: toAccountHealthProfile(toPublishAccountProfile(detected)),
|
||
sessionFingerprint: sanitizeDetectedAccount(detected).platformUid,
|
||
};
|
||
}
|
||
|
||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
code: "account_api_empty",
|
||
},
|
||
};
|
||
}
|
||
|
||
const consoleReady = account.platform === "zol"
|
||
? await verifyZolConsoleAccess(window, handle.session, account)
|
||
: await verifyPublishAccountConsoleAccess(account, handle);
|
||
if (consoleReady) {
|
||
return {
|
||
verdict: "active",
|
||
reason: "probe_success",
|
||
profile: fallbackAccountHealthProfile(account),
|
||
sessionFingerprint: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
|
||
};
|
||
}
|
||
|
||
return {
|
||
verdict: "expired",
|
||
reason: "login_redirect",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
url: currentURL,
|
||
},
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
verdict: "network_error",
|
||
reason: "network_error",
|
||
profile: null,
|
||
sessionFingerprint: null,
|
||
evidence: {
|
||
message: error instanceof Error ? error.message : String(error),
|
||
},
|
||
};
|
||
} finally {
|
||
if (!window.isDestroyed()) {
|
||
window.destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
export async function silentRefreshPublishAccountSession(
|
||
account: PublishAccountIdentity,
|
||
partition?: string,
|
||
): Promise<boolean> {
|
||
const handle = partition
|
||
? createSessionHandleForPartition(account.id, partition)
|
||
: await findLocalPublishAccountSessionHandle(account);
|
||
|
||
if (!handle) {
|
||
return false;
|
||
}
|
||
|
||
const definition = platformBindingDefinitions[account.platform];
|
||
if (!definition) {
|
||
return false;
|
||
}
|
||
|
||
if (account.platform === "wangyihao") {
|
||
const result = await probeWangyihaoSession(account, handle.session, { allowCookieWrite: true })
|
||
.catch(() => null);
|
||
if (result?.verdict === "active") {
|
||
await saveWangyihaoSessionCookies(account.id, handle.session);
|
||
}
|
||
return result?.verdict === "active";
|
||
}
|
||
|
||
const window = createBoundWindow(
|
||
`${definition.label} 静默续期`,
|
||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||
handle.session,
|
||
{ show: false },
|
||
);
|
||
|
||
try {
|
||
await waitForWindowNavigationSettled(window, 10_000);
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
await sleep(1_200);
|
||
await flushSessionPersistence(handle.session);
|
||
if (account.platform === "weixin_gzh") {
|
||
const pageState = await readWeixinGzhConsoleState(window.webContents);
|
||
if (pageState?.loggedOut) {
|
||
return false;
|
||
}
|
||
|
||
const detected = await detectWeixinGzh({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||
}
|
||
|
||
if (account.platform === "zol") {
|
||
return await verifyZolConsoleAccess(window, handle.session, account);
|
||
}
|
||
|
||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||
}
|
||
|
||
return !definitionLooksLikeLoginRedirect(window.webContents.getURL(), definition);
|
||
} catch {
|
||
return false;
|
||
} finally {
|
||
if (!window.isDestroyed()) {
|
||
window.destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
export async function inspectPublishAccountLocalSession(
|
||
account: PublishAccountIdentity,
|
||
): Promise<PublishAccountLocalInspection> {
|
||
const handle = await findLocalPublishAccountSessionHandle(account);
|
||
if (!handle) {
|
||
return {
|
||
availability: "missing",
|
||
profile: null,
|
||
};
|
||
}
|
||
|
||
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
||
if (!profile) {
|
||
if (account.platform === "wangyihao") {
|
||
return {
|
||
availability: "expired",
|
||
profile: null,
|
||
};
|
||
}
|
||
|
||
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
||
if (consoleReady) {
|
||
return {
|
||
availability: "ready",
|
||
profile: {
|
||
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
|
||
displayName: account.displayName,
|
||
avatarUrl: null,
|
||
},
|
||
};
|
||
}
|
||
|
||
if (account.platform === "toutiaohao" && await hasToutiaoAuthCookies(handle.session)) {
|
||
return {
|
||
availability: "ready",
|
||
profile: {
|
||
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
|
||
displayName: account.displayName,
|
||
avatarUrl: null,
|
||
},
|
||
};
|
||
}
|
||
|
||
return {
|
||
availability: "expired",
|
||
profile: null,
|
||
};
|
||
}
|
||
|
||
return {
|
||
availability: "ready",
|
||
profile,
|
||
};
|
||
}
|
||
|
||
export async function resolvePublishAccountProfile(
|
||
account: PublishAccountIdentity,
|
||
): Promise<PublishAccountProfile | null> {
|
||
const handle = await ensurePublishAccountSessionHandle(account);
|
||
return await resolvePublishAccountProfileFromHandle(account, handle);
|
||
}
|
||
|
||
async function finalizeDetectedAccountForBind(
|
||
_definition: PublishPlatformBindingDefinition,
|
||
_context: DetectContext,
|
||
detected: DetectedAccount,
|
||
): Promise<DetectedAccount> {
|
||
return detected;
|
||
}
|
||
|
||
function isToutiaoLoginURL(url: string): boolean {
|
||
return url.startsWith(TOUTIAO_LOGIN_URL);
|
||
}
|
||
|
||
async function waitForWindowNavigationSettled(window: BrowserWindow, timeoutMs = 8_000): Promise<void> {
|
||
const startedAt = Date.now();
|
||
let lastURL = "";
|
||
let stableSince = Date.now();
|
||
|
||
while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) {
|
||
const currentURL = window.webContents.getURL();
|
||
if (currentURL !== lastURL) {
|
||
lastURL = currentURL;
|
||
stableSince = Date.now();
|
||
}
|
||
|
||
if (!window.webContents.isLoading() && Date.now() - stableSince >= 450) {
|
||
return;
|
||
}
|
||
|
||
await sleep(150);
|
||
}
|
||
}
|
||
|
||
async function flushSessionPersistence(target: Session): Promise<void> {
|
||
const maybeFlushStorageData = (target as Session & {
|
||
flushStorageData?: () => Promise<void>;
|
||
}).flushStorageData;
|
||
|
||
if (typeof maybeFlushStorageData === "function") {
|
||
try {
|
||
await maybeFlushStorageData.call(target);
|
||
} catch (error) {
|
||
console.warn("[desktop-session] flushStorageData failed", {
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
}
|
||
}
|
||
|
||
try {
|
||
await target.cookies.flushStore();
|
||
} catch (error) {
|
||
console.warn("[desktop-session] flushStore failed", {
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
}
|
||
}
|
||
|
||
async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Session): Promise<boolean> {
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
await loadWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, "头条号创作台校验");
|
||
await waitForWindowNavigationSettled(window);
|
||
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
const currentURL = window.webContents.getURL();
|
||
if (isToutiaoLoginURL(currentURL)) {
|
||
return false;
|
||
}
|
||
|
||
if (TOUTIAO_WORKBENCH_URL_PATTERN.test(currentURL)) {
|
||
return true;
|
||
}
|
||
|
||
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
|
||
window.webContents,
|
||
"https://mp.toutiao.com/mp/agw/media/get_media_info",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
);
|
||
if (extractToutiaoAccount(pageResponse)) {
|
||
return true;
|
||
}
|
||
|
||
const sessionResponse = await sessionCookieFetchJson<ToutiaoMediaInfoResponse>(
|
||
session,
|
||
"https://mp.toutiao.com/mp/agw/media/get_media_info",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
);
|
||
|
||
return extractToutiaoAccount(sessionResponse) !== null;
|
||
}
|
||
|
||
function isWangyihaoLoggedOutText(text: string): boolean {
|
||
return /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i
|
||
.test(text);
|
||
}
|
||
|
||
async function readWangyihaoConsoleState(
|
||
webContents: WebContents | null | undefined,
|
||
): Promise<{ loggedOut: boolean; title: string; textSnippet: string } | null> {
|
||
if (!webContents || webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const state = await webContents.executeJavaScript(
|
||
`(() => {
|
||
try {
|
||
const text = String(document.body?.innerText || "");
|
||
const title = String(document.title || "");
|
||
const source = title + "\\n" + text.slice(0, 1200);
|
||
return {
|
||
title,
|
||
textSnippet: text.slice(0, 500),
|
||
loggedOut: /(登录超时|登录态失效|登录状态失效|登录已过期|会话已过期|请重新登录|请先登录|未登录|网易邮箱账号登录|网易邮箱帐号登录|手机号登录|扫码登录)/i.test(source),
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();`,
|
||
true,
|
||
).catch(() => null);
|
||
|
||
if (!state || typeof state !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const typed = state as { loggedOut?: unknown; title?: unknown; textSnippet?: unknown };
|
||
const title = typeof typed.title === "string" ? typed.title : "";
|
||
const textSnippet = typeof typed.textSnippet === "string" ? typed.textSnippet : "";
|
||
return {
|
||
loggedOut: typed.loggedOut === true || isWangyihaoLoggedOutText(`${title}\n${textSnippet}`),
|
||
title,
|
||
textSnippet,
|
||
};
|
||
}
|
||
|
||
async function verifyWangyihaoConsoleAccess(
|
||
window: BrowserWindow,
|
||
session: Session,
|
||
account: PublishAccountIdentity,
|
||
): Promise<boolean> {
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
const currentURL = window.webContents.getURL();
|
||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||
return false;
|
||
}
|
||
|
||
const definition = platformBindingDefinitions.wangyihao;
|
||
if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||
return false;
|
||
}
|
||
|
||
const pageState = await readWangyihaoConsoleState(window.webContents);
|
||
if (pageState?.loggedOut) {
|
||
return false;
|
||
}
|
||
|
||
const detected = await detectWangyihaoFromSession(session).catch(() => null);
|
||
if (detected) {
|
||
return detectedAccountMatchesIdentity(detected, account);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function detectedAccountsLikelyMatch(left: DetectedAccount, right: DetectedAccount): boolean {
|
||
const leftUid = normalizeAccountPlatformUid(left.platformUid) || left.platformUid;
|
||
const rightUid = normalizeAccountPlatformUid(right.platformUid) || right.platformUid;
|
||
if (leftUid && rightUid) {
|
||
return sameAccountPlatformUid(leftUid, rightUid);
|
||
}
|
||
|
||
const leftName = normalizeComparableText(left.displayName);
|
||
const rightName = normalizeComparableText(right.displayName);
|
||
return Boolean(leftName && rightName && leftName === rightName);
|
||
}
|
||
|
||
async function verifyWangyihaoBindWindowReady(
|
||
window: BrowserWindow,
|
||
session: Session,
|
||
expected: DetectedAccount,
|
||
): Promise<boolean> {
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
const currentURL = window.webContents.getURL();
|
||
const definition = platformBindingDefinitions.wangyihao;
|
||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||
return false;
|
||
}
|
||
if (definition && definitionLooksLikeLoginRedirect(currentURL, definition)) {
|
||
return false;
|
||
}
|
||
|
||
const pageState = await readWangyihaoConsoleState(window.webContents);
|
||
if (pageState?.loggedOut) {
|
||
return false;
|
||
}
|
||
|
||
const detected = await detectWangyihaoFromSession(session).catch(() => null);
|
||
return detected ? detectedAccountsLikelyMatch(detected, expected) : false;
|
||
}
|
||
|
||
function detectedAccountMatchesIdentity(
|
||
detected: DetectedAccount,
|
||
account: PublishAccountIdentity,
|
||
): boolean {
|
||
const expected = normalizeAccountPlatformUid(account.platformUid) || account.platformUid;
|
||
if (!expected) {
|
||
return true;
|
||
}
|
||
return sameAccountPlatformUid(detected.platformUid, expected);
|
||
}
|
||
|
||
function platformRequiresDetectedAccountForConsoleAccess(platformId: string): boolean {
|
||
return platformId === "qiehao";
|
||
}
|
||
|
||
function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean {
|
||
return platformId === "qiehao" || platformId === "wangyihao" || platformId === "zol";
|
||
}
|
||
|
||
async function verifyZolConsoleAccess(
|
||
window: BrowserWindow,
|
||
session: Session,
|
||
account: PublishAccountIdentity,
|
||
): Promise<boolean> {
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
const detected = await detectZol({
|
||
session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||
}
|
||
|
||
const LOGIN_REDIRECT_TOKEN_RE =
|
||
/(^|[/?#=&._-])(login|signin|sign-in|sign_in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i;
|
||
const LOGIN_REDIRECT_HOST_RE = /(^|[.-])(login|passport|auth|sso|reg)(?=$|[.-])/i;
|
||
|
||
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
|
||
if (!currentURL || !loginURL) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const current = new URL(currentURL);
|
||
const login = new URL(loginURL);
|
||
if (current.origin !== login.origin) {
|
||
return false;
|
||
}
|
||
|
||
const currentPath = normalizeURLPath(current.pathname);
|
||
const loginPath = normalizeURLPath(login.pathname);
|
||
if (!loginPath) {
|
||
return LOGIN_REDIRECT_TOKEN_RE.test(`${current.pathname}${current.search}${current.hash}`)
|
||
|| LOGIN_REDIRECT_HOST_RE.test(current.hostname);
|
||
}
|
||
|
||
return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function definitionLooksLikeLoginRedirect(
|
||
currentURL: string,
|
||
definition: PublishPlatformBindingDefinition,
|
||
): boolean {
|
||
const loginRedirectUrls = definition.loginRedirectUrls ?? [definition.loginUrl];
|
||
return loginRedirectUrls.some((loginURL) => looksLikeLoginRedirect(currentURL, loginURL));
|
||
}
|
||
|
||
async function verifyPublishAccountConsoleAccess(
|
||
account: PublishAccountIdentity,
|
||
handle: SessionHandle,
|
||
): Promise<boolean> {
|
||
const definition = platformBindingDefinitions[account.platform];
|
||
if (!definition) {
|
||
return false;
|
||
}
|
||
|
||
const window = createBoundWindow(
|
||
`${definition.label} 静默校验`,
|
||
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||
handle.session,
|
||
{ show: false },
|
||
);
|
||
|
||
try {
|
||
if (account.platform === "toutiaohao") {
|
||
return await verifyToutiaoConsoleAccess(window, handle.session);
|
||
}
|
||
|
||
await waitForWindowNavigationSettled(window);
|
||
if (window.isDestroyed()) {
|
||
return false;
|
||
}
|
||
|
||
const currentURL = window.webContents.getURL();
|
||
if (!currentURL || currentURL.startsWith("data:text/html")) {
|
||
return false;
|
||
}
|
||
|
||
if (isAIPlatformBinding(account.platform)) {
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected !== null;
|
||
}
|
||
|
||
if (account.platform === "weixin_gzh") {
|
||
const pageState = await readWeixinGzhConsoleState(window.webContents);
|
||
if (pageState?.loggedOut) {
|
||
return false;
|
||
}
|
||
|
||
const detected = await detectWeixinGzh({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||
}
|
||
|
||
if (account.platform === "wangyihao") {
|
||
return await verifyWangyihaoConsoleAccess(window, handle.session, account);
|
||
}
|
||
|
||
if (account.platform === "zol") {
|
||
return await verifyZolConsoleAccess(window, handle.session, account);
|
||
}
|
||
|
||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: window.webContents,
|
||
}).catch(() => null);
|
||
return detected ? detectedAccountMatchesIdentity(detected, account) : false;
|
||
}
|
||
|
||
return !definitionLooksLikeLoginRedirect(currentURL, definition);
|
||
} finally {
|
||
if (!window.isDestroyed()) {
|
||
window.destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function detectBaijiahao({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<BaijiahaoAppInfoResponse>(
|
||
session,
|
||
"https://baijiahao.baidu.com/builder/app/appinfo",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
const user = response?.data?.user;
|
||
const uid = stringId(user?.userid);
|
||
const nickname = user?.name || user?.uname || user?.username || "";
|
||
if (!uid || !nickname) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: nickname,
|
||
avatarUrl: normalizeRemoteUrl(user?.avatar),
|
||
};
|
||
}
|
||
|
||
async function detectSohu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<SohuRegisterInfoResponse>(
|
||
session,
|
||
"https://mp.sohu.com/mpbp/bp/account/register-info",
|
||
).catch(() => null);
|
||
|
||
const account = response?.data?.account;
|
||
const uid = stringId(account?.id);
|
||
if (!uid || !account?.nickName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: account.nickName,
|
||
avatarUrl: normalizeRemoteUrl(account.avatar),
|
||
};
|
||
}
|
||
|
||
async function detectQiehao({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const cookie = await sessionCookieHeader(session, "om.qq.com");
|
||
const response = await sessionFetchJson<QiehaoBasicInfoResponse>(
|
||
session,
|
||
"https://om.qq.com/maccountsetting/basicinfo?relogin=1",
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
...(cookie ? { cookie } : {}),
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
const cpInfo = response?.data?.cpInfo;
|
||
const uid = stringId(cpInfo?.mediaId);
|
||
if (!uid || !cpInfo?.mediaName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: cpInfo.mediaName,
|
||
avatarUrl: normalizeRemoteUrl(cpInfo.header),
|
||
};
|
||
}
|
||
|
||
async function detectZhihu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<ZhihuMeResponse>(
|
||
session,
|
||
"https://www.zhihu.com/api/v4/me",
|
||
{
|
||
headers: {
|
||
"x-requested-with": "fetch",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
const uid = stringId(response?.uid || response?.id);
|
||
if (!uid || !response?.name) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: response.name,
|
||
avatarUrl: normalizeRemoteUrl(response.avatar_url),
|
||
};
|
||
}
|
||
|
||
async function detectWangyihao({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
return await detectWangyihaoFromSession(session);
|
||
}
|
||
|
||
async function detectJianshu({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<JianshuUser>(
|
||
session,
|
||
"https://www.jianshu.com/author/current_user",
|
||
{
|
||
headers: {
|
||
accept: "application/json",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
const uid = stringId(response?.id);
|
||
if (!uid || !response?.nickname) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: response.nickname,
|
||
avatarUrl: normalizeRemoteUrl(response.avatar),
|
||
};
|
||
}
|
||
|
||
async function fetchBilibiliSpaceAvatar(
|
||
session: Session,
|
||
uid: string,
|
||
nav: BilibiliNavResponse | null,
|
||
cookie: string,
|
||
): Promise<string | null> {
|
||
const query = buildBilibiliWbiQuery(nav, {
|
||
mid: uid,
|
||
platform: "web",
|
||
token: "",
|
||
web_location: 1550101,
|
||
});
|
||
if (!query) {
|
||
return null;
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
const timeout = setTimeout(() => {
|
||
controller.abort();
|
||
}, 1800);
|
||
|
||
try {
|
||
const response = await sessionFetchJson<BilibiliSpaceInfoResponse>(
|
||
session,
|
||
`${BILIBILI_API_ORIGIN}/x/space/wbi/acc/info?${query}`,
|
||
{
|
||
signal: controller.signal,
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
referer: `https://space.bilibili.com/${encodeURIComponent(uid)}`,
|
||
...(cookie ? { cookie } : {}),
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
if (response?.code !== 0) {
|
||
return null;
|
||
}
|
||
|
||
return normalizeBilibiliAvatarUrl(response.data?.face);
|
||
} finally {
|
||
clearTimeout(timeout);
|
||
}
|
||
}
|
||
|
||
async function detectBilibili({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const cookie = await sessionCookieHeader(session, "bilibili.com");
|
||
const response = await sessionFetchJson<BilibiliNavResponse>(
|
||
session,
|
||
`${BILIBILI_API_ORIGIN}/x/web-interface/nav?build=0&mobi_app=web`,
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
...(cookie ? { cookie } : {}),
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
|
||
const user = response?.data;
|
||
const uid = stringId(user?.mid);
|
||
if (response?.code !== 0 || !user?.isLogin || !uid || !user.uname) {
|
||
return null;
|
||
}
|
||
|
||
const avatarUrl =
|
||
normalizeBilibiliAvatarUrl(user.face)
|
||
?? await fetchBilibiliSpaceAvatar(session, uid, response, cookie);
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: user.uname,
|
||
avatarUrl,
|
||
};
|
||
}
|
||
|
||
async function detectJuejin({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<JuejinProfileResponse>(
|
||
session,
|
||
"https://api.juejin.cn/user_api/v1/user/get_profile",
|
||
{
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
accept: "application/json",
|
||
},
|
||
body: "{}",
|
||
},
|
||
).catch(() => null);
|
||
|
||
const user = response?.data?.bui_user;
|
||
if (!user?.user_id || !user.screen_name) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: user.user_id,
|
||
displayName: user.screen_name,
|
||
avatarUrl: normalizeRemoteUrl(user.avatar_url ?? user.avatar_large),
|
||
};
|
||
}
|
||
|
||
function normalizeSmzdmUid(value: unknown): string {
|
||
const uid = stringId(value);
|
||
return uid && uid !== "0" ? uid : "";
|
||
}
|
||
|
||
function smzdmAccountFromCurrentUser(
|
||
response: SmzdmCurrentUser | null | undefined,
|
||
fallback?: {
|
||
displayName?: string | null;
|
||
avatarUrl?: string | null;
|
||
} | null,
|
||
): DetectedAccount | null {
|
||
const user = response?.data ?? response;
|
||
const uid = normalizeSmzdmUid(user?.smzdm_id);
|
||
if (!uid) {
|
||
return null;
|
||
}
|
||
|
||
const nickname =
|
||
normalizeText(user?.nickname)
|
||
?? normalizeText(user?.audit_nickname)
|
||
?? normalizeText(fallback?.displayName)
|
||
?? `值友${uid}`;
|
||
return {
|
||
platformUid: uid,
|
||
displayName: nickname,
|
||
avatarUrl: normalizeRemoteUrl(user?.avatar) ?? normalizeRemoteUrl(fallback?.avatarUrl),
|
||
};
|
||
}
|
||
|
||
async function readSmzdmPageState(
|
||
webContents: WebContents | null | undefined,
|
||
): Promise<SmzdmPageState | null> {
|
||
if (!webContents || webContents.isDestroyed()) {
|
||
return null;
|
||
}
|
||
|
||
const serialized = await webContents.executeJavaScript(`(() => {
|
||
try {
|
||
const normalize = (value) => {
|
||
if (typeof value !== "string") return null;
|
||
const trimmed = value.trim().replace(/\\s+/g, " ");
|
||
return trimmed || null;
|
||
};
|
||
const compact = (value) => normalize(value)?.replace(/\\s+/g, "") || null;
|
||
const isVisible = (node) => {
|
||
if (!(node instanceof Element)) return false;
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
||
return false;
|
||
}
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
};
|
||
const ignored = /^(首页|好价|社区|百科|集团官网|商业合作|爆料投稿|个人中心|我的消息|兑换记录|账户设置|关注|粉丝|社区达人榜|查看更多|搜索|文章|笔记|视频|商品|众测|评论|收藏|品牌认领|金币|碎银子|经验|登录|注册|扫码登录|手机号登录)$/;
|
||
const scoreName = (text, rect) => {
|
||
let score = 0;
|
||
if (/^值友[0-9A-Za-z_-]{4,}$/.test(text)) score += 80;
|
||
if (rect.top >= 120 && rect.top <= 360 && rect.left >= 160 && rect.left <= 820) score += 40;
|
||
if (rect.top >= 40 && rect.top <= 140 && rect.left >= window.innerWidth * 0.72) score += 20;
|
||
return score;
|
||
};
|
||
const pickDisplayName = () => {
|
||
const candidates = [];
|
||
const selectors = [
|
||
"[class*=nickname]",
|
||
"[class*=user] [class*=name]",
|
||
"[class*=account] [class*=name]",
|
||
"[class*=profile] [class*=name]",
|
||
".name",
|
||
];
|
||
for (const selector of selectors) {
|
||
for (const node of document.querySelectorAll(selector)) {
|
||
if (!isVisible(node)) continue;
|
||
const text = compact(node.textContent || "");
|
||
if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue;
|
||
const rect = node.getBoundingClientRect();
|
||
candidates.push({ text, score: scoreName(text, rect) + 30 });
|
||
}
|
||
}
|
||
for (const node of document.querySelectorAll("body *")) {
|
||
if (!isVisible(node)) continue;
|
||
const text = compact(node.textContent || "");
|
||
if (!text || text.length < 2 || text.length > 40 || ignored.test(text)) continue;
|
||
const rect = node.getBoundingClientRect();
|
||
if (rect.top < 0 || rect.top > 420 || rect.left < 80) continue;
|
||
const score = scoreName(text, rect);
|
||
if (score > 0) {
|
||
candidates.push({ text, score });
|
||
}
|
||
}
|
||
candidates.sort((left, right) => right.score - left.score);
|
||
if (candidates[0]?.text) {
|
||
return candidates[0].text;
|
||
}
|
||
const bodyText = document.body?.innerText || "";
|
||
return compact(bodyText.match(/值友[0-9A-Za-z_-]{4,}/)?.[0] || "");
|
||
};
|
||
const pickAvatar = () => {
|
||
const images = Array.from(document.images)
|
||
.map((image) => ({ src: image.currentSrc || image.src || "", rect: image.getBoundingClientRect() }))
|
||
.filter(({ src, rect }) =>
|
||
src
|
||
&& !/(logo|favicon|sprite|blank)/i.test(src)
|
||
&& rect.top >= 100
|
||
&& rect.top <= 420
|
||
&& rect.left >= 80
|
||
&& rect.left <= 520
|
||
&& rect.width >= 40
|
||
&& rect.height >= 40
|
||
)
|
||
.sort((left, right) => {
|
||
const leftArea = left.rect.width * left.rect.height;
|
||
const rightArea = right.rect.width * right.rect.height;
|
||
return rightArea - leftArea;
|
||
});
|
||
return images[0]?.src || null;
|
||
};
|
||
const displayName = pickDisplayName();
|
||
const bodyText = document.body?.innerText || "";
|
||
const path = window.location.pathname || "";
|
||
const loggedInPage =
|
||
/(?:^|\\.)smzdm\\.com$/i.test(window.location.hostname)
|
||
&& !/\\/user\\/login/i.test(path)
|
||
&& Boolean(displayName)
|
||
&& (
|
||
/个人中心/.test(bodyText)
|
||
|| /账户设置/.test(bodyText)
|
||
|| /我的消息/.test(bodyText)
|
||
|| /^值友[0-9A-Za-z_-]{4,}$/.test(displayName)
|
||
);
|
||
|
||
return JSON.stringify({
|
||
displayName,
|
||
avatarUrl: pickAvatar(),
|
||
loggedInPage,
|
||
});
|
||
} catch {
|
||
return JSON.stringify(null);
|
||
}
|
||
})()`, true).catch(() => null);
|
||
|
||
if (typeof serialized !== "string" || !serialized) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(serialized) as Partial<SmzdmPageState> | null;
|
||
if (!parsed || typeof parsed !== "object") {
|
||
return null;
|
||
}
|
||
return {
|
||
displayName: normalizeText(parsed.displayName),
|
||
avatarUrl: normalizeRemoteUrl(parsed.avatarUrl),
|
||
loggedInPage: parsed.loggedInPage === true,
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function deriveSmzdmCookieAccount(
|
||
session: Session,
|
||
pageState?: SmzdmPageState | null,
|
||
): Promise<DetectedAccount | null> {
|
||
const uid = normalizeSmzdmUid(await sessionCookieValue(session, "smzdm.com", "smzdm_id").catch(() => ""));
|
||
const userCookie = await sessionCookieValue(session, "smzdm.com", "user").catch(() => "");
|
||
const sessCookie = await sessionCookieValue(session, "smzdm.com", "sess").catch(() => "");
|
||
const decodedUser = normalizeText(decodeCookieText(userCookie));
|
||
const displayName =
|
||
decodedUser
|
||
?? normalizeText(pageState?.displayName)
|
||
?? (uid ? `值友${uid}` : null);
|
||
|
||
if (uid && displayName) {
|
||
return {
|
||
platformUid: uid,
|
||
displayName,
|
||
avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl),
|
||
};
|
||
}
|
||
|
||
const authFingerprint = normalizeText(sessCookie);
|
||
if (authFingerprint && displayName) {
|
||
return {
|
||
platformUid: boundedIdentity("smzdm-sess", authFingerprint),
|
||
displayName,
|
||
avatarUrl: normalizeRemoteUrl(pageState?.avatarUrl),
|
||
};
|
||
}
|
||
|
||
if (pageState?.loggedInPage && pageState.displayName) {
|
||
const cookieHeader = await sessionCookieHeader(session, "smzdm.com").catch(() => "");
|
||
return {
|
||
platformUid: boundedIdentity("smzdm-page", cookieHeader || pageState.displayName),
|
||
displayName: pageState.displayName,
|
||
avatarUrl: normalizeRemoteUrl(pageState.avatarUrl),
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
async function detectSmzdm({ session, webContents }: DetectContext): Promise<DetectedAccount | null> {
|
||
const currentUserUrl = "https://zhiyou.smzdm.com/user/info/jsonp_get_current";
|
||
const pageState = await readSmzdmPageState(webContents).catch(() => null);
|
||
const pageResponse = await pageFetchJson<SmzdmCurrentUser>(
|
||
webContents,
|
||
currentUserUrl,
|
||
{
|
||
credentials: "include",
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
const fromPage = smzdmAccountFromCurrentUser(pageResponse, pageState);
|
||
if (fromPage) {
|
||
return fromPage;
|
||
}
|
||
|
||
const sessionResponse = await sessionFetchJson<SmzdmCurrentUser>(
|
||
session,
|
||
currentUserUrl,
|
||
{
|
||
credentials: "include",
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
referer: "https://zhiyou.smzdm.com/user",
|
||
origin: "https://zhiyou.smzdm.com",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
const fromSession = smzdmAccountFromCurrentUser(sessionResponse, pageState);
|
||
if (fromSession) {
|
||
return fromSession;
|
||
}
|
||
|
||
const cookieResponse = await sessionCookieFetchJson<SmzdmCurrentUser>(
|
||
session,
|
||
currentUserUrl,
|
||
{
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
},
|
||
},
|
||
).catch(() => null);
|
||
const fromCookieRequest = smzdmAccountFromCurrentUser(cookieResponse, pageState);
|
||
if (fromCookieRequest) {
|
||
return fromCookieRequest;
|
||
}
|
||
|
||
return await deriveSmzdmCookieAccount(session, pageState);
|
||
}
|
||
|
||
async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const html = await sessionFetchText(session, WEIXIN_GZH_HOME_URL).catch(() => "");
|
||
if (!html) {
|
||
return null;
|
||
}
|
||
|
||
const token = extractWeixinGzhToken(html);
|
||
const userName = html.match(/user_name:\s*["']([^"']+)["']/)?.[1] ?? "";
|
||
const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? "";
|
||
const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? "";
|
||
const avatarFromMeta = html.match(/head_img:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
||
const avatarUrl = (avatarFromHead || avatarFromMeta || "").replace(/^http:\/\//, "https://");
|
||
|
||
if (!token || !userName || !nickName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: userName,
|
||
displayName: nickName,
|
||
avatarUrl: normalizeRemoteUrl(avatarUrl),
|
||
};
|
||
}
|
||
|
||
async function zolRequestHeaders(session: Session): Promise<Record<string, string>> {
|
||
const cookie = await sessionCookieHeader(session, "zol.com.cn").catch(() => "");
|
||
return {
|
||
accept: "application/json, text/plain, */*",
|
||
origin: "https://post.zol.com.cn",
|
||
referer: ZOL_REFERER,
|
||
...(cookie ? { cookie } : {}),
|
||
};
|
||
}
|
||
|
||
async function detectZol({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<ZolUserInfoResponse>(
|
||
session,
|
||
`${ZOL_API_ORIGIN}/api/v1/creator.user.getinfo`,
|
||
{
|
||
credentials: "include",
|
||
headers: await zolRequestHeaders(session),
|
||
},
|
||
).catch(() => null);
|
||
|
||
const user = response?.data;
|
||
const uid = stringId(user?.userId);
|
||
if (!uid || !user?.nickName) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: uid,
|
||
displayName: user.nickName,
|
||
avatarUrl: normalizeRemoteUrl(user.photo),
|
||
};
|
||
}
|
||
|
||
async function detectDongchedi({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||
const response = await sessionFetchJson<DongchediAccountInfoResponse>(
|
||
session,
|
||
"https://mp.dcdapp.com/passport/account/info/v2/?aid=2302&account_sdk_source=web",
|
||
).catch(() => null);
|
||
|
||
const user = response?.data;
|
||
if (!user?.user_id_str || !user.name) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
platformUid: user.user_id_str,
|
||
displayName: user.name,
|
||
avatarUrl: normalizeRemoteUrl(user.avatar_url),
|
||
};
|
||
}
|
||
|
||
const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = {
|
||
toutiaohao: {
|
||
id: "toutiaohao",
|
||
label: "头条号",
|
||
loginUrl: TOUTIAO_LOGIN_URL,
|
||
consoleUrl: TOUTIAO_CONSOLE_HOME_URL,
|
||
detect: detectToutiao,
|
||
},
|
||
baijiahao: {
|
||
id: "baijiahao",
|
||
label: "百家号",
|
||
loginUrl: "https://baijiahao.baidu.com/builder/theme/bjh/login",
|
||
consoleUrl: "https://baijiahao.baidu.com/builder/rc/edit?type=news",
|
||
detect: detectBaijiahao,
|
||
},
|
||
sohuhao: {
|
||
id: "sohuhao",
|
||
label: "搜狐号",
|
||
loginUrl: "https://mp.sohu.com/mpfe/v4/login",
|
||
consoleUrl: "https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle",
|
||
detect: detectSohu,
|
||
},
|
||
qiehao: {
|
||
id: "qiehao",
|
||
label: "企鹅号",
|
||
loginUrl: "https://om.qq.com",
|
||
consoleUrl: "https://om.qq.com/main.html#/article/add",
|
||
detect: detectQiehao,
|
||
},
|
||
zhihu: {
|
||
id: "zhihu",
|
||
label: "知乎",
|
||
loginUrl: "https://www.zhihu.com/signin",
|
||
consoleUrl: "https://zhuanlan.zhihu.com/write",
|
||
detect: detectZhihu,
|
||
},
|
||
wangyihao: {
|
||
id: "wangyihao",
|
||
label: "网易号",
|
||
loginUrl: "https://mp.163.com/login.html",
|
||
consoleUrl: WANGYI_CONSOLE_URL,
|
||
loginRedirectUrls: WANGYI_LOGIN_REDIRECT_URLS,
|
||
detect: detectWangyihao,
|
||
},
|
||
jianshu: {
|
||
id: "jianshu",
|
||
label: "简书",
|
||
loginUrl: "https://www.jianshu.com/sign_in",
|
||
consoleUrl: "https://www.jianshu.com/writer",
|
||
detect: detectJianshu,
|
||
},
|
||
bilibili: {
|
||
id: "bilibili",
|
||
label: "bilibili",
|
||
loginUrl: "https://www.bilibili.com",
|
||
consoleUrl: "https://member.bilibili.com/platform/upload/text/edit",
|
||
detect: detectBilibili,
|
||
},
|
||
juejin: {
|
||
id: "juejin",
|
||
label: "稀土掘金",
|
||
loginUrl: "https://juejin.cn/login",
|
||
consoleUrl: "https://juejin.cn/editor/drafts/new/v3",
|
||
detect: detectJuejin,
|
||
},
|
||
smzdm: {
|
||
id: "smzdm",
|
||
label: "什么值得买",
|
||
loginUrl: "https://zhiyou.smzdm.com/user/login",
|
||
consoleUrl: "https://post.smzdm.com/",
|
||
detect: detectSmzdm,
|
||
},
|
||
weixin_gzh: {
|
||
id: "weixin_gzh",
|
||
label: "微信公众号",
|
||
loginUrl: WEIXIN_GZH_LOGIN_URL,
|
||
consoleUrl: buildWeixinGzhDraftListURL(),
|
||
detect: detectWeixinGzh,
|
||
},
|
||
zol: {
|
||
id: "zol",
|
||
label: "中关村在线",
|
||
loginUrl: ZOL_CONSOLE_URL,
|
||
consoleUrl: ZOL_CONSOLE_URL,
|
||
loginRedirectUrls: ZOL_LOGIN_REDIRECT_URLS,
|
||
detect: detectZol,
|
||
},
|
||
dongchedi: {
|
||
id: "dongchedi",
|
||
label: "懂车帝",
|
||
loginUrl: "https://mp.dcdapp.com/login",
|
||
consoleUrl: "https://mp.dcdapp.com/profile_v2/publish/article",
|
||
detect: detectDongchedi,
|
||
},
|
||
};
|
||
|
||
const aiBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = Object.fromEntries(
|
||
aiPlatformCatalog.map((platform) => [
|
||
platform.id,
|
||
{
|
||
id: platform.id,
|
||
label: platform.label,
|
||
loginUrl: platform.loginUrl,
|
||
consoleUrl: platform.consoleUrl,
|
||
detect: (context: DetectContext) => detectAIPlatformAccount(platform.id, platform.label, context),
|
||
},
|
||
]),
|
||
) as Record<string, PublishPlatformBindingDefinition>;
|
||
|
||
const platformBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = {
|
||
...publishBindingDefinitions,
|
||
...aiBindingDefinitions,
|
||
};
|
||
|
||
function createBoundWindow(
|
||
title: string,
|
||
targetURL: string,
|
||
sessionHandle: Session,
|
||
options: { show?: boolean } = {},
|
||
): BrowserWindow {
|
||
const show = options.show ?? true;
|
||
const window = new BrowserWindow({
|
||
show,
|
||
width: 1320,
|
||
height: 900,
|
||
minWidth: 1100,
|
||
minHeight: 760,
|
||
title,
|
||
autoHideMenuBar: true,
|
||
skipTaskbar: !show,
|
||
focusable: show,
|
||
hiddenInMissionControl: !show,
|
||
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
|
||
webPreferences: {
|
||
session: sessionHandle,
|
||
sandbox: true,
|
||
contextIsolation: true,
|
||
nodeIntegration: false,
|
||
},
|
||
});
|
||
|
||
window.webContents.setUserAgent(STANDARD_USER_AGENT);
|
||
attachWindowDiagnostics(window, title);
|
||
|
||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||
if (/^https?:\/\//i.test(url)) {
|
||
setImmediate(() => {
|
||
if (!window.isDestroyed()) {
|
||
void loadWindowURLSafely(window, url, `${title} popup`);
|
||
}
|
||
});
|
||
}
|
||
return { action: "deny" };
|
||
});
|
||
|
||
void loadWindowURLSafely(window, targetURL, title);
|
||
return window;
|
||
}
|
||
|
||
function focusBindWindow(window: BrowserWindow): void {
|
||
if (window.isDestroyed()) {
|
||
return;
|
||
}
|
||
|
||
if (window.isMinimized()) {
|
||
window.restore();
|
||
}
|
||
if (!window.isVisible()) {
|
||
window.show();
|
||
}
|
||
|
||
window.focus();
|
||
}
|
||
|
||
function cleanupInactiveBindOperations(): void {
|
||
for (const [platformId, operation] of activeBindOperations.entries()) {
|
||
if (operation.window.isDestroyed()) {
|
||
activeBindOperations.delete(platformId);
|
||
}
|
||
}
|
||
}
|
||
|
||
function clearActiveBindOperation(platformId: string, window: BrowserWindow): void {
|
||
const operation = activeBindOperations.get(platformId);
|
||
if (operation?.window === window) {
|
||
activeBindOperations.delete(platformId);
|
||
}
|
||
}
|
||
|
||
export async function bindPublishAccount(platformId: string): Promise<DesktopAccountInfo> {
|
||
const definition = platformBindingDefinitions[platformId];
|
||
if (!definition) {
|
||
throw new Error(`desktop_platform_not_supported:${platformId}`);
|
||
}
|
||
|
||
cleanupInactiveBindOperations();
|
||
|
||
const existingOperation = activeBindOperations.get(platformId);
|
||
if (existingOperation && !existingOperation.window.isDestroyed()) {
|
||
focusBindWindow(existingOperation.window);
|
||
return existingOperation.promise;
|
||
}
|
||
|
||
if (activeBindOperations.size >= MAX_CONCURRENT_BIND_WINDOWS) {
|
||
throw new Error("desktop_account_bind_limit_reached");
|
||
}
|
||
|
||
const handle = createPendingSessionHandle(platformId);
|
||
let window: BrowserWindow;
|
||
|
||
try {
|
||
window = createBoundWindow(`绑定 ${definition.label}`, definition.loginUrl, handle.session);
|
||
} catch (error) {
|
||
forgetSessionHandle(handle.accountId);
|
||
throw error instanceof Error ? error : new Error("desktop_account_bind_failed");
|
||
}
|
||
|
||
const bindPromise = new Promise<DesktopAccountInfo>((resolve, reject) => {
|
||
let finished = false;
|
||
let checking = false;
|
||
let detectReady = false;
|
||
let detectDebounceHandle: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
const finalizeOperation = () => {
|
||
clearInterval(intervalHandle);
|
||
if (detectDebounceHandle) {
|
||
clearTimeout(detectDebounceHandle);
|
||
detectDebounceHandle = null;
|
||
}
|
||
clearActiveBindOperation(platformId, window);
|
||
};
|
||
|
||
const settleSuccess = (account: DesktopAccountInfo) => {
|
||
if (finished) {
|
||
return;
|
||
}
|
||
finished = true;
|
||
finalizeOperation();
|
||
attachSessionHandle(account.id, handle);
|
||
resolve(account);
|
||
if (!window.isDestroyed()) {
|
||
window.setTitle(`${definition.label}(${account.display_name}) 已绑定`);
|
||
setTimeout(() => {
|
||
if (!window.isDestroyed()) {
|
||
window.close();
|
||
}
|
||
}, 800);
|
||
}
|
||
};
|
||
|
||
const settleFailure = (error: Error) => {
|
||
if (finished) {
|
||
return;
|
||
}
|
||
finished = true;
|
||
finalizeOperation();
|
||
forgetSessionHandle(handle.accountId);
|
||
if (!window.isDestroyed()) {
|
||
window.close();
|
||
}
|
||
reject(error);
|
||
};
|
||
|
||
const syncDetectionState = () => {
|
||
if (window.isDestroyed()) {
|
||
detectReady = false;
|
||
return;
|
||
}
|
||
const currentURL = window.webContents.getURL();
|
||
const fallbackReady =
|
||
/^https?:\/\//i.test(currentURL)
|
||
&& !currentURL.startsWith("data:text/html")
|
||
&& !window.webContents.isLoading();
|
||
detectReady = isWindowReadyForDetection(window) || fallbackReady;
|
||
};
|
||
|
||
const scheduleDetectAndBind = (delayMs = 120) => {
|
||
if (finished || window.isDestroyed()) {
|
||
return;
|
||
}
|
||
if (detectDebounceHandle) {
|
||
clearTimeout(detectDebounceHandle);
|
||
}
|
||
detectDebounceHandle = setTimeout(() => {
|
||
detectDebounceHandle = null;
|
||
void detectAndBind();
|
||
}, delayMs);
|
||
};
|
||
|
||
const handleNavigationReady = () => {
|
||
syncDetectionState();
|
||
scheduleDetectAndBind();
|
||
};
|
||
|
||
window.webContents.on("did-finish-load", handleNavigationReady);
|
||
window.webContents.on("did-navigate", handleNavigationReady);
|
||
window.webContents.on("did-navigate-in-page", handleNavigationReady);
|
||
window.webContents.on("page-title-updated", () => scheduleDetectAndBind(250));
|
||
window.webContents.on(
|
||
"did-fail-load",
|
||
(_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => {
|
||
if (isMainFrame && errorCode !== -3) {
|
||
detectReady = false;
|
||
}
|
||
},
|
||
);
|
||
|
||
const detectAndBind = async () => {
|
||
syncDetectionState();
|
||
const currentURL = window.isDestroyed() ? "" : window.webContents.getURL();
|
||
const canProbe = detectReady || /^https?:\/\//i.test(currentURL);
|
||
if (finished || checking || !canProbe || window.isDestroyed()) {
|
||
return;
|
||
}
|
||
checking = true;
|
||
try {
|
||
console.info("[desktop-bind] detect tick", {
|
||
platform: definition.id,
|
||
detectReady,
|
||
canProbe,
|
||
loading: window.webContents.isLoading(),
|
||
url: currentURL,
|
||
});
|
||
const detected = await definition
|
||
.detect({ session: handle.session, webContents: window.webContents })
|
||
.catch((error) => {
|
||
console.warn(`[desktop-bind] ${definition.id} detect failed`, {
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
return null;
|
||
});
|
||
if (!detected) {
|
||
return;
|
||
}
|
||
let normalizedDetected = sanitizeDetectedAccount(detected);
|
||
console.info("[desktop-bind] detect success", {
|
||
platform: definition.id,
|
||
displayName: normalizedDetected.displayName,
|
||
platformUid: normalizedDetected.platformUid,
|
||
url: currentURL,
|
||
});
|
||
|
||
if (definition.id === "toutiaohao") {
|
||
const consoleReady = await verifyToutiaoConsoleAccess(window, handle.session);
|
||
console.info("[desktop-bind] toutiao console verification", {
|
||
platform: definition.id,
|
||
ready: consoleReady,
|
||
url: window.isDestroyed() ? "" : window.webContents.getURL(),
|
||
});
|
||
if (!consoleReady) {
|
||
return;
|
||
}
|
||
|
||
const detectedFromWorkbench = await definition
|
||
.detect({ session: handle.session, webContents: window.webContents })
|
||
.catch(() => null);
|
||
if (detectedFromWorkbench) {
|
||
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench);
|
||
}
|
||
}
|
||
|
||
if (definition.id === "wangyihao") {
|
||
const consoleReady = await verifyWangyihaoBindWindowReady(
|
||
window,
|
||
handle.session,
|
||
normalizedDetected,
|
||
);
|
||
console.info("[desktop-bind] wangyihao console verification", {
|
||
platform: definition.id,
|
||
ready: consoleReady,
|
||
url: window.isDestroyed() ? "" : window.webContents.getURL(),
|
||
});
|
||
if (!consoleReady) {
|
||
return;
|
||
}
|
||
|
||
const detectedFromWorkbench = await definition
|
||
.detect({ session: handle.session, webContents: window.webContents })
|
||
.catch(() => null);
|
||
if (detectedFromWorkbench) {
|
||
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench);
|
||
}
|
||
}
|
||
|
||
normalizedDetected = await finalizeDetectedAccountForBind(
|
||
definition,
|
||
{ session: handle.session, webContents: window.webContents },
|
||
normalizedDetected,
|
||
);
|
||
|
||
void flushSessionPersistence(handle.session);
|
||
|
||
try {
|
||
const account = await upsertDesktopAccount({
|
||
platform: definition.id,
|
||
platform_uid: normalizedDetected.platformUid,
|
||
display_name: normalizedDetected.displayName,
|
||
avatar_url: normalizedDetected.avatarUrl,
|
||
account_fingerprint: normalizedDetected.platformUid,
|
||
health: "live",
|
||
verified_at: new Date().toISOString(),
|
||
tags: [],
|
||
});
|
||
|
||
if (definition.id === "wangyihao") {
|
||
await saveWangyihaoSessionCookies(account.id, handle.session);
|
||
}
|
||
|
||
settleSuccess(account);
|
||
} catch (error) {
|
||
console.error("[desktop-bind] upsert failed", {
|
||
platform: definition.id,
|
||
url: currentURL,
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
if (!window.isDestroyed()) {
|
||
window.setTitle(`${definition.label} 绑定保存失败,保留窗口重试`);
|
||
}
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
console.error("[desktop-bind] bind failed", {
|
||
platform: definition.id,
|
||
url: currentURL,
|
||
message: error instanceof Error ? error.message : String(error),
|
||
});
|
||
settleFailure(error instanceof Error ? error : new Error("desktop_account_bind_failed"));
|
||
} finally {
|
||
checking = false;
|
||
}
|
||
};
|
||
|
||
const intervalHandle = setInterval(() => {
|
||
void detectAndBind();
|
||
}, 650);
|
||
|
||
window.on("closed", () => {
|
||
clearActiveBindOperation(platformId, window);
|
||
if (!finished) {
|
||
clearInterval(intervalHandle);
|
||
forgetSessionHandle(handle.accountId);
|
||
reject(new Error("desktop_account_bind_window_closed"));
|
||
}
|
||
});
|
||
|
||
syncDetectionState();
|
||
void detectAndBind();
|
||
});
|
||
|
||
activeBindOperations.set(platformId, {
|
||
platformId,
|
||
window,
|
||
promise: bindPromise,
|
||
});
|
||
focusBindWindow(window);
|
||
|
||
return await bindPromise;
|
||
}
|
||
|
||
export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise<void> {
|
||
const definition = platformBindingDefinitions[account.platform];
|
||
if (!definition) {
|
||
throw new Error(`desktop_platform_not_supported:${account.platform}`);
|
||
}
|
||
|
||
const handle = await ensurePublishAccountSessionHandle(account);
|
||
const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session);
|
||
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
|
||
const detected = await definition.detect({
|
||
session: handle.session,
|
||
webContents: undefined as unknown as WebContents,
|
||
}).catch(() => null);
|
||
if (!detected || !detectedAccountMatchesIdentity(detected, account)) {
|
||
throw new Error(`desktop_account_session_expired:${account.platform}`);
|
||
}
|
||
}
|
||
const window = createBoundWindow(`${definition.label} 创作台`, consoleURL, handle.session);
|
||
window.show();
|
||
window.focus();
|
||
}
|