From e89349399b77a6613ef6d6fe76e82eac0196704d Mon Sep 17 00:00:00 2001 From: liangxu Date: Wed, 29 Apr 2026 17:11:35 +0800 Subject: [PATCH] feat(desktop/weixin-gzh): probe authenticated console with token resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Naked /cgi-bin/home redirects to the login page even with a live cookie, which made probe/refresh/verify mis-classify accounts as expired. Resolve the tokenized console URL via mp.weixin.qq.com first, then read the page state to detect 登录超时/未登录 banners and reuse detectWeixinGzh to confirm identity. Open-console also now lands on the tokenized URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop-client/src/main/account-binder.ts | 188 +++++++++++++++++- 1 file changed, 180 insertions(+), 8 deletions(-) diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index d50a175..534a066 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -88,6 +88,12 @@ interface ActiveBindOperation { promise: Promise; } +interface WeixinGzhConsoleState { + loggedOut: boolean; + title: string; + textSnippet: string; +} + type ToutiaoMediaInfoResponse = { data?: { user?: { @@ -242,6 +248,8 @@ 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_CONSOLE_HOME_URL = "https://mp.weixin.qq.com/cgi-bin/home"; 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/"; @@ -435,6 +443,87 @@ function extractToutiaoAccount(response: ToutiaoMediaInfoResponse | null | undef return candidates.find((item) => item !== null) ?? null; } +function extractWeixinGzhToken(html: string): string { + return html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? ""; +} + +function isWeixinGzhLoggedOutText(text: string): boolean { + return /(登录超时|登录态超时|登录已过期|会话已过期|请重新登录|未登录)/.test(text); +} + +async function readWeixinGzhConsoleState( + webContents: WebContents | null | undefined, +): Promise { + 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; + 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 { + 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_HOME_URL; + } + + const url = new URL(WEIXIN_GZH_CONSOLE_HOME_URL); + url.searchParams.set("t", "home/index"); + url.searchParams.set("lang", "zh_CN"); + url.searchParams.set("token", token); + return url.toString(); +} + +async function resolvePublishAccountConsoleURL( + definition: PublishPlatformBindingDefinition, + session: Session, +): Promise { + if (definition.id === "weixin_gzh") { + return await resolveWeixinGzhConsoleURL(session); + } + return definition.consoleUrl; +} + async function detectToutiaoFromPageState( webContents: WebContents, ): Promise | null> { @@ -1609,7 +1698,7 @@ async function resolvePublishAccountProfileFromHandle( if (isAIPlatformBinding(account.platform)) { const window = createBoundWindow( `${definition.label} 静默识别`, - definition.consoleUrl, + await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); @@ -1673,7 +1762,7 @@ export async function probePublishAccountSession( const window = createBoundWindow( `${definition.label} 静默探针`, - definition.consoleUrl, + await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); @@ -1749,6 +1838,62 @@ export async function probePublishAccountSession( }; } + 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) { @@ -1954,7 +2099,7 @@ export async function silentRefreshPublishAccountSession( const window = createBoundWindow( `${definition.label} 静默续期`, - definition.consoleUrl, + await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); @@ -1967,6 +2112,19 @@ export async function silentRefreshPublishAccountSession( 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); } @@ -2214,7 +2372,7 @@ async function verifyPublishAccountConsoleAccess( const window = createBoundWindow( `${definition.label} 静默校验`, - definition.consoleUrl, + await resolvePublishAccountConsoleURL(definition, handle.session), handle.session, { show: false }, ); @@ -2242,6 +2400,19 @@ async function verifyPublishAccountConsoleAccess( 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 === "zol") { return await verifyZolConsoleAccess(window, handle.session, account); } @@ -2746,12 +2917,12 @@ async function detectSmzdm({ session, webContents }: DetectContext): Promise { - const html = await sessionFetchText(session, "https://mp.weixin.qq.com/").catch(() => ""); + const html = await sessionFetchText(session, WEIXIN_GZH_HOME_URL).catch(() => ""); if (!html) { return null; } - const token = html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? ""; + 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] ?? ""; @@ -2895,7 +3066,7 @@ const publishBindingDefinitions: Record