Files
arcrun-collector/progress.go
T
Leo ef8126201c P8 短板齊平:模型品質實測(granite 否決、qwen3-30b 定案)+額度撞牆三句話接上畫面
① docs/benchmarks/p8-extractor-quality/:leo 真筆記 8 篇 × 5 模型可複驗實測
   (production 同 prompt/參數、CF 原生 usage.neurons 計量)——
   scout 84 n/檔(119 檔/日)→ qwen3-30b 43 n/檔(232 檔/日)品質不降反升;
   granite 最便宜(858 檔/日)但 5/8 缺段、三元組全滅=否決。
   誠實結論:免金鑰路物理撐不起「幾千篇當天匯入」(差 13 倍),
   正解=消化節奏(3,000 篇約 13 天背景跑完、新筆記優先)+出口(Gemini/付費/ollama)。
② 撞牆體驗:quota_message 08-07 就在 status.json,App 從來沒接——
   app.go pickQuotaNotice(過期快照不說謊)+ main.js cardQuota(三句話卡)+
   progress.go ClassifyFailure 補認三句話(原本掉「其他」)。
   Go 兩 module 全綠;瀏覽器實載 dist 深淺兩主題截圖、console 0 錯。
③ 公開鏡像排除 docs/benchmarks(輸入是 leo 私人筆記)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:44:34 +08:00

