From fcf00bb5df0944c12c804460409ca2cc9c11e4f6 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 18:41:31 +0800 Subject: [PATCH] =?UTF-8?q?v0.18.11=EF=BC=9A=E4=BF=AE=E5=A5=BD=20Windows?= =?UTF-8?q?=E3=80=8C=E8=A3=9D=E4=BA=86=E5=AE=8C=E5=85=A8=E4=B8=8D=E6=9C=83?= =?UTF-8?q?=E5=8B=95=E3=80=8D=E7=9A=84=E7=9C=9F=E5=85=87=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=85=A8=E6=96=B0=20config=20=E7=BC=BA=20manifest=20=E6=AC=84?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 全新裝一次才算通。 --- cmd/arcrun-app/.version-source.json | 3 +- cmd/arcrun-app/app.go | 10 +++++ cmd/arcrun-app/manifest_field_test.go | 60 +++++++++++++++++++++++++++ supervisor/supervisor.go | 11 +++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 cmd/arcrun-app/manifest_field_test.go diff --git a/cmd/arcrun-app/.version-source.json b/cmd/arcrun-app/.version-source.json index 39e3625..6b7a68a 100644 --- a/cmd/arcrun-app/.version-source.json +++ b/cmd/arcrun-app/.version-source.json @@ -1,4 +1,5 @@ { "v0.18.9": "8be9260eb542f527", - "v0.18.10": "a440f044a849b7f2" + "v0.18.10": "a440f044a849b7f2", + "v0.18.11": "1538689e181444f6" } diff --git a/cmd/arcrun-app/app.go b/cmd/arcrun-app/app.go index 16d0eb7..5a17655 100644 --- a/cmd/arcrun-app/app.go +++ b/cmd/arcrun-app/app.go @@ -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 { diff --git a/cmd/arcrun-app/manifest_field_test.go b/cmd/arcrun-app/manifest_field_test.go new file mode 100644 index 0000000..fd9a48d --- /dev/null +++ b/cmd/arcrun-app/manifest_field_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + collector "arcrun-rag/collector" +) + +// TestFreshConfigIsUsableByCollector 釘住 leo 2026-08-06 Windows 封測的真兇: +// +// App 存出來的**全新** config 少了 `manifest` 這個必填欄位 +// ⇒ collector 一啟動就 exit status 2 ⇒ supervisor 無限重拉 +// ⇒ 畫面在「看守中/沒有在跑」之間閃、加資料夾也沒反應。 +// +// 為什麼一路活到封測:開發機的 config 是舊版留下的、早就有這一欄 +// ——「我這台好好的」正是它能活下來的原因。所以這支測試**從零建 config**。 +func TestFreshConfigIsUsableByCollector(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) // Windows 的家目錄變數 + + // 模擬全新安裝:完全沒有 config,使用者連上一個知識庫後存檔 + cfg := &directConfig{ + Accounts: []accountCfg{{ + CypherURL: "https://example.workers.dev", + Namespace: "abc123", + APIKey: "abc123", + }}, + Extractor: "workers-ai", + } + if err := saveCfg(cfg); err != nil { + t.Fatalf("存檔失敗:%v", err) + } + + raw, err := os.ReadFile(configPath()) + if err != nil { + t.Fatalf("讀不回剛存的 config:%v", err) + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatalf("存出來的不是合法 JSON:%v", err) + } + if s, _ := m["manifest"].(string); strings.TrimSpace(s) == "" { + t.Fatal("全新 config 沒有 manifest ⇒ collector 會 exit 2 無限重試(就是 leo 撞到的那個)") + } + + // 真正的驗收不是「欄位在不在」,是**collector 吃不吃得下去**。 + if _, err := collector.LoadDirectConfig(configPath()); err != nil { + t.Fatalf("collector 讀不了 App 存的 config:%v", err) + } + + // manifest 要落在 app 目錄下,不能指到別人家 + if got, _ := m["manifest"].(string); filepath.Dir(got) != appDir() { + t.Errorf("manifest 應該放在 %s,實得 %s", appDir(), got) + } +} diff --git a/supervisor/supervisor.go b/supervisor/supervisor.go index 50fb4b1..973afc8 100644 --- a/supervisor/supervisor.go +++ b/supervisor/supervisor.go @@ -242,10 +242,21 @@ func (s *Supervisor) loop(ctx context.Context) { return } // 非預期退出:記錯、重起計數、退避後再拉 + // 🔴 2026-08-06(leo Windows 實測):這裡以前直接用 cmd.Wait() 的錯誤當訊息, + // 於是「exit status 2」**蓋掉**了 stderr 剛剛寫進 LastError 的那句真話 + // (例如「collector direct: config JSON 解析失敗:…」)。 + // 使用者與遠端的我拿到的都是最沒用的那一句——退化成猜謎。 + // ⇒ 有 stderr 就以 stderr 為主、退出碼放括號裡當補充。 msg := "行程結束" if err != nil { msg = err.Error() } + s.mu.Lock() + stderrLine := s.status.LastError + s.mu.Unlock() + if stderrLine != "" && stderrLine != msg { + msg = stderrLine + "(" + msg + ")" + } s.setState(func(st *Status) { st.State = StateError st.LastError = msg