fix(publish): require submit marker before platform write
Desktop Client Build / Resolve Build Metadata (push) Successful in 20s
Frontend CI / Frontend (push) Successful in 2m57s
Desktop Client Build / Build Desktop Client (push) Successful in 25m11s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 34s
Backend CI / Backend (push) Successful in 50m56s
Desktop Client Build / Resolve Build Metadata (push) Successful in 20s
Frontend CI / Frontend (push) Successful in 2m57s
Desktop Client Build / Build Desktop Client (push) Successful in 25m11s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 34s
Backend CI / Backend (push) Successful in 50m56s
This commit is contained in:
@@ -343,10 +343,14 @@ async function processContentImages(
|
||||
html: string,
|
||||
appId: string,
|
||||
): Promise<string> {
|
||||
const processed = await uploadHtmlImages(html, async (sourceUrl) => {
|
||||
await waitForHumanPace(context.signal)
|
||||
return await uploadImage(context, sourceUrl, appId, false)
|
||||
}, { signal: context.signal })
|
||||
const processed = await uploadHtmlImages(
|
||||
html,
|
||||
async (sourceUrl) => {
|
||||
await waitForHumanPace(context.signal)
|
||||
return await uploadImage(context, sourceUrl, appId, false)
|
||||
},
|
||||
{ signal: context.signal },
|
||||
)
|
||||
|
||||
return processed.html
|
||||
}
|
||||
@@ -562,7 +566,7 @@ export const baijiahaoAdapter: PublishAdapter = {
|
||||
throw new Error('baijiahao_cover_upload_failed')
|
||||
}
|
||||
|
||||
context.reportProgress('baijiahao.submit')
|
||||
await context.reportProgress('baijiahao.submit')
|
||||
await waitForHumanPace(context.signal)
|
||||
const response: BaijiahaoPublishResponse = await sessionFetchJson<BaijiahaoPublishResponse>(
|
||||
context.session,
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface AdapterContext {
|
||||
} | null
|
||||
signal: AbortSignal
|
||||
phase: AdapterTaskPhase
|
||||
reportProgress(stage: string): void
|
||||
reportProgress(stage: string): void | Promise<void>
|
||||
}
|
||||
|
||||
export interface PublishAdapterContext extends AdapterContext {
|
||||
|
||||
@@ -72,9 +72,7 @@ type BilibiliEditorResult = {
|
||||
articleId?: string
|
||||
}
|
||||
|
||||
type BilibiliSubmitOutcome =
|
||||
| { ok: true; articleId: string }
|
||||
| { ok: false; message: 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,
|
||||
@@ -319,10 +317,7 @@ async function fillAndSubmitEditor(
|
||||
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => undefined)
|
||||
|
||||
const fillResult = await page.evaluate(
|
||||
async ({
|
||||
title: nextTitle,
|
||||
html: nextHtml,
|
||||
}): Promise<BilibiliEditorResult> => {
|
||||
async ({ title: nextTitle, html: nextHtml }): Promise<BilibiliEditorResult> => {
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
@@ -740,8 +735,7 @@ export const bilibiliAdapter: PublishAdapter = {
|
||||
|
||||
const coverAssetUrl = context.article.cover_asset_url?.trim() || ''
|
||||
if (coverAssetUrl) {
|
||||
const hasExistingCover =
|
||||
extractImageSources(html)[0] === coverAssetUrl
|
||||
const hasExistingCover = extractImageSources(html)[0] === coverAssetUrl
|
||||
if (!hasExistingCover) {
|
||||
html = `<img src="${coverAssetUrl}" />${html}`
|
||||
}
|
||||
@@ -752,7 +746,7 @@ export const bilibiliAdapter: PublishAdapter = {
|
||||
const title = context.article.title?.trim() || '未命名文章'
|
||||
const startedAt = Date.now()
|
||||
|
||||
context.reportProgress('bilibili.submit_editor')
|
||||
await context.reportProgress('bilibili.submit_editor')
|
||||
const editorResult = await fillAndSubmitEditor(context, title, processedHtml)
|
||||
if (!editorResult.success) {
|
||||
throw new Error(editorResult.message || 'bilibili_publish_failed')
|
||||
|
||||
@@ -291,26 +291,26 @@ async function dongchediPageFetchJson<T>(
|
||||
if (page && !page.isClosed()) {
|
||||
const envelope = await raceWithAbort(
|
||||
page.evaluate(
|
||||
async ({ fetchInput, fetchInit }) => {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30_000)
|
||||
try {
|
||||
const response = await fetch(fetchInput, {
|
||||
...fetchInit,
|
||||
signal: controller.signal,
|
||||
})
|
||||
const text = await response.text().catch(() => '')
|
||||
return { ok: response.ok, status: response.status, body: text }
|
||||
} catch (error) {
|
||||
return { ok: false, error: String((error && (error as Error).message) || error) }
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
},
|
||||
{
|
||||
fetchInput: input,
|
||||
fetchInit: effectiveInit,
|
||||
},
|
||||
async ({ fetchInput, fetchInit }) => {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30_000)
|
||||
try {
|
||||
const response = await fetch(fetchInput, {
|
||||
...fetchInit,
|
||||
signal: controller.signal,
|
||||
})
|
||||
const text = await response.text().catch(() => '')
|
||||
return { ok: response.ok, status: response.status, body: text }
|
||||
} catch (error) {
|
||||
return { ok: false, error: String((error && (error as Error).message) || error) }
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
},
|
||||
{
|
||||
fetchInput: input,
|
||||
fetchInit: effectiveInit,
|
||||
},
|
||||
),
|
||||
{ signal: context.signal, timeoutMs: 35_000 },
|
||||
)
|
||||
@@ -337,10 +337,13 @@ async function dongchediPageFetchJson<T>(
|
||||
}
|
||||
})()`
|
||||
|
||||
const serialized = await raceWithAbort(webContents.executeJavaScript(script, true) as Promise<string>, {
|
||||
signal: context.signal,
|
||||
timeoutMs: 35_000,
|
||||
})
|
||||
const serialized = await raceWithAbort(
|
||||
webContents.executeJavaScript(script, true) as Promise<string>,
|
||||
{
|
||||
signal: context.signal,
|
||||
timeoutMs: 35_000,
|
||||
},
|
||||
)
|
||||
if (typeof serialized !== 'string' || !serialized) {
|
||||
throw new Error('dongchedi_page_fetch_failed')
|
||||
}
|
||||
@@ -406,29 +409,32 @@ async function fetchDongchediCsrfToken(context: PublishAdapterContext): Promise<
|
||||
|
||||
const page = context.playwright?.page
|
||||
if (page && !page.isClosed()) {
|
||||
const token = await raceWithAbort(page.evaluate(async (url) => {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 15_000)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'x-secsdk-csrf-request': '1',
|
||||
'x-secsdk-csrf-version': '1.2.10',
|
||||
},
|
||||
})
|
||||
return response.headers.get('x-ware-csrf-token') || ''
|
||||
} catch {
|
||||
return ''
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, `${DONGCHEDI_ORIGIN}/motor/mp_index/api/user/info?`), {
|
||||
signal: context.signal,
|
||||
timeoutMs: 18_000,
|
||||
})
|
||||
const token = await raceWithAbort(
|
||||
page.evaluate(async (url) => {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 15_000)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'x-secsdk-csrf-request': '1',
|
||||
'x-secsdk-csrf-version': '1.2.10',
|
||||
},
|
||||
})
|
||||
return response.headers.get('x-ware-csrf-token') || ''
|
||||
} catch {
|
||||
return ''
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, `${DONGCHEDI_ORIGIN}/motor/mp_index/api/user/info?`),
|
||||
{
|
||||
signal: context.signal,
|
||||
timeoutMs: 18_000,
|
||||
},
|
||||
)
|
||||
const parts = token.split(',')
|
||||
return parts.length >= 2 && parts[1] ? parts[1] : null
|
||||
}
|
||||
@@ -1126,7 +1132,7 @@ export const dongchediAdapter: PublishAdapter = {
|
||||
const contentImageSources = extractImageSources(html)
|
||||
const content = await processDongchediContentImages(context, html, contentImageSources)
|
||||
|
||||
context.reportProgress('dongchedi.save_draft')
|
||||
await context.reportProgress('dongchedi.save_draft')
|
||||
const draftBody = buildPublishBody(title, content, verticalCover, feedCover, {
|
||||
save: 0,
|
||||
})
|
||||
@@ -1163,7 +1169,7 @@ export const dongchediAdapter: PublishAdapter = {
|
||||
)
|
||||
}
|
||||
|
||||
context.reportProgress('dongchedi.submit')
|
||||
await context.reportProgress('dongchedi.submit')
|
||||
const publishBody = buildPublishBody(title, content, verticalCover, feedCover, {
|
||||
save: 1,
|
||||
pgcId: draftArticleId,
|
||||
|
||||
@@ -86,8 +86,8 @@ export const jianshuAdapter: PublishAdapter = {
|
||||
fetchJson: (input, init) =>
|
||||
sessionFetchJson(context.session, input, init, { signal: context.signal }),
|
||||
fetchImageBlob: (sourceUrl) => fetchAssetBlob(sourceUrl, context.signal),
|
||||
reportProgress(stage) {
|
||||
context.reportProgress(`jianshu.${stage}`)
|
||||
async reportProgress(stage) {
|
||||
await context.reportProgress(`jianshu.${stage}`)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1060,7 +1060,7 @@ export const juejinAdapter: PublishAdapter = {
|
||||
context.signal,
|
||||
)
|
||||
|
||||
context.reportProgress('juejin.create_draft')
|
||||
await context.reportProgress('juejin.create_draft')
|
||||
const draftId = await createDraft(context, payload, markdown, uuid)
|
||||
const draftUrl = `${JUEJIN_ORIGIN}/editor/drafts/${encodeURIComponent(draftId)}`
|
||||
const publishType = normalizePublishType(payload)
|
||||
@@ -1073,7 +1073,7 @@ export const juejinAdapter: PublishAdapter = {
|
||||
}
|
||||
}
|
||||
|
||||
context.reportProgress('juejin.submit_editor')
|
||||
await context.reportProgress('juejin.submit_editor')
|
||||
const editorResult = await publishDraft(context, draftId)
|
||||
if (!editorResult.success) {
|
||||
throw new Error(editorResult.message || 'juejin_publish_failed')
|
||||
|
||||
@@ -534,7 +534,7 @@ export const qiehaoAdapter: PublishAdapter = {
|
||||
? `${QIEHAO_ORIGIN}/marticlepublish/omPublish`
|
||||
: `${QIEHAO_ORIGIN}/marticlepublish/omSave`
|
||||
|
||||
context.reportProgress('qiehao.submit')
|
||||
await context.reportProgress('qiehao.submit')
|
||||
const response = await sessionFetchJson<QiehaoPublishResponse>(
|
||||
context.session,
|
||||
endpoint,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
normalizeArticleHtml,
|
||||
raceWithAbort,
|
||||
sessionCookieHeader,
|
||||
sessionCookieValue,
|
||||
sessionFetchJson,
|
||||
} from './common'
|
||||
|
||||
@@ -624,8 +623,7 @@ async function calculateEditorSignature(
|
||||
return editor.commands.getTextCount()
|
||||
}, html),
|
||||
{ signal: context.signal, timeoutMs: 20_000 },
|
||||
)
|
||||
.catch(() => null)
|
||||
).catch(() => null)
|
||||
|
||||
const normalizedCount =
|
||||
typeof textCount === 'number' && Number.isFinite(textCount) ? Math.round(textCount) : null
|
||||
@@ -874,7 +872,7 @@ export const smzdmAdapter: PublishAdapter = {
|
||||
throw new Error('smzdm_not_logged_in')
|
||||
}
|
||||
|
||||
context.reportProgress('smzdm.create_article_id')
|
||||
await context.reportProgress('smzdm.create_article_id')
|
||||
const articleId = await getArticleId(context)
|
||||
|
||||
let html = normalizeArticleHtml(context.article)
|
||||
@@ -905,7 +903,7 @@ export const smzdmAdapter: PublishAdapter = {
|
||||
account.uid,
|
||||
)
|
||||
|
||||
context.reportProgress('smzdm.submit')
|
||||
await context.reportProgress('smzdm.submit')
|
||||
await waitForHumanPace(context.signal)
|
||||
const response = await sessionFetchJson<SmzdmSubmitResponse>(
|
||||
context.session,
|
||||
|
||||
@@ -374,7 +374,7 @@ export const sohuhaoAdapter: PublishAdapter = {
|
||||
)
|
||||
const publishType = resolvePublishType(payload)
|
||||
|
||||
context.reportProgress('sohuhao.submit')
|
||||
await context.reportProgress('sohuhao.submit')
|
||||
const articleId = await submitArticle(context, account, processed.html, publishType)
|
||||
|
||||
return {
|
||||
|
||||
@@ -48,8 +48,8 @@ export const toutiaoAdapter: PublishAdapter = {
|
||||
fetchImageBlob: (sourceUrl) => fetchToutiaoImageBlob(sourceUrl, context.signal),
|
||||
uploadHtmlImages: (sourceHtml, uploader) =>
|
||||
uploadHtmlImages(sourceHtml, uploader, { signal: context.signal }),
|
||||
reportProgress(stage) {
|
||||
context.reportProgress(`toutiao.${stage}`)
|
||||
async reportProgress(stage) {
|
||||
await context.reportProgress(`toutiao.${stage}`)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1030,10 +1030,10 @@ export const wangyihaoAdapter: PublishAdapter = {
|
||||
{ signal: context.signal },
|
||||
)
|
||||
|
||||
context.reportProgress('wangyihao.save_draft')
|
||||
await context.reportProgress('wangyihao.save_draft')
|
||||
const draftId = await saveDraft(context, account, detail, processed.html, token)
|
||||
|
||||
context.reportProgress('wangyihao.publish_draft')
|
||||
await context.reportProgress('wangyihao.publish_draft')
|
||||
const published = await publishDraft(context, draftId)
|
||||
if (!published) {
|
||||
throw new Error('wangyihao_publish_click_failed')
|
||||
|
||||
@@ -1609,7 +1609,7 @@ async function completeWeixinPublishConfirmation(
|
||||
return response
|
||||
}
|
||||
|
||||
context.reportProgress('weixin_gzh.disable_mass_notification')
|
||||
await context.reportProgress('weixin_gzh.disable_mass_notification')
|
||||
await waitForWeixinDelay(context.signal, 300)
|
||||
const notification = await disableWeixinMassNotification(page)
|
||||
if (notification.found && notification.reason === 'switch_missing') {
|
||||
@@ -1657,7 +1657,7 @@ async function completeWeixinPublishConfirmation(
|
||||
signal: context.signal,
|
||||
})
|
||||
if (continueConfirm.clicked) {
|
||||
context.reportProgress('weixin_gzh.continue_publish_without_notification')
|
||||
await context.reportProgress('weixin_gzh.continue_publish_without_notification')
|
||||
}
|
||||
|
||||
const response = await waitForWeixinPublishResponseOrAuthorization(
|
||||
@@ -2125,8 +2125,9 @@ export const weixinGzhAdapter: PublishAdapter = {
|
||||
}
|
||||
|
||||
context.reportProgress('weixin_gzh.upload_content_images')
|
||||
const processed = await uploadHtmlImages(html, async (sourceUrl) =>
|
||||
uploadImage(context, meta, sourceUrl),
|
||||
const processed = await uploadHtmlImages(
|
||||
html,
|
||||
async (sourceUrl) => uploadImage(context, meta, sourceUrl),
|
||||
{ signal: context.signal },
|
||||
)
|
||||
const content = wrapWeixinContent(processed.html)
|
||||
@@ -2136,7 +2137,7 @@ export const weixinGzhAdapter: PublishAdapter = {
|
||||
? await uploadCover(context, meta, coverAssetUrl).catch(() => null)
|
||||
: null
|
||||
|
||||
context.reportProgress('weixin_gzh.save_draft')
|
||||
await context.reportProgress('weixin_gzh.save_draft')
|
||||
const form = buildOperateForm(context, meta, content, cover)
|
||||
const response = await sessionFetchJson<WeixinOperateResponse>(
|
||||
context.session,
|
||||
@@ -2169,7 +2170,7 @@ export const weixinGzhAdapter: PublishAdapter = {
|
||||
throw new Error(`weixin_gzh_save_failed:${message}`)
|
||||
}
|
||||
|
||||
context.reportProgress('weixin_gzh.publish')
|
||||
await context.reportProgress('weixin_gzh.publish')
|
||||
const articleTitle = context.article.title?.trim() || '未命名文章'
|
||||
let publishResponse: WeixinOperateResponse | null
|
||||
try {
|
||||
|
||||
@@ -54,10 +54,15 @@ async function zhihuFetch<T>(
|
||||
input: string,
|
||||
init?: RequestInit,
|
||||
): Promise<T> {
|
||||
return await sessionFetchJson<T>(context.session, input, {
|
||||
...init,
|
||||
headers: buildHeaders(init?.headers),
|
||||
}, { signal: context.signal })
|
||||
return await sessionFetchJson<T>(
|
||||
context.session,
|
||||
input,
|
||||
{
|
||||
...init,
|
||||
headers: buildHeaders(init?.headers),
|
||||
},
|
||||
{ signal: context.signal },
|
||||
)
|
||||
}
|
||||
|
||||
function markdownToHtml(markdown: string): string {
|
||||
@@ -245,7 +250,7 @@ async function createDraft(
|
||||
context.reportProgress('zhihu.upload_content_images')
|
||||
const content = await processContentImages(context, transformedContent)
|
||||
|
||||
context.reportProgress('zhihu.create_draft')
|
||||
await context.reportProgress('zhihu.create_draft')
|
||||
const created = await zhihuFetch<ZhihuDraftCreateResponse>(
|
||||
context,
|
||||
'https://zhuanlan.zhihu.com/api/articles/drafts',
|
||||
@@ -276,7 +281,7 @@ async function publishDraft(
|
||||
): Promise<boolean> {
|
||||
const xsrfToken = await sessionCookieValue(context.session, 'zhihu.com', '_xsrf')
|
||||
|
||||
context.reportProgress('zhihu.publish_draft')
|
||||
await context.reportProgress('zhihu.publish_draft')
|
||||
const result = await zhihuFetch<ZhihuPublishResponse>(
|
||||
context,
|
||||
'https://www.zhihu.com/api/v4/content/publish',
|
||||
|
||||
@@ -570,7 +570,7 @@ export const zolAdapter: PublishAdapter = {
|
||||
const form = new FormData()
|
||||
appendBaseFormFields(form, title, account.uid)
|
||||
|
||||
context.reportProgress('zol.create_draft')
|
||||
await context.reportProgress('zol.create_draft')
|
||||
const draftId = await createDraftId(context, form)
|
||||
|
||||
const coverAssetUrl = context.article.cover_asset_url?.trim() || ''
|
||||
@@ -598,7 +598,7 @@ export const zolAdapter: PublishAdapter = {
|
||||
form.append('taskType', '1')
|
||||
form.append('taskIds', '[]')
|
||||
|
||||
context.reportProgress('zol.submit')
|
||||
await context.reportProgress('zol.submit')
|
||||
const response = await sessionFetchJson<ZolPublishResponse>(
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.save.orther`,
|
||||
|
||||
@@ -1677,7 +1677,11 @@ function armPublishTaskDeadline(
|
||||
|
||||
function resolvePublishTaskTimeoutMs(platform: string): number {
|
||||
const envKey = `GEO_DESKTOP_PUBLISH_TIMEOUT_${platform.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_MS`
|
||||
return resolvePositiveEnvInteger(envKey) ?? resolvePositiveEnvInteger('GEO_DESKTOP_PUBLISH_TIMEOUT_MS') ?? defaultPublishTaskTimeoutMs
|
||||
return (
|
||||
resolvePositiveEnvInteger(envKey) ??
|
||||
resolvePositiveEnvInteger('GEO_DESKTOP_PUBLISH_TIMEOUT_MS') ??
|
||||
defaultPublishTaskTimeoutMs
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePositiveEnvInteger(key: string): number | null {
|
||||
@@ -1695,16 +1699,30 @@ function resolvePositiveEnvInteger(key: string): number | null {
|
||||
function isIrreversiblePublishStage(stage: string): boolean {
|
||||
const normalized = stage.trim().toLowerCase()
|
||||
return (
|
||||
normalized.endsWith('.create_note') ||
|
||||
normalized.endsWith('.create_article_id') ||
|
||||
normalized.endsWith('.create_draft') ||
|
||||
normalized.endsWith('.publish') ||
|
||||
normalized.endsWith('.publish_draft') ||
|
||||
normalized.endsWith('.save_content') ||
|
||||
normalized.endsWith('.submit') ||
|
||||
normalized.endsWith('.submit_editor') ||
|
||||
normalized.endsWith('.save_draft') ||
|
||||
normalized === 'create_note' ||
|
||||
normalized === 'create_article_id' ||
|
||||
normalized === 'create_draft' ||
|
||||
normalized === 'publish' ||
|
||||
normalized === 'publish_draft' ||
|
||||
normalized === 'save_content' ||
|
||||
normalized === 'submit' ||
|
||||
normalized === 'submit_editor' ||
|
||||
normalized === 'save_draft'
|
||||
)
|
||||
}
|
||||
|
||||
// Persist a durable "submit started" marker (server-side, before/at the irreversible POST) and a
|
||||
// local flag the abort path reads. Fires at most once per execution and never blocks publishing.
|
||||
function markPublishSubmitStartedIfNeeded(taskId: string, stage: string): void {
|
||||
// Persist a durable "submit started" marker before the irreversible platform write. The local flag
|
||||
// is set only after the server confirms the marker, so recovery can trust publish_submit_started_at.
|
||||
async function markPublishSubmitStartedIfNeeded(taskId: string, stage: string): Promise<void> {
|
||||
if (!isIrreversiblePublishStage(stage)) {
|
||||
return
|
||||
}
|
||||
@@ -1712,19 +1730,31 @@ function markPublishSubmitStartedIfNeeded(taskId: string, stage: string): void {
|
||||
if (!active || active.kind !== 'publish' || active.submitStartedAt !== null) {
|
||||
return
|
||||
}
|
||||
active.submitStartedAt = Date.now()
|
||||
state.activeExecutions.set(taskId, active)
|
||||
|
||||
const leaseToken = state.tasks.get(taskId)?.leaseToken
|
||||
if (!leaseToken) {
|
||||
return
|
||||
throw new Error('publish_submit_marker_lease_missing')
|
||||
}
|
||||
await markDesktopTaskPublishSubmitStarted(taskId, leaseToken)
|
||||
|
||||
active.submitStartedAt = Date.now()
|
||||
state.activeExecutions.set(taskId, active)
|
||||
}
|
||||
|
||||
function asSubmitUncertainExecution(result: AdapterExecutionResult): AdapterExecutionResult {
|
||||
if (result.status !== 'failed') {
|
||||
return result
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
status: 'unknown',
|
||||
summary: '发布可能已提交到平台,结果待人工确认,已避免自动重复发布。',
|
||||
error: {
|
||||
...(result.error ?? {}),
|
||||
publish_submit_uncertain: true,
|
||||
original_status: 'failed',
|
||||
original_summary: result.summary,
|
||||
},
|
||||
}
|
||||
void markDesktopTaskPublishSubmitStarted(taskId, leaseToken).catch((error) => {
|
||||
console.warn('[desktop-runtime] mark publish submit started failed', {
|
||||
taskId,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function pumpExecutionLoop(): void {
|
||||
@@ -2824,11 +2854,7 @@ async function extendActiveLease(taskId: string, leaseToken: string): Promise<vo
|
||||
existing.summary = `发布任务超过 ${Math.round(idleMs / 1000)} 秒没有新进度,已中止并释放,准备由服务端重排${detail}。`
|
||||
existing.updatedAt = Date.now()
|
||||
state.tasks.set(taskId, existing)
|
||||
recordActivity(
|
||||
'warn',
|
||||
'发布任务疑似卡住',
|
||||
`${existing.title} ${existing.summary}`,
|
||||
)
|
||||
recordActivity('warn', '发布任务疑似卡住', `${existing.title} ${existing.summary}`)
|
||||
// Abort the local execution instead of merely letting the lease lapse: this
|
||||
// releases the slot immediately and, crucially, closes the window where the
|
||||
// server could re-queue the task while a stalled adapter is still mid-publish
|
||||
@@ -2914,16 +2940,19 @@ async function executeTaskAdapter(
|
||||
signal,
|
||||
phase: 'initial',
|
||||
article: await loadPublishArticle(task),
|
||||
reportProgress(stage: string) {
|
||||
async reportProgress(stage: string) {
|
||||
noteMonitorExecutionSafePoint(task.id, null)
|
||||
updateTaskProgress(task.id, `publish adapter progress: ${stage}`)
|
||||
markPublishSubmitStartedIfNeeded(task.id, stage)
|
||||
await markPublishSubmitStartedIfNeeded(task.id, stage)
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
await maybeReportTaskAuthFailure(task, result, accountIdentity)
|
||||
return result
|
||||
const active = state.activeExecutions.get(task.id)
|
||||
const normalizedResult =
|
||||
active?.submitStartedAt != null ? asSubmitUncertainExecution(result) : result
|
||||
await maybeReportTaskAuthFailure(task, normalizedResult, accountIdentity)
|
||||
return normalizedResult
|
||||
} finally {
|
||||
await playwrightLease?.release()
|
||||
if (viewHandle) {
|
||||
@@ -3358,7 +3387,11 @@ function notifyLongRunningPublishTasks(): void {
|
||||
task.summary = `${task.summary || '发布任务仍在执行。'}(已执行 ${formatDurationText(now - active.startedAt)},如网络异常可取消后重试。)`
|
||||
task.updatedAt = now
|
||||
state.tasks.set(task.id, task)
|
||||
recordActivity('warn', '发布等待较久', `${task.title} 已执行 ${formatDurationText(now - active.startedAt)},可能存在网络异常。`)
|
||||
recordActivity(
|
||||
'warn',
|
||||
'发布等待较久',
|
||||
`${task.title} 已执行 ${formatDurationText(now - active.startedAt)},可能存在网络异常。`,
|
||||
)
|
||||
emitRuntimeInvalidated('publish-task-progress', { taskId: task.id })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user