// proxyexit.go:ProxyManager 出口决策内部契约(W3 gateway/scheduler 对齐面)。 // // 契约权威:/api/exit 响应形状由 proxymanager.ExitDecision 定义(单源), // 本文件提供 W3 侧调用的反序列化结构 + overlay HTTP 客户端; // 双方 import 同一类型防契约漂移(T1)。 package contract import ( "context" "encoding/json" "fmt" "net/http" "net/url" "time" ) // ProxyExitDecision /api/exit 响应形状(与 proxymanager.ExitDecision 一致)。 // Blocked=true 时 gateway/scheduler 据此拒绝(Reason=deny_rule / unhealthy:… / pool_empty)。 type ProxyExitDecision struct { Proxy string `json:"proxy"` // 统一 mixed 出口(http://mihomo:17890) Node string `json:"node"` // 节点名(provenance 显示用) Region string `json:"region"` // 节点区域 Sticky bool `json:"sticky"` // 是否命中 sticky Blocked bool `json:"blocked"` // true=拒绝分配出口 Reason string `json:"reason,omitempty"` // deny_rule / unhealthy:… / pool_empty } // ProxiesSnapshot /api/proxies 单节点探活状态(脱敏无凭据)。 type ProxiesSnapshot struct { Name string `json:"name"` Type string `json:"type"` Region string `json:"region"` Alive bool `json:"alive"` DelayMs int `json:"delay_ms"` ConsecFails int `json:"consec_fails"` State string `json:"state"` } // ProxyManagerClient W3 调用 ProxyManager overlay API 的客户端 // (scheduler 派发前询问出口;gateway 不直接调用)。 type ProxyManagerClient struct { base string // 如 http://proxymanager:8642 hc *http.Client } // NewProxyManagerClient 构造(base 仅服务名+端口,无凭据)。 func NewProxyManagerClient(base string) *ProxyManagerClient { return &ProxyManagerClient{ base: base, hc: &http.Client{Timeout: 5 * time.Second}, } } // GetExit 查询出口决策。domain/session 至少其一非空(服务端校验)。 func (c *ProxyManagerClient) GetExit(ctx context.Context, domain, session string) (*ProxyExitDecision, error) { if domain == "" && session == "" { return nil, fmt.Errorf("contract: GetExit 需 domain 或 session") } q := url.Values{} if domain != "" { q.Set("domain", domain) } if session != "" { q.Set("session", session) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+"/api/exit?"+q.Encode(), nil) if err != nil { return nil, fmt.Errorf("contract: GetExit 请求构造失败: %w", err) } resp, err := c.hc.Do(req) if err != nil { return nil, fmt.Errorf("contract: GetExit 调用失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("contract: GetExit HTTP %d", resp.StatusCode) } var d ProxyExitDecision if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { return nil, fmt.Errorf("contract: GetExit 响应解析失败: %w", err) } return &d, nil }