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:
@@ -60,6 +60,7 @@ interface PublishPlatformBindingDefinition {
|
|||||||
label: string;
|
label: string;
|
||||||
loginUrl: string;
|
loginUrl: string;
|
||||||
consoleUrl: string;
|
consoleUrl: string;
|
||||||
|
loginRedirectUrls?: string[];
|
||||||
detect(context: DetectContext): Promise<DetectedAccount | null>;
|
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_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 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_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 = [
|
const BILIBILI_WBI_MIXIN_KEY_ENC_TAB = [
|
||||||
46, 47, 18, 2, 53, 8, 23, 32,
|
46, 47, 18, 2, 53, 8, 23, 32,
|
||||||
15, 50, 10, 31, 58, 3, 45, 35,
|
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 {
|
return {
|
||||||
verdict: "expired",
|
verdict: "expired",
|
||||||
reason: "login_redirect",
|
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 {
|
return {
|
||||||
verdict: "expired",
|
verdict: "expired",
|
||||||
reason: "login_redirect",
|
reason: "login_redirect",
|
||||||
@@ -1868,6 +1876,19 @@ export async function probePublishAccountSession(
|
|||||||
webContents: window.webContents,
|
webContents: window.webContents,
|
||||||
});
|
});
|
||||||
if (detected) {
|
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 {
|
return {
|
||||||
verdict: "active",
|
verdict: "active",
|
||||||
reason: "probe_success",
|
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) {
|
if (consoleReady) {
|
||||||
return {
|
return {
|
||||||
verdict: "active",
|
verdict: "active",
|
||||||
@@ -1944,7 +1967,11 @@ export async function silentRefreshPublishAccountSession(
|
|||||||
|
|
||||||
await sleep(1_200);
|
await sleep(1_200);
|
||||||
await flushSessionPersistence(handle.session);
|
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 {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2112,6 +2139,37 @@ async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Sessio
|
|||||||
return extractToutiaoAccount(sessionResponse) !== null;
|
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 {
|
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
|
||||||
if (!currentURL || !loginURL) {
|
if (!currentURL || !loginURL) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2127,8 +2185,8 @@ function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
|
|||||||
const currentPath = normalizeURLPath(current.pathname);
|
const currentPath = normalizeURLPath(current.pathname);
|
||||||
const loginPath = normalizeURLPath(login.pathname);
|
const loginPath = normalizeURLPath(login.pathname);
|
||||||
if (!loginPath) {
|
if (!loginPath) {
|
||||||
return /(^|[/?#=&._-])(login|signin|sign-in|signup|sign-up|register|auth|oauth|passport|scan|qrcode|qrlogin|wxlogin|sso)(?=$|[/?#=&._-])/i
|
return LOGIN_REDIRECT_TOKEN_RE.test(`${current.pathname}${current.search}${current.hash}`)
|
||||||
.test(`${current.pathname}${current.search}${current.hash}`);
|
|| LOGIN_REDIRECT_HOST_RE.test(current.hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`);
|
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(
|
async function verifyPublishAccountConsoleAccess(
|
||||||
account: PublishAccountIdentity,
|
account: PublishAccountIdentity,
|
||||||
handle: SessionHandle,
|
handle: SessionHandle,
|
||||||
@@ -2176,7 +2242,11 @@ async function verifyPublishAccountConsoleAccess(
|
|||||||
return detected !== null;
|
return detected !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !looksLikeLoginRedirect(currentURL, definition.loginUrl);
|
if (account.platform === "zol") {
|
||||||
|
return await verifyZolConsoleAccess(window, handle.session, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !definitionLooksLikeLoginRedirect(currentURL, definition);
|
||||||
} finally {
|
} finally {
|
||||||
if (!window.isDestroyed()) {
|
if (!window.isDestroyed()) {
|
||||||
window.destroy();
|
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> {
|
async function detectZol({ session }: DetectContext): Promise<DetectedAccount | null> {
|
||||||
const response = await sessionFetchJson<ZolUserInfoResponse>(
|
const response = await sessionFetchJson<ZolUserInfoResponse>(
|
||||||
session,
|
session,
|
||||||
"https://open-api.zol.com.cn/api/v1/creator.user.getinfo",
|
`${ZOL_API_ORIGIN}/api/v1/creator.user.getinfo`,
|
||||||
{
|
{
|
||||||
headers: {
|
credentials: "include",
|
||||||
accept: "application/json, text/plain, */*",
|
headers: await zolRequestHeaders(session),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
|
||||||
@@ -2822,8 +2901,9 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
|
|||||||
zol: {
|
zol: {
|
||||||
id: "zol",
|
id: "zol",
|
||||||
label: "中关村在线",
|
label: "中关村在线",
|
||||||
loginUrl: "https://post.zol.com.cn/v2/manage/works/all",
|
loginUrl: ZOL_CONSOLE_URL,
|
||||||
consoleUrl: "https://post.zol.com.cn/v2/manage/works/all",
|
consoleUrl: ZOL_CONSOLE_URL,
|
||||||
|
loginRedirectUrls: ZOL_LOGIN_REDIRECT_URLS,
|
||||||
detect: detectZol,
|
detect: detectZol,
|
||||||
},
|
},
|
||||||
dongchedi: {
|
dongchedi: {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("electron", () => ({
|
||||||
|
nativeImage: {
|
||||||
|
createFromBuffer: () => ({
|
||||||
|
isEmpty: () => false,
|
||||||
|
getSize: () => ({ width: 1, height: 1 }),
|
||||||
|
crop: () => ({
|
||||||
|
isEmpty: () => false,
|
||||||
|
getSize: () => ({ width: 1, height: 1 }),
|
||||||
|
toPNG: () => Buffer.from([]),
|
||||||
|
}),
|
||||||
|
toPNG: () => Buffer.from([]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../transport/api-client", () => ({
|
||||||
|
fetchDesktopApiURL: vi.fn(),
|
||||||
|
resolveDesktopApiURL: (path: string) => `http://127.0.0.1:8080${path}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
directNetworkPicUrl,
|
||||||
|
normalizeZolUploadedImageUrl,
|
||||||
|
} from "./zol";
|
||||||
|
|
||||||
|
describe("zol image helpers", () => {
|
||||||
|
it("does not send local asset URLs to ZOL networkPic upload", () => {
|
||||||
|
expect(directNetworkPicUrl("/api/public/assets/12")).toBeNull();
|
||||||
|
expect(directNetworkPicUrl("http://127.0.0.1:8080/api/public/assets/12")).toBeNull();
|
||||||
|
expect(directNetworkPicUrl("data:image/png;base64,abc")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps public remote image URLs eligible for networkPic upload", () => {
|
||||||
|
expect(directNetworkPicUrl("//cdn.example.com/a.png")).toBe("https://cdn.example.com/a.png");
|
||||||
|
expect(directNetworkPicUrl("https://img.example.com/a.png")).toBe("https://img.example.com/a.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes uploaded ZOL image URLs for editor content", () => {
|
||||||
|
expect(normalizeZolUploadedImageUrl("//icon.zol-img.com.cn/a.png")).toBe("https://icon.zol-img.com.cn/a.png");
|
||||||
|
expect(normalizeZolUploadedImageUrl("/upload/a.png")).toBe("https://post.zol.com.cn/upload/a.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
normalizeRemoteUrl,
|
normalizeRemoteUrl,
|
||||||
sessionCookieHeader,
|
sessionCookieHeader,
|
||||||
sessionFetchJson,
|
sessionFetchJson,
|
||||||
uploadHtmlImages,
|
|
||||||
} from "./common";
|
} from "./common";
|
||||||
import {
|
import {
|
||||||
cropImageAssetBlob,
|
cropImageAssetBlob,
|
||||||
@@ -72,6 +71,7 @@ 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";
|
||||||
const ZOL_MANAGE_URL = "https://post.zol.com.cn/v2/manage/works/all";
|
const ZOL_MANAGE_URL = "https://post.zol.com.cn/v2/manage/works/all";
|
||||||
const ZOL_SUBJECT_LIMIT = 5;
|
const ZOL_SUBJECT_LIMIT = 5;
|
||||||
|
const LOCAL_IMAGE_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0", "api"]);
|
||||||
|
|
||||||
function stringId(value: unknown): string {
|
function stringId(value: unknown): string {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
@@ -92,6 +92,57 @@ function isZolChallengeMessage(message: string): boolean {
|
|||||||
.test(message);
|
.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeZolUploadedImageUrl(value: string | null | undefined): string | null {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (/^\/\//.test(trimmed)) {
|
||||||
|
return `https:${trimmed}`;
|
||||||
|
}
|
||||||
|
if (/^\//.test(trimmed)) {
|
||||||
|
return new URL(trimmed, ZOL_REFERER).toString();
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zolUploadedImageUrl(response: ZolImageUploadResponse | null | undefined): string | null {
|
||||||
|
return normalizeZolUploadedImageUrl(response?.data?.pic) ?? normalizeZolUploadedImageUrl(response?.data?.fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zolImageUploadMessage(response: ZolImageUploadResponse | null | undefined): string {
|
||||||
|
if (!response) {
|
||||||
|
return "empty_response";
|
||||||
|
}
|
||||||
|
return zolResponseMessage(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocalImageHost(hostname: string): boolean {
|
||||||
|
return LOCAL_IMAGE_HOSTS.has(hostname.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directNetworkPicUrl(sourceUrl: string): string | null {
|
||||||
|
const trimmed = sourceUrl.trim();
|
||||||
|
if (!trimmed || /^(data|blob):/i.test(trimmed) || /^\/(?!\/)/.test(trimmed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeRemoteUrl(trimmed);
|
||||||
|
if (!normalized || /^(data|blob):/i.test(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(normalized);
|
||||||
|
if (!["http:", "https:"].includes(parsed.protocol) || isLocalImageHost(parsed.hostname)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function zolHeaders(
|
async function zolHeaders(
|
||||||
context: PublishAdapterContext,
|
context: PublishAdapterContext,
|
||||||
extra: Record<string, string> = {},
|
extra: Record<string, string> = {},
|
||||||
@@ -231,7 +282,21 @@ async function uploadCover(context: PublishAdapterContext, sourceUrl: string): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadContentImage(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
async function uploadContentImage(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||||
const networkPic = normalizeRemoteUrl(sourceUrl);
|
const networkPic = directNetworkPicUrl(sourceUrl);
|
||||||
|
if (networkPic) {
|
||||||
|
const uploaded = await uploadContentImageByNetworkPic(context, networkPic);
|
||||||
|
if (uploaded) {
|
||||||
|
return uploaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await uploadContentImageByFile(context, sourceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadContentImageByNetworkPic(
|
||||||
|
context: PublishAdapterContext,
|
||||||
|
networkPic: string,
|
||||||
|
): Promise<string | null> {
|
||||||
if (!networkPic) {
|
if (!networkPic) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -251,7 +316,80 @@ async function uploadContentImage(context: PublishAdapterContext, sourceUrl: str
|
|||||||
},
|
},
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
|
||||||
return response?.errcode === 0 ? response.data?.pic?.trim() || null : null;
|
const uploaded = zolUploadedImageUrl(response);
|
||||||
|
if (response && response.errcode !== 0 && response.errcode != null) {
|
||||||
|
const message = zolResponseMessage(response);
|
||||||
|
if (isZolChallengeMessage(message)) {
|
||||||
|
throw new Error(`zol_challenge_required:${message}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadContentImageByFile(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||||
|
const image = await fetchImageAssetBlob(sourceUrl);
|
||||||
|
if (!image) {
|
||||||
|
throw new Error(`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:source_fetch_failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (const siteType of ["1", "0"]) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", image.blob, image.fileName);
|
||||||
|
form.append("siteType", siteType);
|
||||||
|
|
||||||
|
const response = await sessionFetchJson<ZolImageUploadResponse>(
|
||||||
|
context.session,
|
||||||
|
`${ZOL_API_ORIGIN}/api/v1/creator.content.image.upload`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: await zolHeaders(context),
|
||||||
|
body: form,
|
||||||
|
},
|
||||||
|
).catch((error): ZolImageUploadResponse => ({
|
||||||
|
errcode: -1,
|
||||||
|
errmsg: error instanceof Error ? error.message : "zol_content_image_upload_failed",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const uploaded = zolUploadedImageUrl(response);
|
||||||
|
if (uploaded) {
|
||||||
|
return uploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = zolImageUploadMessage(response);
|
||||||
|
if (isZolChallengeMessage(message)) {
|
||||||
|
throw new Error(`zol_challenge_required:${message}`);
|
||||||
|
}
|
||||||
|
errors.push(`siteType=${siteType}:${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:${errors.join(";")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadContentImages(context: PublishAdapterContext, html: string): Promise<string> {
|
||||||
|
const sources = extractImageSources(html);
|
||||||
|
if (!sources.length) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploaded = new Map<string, string>();
|
||||||
|
for (const source of sources) {
|
||||||
|
const target = await uploadContentImage(context, source);
|
||||||
|
if (!target) {
|
||||||
|
throw new Error(`zol_content_image_upload_failed:${source.slice(0, 160)}`);
|
||||||
|
}
|
||||||
|
uploaded.set(source, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = html;
|
||||||
|
for (const [source, target] of uploaded.entries()) {
|
||||||
|
next = next.split(source).join(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyRecommendSubjects(context: PublishAdapterContext, form: FormData): Promise<void> {
|
async function applyRecommendSubjects(context: PublishAdapterContext, form: FormData): Promise<void> {
|
||||||
@@ -349,6 +487,17 @@ function failureResult(error: unknown): ReturnType<PublishAdapter["publish"]> ex
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.startsWith("zol_content_image_upload_failed")) {
|
||||||
|
return {
|
||||||
|
status: "failed",
|
||||||
|
summary: "中关村在线正文图片上传失败,已停止发布以避免生成空白图片。",
|
||||||
|
error: {
|
||||||
|
code: "zol_content_image_upload_failed",
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (message.startsWith("zol_challenge_required") || isZolChallengeMessage(message)) {
|
if (message.startsWith("zol_challenge_required") || isZolChallengeMessage(message)) {
|
||||||
return {
|
return {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
@@ -409,11 +558,11 @@ export const zolAdapter: PublishAdapter = {
|
|||||||
await applyRecommendSubjects(context, form);
|
await applyRecommendSubjects(context, form);
|
||||||
|
|
||||||
context.reportProgress("zol.upload_content_images");
|
context.reportProgress("zol.upload_content_images");
|
||||||
const processed = await uploadHtmlImages(html, async (sourceUrl) => uploadContentImage(context, sourceUrl));
|
const processedHtml = await uploadContentImages(context, html);
|
||||||
|
|
||||||
form.set("draftId", draftId);
|
form.set("draftId", draftId);
|
||||||
form.set("saveType", "1");
|
form.set("saveType", "1");
|
||||||
form.set("scontent", processed.html);
|
form.set("scontent", processedHtml);
|
||||||
form.append("draftUpdateId", draftId);
|
form.append("draftUpdateId", draftId);
|
||||||
form.append("taskType", "1");
|
form.append("taskType", "1");
|
||||||
form.append("taskIds", "[]");
|
form.append("taskIds", "[]");
|
||||||
|
|||||||
Reference in New Issue
Block a user