Skip to content

Commit cfeff02

Browse files
committed
fix(update): 下载失败弹窗分类化,去掉超长 URL
之前 emitError 直接把 Go HTTP transport error 原样发到前端,GitHub LFS 重定向后的 release-assets URL 自带几百字符 SAS 签名 token,弹窗 被撑成超长一团没法看。 后端引入 classifyUpdateErr 把错误归到 9 类(network_timeout / tls_handshake / conn_failed / http_5xx / http_404 / checksum_mismatch / url_not_trusted / disk_io / unknown),emitError 只发 kind,原始 err 仍走 slog 日志。前端按 kind 查 i18n 取 title + hint 两行展示, 用户能区分是网络问题、GitHub 挂了还是包校验失败;安全相关 (checksum_mismatch / url_not_trusted) 得到正确强调。 i18n 全 11 个语言(zh-CN / zh-TW / en / fr-FR / ru / ja-JP / es / pt-BR / de / ko / it)都加了完整翻译。 classifyUpdateErr 表驱动测试覆盖 17 个 case。
1 parent b9d4d93 commit cfeff02

14 files changed

Lines changed: 294 additions & 20 deletions

File tree

backend/internal/facade/update_facade.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,14 +602,69 @@ func (f *UpdateFacade) emitReady(path string) {
602602
}
603603

604604
func (f *UpdateFacade) emitError(err error) {
605+
// 原始 err 写日志(保留 URL / SAS token 等技术细节供排查),UI 只收分类后的 kind。
606+
slog.Error("更新下载失败", "error", err.Error())
605607
if WailsContext == nil {
606608
return
607609
}
608610
wailsRuntime.EventsEmit(WailsContext, "update:error", map[string]any{
609-
"message": err.Error(),
611+
"kind": classifyUpdateErr(err),
610612
})
611613
}
612614

615+
// classifyUpdateErr 把下载链路上的各种 error 归到 UI 能展示的有限几类。
616+
// 顺序很重要:先识别结构化错误(httpStatusError / net.Error),再做字符串匹配
617+
// 兜底,避免 errors.As 链路被 fmt.Errorf 包装吞掉时漏判。
618+
func classifyUpdateErr(err error) string {
619+
if err == nil {
620+
return "unknown"
621+
}
622+
623+
var statusErr *httpStatusError
624+
if errors.As(err, &statusErr) {
625+
switch {
626+
case statusErr.code == http.StatusNotFound:
627+
return "http_404"
628+
case statusErr.code >= 500:
629+
return "http_5xx"
630+
}
631+
return "unknown"
632+
}
633+
634+
var netErr net.Error
635+
if errors.As(err, &netErr) && netErr.Timeout() {
636+
return "network_timeout"
637+
}
638+
639+
msg := err.Error()
640+
switch {
641+
case strings.Contains(msg, "TLS handshake"):
642+
return "tls_handshake"
643+
case strings.Contains(msg, "no such host"),
644+
strings.Contains(msg, "connection refused"),
645+
strings.Contains(msg, "connection reset"),
646+
strings.Contains(msg, "broken pipe"),
647+
strings.Contains(msg, "network is unreachable"):
648+
return "conn_failed"
649+
case strings.Contains(msg, "完整性校验"),
650+
strings.Contains(msg, "哈希不匹配"),
651+
strings.Contains(msg, "SHA256SUMS"):
652+
return "checksum_mismatch"
653+
case strings.Contains(msg, "拒绝下载"),
654+
strings.Contains(msg, "非预期的更新包"):
655+
return "url_not_trusted"
656+
case strings.Contains(msg, "创建临时文件"),
657+
strings.Contains(msg, "写入失败"),
658+
strings.Contains(msg, "关闭文件"),
659+
strings.Contains(msg, "计算本地哈希"):
660+
return "disk_io"
661+
case strings.Contains(msg, "timeout"),
662+
strings.Contains(msg, "deadline exceeded"):
663+
return "network_timeout"
664+
}
665+
return "unknown"
666+
}
667+
613668
// extRule 描述合法发布包后缀及其自更新优先级。
614669
type extRule struct {
615670
ext string

backend/internal/facade/update_facade_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,3 +732,44 @@ func TestDoDownload_CancelStopsRetry(t *testing.T) {
732732
}
733733
}
734734

