feat(desktop): drop parked review flow, add publish management

SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除
manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询,
desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次
发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based
desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与
媒体库;plan A / spec 文档同步口径。
This commit is contained in:
2026-04-20 09:52:48 +08:00
parent b16e9f0bd1
commit a617d39a4a
93 changed files with 5519 additions and 3594 deletions
+1
View File
@@ -9,6 +9,7 @@
"zip": "wxt zip"
},
"dependencies": {
"@geo/publisher-platforms": "workspace:*",
"@geo/shared-types": "workspace:*",
"js-md5": "^0.8.3",
"marked": "^17.0.5",
+25 -210
View File
@@ -1,3 +1,8 @@
import {
fetchToutiaoMediaSnapshot,
publishToutiaoArticle,
} from "../../../../packages/publisher-platforms/src/toutiao";
import type { StoredPlatformState } from "../platforms";
import {
@@ -9,236 +14,46 @@ import {
normalizeArticleHtml,
uploadHtmlImages,
} from "./common";
import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types";
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;
};
};
async function fetchMediaInfo(): Promise<ToutiaoMediaInfoResponse["data"] | null> {
const response = await fetchJson<ToutiaoMediaInfoResponse>("https://mp.toutiao.com/mp/agw/media/get_media_info", {
headers: {
accept: "application/json, text/plain, */*",
},
}).catch(() => null);
return response?.data ?? null;
}
import type { AdapterContext, PlatformAdapter } from "./types";
async function detectToutiaohao(platform: StoredPlatformState): Promise<StoredPlatformState> {
try {
const data = await fetchMediaInfo();
const user = data?.user;
if (!user?.id_str || !user.screen_name) {
const media = await fetchToutiaoMediaSnapshot(fetchJson);
if (!media?.platformUid || !media.screenName) {
return disconnectedPlatform(platform, "未检测到头条号登录态");
}
return connectedPlatform(platform, user.id_str, user.screen_name, user.https_avatar_url ?? null);
return connectedPlatform(platform, media.platformUid, media.screenName, media.avatarUrl);
} catch (error) {
return disconnectedPlatform(platform, error instanceof Error ? error.message : "头条号登录检测失败");
}
}
async function uploadCover(sourceUrl: string, publishType: "publish" | "draft") {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
if (publishType === "publish") {
const form = new FormData();
form.append("upfile", blob, "cover.png");
const uploaded = await fetchJson<ToutiaoUploadPictureResponse>(
"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 form = new FormData();
form.append("image", blob, "cover.png");
const first = await fetchJson<ToutiaoSpiceImageResponse>("https://mp.toutiao.com/spice/image?device_platform=web", {
method: "POST",
body: form,
}).catch(() => null);
const imageUrl = first?.data?.image_url;
if (!imageUrl) {
return null;
}
const second = new FormData();
second.append("imageUrl", imageUrl);
const final = await fetchJson<ToutiaoSpiceImageResponse>(
"https://mp.toutiao.com/spice/image?device_platform=web&need_cover_url=1",
async function publishToutiaohao(context: AdapterContext) {
const result = await publishToutiaoArticle(
{
method: "POST",
body: second,
title: context.article.title,
html: normalizeArticleHtml(context.article),
coverAssetUrl: context.article.cover_asset_url,
publishType: context.article.publish_type,
},
).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(sourceUrl: string): Promise<string | null> {
const blob = await fetchImageBlob(sourceUrl);
if (!blob) {
return null;
}
const form = new FormData();
form.append("image", blob, "image.png");
const uploaded = await fetchJson<ToutiaoSpiceImageResponse>("https://mp.toutiao.com/spice/image?device_platform=web", {
method: "POST",
body: form,
}).catch(() => null);
return uploaded?.data?.image_url ?? null;
}
async function publishToutiaohao(context: AdapterContext): Promise<PlatformPublishResult> {
const mediaInfo = await fetchMediaInfo();
const user = mediaInfo?.user;
if (!user?.id_str || !user.screen_name) {
throw new Error("toutiaohao_not_logged_in");
}
const content = normalizeArticleHtml(context.article);
const processed = await uploadHtmlImages(content, uploadContentImage);
const cover = context.article.cover_asset_url?.trim()
? await uploadCover(context.article.cover_asset_url.trim(), context.article.publish_type)
: null;
const hasAdPermission = Boolean(
mediaInfo?.media?.has_third_party_ad_permission || mediaInfo?.media?.has_toutiao_ad_permission,
);
const body = new URLSearchParams({
content: processed.html,
title: context.article.title,
mp_editor_stat: JSON.stringify({ code_block: 1 }),
is_refute_rumor: "0",
save: context.article.publish_type === "publish" ? "1" : "0",
timer_status: "0",
draft_form_data: JSON.stringify({ coverType: 1 }),
article_ad_type: 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: {},
}),
});
const response = await fetchJson<ToutiaoPublishResponse>(
"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,
fetchJson,
fetchImageBlob,
uploadHtmlImages,
},
);
const articleId = response.data?.pgc_id != null ? String(response.data.pgc_id) : "";
if (response.code !== 0 || !articleId) {
throw new Error(response.message || "toutiaohao_publish_failed");
}
if (context.article.publish_type === "draft") {
return manageUrlResult(
true,
"pending_review",
articleId,
"https://mp.toutiao.com/profile_v4/graphic/publish",
"头条号草稿保存成功。",
);
if (!result.success) {
throw new Error(result.message);
}
return manageUrlResult(
true,
"success",
articleId,
"https://mp.toutiao.com/profile_v4/index",
"头条号发布成功。",
`https://www.toutiao.com/article/${articleId}/`,
result.status,
result.articleId,
result.externalManageUrl,
result.message,
result.externalArticleUrl,
);
}