feat(desktop/weixin-gzh): probe authenticated console with token resolver

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 17:11:35 +08:00
parent 8dcd60b604
commit e89349399b
+180 -8
View File
@@ -88,6 +88,12 @@ interface ActiveBindOperation {
promise: Promise<DesktopAccountInfo>;
}
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<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_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<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> {
@@ -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<Det
}
async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAccount | null> {
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<string, PublishPlatformBindingDefinition
id: "weixin_gzh",
label: "微信公众号",
loginUrl: "https://mp.weixin.qq.com/cgi-bin/loginpage",
consoleUrl: "https://mp.weixin.qq.com/cgi-bin/home",
consoleUrl: WEIXIN_GZH_CONSOLE_HOME_URL,
detect: detectWeixinGzh,
},
zol: {
@@ -3258,7 +3429,8 @@ export async function openPublishAccountConsole(account: PublishAccountIdentity)
}
const handle = await ensurePublishAccountSessionHandle(account);
const window = createBoundWindow(`${definition.label} 创作台`, definition.consoleUrl, handle.session);
const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session);
const window = createBoundWindow(`${definition.label} 创作台`, consoleURL, handle.session);
window.show();
window.focus();
}