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", id: "dongchedi",
label: "懂车帝", label: "懂车帝",
loginUrl: "https://mp.dcdapp.com/login", 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, detect: detectDongchedi,
}, },
}; };
@@ -2,6 +2,7 @@ import { createHash, createHmac } from "node:crypto";
import type { JsonValue } from "@geo/shared-types"; import type { JsonValue } from "@geo/shared-types";
import { STANDARD_ACCEPT_LANGUAGES, STANDARD_USER_AGENT } from "../user-agent";
import { import {
extractImageSources, extractImageSources,
normalizeArticleHtml, normalizeArticleHtml,
@@ -91,6 +92,8 @@ const DONGCHEDI_ORIGIN = "https://mp.dcdapp.com";
const DONGCHEDI_IMAGE_X_ORIGIN = "https://imagex.bytedanceapi.com"; const DONGCHEDI_IMAGE_X_ORIGIN = "https://imagex.bytedanceapi.com";
const DONGCHEDI_SERVICE_ID = "f042mdwyw7"; const DONGCHEDI_SERVICE_ID = "f042mdwyw7";
const DONGCHEDI_MANAGE_URL = `${DONGCHEDI_ORIGIN}/profile_v2/content/manage/article`; 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 { function stringId(value: unknown): string {
if (typeof value === "number") { if (typeof value === "number") {
@@ -125,12 +128,35 @@ async function dongchediHeaders(
}; };
} }
async function mainFetchJson<T>(input: string, init?: RequestInit): Promise<T> { async function imagexFetchText(
const response = await fetch(input, init); 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(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(text || `request_failed_${response.status}`); 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) { if (!text) {
return {} as T; return {} as T;
} }
@@ -191,7 +217,7 @@ function signAWS4(input: {
const amzDate = formatAmzDate(now); const amzDate = formatAmzDate(now);
const dateStamp = formatDateStamp(now); const dateStamp = formatDateStamp(now);
const query = [...url.searchParams.entries()] 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)}`) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&"); .join("&");
const signedHeaders: Record<string, string> = { const signedHeaders: Record<string, string> = {
@@ -325,7 +351,8 @@ async function uploadImage(
date: new Date(token.CurrentTime), date: new Date(token.CurrentTime),
}); });
const applyResponse = await mainFetchJson<DongchediImageXApplyResponse>( const applyResponse = await imagexFetchJson<DongchediImageXApplyResponse>(
context,
applyUrl.toString(), applyUrl.toString(),
{ {
headers: signedApplyHeaders, headers: signedApplyHeaders,
@@ -343,14 +370,17 @@ async function uploadImage(
} }
const buffer = await image.blob.arrayBuffer(); 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", method: "PUT",
headers: { headers: putHeaders,
"content-disposition": 'attachment; filename="undefined"',
"content-type": "application/octet-stream",
"content-crc32": crc32(new Uint8Array(buffer)),
authorization: storeInfo.Auth,
},
body: image.blob, body: image.blob,
}); });
if (!putResponse.ok) { if (!putResponse.ok) {
@@ -371,7 +401,8 @@ async function uploadImage(
securityToken: token.SessionToken, securityToken: token.SessionToken,
}); });
await mainFetchJson<Record<string, unknown>>( await imagexFetchJson<Record<string, unknown>>(
context,
commitUrl.toString(), commitUrl.toString(),
{ {
method: "POST", method: "POST",