fix(host): modernize default host selection order

This commit is contained in:
lc6464
2026-04-13 22:49:25 +08:00
parent 448027c02a
commit e7b3654313
13 changed files with 497 additions and 98 deletions
+2 -2
View File
@@ -503,7 +503,7 @@ func TestDefaultConfig_Temperature(t *testing.T) {
func TestDefaultConfig_Gateway(t *testing.T) {
cfg := DefaultConfig()
if cfg.Gateway.Host != "127.0.0.1" {
if cfg.Gateway.Host != "localhost" {
t.Error("Gateway host should have default value")
}
if cfg.Gateway.Port == 0 {
@@ -739,7 +739,7 @@ func TestConfig_Complete(t *testing.T) {
if cfg.Agents.Defaults.MaxToolIterations == 0 {
t.Error("MaxToolIterations should not be zero")
}
if cfg.Gateway.Host != "127.0.0.1" {
if cfg.Gateway.Host != "localhost" {
t.Error("Gateway host should have default value")
}
if cfg.Gateway.Port == 0 {
+1 -1
View File
@@ -259,7 +259,7 @@ func DefaultConfig() *Config {
},
},
Gateway: GatewayConfig{
Host: "127.0.0.1",
Host: "localhost",
Port: 18790,
HotReload: false,
LogLevel: DefaultGatewayLogLevel,
+97 -7
View File
@@ -2,8 +2,10 @@ package config
import (
"encoding/json"
"net"
"os"
"strings"
"sync"
"github.com/sipeed/picoclaw/pkg/logger"
)
@@ -50,17 +52,105 @@ func EffectiveGatewayLogLevel(cfg *Config) string {
return normalizeGatewayLogLevel(cfg.Gateway.LogLevel)
}
var (
gatewayIPFamiliesOnce sync.Once
gatewayHasIPv4 bool
gatewayHasIPv6 bool
)
func detectGatewayIPFamilies() (bool, bool) {
gatewayIPFamiliesOnce.Do(func() {
if ips, err := net.LookupIP("localhost"); err == nil {
for _, ip := range ips {
if ip == nil {
continue
}
if ip.To4() != nil {
gatewayHasIPv4 = true
continue
}
gatewayHasIPv6 = true
}
}
if gatewayHasIPv4 && gatewayHasIPv6 {
return
}
if addrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok || ipnet.IP == nil {
continue
}
if ipnet.IP.To4() != nil {
gatewayHasIPv4 = true
continue
}
gatewayHasIPv6 = true
}
}
})
return gatewayHasIPv4, gatewayHasIPv6
}
func selectAdaptiveGatewayLoopbackHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "localhost"
case hasIPv6:
return "::1"
case hasIPv4:
return "127.0.0.1"
default:
return "localhost"
}
}
func selectAdaptiveGatewayAnyHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "::"
case hasIPv6:
return "::"
case hasIPv4:
return "0.0.0.0"
default:
return "::"
}
}
func resolveAdaptiveGatewayLoopbackHost() string {
hasIPv4, hasIPv6 := detectGatewayIPFamilies()
return selectAdaptiveGatewayLoopbackHost(hasIPv4, hasIPv6)
}
func resolveAdaptiveGatewayAnyHost() string {
hasIPv4, hasIPv6 := detectGatewayIPFamilies()
return selectAdaptiveGatewayAnyHost(hasIPv4, hasIPv6)
}
func normalizeGatewayHost(host string) string {
host = strings.TrimSpace(host)
if host != "" {
return host
if host == "" {
host = strings.TrimSpace(DefaultConfig().Gateway.Host)
}
defaultHost := strings.TrimSpace(DefaultConfig().Gateway.Host)
if defaultHost == "" {
return "127.0.0.1"
if host == "" {
host = "localhost"
}
return defaultHost
if strings.EqualFold(host, "localhost") {
return resolveAdaptiveGatewayLoopbackHost()
}
trimmed := strings.Trim(host, "[]")
if ip := net.ParseIP(trimmed); ip != nil && ip.IsUnspecified() {
return resolveAdaptiveGatewayAnyHost()
}
return host
}
func resolveGatewayHostFromEnv(baseHost string) string {
@@ -74,7 +164,7 @@ func resolveGatewayHostFromEnv(baseHost string) string {
return normalizeGatewayHost(baseHost)
}
return envHost
return normalizeGatewayHost(envHost)
}
// ResolveGatewayLogLevel reads the configured gateway log level without triggering
+19 -4
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
@@ -40,8 +39,9 @@ func TestLoadConfig_GatewayHostBlankEnvFallsBackToConfigHost(t *testing.T) {
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
if cfg.Gateway.Host != "localhost" {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, "localhost")
want := normalizeGatewayHost("localhost")
if cfg.Gateway.Host != want {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, want)
}
}
@@ -54,8 +54,23 @@ func TestLoadConfig_GatewayHostBlankEnvAndConfigFallsBackToDefault(t *testing.T)
t.Fatalf("LoadConfig() error: %v", err)
}
defaultHost := strings.TrimSpace(DefaultConfig().Gateway.Host)
defaultHost := normalizeGatewayHost(DefaultConfig().Gateway.Host)
if cfg.Gateway.Host != defaultHost {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, defaultHost)
}
}
func TestLoadConfig_GatewayHostEnvWildcardUsesAdaptiveAnyHost(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, "localhost")
t.Setenv(EnvGatewayHost, " 0.0.0.0 ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
want := normalizeGatewayHost("0.0.0.0")
if cfg.Gateway.Host != want {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, want)
}
}
+8 -5
View File
@@ -3,10 +3,12 @@ package gateway
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -217,7 +219,8 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
runningServices.HealthServer.SetReloadFunc(reloadTrigger)
agentLoop.SetReloadFunc(reloadTrigger)
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
listenAddr := net.JoinHostPort(cfg.Gateway.Host, strconv.Itoa(cfg.Gateway.Port))
fmt.Printf("✓ Gateway started on %s\n", listenAddr)
fmt.Println("Press Ctrl+C to stop")
ctx, cancel := context.WithCancel(context.Background())
@@ -390,7 +393,7 @@ func setupAndStartServices(
fmt.Println("⚠ Warning: No channels enabled")
}
addr := fmt.Sprintf("%s:%d", cfg.Gateway.Host, cfg.Gateway.Port)
addr := net.JoinHostPort(cfg.Gateway.Host, strconv.Itoa(cfg.Gateway.Port))
runningServices.authToken = authToken
runningServices.HealthServer = health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port, authToken)
runningServices.ChannelManager.SetupHTTPServer(addr, runningServices.HealthServer)
@@ -409,10 +412,10 @@ func setupAndStartServices(
voiceAgent.Start(vaCtx)
}
healthAddr := net.JoinHostPort(cfg.Gateway.Host, strconv.Itoa(cfg.Gateway.Port))
fmt.Printf(
"✓ Health endpoints available at http://%s:%d/health, /ready and /reload (POST)\n",
cfg.Gateway.Host,
cfg.Gateway.Port,
"✓ Health endpoints available at http://%s/health, /ready and /reload (POST)\n",
healthAddr,
)
stateManager := state.NewManager(cfg.WorkspacePath())
+3 -2
View File
@@ -4,10 +4,11 @@ import (
"context"
"crypto/subtle"
"encoding/json"
"fmt"
"maps"
"net"
"net/http"
"os"
"strconv"
"sync"
"time"
)
@@ -49,7 +50,7 @@ func NewServer(host string, port int, token string) *Server {
mux.HandleFunc("/ready", s.readyHandler)
mux.HandleFunc("/reload", s.reloadHandler)
addr := fmt.Sprintf("%s:%d", host, port)
addr := net.JoinHostPort(host, strconv.Itoa(port))
s.server = &http.Server{
Addr: addr,
Handler: mux,
+10
View File
@@ -305,6 +305,16 @@ func TestNewServer(t *testing.T) {
}
}
func TestNewServer_IPv6ListenAddrFormatting(t *testing.T) {
s := NewServer("::", 18790, "")
if s.server == nil {
t.Fatal("server should be initialized")
}
if s.server.Addr != "[::]:18790" {
t.Fatalf("server.Addr = %q, want %q", s.server.Addr, "[::]:18790")
}
}
func TestStartContext_Cancellation(t *testing.T) {
s := NewServer("127.0.0.1", 0, "")