feat(desktop): add client release updater
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s

This commit is contained in:
2026-05-25 19:23:49 +08:00
parent 41b4a765ac
commit 5f6e9f11da
46 changed files with 5508 additions and 160 deletions
+87 -26
View File
@@ -13,7 +13,11 @@ import type {
AccountProbeState,
AuthProbeResult,
} from './auth-types'
import { getPlatformAdapter, type PlatformFailureInput } from './platform-auth-adapters'
import {
getPlatformAdapter,
type PlatformAdapter,
type PlatformFailureInput,
} from './platform-auth-adapters'
import { emitRuntimeInvalidated } from './runtime-events'
import { getPersistedPartition, getSessionHandle } from './session-registry'
@@ -30,6 +34,7 @@ const MAX_CONCURRENT_ACCOUNT_PROBES = 2
const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000
const MANUAL_PROBE_MIN_INTERVAL_MS = 15_000
const ACCOUNT_PROBE_RETRY_BACKOFF_MS = [1_000, 3_000, 10_000] as const
const CHALLENGE_RECHECK_ATTEMPTS = 1
type ProbeTrigger = 'scheduled' | 'manual' | 'preflight' | 'confirm'
@@ -366,6 +371,62 @@ function applyNetworkResult(
return commitRecord(record, previousSignature)
}
function networkProbeResult(): AuthProbeResult {
return {
verdict: 'network_error',
reason: 'network_error',
profile: null,
sessionFingerprint: null,
}
}
async function probeAdapter(
adapter: PlatformAdapter,
account: PublishAccountIdentity,
partition: string,
): Promise<AuthProbeResult> {
return await adapter.probe(account, partition).catch<AuthProbeResult>(() => networkProbeResult())
}
async function recheckChallengeResult(
adapter: PlatformAdapter,
account: PublishAccountIdentity,
partition: string,
result: AuthProbeResult,
): Promise<AuthProbeResult> {
if (result.verdict !== 'challenge_required') {
return result
}
let confirmed = result
for (let attempt = 0; attempt < CHALLENGE_RECHECK_ATTEMPTS; attempt += 1) {
confirmed = await probeAdapter(adapter, account, partition)
if (confirmed.verdict !== 'challenge_required') {
return confirmed
}
}
return confirmed
}
function applyProbeResult(
record: AccountHealthRecord,
result: AuthProbeResult,
now: number,
trigger: ProbeTrigger,
): AccountHealthSnapshot {
switch (result.verdict) {
case 'active':
return applyActiveResult(record, result, now)
case 'expired':
return applyExpiredResult(record, result, now)
case 'challenge_required':
return applyChallengeResult(record, result, now)
default:
return applyNetworkResult(record, result, now, trigger)
}
}
async function performProbeLocked(
account: PublishAccountIdentity,
options: {
@@ -407,28 +468,18 @@ async function performProbeLocked(
}
}
const result = await adapter.probe(account, partition).catch<AuthProbeResult>(() => ({
verdict: 'network_error',
reason: 'network_error',
profile: null,
sessionFingerprint: null,
}))
const result = await probeAdapter(adapter, account, partition)
if (record.authRevision !== probeAuthRevision) {
return toPublicSnapshot(record)
}
const now = Date.now()
switch (result.verdict) {
case 'active':
return applyActiveResult(record, result, now)
case 'expired':
return applyExpiredResult(record, result, now)
case 'challenge_required':
return applyChallengeResult(record, result, now)
default:
return applyNetworkResult(record, result, now, options.trigger)
const confirmedResult = await recheckChallengeResult(adapter, account, partition, result)
if (record.authRevision !== probeAuthRevision) {
return toPublicSnapshot(record)
}
return applyProbeResult(record, confirmedResult, Date.now(), options.trigger)
}
function canUseCachedProbe(
@@ -922,15 +973,25 @@ export async function reportAccountFailure(
const now = Date.now()
if (classification === 'challenge' || classification === 'risk_control') {
record.authState = 'challenge_required'
record.probeState = 'idle'
record.authReason = classification === 'risk_control' ? 'risk_control' : 'captcha_gate'
record.lastAuthFailureAt = now
record.lastProbeAt = now
record.nextProbeAt = nextRegularProbeAt(now)
record.suspectedExpiredAt = null
record.confirmFailureCount = 0
return commitRecord(record, previousSignature)
record.probeState = 'probing'
commitRecord(record, previousSignature)
const partition = resolveKnownPartition(account.id)
const result = partition
? await recheckChallengeResult(adapter, account, partition, {
verdict: 'challenge_required',
reason: classification === 'risk_control' ? 'risk_control' : 'captcha_gate',
profile: null,
sessionFingerprint: null,
})
: {
verdict: 'expired' as const,
reason: 'missing_partition' as const,
profile: null,
sessionFingerprint: null,
}
return applyProbeResult(record, result, Date.now(), 'confirm')
}
if (classification === 'network') {