feat(publish): add seven publish adapters for new media platforms
Wire sohuhao, wangyihao, juejin, smzdm, weixin-gzh, zol, and dongchedi into the publish runtime: each ships its own publish protocol and auth-failure classifier (login/token/challenge codes) plus registry entries in selectPublishAdapter and the auth adapter map. Smzdm bind detection now triple-falls-back across page-context fetch, session fetch, cookie fetch, and a final cookie-derived account so the nickname-less guest profile still resolves to a stable platform UID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,686 @@
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
|
||||
import type { JsonValue } from "@geo/shared-types";
|
||||
|
||||
import {
|
||||
extractImageSources,
|
||||
normalizeArticleHtml,
|
||||
normalizeRemoteUrl,
|
||||
sessionCookieHeader,
|
||||
sessionFetchJson,
|
||||
uploadHtmlImages,
|
||||
} from "./common";
|
||||
import {
|
||||
cropImageAssetBlob,
|
||||
fetchImageAssetBlob,
|
||||
type ImageAssetBlob,
|
||||
} from "./media-image";
|
||||
import type { PublishAdapter, PublishAdapterContext } from "./base";
|
||||
|
||||
type DongchediAccountInfoResponse = {
|
||||
data?: {
|
||||
user_id_str?: string;
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediUploadAuthResponse = {
|
||||
data?: {
|
||||
token?: DongchediImageXToken;
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediImageXToken = {
|
||||
AccessKeyId?: string;
|
||||
SecretAccessKey?: string;
|
||||
SessionToken?: string;
|
||||
CurrentTime?: string;
|
||||
};
|
||||
|
||||
type DongchediImageXApplyResponse = {
|
||||
Result?: {
|
||||
UploadAddress?: {
|
||||
StoreInfos?: Array<{
|
||||
StoreUri?: string;
|
||||
Auth?: string;
|
||||
}>;
|
||||
UploadHosts?: string[];
|
||||
SessionKey?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediImageURLResponse = {
|
||||
data?: {
|
||||
img_url_map?: Record<string, {
|
||||
main_url?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediSpiceImageResponse = {
|
||||
data?: {
|
||||
image_url?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediPublishResponse = {
|
||||
message?: string;
|
||||
data?: {
|
||||
message?: string;
|
||||
data?: {
|
||||
pgc_id?: string | number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DongchediAccount = {
|
||||
uid: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type DongchediUploadedImage = {
|
||||
uri: string;
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const DONGCHEDI_ORIGIN = "https://mp.dcdapp.com";
|
||||
const DONGCHEDI_IMAGE_X_ORIGIN = "https://imagex.bytedanceapi.com";
|
||||
const DONGCHEDI_SERVICE_ID = "f042mdwyw7";
|
||||
const DONGCHEDI_MANAGE_URL = `${DONGCHEDI_ORIGIN}/profile_v2/content/manage/article`;
|
||||
|
||||
function stringId(value: unknown): string {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? String(value) : "";
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function dongchediResponseMessage(response: DongchediPublishResponse | null | undefined): string {
|
||||
return response?.data?.message || response?.message || "dongchedi_publish_failed";
|
||||
}
|
||||
|
||||
function isDongchediChallengeMessage(message: string): boolean {
|
||||
return /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|扫码验证|captcha|challenge|verification required|风险|风控|访问频繁)/i
|
||||
.test(message);
|
||||
}
|
||||
|
||||
async function dongchediHeaders(
|
||||
context: PublishAdapterContext,
|
||||
extra: Record<string, string> = {},
|
||||
): Promise<Record<string, string>> {
|
||||
const cookie = await sessionCookieHeader(context.session, "dongchedi.com").catch(() => "");
|
||||
return {
|
||||
accept: "application/json, text/plain, */*",
|
||||
origin: DONGCHEDI_ORIGIN,
|
||||
referer: DONGCHEDI_ORIGIN,
|
||||
...(cookie ? { cookie } : {}),
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
async function mainFetchJson<T>(input: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(input, init);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text || `request_failed_${response.status}`);
|
||||
}
|
||||
if (!text) {
|
||||
return {} as T;
|
||||
}
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
async function fetchAccount(context: PublishAdapterContext): Promise<DongchediAccount | null> {
|
||||
const url = new URL(`${DONGCHEDI_ORIGIN}/passport/account/info/v2/`);
|
||||
url.searchParams.set("aid", "2302");
|
||||
url.searchParams.set("account_sdk_source", "web");
|
||||
|
||||
const response = await sessionFetchJson<DongchediAccountInfoResponse>(
|
||||
context.session,
|
||||
url.toString(),
|
||||
{
|
||||
credentials: "include",
|
||||
headers: await dongchediHeaders(context),
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
const user = response?.data;
|
||||
return user?.user_id_str && user.name ? { uid: user.user_id_str, name: user.name } : null;
|
||||
}
|
||||
|
||||
function sha256Hex(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function hmacSha256(key: Buffer | string, value: string): Buffer {
|
||||
return createHmac("sha256", key).update(value).digest();
|
||||
}
|
||||
|
||||
function formatAmzDate(date: Date): string {
|
||||
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
||||
}
|
||||
|
||||
function formatDateStamp(date: Date): string {
|
||||
return date.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
}
|
||||
|
||||
function signAWS4(input: {
|
||||
method: string;
|
||||
url: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
securityToken?: string;
|
||||
region?: string;
|
||||
service?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
date?: Date;
|
||||
}): Record<string, string> {
|
||||
const region = input.region ?? "cn-north-1";
|
||||
const service = input.service ?? "imagex";
|
||||
const body = input.body ?? "";
|
||||
const url = new URL(input.url);
|
||||
const now = input.date ?? new Date();
|
||||
const amzDate = formatAmzDate(now);
|
||||
const dateStamp = formatDateStamp(now);
|
||||
const query = [...url.searchParams.entries()]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
const signedHeaders: Record<string, string> = {
|
||||
"x-amz-date": amzDate,
|
||||
...(input.securityToken ? { "x-amz-security-token": input.securityToken } : {}),
|
||||
...(input.headers ?? {}),
|
||||
};
|
||||
const signedHeaderNames = Object.keys(signedHeaders)
|
||||
.map((key) => key.toLowerCase())
|
||||
.sort()
|
||||
.join(";");
|
||||
const canonicalHeaders = Object.entries(signedHeaders)
|
||||
.map(([key, value]) => `${key.toLowerCase()}:${value.trim()}`)
|
||||
.sort()
|
||||
.join("\n") + "\n";
|
||||
const canonicalRequest = [
|
||||
input.method.toUpperCase(),
|
||||
url.pathname || "/",
|
||||
query,
|
||||
canonicalHeaders,
|
||||
signedHeaderNames,
|
||||
sha256Hex(body),
|
||||
].join("\n");
|
||||
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
||||
const stringToSign = [
|
||||
"AWS4-HMAC-SHA256",
|
||||
amzDate,
|
||||
credentialScope,
|
||||
sha256Hex(canonicalRequest),
|
||||
].join("\n");
|
||||
const kDate = hmacSha256(`AWS4${input.secretAccessKey}`, dateStamp);
|
||||
const kRegion = hmacSha256(kDate, region);
|
||||
const kService = hmacSha256(kRegion, service);
|
||||
const kSigning = hmacSha256(kService, "aws4_request");
|
||||
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
||||
|
||||
return {
|
||||
authorization:
|
||||
`AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames}, Signature=${signature}`,
|
||||
"x-amz-date": amzDate,
|
||||
...(input.securityToken ? { "x-amz-security-token": input.securityToken } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function crc32(data: Uint8Array): string {
|
||||
let crc = 0xFFFFFFFF;
|
||||
const table = crc32Table();
|
||||
for (const byte of data) {
|
||||
crc = (crc >>> 8) ^ table[(crc ^ byte) & 0xFF];
|
||||
}
|
||||
return ((crc ^ 0xFFFFFFFF) >>> 0).toString(16).padStart(8, "0");
|
||||
}
|
||||
|
||||
let cachedCrc32Table: Uint32Array | null = null;
|
||||
|
||||
function crc32Table(): Uint32Array {
|
||||
if (cachedCrc32Table) {
|
||||
return cachedCrc32Table;
|
||||
}
|
||||
|
||||
const table = new Uint32Array(256);
|
||||
for (let index = 0; index < 256; index += 1) {
|
||||
let value = index;
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
value = (value & 1) ? (0xEDB88320 ^ (value >>> 1)) : (value >>> 1);
|
||||
}
|
||||
table[index] = value;
|
||||
}
|
||||
cachedCrc32Table = table;
|
||||
return table;
|
||||
}
|
||||
|
||||
async function fetchUploadToken(context: PublishAdapterContext): Promise<Required<DongchediImageXToken>> {
|
||||
const response = await sessionFetchJson<DongchediUploadAuthResponse>(
|
||||
context.session,
|
||||
`${DONGCHEDI_ORIGIN}/motor/car_page/v6/img/get_upload_auth`,
|
||||
{
|
||||
credentials: "include",
|
||||
headers: await dongchediHeaders(context),
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
const token = response?.data?.token;
|
||||
if (!token?.AccessKeyId || !token.SecretAccessKey || !token.SessionToken) {
|
||||
throw new Error("dongchedi_image_token_failed");
|
||||
}
|
||||
|
||||
return {
|
||||
AccessKeyId: token.AccessKeyId,
|
||||
SecretAccessKey: token.SecretAccessKey,
|
||||
SessionToken: token.SessionToken,
|
||||
CurrentTime: token.CurrentTime || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareImage(source: ImageAssetBlob, ratio?: number): Promise<ImageAssetBlob | null> {
|
||||
if (!ratio) {
|
||||
return source;
|
||||
}
|
||||
return await cropImageAssetBlob(source, ratio);
|
||||
}
|
||||
|
||||
async function uploadImage(
|
||||
context: PublishAdapterContext,
|
||||
sourceUrl: string,
|
||||
ratio?: number,
|
||||
): Promise<DongchediUploadedImage | null> {
|
||||
const source = await fetchImageAssetBlob(sourceUrl);
|
||||
if (!source) {
|
||||
throw new Error(ratio ? "dongchedi_cover_fetch_failed" : "dongchedi_image_fetch_failed");
|
||||
}
|
||||
|
||||
const image = await prepareImage(source, ratio);
|
||||
if (!image) {
|
||||
throw new Error(ratio ? "dongchedi_cover_fetch_failed" : "dongchedi_image_fetch_failed");
|
||||
}
|
||||
|
||||
const token = await fetchUploadToken(context);
|
||||
const nonce = Math.random().toString(36).slice(2);
|
||||
const applyUrl = new URL(`${DONGCHEDI_IMAGE_X_ORIGIN}/`);
|
||||
applyUrl.searchParams.set("Action", "ApplyImageUpload");
|
||||
applyUrl.searchParams.set("Version", "2018-08-01");
|
||||
applyUrl.searchParams.set("ServiceId", DONGCHEDI_SERVICE_ID);
|
||||
applyUrl.searchParams.set("s", nonce);
|
||||
const signedApplyHeaders = signAWS4({
|
||||
method: "GET",
|
||||
url: applyUrl.toString(),
|
||||
accessKeyId: token.AccessKeyId,
|
||||
secretAccessKey: token.SecretAccessKey,
|
||||
securityToken: token.SessionToken,
|
||||
date: new Date(token.CurrentTime),
|
||||
});
|
||||
|
||||
const applyResponse = await mainFetchJson<DongchediImageXApplyResponse>(
|
||||
applyUrl.toString(),
|
||||
{
|
||||
headers: signedApplyHeaders,
|
||||
},
|
||||
).catch((error) => {
|
||||
throw new Error(error instanceof Error ? `dongchedi_image_apply_upload_failed:${error.message}` : "dongchedi_image_apply_upload_failed");
|
||||
});
|
||||
|
||||
const uploadAddress = applyResponse.Result?.UploadAddress;
|
||||
const storeInfo = uploadAddress?.StoreInfos?.[0];
|
||||
const uploadHost = uploadAddress?.UploadHosts?.[0];
|
||||
const storeUri = storeInfo?.StoreUri?.trim() || "";
|
||||
if (!storeUri || !storeInfo?.Auth || !uploadHost || !uploadAddress?.SessionKey) {
|
||||
throw new Error("dongchedi_image_apply_upload_failed");
|
||||
}
|
||||
|
||||
const buffer = await image.blob.arrayBuffer();
|
||||
const putResponse = await fetch(`https://${uploadHost}/${storeUri}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-disposition": 'attachment; filename="undefined"',
|
||||
"content-type": "application/octet-stream",
|
||||
"content-crc32": crc32(new Uint8Array(buffer)),
|
||||
authorization: storeInfo.Auth,
|
||||
},
|
||||
body: image.blob,
|
||||
});
|
||||
if (!putResponse.ok) {
|
||||
const text = await putResponse.text().catch(() => "");
|
||||
throw new Error(text || `dongchedi_image_tos_upload_failed_${putResponse.status}`);
|
||||
}
|
||||
|
||||
const commitUrl = new URL(DONGCHEDI_IMAGE_X_ORIGIN);
|
||||
commitUrl.searchParams.set("Action", "CommitImageUpload");
|
||||
commitUrl.searchParams.set("Version", "2018-08-01");
|
||||
commitUrl.searchParams.set("SessionKey", uploadAddress.SessionKey);
|
||||
commitUrl.searchParams.set("ServiceId", DONGCHEDI_SERVICE_ID);
|
||||
const signedCommitHeaders = signAWS4({
|
||||
method: "POST",
|
||||
url: commitUrl.toString(),
|
||||
accessKeyId: token.AccessKeyId,
|
||||
secretAccessKey: token.SecretAccessKey,
|
||||
securityToken: token.SessionToken,
|
||||
});
|
||||
|
||||
await mainFetchJson<Record<string, unknown>>(
|
||||
commitUrl.toString(),
|
||||
{
|
||||
method: "POST",
|
||||
headers: signedCommitHeaders,
|
||||
},
|
||||
).catch((error) => {
|
||||
throw new Error(error instanceof Error ? `dongchedi_image_commit_upload_failed:${error.message}` : "dongchedi_image_commit_upload_failed");
|
||||
});
|
||||
|
||||
const imageUrlResponse = await sessionFetchJson<DongchediImageURLResponse>(
|
||||
context.session,
|
||||
`${DONGCHEDI_ORIGIN}/motor/car_page/v6/img/get_url`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: await dongchediHeaders(context, {
|
||||
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
}),
|
||||
body: new URLSearchParams({
|
||||
img_uris: storeUri,
|
||||
img_url_type: "2",
|
||||
img_param: "noop",
|
||||
img_format: "image",
|
||||
}),
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
const imageUrl = imageUrlResponse?.data?.img_url_map?.[storeUri]?.main_url?.trim() || "";
|
||||
if (!imageUrl) {
|
||||
throw new Error("dongchedi_image_url_failed");
|
||||
}
|
||||
|
||||
return {
|
||||
uri: storeUri,
|
||||
url: imageUrl,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
|
||||
async function spiceImage(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||
const imageUrl = normalizeRemoteUrl(sourceUrl);
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await sessionFetchJson<DongchediSpiceImageResponse>(
|
||||
context.session,
|
||||
`${DONGCHEDI_ORIGIN}/spice/image?sk=dcd`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: await dongchediHeaders(context, {
|
||||
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
}),
|
||||
body: new URLSearchParams({
|
||||
imageUrl,
|
||||
}),
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
return response?.data?.image_url?.trim() || null;
|
||||
}
|
||||
|
||||
async function uploadContentImage(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||
const spiced = await spiceImage(context, sourceUrl);
|
||||
if (spiced) {
|
||||
return spiced;
|
||||
}
|
||||
|
||||
const uploaded = await uploadImage(context, sourceUrl).catch(() => null);
|
||||
return uploaded?.url ?? null;
|
||||
}
|
||||
|
||||
function htmlTextCount(html: string): number {
|
||||
return html
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, "\"")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\s+/g, "")
|
||||
.length;
|
||||
}
|
||||
|
||||
function buildPublishBody(
|
||||
title: string,
|
||||
content: string,
|
||||
verticalCover: DongchediUploadedImage,
|
||||
feedCover: DongchediUploadedImage,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
extra: {
|
||||
article_ad_type: 3,
|
||||
timer_status: 0,
|
||||
timer_time: "",
|
||||
vertical_cover_image: JSON.stringify({
|
||||
uri: verticalCover.uri,
|
||||
height: verticalCover.height,
|
||||
width: verticalCover.width,
|
||||
is_ai_cover: false,
|
||||
}),
|
||||
pgc_feed_covers: [
|
||||
{
|
||||
url: feedCover.url,
|
||||
uri: feedCover.uri,
|
||||
thumb_width: feedCover.width,
|
||||
thumb_height: feedCover.height,
|
||||
},
|
||||
],
|
||||
content_word_cnt: htmlTextCount(content),
|
||||
title_id: "",
|
||||
},
|
||||
save: 1,
|
||||
title,
|
||||
content,
|
||||
source: 20,
|
||||
};
|
||||
}
|
||||
|
||||
function buildResultPayload(articleId: string, mediaName: string): Record<string, JsonValue> {
|
||||
return {
|
||||
platform: "dongchedi",
|
||||
media_name: mediaName,
|
||||
external_article_id: articleId,
|
||||
external_manage_url: DONGCHEDI_MANAGE_URL,
|
||||
external_article_url: `https://www.dongchedi.com/article/${encodeURIComponent(articleId)}`,
|
||||
publish_type: "publish",
|
||||
};
|
||||
}
|
||||
|
||||
function failureResult(error: unknown): ReturnType<PublishAdapter["publish"]> extends Promise<infer T> ? T : never {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (message === "dongchedi_not_logged_in") {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝登录态失效,无法执行发布。",
|
||||
error: {
|
||||
code: "dongchedi_not_logged_in",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message === "publish_cover_required") {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝发布必须上传封面图。",
|
||||
error: {
|
||||
code: "publish_cover_required",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message === "article_content_empty") {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "文章内容为空,无法推送到懂车帝。",
|
||||
error: {
|
||||
code: "article_content_empty",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message === "dongchedi_cover_fetch_failed" || message.startsWith("dongchedi_cover_fetch_failed:")) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝封面文件读取失败,请重新上传封面图后重试。",
|
||||
error: {
|
||||
code: "dongchedi_cover_fetch_failed",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message === "dongchedi_image_fetch_failed" || message.startsWith("dongchedi_image_fetch_failed:")) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝正文图片读取失败,请检查文章图片后重试。",
|
||||
error: {
|
||||
code: "dongchedi_image_fetch_failed",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message.startsWith("dongchedi_cover_upload_failed")) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝封面上传失败,请稍后重试或更换封面图。",
|
||||
error: {
|
||||
code: "dongchedi_cover_upload_failed",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
message.startsWith("dongchedi_image_token_failed")
|
||||
|| message.startsWith("dongchedi_image_apply_upload_failed")
|
||||
|| message.startsWith("dongchedi_image_tos_upload_failed")
|
||||
|| message.startsWith("dongchedi_image_commit_upload_failed")
|
||||
|| message.startsWith("dongchedi_image_url_failed")
|
||||
) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝图片上传失败,请稍后重试或更换图片。",
|
||||
error: {
|
||||
code: "dongchedi_image_upload_failed",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (message.startsWith("dongchedi_challenge_required") || isDongchediChallengeMessage(message)) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝触发平台验证,请在懂车帝后台完成验证后重试。",
|
||||
error: {
|
||||
code: "dongchedi_challenge_required",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "懂车帝发布失败。",
|
||||
error: {
|
||||
code: "dongchedi_publish_failed",
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const dongchediAdapter: PublishAdapter = {
|
||||
platform: "dongchedi",
|
||||
executionMode: "session",
|
||||
async publish(context) {
|
||||
try {
|
||||
context.reportProgress("dongchedi.detect_login");
|
||||
const account = await fetchAccount(context);
|
||||
if (!account) {
|
||||
throw new Error("dongchedi_not_logged_in");
|
||||
}
|
||||
|
||||
const coverAssetUrl = context.article.cover_asset_url?.trim() || "";
|
||||
if (!coverAssetUrl) {
|
||||
throw new Error("publish_cover_required");
|
||||
}
|
||||
|
||||
let html = normalizeArticleHtml(context.article);
|
||||
if (!html) {
|
||||
throw new Error("article_content_empty");
|
||||
}
|
||||
|
||||
if (extractImageSources(html).length === 0) {
|
||||
html = `<img src="${coverAssetUrl}" />${html}`;
|
||||
}
|
||||
|
||||
context.reportProgress("dongchedi.upload_cover");
|
||||
const verticalCover = await uploadImage(context, coverAssetUrl, 3 / 4);
|
||||
const feedCover = await uploadImage(context, coverAssetUrl, 4 / 3);
|
||||
if (!verticalCover || !feedCover) {
|
||||
throw new Error("dongchedi_cover_upload_failed");
|
||||
}
|
||||
|
||||
context.reportProgress("dongchedi.upload_content_images");
|
||||
const processed = await uploadHtmlImages(html, async (sourceUrl) => uploadContentImage(context, sourceUrl));
|
||||
|
||||
context.reportProgress("dongchedi.submit");
|
||||
const title = context.article.title?.trim() || "未命名文章";
|
||||
const response = await sessionFetchJson<DongchediPublishResponse>(
|
||||
context.session,
|
||||
`${DONGCHEDI_ORIGIN}/motor/content_publish/publish_mp_article/v1`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: await dongchediHeaders(context, {
|
||||
"content-type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify(buildPublishBody(title, processed.html, verticalCover, feedCover)),
|
||||
},
|
||||
).catch((error): DongchediPublishResponse => ({
|
||||
message: error instanceof Error ? error.message : "dongchedi_publish_failed",
|
||||
}));
|
||||
|
||||
const articleId = stringId(response.data?.data?.pgc_id);
|
||||
if (!articleId) {
|
||||
const message = dongchediResponseMessage(response);
|
||||
if (isDongchediChallengeMessage(message)) {
|
||||
throw new Error(`dongchedi_challenge_required:${message}`);
|
||||
}
|
||||
throw new Error(message || "dongchedi_publish_failed");
|
||||
}
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
payload: buildResultPayload(articleId, account.name),
|
||||
summary: "懂车帝发布成功。",
|
||||
};
|
||||
} catch (error) {
|
||||
return failureResult(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user