fix(publish): harden zol login detection and content image upload

- Detect zol login redirects via passport/service hostnames and verify
  console access by re-detecting the bound account so a different logged
  in user is treated as expired instead of active.
- Skip networkPic upload for local/private URLs and fall back to file
  upload (with siteType retry) so desktop-hosted assets no longer reach
  ZOL as unreachable links and produce blank images.
- Surface zol_content_image_upload_failed and challenge errors instead
  of silently substituting empty image URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 12:21:12 +08:00
parent b4cfc6f992
commit e1a1916bc5
3 changed files with 293 additions and 18 deletions
+93 -13
View File
@@ -60,6 +60,7 @@ interface PublishPlatformBindingDefinition {
label: string;
loginUrl: string;
consoleUrl: string;
loginRedirectUrls?: string[];
detect(context: DetectContext): Promise<DetectedAccount | null>;
}
@@ -242,6 +243,13 @@ 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 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 BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32,
15, 50, 10, 31, 58, 3, 45, 35,
@@ -1729,7 +1737,7 @@ export async function probePublishAccountSession(
};
}
if (looksLikeLoginRedirect(currentURL, definition.loginUrl)) {
if (definitionLooksLikeLoginRedirect(currentURL, definition)) {
return {
verdict: "expired",
reason: "login_redirect",
@@ -1810,7 +1818,7 @@ export async function probePublishAccountSession(
};
}
if (pageState.loggedOutSignalCount > 0 || looksLikeLoginRedirect(currentURL, definition.loginUrl)) {
if (pageState.loggedOutSignalCount > 0 || definitionLooksLikeLoginRedirect(currentURL, definition)) {
return {
verdict: "expired",
reason: "login_redirect",
@@ -1868,6 +1876,19 @@ export async function probePublishAccountSession(
webContents: window.webContents,
});
if (detected) {
if (account.platform === "zol" && !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",
@@ -1876,7 +1897,9 @@ export async function probePublishAccountSession(
};
}
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle);
const consoleReady = account.platform === "zol"
? await verifyZolConsoleAccess(window, handle.session, account)
: await verifyPublishAccountConsoleAccess(account, handle);
if (consoleReady) {
return {
verdict: "active",
@@ -1944,7 +1967,11 @@ export async function silentRefreshPublishAccountSession(
await sleep(1_200);
await flushSessionPersistence(handle.session);
return !looksLikeLoginRedirect(window.webContents.getURL(), definition.loginUrl);
if (account.platform === "zol") {
return await verifyZolConsoleAccess(window, handle.session, account);
}
return !definitionLooksLikeLoginRedirect(window.webContents.getURL(), definition);
} catch {
return false;
} finally {
@@ -2112,6 +2139,37 @@ async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Sessio
return extractToutiaoAccount(sessionResponse) !== null;
}
function detectedAccountMatchesIdentity(
detected: DetectedAccount,
account: PublishAccountIdentity,
): boolean {
const expected = normalizeAccountPlatformUid(account.platformUid) || account.platformUid;
if (!expected) {
return true;
}
return sameAccountPlatformUid(detected.platformUid, expected);
}
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)(?=$|[.-])/i;
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
if (!currentURL || !loginURL) {
return false;
@@ -2127,8 +2185,8 @@ function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
const currentPath = normalizeURLPath(current.pathname);
const loginPath = normalizeURLPath(login.pathname);
if (!loginPath) {
return /(^|[/?#=&._-])(login|signin|sign-in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i
.test(`${current.pathname}${current.search}${current.hash}`);
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}/`);
@@ -2137,6 +2195,14 @@ function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
}
}
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,
@@ -2176,7 +2242,11 @@ async function verifyPublishAccountConsoleAccess(
return detected !== null;
}
return !looksLikeLoginRedirect(currentURL, definition.loginUrl);
if (account.platform === "zol") {
return await verifyZolConsoleAccess(window, handle.session, account);
}
return !definitionLooksLikeLoginRedirect(currentURL, definition);
} finally {
if (!window.isDestroyed()) {
window.destroy();
@@ -2699,14 +2769,23 @@ async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAcco
};
}
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,
"https://open-api.zol.com.cn/api/v1/creator.user.getinfo",
`${ZOL_API_ORIGIN}/api/v1/creator.user.getinfo`,
{
headers: {
accept: "application/json, text/plain, */*",
},
credentials: "include",
headers: await zolRequestHeaders(session),
},
).catch(() => null);
@@ -2822,8 +2901,9 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
zol: {
id: "zol",
label: "中关村在线",
loginUrl: "https://post.zol.com.cn/v2/manage/works/all",
consoleUrl: "https://post.zol.com.cn/v2/manage/works/all",
loginUrl: ZOL_CONSOLE_URL,
consoleUrl: ZOL_CONSOLE_URL,
loginRedirectUrls: ZOL_LOGIN_REDIRECT_URLS,
detect: detectZol,
},
dongchedi: {