Files
Cyrene_v2/backend/services/gateway/ws/hub.go
T
Aska 6811480786 feat(gateway): wire real services into Gateway API handlers
- Router accepts full Dependencies (memory/voice/orchestrator/WShub)
- Memory API: CRUD + search endpoints with PostgreSQL persistence
- Voice API: STT/TTS endpoints
- Admin dashboard: real uptime + service status
- WebSocket chat: routes to AI-Core orchestrator pipeline
- APIAdapter bridges memory domain model to REST interface
- Smoke test: memory CRUD + session creation verified
2026-08-12 12:04:31 +08:00

221 lines
4.7 KiB
Go

// Package ws provides WebSocket connection management.
package ws
import (
"context"
"encoding/json"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Orchestrator is the interface for the AI dialogue pipeline.
type Orchestrator interface {
ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error)
}
// 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
orchestrator Orchestrator
}
// Client represents a single WebSocket connection.
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
UserID string
SessionID string
closed bool
mu sync.Mutex
}
// WSMessage represents a client-server message.
type WSMessage 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"`
Devices []any `json:"devices,omitempty"`
}
// NewHub creates a WebSocket hub with an optional orchestrator.
func NewHub(orch Orchestrator) *Hub {
h := &Hub{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte, 256),
orchestrator: orch,
}
go h.run()
return h
}
func (h *Hub) run() {
for {
select {
case c := <-h.register:
h.mu.Lock()
h.clients[c] = true
n := len(h.clients)
h.mu.Unlock()
log.Printf("[ws] client connected (total=%d)", n)
case c := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.send)
}
h.mu.Unlock()
case msg := <-h.broadcast:
h.mu.RLock()
for c := range h.clients {
select {
case c.send <- msg:
default:
}
}
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
}
uid := r.URL.Query().Get("user_id")
if uid == "" { uid = "anonymous" }
client := &Client{
hub: h,
conn: conn,
send: make(chan []byte, 128),
UserID: uid,
}
h.register <- client
go client.writePump()
go client.readPump()
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, raw, err := c.conn.ReadMessage()
if err != nil {
break
}
var msg WSMessage
if err := json.Unmarshal(raw, &msg); err != nil {
continue
}
switch msg.Type {
case "message":
c.handleChatMessage(msg)
default:
c.sendJSON(WSMessage{Type: "response", Content: "unknown message type: " + msg.Type})
}
}
}
func (c *Client) handleChatMessage(msg WSMessage) {
c.SessionID = msg.SessionID
// If orchestrator not available, echo
if c.hub.orchestrator == nil {
c.sendJSON(WSMessage{Type: "response", Content: "[系统] AI 核心未配置,当前为 echo 模式: " + msg.Content, SessionID: msg.SessionID})
return
}
// Call orchestrator
tokenCh, intent, err := c.hub.orchestrator.ProcessInput(context.Background(), msg.SessionID, msg.Content)
if err != nil {
c.sendJSON(WSMessage{Type: "error", Error: err.Error()})
return
}
// Send intent info
if intent != nil {
c.sendJSON(WSMessage{Type: "response", Content: "[Intent: " + intent.Type + "]", SessionID: msg.SessionID})
}
// Stream tokens
for token := range tokenCh {
if token.Done {
c.sendJSON(WSMessage{Type: "stream_end", SessionID: msg.SessionID})
break
}
c.sendJSON(WSMessage{Type: "stream_chunk", Content: token.Content, SessionID: msg.SessionID})
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for msg := range c.send {
c.mu.Lock()
err := c.conn.WriteMessage(websocket.TextMessage, msg)
c.mu.Unlock()
if err != nil {
return
}
}
}
func (c *Client) sendJSON(v any) {
data, _ := json.Marshal(v)
select {
case c.send <- data:
default:
}
}
// Broadcast sends a message to all connected clients.
func (h *Hub) Broadcast(msg any) 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
}