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