feat(bilibili): enhance article submission process and error handling
Desktop Client Build / Resolve Build Metadata (push) Successful in 29s
Frontend CI / Frontend (push) Successful in 5m28s
Desktop Client Build / Build Desktop Client (push) Successful in 26m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 39s

This commit is contained in:
2026-05-11 19:37:14 +08:00
parent 18011a7892
commit 83c5cc76d6
+112 -36
View File
@@ -6,6 +6,7 @@ import { renderTablesAsParagraphRows } from '../../../../../packages/publisher-p
import { resolveDesktopApiURL } from '../transport/api-client' import { resolveDesktopApiURL } from '../transport/api-client'
import type { PublishAdapter, PublishAdapterContext } from './base' import type { PublishAdapter, PublishAdapterContext } from './base'
import { import {
extractImageSources,
fetchImageBlob, fetchImageBlob,
normalizeArticleHtml, normalizeArticleHtml,
sessionCookieHeader, sessionCookieHeader,
@@ -66,8 +67,13 @@ type BilibiliEditorResult = {
success: boolean success: boolean
message?: string message?: string
url?: string url?: string
articleId?: string
} }
type BilibiliSubmitOutcome =
| { ok: true; articleId: string }
| { ok: false; message: string }
const mixinKeyEncTab = [ const mixinKeyEncTab = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28,
14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54,
@@ -302,14 +308,15 @@ async function fillAndSubmitEditor(
}) })
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => undefined) await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => undefined)
return await page.evaluate( const fillResult = await page.evaluate(
async ({ title: nextTitle, html: nextHtml }): Promise<BilibiliEditorResult> => { async ({
title: nextTitle,
html: nextHtml,
}): Promise<BilibiliEditorResult> => {
const sleep = (ms: number) => const sleep = (ms: number) =>
new Promise((resolve) => { new Promise((resolve) => {
setTimeout(resolve, ms) setTimeout(resolve, ms)
}) })
const normalizeText = (value: string | null | undefined) =>
(value || '').trim().replace(/\s+/g, '')
const waitForElement = async <T extends Element>( const waitForElement = async <T extends Element>(
selector: string, selector: string,
timeoutMs: number, timeoutMs: number,
@@ -415,18 +422,6 @@ async function fillAndSubmitEditor(
return true return true
} }
const clickButtonByText = async (texts: string[], timeoutMs: number): Promise<boolean> => {
const button = await waitForElement<HTMLButtonElement>('button', timeoutMs, (candidate) => {
const text = normalizeText(candidate.innerText || candidate.textContent)
return texts.some((target) => text.includes(normalizeText(target)))
})
if (!button) {
return false
}
button.click()
return true
}
const titleReady = await waitForElement<HTMLTextAreaElement>( const titleReady = await waitForElement<HTMLTextAreaElement>(
'.b-read-editor__title textarea, textarea', '.b-read-editor__title textarea, textarea',
15_000, 15_000,
@@ -463,21 +458,6 @@ async function fillAndSubmitEditor(
} }
await sleep(500) await sleep(500)
const submitted = await clickButtonByText(['提交文章', '发布'], 8_000)
if (!submitted) {
return {
success: false,
message: 'bilibili_submit_button_missing',
url: location.href,
}
}
await sleep(3_000)
const confirmed = await clickButtonByText(['确认提交', '确定', '确认'], 1_500)
if (confirmed) {
await sleep(2_000)
}
return { return {
success: true, success: true,
url: location.href, url: location.href,
@@ -488,6 +468,57 @@ async function fillAndSubmitEditor(
html, html,
}, },
) )
if (!fillResult.success) {
return fillResult
}
const submitOutcome = await page.evaluate(async (): Promise<BilibiliSubmitOutcome> => {
const sleep = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms)
})
const submit = Array.from(
document.querySelectorAll<HTMLButtonElement>('.b-read-editor__btns button'),
).find((btn) => (btn.textContent || '').includes('提交文章'))
if (!submit) {
return { ok: false, message: 'bilibili_submit_button_missing' }
}
submit.click()
const startedAt = Date.now()
while (Date.now() - startedAt < 45_000) {
const success = document.querySelector('.bre-submit-success')
if (success) {
const match = location.hash.match(/[?&]aid=(\d+)/)
return { ok: true, articleId: match?.[1] || '' }
}
const error = document.querySelector(
'.toaster-wrp.error, .toaster-wrp.topcenter.error',
) as HTMLElement | null
if (error) {
const text = (error.innerText || error.textContent || '').trim()
return { ok: false, message: text || 'bilibili_submit_rejected' }
}
await sleep(250)
}
return { ok: false, message: 'bilibili_submit_timeout' }
})
if (!submitOutcome.ok) {
return {
success: false,
message: submitOutcome.message,
url: page.url(),
}
}
return {
success: true,
articleId: submitOutcome.articleId || undefined,
url: page.url(),
}
} }
function titleEquals(left: string | undefined, right: string): boolean { function titleEquals(left: string | undefined, right: string): boolean {
@@ -642,6 +673,34 @@ function buildFailureResult(
} }
} }
if (message === 'bilibili_submit_timeout') {
return {
status: 'failed',
summary: 'bilibili 已点击提交但未在 45 秒内观察到成功反馈,请稍后到创作中心确认。',
error: { code: 'bilibili_submit_timeout', message },
}
}
if (message.includes('图片内容异常') || message.includes('图片')) {
return {
status: 'failed',
summary: `bilibili 拒绝了文章中的图片:${message}`,
error: { code: 'bilibili_image_rejected', message },
}
}
if (
message.startsWith('bilibili_submit_rejected') ||
message.includes('请先') ||
message.includes('异常')
) {
return {
status: 'failed',
summary: `bilibili 提交被前端校验拦下:${message}`,
error: { code: 'bilibili_submit_rejected', message },
}
}
return { return {
status: 'failed', status: 'failed',
summary: 'bilibili 发布失败。', summary: 'bilibili 发布失败。',
@@ -663,11 +722,20 @@ export const bilibiliAdapter: PublishAdapter = {
throw new Error('bilibili_not_logged_in') throw new Error('bilibili_not_logged_in')
} }
const html = renderTablesAsParagraphRows(normalizeArticleHtml(context.article)) let html = renderTablesAsParagraphRows(normalizeArticleHtml(context.article))
if (!html) { if (!html) {
throw new Error('article_content_empty') throw new Error('article_content_empty')
} }
const coverAssetUrl = context.article.cover_asset_url?.trim() || ''
if (coverAssetUrl) {
const hasExistingCover =
extractImageSources(html)[0] === coverAssetUrl
if (!hasExistingCover) {
html = `<img src="${coverAssetUrl}" />${html}`
}
}
context.reportProgress('bilibili.upload_content_images') context.reportProgress('bilibili.upload_content_images')
const processedHtml = await processContentImages(context, html) const processedHtml = await processContentImages(context, html)
const title = context.article.title?.trim() || '未命名文章' const title = context.article.title?.trim() || '未命名文章'
@@ -680,14 +748,22 @@ export const bilibiliAdapter: PublishAdapter = {
} }
context.reportProgress('bilibili.resolve_article_url') context.reportProgress('bilibili.resolve_article_url')
const article = await findPublishedArticle(context, title, startedAt) let articleId = editorResult.articleId?.trim() || ''
if (!article) { let articleUrl = articleId
throw new Error('bilibili_publish_result_missing') ? `https://www.bilibili.com/read/cv${encodeURIComponent(articleId)}`
: ''
if (!articleId) {
const fallback = await findPublishedArticle(context, title, startedAt)
if (!fallback) {
throw new Error('bilibili_publish_result_missing')
}
articleId = fallback.id
articleUrl = fallback.url
} }
return { return {
status: 'succeeded', status: 'succeeded',
payload: buildResultPayload(article.id, account.name, BILIBILI_MANAGE_URL, article.url), payload: buildResultPayload(articleId, account.name, BILIBILI_MANAGE_URL, articleUrl),
summary: 'bilibili 发布成功。', summary: 'bilibili 发布成功。',
} }
} catch (error) { } catch (error) {