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 { return fetchJson("https://www.jianshu.com/author/current_user", { headers: { accept: "application/json", }, }).catch(() => null); } async function detectJianshu(platform: StoredPlatformState): Promise { 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 { const blob = await fetchImageBlob(sourceUrl); if (!blob) { return null; } const uploadToken = await fetchJson( `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 { const user = await fetchCurrentUser(); if (!user?.id || !user.nickname) { throw new Error("jianshu_not_logged_in"); } const notebooks = await fetchJson("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("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, };