From 83c5cc76d66ed1a7c3e066465a0d0553c26ac6f1 Mon Sep 17 00:00:00 2001 From: liangxu Date: Mon, 11 May 2026 19:37:14 +0800 Subject: [PATCH] feat(bilibili): enhance article submission process and error handling --- .../src/main/adapters/bilibili.ts | 148 +++++++++++++----- 1 file changed, 112 insertions(+), 36 deletions(-) diff --git a/apps/desktop-client/src/main/adapters/bilibili.ts b/apps/desktop-client/src/main/adapters/bilibili.ts index d11b56d..9630f2c 100644 --- a/apps/desktop-client/src/main/adapters/bilibili.ts +++ b/apps/desktop-client/src/main/adapters/bilibili.ts @@ -6,6 +6,7 @@ import { renderTablesAsParagraphRows } from '../../../../../packages/publisher-p import { resolveDesktopApiURL } from '../transport/api-client' import type { PublishAdapter, PublishAdapterContext } from './base' import { + extractImageSources, fetchImageBlob, normalizeArticleHtml, sessionCookieHeader, @@ -66,8 +67,13 @@ type BilibiliEditorResult = { success: boolean message?: string url?: string + articleId?: string } +type BilibiliSubmitOutcome = + | { ok: true; articleId: string } + | { ok: false; message: string } + 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, 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) - return await page.evaluate( - async ({ title: nextTitle, html: nextHtml }): Promise => { + const fillResult = await page.evaluate( + async ({ + title: nextTitle, + html: nextHtml, + }): Promise => { const sleep = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms) }) - const normalizeText = (value: string | null | undefined) => - (value || '').trim().replace(/\s+/g, '') const waitForElement = async ( selector: string, timeoutMs: number, @@ -415,18 +422,6 @@ async function fillAndSubmitEditor( return true } - const clickButtonByText = async (texts: string[], timeoutMs: number): Promise => { - const button = await waitForElement('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( '.b-read-editor__title textarea, textarea', 15_000, @@ -463,21 +458,6 @@ async function fillAndSubmitEditor( } 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 { success: true, url: location.href, @@ -488,6 +468,57 @@ async function fillAndSubmitEditor( html, }, ) + + if (!fillResult.success) { + return fillResult + } + + const submitOutcome = await page.evaluate(async (): Promise => { + const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + + const submit = Array.from( + document.querySelectorAll('.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 { @@ -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 { status: 'failed', summary: 'bilibili 发布失败。', @@ -663,11 +722,20 @@ export const bilibiliAdapter: PublishAdapter = { throw new Error('bilibili_not_logged_in') } - const html = renderTablesAsParagraphRows(normalizeArticleHtml(context.article)) + let html = renderTablesAsParagraphRows(normalizeArticleHtml(context.article)) if (!html) { 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 = `${html}` + } + } + context.reportProgress('bilibili.upload_content_images') const processedHtml = await processContentImages(context, html) const title = context.article.title?.trim() || '未命名文章' @@ -680,14 +748,22 @@ export const bilibiliAdapter: PublishAdapter = { } context.reportProgress('bilibili.resolve_article_url') - const article = await findPublishedArticle(context, title, startedAt) - if (!article) { - throw new Error('bilibili_publish_result_missing') + let articleId = editorResult.articleId?.trim() || '' + let articleUrl = articleId + ? `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 { status: 'succeeded', - payload: buildResultPayload(article.id, account.name, BILIBILI_MANAGE_URL, article.url), + payload: buildResultPayload(articleId, account.name, BILIBILI_MANAGE_URL, articleUrl), summary: 'bilibili 发布成功。', } } catch (error) {