Files
Cyrene_v2/backend/services/gateway/ws/hub.go
T
Aska 99806fefad feat: add gateway API, voice service, WebSocket hub, and startup entry
- services/gateway/api: net/http router with JWT auth, stub login, 6 tests
- services/gateway/ws: WebSocket hub with echo support
- services/voice: STT/TTS provider chain with fallback, 6 tests
- cmd/cyrene/main.go: single binary startup, 108 tests total
- Smoke test: health check + login verified on :8080
2026-08-09 21:09:43 +08:00

176 lines
3.5 KiB
Go

// Package ws provides WebSocket connection management for Cyrene.
package ws
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Hub manages all active WebSocket connections.
type Hub struct {
mu sync.RWMutex
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan []byte
}
// Client represents a single WebSocket connection.
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
UserID string
closed bool
mu sync.Mutex
}
// Message represents a WebSocket message envelope.
type Message struct {
Type string `json:"type"`
Content string `json:"content,omitempty"`
SessionID string `json:"session_id,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Error string `json:"error,omitempty"`
}
// NewHub creates and starts a new WebSocket hub.
func NewHub() *Hub {
h := &Hub{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte, 256),
}
go h.run()
return h
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("[ws] client connected (%d total)", len(h.clients))
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
log.Printf("[ws] client disconnected (%d remaining)", len(h.clients))
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
// Client too slow, skip
}
}
h.mu.RUnlock()
}
}
}
// HandleUpgrade upgrades an HTTP connection to WebSocket.
func (h *Hub) HandleUpgrade(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("[ws] upgrade error: %v", err)
return
}
userID := r.URL.Query().Get("user_id")
if userID == "" {
userID = "anonymous"
}
client := &Client{
hub: h,
conn: conn,
send: make(chan []byte, 64),
UserID: userID,
}
h.register <- client
go client.writePump()
go client.readPump()
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, msgBytes, err := c.conn.ReadMessage()
if err != nil {
break
}
var msg Message
if err := json.Unmarshal(msgBytes, &msg); err != nil {
continue
}
// Echo back for now (will route to AI-Core later)
reply := Message{
Type: "response",
Content: "echo: " + msg.Content,
SessionID: msg.SessionID,
}
replyBytes, _ := json.Marshal(reply)
c.send <- replyBytes
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for msg := range c.send {
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
}
}
// Broadcast sends a message to all connected clients.
func (h *Hub) Broadcast(msg Message) error {
data, err := json.Marshal(msg)
if err != nil {
return err
}
h.broadcast <- data
return nil
}
// Count returns the number of connected clients.
func (h *Hub) Count() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.clients)
}
// UpgradeHandler returns an http.HandlerFunc for WebSocket upgrades.
func (h *Hub) UpgradeHandler() http.HandlerFunc {
return h.HandleUpgrade
}