fix: improve toutiao image uploads

This commit is contained in:
2026-06-18 20:47:41 +08:00
parent 31c4dd9358
commit 4fa5adc25a
6 changed files with 77 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@geo/desktop-client",
"version": "0.1.1",
"version": "0.1.2",
"private": true,
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
"author": {
@@ -33,6 +33,14 @@ describe('media image helpers', () => {
])
})
it('can prefer jpeg conversion for platforms that reject large png uploads', () => {
expect(buildAssetURLCandidates('/api/public/assets/12', { preferredFormat: 'jpg' })).toEqual([
'http://127.0.0.1:8080/api/public/assets/12?format=jpg',
'http://127.0.0.1:8080/api/desktop/content/assets/12?format=jpg',
'http://127.0.0.1:8080/api/public/assets/12',
])
})
it('centers crop rectangles at the requested ratio', () => {
const rect = getCenteredCropRect(1600, 1000, 4 / 3)
@@ -19,9 +19,23 @@ export interface CropRect {
height: number
}
const passthroughImageTypes = new Set(['image/png', 'image/jpeg', 'image/jpg', 'image/gif'])
export type AssetImageFormat = 'png' | 'jpg' | 'jpeg'
function buildDesktopAssetFallbackURL(publicAssetUrl: URL): string | null {
export interface FetchImageAssetBlobOptions extends AdapterFetchOptions {
preferredFormat?: AssetImageFormat
passthroughTypes?: string[]
}
const defaultPassthroughImageTypes = new Set(['image/png', 'image/jpeg', 'image/jpg', 'image/gif'])
function normalizeAssetImageFormat(format: AssetImageFormat | undefined): AssetImageFormat {
return format === 'jpg' || format === 'jpeg' ? 'jpg' : 'png'
}
function buildDesktopAssetFallbackURL(
publicAssetUrl: URL,
preferredFormat: AssetImageFormat,
): string | null {
if (!publicAssetUrl.pathname.startsWith('/api/public/assets/')) {
return null
}
@@ -35,11 +49,14 @@ function buildDesktopAssetFallbackURL(publicAssetUrl: URL): string | null {
publicAssetUrl.searchParams.forEach((value, key) => {
fallbackURL.searchParams.set(key, value)
})
fallbackURL.searchParams.set('format', 'png')
fallbackURL.searchParams.set('format', preferredFormat)
return fallbackURL.toString()
}
export function buildAssetURLCandidates(sourceUrl: string): string[] {
export function buildAssetURLCandidates(
sourceUrl: string,
options: { preferredFormat?: AssetImageFormat } = {},
): string[] {
const trimmed = sourceUrl.trim()
if (!trimmed) {
return []
@@ -57,16 +74,17 @@ export function buildAssetURLCandidates(sourceUrl: string): string[] {
}
try {
const preferredFormat = normalizeAssetImageFormat(options.preferredFormat)
const parsed = new URL(trimmed, resolveDesktopApiURL('/'))
if (parsed.pathname.startsWith('/api/')) {
const apiAssetUrl = new URL(resolveDesktopApiURL(`${parsed.pathname}${parsed.search}`))
if (apiAssetUrl.pathname.startsWith('/api/public/assets/')) {
apiAssetUrl.searchParams.set('format', 'png')
apiAssetUrl.searchParams.set('format', preferredFormat)
}
pushCandidate(apiAssetUrl.toString())
const desktopAssetFallbackURL = buildDesktopAssetFallbackURL(apiAssetUrl)
const desktopAssetFallbackURL = buildDesktopAssetFallbackURL(apiAssetUrl, preferredFormat)
if (desktopAssetFallbackURL) {
pushCandidate(desktopAssetFallbackURL)
}
@@ -100,9 +118,17 @@ function fileNameForImageType(sourceType: string): string {
export async function fetchImageAssetBlob(
sourceUrl: string,
options: AdapterFetchOptions = {},
options: FetchImageAssetBlobOptions = {},
): Promise<ImageAssetBlob | null> {
for (const candidate of buildAssetURLCandidates(sourceUrl)) {
const passthroughImageTypes = new Set(
(options.passthroughTypes ?? [...defaultPassthroughImageTypes]).map((type) =>
type.toLowerCase(),
),
)
for (const candidate of buildAssetURLCandidates(sourceUrl, {
preferredFormat: options.preferredFormat,
})) {
const response = await (
shouldUseDesktopAssetFetch(candidate)
? fetchDesktopApiURL(candidate, {
@@ -62,11 +62,19 @@ describe('toutiao adapter', () => {
expect(coverBlob?.type).toBe('image/png')
expect(fetchImageAssetBlob).toHaveBeenCalledWith(
'/api/public/assets/body-token',
expect.anything(),
expect.objectContaining({
preferredFormat: 'jpg',
passthroughTypes: ['image/jpeg', 'image/jpg', 'image/png'],
timeoutMs: 30_000,
}),
)
expect(fetchImageAssetBlob).toHaveBeenCalledWith(
'/api/public/assets/cover-token',
expect.anything(),
expect.objectContaining({
preferredFormat: 'jpg',
passthroughTypes: ['image/jpeg', 'image/jpg', 'image/png'],
timeoutMs: 30_000,
}),
)
})
})
@@ -9,7 +9,14 @@ async function fetchToutiaoImageBlob(
sourceUrl: string,
signal?: AbortSignal,
): Promise<Blob | null> {
return (await fetchImageAssetBlob(sourceUrl, { signal }))?.blob ?? null
return (
(await fetchImageAssetBlob(sourceUrl, {
signal,
preferredFormat: 'jpg',
passthroughTypes: ['image/jpeg', 'image/jpg', 'image/png'],
timeoutMs: 30_000,
}))?.blob ?? null
)
}
function buildResultPayload(
@@ -47,7 +54,11 @@ export const toutiaoAdapter: PublishAdapter = {
sessionFetchJson(context.session, input, init, { signal: context.signal }),
fetchImageBlob: (sourceUrl) => fetchToutiaoImageBlob(sourceUrl, context.signal),
uploadHtmlImages: (sourceHtml, uploader) =>
uploadHtmlImages(sourceHtml, uploader, { signal: context.signal }),
uploadHtmlImages(sourceHtml, uploader, {
signal: context.signal,
imageTimeoutMs: 30_000,
timeoutMs: 120_000,
}),
async reportProgress(stage) {
await context.reportProgress(`toutiao.${stage}`)
},
+11 -3
View File
@@ -138,7 +138,7 @@ async function uploadCover(
if (publishType === 'publish') {
const form = new FormData()
form.append('upfile', blob, 'cover.png')
form.append('upfile', blob, imageFileName(blob, 'cover'))
const uploaded = await transport
.fetchJson<ToutiaoUploadPictureResponse>(
'https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture',
@@ -164,7 +164,7 @@ async function uploadCover(
}
const firstForm = new FormData()
firstForm.append('image', blob, 'cover.png')
firstForm.append('image', blob, imageFileName(blob, 'cover'))
const first = await transport
.fetchJson<ToutiaoSpiceImageResponse>(
'https://mp.toutiao.com/spice/image?device_platform=web',
@@ -220,7 +220,7 @@ async function uploadContentImage(
}
const form = new FormData()
form.append('image', blob, 'image.png')
form.append('image', blob, imageFileName(blob, 'image'))
const uploaded = await transport
.fetchJson<ToutiaoSpiceImageResponse>(
'https://mp.toutiao.com/spice/image?device_platform=web',
@@ -234,6 +234,14 @@ async function uploadContentImage(
return uploaded?.data?.image_url ?? null
}
function imageFileName(blob: Blob, baseName: string): string {
const type = blob.type.toLowerCase()
if (type === 'image/jpeg' || type === 'image/jpg') {
return `${baseName}.jpg`
}
return `${baseName}.png`
}
export async function publishToutiaoArticle(
input: ToutiaoPublishArticleInput,
transport: ToutiaoPublishTransport,