From 5daba301cac2f08aac251b419a09cb919dfcf7cc Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 5 Oct 2026 01:04:07 +0800 Subject: [PATCH] =?UTF-8?q?agent:=20WorkBuddy's=20magpie=20models=20pointe?= =?UTF-8?q?d=20at=20another=20machine's=20magpie=20stay=20there=20through?= =?UTF-8?q?=20a=20sync=20(=E6=82=A0=E6=82=A0=E5=93=A5=20on=20Discord:=20?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E9=97=AE=E9=A2=98workbuddy=E4=B9=9F=E4=BC=9A?= =?UTF-8?q?=E6=9C=89)=20=E2=80=94=20as=20v0.1.891=20did=20for=20ZCode:=20a?= =?UTF-8?q?=20sync=20rewrote=20every=20magpie=20entry=20in=20~/.workbuddy/?= =?UTF-8?q?models.json=20to=20this=20machine's=20gateway=20and=20key,=20so?= =?UTF-8?q?=20models=20the=20user=20had=20moved=20to=20the=20magpie=20on?= =?UTF-8?q?=20a=20NAS=20went=20back=20to=20the=20local=20one.=20A=20sync?= =?UTF-8?q?=20now=20keeps=20the=20url=20and=20apiKey=20of=20a=20magpie=20e?= =?UTF-8?q?ntry=20on=20another=20machine=20(onAnotherMachine)=20for=20all?= =?UTF-8?q?=20of=20magpie's=20entries;=20one=20on=20this=20machine's=20loo?= =?UTF-8?q?pback=20follows=20the=20gateway=20as=20before.=20TestWorkBuddyK?= =?UTF-8?q?eepsRemoteAddress=20fails=20on=20the=20old=20code=20(url=20http?= =?UTF-8?q?://127.0.0.1:3425/=E2=80=A6,=20key=20magpie-workbuddy).=20go=20?= =?UTF-8?q?vet,=20builds=20(darwin,=20GOOS=3Dlinux=20nogui,=20GOOS=3Dwindo?= =?UTF-8?q?ws)=20and=20go=20test=20-tags=20nogui=20-count=3D1=20./...=20un?= =?UTF-8?q?der=20a=20temp=20HOME=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/agent/workbuddy.go | 16 +++++++- internal/agent/workbuddy_test.go | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/internal/agent/workbuddy.go b/internal/agent/workbuddy.go index ab9c0f9a..5671448e 100644 --- a/internal/agent/workbuddy.go +++ b/internal/agent/workbuddy.go @@ -94,6 +94,8 @@ type workbuddyEntry struct { ID string `json:"id"` Vendor string `json:"vendor"` Disabled *bool `json:"disabled"` + URL string `json:"url"` + APIKey string `json:"apiKey"` } func workbuddyMine(raw json.RawMessage) (workbuddyEntry, bool) { @@ -109,7 +111,9 @@ func workbuddyWired(path string) bool { // workbuddyWrite puts magpie's models into models.json (on) or takes them // out, leaving every other entry and key as WorkBuddy wrote it. A model -// turned off in WorkBuddy stays off. +// turned off in WorkBuddy stays off, and models the user pointed at a magpie +// on another machine (a NAS's) stay there with its key, as ZCode's do +// (zcodeAddress): a sync brings the models up to date, not the address. func workbuddyWrite(path string, on bool) error { d, err := workbuddyRead(path) if err != nil { @@ -118,12 +122,16 @@ func workbuddyWrite(path string, on bool) error { var kept []json.RawMessage var ours []string off := map[string]bool{} + var remote, remoteKey string at := -1 // where magpie's were, which they keep for _, raw := range d.models { if e, ok := workbuddyMine(raw); ok { if at < 0 { at = len(kept) } + if remote == "" && onAnotherMachine(e.URL) { + remote, remoteKey = e.URL, e.APIKey + } ours = append(ours, e.ID) if e.Disabled != nil && *e.Disabled { off[e.ID] = true @@ -143,6 +151,12 @@ func workbuddyWrite(path string, on bool) error { if off[m.ID] { e["disabled"] = true } + if remote != "" { + e["url"] = remote + if remoteKey != "" { + e["apiKey"] = remoteKey + } + } b, err := json.Marshal(e) if err != nil { return err diff --git a/internal/agent/workbuddy_test.go b/internal/agent/workbuddy_test.go index f44f1c5e..1751f0e4 100644 --- a/internal/agent/workbuddy_test.go +++ b/internal/agent/workbuddy_test.go @@ -123,3 +123,69 @@ func TestWorkBuddyEfforts(t *testing.T) { t.Fatalf("%v", e) } } + +// WorkBuddy pointed at a magpie on another machine stays there through a +// sync, with that magpie's key (悠悠哥 on Discord: models moved to the +// magpie on a NAS were put back on this machine's), while one on this +// machine's loopback follows the gateway. +func TestWorkBuddyKeepsRemoteAddress(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) + t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache")) + t.Setenv("WORKBUDDY_CONFIG_DIR", "") + if err := provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k", Models: []string{"pro", "flash"}}); err != nil { + t.Fatal(err) + } + path := filepath.Join(home, ".workbuddy", "models.json") + os.MkdirAll(filepath.Dir(path), 0o755) + a := workbuddy(home) + if err := a.Field("provider").Set(magpieID); err != nil { + t.Fatal(err) + } + read := func() []map[string]any { + t.Helper() + var ms []map[string]any + b, _ := os.ReadFile(path) + if err := json.Unmarshal(b, &ms); err != nil { + t.Fatalf("%v\n%s", err, b) + } + return ms + } + point := func(url, key string) { + t.Helper() + ms := read() + for _, m := range ms { + m["url"], m["apiKey"] = url, key + } + b, _ := json.Marshal(ms) + os.WriteFile(path, b, 0o600) + } + + const nas = "http://192.168.1.20:3425/v1/chat/completions" + point(nas, "nas-key") + if err := a.Sync(); err != nil { + t.Fatal(err) + } + ms := read() + if len(ms) != 2 { + t.Fatalf("models: %v", ms) + } + for _, m := range ms { + if m["url"] != nas || m["apiKey"] != "nas-key" { + t.Fatalf("NAS address not kept: %v", m) + } + } + + // an address on this machine is magpie's own and follows the gateway + point("http://localhost:9999/v1/chat/completions", "old") + if err := a.Sync(); err != nil { + t.Fatal(err) + } + for _, m := range read() { + if m["url"] != gatewayV1()+"/chat/completions" || m["apiKey"] != "magpie-workbuddy" { + t.Fatalf("local address not followed: %v", m) + } + } +}