单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。 Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.9 KiB
Go
83 lines
2.9 KiB
Go
// 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
|
||
}
|