Files
arcrun-collector/live_status.go
T

187 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
}
}