diff --git a/direct.go b/direct.go index 624c423..87b370f 100644 --- a/direct.go +++ b/direct.go @@ -56,6 +56,26 @@ type DirectConfig struct { MaxRemoved float64 `json:"max_removed_ratio"` // 大量刪除防呆門檻(空/0=0.4) } +// expandHome 把開頭的 `~/`(或單獨的 `~`)展開成使用者家目錄的絕對路徑。 +// +// t39(07-24 安裝器實案):成功頁下載的 config.json 把 manifest 寫成 `~/.arcrun-rag/manifest.json`—— +// 那是給人看的寫法,**Go 不會展開波浪號**,os.ReadFile 會去找一個字面上叫 "~" 的資料夾。 +// 修在 daemon 端而不是前端:用戶自己手打 config、或把設定搬到別台機器時,`~/` 都該會動; +// 前端也拿不到用戶的家目錄。展開失敗(拿不到家目錄)就原樣返回,讓後續錯誤照常誠實浮現。 +func expandHome(p string) string { + if p != "~" && !strings.HasPrefix(p, "~/") { + return p + } + home, err := os.UserHomeDir() + if err != nil || home == "" { + return p + } + if p == "~" { + return home + } + return filepath.Join(home, strings.TrimPrefix(p, "~/")) +} + // LoadDirectConfig 讀設定檔並補預設值 + 基本驗證。 func LoadDirectConfig(path string) (*DirectConfig, error) { data, err := os.ReadFile(path) @@ -110,6 +130,12 @@ func LoadDirectConfig(path string) (*DirectConfig, error) { c.MaxRemoved = DefaultMaxRemovedRatio } c.CypherURL = strings.TrimSuffix(c.CypherURL, "/") + // t39:路徑欄位一律展開 `~/`(config 是給人填/人讀的,波浪號是人的寫法) + c.Manifest = expandHome(c.Manifest) + c.WatchFolder = expandHome(c.WatchFolder) + for i, p := range c.WatchFolders { + c.WatchFolders[i] = expandHome(p) + } return &c, nil } diff --git a/direct_multi_test.go b/direct_multi_test.go index 4b6eb2b..ca9d1db 100644 --- a/direct_multi_test.go +++ b/direct_multi_test.go @@ -122,3 +122,59 @@ func TestRunDirectOnceMultiRootDryRun(t *testing.T) { t.Fatalf("rootB 事件錯:%v", byRoot[rootB]) } } + +// t39:config 的 `~/` 由 daemon 端展開——安裝器成功頁下載的 config.json 用 +// `~/.arcrun-rag/manifest.json` 這種人看的寫法,Go 不展開波浪號=會去找字面上叫 "~" 的資料夾。 +func TestLoadDirectConfigExpandsHome(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil || home == "" { + t.Skip("拿不到家目錄,跳過") + } + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json") + body := `{ + "watch_folders": ["~/Documents/知識庫", "/tmp/絕對路徑不動"], + "manifest": "~/.arcrun-rag/manifest.json", + "cypher_url": "https://cypher.example.workers.dev", + "namespace": "ns1" +}` + if err := os.WriteFile(cfgPath, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + cfg, err := LoadDirectConfig(cfgPath) + if err != nil { + t.Fatalf("LoadDirectConfig: %v", err) + } + wantManifest := filepath.Join(home, ".arcrun-rag/manifest.json") + if cfg.Manifest != wantManifest { + t.Errorf("manifest 未展開:got %q want %q", cfg.Manifest, wantManifest) + } + folders := cfg.Folders() + wantFolder := filepath.Join(home, "Documents/知識庫") + if folders[0] != wantFolder { + t.Errorf("watch_folders[0] 未展開:got %q want %q", folders[0], wantFolder) + } + if folders[1] != "/tmp/絕對路徑不動" { + t.Errorf("絕對路徑不該被動:got %q", folders[1]) + } +} + +func TestExpandHomeEdgeCases(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil || home == "" { + t.Skip("拿不到家目錄,跳過") + } + cases := map[string]string{ + "~": home, + "~/a/b": filepath.Join(home, "a/b"), + "/abs/path": "/abs/path", + "relative/dir": "relative/dir", + "": "", + "~notme/x": "~notme/x", // 只認 `~/`;`~user` 形式不猜(Go 無法解析他人家目錄) + } + for in, want := range cases { + if got := expandHome(in); got != want { + t.Errorf("expandHome(%q)=%q,want %q", in, got, want) + } + } +}