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:
@@ -88,6 +88,12 @@ interface ActiveBindOperation {
|
|||||||
promise: Promise<DesktopAccountInfo>;
|
promise: Promise<DesktopAccountInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WeixinGzhConsoleState {
|
||||||
|
loggedOut: boolean;
|
||||||
|
title: string;
|
||||||
|
textSnippet: string;
|
||||||
|
}
|
||||||
|
|
||||||
type ToutiaoMediaInfoResponse = {
|
type ToutiaoMediaInfoResponse = {
|
||||||
data?: {
|
data?: {
|
||||||
user?: {
|
user?: {
|
||||||
@@ -242,6 +248,8 @@ const MAX_CONCURRENT_BIND_WINDOWS = 2;
|
|||||||
const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login";
|
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_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index";
|
||||||
const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i;
|
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 BILIBILI_API_ORIGIN = "https://api.bilibili.com";
|
||||||
const ZOL_API_ORIGIN = "https://open-api.zol.com.cn";
|
const ZOL_API_ORIGIN = "https://open-api.zol.com.cn";
|
||||||
const ZOL_REFERER = "https://post.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;
|
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(
|
async function detectToutiaoFromPageState(
|
||||||
webContents: WebContents,
|
webContents: WebContents,
|
||||||
): Promise<Pick<DetectedAccount, "platformUid" | "displayName" | "avatarUrl"> | null> {
|
): Promise<Pick<DetectedAccount, "platformUid" | "displayName" | "avatarUrl"> | null> {
|
||||||
@@ -1609,7 +1698,7 @@ async function resolvePublishAccountProfileFromHandle(
|
|||||||
if (isAIPlatformBinding(account.platform)) {
|
if (isAIPlatformBinding(account.platform)) {
|
||||||
const window = createBoundWindow(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默识别`,
|
`${definition.label} 静默识别`,
|
||||||
definition.consoleUrl,
|
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||||
handle.session,
|
handle.session,
|
||||||
{ show: false },
|
{ show: false },
|
||||||
);
|
);
|
||||||
@@ -1673,7 +1762,7 @@ export async function probePublishAccountSession(
|
|||||||
|
|
||||||
const window = createBoundWindow(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默探针`,
|
`${definition.label} 静默探针`,
|
||||||
definition.consoleUrl,
|
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||||
handle.session,
|
handle.session,
|
||||||
{ show: false },
|
{ 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") {
|
if (account.platform === "toutiaohao") {
|
||||||
const detected = await detectToutiaoFromSession(handle.session);
|
const detected = await detectToutiaoFromSession(handle.session);
|
||||||
if (detected) {
|
if (detected) {
|
||||||
@@ -1954,7 +2099,7 @@ export async function silentRefreshPublishAccountSession(
|
|||||||
|
|
||||||
const window = createBoundWindow(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默续期`,
|
`${definition.label} 静默续期`,
|
||||||
definition.consoleUrl,
|
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||||
handle.session,
|
handle.session,
|
||||||
{ show: false },
|
{ show: false },
|
||||||
);
|
);
|
||||||
@@ -1967,6 +2112,19 @@ export async function silentRefreshPublishAccountSession(
|
|||||||
|
|
||||||
await sleep(1_200);
|
await sleep(1_200);
|
||||||
await flushSessionPersistence(handle.session);
|
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") {
|
if (account.platform === "zol") {
|
||||||
return await verifyZolConsoleAccess(window, handle.session, account);
|
return await verifyZolConsoleAccess(window, handle.session, account);
|
||||||
}
|
}
|
||||||
@@ -2214,7 +2372,7 @@ async function verifyPublishAccountConsoleAccess(
|
|||||||
|
|
||||||
const window = createBoundWindow(
|
const window = createBoundWindow(
|
||||||
`${definition.label} 静默校验`,
|
`${definition.label} 静默校验`,
|
||||||
definition.consoleUrl,
|
await resolvePublishAccountConsoleURL(definition, handle.session),
|
||||||
handle.session,
|
handle.session,
|
||||||
{ show: false },
|
{ show: false },
|
||||||
);
|
);
|
||||||
@@ -2242,6 +2400,19 @@ async function verifyPublishAccountConsoleAccess(
|
|||||||
return detected !== 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 === "zol") {
|
if (account.platform === "zol") {
|
||||||
return await verifyZolConsoleAccess(window, handle.session, account);
|
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> {
|
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) {
|
if (!html) {
|
||||||
return null;
|
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 userName = html.match(/user_name:\s*["']([^"']+)["']/)?.[1] ?? "";
|
||||||
const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? "";
|
const nickName = html.match(/nick_name:\s*["']([^"']+)["']/)?.[1] ?? "";
|
||||||
const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? "";
|
const avatarFromHead = html.match(/class="weui-desktop-account__thumb"[^>]*src="([^"]+)"/)?.[1] ?? "";
|
||||||
@@ -2895,7 +3066,7 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
|
|||||||
id: "weixin_gzh",
|
id: "weixin_gzh",
|
||||||
label: "微信公众号",
|
label: "微信公众号",
|
||||||
loginUrl: "https://mp.weixin.qq.com/cgi-bin/loginpage",
|
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,
|
detect: detectWeixinGzh,
|
||||||
},
|
},
|
||||||
zol: {
|
zol: {
|
||||||
@@ -3258,7 +3429,8 @@ export async function openPublishAccountConsole(account: PublishAccountIdentity)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handle = await ensurePublishAccountSessionHandle(account);
|
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.show();
|
||||||
window.focus();
|
window.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user