sync: collector/ 同步自 inkstone/arcrun-rag@10eb2cb(桌面小幫手 0.18.53)

This commit is contained in:
2026-09-14 07:05:34 +08:00
parent bce422f6fe
commit e4327ecd37
13 changed files with 820 additions and 22 deletions
+186
View File
@@ -0,0 +1,186 @@
// live_status.go — 一輪還沒跑完,畫面也要知道做到哪(`inkstone/arcrun-rag#200`)。
//
// 🔴 病(2026-09-13 leo 截圖,小幫手 0.18.52):
// 「同步中… 正在讀檔並整理成知識卡」,ISEP 每一層都是 0 / N,leo:「它沒有跑任何一個東西」。
// 實查同一時段:
//
// status.json 最後修改 12:00(上一個版本收工那次),之後一次都沒寫
// folder-trees.json ISEP 那棵的 generated_at 是 23:38**開始處理之前**),synced 全部 0
// ISEP 的 manifest 23:53 已經記下 11 份送上雲端;AR-Mira 查得到 isep/README.md 的卡
//
// ⇒ 不是沒跑,是**跑了但一個字都沒回寫**。兩份給畫面看的檔都只在固定時點寫:
//
// ① status.json 只在整輪收工時寫——三個帳號、每發雲端 24〜57 秒,一輪是好幾個小時
// ② 樹只在「掃描完、開始送之前」寫一次(#153 第二輪搬的),那時分子必然還是 0;
// 收工那次合併用的也是**同一棵開工前的樹**,所以就算整輪跑完,數字還是 0。
//
// 這支檔只做一件事:讓 roundGuard(本來就跟著整輪走、每個帳號每個資料夾共用的那一份)
// 在**事情發生的當下**把「做到哪」寫進 status.json。數字本身不另算:
// 資料夾進度是 (*Manifest).Progress() 的原件,樹是 BuildFolderTree 的原件——
// 與收工那次同一個函式,只是早一點寫。
package collector
import "time"
// folderSnapshot=某個看守資料夾此刻的進度(寫進 status.json 的 folder_progress)。
type folderSnapshot struct {
root string
progress SyncProgress
}
// beginRound 標記「一輪開始了」,並記住狀態檔在哪。statusPath 空=不寫(dry-run/沒有 manifest)。
func (g *roundGuard) beginRound(statusPath string, now time.Time) {
if g == nil || statusPath == "" {
return
}
g.mu.Lock()
g.statusPath = statusPath
g.live = RoundProgress{StartedAt: now.Format(time.RFC3339)}
g.mu.Unlock()
g.persistLive(nil)
}
// enterFolder 標記「現在輪到哪個知識庫的哪個資料夾」。folder 空=帳號層的事(探測雲端 AI 之類)。
func (g *roundGuard) enterFolder(account, folder string) {
if g == nil {
return
}
g.mu.Lock()
g.live.Account, g.live.Folder = account, folder
g.live.Step, g.live.StepSince, g.live.Waiting = "", "", nil
g.mu.Unlock()
g.persistLive(nil)
}
// stepStarted 標記「開始做某件雲端呼叫」。由 openGate 呼叫——每一發雲端呼叫都經過那道閘,
// 所以不必在每個呼叫端各自記得寫(「忘了接」這個失敗模式不該存在)。
func (g *roundGuard) stepStarted(step callStep, at time.Time) {
if g == nil {
return
}
g.mu.Lock()
g.live.Step = step.Name
g.live.StepSince = at.Format(time.RFC3339)
g.live.Waiting = nil
g.mu.Unlock()
g.persistLive(nil)
}
// noteWaiting 標記「那件事已經等很久了」——這就是「卡在哪一步」。
func (g *roundGuard) noteWaiting(s StalledCall) {
if g == nil {
return
}
g.mu.Lock()
w := s
g.live.Waiting = &w
g.mu.Unlock()
g.persistLive(nil)
}
// stepDone 那件事回來了:如果畫面上正掛著「還在等它」,拿掉。
func (g *roundGuard) stepDone(step callStep) {
if g == nil {
return
}
g.mu.Lock()
clear := g.live.Waiting != nil && g.live.Waiting.Step == step.Name
if clear {
g.live.Waiting = nil
}
g.mu.Unlock()
if clear {
g.persistLive(nil)
}
}
// fileFinished 一份檔處理完了(outcomeingestedfailed/其他)。
// progress=那個資料夾此刻的 (*Manifest).Progress()。
func (g *roundGuard) fileFinished(root, outcome string, progress SyncProgress) {
if g == nil {
return
}
g.mu.Lock()
switch outcome {
case "ingested":
g.live.Ingested++
case "failed":
g.live.Failed++
}
g.live.Waiting = nil
g.mu.Unlock()
g.persistLive(&folderSnapshot{root: root, progress: progress})
}
// persistStalls 把「這一輪等太久的事」立刻寫進去(strike 呼叫)。
func (g *roundGuard) persistStalls() { g.persistLive(nil) }
// finishRound 封口:之後任何途中寫入都不再動 status.json。
//
// 🔴 一定要在收工那次 SaveSyncStatus **之前**呼叫。播報「還在等」的是另一條
// goroutine,它可能比收工晚一點點才寫——沒有封口的話,收工剛寫好的那份會被
// 一格過時的 in_round 蓋回去,畫面就永遠停在「同步中」。
// 封口的判斷做在 statusFileMu 裡(見 persistLive),所以不存在「檢查完才被封口」的縫。
func (g *roundGuard) finishRound() {
if g == nil {
return
}
g.mu.Lock()
g.closed = true
g.mu.Unlock()
}
// persistLive 把此刻的「做到哪」寫進 status.json;fs 非 nil 時一併更新那個資料夾的進度。
// 寫不進去一律吞掉:這是給畫面看的,不能擋住同步本體。
func (g *roundGuard) persistLive(fs *folderSnapshot) {
if g == nil {
return
}
g.mu.Lock()
path, closed := g.statusPath, g.closed
g.mu.Unlock()
if path == "" || closed {
return
}
_ = updateSyncStatus(path, func(s *SyncStatus) bool {
g.mu.Lock()
defer g.mu.Unlock()
if g.closed {
return false
}
live := g.live
live.UpdatedAt = time.Now().Format(time.RFC3339)
if live.Waiting != nil {
w := *live.Waiting
live.Waiting = &w
}
s.InRound = &live
// 這一輪到目前為止等太久的事(同 Stalls 的語意:只講這一輪,不帶上一輪的)。
s.Stalls = append([]StalledCall(nil), g.stalls...)
if fs != nil {
if s.FolderProgress == nil {
s.FolderProgress = map[string]SyncProgress{}
}
// 首頁那行總量=各資料夾加總+讀不了的檔。換掉這個資料夾的那一份,
// 其他資料夾與 Unreadable 原樣保留——不另算一套總量。
if old, ok := s.FolderProgress[fs.root]; ok {
s.Progress = progressMinus(s.Progress, old)
}
s.FolderProgress[fs.root] = fs.progress
s.Progress = s.Progress.Add(fs.progress)
}
return true
})
}
// progressMinusSyncProgress.Add 的反向(只在換掉某個資料夾那一份時用)。
func progressMinus(p, o SyncProgress) SyncProgress {
return SyncProgress{
Total: p.Total - o.Total,
Done: p.Done - o.Done,
Pending: p.Pending - o.Pending,
Stuck: p.Stuck - o.Stuck,
Unreadable: p.Unreadable - o.Unreadable,
Failing: p.Failing - o.Failing,
}
}