From f8450815d364b72e50d87ee54902373715353dc4 Mon Sep 17 00:00:00 2001 From: richblack Date: Tue, 28 Jul 2026 16:15:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(t103):=20daemon=20=E5=81=B5=E6=B8=AC?= =?UTF-8?q?=E9=9B=B2=E7=AB=AF=E9=81=8E=E8=88=8A=EF=BC=8B=E6=89=98=E7=9B=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=85=A5=E5=8F=A3=E2=80=94=E2=80=94=E9=80=A3?= =?UTF-8?q?=E5=8B=95=E9=96=89=E7=92=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 每輪 GET /health 取 bundle_version(5s timeout 失敗靜默);空或日期(或純 YYYY-MM-DD) + date := strings.SplitN(version, "+", 2)[0] + return date < minCloudBuilt +} diff --git a/cloud_version_test.go b/cloud_version_test.go new file mode 100644 index 0000000..22b295b --- /dev/null +++ b/cloud_version_test.go @@ -0,0 +1,50 @@ +// cloud_version_test.go — t103 雲端版本判定函式 4 案單元測試。 +package main + +import ( + "os" + "testing" +) + +// TestMain 在所有測試執行前把 fetchCloudVersion 換成 stub(回 ("", false)), +// 避免各測試的假 httptest.Server 或離線環境因真實 /health 呼叫炸掉測試。 +// TestCloudVersionStale 直接測 cloudVersionStale 邏輯,不受此 stub 影響。 +func TestMain(m *testing.M) { + fetchCloudVersion = func(string) (string, bool) { return "", false } + os.Exit(m.Run()) +} + +// TestCloudVersionStale 驗證四種判定情境:空版本/過舊/正常/解析失敗。 +func TestCloudVersionStale(t *testing.T) { + cases := []struct { + name string + version string + checkOK bool + wantStale bool + }{ + { + // 老實例:/health 可達但 bundle_version 為空字串 → 需要更新 + name: "空版本(老實例)", version: "", checkOK: true, wantStale: true, + }, + { + // 日期早於 minCloudBuilt → 需要更新 + name: "過舊日期", version: "2026-07-27+abc1234", checkOK: true, wantStale: true, + }, + { + // 日期等於 minCloudBuilt → 正常(剛好達標) + name: "正常(剛好達標)", version: "2026-07-28+abc1234", checkOK: true, wantStale: false, + }, + { + // /health 不可達(解析失敗/逾時)→ 靜默不判定 + name: "解析失敗(/health 不可達)", version: "", checkOK: false, wantStale: false, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := cloudVersionStale(tc.version, tc.checkOK) + if got != tc.wantStale { + t.Errorf("cloudVersionStale(%q, %v) = %v,want %v", tc.version, tc.checkOK, got, tc.wantStale) + } + }) + } +} diff --git a/cmd/arcrun-tray/main.go b/cmd/arcrun-tray/main.go index 985f787..952d460 100644 --- a/cmd/arcrun-tray/main.go +++ b/cmd/arcrun-tray/main.go @@ -90,6 +90,28 @@ type traySyncStatus struct { Failures []trayExtFail `json:"failures,omitempty"` ExtractorOK bool `json:"extractor_ok"` ExtractorError string `json:"extractor_error,omitempty"` + // t103:雲端版本偵測 + CloudVersion string `json:"cloud_version,omitempty"` // collector 每輪 GET /health 取得的 bundle_version + CloudCheckOK bool `json:"cloud_check_ok"` // /health 可達才 true;false 不判定(靜默) +} + +// ── t103 雲端版本偵測 ────────────────────────────────────────────────────────── + +// trayMinCloudBuilt 是雲端最低相容建置日期;與 collector/cloud_version.go 的 minCloudBuilt 保持同值。 +// 雲端契約變更時兩處一起升。 +const trayMinCloudBuilt = "2026-07-28" + +// trayCloudVersionStale 判斷雲端是否需要更新(同 collector/cloudVersionStale 邏輯)。 +// checkOK=false(/health 不可達)回 false(靜默不判定)。 +func trayCloudVersionStale(version string, checkOK bool) bool { + if !checkOK { + return false + } + if version == "" { + return true + } + date := strings.SplitN(version, "+", 2)[0] + return date < trayMinCloudBuilt } type trayExtFail struct { @@ -665,6 +687,14 @@ func main() { }) items = append(items, failItem) } + // t103:雲端版本過舊(或老實例)時提示用戶更新,點了開瀏覽器到安裝頁。 + if trayCloudVersionStale(sync.CloudVersion, sync.CloudCheckOK) { + items = append(items, fyne.NewMenuItem("⚠ 知識庫需要更新(點我)", func() { + if u, err := neturl.Parse("https://install.arcrun.dev/"); err == nil { + _ = a.OpenURL(u) + } + })) + } items = append(items, fyne.NewMenuItemSeparator()) for _, f := range cfg.Folders() { folder := f // capture diff --git a/direct.go b/direct.go index 2329f43..a35c6dd 100644 --- a/direct.go +++ b/direct.go @@ -355,10 +355,14 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge // t91:每輪寫狀態檔(只有 extractor 模式才有意義的計數;direct 雲端萃模式 extracted_ok=0)。 if !dryRun && cfg.Manifest != "" { + // t103:每輪順手 GET /health 取雲端版本(5s timeout,失敗靜默)。 + cloudVer, cloudOK := fetchCloudVersion(cfg.CypherURL) st := SyncStatus{ LastSync: time.Now().Format(time.RFC3339), ExtractorOK: extractorOK, ExtractorError: extractorError, + CloudVersion: cloudVer, + CloudCheckOK: cloudOK, } if cfg.Extractor != "" { for _, r := range results { diff --git a/sync_status.go b/sync_status.go index d8c2552..4c15b9f 100644 --- a/sync_status.go +++ b/sync_status.go @@ -17,6 +17,9 @@ type SyncStatus struct { Failures []ExtractFail `json:"failures,omitempty"` // 失敗清單(路徑+白話原因) ExtractorOK bool `json:"extractor_ok"` // 萃取器本身是否就緒(預檢) ExtractorError string `json:"extractor_error,omitempty"` // 未就緒的白話原因 + // t103:雲端版本偵測(每輪 GET /health) + CloudVersion string `json:"cloud_version,omitempty"` // bundle_version 回傳值(空=老實例) + CloudCheckOK bool `json:"cloud_check_ok"` // /health 可達才為 true;false 代表網路失敗,不判定過舊 } // ExtractFail 記一筆萃取失敗(路徑+白話原因)。