146 lines
7.1 KiB
Go
Raw Permalink 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.
// progress.go — 「你有幾份、我做完幾份、卡住的是哪幾類」(2026-08-08,Evan 封測 + leo 重新定形)。
//
// 🔴 這一層同時服務兩件事,所以獨立成檔:
//
// t210 首頁統計(使用者每天看的)
// t213 診斷檔(出事時唯一能拿到的東西)
//
// 兩邊必須是**同一組數字**——不然又會出現「畫面說 20、檔案說別的」。
//
// ── 為什麼要有分母 ────────────────────────────────────────────────────────
// leo 轉述 Evan:「我有 9000 個檔,雲端只有 101 張卡,畫面卻說『20 份沒送進知識庫』
// ——這幾個數字到底是怎麼回事?是壞了嗎?還是繼續在跑?」
// 病根:首頁每個數字都是**本輪**的(單輪上限 25 筆),使用者問的是**總量**的。
// 兩者永遠對不起來 ⇒ 他無法判斷「還在跑」還是「壞了」=會不會來找客服的分水嶺。
//
// ── 為什麼是分類而不是逐檔 ────────────────────────────────────────────────
// leo 2026-08-08:「**我不要枚舉每個檔案可能的問題和解法,應該是統計的**,
// 因為客戶的可能性太多了,以後會花很多力氣去想為什麼不能支援」
// ⇒ 分類**收斂且窮舉得完**,多出來的一律進「其他」。畫面不解釋、不給解法,
//
// 細節去 Docs FAQ。這取代 08-06 那套逐檔白話翻譯(humanizeFailure)。
package collector
import "strings"
// SyncProgress=總量進度。**單位一律是「份檔案」**(使用者丟進資料夾的東西)。
//
// 🔴 單位為什麼不是「張知識卡」(leo 原稿寫的):地端**算不出**待同步幾張卡——
// 還沒萃取的檔不知道會生出幾張。卡數只有雲端知道。而 leo 最初的痛點原話
// (「他的原檔 9000⋯⋯送到雲端 101/9000」)本來就是檔案單位。
// ⇒ 同一行內單位一致,數字才加得起來;卡與關聯另起一行當成果展示。
//
// 不變式:Total == Done + Pending + Stuck + Unreadablet210 驗法①,測試守著)
type SyncProgress struct {
Total int `json:"total"` // 看守的資料夾裡,我認得的檔案總數
Done int `json:"done"` // 已送進知識庫且內容沒再變過
Pending int `json:"pending"` // 還沒送,會自動接著做(含退避等待中的)
Stuck int `json:"stuck"` // 連續失敗達上限、已暫停自動重試——**不會自己好**
Unreadable int `json:"unreadable"` // 格式讀不了,根本沒進 manifest(由呼叫端補進來)
}
// Add 把另一個資料夾/帳號的進度累加進來——首頁與診斷檔講的都是總量。
func (p SyncProgress) Add(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,
}
}
// Progress 數這份 manifest 的現況。
// manifest 每輪由 Scan() 重建、涵蓋現況所有檔(scan.go 第 7 步「現況檔全數收錄」)
// ⇒ 這裡原地數就是對的,不必另外維護計數器(也就不會有「計數器漂掉」那類病)。
func (m *Manifest) Progress() SyncProgress {
var p SyncProgress
for _, e := range m.Entries {
if e == nil {
continue
}
p.Total++
switch {
case e.IngestedHash != "" && e.IngestedHash == e.ContentHash:
p.Done++
case e.FailCount >= MaxFailBeforeSkip:
// 已經放棄自動重試的不能混在 Pending 裡假裝還在排隊——
// 使用者會一直等一件永遠不會發生的事。
p.Stuck++
default:
p.Pending++
}
}
return p
}
// ── 失敗分類(取代逐檔解釋)──────────────────────────────────────────────
//
// 🔴 收斂原則:這個清單**不准為每個新錯誤一直加**。認不出來的一律進 FailOther,
// 需要細節的人去 Docs FAQ 查。否則就會回到 leo 說的「花很多力氣去想為什麼不能支援」。
const (
FailUnsupportedFormat = "格式不支援"
FailQuotaExhausted = "今天的 AI 額度用完了"
FailNoTextInFile = "檔案讀不出文字"
FailOther = "其他"
)
// FailCategories 是分類的**固定順序**(畫面與診斷檔都照這個序,不隨 map 迭代跳動)。
var FailCategories = []string{FailUnsupportedFormat, FailQuotaExhausted, FailNoTextInFile, FailOther}
// ClassifyFailure 把 collector/雲端吐出的原始錯誤歸進上面四類。
//
// 判斷用的識別字全部來自實撞紀錄,不是猜的:
// - `4006` / `neurons`Cloudflare Workers AI 當日免費額度用完(leo 2026-08-06 從
// Windows collector.log 挖出來的原文)
// - `沒有可抽取的文字`:掃描成圖片的 PDF(同日第二例)
// - `尚未支援的檔案格式`:白名單外的副檔名(G-6.2)
// - 退避訊息會把上游真因接在「|原因:」後面(direct.go retrySkipReason
// ⇒ 先看得到真因就照真因分類,看不到才落到「其他」
func ClassifyFailure(raw string) string {
switch {
case strings.Contains(raw, "neurons"), strings.Contains(raw, "4006"),
strings.Contains(raw, "額度"),
// 2026-08-09(P8):額度用完的檔,錯誤欄位存的是 QuotaNotice 三句話
// quota.go Combined()),文案裡刻意沒有「額度」「4006」這些字——
// 少了這兩個識別字,這批檔會被歸進「其他」,畫面上「今天的 AI 額度用完了」
// 反而一份都沒有。識別字取三句話裡最不會變動的兩段。
strings.Contains(raw, "幫你整理了"), strings.Contains(raw, "會自動恢復"):
return FailQuotaExhausted
case strings.Contains(raw, "沒有可抽取的文字"):
return FailNoTextInFile
case strings.Contains(raw, "尚未支援的檔案格式"), strings.Contains(raw, "不支援"):
return FailUnsupportedFormat
}
return FailOther
}
// FailureBreakdown 是「無法同步 F 份」展開後的內容:只有分類與份數,**沒有檔名、沒有解法**。
// 用固定順序的 slice 而非 map——JSON 輸出穩定、畫面不會每次重排。
type FailureBreakdown struct {
Total int `json:"total"`
Groups []FailureGroup `json:"groups"`
}
// FailureGroup=一類與它的份數。
type FailureGroup struct {
Category string `json:"category"`
Count int `json:"count"`
}
// BuildFailureBreakdown 把一批原始錯誤訊息歸類統計。
// 份數為 0 的分類不出現(沒事就別佔畫面,同 buildSkipped 的慣例)。
func BuildFailureBreakdown(rawErrors []string) FailureBreakdown {
counts := map[string]int{}
for _, raw := range rawErrors {
counts[ClassifyFailure(raw)]++
}
b := FailureBreakdown{Total: len(rawErrors)}
for _, cat := range FailCategories {
if n := counts[cat]; n > 0 {
b.Groups = append(b.Groups, FailureGroup{Category: cat, Count: n})
}
}
return b
}