From e0265088b77c119ee0141921f3ed1f3b11771132 Mon Sep 17 00:00:00 2001 From: Alix-007 <267018309+Alix-007@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:10:02 +0800 Subject: [PATCH] fix(weixin): probe sync dir writability before choosing home --- pkg/channels/weixin/state.go | 38 ++++++++++++++++++++++++------ pkg/channels/weixin/weixin_test.go | 38 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/pkg/channels/weixin/state.go b/pkg/channels/weixin/state.go index c27483b5c..f1f1a9bd4 100644 --- a/pkg/channels/weixin/state.go +++ b/pkg/channels/weixin/state.go @@ -40,6 +40,31 @@ type contextTokensFile struct { Tokens map[string]string `json:"tokens"` } +func syncDirForHome(home string) string { + return filepath.Join(home, "channels", "weixin", "sync") +} + +func probeSyncDirWritable(home string) error { + syncDir := syncDirForHome(home) + if err := os.MkdirAll(syncDir, 0o700); err != nil { + return err + } + + probeFile, err := os.CreateTemp(syncDir, ".write-probe-*") + if err != nil { + return err + } + probePath := probeFile.Name() + if err := probeFile.Close(); err != nil { + _ = os.Remove(probePath) + return err + } + if err := os.Remove(probePath); err != nil { + return err + } + return nil +} + func picoclawHomeDir() string { if home := os.Getenv(config.EnvHome); home != "" { return home @@ -47,15 +72,14 @@ func picoclawHomeDir() string { if userHome, err := os.UserHomeDir(); err == nil && userHome != "" { home := filepath.Join(userHome, ".picoclaw") - syncDir := filepath.Join(home, "channels", "weixin", "sync") - mkErr := os.MkdirAll(syncDir, 0o700) - if mkErr == nil { + if probeErr := probeSyncDirWritable(home); probeErr == nil { return home + } else { + logger.WarnCF("weixin", "Default picoclaw home is not writable; using temp directory for sync cursor", map[string]any{ + "path": home, + "error": probeErr.Error(), + }) } - logger.WarnCF("weixin", "Default picoclaw home is not writable; using temp directory for sync cursor", map[string]any{ - "path": home, - "error": mkErr.Error(), - }) } return filepath.Join(os.TempDir(), "picoclaw") diff --git a/pkg/channels/weixin/weixin_test.go b/pkg/channels/weixin/weixin_test.go index 3bac9822b..ca3ab131d 100644 --- a/pkg/channels/weixin/weixin_test.go +++ b/pkg/channels/weixin/weixin_test.go @@ -294,6 +294,44 @@ func TestBuildWeixinSyncBufPathFallsBackWhenHomeIsUnusable(t *testing.T) { } } +func TestBuildWeixinSyncBufPathFallsBackWhenSyncDirUnwritable(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("permission mode behavior differs on windows") + } + if os.Geteuid() == 0 { + t.Skip("root can usually bypass directory write permissions") + } + + t.Setenv(config.EnvHome, "") + home := t.TempDir() + t.Setenv("HOME", home) + + syncDir := filepath.Join(home, ".picoclaw", "channels", "weixin", "sync") + if err := os.MkdirAll(syncDir, 0o700); err != nil { + t.Fatalf("MkdirAll(syncDir) error = %v", err) + } + if err := os.Chmod(syncDir, 0o500); err != nil { + t.Fatalf("Chmod(syncDir, 0500) error = %v", err) + } + t.Cleanup(func() { _ = os.Chmod(syncDir, 0o700) }) + + if probeFile, err := os.CreateTemp(syncDir, ".probe-*"); err == nil { + _ = probeFile.Close() + _ = os.Remove(probeFile.Name()) + t.Skip("environment does not enforce non-writable directory permissions") + } + + wxCfg := config.WeixinConfig{ + BaseURL: "https://ilinkai.weixin.qq.com/", + } + wxCfg.SetToken("token-123") + got := buildWeixinSyncBufPath(wxCfg) + wantDir := filepath.Join(os.TempDir(), "picoclaw", "channels", "weixin", "sync") + if filepath.Dir(got) != wantDir { + t.Fatalf("sync path dir = %q, want %q", filepath.Dir(got), wantDir) + } +} + func TestSessionPauseGuard(t *testing.T) { ch := &WeixinChannel{ typingCache: make(map[string]typingTicketCacheEntry),