feat(desktop/weixin-gzh): drive editor to publish with draft fallback on verify

Replace the masssend HTTP path with a Playwright-driven editor flow that
clicks "群发" and resolves the publish response, then polls appmsgpublish
for the public mp.weixin.qq.com/s URL. When the dialog asks for admin
verification / scan-code authorization, swallow it as a successful draft
save and surface fallback_reason=publish_authorization_required so the
operator can finish from the console. Also broaden token extraction in
account-binder and treat the draft list URL (cgi-bin/appmsg) as the
console landing page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 19:56:56 +08:00
parent ed6b2c09fa
commit a09d44d8f0
2 changed files with 1227 additions and 89 deletions
+33 -10
View File
@@ -249,7 +249,8 @@ 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 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/";
@@ -444,7 +445,33 @@ function extractToutiaoAccount(response: ToutiaoMediaInfoResponse | null | undef
}
function extractWeixinGzhToken(html: string): string {
return html.match(/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/)?.[1] ?? "";
const patterns = [
/data:\s*\{[\s\S]*?t:\s*["']([^"']+)["']/,
/(?:\?|&amp;|&)token=(\d+)(?:&amp;|&|$)/,
/["']?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 {
@@ -504,14 +531,10 @@ async function resolveWeixinGzhConsoleURL(session: Session): Promise<string> {
const token = extractWeixinGzhToken(html);
if (!token) {
return WEIXIN_GZH_HOME_URL;
return WEIXIN_GZH_LOGIN_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();
return buildWeixinGzhDraftListURL(token);
}
async function resolvePublishAccountConsoleURL(
@@ -3065,8 +3088,8 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
weixin_gzh: {
id: "weixin_gzh",
label: "微信公众号",
loginUrl: "https://mp.weixin.qq.com/cgi-bin/loginpage",
consoleUrl: WEIXIN_GZH_CONSOLE_HOME_URL,
loginUrl: WEIXIN_GZH_LOGIN_URL,
consoleUrl: buildWeixinGzhDraftListURL(),
detect: detectWeixinGzh,
},
zol: {
File diff suppressed because it is too large Load Diff