Files
magpie/internal/gui/backend.go
T
yetone bb5e8a2b61 factory: the Gateway page says when an older magpie serves the gateway, and a Factory account can be added by its API key (#506, 黄瑞 and aohun: Factory 403 "Factory refused this account the request; check that droid… sign in to it again" on v0.1.630; aohun: support FACTORY_API_KEY as droid2api does) — the 403's wording quoted in #506 was removed in 5523eb17 (first in v0.1.551; git grep finds it in v0.1.550, not in v0.1.630), so those requests were served by a magpie at v0.1.550 or older still holding :3425 (a magpie serve, a second copy, one that didn't quit): serveGateway leaves the port to any magpie answering GET / and the page said only "running · served by another magpie", so none of the later Factory fixes (droid's opening line on /api/llm/o in v0.1.630 among them) reached their requests. gateway.ServedBy reads the serving magpie's version from its GET / answer (Serving wraps it); the providers state carries it with older when it is below this one's, the Gateway card then says "running · served by magpie <v>, older than this one" with an amber dot and that agents' requests go through it without this version's fixes until it quits, when this one takes over within 15 s (zh too, wrapped, never cut off), and the log says so. A Factory API key (fk-…) is added from the import box (Factory's accounts list "Add an account by API key…", and "Use an API key instead…" on the warning before a sign-in): as droid's om() takes FACTORY_API_KEY, the key is the bearer with droid's headers, never renewed, and sends no X-Factory-Org-Id (droid's active org comes from a sign-in, st() is null for a key); whoami asked with the key says whose it is and its region/host, as droid's yP checks a key ("invalid-api-key" when refused), and a refused key isn't added. factoryFresh hands a key account on as it is (no WorkOS, no whoami for an org) and factoryMendOrg leaves it alone; when Factory has moved to its plugin the key isn't offered, the built-in being what keeps it. No request was made to Factory, no real key or account was tried, and whether Factory serves a key's requests from agents other than droid is not known. TestGatewayServedByAnOlderMagpie (fake magpie answering 0.1.550/0.1.630/0.1.640/no version), TestFactoryAPIKey (fake Factory: import, bearer and headers, no org, no WorkOS, refused key, "exists" again; fails with a 403 without the key handling in factoryFresh) and gateway-older.test.cjs, factory-key.test.cjs (Chromium and WebKit, en and zh) pass; login-import, factory-sub, plugin-moved-signin, gateway-fold pass in WebKit (Chromium's channel launch lacks ICU here). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 14:58:49 +08:00

103 lines
3.1 KiB
Go

package gui
import (
"context"
"log"
"sync"
"sync/atomic"
"time"
"github.com/yetone/magpie/internal/agent"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/gateway"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/update"
)
// served is the gateway this process serves, nil while another magpie
// has it.
var served atomic.Pointer[gateway.Server]
// backendCtx ends the gateway this process serves (stopServing).
var backendCtx, endBackend = context.WithCancel(context.Background())
// serving is the gateway this process serves, until it has finished.
var serving sync.WaitGroup
// stopServing stops this process's gateway taking requests and returns
// once those in flight have finished.
func stopServing() {
endBackend()
serving.Wait()
}
// startBackend starts what serves the page and the agents: the gateway,
// unless another magpie has it (then that one serves and this one only
// shows its status, and gw is nil — until that one is gone: a magpie left
// running from before an update, a magpie serve in a terminal), and the
// model lists kept warm.
func startBackend() (gw *gateway.Server) {
gateway.Window = true // the routing this process serves is shown on its page
gw = serveGateway()
go watchGateway()
// Model lists are fetched, never compiled in: whatever the agents can see
// comes from the models.dev catalog plus each vendor's own /models answer.
// Keep both halves warm without making the user click anything.
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if catalog.Stale() {
if err := catalog.Sync(ctx); err != nil {
log.Println("catalog:", err)
}
}
cancel()
// A signed-in agent's list exists only at the vendor; fill it in the
// first time so the picker never shows a stale snapshot.
provider.FetchNew(20 * time.Second)
// lists an older magpie wrote into agents' files, without what
// it has learnt since (context windows, providers added)
agent.SyncCatalog()
// and fetched again each day it stays open, for new models' prices
catalog.KeepFresh()
}()
return gw
}
var gatewayWatch = 15 * time.Second
// watchGateway takes the gateway up once the magpie that had it is gone.
func watchGateway() {
for {
time.Sleep(gatewayWatch)
if served.Load() == nil && !gateway.Running() {
serveGateway()
}
}
}
// serveGateway starts the gateway here when no magpie has it: the one
// started, or nil.
func serveGateway() *gateway.Server {
// handing over, the one there is this one's predecessor, which lets go
// once this one listens beside it
if !gateway.Handover {
if o := gateway.ServedBy(); o.Running {
if update.Newer(gateway.Version, o.Version) {
log.Printf("gateway: magpie %s serves %s, older than this one (%s); agents' requests go through it until it quits", o.Version, gateway.URL(), gateway.Version)
}
return nil
}
}
gw := gateway.New()
served.Store(gw)
serving.Add(1)
go func() {
defer serving.Done()
if err := gw.ListenAndServe(backendCtx); err != nil {
log.Println("gateway:", err)
served.CompareAndSwap(gw, nil) // another took the port first
}
}()
return gw
}