fix(publish): prevent stuck publish queue and duplicate posting
Desktop Client Build / Resolve Build Metadata (push) Successful in 41s
Backend CI / Backend (push) Failing after 7m14s
Desktop Client Build / Build Desktop Client (push) Successful in 23m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 56s
Desktop Client Build / Resolve Build Metadata (push) Successful in 41s
Backend CI / Backend (push) Failing after 7m14s
Desktop Client Build / Build Desktop Client (push) Successful in 23m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 56s
The desktop publish queue could stall for a long time and end users assumed the software was broken. Root cause: a hung adapter held its execution slot with no wall-clock timeout while auto-renewing its lease forever, so the server never reclaimed it and every queued task behind it stayed 等待发布. Auto-recovery also risked silently re-posting a non-idempotent article. Client (Electron): - per-task wall-clock deadline + abort; progress-gated lease renewal that stops and aborts a stalled task instead of renewing it forever - decouple the concurrency cap from CDP-induced CPU/memory pressure (admission gate instead of self-throttling collapse); 15s watchdog pump - all adapter network I/O now has fetch timeouts and honors context.signal; bounded image-upload concurrency with per-image timeout - surface live adapter progress, elapsed time, queue position and a working-vs-queued distinction in the publish view Server (tenant-api): - publish lease-recovery worker (every 3m) + supporting index: reclaim expired in_progress publish leases, requeue (<3 attempts) or terminal-fail - 3-minute lease TTL with client-presence-gated extension; max 3 attempts Idempotency (production-grade core): - durable publish_submit_started_at marker, set before the irreversible platform submit POST; recovery and abort route a maybe-submitted task to unknown (manual reconcile, kept in the dedup set) instead of re-posting - desktop UI requires explicit confirmation before retrying a possibly- already-published task Verified: go build/vet/test (incl. resolvePublishRecoveryOutcome), vue-tsc, vitest 141/141, gofmt all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,7 @@ async function fetchAccount(context: PublishAdapterContext): Promise<SohuAccount
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context),
|
||||
},
|
||||
{ signal: context.signal },
|
||||
).catch(() => null)
|
||||
|
||||
const account = accountFromRaw(registerInfo?.data?.account)
|
||||
@@ -157,6 +158,7 @@ async function fetchAccount(context: PublishAdapterContext): Promise<SohuAccount
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context),
|
||||
},
|
||||
{ signal: context.signal },
|
||||
).catch(() => null)
|
||||
|
||||
for (const group of list?.data?.data ?? []) {
|
||||
@@ -176,7 +178,7 @@ async function uploadImage(
|
||||
accountId: string,
|
||||
sourceUrl: string,
|
||||
): Promise<string | null> {
|
||||
const image = await fetchImageAssetBlob(sourceUrl)
|
||||
const image = await fetchImageAssetBlob(sourceUrl, { signal: context.signal })
|
||||
if (!image) {
|
||||
return null
|
||||
}
|
||||
@@ -188,14 +190,19 @@ async function uploadImage(
|
||||
const url = new URL(`${SOHU_ORIGIN}/commons/front/outerUpload/image/file`)
|
||||
url.searchParams.set('accountId', accountId)
|
||||
|
||||
const response = await sessionFetchJson<SohuUploadResponse>(context.session, url.toString(), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context, {
|
||||
'sp-cm': await sohuSpCm(context),
|
||||
}),
|
||||
body: form,
|
||||
}).catch(() => null)
|
||||
const response = await sessionFetchJson<SohuUploadResponse>(
|
||||
context.session,
|
||||
url.toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context, {
|
||||
'sp-cm': await sohuSpCm(context),
|
||||
}),
|
||||
body: form,
|
||||
},
|
||||
{ signal: context.signal },
|
||||
).catch(() => null)
|
||||
|
||||
return response?.url?.trim() || null
|
||||
}
|
||||
@@ -225,16 +232,21 @@ async function submitArticle(
|
||||
infoResource: 0,
|
||||
}
|
||||
|
||||
const response = await sessionFetchJson<SohuSubmitResponse>(context.session, url.toString(), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context, {
|
||||
'content-type': 'application/json',
|
||||
'dv-id': generateDeviceId(),
|
||||
'sp-cm': await sohuSpCm(context),
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
}).catch(
|
||||
const response = await sessionFetchJson<SohuSubmitResponse>(
|
||||
context.session,
|
||||
url.toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await sohuHeaders(context, {
|
||||
'content-type': 'application/json',
|
||||
'dv-id': generateDeviceId(),
|
||||
'sp-cm': await sohuSpCm(context),
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
{ signal: context.signal },
|
||||
).catch(
|
||||
(error): SohuSubmitResponse => ({
|
||||
code: -1,
|
||||
msg: error instanceof Error ? error.message : 'sohuhao_submit_failed',
|
||||
@@ -355,8 +367,10 @@ export const sohuhaoAdapter: PublishAdapter = {
|
||||
}
|
||||
|
||||
context.reportProgress('sohuhao.upload_content_images')
|
||||
const processed = await uploadHtmlImages(html, async (sourceUrl) =>
|
||||
uploadImage(context, account.id, sourceUrl),
|
||||
const processed = await uploadHtmlImages(
|
||||
html,
|
||||
async (sourceUrl) => uploadImage(context, account.id, sourceUrl),
|
||||
{ signal: context.signal },
|
||||
)
|
||||
const publishType = resolvePublishType(payload)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user