diff --git a/cmd/arcrun-app/.version-source.json b/cmd/arcrun-app/.version-source.json index 5aacbec..384b84a 100644 --- a/cmd/arcrun-app/.version-source.json +++ b/cmd/arcrun-app/.version-source.json @@ -1,5 +1,6 @@ { "_algo": 2, "v0.18.17": "cb6e53a052b87482", - "v0.18.18": "a495451d9d9da78a" + "v0.18.18": "a495451d9d9da78a", + "v0.18.19": "b754d7ff97396794" } diff --git a/cmd/arcrun-app/app.go b/cmd/arcrun-app/app.go index 941337a..c880ff4 100644 --- a/cmd/arcrun-app/app.go +++ b/cmd/arcrun-app/app.go @@ -23,8 +23,10 @@ import ( "os" "os/exec" "path/filepath" + "regexp" runtime2 "runtime" "sort" + "strconv" "strings" "time" @@ -278,6 +280,39 @@ type UIFailItem struct { // maxFailShown=最多列幾份(多了會洗版;總數仍照實講)。 const maxFailShown = 8 +// parseRetryMessage 從退避訊息裡取出「試了幾次、多久後再試、原因是什麼」。 +// 格式來源=collector 的 retrySkipReason()。解析失敗就回 ok=false,由呼叫端原樣顯示。 +func parseRetryMessage(raw string) (n int, wait, reason string, ok bool) { + m := retryMsgRe.FindStringSubmatch(raw) + if m == nil { + return 0, "", "", false + } + n, _ = strconv.Atoi(m[1]) + wait = humanizeDuration(m[2]) + if i := strings.Index(raw, "|原因:"); i >= 0 { + reason = raw[i+len("|原因:"):] + } + return n, wait, reason, true +} + +var retryMsgRe = regexp.MustCompile(`上次失敗(第 (\d+) 次),([0-9hms.]+) 後重試`) + +// humanizeDuration 把 `5h38m38s` 講成「5 小時」——使用者不需要精確到秒。 +func humanizeDuration(d string) string { + dur, err := time.ParseDuration(d) + if err != nil { + return d + } + switch { + case dur >= time.Hour: + return fmt.Sprintf("%d 小時", int(dur.Hours())) + case dur >= time.Minute: + return fmt.Sprintf("%d 分鐘", int(dur.Minutes())) + default: + return "不到 1 分鐘" + } +} + // humanizeFailure 把 collector 的技術錯誤翻成使用者看得懂、而且**知道要不要行動**的一句話。 // // 🔴 leo 2026-08-06 實測,兩個真實案例(從他 Windows 的 collector.log 挖出來的): @@ -298,8 +333,22 @@ func humanizeFailure(raw string) string { return "這份 PDF 看起來是掃描的圖片,沒有文字可以讀 ⇒ 需要先做文字辨識(OCR),或換一份有文字的版本。" case strings.Contains(raw, "尚未支援的檔案格式"): return "這種檔案格式還讀不了 ⇒ 先另存成 PDF 或 Word(.docx)再放回同一個資料夾。" - case strings.Contains(raw, "後重試"): - return raw + "(上面是自動重試的排程,真正的原因是前一次失敗)" + } + + // 退避訊息:collector 給的是「上次失敗(第 5 次),5h38m38s 後重試|原因:…」。 + // + // 🔴 leo 2026-08-06:「**這個錯誤訊息我真的看不懂,明白說比較好**」 + // ——我原本只是把原文轉貼、再加一句「上面是自動重試的排程」,那是寫給我自己看的。 + // 使用者要的是三件事:**現在怎樣、為什麼、我要不要做什麼**。 + if n, wait, reason, ok := parseRetryMessage(raw); ok { + if reason != "" { + // 有原因就以原因為主,排程放後面當補充(他真正需要判斷的是原因) + return humanizeFailure(reason) + + fmt.Sprintf("(已經自動試過 %d 次,約 %s後會再試一次)", n, wait) + } + return fmt.Sprintf("已經自動試過 %d 次都沒成功,約 %s後會再試一次。"+ + "這個檔是在舊版失敗的,當時沒有記下原因;如果一直沒好,"+ + "到「版本與更新」旁邊把紀錄檔傳給我們。", n, wait) } return raw }