v0.18.11:修好 Windows「裝了完全不會動」的真兇——全新 config 缺 manifest 欄位

leo 08-06 提供決定性線索:**exit status 2,已自動重試 21 次失敗**。

## 真兇
`app.go` 的 `saveCfg` 只寫四個鍵(accounts/extractor/extractor_explicit/
gemini_api_key),**從來不寫 `manifest`**。
而 `manifest` 是 collector 的必填欄位(`direct.go`:
`if c.Manifest == "" { missing = append(missing, "manifest") }`)
⇒ 讀 config 失敗 → `runDirect` 回 2 → supervisor 無限重拉
⇒ ①「看守中/沒有在跑」一直閃、重開無效(死因沒變)
⇒ ②加資料夾沒反應(collector 從沒跑完一輪)

## 為什麼一路活到封測
**開發機的 config 是舊版留下的、早就有這一欄。**
只有「從零長出 config」的機器才會撞到——也就是**每一台封測者的機器**。
「我這台好好的」正是這個 bug 能活下來的原因。
⇒ 新測試 `TestFreshConfigIsUsableByCollector` **從零建 config**,
   並且不只驗欄位在不在,而是**真的餵給 `collector.LoadDirectConfig`**。
   已反向驗證:把修正拿掉,測試會紅(不是假測試)。

## 順帶修好「為什麼只看得到 exit status 2」
supervisor 在行程結束時用 `cmd.Wait()` 的錯誤當訊息,
**蓋掉**了 stderr 剛寫進 `LastError` 的那句真話
(`collector direct: config 缺必填欄位:manifest`)。
⇒ 改成以 stderr 為主、退出碼放括號補充。
(v0.18.10 已讓死因上畫面+進 app.log,這版讓那句話是**對的**那一句。)

## 三種 exit 2 的實測輸出(重現測試打出來的,非推測)
- config 不是合法 JSON → `config JSON 解析失敗:invalid character …`
- 缺必填欄位 → `config 缺必填欄位:manifest, accounts(或 cypher_url 連線設定)`
- Windows 反斜線路徑 → 正常解析(**排除「反斜線害的」這個猜測**)

## 驗
collector 全測試過、app 測試過、mac+windows 皆編得過;
產物 `Arcrun-win-v0.18.11.exe`(22M)已放 leo 桌面。
⚠️ **尚未真機驗**——要 leo 在 Windows 全新裝一次才算通。
This commit is contained in:
2026-08-06 18:41:31 +08:00
parent 6698f9fbda
commit fcf00bb5df
4 changed files with 83 additions and 1 deletions
+10
View File
@@ -153,6 +153,16 @@ func saveCfg(c *directConfig) error {
c.raw["extractor"] = c.Extractor
c.raw["extractor_explicit"] = c.ExtractorExplicit
c.raw["gemini_api_key"] = c.GeminiAPIKey
// 🔴 2026-08-06 leo Windows 封測的真兇:`manifest` 是 collector 的**必填欄位**
// direct.go 的驗證:`if c.Manifest == "" { missing = append(missing, "manifest") }`),
// 而這支從來沒寫過它 ⇒ **全新安裝的機器**(config 從零長出來)永遠缺這一欄
// ⇒ collector 一啟動就 `exit status 2`、supervisor 無限重拉
// ⇒ 畫面在「看守中/沒有在跑」之間閃、加資料夾也沒反應。
// 為什麼開發機沒撞到:leo 的 Mac config 是舊版留下的、早就有這一欄
// ——**「我這台好好的」正是這個 bug 能活到封測的原因**。
if v, ok := c.raw["manifest"].(string); !ok || strings.TrimSpace(v) == "" {
c.raw["manifest"] = filepath.Join(appDir(), "manifest.json")
}
out, err := json.MarshalIndent(c.raw, "", " ")
if err != nil {