Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

205 lines
6.9 KiB
Go

package agentstatus
import (
"context"
"log/slog"
"net"
"net/http"
"net/url"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/runtimecmd"
)
// logNetworkProbe records each provider's network probe outcome so a confusing
// "service API unreachable but the agent works" report is diagnosable from logs.
func logNetworkProbe(
provider string,
registry NetworkEndpointStatus,
api *NetworkEndpointStatus,
proxy *NetworkProxyStatus,
) {
apiReachable := "skipped"
apiEndpoint := ""
if api != nil {
apiReachable = boolWord(api.Reachable)
apiEndpoint = api.Endpoint
}
proxyConfigured := false
proxyReachable := false
proxyURL := ""
if proxy != nil {
proxyConfigured = proxy.Configured
proxyReachable = proxy.Reachable
proxyURL = proxy.URL
}
slog.Info("agent network probe",
"provider", provider,
"registry_reachable", registry.Reachable,
"registry_endpoint", registry.Endpoint,
"api_reachable", apiReachable,
"api_endpoint", apiEndpoint,
"proxy_configured", proxyConfigured,
"proxy_reachable", proxyReachable,
"proxy_url", proxyURL,
)
}
func boolWord(value bool) string {
if value {
return "reachable"
}
return "unreachable"
}
// NetworkEndpointStatus is the verdict of probing a single endpoint: whether it
// was reachable and which URL answered (or was tried).
type NetworkEndpointStatus struct {
Reachable bool
Endpoint string
ReasonCode string
}
// NetworkStatus splits connectivity into the links the agent actually needs,
// reported separately: the npm registry (install/upgrade path), the provider's
// API (run/login path), and the proxy in front of them. ProviderAPI is nil for
// providers with no known public endpoint or when the CLI is configured with a
// custom API key (so the default endpoint isn't what it talks to). Proxy is nil
// only if proxy resolution itself could not run.
type NetworkStatus struct {
Registry NetworkEndpointStatus
ProviderAPI *NetworkEndpointStatus
Proxy *NetworkProxyStatus
}
// NetworkProxyStatus reports whether an HTTP proxy is in effect (from the
// HTTP(S)_PROXY env or the macOS system proxy), its host:port, and whether that
// proxy is reachable.
type NetworkProxyStatus struct {
Configured bool
URL string
Reachable bool
ReasonCode string
}
// networkProbeAttemptTimeout bounds each endpoint attempt so a blocked host fails
// over quickly. Connection refusals / DNS failures return well under this; only a
// black-holed network waits the full window.
const networkProbeAttemptTimeout = 1500 * time.Millisecond
// probeEndpoint issues a cheap HEAD request. Any HTTP response means the host was
// reached (even a 4xx/405 proves connectivity); only a transport-level failure
// counts as unreachable.
func (s Service) probeEndpoint(ctx context.Context, endpoint string) NetworkEndpointStatus {
attemptCtx, cancel := context.WithTimeout(ctx, networkProbeAttemptTimeout)
defer cancel()
request, err := http.NewRequestWithContext(attemptCtx, http.MethodHead, endpoint, nil)
if err != nil {
return NetworkEndpointStatus{Reachable: false, Endpoint: endpoint, ReasonCode: "network_error"}
}
response, err := s.httpClient().Do(request)
if err != nil {
return NetworkEndpointStatus{Reachable: false, Endpoint: endpoint, ReasonCode: "network_error"}
}
_ = response.Body.Close()
return NetworkEndpointStatus{Reachable: true, Endpoint: endpoint}
}
// probeRegistry checks the npm registry fallback chain in the same ranked order
// install uses for the requested package. When none answer, it reports the
// primary registry as the host that could not be reached.
func (s Service) probeRegistry(ctx context.Context, packageName string) NetworkEndpointStatus {
registries := s.rankedAgentNPMRegistries(ctx, packageName)
for _, registry := range registries {
status := s.probeEndpoint(ctx, npmRegistryPackageEndpoint(registry, packageName))
if status.Reachable {
status.Endpoint = registry
return status
}
}
return NetworkEndpointStatus{
Reachable: false,
Endpoint: s.primaryAgentNPMRegistry(),
ReasonCode: "network_error",
}
}
// providerAPIEndpoints lists the actual base URL(s) the provider's CLI talks to
// at run/login time, in priority order. Reachability of ANY of them counts — we
// only check connectivity (DNS/TLS/proxy/region), not which account/mode is in
// use. Empty for providers with no known public endpoint (the check is skipped).
//
// Codex's base URL depends on auth mode (verified against the codex source,
// codex-rs/model-provider-info/src/lib.rs `to_api_provider`): a ChatGPT login
// uses https://chatgpt.com/backend-api/codex, while an API key uses
// https://api.openai.com/v1. Probing only api.openai.com produced a false
// "unreachable" for ChatGPT-login users where that host is blocked but
// chatgpt.com — what codex actually uses — is reachable.
func providerAPIEndpoints(provider string) []string {
if status, ok := migratedProviderStatus(provider); ok {
return append([]string(nil), status.APIEndpoints...)
}
return nil
}
// probeProviderAPI checks the provider's API endpoint(s) — reachable if any one
// answers. Returns nil when the provider has no known endpoint, or when the CLI
// is configured with a custom API key / endpoint (env or on-disk config), since
// the user points at their own base URL/gateway and probing the defaults would
// mislead.
func (s Service) probeProviderAPI(ctx context.Context, provider string) *NetworkEndpointStatus {
endpoints := providerAPIEndpoints(provider)
if len(endpoints) == 0 || s.providerUsesCustomConfig(provider) {
return nil
}
var status NetworkEndpointStatus
for _, endpoint := range endpoints {
status = s.probeEndpoint(ctx, endpoint)
if status.Reachable {
return &status
}
}
return &status
}
// probeProxy detects whether an HTTP proxy is in effect for outbound requests
// and, if so, whether it is reachable. Resolution mirrors the proxy the probe
// HTTP client itself uses (env first, then the macOS system proxy).
func (s Service) probeProxy(ctx context.Context) *NetworkProxyStatus {
resolve := s.ResolveProxy
if resolve == nil {
resolve = runtimecmd.DynamicProxyFunc()
}
request, err := http.NewRequest(http.MethodHead, officialNPMRegistry, nil)
if err != nil {
return nil
}
proxyURL, err := resolve(request)
if err != nil || proxyURL == nil {
return &NetworkProxyStatus{Configured: false}
}
addr := proxyAddr(proxyURL)
status := &NetworkProxyStatus{Configured: true, URL: addr}
dialer := net.Dialer{Timeout: networkProbeAttemptTimeout}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
status.ReasonCode = "network_error"
return status
}
_ = conn.Close()
status.Reachable = true
return status
}
// proxyAddr renders a proxy URL as host:port, inferring the default port from
// the scheme when none is given.
func proxyAddr(u *url.URL) string {
if u.Port() != "" {
return u.Host
}
if u.Scheme == "https" {
return u.Hostname() + ":443"
}
return u.Hostname() + ":80"
}