onesvm-browser-server/server/internal/contract/proxyexit.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

83 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}