feat(t215): 每個知識庫顯示是否要更新,落後就給 install 頁連結
leo 08-08:「在每個知識庫會看到其他需要更新的,在每個知識庫上顯示是否要更新, 如果要,加開啓 install 頁的連結」。小幫手以前只提示自己(daemon 本體)要不要 更新,使用者連著多個雲端知識庫時完全看不出哪一個落後。 - collector/cloud_latest.go:EvalCloudUpdate + FetchLatestCloudRelease(30 分鐘節流), 判準與 portal 版本卡 loadVersion() 同一套(bundle_version vs install.arcrun.dev/ api/latest 的 release,semver 逐段整數比較),不是 t103 minCloudRelease 那把相容 底線,避免同一個知識庫在兩處得到相反答案。 - direct.go/sync_status.go:每輪同步順帶算好每個帳號的 CloudUpdateKnown/ CloudUpdateStale/CloudLatest,寫進 status.json。 - app.go:GetState 把這些欄位接進 UIAccount,供前端讀。 - main.js/style.css:首頁新增「知識庫版本」卡(每庫一行,落後才出現「前往安裝頁 更新」按鈕,帶 email 預填);側邊欄庫名旁加警示點;各庫頁也顯示同一行版本狀態。 查不到版本(連不上/latest 暫時查不到)一律誠實說「查不到」,不當成「已最新」。 已用假 window.go 在瀏覽器實測四種情境(落後/已最新/連不上/查得到 mine 但查不到 latest)+零知識庫的 onboarding 頁+深色模式,畫面與按鈕行為皆符合預期。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
// cloud_latest.go — t215(2026-08-08)「每個知識庫要不要更新」的全域比較基準。
|
||||
//
|
||||
// leo 原話:「在每個知識庫上顯示是否要更新,如果要,加開啓 install 頁的連結」。
|
||||
// 一個使用者可能連著不只一個雲端知識庫(帳號),每個帳號各自的雲端版本可能不同步——
|
||||
// 之前只有 t150 那套「小幫手自己」的更新提示,雲端側完全沒有對應的畫面。
|
||||
//
|
||||
// 🔴 判準**必須跟 portal 設定頁那張版本卡一致**(不能自己另立一套,見驗收要求):
|
||||
// `console-ui/public/portal/index.html` 的 `loadVersion()` 拿自己的 `bundle_version`
|
||||
// (cypher `/health`)比對 `install.arcrun.dev/api/latest` 的 `release` 欄位,
|
||||
// 用逐段整數比較(不是字串比較),非 semver 格式一律視為落後。
|
||||
// 這裡原樣照抄同一個比法與同一個資料源,**不是**沿用 cloud_version.go 的
|
||||
// `cloudVersionStale`——那支比的是 `minCloudRelease`(協定相容底線,「太舊會壞掉」),
|
||||
// 跟這裡要回答的「有沒有更新版可以裝」是兩個不同的問題,兩把尺不能混用,
|
||||
// 否則同一個知識庫會在小幫手與 portal 兩處得到相反答案。
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const installLatestURL = "https://install.arcrun.dev/api/latest"
|
||||
|
||||
// latestCacheTTL:install.arcrun.dev 自己在 CF edge 快取 5 分鐘(landing/worker.js
|
||||
// 同一顆端點的既有用法),daemon 端沒必要比它更頻繁去打;同步輪詢間隔常常只有十幾秒,
|
||||
// 若不節流,每個帳號每輪都會外打一次全域端點=不必要的高頻請求。30 分鐘一輪已經足夠
|
||||
// 讓使用者在「新版剛發佈」後半小時內看到提示。
|
||||
const latestCacheTTL = 30 * time.Minute
|
||||
|
||||
var (
|
||||
latestMu sync.Mutex
|
||||
latestCached string
|
||||
latestCachedOK bool
|
||||
latestFetched time.Time
|
||||
)
|
||||
|
||||
// fetchLatestCloudReleaseRaw 可在測試中替換為 stub(同 fetchCloudVersion 慣例)。
|
||||
var fetchLatestCloudReleaseRaw = func() (string, bool) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(installLatestURL)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var payload struct {
|
||||
Release string `json:"release"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 4096)).Decode(&payload); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if strings.TrimSpace(payload.Release) == "" {
|
||||
return "", false
|
||||
}
|
||||
return payload.Release, true
|
||||
}
|
||||
|
||||
// FetchLatestCloudRelease 回傳目前已知的「雲端最新版」,內建節流(見 latestCacheTTL)。
|
||||
// 節流窗內回快取值(含失敗快取=ok=false);窗口過了才真的重打一次。
|
||||
// 這是**全域單一值**(不分帳號)——所有知識庫比的是同一個「目前最新版是什麼」。
|
||||
func FetchLatestCloudRelease() (release string, ok bool) {
|
||||
latestMu.Lock()
|
||||
if time.Since(latestFetched) < latestCacheTTL {
|
||||
release, ok = latestCached, latestCachedOK
|
||||
latestMu.Unlock()
|
||||
return
|
||||
}
|
||||
latestMu.Unlock()
|
||||
|
||||
release, ok = fetchLatestCloudReleaseRaw()
|
||||
|
||||
latestMu.Lock()
|
||||
latestCached, latestCachedOK, latestFetched = release, ok, time.Now()
|
||||
latestMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// CloudUpdateStatus 是「這個知識庫要不要更新」的判定結果。
|
||||
// Known=false 時前端要照實講「查不到」,不能當成「已是最新」——
|
||||
// 靜默把「不知道」呈現成「一切正常」正是 cloud_version.go 開頭記過的那個坑。
|
||||
type CloudUpdateStatus struct {
|
||||
Known bool // 兩邊版本都拿得到才能下判斷
|
||||
NeedsUpdate bool // Known 且落後
|
||||
Mine string // 這個帳號目前的 bundle_version(可能是空字串或舊格式)
|
||||
Latest string // 已知的最新版(可能是空字串=暫時查不到)
|
||||
}
|
||||
|
||||
// EvalCloudUpdate 比較單一帳號的 bundle_version 與全域最新版。
|
||||
// 與 portal 版本卡 loadVersion() 的 cmpSemver 同一套邏輯:
|
||||
// - mine 拿不到、或 latest 拿不到 → Known=false(誠實說「查不到」)
|
||||
// - mine 不是 semver 格式(老實例的 YYYY-MM-DD+sha)→ 一律當落後
|
||||
// (新版才會寫 semver 進來,portal 端註解原話同此)
|
||||
// - 兩邊都是 semver → 逐段整數比較,mine < latest 才算落後
|
||||
func EvalCloudUpdate(mine string, mineOK bool, latest string, latestOK bool) CloudUpdateStatus {
|
||||
mine = strings.TrimSpace(mine)
|
||||
latest = strings.TrimSpace(latest)
|
||||
if !mineOK || mine == "" || !latestOK || latest == "" {
|
||||
return CloudUpdateStatus{Known: false, Mine: mine, Latest: latest}
|
||||
}
|
||||
behind := !isSemverLike(mine) || compareSemver(mine, latest) < 0
|
||||
return CloudUpdateStatus{Known: true, NeedsUpdate: behind, Mine: mine, Latest: latest}
|
||||
}
|
||||
Reference in New Issue
Block a user