fix(t39): daemon 端展開 config 的 ~/ 路徑

安裝器成功頁下載的 config.json 把 manifest 寫成 ~/.arcrun-rag/manifest.json——那是給人
看的寫法,Go 不展開波浪號,os.ReadFile 會去找一個字面上叫 "~" 的資料夾。

修在 daemon 端而非前端:用戶自己手打 config、或把設定搬到另一台機器時 ~/ 都該會動,
而前端也拿不到用戶家目錄。manifest 與 watch_folder(s) 全展開;只認 ~/ 與單獨的 ~,
~user 形式不猜(Go 無法解析他人家目錄);拿不到家目錄就原樣返回讓錯誤誠實浮現。

測試 +2(config 展開含 CJK 路徑與絕對路徑不動、expandHome 六種邊界),collector 全套綠。
This commit is contained in:
Claude
2026-07-25 08:18:21 +00:00
parent 125377b899
commit e548c57efa
2 changed files with 82 additions and 0 deletions
+26
View File
@@ -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
}
+56
View File
@@ -122,3 +122,59 @@ func TestRunDirectOnceMultiRootDryRun(t *testing.T) {
t.Fatalf("rootB 事件錯:%v", byRoot[rootB])
}
}
// t39config 的 `~/` 由 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)%qwant %q", in, got, want)
}
}
}