735+
// ─── classifyUpdateErr:UI 弹窗按 kind 查 i18n,分类错了用户看到的就是错文案 ──
736+
737+
type fakeTimeoutErr struct{}
738+
739+
func (fakeTimeoutErr) Error() string { return "i/o timeout" }
740+
func (fakeTimeoutErr) Timeout() bool { return true }
741+
func (fakeTimeoutErr) Temporary() bool { return true }
742+
743+
func TestClassifyUpdateErr(t *testing.T) {
744+
cases := []struct {
745+
name string
746+
err error
747+
want string
748+
}{
749+
{"nil", nil, "unknown"},
750+
{"http_404", &httpStatusError{code: 404}, "http_404"},
751+
{"http_500", &httpStatusError{code: 500}, "http_5xx"},
752+
{"http_502", &httpStatusError{code: 502}, "http_5xx"},
753+
{"http_403", &httpStatusError{code: 403}, "unknown"},
754+
{"net_timeout", fakeTimeoutErr{}, "network_timeout"},
755+
{"tls_handshake", errors.New(`Get "https://x": net/http: TLS handshake timeout`), "tls_handshake"},
756+
{"no_such_host", errors.New("dial tcp: lookup x: no such host"), "conn_failed"},
757+
{"conn_refused", errors.New("dial tcp 1.2.3.4:443: connection refused"), "conn_failed"},
758+
{"conn_reset", errors.New("read tcp: connection reset by peer"), "conn_failed"},
759+
{"checksum", errors.New("完整性校验失败: 期望 abc, 实际 def"), "checksum_mismatch"},
760+
{"sums_missing", errors.New("SHA256SUMS 中未找到 x"), "checksum_mismatch"},
761+
{"url_blocked", errors.New("拒绝下载:非预期的更新包 URL: https://evil"), "url_not_trusted"},
762+
{"tmpfile", errors.New("创建临时文件失败: permission denied"), "disk_io"},
763+
{"write_fail", errors.New("写入失败: no space left on device"), "disk_io"},
764+
{"generic_timeout", errors.New("context deadline exceeded"), "network_timeout"},
765+
{"unknown_fallback", errors.New("something completely different"), "unknown"},
766+
}
767+
for _, tc := range cases {
768+
t.Run(tc.name, func(t *testing.T) {
769+
if got := classifyUpdateErr(tc.err); got != tc.want {
770+
t.Errorf("classifyUpdateErr(%v) = %q, want %q", tc.err, got, tc.want)
771+
}
772+
})
773+
}
774+
}
775+

frontend/src/layouts/MainLayout.vue

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ fill-rule="evenodd" clip-rule="evenodd"
290290
</div>
291291
<div v-else-if="updateState === 'error'" class="flex items-start gap-2 text-xs text-destructive">
292292
<ExclamationCircleIcon class="size-4 flex-shrink-0 mt-0.5" />
293-
<span class="break-all">{{ updateError }}</span>
293+
<div class="space-y-0.5">
294+
<div class="font-medium">{{ t(`update.error.${updateErrorKind}.title`) }}</div>
295+
<div class="text-muted-foreground text-[11px]">{{ t(`update.error.${updateErrorKind}.hint`) }}</div>
296+
</div>
294297
</div>
295298
</div>
296299

