package proxy import ( "context" "fmt" "net/http" "strconv" "strings" "time" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "ant-chrome/backend/internal/config" "ant-chrome/backend/internal/logger" ) // ─── Clash 标准测速 URL ─── // 使用 HTTP 与 Clash 客户端保持一致 const DefaultSpeedTestURL = "http://www.gstatic.com/generate_204" // SpeedTestConfig 测速参数 type SpeedTestConfig struct { Timeout time.Duration TCPTimeout time.Duration URLs []string ExpectedStatus []int } var DefaultSpeedTestConfig = SpeedTestConfig{ Timeout: 3 * time.Second, TCPTimeout: 3 * time.Second, } // ─── 对外入口 ─── // SpeedTest 按单个代理的内核决策执行轻量 HTTP 延迟测试。 func SpeedTest( proxyId string, proxies []config.BrowserProxy, xrayMgr *XrayManager, singboxMgr *SingBoxManager, cfg *SpeedTestConfig, ) TestResult { return SpeedTestWithConnector(proxyId, proxies, xrayMgr, singboxMgr, nil, config.BrowserConnectorXray, cfg) } // SpeedTestWithConnector 保留 connectorType 参数用于旧调用兼容。 // 实际测速内核由 ResolveProxyKernel 按单个代理决定。 func SpeedTestWithConnector( proxyId string, proxies []config.BrowserProxy, xrayMgr *XrayManager, singboxMgr *SingBoxManager, clashMgr *ClashManager, connectorType string, cfg *SpeedTestConfig, ) TestResult { connectorType = config.NormalizeBrowserConnectorType(connectorType) return lightHTTPDelayTestWithConnector(proxyId, proxies, xrayMgr, singboxMgr, clashMgr, connectorType, cfg) } func lightHTTPDelayTestWithConnector( proxyId string, proxies []config.BrowserProxy, xrayMgr *XrayManager, singboxMgr *SingBoxManager, clashMgr *ClashManager, connectorType string, cfg *SpeedTestConfig, ) TestResult { log := logger.New("SpeedTest") if cfg == nil { c := DefaultSpeedTestConfig cfg = &c } src := resolveProxyConfig("", proxies, proxyId) if src == "" { return TestResult{ProxyId: proxyId, Ok: false, Engine: connectorType, Error: "代理配置为空"} } if strings.ToLower(src) == "direct://" { return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: 0, Engine: "direct"} } testURLs := speedTestTargetURLs(cfg) if len(testURLs) == 0 { return TestResult{ProxyId: proxyId, Ok: false, Engine: connectorType, Error: "测速目标 URL 为空"} } engine := speedTestProbeEngine(src, proxies, proxyId, connectorType) log.Info("开始代理测速", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("timeout_ms", cfg.Timeout.Milliseconds()), logger.F("tcp_timeout_ms", cfg.TCPTimeout.Milliseconds()), logger.F("targets", strings.Join(testURLs, ",")), ) client, err := buildSpeedTestHTTPClient(src, proxyId, proxies, xrayMgr, singboxMgr, clashMgr, connectorType, cfg) if err != nil { log.Warn("代理测速 HTTP 客户端创建失败", logger.F("proxy_id", proxyId), logger.F("error", err.Error()), ) return TestResult{ProxyId: proxyId, Ok: false, Engine: engine, Error: err.Error()} } var lastErr error var lastLatency int64 for _, testURL := range testURLs { latency, statusCode, err := doSpeedTestRequest(client, testURL) lastLatency = latency if err != nil { lastErr = err log.Warn("代理测速请求失败", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("url", testURL), logger.F("latency_ms", latency), logger.F("error", err.Error()), ) continue } if speedTestStatusOK(statusCode, cfg) { log.Info("代理测速成功", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("url", testURL), logger.F("status", statusCode), logger.F("latency_ms", latency), ) return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency, Engine: engine} } lastErr = fmt.Errorf("HTTP %d", statusCode) log.Warn("代理测速状态码不符合预期", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("url", testURL), logger.F("status", statusCode), logger.F("latency_ms", latency), ) } if lastErr != nil { log.Warn("代理测速失败", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("latency_ms", lastLatency), logger.F("error", lastErr.Error()), ) return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: lastLatency, Engine: engine, Error: lastErr.Error()} } log.Warn("代理测速失败", logger.F("proxy_id", proxyId), logger.F("engine", engine), logger.F("error", "测速失败")) return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: lastLatency, Engine: engine, Error: "测速失败"} } func buildSpeedTestHTTPClient( src string, proxyId string, proxies []config.BrowserProxy, xrayMgr *XrayManager, singboxMgr *SingBoxManager, clashMgr *ClashManager, connectorType string, cfg *SpeedTestConfig, ) (*http.Client, error) { timeout := DefaultSpeedTestConfig.Timeout prepareTimeout := DefaultSpeedTestConfig.TCPTimeout if cfg != nil { if cfg.Timeout > 0 { timeout = cfg.Timeout } if cfg.TCPTimeout > 0 { prepareTimeout = cfg.TCPTimeout } } if prepareTimeout <= 0 { prepareTimeout = timeout } type clientResult struct { client *http.Client err error } resultCh := make(chan clientResult, 1) go func() { client, err := buildProxyHTTPClient(src, proxyId, proxies, xrayMgr, singboxMgr, clashMgr, connectorType, timeout) resultCh <- clientResult{client: client, err: err} }() timer := time.NewTimer(prepareTimeout) defer timer.Stop() select { case result := <-resultCh: return result.client, result.err case <-timer.C: return nil, fmt.Errorf("代理准备超时(%dms)", prepareTimeout.Milliseconds()) } } func primarySpeedTestURL(cfg *SpeedTestConfig) string { if cfg != nil { if urls := normalizeSpeedTestURLs(cfg.URLs); len(urls) > 0 { return urls[0] } } return strings.TrimSpace(DefaultSpeedTestURL) } func speedTestTargetURLs(cfg *SpeedTestConfig) []string { if cfg != nil { if urls := uniqueSpeedTestURLs(cfg.URLs); len(urls) > 0 { return urls } } return []string{DefaultSpeedTestURL} } func speedTestProbeEngine(src string, proxies []config.BrowserProxy, proxyId string, connectorType string) string { resolution, err := ResolveProxyKernelForConnector(src, proxies, proxyId, connectorType) if err != nil { if resolution.Kernel != "" { return resolution.Kernel } return config.NormalizeBrowserConnectorType(connectorType) } if resolution.Kernel == ProxyKernelNative { return "native" } return resolution.Kernel } func doSpeedTestRequest(client *http.Client, testURL string) (int64, int, error) { latency, statusCode, err := doSpeedTestRequestWithMethod(client, http.MethodHead, testURL) if err != nil || statusCode != http.StatusMethodNotAllowed { if err != nil { return latency, statusCode, err } secondLatency, secondStatusCode, secondErr := doSpeedTestRequestWithMethod(client, http.MethodHead, testURL) if secondErr == nil { return secondLatency, secondStatusCode, nil } return latency, statusCode, nil } return doSpeedTestRequestWithMethod(client, http.MethodGet, testURL) } func doSpeedTestRequestWithMethod(client *http.Client, method string, testURL string) (int64, int, error) { start := time.Now() req, err := http.NewRequest(method, testURL, nil) if err != nil { return 0, 0, fmt.Errorf("测速请求创建失败: %w", err) } resp, err := client.Do(req) latency := time.Since(start).Milliseconds() if err != nil { return latency, 0, err } _ = resp.Body.Close() return latency, resp.StatusCode, nil } func speedTestStatusOK(statusCode int, cfg *SpeedTestConfig) bool { if cfg != nil && len(cfg.ExpectedStatus) > 0 { for _, expected := range cfg.ExpectedStatus { if statusCode == expected { return true } } return false } return isSpeedTestSuccessStatus(statusCode) } func mihomoURLTest(proxyId string, proxyInstance C.Proxy, testURL string, cfg *SpeedTestConfig) TestResult { timeout := DefaultSpeedTestConfig.Timeout if cfg != nil && cfg.Timeout > 0 { timeout = cfg.Timeout } expectedStatus, err := speedTestExpectedStatus(cfg) if err != nil { return TestResult{ProxyId: proxyId, Ok: false, Engine: "mihomo", Error: err.Error()} } adapter.UnifiedDelay.Store(true) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() delay, err := proxyInstance.URLTest(ctx, testURL, expectedStatus) latency := int64(delay) if ctx.Err() != nil { return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Engine: "mihomo", Error: "测速超时"} } if err != nil || delay == 0 { if err != nil { return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Engine: "mihomo", Error: err.Error()} } return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Engine: "mihomo", Error: "mihomo 延迟测试无结果"} } return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency, Engine: "mihomo"} } func speedTestExpectedStatus(cfg *SpeedTestConfig) (utils.IntRanges[uint16], error) { if cfg == nil || len(cfg.ExpectedStatus) == 0 { return nil, nil } items := make([]string, 0, len(cfg.ExpectedStatus)) for _, status := range cfg.ExpectedStatus { if status <= 0 || status > 65535 { return nil, fmt.Errorf("无效测速状态码: %d", status) } items = append(items, strconv.Itoa(status)) } return utils.NewUnsignedRangesFromList[uint16](items) }