feat: report desktop release upload completion progress

This commit is contained in:
2026-06-23 00:04:26 +08:00
parent f0de364063
commit d5e592e290
6 changed files with 395 additions and 27 deletions
+59 -24
View File
@@ -91,6 +91,7 @@ export interface DesktopClientReleaseUploadProgress {
total?: number
percent?: number
stage?: 'hashing' | 'signing' | 'uploading' | 'verifying' | 'complete'
message?: string
chunkIndex?: number
chunkCount?: number
}
@@ -128,6 +129,19 @@ interface DesktopClientReleaseFileHashes {
sha256: string
}
interface DesktopClientReleaseCompleteJob {
job_id: string
status: 'running' | 'succeeded' | 'failed'
stage: string
message: string
percent: number
result?: DesktopClientReleaseUploadResult
error_code?: string
error?: string
created_at: string
updated_at: string
}
export const platformOptions = [
{ label: 'macOS', value: 'darwin' },
{ label: 'Windows', value: 'win32' },
@@ -298,14 +312,8 @@ async function completeDirectUpload(
session: DesktopClientReleaseDirectUploadSession,
target: DesktopClientReleaseUploadTarget,
): Promise<DesktopClientReleaseUploadResult> {
target.onProgress?.({
loaded: session.file_size_bytes,
total: session.file_size_bytes,
percent: 100,
stage: 'verifying',
})
const result = await http.post<DesktopClientReleaseUploadResult>(
'/desktop-client/releases/direct-upload/complete',
const job = await http.post<DesktopClientReleaseCompleteJob>(
'/desktop-client/releases/direct-upload/complete-jobs',
{
platform: target.platform,
arch: target.arch,
@@ -318,13 +326,7 @@ async function completeDirectUpload(
sha256: session.sha256,
},
)
target.onProgress?.({
loaded: result.file_size_bytes,
total: result.file_size_bytes,
percent: 100,
stage: 'complete',
})
return result
return pollCompleteJob(job.job_id, session.file_size_bytes, target)
}
function isDirectUploadNotFoundError(error: unknown): boolean {
@@ -343,6 +345,45 @@ function shouldFallbackToChunkedUpload(error: unknown): boolean {
return error instanceof Error && error.message === 'missing_direct_upload_url'
}
async function pollCompleteJob(
jobID: string,
totalBytes: number,
target: DesktopClientReleaseUploadTarget,
chunkCount?: number,
): Promise<DesktopClientReleaseUploadResult> {
for (;;) {
const job = await http.get<DesktopClientReleaseCompleteJob>(
`/desktop-client/release-upload-complete-jobs/${jobID}`,
)
target.onProgress?.({
loaded: totalBytes,
total: totalBytes,
percent: Math.min(99, Math.max(1, job.percent)),
stage: 'verifying',
message: job.message,
chunkCount,
})
if (job.status === 'succeeded' && job.result) {
target.onProgress?.({
loaded: job.result.file_size_bytes,
total: job.result.file_size_bytes,
percent: 100,
stage: 'complete',
message: job.message || '上传完成',
chunkCount,
})
return job.result
}
if (job.status === 'failed') {
throw new OpsApiError({
message: job.error_code || 'desktop_client_release_upload_failed',
detail: job.error || job.message || '上传确认失败',
})
}
await delay(1000)
}
}
async function hashFile(
file: File,
onProgress?: (loaded: number) => void,
@@ -404,18 +445,12 @@ async function uploadOSSInChunks(
emitChunkProgress(target, confirmedBytes, file.size, chunkCount, index)
}
target.onProgress?.({
loaded: file.size,
total: file.size,
percent: 100,
stage: 'complete',
chunkCount,
})
return await http.post<DesktopClientReleaseUploadResult>(
`/desktop-client/release-uploads/${session.upload_id}/complete`,
const job = await http.post<DesktopClientReleaseCompleteJob>(
`/desktop-client/release-uploads/${session.upload_id}/complete-jobs`,
undefined,
{ timeout: DESKTOP_CLIENT_RELEASE_COMPLETE_TIMEOUT_MS },
)
return await pollCompleteJob(job.job_id, file.size, target, chunkCount)
} catch (error) {
void http.delete(`/desktop-client/release-uploads/${session.upload_id}`).catch(() => undefined)
throw error