From 8cc409ce68e94916bff3bf4e48ef2edd46d9730a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:28:50 +0000 Subject: [PATCH 1/2] Initial plan From 63219e4802e99d87b0b3ce28349aa464d4331456 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:40:20 +0000 Subject: [PATCH 2/2] fix: skip version check for preview channel and add upload progress bar Co-authored-by: Rain-kl <63696351+Rain-kl@users.noreply.github.com> --- atsf_server/service/update.go | 10 ++- atsf_server/service/update_test.go | 30 +++++++++ .../components/layout/dashboard-topbar.tsx | 10 ++- atsf_server/web/features/update/api/update.ts | 61 +++++++++++++++++-- .../components/version-upgrade-modal.tsx | 17 ++++++ 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/atsf_server/service/update.go b/atsf_server/service/update.go index 81d7c574..0d3adf8d 100644 --- a/atsf_server/service/update.go +++ b/atsf_server/service/update.go @@ -376,7 +376,15 @@ func buildLatestServerReleaseView(release *githubReleaseResponse, channel Releas isDevBuild := currentVersion == "" || strings.EqualFold(currentVersion, "dev") hasUpdate := false if release != nil && !isDevBuild { - hasUpdate = isVersionNewer(currentVersion, release.TagName) + if channel == ReleaseChannelPreview { + // Preview releases use a "major.minor.patch-git-" scheme that cannot + // be meaningfully compared against the running stable version, so we skip the + // version check and always allow upgrading when the user explicitly selects + // the preview channel. + hasUpdate = true + } else { + hasUpdate = isVersionNewer(currentVersion, release.TagName) + } } serverUpgradeState.Lock() diff --git a/atsf_server/service/update_test.go b/atsf_server/service/update_test.go index 340d4866..7ceaa85b 100644 --- a/atsf_server/service/update_test.go +++ b/atsf_server/service/update_test.go @@ -140,6 +140,36 @@ func TestBuildLatestServerReleaseViewPreview(t *testing.T) { } } +// TestBuildLatestServerReleaseViewPreviewBypassVersionCheck verifies that switching to +// the preview channel always reports has_update=true, even when the preview tag uses a +// "major.minor.patch-git-" scheme that would otherwise compare as equal-or-older +// than the currently running stable version. +func TestBuildLatestServerReleaseViewPreviewBypassVersionCheck(t *testing.T) { + originalVersion := common.Version + common.Version = "v1.0.0" + t.Cleanup(func() { + common.Version = originalVersion + resetServerUpgradeTestState(t) + }) + + // A typical preview tag: same base version as stable but with a git-commit suffix. + // Without the bypass, isVersionNewer("v1.0.0", "v1.0.0-git-abc1234") returns false + // because a version without a prerelease identifier is considered higher than one + // with a prerelease identifier under semver rules. + view := buildLatestServerReleaseView(&githubReleaseResponse{ + TagName: "v1.0.0-git-abc1234", + Prerelease: true, + PublishedAt: "2026-03-12T00:00:00Z", + }, ReleaseChannelPreview) + + if !view.HasUpdate { + t.Fatal("expected preview channel to bypass version comparison and report has_update=true") + } + if view.Channel != ReleaseChannelPreview.String() { + t.Fatalf("unexpected channel: %s", view.Channel) + } +} + func TestUploadManualServerBinary(t *testing.T) { originalVersion := common.Version common.Version = "v0.4.0" diff --git a/atsf_server/web/components/layout/dashboard-topbar.tsx b/atsf_server/web/components/layout/dashboard-topbar.tsx index 7b65dcc7..90dcdb39 100644 --- a/atsf_server/web/components/layout/dashboard-topbar.tsx +++ b/atsf_server/web/components/layout/dashboard-topbar.tsx @@ -45,6 +45,7 @@ export function DashboardTopbar() { ); const [uploadedBinary, setUploadedBinary] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); const menuRef = useRef(null); const isRoot = (user?.role ?? 0) >= 100; const upgradeStatusPollInterval = 3000; @@ -102,14 +103,19 @@ export function DashboardTopbar() { }); const uploadBinaryMutation = useMutation({ - mutationFn: uploadServerBinary, + mutationFn: (binary: File) => + uploadServerBinary(binary, (progress) => { + setUploadProgress(progress); + }), onSuccess: (candidate) => { + setUploadProgress(0); setVersionFeedback(null); setManualUpgradeError(null); setUploadedBinary(candidate); setManualUpgradeStatus(candidate.comparison_message); }, onError: (error) => { + setUploadProgress(0); setUploadedBinary(null); setManualUpgradeStatus(null); setManualUpgradeError( @@ -217,6 +223,7 @@ export function DashboardTopbar() { }; const handleUploadBinary = (binary: File) => { + setUploadProgress(0); setManualUpgradeStatus(null); setManualUpgradeError(null); uploadBinaryMutation.mutate(binary); @@ -359,6 +366,7 @@ export function DashboardTopbar() { } isUpgrading={upgradeMutation.isPending} isUploadingBinary={uploadBinaryMutation.isPending} + uploadProgress={uploadProgress} isConfirmingManualUpgrade={confirmManualUpgradeMutation.isPending} onChannelChange={handleReleaseChannelChange} onCheck={handleCheckRelease} diff --git a/atsf_server/web/features/update/api/update.ts b/atsf_server/web/features/update/api/update.ts index 1c52b35c..7b5e8dad 100644 --- a/atsf_server/web/features/update/api/update.ts +++ b/atsf_server/web/features/update/api/update.ts @@ -1,4 +1,5 @@ -import { apiRequest } from '@/lib/api/client'; +import { ApiError, apiRequest, getApiUrl } from '@/lib/api/client'; +import type { ApiEnvelope } from '@/types/api'; import type { LatestReleaseInfo, @@ -19,13 +20,63 @@ export function upgradeServer(channel: ReleaseChannel = 'stable') { }); } -export function uploadServerBinary(binary: File) { +export function uploadServerBinary( + binary: File, + onProgress?: (progress: number) => void, +): Promise { const formData = new FormData(); formData.append('binary', binary); - return apiRequest('/update/manual-upload', { - method: 'POST', - body: formData, + if (!onProgress) { + return apiRequest('/update/manual-upload', { + method: 'POST', + body: formData, + }); + } + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', getApiUrl('/update/manual-upload')); + xhr.withCredentials = true; + + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + onProgress(Math.round((event.loaded / event.total) * 100)); + } + }); + + xhr.addEventListener('load', () => { + let payload: ApiEnvelope | null = null; + try { + payload = JSON.parse(xhr.responseText) as ApiEnvelope; + } catch { + payload = null; + } + if (xhr.status < 200 || xhr.status >= 300) { + reject( + new ApiError( + payload?.message || `请求失败(${xhr.status})`, + xhr.status, + ), + ); + return; + } + if (!payload) { + reject(new ApiError('响应格式无效', xhr.status)); + return; + } + if (!payload.success) { + reject(new ApiError(payload.message || '请求失败', xhr.status)); + return; + } + resolve(payload.data); + }); + + xhr.addEventListener('error', () => { + reject(new ApiError('上传过程中网络连接中断,请检查网络后重试', 0)); + }); + + xhr.send(formData); }); } diff --git a/atsf_server/web/features/update/components/version-upgrade-modal.tsx b/atsf_server/web/features/update/components/version-upgrade-modal.tsx index 97555f16..219f2ae5 100644 --- a/atsf_server/web/features/update/components/version-upgrade-modal.tsx +++ b/atsf_server/web/features/update/components/version-upgrade-modal.tsx @@ -34,6 +34,7 @@ interface VersionUpgradeModalProps { isChecking: boolean; isUpgrading: boolean; isUploadingBinary: boolean; + uploadProgress: number; isConfirmingManualUpgrade: boolean; onChannelChange: (channel: ReleaseChannel) => void; onCheck: () => void; @@ -71,6 +72,7 @@ export function VersionUpgradeModal({ isChecking, isUpgrading, isUploadingBinary, + uploadProgress, isConfirmingManualUpgrade, onChannelChange, onCheck, @@ -290,6 +292,21 @@ export function VersionUpgradeModal({ )} + {isUploadingBinary ? ( +
+
+ 上传中... + {uploadProgress}% +
+
+
+
+
+ ) : null} + {manualErrorMessage ? (