import { Buffer } from "node:buffer"; import type { JsonValue } from "@geo/shared-types"; import { prepareBaijiahaoArticleHtml, prepareBaijiahaoMarkdown, } from "../../../../../packages/publisher-platforms/src/baijiahao"; import { nativeImage } from "electron"; import { normalizeArticleHtml, sessionFetchJson, sessionFetchText, uploadHtmlImages, } from "./common"; import type { PublishAdapter, PublishAdapterContext } from "./base"; import { resolveDesktopApiURL } from "../transport/api-client"; type BaijiahaoAppInfoResponse = { data?: { user?: { userid?: string | number; name?: string; uname?: string; username?: string; avatar?: string; app_id?: string | number; }; }; }; type BaijiahaoPublishResponse = { errno?: number; errmsg?: string; ret?: { id?: string | number; }; }; type BaijiahaoUploadResponse = { errno?: number; errmsg?: string; err_msg?: string; ret?: { https_url?: string; url?: string; }; }; type BaijiahaoCutResponse = { data?: { new_url?: string; }; }; type BaijiahaoUser = NonNullable["user"]>; type BaijiahaoImageBlob = { blob: Blob; fileName: string; }; const BAIJIAHAO_SUPPORTED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]); const BAIJIAHAO_ORIGIN = "https://baijiahao.baidu.com"; const BAIJIAHAO_EDIT_URL = `${BAIJIAHAO_ORIGIN}/builder/rc/edit?type=news`; const BAIJIAHAO_STEP_DELAY_MS = 1400; const BAIJIAHAO_RISK_CONTROL_PROMPT = "您触发平台风控,请手动发稿一篇并完成验证,方可继续使用"; function normalizeUserId(value: unknown): string { if (value === null || value === undefined) { return ""; } return String(value).trim(); } function displayNameFromUser(user: BaijiahaoUser): string { return user.name?.trim() || user.uname?.trim() || user.username?.trim() || ""; } function baijiahaoHeaders(headers?: HeadersInit): Headers { const next = new Headers(headers); if (!next.has("origin")) { next.set("origin", BAIJIAHAO_ORIGIN); } if (!next.has("referer")) { next.set("referer", BAIJIAHAO_EDIT_URL); } return next; } function waitForHumanPace(signal?: AbortSignal): Promise { if (signal?.aborted) { return Promise.reject(new Error("adapter_aborted")); } return new Promise((resolve, reject) => { const cleanup = () => { signal?.removeEventListener("abort", onAbort); }; const timeout = setTimeout(() => { cleanup(); resolve(); }, BAIJIAHAO_STEP_DELAY_MS); const onAbort = () => { clearTimeout(timeout); cleanup(); reject(new Error("adapter_aborted")); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } function baijiahaoResponseMessage(response: { errno?: number; errmsg?: string; err_msg?: string; message?: string; } | null | undefined): string { return response?.errmsg || response?.err_msg || response?.message || `errno_${response?.errno ?? "unknown"}`; } function isBaijiahaoChallengeMessage(message: string): boolean { return /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|扫码验证|captcha|challenge|verification required)/i.test(message); } async function fetchAppInfo(context: PublishAdapterContext): Promise { const response = await sessionFetchJson( context.session, "https://baijiahao.baidu.com/builder/app/appinfo", { credentials: "include", headers: baijiahaoHeaders({ accept: "application/json, text/plain, */*", }), }, ).catch(() => null); return response?.data?.user ?? null; } async function getPublishToken(context: PublishAdapterContext): Promise { const html = await sessionFetchText( context.session, BAIJIAHAO_EDIT_URL, { credentials: "include", headers: baijiahaoHeaders({ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", }), }, ); return /window\.__BJH__INIT__AUTH__\s*=\s*(["'])(.*?)\1/.exec(html)?.[2] ?? ""; } async function uploadImage( context: PublishAdapterContext, sourceUrl: string, appId: string, cropCover: boolean, ): Promise { const image = await fetchBaijiahaoImageBlob(sourceUrl); if (!image) { throw new Error(cropCover ? "baijiahao_cover_fetch_failed" : "baijiahao_image_fetch_failed"); } const form = new FormData(); form.append("media", image.blob, "cover.png"); form.append("org_file_name", "cover.png"); form.append("type", "image"); form.append("app_id", appId); form.append("is_waterlog", "0"); form.append("save_material", "1"); form.append("no_compress", "0"); form.append("article_type", "news"); const uploaded = await sessionFetchJson( context.session, `${BAIJIAHAO_ORIGIN}/materialui/picture/uploadProxy`, { method: "POST", credentials: "include", headers: baijiahaoHeaders(), body: form, }, ).catch((error) => { throw new Error(error instanceof Error ? `baijiahao_upload_proxy_request_failed:${error.message}` : "baijiahao_upload_proxy_request_failed"); }); const uploadedUrl = uploaded?.ret?.https_url ?? uploaded?.ret?.url ?? null; if (!uploadedUrl) { const message = baijiahaoResponseMessage(uploaded); if (isBaijiahaoChallengeMessage(message)) { throw new Error(`baijiahao_challenge_required:${message}`); } throw new Error(`baijiahao_upload_proxy_failed:${message}`); } if (!cropCover) { return uploadedUrl; } const cut = await sessionFetchJson( context.session, `${BAIJIAHAO_ORIGIN}/materialui/picture/auto_cutting`, { method: "POST", credentials: "include", headers: baijiahaoHeaders({ "content-type": "application/x-www-form-urlencoded", }), body: new URLSearchParams({ org_url: uploadedUrl, type: "news", cutting_type: "cover_image", }), }, ).catch((error) => { throw new Error(error instanceof Error ? `baijiahao_cover_cut_failed:${error.message}` : "baijiahao_cover_cut_failed"); }); return cut?.data?.new_url ?? uploadedUrl; } function buildImageURLCandidates(sourceUrl: string): string[] { const trimmed = sourceUrl.trim(); if (!trimmed) { return []; } if (/^(data|blob):/i.test(trimmed)) { return [trimmed]; } const candidates: string[] = []; const pushCandidate = (value: string) => { if (!candidates.includes(value)) { candidates.push(value); } }; try { const parsed = new URL(trimmed, resolveDesktopApiURL("/")); if (parsed.pathname.startsWith("/api/")) { const apiAssetUrl = new URL(resolveDesktopApiURL(`${parsed.pathname}${parsed.search}`)); if (apiAssetUrl.pathname.startsWith("/api/public/assets/")) { apiAssetUrl.searchParams.set("format", "png"); } pushCandidate(apiAssetUrl.toString()); } pushCandidate(parsed.toString()); } catch { pushCandidate(trimmed); } return candidates; } async function fetchBaijiahaoImageBlob(sourceUrl: string): Promise { for (const candidate of buildImageURLCandidates(sourceUrl)) { const response = await fetch(candidate).catch(() => null); if (!response?.ok) { continue; } const sourceBlob = await response.blob().catch(() => null); if (!sourceBlob || !sourceBlob.type.startsWith("image/")) { continue; } return normalizeBaijiahaoImageBlob(sourceBlob); } return null; } async function normalizeBaijiahaoImageBlob(sourceBlob: Blob): Promise { const sourceType = sourceBlob.type.toLowerCase(); if (BAIJIAHAO_SUPPORTED_IMAGE_TYPES.has(sourceType)) { return { blob: sourceBlob, fileName: sourceType === "image/gif" ? "image.gif" : sourceType === "image/jpeg" || sourceType === "image/jpg" ? "image.jpg" : "image.png", }; } const buffer = Buffer.from(await sourceBlob.arrayBuffer()); const image = nativeImage.createFromBuffer(buffer); if (image.isEmpty()) { return null; } const png = image.toPNG(); return { blob: new Blob([new Uint8Array(png)], { type: "image/png" }), fileName: "image.png", }; } async function processContentImages( context: PublishAdapterContext, html: string, appId: string, ): Promise { const processed = await uploadHtmlImages(html, async (sourceUrl) => { await waitForHumanPace(context.signal); return await uploadImage(context, sourceUrl, appId, false); }); return processed.html; } function buildActivityList(): string { return JSON.stringify([ { id: "ttv", is_checked: 0 }, { id: "ai_tts", is_checked: 0 }, { id: "aigc_bjh_status", is_checked: 0 }, { id: "reward", is_checked: 0 }, ]); } function buildCoverImages(coverUrl: string): string { return JSON.stringify([ { src: coverUrl, cropData: {}, machine_chooseimg: 0, isLegal: 0, cover_source_tag: "local", }, ]); } function baijiahaoManageUrl(articleId: string): string { return `${BAIJIAHAO_EDIT_URL}&article_id=${encodeURIComponent(articleId)}`; } function baijiahaoPublicUrl(articleId: string): string { return `${BAIJIAHAO_ORIGIN}/s?id=${encodeURIComponent(articleId)}`; } function buildResultPayload( articleId: string, mediaName: string, externalManageUrl: string, externalArticleUrl: string, ): Record { return { platform: "baijiahao", media_name: mediaName, external_article_id: articleId, external_manage_url: externalManageUrl, external_article_url: externalArticleUrl, publish_type: "publish", }; } function failureResult(error: unknown): ReturnType extends Promise ? T : never { const message = error instanceof Error ? error.message : String(error); if (message === "baijiahao_not_logged_in") { return { status: "failed", summary: "百家号登录态失效,无法执行发布。", error: { code: "baijiahao_not_logged_in", message, }, }; } if (message === "baijiahao_token_missing") { return { status: "failed", summary: "百家号发布 Token 获取失败,请重新打开百家号后台确认登录态。", error: { code: "baijiahao_token_missing", message, }, }; } if (message === "publish_cover_required") { return { status: "failed", summary: "百家号发布必须上传封面图。", error: { code: "publish_cover_required", message, }, }; } if (message === "baijiahao_cover_upload_failed") { return { status: "failed", summary: "百家号封面上传失败,请更换封面图后重试。", error: { code: "baijiahao_cover_upload_failed", message, }, }; } if (message === "baijiahao_cover_fetch_failed" || message.startsWith("baijiahao_cover_fetch_failed:")) { return { status: "failed", summary: "百家号封面文件读取失败,请重新上传封面图后重试。", error: { code: "baijiahao_cover_fetch_failed", message, }, }; } if (message.startsWith("baijiahao_upload_proxy_failed") || message.startsWith("baijiahao_upload_proxy_request_failed")) { return { status: "failed", summary: "百家号素材上传接口拒绝了封面图。", error: { code: "baijiahao_upload_proxy_failed", message, }, }; } if (message.startsWith("baijiahao_cover_cut_failed")) { return { status: "failed", summary: "百家号封面裁剪接口处理失败。", error: { code: "baijiahao_cover_cut_failed", message, }, }; } if (message.startsWith("baijiahao_challenge_required")) { return { status: "failed", summary: BAIJIAHAO_RISK_CONTROL_PROMPT, error: { code: "baijiahao_challenge_required", message, }, }; } if (message === "article_content_empty") { return { status: "failed", summary: "文章内容为空,无法推送到百家号。", error: { code: "article_content_empty", message, }, }; } return { status: "failed", summary: "百家号发布失败。", error: { code: "baijiahao_publish_failed", message, }, }; } export const baijiahaoAdapter: PublishAdapter = { platform: "baijiahao", executionMode: "session", async publish(context) { try { context.reportProgress("baijiahao.detect_login"); const user = await fetchAppInfo(context); const platformUid = normalizeUserId(user?.userid); const mediaName = user ? displayNameFromUser(user) : ""; const appId = normalizeUserId(user?.app_id); if (!platformUid || !mediaName || !appId) { throw new Error("baijiahao_not_logged_in"); } context.reportProgress("baijiahao.get_token"); await waitForHumanPace(context.signal); const token = await getPublishToken(context); if (!token) { throw new Error("baijiahao_token_missing"); } const coverAssetUrl = context.article.cover_asset_url?.trim() || ""; if (!coverAssetUrl) { throw new Error("publish_cover_required"); } const html = prepareBaijiahaoArticleHtml( normalizeArticleHtml(context.article, { prepareMarkdown: prepareBaijiahaoMarkdown, }), ); if (!html) { throw new Error("article_content_empty"); } context.reportProgress("baijiahao.upload_content_images"); await waitForHumanPace(context.signal); const content = await processContentImages(context, html, appId); context.reportProgress("baijiahao.upload_cover"); await waitForHumanPace(context.signal); const coverUrl = await uploadImage(context, coverAssetUrl, appId, true); if (!coverUrl) { throw new Error("baijiahao_cover_upload_failed"); } context.reportProgress("baijiahao.submit"); await waitForHumanPace(context.signal); const response: BaijiahaoPublishResponse = await sessionFetchJson( context.session, `${BAIJIAHAO_ORIGIN}/pcui/article/publish`, { method: "POST", credentials: "include", headers: baijiahaoHeaders({ "content-type": "application/x-www-form-urlencoded", Token: token, }), body: new URLSearchParams({ type: "news", title: context.article.title?.trim() || "未命名文章", content, abstract_from: "1", cover_layout: "one", cover_images: buildCoverImages(coverUrl), activity_list: buildActivityList(), }), }, ).catch((requestError): BaijiahaoPublishResponse => ({ errno: -1, errmsg: requestError instanceof Error ? requestError.message : "baijiahao_publish_failed", })); const articleId = response.ret?.id != null ? String(response.ret.id) : ""; if (response.errno !== 0 || !articleId) { const message = baijiahaoResponseMessage(response); if (isBaijiahaoChallengeMessage(message)) { throw new Error(`baijiahao_challenge_required:${message}`); } throw new Error(message || "baijiahao_publish_failed"); } const manageUrl = baijiahaoManageUrl(articleId); const articleUrl = baijiahaoPublicUrl(articleId); return { status: "succeeded", payload: buildResultPayload(articleId, mediaName, manageUrl, articleUrl), summary: "百家号发布成功。", }; } catch (error) { return failureResult(error); } }, };