fix(publish/dongchedi): route imagex traffic through session.fetch

Use the account session for imagex apply/upload/commit so the requests carry
the same cookies/UA the bind console saw, instead of falling out of the
session via a bare Node fetch — bytedance imagex was rejecting these as
unauthenticated under the bound session. Also align the binding console URL
with the v2 publish article path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 21:34:15 +08:00
parent 412befef53
commit 1ef7b872e3
2 changed files with 44 additions and 13 deletions
@@ -2910,7 +2910,7 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
id: "dongchedi",
label: "懂车帝",
loginUrl: "https://mp.dcdapp.com/login",
consoleUrl: "https://mp.dcdapp.com/content/article/create",
consoleUrl: "https://mp.dcdapp.com/profile_v2/publish/article",
detect: detectDongchedi,
},
};
@@ -2,6 +2,7 @@ import { createHash, createHmac } from "node:crypto";
import type { JsonValue } from "@geo/shared-types";
import { STANDARD_ACCEPT_LANGUAGES, STANDARD_USER_AGENT } from "../user-agent";
import {
extractImageSources,
normalizeArticleHtml,
@@ -91,6 +92,8 @@ 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`;
const IMAGEX_USER_AGENT = STANDARD_USER_AGENT;
const IMAGEX_ACCEPT_LANGUAGE = STANDARD_ACCEPT_LANGUAGES;
function stringId(value: unknown): string {
if (typeof value === "number") {
@@ -125,12 +128,35 @@ async function dongchediHeaders(
};
}
async function mainFetchJson<T>(input: string, init?: RequestInit): Promise<T> {
const response = await fetch(input, init);
async function imagexFetchText(
context: PublishAdapterContext,
input: string,
init?: RequestInit,
): Promise<string> {
const headers = new Headers(init?.headers);
if (!headers.has("user-agent")) {
headers.set("user-agent", IMAGEX_USER_AGENT);
}
if (!headers.has("accept-language")) {
headers.set("accept-language", IMAGEX_ACCEPT_LANGUAGE);
}
if (!headers.has("accept")) {
headers.set("accept", "application/json, text/plain, */*");
}
const response = await context.session.fetch(input, { ...init, headers });
const text = await response.text();
if (!response.ok) {
throw new Error(text || `request_failed_${response.status}`);
}
return text;
}
async function imagexFetchJson<T>(
context: PublishAdapterContext,
input: string,
init?: RequestInit,
): Promise<T> {
const text = await imagexFetchText(context, input, init);
if (!text) {
return {} as T;
}
@@ -191,7 +217,7 @@ function signAWS4(input: {
const amzDate = formatAmzDate(now);
const dateStamp = formatDateStamp(now);
const query = [...url.searchParams.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
const signedHeaders: Record<string, string> = {
@@ -325,7 +351,8 @@ async function uploadImage(
date: new Date(token.CurrentTime),
});
const applyResponse = await mainFetchJson<DongchediImageXApplyResponse>(
const applyResponse = await imagexFetchJson<DongchediImageXApplyResponse>(
context,
applyUrl.toString(),
{
headers: signedApplyHeaders,
@@ -343,14 +370,17 @@ async function uploadImage(
}
const buffer = await image.blob.arrayBuffer();
const putResponse = await fetch(`https://${uploadHost}/${storeUri}`, {
const putHeaders = new Headers({
"content-disposition": 'attachment; filename="undefined"',
"content-type": "application/octet-stream",
"content-crc32": crc32(new Uint8Array(buffer)),
authorization: storeInfo.Auth,
"user-agent": IMAGEX_USER_AGENT,
"accept-language": IMAGEX_ACCEPT_LANGUAGE,
});
const putResponse = await context.session.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,
},
headers: putHeaders,
body: image.blob,
});
if (!putResponse.ok) {
@@ -371,7 +401,8 @@ async function uploadImage(
securityToken: token.SessionToken,
});
await mainFetchJson<Record<string, unknown>>(
await imagexFetchJson<Record<string, unknown>>(
context,
commitUrl.toString(),
{
method: "POST",