87e329207c
Wires up Baijiahao (百家号) and Jianshu (简书) as first-class desktop publish targets, with risk-control prompts surfaced in the runtime controller and a normalized error message in publish records. Adds external-link buttons in the publish management table, an asset format conversion endpoint for cover image compatibility, and reorders publish-status display priority so failures take precedence over partial successes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
231 lines
6.4 KiB
TypeScript
231 lines
6.4 KiB
TypeScript
import type { StoredPlatformState } from "../platforms";
|
|
import {
|
|
prepareBaijiahaoArticleHtml,
|
|
prepareBaijiahaoMarkdown,
|
|
} from "../../../../packages/publisher-platforms/src/baijiahao";
|
|
|
|
import {
|
|
connectedPlatform,
|
|
disconnectedPlatform,
|
|
fetchImageBlob,
|
|
fetchJson,
|
|
fetchText,
|
|
manageUrlResult,
|
|
normalizeArticleHtml,
|
|
uploadHtmlImages,
|
|
} from "./common";
|
|
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
|
|
|
|
type BaijiahaoAppInfoResponse = {
|
|
data?: {
|
|
user?: {
|
|
userid?: string | number;
|
|
name?: string;
|
|
uname?: string;
|
|
username?: string;
|
|
avatar?: string;
|
|
app_id?: string;
|
|
};
|
|
};
|
|
};
|
|
|
|
type BaijiahaoPublishResponse = {
|
|
errno?: number;
|
|
errmsg?: string;
|
|
ret?: {
|
|
id?: string | number;
|
|
};
|
|
};
|
|
|
|
type BaijiahaoUploadResponse = {
|
|
ret?: {
|
|
https_url?: string;
|
|
};
|
|
};
|
|
|
|
type BaijiahaoCutResponse = {
|
|
data?: {
|
|
new_url?: string;
|
|
};
|
|
};
|
|
|
|
function baijiahaoManageUrl(articleId: string): string {
|
|
return `https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id=${encodeURIComponent(articleId)}`;
|
|
}
|
|
|
|
function baijiahaoPublicUrl(articleId: string): string {
|
|
return `https://baijiahao.baidu.com/s?id=${encodeURIComponent(articleId)}`;
|
|
}
|
|
|
|
async function fetchAppInfo(): Promise<NonNullable<BaijiahaoAppInfoResponse["data"]>["user"] | null> {
|
|
const response = await fetchJson<BaijiahaoAppInfoResponse>("https://baijiahao.baidu.com/builder/app/appinfo", {
|
|
headers: {
|
|
accept: "application/json, text/plain, */*",
|
|
},
|
|
}).catch(() => null);
|
|
|
|
return response?.data?.user ?? null;
|
|
}
|
|
|
|
async function detectBaijiahao(platform: StoredPlatformState): Promise<StoredPlatformState> {
|
|
try {
|
|
const user = await fetchAppInfo();
|
|
const uid = user?.userid != null ? String(user.userid) : "";
|
|
const nickname = user?.name || user?.uname || user?.username || "";
|
|
if (!uid || !nickname) {
|
|
return disconnectedPlatform(platform, "未检测到百家号登录态");
|
|
}
|
|
return connectedPlatform(platform, uid, nickname, user?.avatar ?? null);
|
|
} catch (error) {
|
|
return disconnectedPlatform(platform, error instanceof Error ? error.message : "百家号登录检测失败");
|
|
}
|
|
}
|
|
|
|
async function getPublishToken(): Promise<string> {
|
|
const html = await fetchText("https://baijiahao.baidu.com/builder/rc/edit?type=news", {
|
|
headers: {
|
|
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(sourceUrl: string, appId: string, cropCover: boolean): Promise<string | null> {
|
|
const blob = await fetchImageBlob(sourceUrl);
|
|
if (!blob) {
|
|
return null;
|
|
}
|
|
|
|
const form = new FormData();
|
|
form.append("media", blob, "image.png");
|
|
form.append("org_file_name", "image.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 fetchJson<BaijiahaoUploadResponse>("https://baijiahao.baidu.com/materialui/picture/uploadProxy", {
|
|
method: "POST",
|
|
body: form,
|
|
}).catch(() => null);
|
|
|
|
const uploadedUrl = uploaded?.ret?.https_url ?? null;
|
|
if (!uploadedUrl) {
|
|
return null;
|
|
}
|
|
|
|
if (!cropCover) {
|
|
return uploadedUrl;
|
|
}
|
|
|
|
const cut = await fetchJson<BaijiahaoCutResponse>("https://baijiahao.baidu.com/materialui/picture/auto_cutting", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: new URLSearchParams({
|
|
org_url: uploadedUrl,
|
|
type: "news",
|
|
cutting_type: "cover_image",
|
|
}),
|
|
}).catch(() => null);
|
|
|
|
return cut?.data?.new_url ?? uploadedUrl;
|
|
}
|
|
|
|
async function publishBaijiahao(context: AdapterContext): Promise<PlatformPublishResult> {
|
|
const user = await fetchAppInfo();
|
|
const appId = user?.app_id ?? "";
|
|
if (!user?.userid || !appId) {
|
|
throw new Error("baijiahao_not_logged_in");
|
|
}
|
|
|
|
const token = await getPublishToken();
|
|
if (!token) {
|
|
throw new Error("baijiahao_token_missing");
|
|
}
|
|
|
|
const content = prepareBaijiahaoArticleHtml(
|
|
normalizeArticleHtml(context.article, {
|
|
prepareMarkdown: prepareBaijiahaoMarkdown,
|
|
}),
|
|
);
|
|
const processed = await uploadHtmlImages(
|
|
content,
|
|
async (sourceUrl) => uploadImage(sourceUrl, appId, false),
|
|
);
|
|
|
|
const coverAssetUrl = context.article.cover_asset_url?.trim() || "";
|
|
if (!coverAssetUrl) {
|
|
throw new Error("publish_cover_required");
|
|
}
|
|
|
|
const coverUrl = await uploadImage(coverAssetUrl, appId, true);
|
|
if (!coverUrl) {
|
|
throw new Error("baijiahao_cover_upload_failed");
|
|
}
|
|
|
|
const endpoint =
|
|
context.article.publish_type === "publish"
|
|
? "https://baijiahao.baidu.com/pcui/article/publish"
|
|
: "https://baijiahao.baidu.com/pcui/article/save";
|
|
|
|
const response = await fetchJson<BaijiahaoPublishResponse>(endpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
Token: token,
|
|
},
|
|
body: new URLSearchParams({
|
|
type: "news",
|
|
title: context.article.title,
|
|
content: processed.html,
|
|
abstract_from: "1",
|
|
cover_layout: "one",
|
|
cover_images: JSON.stringify([
|
|
{
|
|
src: coverUrl,
|
|
cropData: {},
|
|
machine_chooseimg: 0,
|
|
isLegal: 0,
|
|
cover_source_tag: "local",
|
|
},
|
|
]),
|
|
activity_list: 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 },
|
|
]),
|
|
}),
|
|
});
|
|
|
|
const articleId = response?.ret?.id != null ? String(response.ret.id) : "";
|
|
if (response.errno !== 0 || !articleId) {
|
|
throw new Error(response.errmsg || "baijiahao_publish_failed");
|
|
}
|
|
|
|
const manageUrl = baijiahaoManageUrl(articleId);
|
|
const articleUrl = baijiahaoPublicUrl(articleId);
|
|
if (context.article.publish_type === "draft") {
|
|
return manageUrlResult(true, "pending_review", articleId, manageUrl, "百家号草稿保存成功。");
|
|
}
|
|
|
|
return manageUrlResult(
|
|
true,
|
|
"success",
|
|
articleId,
|
|
manageUrl,
|
|
"百家号发布成功。",
|
|
articleUrl,
|
|
);
|
|
}
|
|
|
|
export const baijiahaoAdapter: PlatformAdapter = {
|
|
platformId: "baijiahao",
|
|
detect: detectBaijiahao,
|
|
publish: publishBaijiahao,
|
|
};
|