fix(toutiao): fail publish when body images fail to upload

Route content/cover images through the desktop media-image asset
pipeline and abort before submitting when any body image was not
replaced with a Toutiao-hosted URL, so we don't publish articles
that reference our own private asset endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:24:28 +08:00
parent 8761e47f78
commit a9591143e8
4 changed files with 215 additions and 4 deletions
+30 -2
View File
@@ -60,7 +60,7 @@ export interface ToutiaoPublishTransport {
uploadHtmlImages(
html: string,
uploader: (sourceUrl: string) => Promise<string | null>,
): Promise<{ html: string }>
): Promise<{ html: string; uploaded?: Map<string, string> }>
reportProgress?(stage: 'media_info' | 'upload_content_images' | 'upload_cover' | 'submit'): void
}
@@ -77,10 +77,25 @@ export type ToutiaoPublishResult =
| {
success: false
status: 'failed'
code: 'toutiaohao_not_logged_in' | 'article_content_empty' | 'toutiaohao_publish_failed'
code:
| 'toutiaohao_not_logged_in'
| 'article_content_empty'
| 'toutiaohao_image_upload_failed'
| 'toutiaohao_publish_failed'
message: string
}
function extractHtmlImageSources(html: string): string[] {
const sources = new Set<string>()
for (const match of html.matchAll(/<img\b[^>]*src=(['"])(.*?)\1[^>]*>/gi)) {
const source = match[2]?.trim()
if (source) {
sources.add(source)
}
}
return [...sources]
}
export async function fetchToutiaoMediaSnapshot(
fetchJson: <T>(input: string, init?: RequestInit) => Promise<T>,
): Promise<ToutiaoMediaSnapshot | null> {
@@ -246,6 +261,19 @@ export async function publishToutiaoArticle(
const processed = await transport.uploadHtmlImages(normalizedHtml, async (sourceUrl) =>
uploadContentImage(transport, sourceUrl),
)
const contentImageSources = extractHtmlImageSources(normalizedHtml)
const uploadedSources = processed.uploaded ?? new Map<string, string>()
const missingUpload = contentImageSources.find(
(source) => !uploadedSources.has(source) && processed.html.includes(source),
)
if (missingUpload) {
return {
success: false,
status: 'failed',
code: 'toutiaohao_image_upload_failed',
message: `toutiaohao_image_upload_failed: ${missingUpload}`,
}
}
transport.reportProgress?.('upload_cover')
const cover = input.coverAssetUrl?.trim()