Compare commits
2 Commits
c16a684add
...
83c5cc76d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 83c5cc76d6 | |||
| 18011a7892 |
@@ -0,0 +1,54 @@
|
||||
const RELOAD_FLAG_KEY = '__chunk_reload_at'
|
||||
const RELOAD_COOLDOWN_MS = 10_000
|
||||
|
||||
function shouldReload(): boolean {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(RELOAD_FLAG_KEY)
|
||||
if (!raw) return true
|
||||
const at = Number(raw)
|
||||
if (!Number.isFinite(at)) return true
|
||||
return Date.now() - at > RELOAD_COOLDOWN_MS
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function markReload(): void {
|
||||
try {
|
||||
sessionStorage.setItem(RELOAD_FLAG_KEY, String(Date.now()))
|
||||
} catch {
|
||||
// ignore storage failures (private mode, quota)
|
||||
}
|
||||
}
|
||||
|
||||
export function reloadForStaleChunk(): boolean {
|
||||
if (!shouldReload()) return false
|
||||
markReload()
|
||||
location.reload()
|
||||
return true
|
||||
}
|
||||
|
||||
const CHUNK_ERROR_PATTERNS = [
|
||||
/Failed to fetch dynamically imported module/i,
|
||||
/error loading dynamically imported module/i,
|
||||
/Importing a module script failed/i,
|
||||
/Unable to preload CSS/i,
|
||||
]
|
||||
|
||||
export function isChunkLoadError(err: unknown): boolean {
|
||||
const msg =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === 'string'
|
||||
? err
|
||||
: ''
|
||||
return CHUNK_ERROR_PATTERNS.some((re) => re.test(msg))
|
||||
}
|
||||
|
||||
export function registerChunkReloadHandlers(): void {
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
if (reloadForStaleChunk()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -53,11 +53,14 @@ import { createApp } from 'vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import App from './App.vue'
|
||||
import { i18n } from './i18n'
|
||||
import { registerChunkReloadHandlers } from './lib/chunk-reload'
|
||||
import { subscribeAuthExpired, subscribeStoredSession } from './lib/session'
|
||||
import { router } from './router'
|
||||
import { pinia } from './stores/pinia'
|
||||
import './styles.css'
|
||||
|
||||
registerChunkReloadHandlers()
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { isChunkLoadError, reloadForStaleChunk } from '@/lib/chunk-reload'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { pinia } from '@/stores/pinia'
|
||||
|
||||
@@ -282,4 +283,10 @@ router.beforeEach(async (to) => {
|
||||
return true
|
||||
})
|
||||
|
||||
router.onError((err) => {
|
||||
if (isChunkLoadError(err)) {
|
||||
reloadForStaleChunk()
|
||||
}
|
||||
})
|
||||
|
||||
export { router }
|
||||
|
||||
@@ -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<BilibiliEditorResult> => {
|
||||
const fillResult = await page.evaluate(
|
||||
async ({
|
||||
title: nextTitle,
|
||||
html: nextHtml,
|
||||
}): Promise<BilibiliEditorResult> => {
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
const normalizeText = (value: string | null | undefined) =>
|
||||
(value || '').trim().replace(/\s+/g, '')
|
||||
const waitForElement = async <T extends Element>(
|
||||
selector: string,
|
||||
timeoutMs: number,
|
||||
@@ -415,18 +422,6 @@ async function fillAndSubmitEditor(
|
||||
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>(
|
||||
'.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<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 {
|
||||
@@ -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 = `<img src="${coverAssetUrl}" />${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) {
|
||||
|
||||
Reference in New Issue
Block a user