Files
magpie/internal/provider/proxy.go
T
yetone 3dc8f00f06 providers: each account of a subscription holding several can go through its own proxy — the provider's, none, or its own address (Discord, gakki: 是否可以为不同的codex账号设置不用的代理,现在是同一个供应商下使用相同的代理) — the per-provider proxy (#237) was one for all of Codex's signed-in accounts. A subscription's entry in providers.json now keeps accountProxies, by the account's name in lower case, each checked by settings.CheckProxy on save (ftp:// refused), addresses trimmed, accounts following the provider's proxy left out. Provider.ProxyChoice picks an account's own proxy, else the provider's, else the global one, so everything that already went through Provider.Via follows the account: gateway turns and fallback across accounts, usage and quota, token refresh, Codex's warm-up, reset and saved accounts' model lists, Claude's warm-up and the *_PROXY of Claude Code runs (ViaLogin / ProxyOfLogin), and Codex's own sign-in relayed as it came (ViaSignedIn). Extra accounts of Copilot, ZCode, Kiro and the like, made afresh without the provider's settings, now read them from providers.json, so they honour the provider's proxy too. A save without accountProxies keeps them; {} clears them. The provider editor, for a subscription with two or more accounts, lists each under the Proxy row: Provider's proxy | Direct | Custom, the address field's room kept so a pick moves nothing; Custom with no address is refused before anything is posted, its field focused without scrolling. en and zh together; no border stripes. Tested with internal/gateway/account_proxy_test.go in a sandbox HOME against fake proxies and a fake ChatGPT backend: three Codex accounts (own proxy, direct, following the provider) each reach the backend their own way in one turn and for usage; clearing an account's proxy sends it through the provider's, clearing the provider's through the global one, direct staying direct; ftp:// refused; TestClaudeAccountProxyEnv covers each Claude account's HTTPS_PROXY. Both fail with the per-account lookup off. account-proxy.test.cjs, Chromium and WebKit, en and zh (new), provider-proxy, click-scroll and old-webkit pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real proxy, real accounts or the real Wails window; Grok's per-account usage and Kiro's usage on a key still use the provider's proxy.
2026-09-30 04:52:03 +08:00

125 lines
3.6 KiB
Go

package provider
import (
"context"
"fmt"
"strings"
"github.com/yetone/magpie/internal/netproxy"
"github.com/yetone/magpie/internal/settings"
)
// Via is ctx for requests made on the provider's behalf: they go through
// its own proxy (ProxyChoice), or the global one when it has none.
func (p Provider) Via(ctx context.Context) context.Context {
return netproxy.With(ctx, p.ProxyChoice())
}
// ProxyChoice is the proxy requests made on the provider's behalf go
// through, as netproxy.With takes it: for one account of a subscription
// that holds several, that account's own (AccountProxies), else the
// provider's (Proxy); "" follows the global one. An account in use beside
// the agent's own is made afresh by some agents (AlsoOn), without the
// provider's settings: those are read from providers.json.
func (p Provider) ProxyChoice() string {
if p.Account == nil {
return p.Proxy
}
own, prov := p.AccountProxies, p.Proxy
if own == nil && prov == "" {
if s, ok := storedPicks(p.ID); ok {
own, prov = s.AccountProxies, s.Proxy
}
}
if v := own[accountKey(p.Account.User)]; v != "" {
return v
}
return prov
}
// ProxyOf is the own proxy of the provider or signed-in account with id,
// "" when it follows the global one.
func ProxyOf(id string) string {
if s, ok := storedPicks(id); ok {
return s.Proxy
}
return ""
}
// ProxyOfLogin is the proxy requests made on behalf of the account user
// of the subscription id go through (gakki: one Codex account through one
// proxy, another through another): the account's own, or else the
// subscription's (ProxyOf); "" follows the global one.
func ProxyOfLogin(id, user string) string {
s, ok := storedPicks(id)
if !ok {
return ""
}
if v := s.AccountProxies[accountKey(user)]; v != "" {
return v
}
return s.Proxy
}
// Via is ctx for requests made on behalf of the provider or signed-in
// account with id (see Provider.Via).
func Via(ctx context.Context, id string) context.Context {
return netproxy.With(ctx, ProxyOf(id))
}
// ViaLogin is ctx for requests made on behalf of the account user of the
// subscription id (ProxyOfLogin).
func ViaLogin(ctx context.Context, id, user string) context.Context {
return netproxy.With(ctx, ProxyOfLogin(id, user))
}
// ViaSignedIn is ctx for requests made with the sign-in an agent has now
// (Codex's own, relayed as it came): through the proxy of the account it
// is signed in to.
func ViaSignedIn(ctx context.Context, id string) context.Context {
if l, ok := liveLogin(id); ok {
return ViaLogin(ctx, id, l.User)
}
return Via(ctx, id)
}
func storedPicks(id string) (Provider, bool) {
for _, p := range load().Providers {
if p.ID == id {
return p, true
}
}
return Provider{}, false
}
// accountKey is how an account is known in AccountProxies: its name as the
// accounts list shows it, in lower case, as accounts are told apart.
func accountKey(user string) string { return strings.ToLower(strings.TrimSpace(user)) }
// normalAccountProxies is m as it is kept: keys in lower case, addresses
// trimmed, accounts following the provider's proxy left out; nil for none.
func normalAccountProxies(m map[string]string) map[string]string {
var out map[string]string
for u, v := range m {
u, v = accountKey(u), strings.TrimSpace(v)
if u == "" || v == "" {
continue
}
if out == nil {
out = map[string]string{}
}
out[u] = v
}
return out
}
// checkAccountProxies refuses an account's proxy Settings would refuse.
func checkAccountProxies(m map[string]string) error {
for u, v := range m {
if err := settings.CheckProxy(v); err != nil {
return fmt.Errorf("%s: %w", u, err)
}
}
return nil
}