feat: add web gateway hot reload and polling state sync (#1684)

* feat(gateway): support hot reload and empty startup

- extract gateway runtime into pkg/gateway
- add gateway.hot_reload config with default and example values
- allow starting the gateway without a default model via --allow-empty
- stop treating missing enabled channels as a startup error
- update related tests

* feat: replace gateway SSE updates with polling-based state sync

- remove gateway SSE broadcasting and event endpoint
- add polling-based gateway status refresh with stopping state handling
- detect when gateway restart is required after default model changes
- resolve gateway health and websocket proxy targets from configured host
- update gateway UI labels and add backend/frontend test coverage
This commit is contained in:
wenjie
2026-03-17 18:46:00 +08:00
committed by GitHub
parent 11207186c8
commit 8a44410e37
24 changed files with 700 additions and 543 deletions
+7 -17
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/sipeed/picoclaw/pkg/config"
@@ -22,20 +21,13 @@ func (h *Handler) registerPicoRoutes(mux *http.ServeMux) {
// WebSocket proxy: forward /pico/ws to gateway
// This allows the frontend to connect via the same port as the web UI,
// avoiding the need to expose extra ports for WebSocket communication.
wsProxy := h.createWsProxy()
mux.HandleFunc("GET /pico/ws", h.handleWebSocketProxy(wsProxy))
mux.HandleFunc("GET /pico/ws", h.handleWebSocketProxy())
}
// createWsProxy creates a reverse proxy to the gateway WebSocket endpoint.
// The gateway port is read from the configuration.
// createWsProxy creates a reverse proxy to the current gateway WebSocket endpoint.
// The gateway bind host and port are resolved from the latest configuration.
func (h *Handler) createWsProxy() *httputil.ReverseProxy {
cfg, err := config.LoadConfig(h.configPath)
gatewayPort := 18790 // default
if err == nil && cfg.Gateway.Port != 0 {
gatewayPort = cfg.Gateway.Port
}
gatewayURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort))
wsProxy := httputil.NewSingleHostReverseProxy(gatewayURL)
wsProxy := httputil.NewSingleHostReverseProxy(h.gatewayProxyURL())
wsProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, "Gateway unavailable: "+err.Error(), http.StatusBadGateway)
}
@@ -43,12 +35,10 @@ func (h *Handler) createWsProxy() *httputil.ReverseProxy {
}
// handleWebSocketProxy wraps a reverse proxy to handle WebSocket connections.
// It ensures the Connection and Upgrade headers are properly forwarded.
func (h *Handler) handleWebSocketProxy(proxy *httputil.ReverseProxy) http.HandlerFunc {
// The reverse proxy forwards the incoming upgrade handshake as-is.
func (h *Handler) handleWebSocketProxy() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set headers for WebSocket upgrade
r.Header.Set("Connection", "upgrade")
r.Header.Set("Upgrade", "websocket")
proxy := h.createWsProxy()
proxy.ServeHTTP(w, r)
}
}