@@ -458,7 +461,7 @@ const updateState = ref<UpdateState>('idle')
458461
const downloadReceived = ref(0)
459462
const downloadTotal = ref(0)
460463
const downloadPercent = ref(0)
461-
const updateError = ref('')
464+
const updateErrorKind = ref('unknown')
462465
463466
const formatBytes = (n: number) => {
464467
if (!n || n <= 0) return '0 B'
@@ -782,17 +785,17 @@ const resetDownloadState = () => {
782785
downloadReceived.value = 0
783786
downloadTotal.value = 0
784787
downloadPercent.value = 0
785-
updateError.value = ''
788+
updateErrorKind.value = 'unknown'
786789
}
787790
788791
const startUpdate = async () => {
789792
resetDownloadState()
790793
updateState.value = 'downloading'
791794
try {
792795
await StartDownload()
793-
} catch (err: any) {
796+
} catch {
794797
updateState.value = 'error'
795-
updateError.value = String(err?.message || err)
798+
updateErrorKind.value = 'unknown'
796799
}
797800
}
798801
@@ -805,9 +808,9 @@ const applyUpdate = async () => {
805808
try {
806809
await ApplyUpdate()
807810
// 后端会自行重启应用,前端不需要额外处理
808-
} catch (err: any) {
811+
} catch {
809812
updateState.value = 'error'
810-
updateError.value = String(err?.message || err)
813+
updateErrorKind.value = 'unknown'
811814
}
812815
}
813816
@@ -924,7 +927,7 @@ onMounted(() => {
924927
})
925928
EventsOn('update:error', (payload: any) => {
926929
updateState.value = 'error'
927-
updateError.value = payload?.message || 'Unknown error'
930+
updateErrorKind.value = payload?.kind || 'unknown'
928931
})
929932
930933
// 原生菜单调用部署

frontend/src/locales/de.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "Neu starten",
4444
"retry": "Erneut versuchen",
4545
"readyToRestart": "Download abgeschlossen. Neu starten, um das Update anzuwenden.",
46-
"upToDate": "Bereits auf dem neuesten Stand"
46+
"upToDate": "Bereits auf dem neuesten Stand",
47+
"error": {
48+
"network_timeout": { "title": "Zeitüberschreitung", "hint": "Netzwerk instabil, bitte erneut versuchen" },
49+
"tls_handshake": { "title": "Sichere Verbindung fehlgeschlagen", "hint": "Firewall könnte GitHub blockieren" },
50+
"conn_failed": { "title": "Server nicht erreichbar", "hint": "Zugriff auf github.com prüfen" },
51+
"http_5xx": { "title": "GitHub vorübergehend nicht verfügbar", "hint": "Später erneut versuchen" },
52+
"http_404": { "title": "Download-Link abgelaufen", "hint": "Erneut nach Updates suchen" },
53+
"checksum_mismatch": { "title": "Paketprüfung fehlgeschlagen", "hint": "Datei gelöscht, bitte erneut versuchen" },
54+
"url_not_trusted": { "title": "Unerwartete Quelle", "hint": "Download aus Sicherheitsgründen blockiert" },
55+
"disk_io": { "title": "Lokaler Dateifehler", "hint": "Speicherplatz und Berechtigungen prüfen" },
56+
"unknown": { "title": "Download fehlgeschlagen", "hint": "Netzwerk prüfen und erneut versuchen" }
57+
}
4758
},
4859
"article": {
4960
"new": "Neu",

frontend/src/locales/en.json

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,45 @@
4343
"restart": "Restart",
4444
"retry": "Retry",
4545
"readyToRestart": "Download complete. Restart to apply update.",
46-
"upToDate": "You're on the latest version"
46+
"upToDate": "You're on the latest version",
47+
"error": {
48+
"network_timeout": {
49+
"title": "Download timed out",
50+
"hint": "Network may be slow or unstable. Please try again."
51+
},
52+
"tls_handshake": {
53+
"title": "Cannot establish secure connection",
54+
"hint": "Check your network — corporate networks, VPNs, or firewalls may block GitHub."
55+
},
56+
"conn_failed": {
57+
"title": "Cannot reach server",
58+
"hint": "Please check your network and ensure github.com is reachable."
59+
},
60+
"http_5xx": {
61+
"title": "GitHub service temporarily unavailable",
62+
"hint": "Please try again later."
63+
},
64+
"http_404": {
65+
"title": "Download link expired",
66+
"hint": "Please check for updates again."
67+
},
68+
"checksum_mismatch": {
69+
"title": "Package integrity check failed",
70+
"hint": "The file has been removed. Please retry. If the issue persists, report it to the developers."
71+
},
72+
"url_not_trusted": {
73+
"title": "Unexpected update source",
74+
"hint": "An unexpected download source was blocked. Please report this to the developers."
75+
},
76+
"disk_io": {
77+
"title": "Local file operation failed",
78+
"hint": "Please check disk space and write permissions."
79+
},
80+
"unknown": {
81+
"title": "Download failed",
82+
"hint": "Please check your network and try again."
83+
}
84+
}
4785
},
4886
"article": {
4987
"new": "New",

frontend/src/locales/es.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "Reiniciar",
4444
"retry": "Reintentar",
4545
"readyToRestart": "Descarga completa. Reinicia para aplicar la actualización.",
46-
"upToDate": "Ya estás al día"
46+
"upToDate": "Ya estás al día",
47+
"error": {
48+
"network_timeout": { "title": "Tiempo agotado", "hint": "Red inestable, inténtalo de nuevo" },
49+
"tls_handshake": { "title": "Conexión segura fallida", "hint": "Un firewall puede bloquear GitHub" },
50+
"conn_failed": { "title": "Servidor inaccesible", "hint": "Verifica el acceso a github.com" },
51+
"http_5xx": { "title": "Servicio GitHub no disponible", "hint": "Inténtalo más tarde" },
52+
"http_404": { "title": "Enlace caducado", "hint": "Vuelve a buscar actualizaciones" },
53+
"checksum_mismatch": { "title": "Verificación del paquete fallida", "hint": "Archivo eliminado, reinténtalo" },
54+
"url_not_trusted": { "title": "Origen inesperado", "hint": "Descarga bloqueada por seguridad" },
55+
"disk_io": { "title": "Error de archivo local", "hint": "Verifica espacio en disco y permisos" },
56+
"unknown": { "title": "Error de descarga", "hint": "Verifica la red e inténtalo de nuevo" }
57+
}
4758
},
4859
"article": {
4960
"new": "Nuevo",

frontend/src/locales/fr-FR.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "Redémarrer",
4444
"retry": "Réessayer",
4545
"readyToRestart": "Téléchargement terminé. Redémarrez pour appliquer la mise à jour.",
46-
"upToDate": "Vous êtes à jour"
46+
"upToDate": "Vous êtes à jour",
47+
"error": {
48+
"network_timeout": { "title": "Délai dépassé", "hint": "Réseau instable, veuillez réessayer" },
49+
"tls_handshake": { "title": "Connexion sécurisée impossible", "hint": "Un pare-feu peut bloquer GitHub" },
50+
"conn_failed": { "title": "Serveur inaccessible", "hint": "Vérifiez l'accès à github.com" },
51+
"http_5xx": { "title": "Service GitHub indisponible", "hint": "Veuillez réessayer plus tard" },
52+
"http_404": { "title": "Lien de téléchargement expiré", "hint": "Veuillez vérifier les mises à jour" },
53+
"checksum_mismatch": { "title": "Vérification du paquet échouée", "hint": "Fichier supprimé, veuillez réessayer" },
54+
"url_not_trusted": { "title": "Source inattendue", "hint": "Téléchargement bloqué pour sécurité" },
55+
"disk_io": { "title": "Erreur de fichier local", "hint": "Vérifiez l'espace disque et les permissions" },
56+
"unknown": { "title": "Échec du téléchargement", "hint": "Vérifiez le réseau et réessayez" }
57+
}
4758
},
4859
"article": {
4960
"new": "Nouveau",

frontend/src/locales/it.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "Riavvia",
4444
"retry": "Riprova",
4545
"readyToRestart": "Download completato. Riavvia per applicare l'aggiornamento.",
46-
"upToDate": "Sei già aggiornato"
46+
"upToDate": "Sei già aggiornato",
47+
"error": {
48+
"network_timeout": { "title": "Tempo scaduto", "hint": "Rete instabile, riprova" },
49+
"tls_handshake": { "title": "Connessione sicura fallita", "hint": "Un firewall potrebbe bloccare GitHub" },
50+
"conn_failed": { "title": "Server irraggiungibile", "hint": "Verifica accesso a github.com" },
51+
"http_5xx": { "title": "Servizio GitHub non disponibile", "hint": "Riprova più tardi" },
52+
"http_404": { "title": "Link scaduto", "hint": "Cerca di nuovo gli aggiornamenti" },
53+
"checksum_mismatch": { "title": "Verifica pacchetto fallita", "hint": "File rimosso, riprova" },
54+
"url_not_trusted": { "title": "Origine sospetta", "hint": "Download bloccato per sicurezza" },
55+
"disk_io": { "title": "Errore file locale", "hint": "Verifica spazio disco e permessi" },
56+
"unknown": { "title": "Download fallito", "hint": "Verifica la rete e riprova" }
57+
}
4758
},
4859
"article": {
4960
"new": "Nuovo",

frontend/src/locales/ja-JP.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "再起動",
4444
"retry": "再試行",
4545
"readyToRestart": "ダウンロード完了。再起動して更新を適用",
46-
"upToDate": "すでに最新バージョンです"
46+
"upToDate": "すでに最新バージョンです",
47+
"error": {
48+
"network_timeout": { "title": "ダウンロードがタイムアウト", "hint": "ネットワークが不安定です。再試行してください" },
49+
"tls_handshake": { "title": "セキュア接続に失敗", "hint": "ファイアウォール等で GitHub がブロックされている可能性" },
50+
"conn_failed": { "title": "サーバーに接続できません", "hint": "github.com への接続を確認してください" },
51+
"http_5xx": { "title": "GitHub サービス利用不可", "hint": "しばらくしてから再試行してください" },
52+
"http_404": { "title": "ダウンロードリンクが期限切れ", "hint": "更新を再確認してください" },
53+
"checksum_mismatch": { "title": "パッケージ検証失敗", "hint": "ファイルを削除しました。再試行してください" },
54+
"url_not_trusted": { "title": "不審な更新元", "hint": "ダウンロードがブロックされました" },
55+
"disk_io": { "title": "ローカルファイル操作エラー", "hint": "ディスク容量と権限を確認してください" },
56+
"unknown": { "title": "ダウンロード失敗", "hint": "ネットワークを確認して再試行してください" }
57+
}
4758
},
4859
"article": {
4960
"new": "新規記事",

frontend/src/locales/ko.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
"restart": "재시작",
4444
"retry": "다시 시도",
4545
"readyToRestart": "다운로드 완료. 재시작하여 업데이트 적용",
46-
"upToDate": "이미 최신 버전입니다"
46+
"upToDate": "이미 최신 버전입니다",
47+
"error": {
48+
"network_timeout": { "title": "다운로드 시간 초과", "hint": "네트워크 불안정, 다시 시도하세요" },
49+
"tls_handshake": { "title": "보안 연결 실패", "hint": "방화벽이 GitHub을 차단할 수 있습니다" },
50+
"conn_failed": { "title": "서버에 연결할 수 없음", "hint": "github.com 접속을 확인하세요" },
51+
"http_5xx": { "title": "GitHub 일시 중단", "hint": "잠시 후 다시 시도하세요" },
52+
"http_404": { "title": "다운로드 링크 만료", "hint": "업데이트를 다시 확인하세요" },
53+
"checksum_mismatch": { "title": "패키지 검증 실패", "hint": "파일이 삭제됨, 다시 시도하세요" },
54+
"url_not_trusted": { "title": "비정상 출처", "hint": "보안상 다운로드 차단됨" },
55+
"disk_io": { "title": "로컬 파일 오류", "hint": "디스크 공간과 권한 확인" },
56+
"unknown": { "title": "다운로드 실패", "hint": "네트워크 확인 후 다시 시도" }
57+
}
4758
},
4859
"article": {
4960
"new": "새 글",

0 commit comments

Comments
 (0)