feat: Add platform adapters for various content publishing platforms

- Implemented Dongchedi adapter for user detection and publishing.
- Implemented Jianshu adapter for user detection and publishing.
- Implemented Juejin adapter for user detection and publishing.
- Implemented Qiehao adapter for user detection and publishing.
- Implemented Smzdm adapter for user detection and publishing.
- Implemented Sohuhao adapter for user detection and publishing.
- Implemented Toutiaohao adapter for user detection and publishing.
- Implemented Wangyihao adapter for user detection and publishing.
- Implemented Weixin Gzh adapter for user detection and publishing.
- Implemented Zol adapter for user detection and publishing.
- Added documentation for manual testing of media publishing.
This commit is contained in:
2026-04-03 17:48:30 +08:00
parent 32d6a462cd
commit 134dd063c3
33 changed files with 2722 additions and 457 deletions
@@ -0,0 +1,210 @@
import type { StoredPlatformState } from "../platforms";
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;
};
};
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 = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(
content,
async (sourceUrl) => uploadImage(sourceUrl, appId, false),
["baijiahao.baidu.com", "bdstatic.com", "bcebos.com"],
);
const coverUrl = context.article.cover_asset_url?.trim()
? await uploadImage(context.article.cover_asset_url.trim(), appId, true)
: null;
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: coverUrl ? "one" : "",
cover_images: coverUrl
? 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: 1 },
{ 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 = `https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id=${articleId}`;
if (context.article.publish_type === "draft") {
return manageUrlResult(true, "pending_review", articleId, manageUrl, "百家号草稿保存成功。");
}
return manageUrlResult(
true,
"success",
articleId,
manageUrl,
"百家号发布成功,公开链接待补查。",
null,
);
}
export const baijiahaoAdapter: PlatformAdapter = {
platformId: "baijiahao",
detect: detectBaijiahao,
publish: publishBaijiahao,
};