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:
@@ -0,0 +1,222 @@
|
||||
import type { StoredPlatformState } from "../platforms";
|
||||
|
||||
import {
|
||||
articleMarkdown,
|
||||
connectedPlatform,
|
||||
disconnectedPlatform,
|
||||
fetchImageBlob,
|
||||
fetchJson,
|
||||
manageUrlResult,
|
||||
normalizeArticleHtml,
|
||||
randomHex,
|
||||
uploadHtmlImages,
|
||||
uploadMarkdownImages,
|
||||
} from "./common";
|
||||
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
|
||||
|
||||
type JianshuUser = {
|
||||
id?: string | number;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
preferred_note_type?: string;
|
||||
};
|
||||
|
||||
type JianshuNotebook = {
|
||||
id?: string | number;
|
||||
};
|
||||
|
||||
type JianshuNote = {
|
||||
id?: string | number;
|
||||
};
|
||||
|
||||
type JianshuUploadToken = {
|
||||
token?: string;
|
||||
key?: string;
|
||||
};
|
||||
|
||||
type JianshuErrorResponse = {
|
||||
error?: Array<{
|
||||
message?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const JIANSHU_JSON_HEADERS = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
};
|
||||
|
||||
async function fetchCurrentUser(): Promise<JianshuUser | null> {
|
||||
return fetchJson<JianshuUser>("https://www.jianshu.com/author/current_user", {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
async function detectJianshu(platform: StoredPlatformState): Promise<StoredPlatformState> {
|
||||
try {
|
||||
const user = await fetchCurrentUser();
|
||||
const uid = user?.id != null ? String(user.id) : "";
|
||||
if (!uid || !user?.nickname) {
|
||||
return disconnectedPlatform(platform, "未检测到简书登录态");
|
||||
}
|
||||
return connectedPlatform(platform, uid, user.nickname, user.avatar ?? null);
|
||||
} catch (error) {
|
||||
return disconnectedPlatform(platform, error instanceof Error ? error.message : "简书登录检测失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadImage(noteId: string, sourceUrl: string): Promise<string | null> {
|
||||
const blob = await fetchImageBlob(sourceUrl);
|
||||
if (!blob) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadToken = await fetchJson<JianshuUploadToken>(
|
||||
`https://www.jianshu.com/upload_images/token.json?filename=${noteId}.png`,
|
||||
{
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
if (!uploadToken?.token || !uploadToken.key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append("token", uploadToken.token);
|
||||
form.append("key", uploadToken.key);
|
||||
form.append("file", blob, `${noteId}.png`);
|
||||
form.append("x:protocol", "https");
|
||||
|
||||
const uploaded = await fetchJson<{ url?: string }>("https://upload.qiniup.com", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
body: form,
|
||||
}).catch(() => null);
|
||||
|
||||
return uploaded?.url ?? null;
|
||||
}
|
||||
|
||||
async function publishJianshu(context: AdapterContext): Promise<PlatformPublishResult> {
|
||||
const user = await fetchCurrentUser();
|
||||
if (!user?.id || !user.nickname) {
|
||||
throw new Error("jianshu_not_logged_in");
|
||||
}
|
||||
|
||||
const notebooks = await fetchJson<JianshuNotebook[]>("https://www.jianshu.com/author/notebooks", {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const notebookId = notebooks[0]?.id != null ? String(notebooks[0].id) : "";
|
||||
if (!notebookId) {
|
||||
throw new Error("jianshu_notebook_missing");
|
||||
}
|
||||
|
||||
let noteId = "";
|
||||
try {
|
||||
const created = await fetchJson<JianshuNote>("https://www.jianshu.com/author/notes", {
|
||||
method: "POST",
|
||||
headers: JIANSHU_JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
notebook_id: notebookId,
|
||||
title: context.article.title,
|
||||
at_bottom: true,
|
||||
}),
|
||||
});
|
||||
|
||||
noteId = created.id != null ? String(created.id) : "";
|
||||
if (!noteId) {
|
||||
throw new Error("jianshu_note_create_failed");
|
||||
}
|
||||
|
||||
const preferredType = user.preferred_note_type || "plain";
|
||||
const rawMarkdown = articleMarkdown(context.article).trim();
|
||||
const useMarkdownMode = preferredType !== "plain" && Boolean(rawMarkdown);
|
||||
|
||||
const markdownProcessed = useMarkdownMode
|
||||
? await uploadMarkdownImages(rawMarkdown, async (sourceUrl) => uploadImage(noteId, sourceUrl))
|
||||
: null;
|
||||
const htmlProcessed = useMarkdownMode
|
||||
? null
|
||||
: await uploadHtmlImages(normalizeArticleHtml(context.article), async (sourceUrl) => uploadImage(noteId, sourceUrl));
|
||||
|
||||
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}`, {
|
||||
method: "PUT",
|
||||
headers: JIANSHU_JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
id: noteId,
|
||||
autosave_control: 1,
|
||||
title: context.article.title,
|
||||
content: useMarkdownMode ? (markdownProcessed?.markdown ?? rawMarkdown) : (htmlProcessed?.html ?? ""),
|
||||
}),
|
||||
});
|
||||
|
||||
if (context.article.cover_asset_url?.trim()) {
|
||||
const coverUrl = await uploadImage(noteId || randomHex(12), context.article.cover_asset_url.trim());
|
||||
if (coverUrl) {
|
||||
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}/update_cover_images`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
flow_type: 14,
|
||||
abbr: "",
|
||||
cover_image_urls: [coverUrl],
|
||||
}),
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
const manageUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${noteId}`;
|
||||
if (context.article.publish_type === "draft") {
|
||||
return manageUrlResult(true, "pending_review", noteId, manageUrl, "简书草稿保存成功。");
|
||||
}
|
||||
|
||||
const publishResponse = await fetch(`https://www.jianshu.com/author/notes/${noteId}/publicize`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: JIANSHU_JSON_HEADERS,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const publishBody = (await publishResponse.json().catch(() => ({}))) as JianshuErrorResponse;
|
||||
if (!publishResponse.ok || publishBody.error?.[0]?.message) {
|
||||
throw new Error(publishBody.error?.[0]?.message || "jianshu_publish_failed");
|
||||
}
|
||||
|
||||
noteId = "";
|
||||
return manageUrlResult(
|
||||
true,
|
||||
"success",
|
||||
created.id != null ? String(created.id) : "",
|
||||
manageUrl,
|
||||
"简书发布成功。",
|
||||
`https://www.jianshu.com/p/${created.id}`,
|
||||
);
|
||||
} finally {
|
||||
if (noteId) {
|
||||
await fetch(`https://www.jianshu.com/author/notes/${noteId}/soft_destroy`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const jianshuAdapter: PlatformAdapter = {
|
||||
platformId: "jianshu",
|
||||
detect: detectJianshu,
|
||||
publish: publishJianshu,
|
||||
};
|
||||
Reference in New Issue
Block a user