The WS hub and platform bridge now call Pipeline.FinishRound when the response stream completes, recording the assistant reply and invoking the LLM memory extractor asynchronously.
230 lines
5.2 KiB
Go
230 lines
5.2 KiB
Go
// Package ws provides WebSocket connection management.
|
|
package ws
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
"net/http"
|
|
"strings"
|
|
"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)
|
|
// FinishRound records the assistant reply and triggers post-round work
|
|
// (e.g. memory extraction) after the response stream completes.
|
|
FinishRound(sessionID, userID, assistantReply string)
|
|
}
|
|
|
|
// 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()
|
|
logger.Infof("[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 {
|
|
logger.Infof("[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
|
|
var fullContent strings.Builder
|
|
for token := range tokenCh {
|
|
fullContent.WriteString(token.Content)
|
|
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})
|
|
}
|
|
|
|
// Record the reply and trigger post-round work (memory extraction etc.)
|
|
c.hub.orchestrator.FinishRound(msg.SessionID, c.UserID, fullContent.String())
|
|
}
|
|
|
|
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
|
|
}
|