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

This commit is contained in:
2026-05-30 22:29:28 +08:00
parent dbd2a1c9f6
commit 8ea4a2ffff
20 changed files with 227 additions and 155 deletions
@@ -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 })
}
}