type ToutiaoMediaInfoResponse = { data?: { user?: { id_str?: string; screen_name?: string; https_avatar_url?: string; }; media?: { has_third_party_ad_permission?: boolean; has_toutiao_ad_permission?: boolean; }; }; code?: number; message?: string; }; type ToutiaoUploadPictureResponse = { url?: string; web_uri?: string; origin_web_uri?: string; rigin_web_uri?: string; width?: number; height?: number; }; type ToutiaoSpiceImageResponse = { data?: { image_url?: string; image_uri?: string; image_width?: number; image_height?: number; }; }; type ToutiaoPublishResponse = { code?: number; message?: string; data?: { pgc_id?: string | number; }; }; export interface ToutiaoMediaSnapshot { platformUid: string | null; screenName: string | null; avatarUrl: string | null; hasAdPermission: boolean; } export interface ToutiaoPublishArticleInput { title: string; html: string; coverAssetUrl?: string | null; publishType: "publish" | "draft"; } export interface ToutiaoPublishTransport { fetchJson(input: string, init?: RequestInit): Promise; fetchImageBlob(sourceUrl: string): Promise; uploadHtmlImages( html: string, uploader: (sourceUrl: string) => Promise, ): Promise<{ html: string }>; reportProgress?(stage: "media_info" | "upload_content_images" | "upload_cover" | "submit"): void; } export type ToutiaoPublishResult = | { success: true; status: "success" | "pending_review"; articleId: string; mediaName: string; externalManageUrl: string; externalArticleUrl: string | null; message: string; } | { success: false; status: "failed"; code: "toutiaohao_not_logged_in" | "article_content_empty" | "toutiaohao_publish_failed"; message: string; }; export async function fetchToutiaoMediaSnapshot( fetchJson: (input: string, init?: RequestInit) => Promise, ): Promise { const response = await fetchJson( "https://mp.toutiao.com/mp/agw/media/get_media_info", { headers: { accept: "application/json, text/plain, */*", }, }, ).catch(() => null); const user = response?.data?.user; if (!user?.id_str || !user.screen_name) { return null; } return { platformUid: user.id_str, screenName: user.screen_name, avatarUrl: user.https_avatar_url ?? null, hasAdPermission: Boolean( response?.data?.media?.has_third_party_ad_permission || response?.data?.media?.has_toutiao_ad_permission, ), }; } async function uploadCover( transport: ToutiaoPublishTransport, sourceUrl: string, publishType: "publish" | "draft", ) { const blob = await transport.fetchImageBlob(sourceUrl); if (!blob) { return null; } if (publishType === "publish") { const form = new FormData(); form.append("upfile", blob, "cover.png"); const uploaded = await transport.fetchJson( "https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture", { method: "POST", body: form, }, ).catch(() => null); if (!uploaded?.url || !uploaded.web_uri) { return null; } return { id: 0, url: uploaded.url, uri: uploaded.web_uri, origin_uri: uploaded.origin_web_uri ?? uploaded.rigin_web_uri ?? "", thumb_width: uploaded.width ?? 0, thumb_height: uploaded.height ?? 0, }; } const firstForm = new FormData(); firstForm.append("image", blob, "cover.png"); const first = await transport.fetchJson( "https://mp.toutiao.com/spice/image?device_platform=web", { method: "POST", body: firstForm, }, ).catch(() => null); const imageUrl = first?.data?.image_url; if (!imageUrl) { return null; } const secondForm = new FormData(); secondForm.append("imageUrl", imageUrl); const final = await transport.fetchJson( "https://mp.toutiao.com/spice/image?device_platform=web&need_cover_url=1", { method: "POST", body: secondForm, }, ).catch(() => null); if (!final?.data?.image_url || !first.data?.image_uri) { return null; } return { id: "", url: final.data.image_url, uri: first.data.image_uri, ic_uri: "", thumb_width: first.data.image_width ?? 0, thumb_height: first.data.image_height ?? 0, extra: { from_content_uri: "", from_content: "0", }, }; } async function uploadContentImage( transport: ToutiaoPublishTransport, sourceUrl: string, ): Promise { const blob = await transport.fetchImageBlob(sourceUrl); if (!blob) { return null; } const form = new FormData(); form.append("image", blob, "image.png"); const uploaded = await transport.fetchJson( "https://mp.toutiao.com/spice/image?device_platform=web", { method: "POST", body: form, }, ).catch(() => null); return uploaded?.data?.image_url ?? null; } export async function publishToutiaoArticle( input: ToutiaoPublishArticleInput, transport: ToutiaoPublishTransport, ): Promise { transport.reportProgress?.("media_info"); const media = await fetchToutiaoMediaSnapshot(transport.fetchJson); if (!media?.platformUid || !media.screenName) { return { success: false, status: "failed", code: "toutiaohao_not_logged_in", message: "未检测到头条号登录态", }; } const normalizedHtml = input.html.trim(); if (!normalizedHtml) { return { success: false, status: "failed", code: "article_content_empty", message: "html_content is empty", }; } transport.reportProgress?.("upload_content_images"); const processed = await transport.uploadHtmlImages( normalizedHtml, async (sourceUrl) => uploadContentImage(transport, sourceUrl), ); transport.reportProgress?.("upload_cover"); const cover = input.coverAssetUrl?.trim() ? await uploadCover(transport, input.coverAssetUrl.trim(), input.publishType) : null; const body = new URLSearchParams({ content: processed.html, title: input.title, mp_editor_stat: JSON.stringify({ code_block: 1 }), is_refute_rumor: "0", save: input.publishType === "publish" ? "1" : "0", timer_status: "0", draft_form_data: JSON.stringify({ coverType: 1 }), article_ad_type: media.hasAdPermission ? "3" : "2", pgc_feed_covers: JSON.stringify(cover ? [cover] : []), is_fans_article: "0", govern_forward: "0", praise: "0", disable_praise: "0", tree_plan_article: "0", activity_tag: "0", trends_writing_tag: "0", claim_exclusive: "0", info_source: JSON.stringify({ source_type: 5, source_author_uid: "", time_format: "", position: {}, }), }); transport.reportProgress?.("submit"); const response: ToutiaoPublishResponse = await transport.fetchJson( "https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article&aid=1231", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body, }, ).catch((error) => ({ code: -1, message: error instanceof Error ? error.message : "toutiaohao_publish_failed", })); const articleId = response.data?.pgc_id != null ? String(response.data.pgc_id) : ""; if (response.code !== 0 || !articleId) { return { success: false, status: "failed", code: "toutiaohao_publish_failed", message: response.message || "toutiaohao_publish_failed", }; } if (input.publishType === "draft") { return { success: true, status: "pending_review", articleId, mediaName: media.screenName, externalManageUrl: "https://mp.toutiao.com/profile_v4/graphic/publish", externalArticleUrl: null, message: "头条号草稿保存成功。", }; } return { success: true, status: "success", articleId, mediaName: media.screenName, externalManageUrl: "https://mp.toutiao.com/profile_v4/index", externalArticleUrl: `https://www.toutiao.com/article/${articleId}/`, message: "头条号发布成功。", }; }