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:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"v0.18.9": "8be9260eb542f527",
|
||||
"v0.18.10": "a440f044a849b7f2"
|
||||
"v0.18.10": "a440f044a849b7f2",
|
||||
"v0.18.11": "1538689e181444f6"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user