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}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// cloud_latest_test.go — t215 單元測試:EvalCloudUpdate 的判準要跟 portal 版本卡一致。
|
||||
package collector
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEvalCloudUpdate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mine string
|
||||
mineOK bool
|
||||
latest string
|
||||
latestOK bool
|
||||
wantKnown bool
|
||||
wantUpdate bool
|
||||
}{
|
||||
{
|
||||
name: "兩邊都拿得到、mine 落後", mine: "1.4.1", mineOK: true, latest: "1.4.2", latestOK: true,
|
||||
wantKnown: true, wantUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "兩邊都拿得到、已是最新", mine: "1.4.2", mineOK: true, latest: "1.4.2", latestOK: true,
|
||||
wantKnown: true, wantUpdate: false,
|
||||
},
|
||||
{
|
||||
// 字串比較會誤判 "1.10.0" < "1.9.0";逐段整數比較才對(同 t103 迴歸守衛)。
|
||||
name: "1.10.0 比 1.9.0 新,不該判落後", mine: "1.10.0", mineOK: true, latest: "1.9.0", latestOK: true,
|
||||
wantKnown: true, wantUpdate: false,
|
||||
},
|
||||
{
|
||||
// 老格式(YYYY-MM-DD+sha)——portal 版本卡註解原話:「這種情況一律當成落後」。
|
||||
name: "老格式版本一律當落後", mine: "2026-07-31+8e83589", mineOK: true, latest: "1.4.2", latestOK: true,
|
||||
wantKnown: true, wantUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "連不上這個知識庫(mineOK=false)→ 查不到,不能裝沒事", mine: "", mineOK: false, latest: "1.4.2", latestOK: true,
|
||||
wantKnown: false, wantUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "查得到 mine 但暫時查不到最新版 → 查不到,不是已最新", mine: "1.4.2", mineOK: true, latest: "", latestOK: false,
|
||||
wantKnown: false, wantUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "/health 可達但 bundle_version 空字串(老實例)→ 查不到", mine: "", mineOK: true, latest: "1.4.2", latestOK: true,
|
||||
wantKnown: false, wantUpdate: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := EvalCloudUpdate(tc.mine, tc.mineOK, tc.latest, tc.latestOK)
|
||||
if got.Known != tc.wantKnown {
|
||||
t.Errorf("Known = %v,want %v", got.Known, tc.wantKnown)
|
||||
}
|
||||
if got.NeedsUpdate != tc.wantUpdate {
|
||||
t.Errorf("NeedsUpdate = %v,want %v", got.NeedsUpdate, tc.wantUpdate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFetchLatestCloudReleaseThrottle 驗證節流:窗口內第二次呼叫不重打 fetchLatestCloudReleaseRaw。
|
||||
func TestFetchLatestCloudReleaseThrottle(t *testing.T) {
|
||||
calls := 0
|
||||
orig := fetchLatestCloudReleaseRaw
|
||||
defer func() {
|
||||
fetchLatestCloudReleaseRaw = orig
|
||||
latestMu.Lock()
|
||||
latestCached, latestCachedOK, latestFetched = "", false, latestFetched.Add(-2*latestCacheTTL)
|
||||
latestMu.Unlock()
|
||||
}()
|
||||
fetchLatestCloudReleaseRaw = func() (string, bool) { calls++; return "1.4.2", true }
|
||||
// 強制第一次一定重打(避開其他測試留下的快取)。
|
||||
latestMu.Lock()
|
||||
latestFetched = latestFetched.Add(-2 * latestCacheTTL)
|
||||
latestMu.Unlock()
|
||||
|
||||
r1, ok1 := FetchLatestCloudRelease()
|
||||
r2, ok2 := FetchLatestCloudRelease()
|
||||
if calls != 1 {
|
||||
t.Errorf("節流窗口內第二次呼叫不該重打,calls = %d", calls)
|
||||
}
|
||||
if r1 != "1.4.2" || !ok1 || r2 != "1.4.2" || !ok2 {
|
||||
t.Errorf("兩次結果應相同,got (%q,%v) (%q,%v)", r1, ok1, r2, ok2)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
// TestCloudVersionStale 直接測 cloudVersionStale 邏輯,不受此 stub 影響。
|
||||
func TestMain(m *testing.M) {
|
||||
fetchCloudVersion = func(string) (string, bool) { return "", false }
|
||||
// t215:同一個理由——避免 EvalCloudUpdate 相關測試因為真的打了
|
||||
// install.arcrun.dev 而變成看網路臉色的測試。
|
||||
fetchLatestCloudReleaseRaw = func() (string, bool) { return "", false }
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
|
||||
+24
-2
@@ -96,6 +96,11 @@ type syncStatus struct {
|
||||
// 不重新定義結構,避免兩邊的欄位定義漂移。
|
||||
Progress collector.SyncProgress `json:"progress"`
|
||||
FailureBreakdown collector.FailureBreakdown `json:"failure_breakdown"`
|
||||
// t215(2026-08-08):per-account 雲端版本狀態——collector.AccountSyncStatus 已經是
|
||||
// GetState 要的形狀(含 t215 新欄位 CloudUpdateKnown/CloudUpdateStale/CloudLatest),
|
||||
// 直接原樣接住,不重新定義一份會漂移的結構。key = instanceHostOf(cypher_url)
|
||||
// (與 UIAccount.Host 同一套算法,見 shortHost)。
|
||||
AccountDetails map[string]collector.AccountSyncStatus `json:"account_details,omitempty"`
|
||||
}
|
||||
|
||||
type skippedDoc struct {
|
||||
@@ -219,6 +224,15 @@ type UIAccount struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Folders []UIFolder `json:"folders"`
|
||||
// t215(2026-08-08,leo:「在每個知識庫上顯示是否要更新,如果要,加開啓 install 頁的
|
||||
// 連結」)——一個使用者可能連著不只一個知識庫,之前只有小幫手自己的版本會提示更新,
|
||||
// 每個知識庫各自的雲端版本完全沒有畫面。判準與 portal 版本卡同一套
|
||||
// (collector.EvalCloudUpdate,不是 t103 的相容底線),這裡只翻成人話,不重新判斷。
|
||||
CloudVerKnown bool `json:"cloudVerKnown"` // false=查不到(連不上/還沒查過),前端要老實說「查不到」
|
||||
CloudVerStale bool `json:"cloudVerStale"` // true=有新版可更新
|
||||
CloudVerMine string `json:"cloudVerMine,omitempty"` // 這個知識庫目前的版本(可能連 Known=false 時也有值)
|
||||
CloudVerLatest string `json:"cloudVerLatest,omitempty"` // 已知的最新版
|
||||
Email string `json:"email,omitempty"` // 供「前往安裝頁更新」預填帳號(同 portal 版本卡的做法)
|
||||
}
|
||||
type UIState struct {
|
||||
Version string `json:"version"`
|
||||
@@ -415,11 +429,20 @@ func (a *App) GetState() UIState {
|
||||
}
|
||||
}
|
||||
|
||||
sync := loadSyncStatus()
|
||||
for i, acc := range cfg.Accounts {
|
||||
ui := UIAccount{Name: accountName(acc), Host: shortHost(acc.CypherURL)}
|
||||
ui := UIAccount{Name: accountName(acc), Host: shortHost(acc.CypherURL), Email: acc.Email}
|
||||
for _, f := range acc.WatchFolders {
|
||||
ui.Folders = append(ui.Folders, UIFolder{Path: f, AccIdx: i})
|
||||
}
|
||||
// t215:per-account 雲端版本狀態——key 與 Host 同一套算法(shortHost),
|
||||
// 對應 collector 寫入 status.json 時用的 instanceHostOf(兩者對一般 https URL 同值)。
|
||||
if accSt, ok := sync.AccountDetails[ui.Host]; ok {
|
||||
ui.CloudVerKnown = accSt.CloudUpdateKnown
|
||||
ui.CloudVerStale = accSt.CloudUpdateStale
|
||||
ui.CloudVerMine = accSt.CloudVersion
|
||||
ui.CloudVerLatest = accSt.CloudLatest
|
||||
}
|
||||
st.Accounts = append(st.Accounts, ui)
|
||||
}
|
||||
|
||||
@@ -431,7 +454,6 @@ func (a *App) GetState() UIState {
|
||||
st.GeminiKey = "••••••••"
|
||||
}
|
||||
|
||||
sync := loadSyncStatus()
|
||||
st.Syncing, st.StatusBig, st.StatusSub = describeStatus(sync)
|
||||
st.Steps = buildSteps(sync, st.Syncing)
|
||||
st.Skipped = buildSkipped(sync)
|
||||
|
||||
@@ -37,6 +37,16 @@ $('overlay').addEventListener('click', (e) => { if (e.target.id === 'overlay') c
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); });
|
||||
|
||||
// ── 側邊欄:每個知識庫一項(leo:「每個帳號有獨立的一個頁面」)──
|
||||
//
|
||||
// t215(2026-08-08,leo:「在每個知識庫上顯示是否要更新」):落後的庫名旁加一顆
|
||||
// 警示點,逛清單時不用點進每個庫就能一眼看出哪個落後(完整說明+更新按鈕在
|
||||
// 首頁 cardKbVersions 與各庫頁 kbVersionLine)。
|
||||
//
|
||||
// 🔴 頂層 status.md 08-08 深夜記過一個**待 leo confirm、尚未定案**的提案:
|
||||
// 「單一更新入口(版本白癡化)」——把小幫手自我更新與雲端知識庫更新合併成一顆按鈕。
|
||||
// 那個提案沒有否定「每庫獨立列出落後狀態」這件事本身(它本來就要「點進去才看細節」),
|
||||
// 只是問「總覽要不要合併」。這裡先實作 leo 這次明確要的「每庫看得到+連得到」,
|
||||
// 之後若那個提案 confirm,是在這層之上疊總覽 pill,不是重做這裡。
|
||||
function renderNav() {
|
||||
const accs = (state && state.accounts) || [];
|
||||
$('nav').innerHTML = `
|
||||
@@ -45,6 +55,7 @@ function renderNav() {
|
||||
${accs.map((a, i) => `
|
||||
<div class="nav" data-p="lib:${i}">
|
||||
<span class="ic">▤</span><span class="nm">${esc(a.name)}</span>
|
||||
${a.cloudVerStale ? `<span class="dot warn" title="有新版可更新"></span>` : ''}
|
||||
<span class="cnt">${(a.folders || []).length}</span>
|
||||
</div>`).join('')}
|
||||
<div class="sec">設定</div>
|
||||
@@ -75,6 +86,7 @@ function pageHome(s) {
|
||||
${cardTrouble(s)}
|
||||
${cardProgress(s.progress)}
|
||||
${cardSkipped(s.skipped)}
|
||||
${cardKbVersions(s)}
|
||||
<div class="card">
|
||||
<h3>總計</h3>
|
||||
<div class="kv" style="margin-top:10px">
|
||||
@@ -84,6 +96,60 @@ function pageHome(s) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// t215(2026-08-08,leo:「在每個知識庫上顯示是否要更新,如果要,加開啓 install 頁的
|
||||
// 連結」)——一個使用者可能連著不只一個知識庫(leo 自己就是),各自雲端版本不同步時,
|
||||
// 以前完全看不出「哪一個」落後、也沒有地方按。這張卡讓使用者不必逐個庫點進去,
|
||||
// 首頁一眼看完全部知識庫的版本狀態。
|
||||
//
|
||||
// 判準**不在這裡重新發明**:後端 collector.EvalCloudUpdate 與 portal 設定頁那張版本卡
|
||||
// (console-ui/public/portal/index.html 的 loadVersion())同一套比法——自己的
|
||||
// bundle_version 比 install.arcrun.dev/api/latest 的 release,兩邊都是 semver 才逐段
|
||||
// 整數比較,非 semver(老格式)一律視為落後。前端只負責把後端已經算好的
|
||||
// cloudVerKnown/cloudVerStale 翻成人話,不做任何版本比較。
|
||||
function cardKbVersions(s) {
|
||||
const accs = s.accounts || [];
|
||||
if (!accs.length) return '';
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>知識庫版本</h3>
|
||||
<div class="kblist">
|
||||
${accs.map((a) => `
|
||||
<div class="kbrow">
|
||||
<span class="nm">${esc(a.name)}</span>
|
||||
<span class="right">${kbVersionLine(a)}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// kbVersionLine:單一知識庫的版本判定文案,首頁卡與各庫頁共用同一份翻譯,
|
||||
// 不讓兩處各寫各的文字(那樣才真的會出現「兩處講不同的話」)。
|
||||
//
|
||||
// 三種情況都要照實講(cloud_version.go 記過的坑:把「查不到」呈現成「一切正常」):
|
||||
// ① Known 且落後 → 紅字+按鈕,按下去帶 email 開 install 頁(同 portal 版本卡的預填做法)
|
||||
// ② Known 且已最新 → 淡字「已是最新版」
|
||||
// ③ 不 Known → 照原因分兩句:連不上這個知識庫/查得到目前版本但暫時查不到最新版
|
||||
function kbVersionLine(a) {
|
||||
if (!a.cloudVerKnown) {
|
||||
const detail = a.cloudVerMine
|
||||
? `已知版本 ${esc(a.cloudVerMine)},暫時查不到最新版本(稍後會自動再查)`
|
||||
: '目前連不上這個知識庫,查不到版本';
|
||||
return `<span class="d">${detail}</span>`;
|
||||
}
|
||||
if (a.cloudVerStale) {
|
||||
return `<span class="warn">有新版可更新(目前 ${esc(a.cloudVerMine)} → 最新 ${esc(a.cloudVerLatest)})</span>
|
||||
<button class="ghost" data-updatekb="${esc(a.email || '')}">前往安裝頁更新</button>`;
|
||||
}
|
||||
return `<span class="d">已是最新版(${esc(a.cloudVerMine)})</span>`;
|
||||
}
|
||||
|
||||
// installURLFor:與 portal 版本卡同一個做法——落後才需要按,按下去帶 email 讓安裝頁
|
||||
// 預填,既有實例更新免辨識碼(安裝器 t154),不必讓使用者自己去 install.arcrun.dev 找。
|
||||
function installURLFor(email) {
|
||||
const base = 'https://install.arcrun.dev/';
|
||||
return email ? base + '?email=' + encodeURIComponent(email) : base;
|
||||
}
|
||||
|
||||
// 🔴 G-6.2「不准安靜地略過」(2026-08-06)——J-1/S6 考題的後半句:
|
||||
// 「Then 我一樣找得到——**或當場被告知這種檔案還不支援**」
|
||||
// 以前 .doc/.pages 這類檔在 collector 掃描時就被丟掉,畫面上一個字都沒有,
|
||||
@@ -172,6 +238,7 @@ function pageLib(s, idx) {
|
||||
<button data-portal="${esc(portal)}">開啟知識庫網頁</button>
|
||||
<button class="primary" data-addto="${idx}">加入資料夾</button>
|
||||
</div>
|
||||
<div class="kbver">${kbVersionLine(a)}</div>
|
||||
${(a.folders || []).map((f) => `
|
||||
<div class="folder">
|
||||
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
|
||||
@@ -291,6 +358,9 @@ function wire() {
|
||||
on('uDiag', exportDiagnostics);
|
||||
document.querySelectorAll('[data-portal]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.portal); });
|
||||
document.querySelectorAll('[data-openurl]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.openurl); });
|
||||
document.querySelectorAll('[data-updatekb]').forEach((b) => {
|
||||
b.onclick = () => go.OpenURL(installURLFor(b.dataset.updatekb));
|
||||
});
|
||||
document.querySelectorAll('[data-addto]').forEach((b) => { b.onclick = () => addFolder(Number(b.dataset.addto)); });
|
||||
document.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
|
||||
|
||||
@@ -247,3 +247,21 @@ input[type=text], input[type=password] {
|
||||
}
|
||||
.breaklist li:first-child { border-top: 0; }
|
||||
.breaklist li span:last-child { color: rgba(var(--ink-rgb), .55); white-space: nowrap; }
|
||||
|
||||
/* t215(2026-08-08,leo:「在每個知識庫上顯示是否要更新」)——首頁「知識庫版本」卡、
|
||||
各庫頁版本列、側邊欄警示點,三處共用同一組顏色:落後=--err(同 .err 那個紅),
|
||||
不是額外發明一個「警告色」。 */
|
||||
.kblist { display: flex; flex-direction: column; gap: 2px; margin-top: 10px; }
|
||||
.kbrow {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 9px 0; border-top: 1px solid rgba(var(--ink-rgb), .08);
|
||||
}
|
||||
.kbrow:first-child { border-top: 0; }
|
||||
.kbrow .nm { font-size: 14px; font-weight: 600; flex: none; }
|
||||
.kbrow .right { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.kbver { margin-bottom: 14px; font-size: 13.5px; display: flex; align-items: center; gap: 10px; }
|
||||
.warn { color: var(--err); font-size: 13px; }
|
||||
#side .nav .dot.warn {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex: none;
|
||||
background: var(--err); margin-left: 4px;
|
||||
}
|
||||
|
||||
@@ -582,6 +582,10 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
var totalProgress SyncProgress
|
||||
var stuckReasons []string
|
||||
|
||||
// t215:全域「雲端最新版」只抓一次(自帶節流,見 cloud_latest.go)——
|
||||
// 這是所有帳號共用的同一把尺,不是逐帳號各打一次。
|
||||
latestRelease, latestOK := FetchLatestCloudRelease()
|
||||
|
||||
accountDetails := map[string]AccountSyncStatus{}
|
||||
for _, acc := range accounts {
|
||||
if acc.CypherURL == "" || acc.Namespace == "" {
|
||||
@@ -592,10 +596,15 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
|
||||
// t103:per-account 雲端版本偵測
|
||||
cloudVer, cloudOK := fetchCloudVersion(accCfg.CypherURL)
|
||||
// t215:這個帳號要不要更新——與 portal 版本卡同一套判準(見檔頭)。
|
||||
cloudUpd := EvalCloudUpdate(cloudVer, cloudOK, latestRelease, latestOK)
|
||||
accSt := AccountSyncStatus{
|
||||
LastSync: time.Now().Format(time.RFC3339),
|
||||
CloudVersion: cloudVer,
|
||||
CloudCheckOK: cloudOK,
|
||||
LastSync: time.Now().Format(time.RFC3339),
|
||||
CloudVersion: cloudVer,
|
||||
CloudCheckOK: cloudOK,
|
||||
CloudUpdateKnown: cloudUpd.Known,
|
||||
CloudUpdateStale: cloudUpd.NeedsUpdate,
|
||||
CloudLatest: cloudUpd.Latest,
|
||||
}
|
||||
|
||||
// 🔴 t182(leo 08-04):「會去**掃一次**看雲端是否裝好,**沒裝好就顯示 workers AI
|
||||
|
||||
+12
-5
@@ -11,11 +11,18 @@ import (
|
||||
// AccountSyncStatus 彙總單一帳號的每輪同步結果(t104 多帳號看守)。
|
||||
// key in SyncStatus.AccountDetails = instanceHostOf(cypher_url)。
|
||||
type AccountSyncStatus struct {
|
||||
LastSync string `json:"last_sync,omitempty"`
|
||||
CloudVersion string `json:"cloud_version,omitempty"` // t103 per-account
|
||||
CloudCheckOK bool `json:"cloud_check_ok"`
|
||||
ExtractedOK int `json:"extracted_ok"`
|
||||
ExtractFailed int `json:"extract_failed"`
|
||||
LastSync string `json:"last_sync,omitempty"`
|
||||
CloudVersion string `json:"cloud_version,omitempty"` // t103 per-account
|
||||
CloudCheckOK bool `json:"cloud_check_ok"`
|
||||
// t215(2026-08-08,leo:「在每個知識庫上顯示是否要更新」):這個帳號的雲端知識庫
|
||||
// 有沒有新版可以裝——與 portal 版本卡同一套判準(EvalCloudUpdate,cloud_latest.go),
|
||||
// **不是**上面 CloudVersion/CloudCheckOK 搭配 t103 cloudVersionStale 那把尺
|
||||
// (那把量的是「太舊會不相容」的協定底線,這裡量的是「有沒有更新版可以裝」)。
|
||||
CloudUpdateKnown bool `json:"cloud_update_known"`
|
||||
CloudUpdateStale bool `json:"cloud_update_stale"`
|
||||
CloudLatest string `json:"cloud_latest,omitempty"` // 已知的最新版(供畫面顯示「最新版 x.y.z」)
|
||||
ExtractedOK int `json:"extracted_ok"`
|
||||
ExtractFailed int `json:"extract_failed"`
|
||||
// t182(leo 08-04:「沒裝好就顯示 workers AI 還沒通,一旦通了就顯示可用」):
|
||||
// 這個帳號的雲端實例有沒有 /portal/daemon/extract。**逐帳號**各自記——
|
||||
// 用戶可能有多個實例、更新進度不同步。只在走 workers-ai 這條路時探測。
|
||||
|
||||
Reference in New Issue
Block a user