Files
geo/apps/browser-extension/src/adapters/jianshu.ts
T
root dbce8515e7 feat(publish): render jianshu html images as markdown
Jianshu's HTML editor strips uploaded <img> tags during save, so previously
uploaded images vanished from the rendered article. After image upload,
convert each <img> (and surrounding <p> wrappers) to a markdown image block
in both the desktop and extension publish paths so the references survive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:02:50 +08:00

225 lines
6.5 KiB
TypeScript

import type { StoredPlatformState } from "../platforms";
import { renderJianshuHtmlImagesAsMarkdown } from "../../../../packages/publisher-platforms/src/jianshu";
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));
const htmlContent = htmlProcessed ? renderJianshuHtmlImagesAsMarkdown(htmlProcessed.html) : "";
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) : htmlContent,
}),
});
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,
};