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
This commit is contained in:
Aska committed 2026-08-12 12:04:31 +08:00
1 parent 4a71965686
commit 6811480786
5 files changed
+448 -111

No files matched your search

+15 -2
View File
@@ -177,9 +177,22 @@ func main() {
// === 12. Gateway === // === 12. Gateway ===
authMgr := api.NewStubAuthManager() authMgr := api.NewStubAuthManager()
apiRouter := api.New(authMgr)
wsHub := ws.NewHub() // Build dependencies for gateway handlers
gwDeps := api.Dependencies{
AuthMgr: authMgr,
Orchestrator: orch,
}
if memSvc != nil {
gwDeps.MemorySvc = memory.NewAPIAdapter(memSvc)
}
gwDeps.VoiceSvc = voiceSvc
wsHub := ws.NewHub(orch)
gwDeps.WSHub = wsHub
apiRouter := api.New(gwDeps)
wsMux := http.NewServeMux() wsMux := http.NewServeMux()
wsMux.HandleFunc("/ws/chat", wsHub.UpgradeHandler()) wsMux.HandleFunc("/ws/chat", wsHub.UpgradeHandler())
+243 -50
View File
@@ -1,58 +1,115 @@
// Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway. // Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway.
// Uses only the standard library net/http to minimize dependencies.
package api package api
import ( import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings" "strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
) )
// Router handles HTTP routing with middleware support. // --- Interfaces for injected services ---
type Router struct {
mux *http.ServeMux
authMgr AuthManager
}
// AuthManager is the interface for JWT authentication. // AuthManager validates JWT tokens.
type AuthManager interface { type AuthManager interface {
ValidateToken(token string) (userID, username, role string, err error) ValidateToken(token string) (userID, username, role string, err error)
} }
// MemoryEntry is a simplified memory entry for API responses.
type MemoryEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Content string `json:"content"`
Summary string `json:"summary"`
Keywords []string `json:"keywords"`
Category string `json:"category"`
Priority float64 `json:"priority"`
}
// MemorySearcher handles memory search queries.
type MemorySearcher interface {
Search(ctx context.Context, userID, query string, limit int) (any, error)
List(ctx context.Context, userID string, limit int) (any, error)
Add(ctx context.Context, userID, content, category string, priority float64) (any, error)
Get(ctx context.Context, id string) (any, error)
Delete(ctx context.Context, id string) error
}
// VoiceService handles STT and TTS.
type VoiceService interface {
Recognize(ctx context.Context, audioData []byte) (string, error)
Synthesize(ctx context.Context, text string, voice string) ([]byte, error)
}
// OrchestratorPipeline handles the main dialogue flow.
type OrchestratorPipeline interface {
ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error)
}
// WebSocketHub manages WS connections.
type WebSocketHub interface {
UpgradeHandler() http.HandlerFunc
}
// Dependencies holds all injectable services.
type Dependencies struct {
AuthMgr AuthManager
MemorySvc MemorySearcher
VoiceSvc VoiceService
Orchestrator OrchestratorPipeline
WSHub WebSocketHub
}
// Router handles HTTP routing with middleware support.
type Router struct {
mux *http.ServeMux
deps Dependencies
}
// New creates a new Router with all routes configured. // New creates a new Router with all routes configured.
func New(authMgr AuthManager) *Router { func New(deps Dependencies) *Router {
r := &Router{ r := &Router{mux: http.NewServeMux(), deps: deps}
mux: http.NewServeMux(),
authMgr: authMgr,
}
r.setupRoutes() r.setupRoutes()
return r return r
} }
func (r *Router) setupRoutes() { func (r *Router) setupRoutes() {
// Public endpoints // Public
r.mux.HandleFunc("/api/v1/health", r.healthHandler) r.mux.HandleFunc("/api/v1/health", r.healthHandler)
r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler) r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler)
r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler) r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler)
// Protected endpoints // Protected
r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler)) r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler))
r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler)) r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler))
// Memory
r.mux.HandleFunc("/api/v1/memory/search", r.authMW(r.memorySearchHandler))
r.mux.HandleFunc("/api/v1/memory", r.authMW(r.memoryHandler))
r.mux.HandleFunc("/api/v1/memory/", r.authMW(r.memoryHandler))
// Voice
r.mux.HandleFunc("/api/v1/voice/stt", r.authMW(r.voiceSTTHandler))
r.mux.HandleFunc("/api/v1/voice/tts", r.authMW(r.voiceTTSHandler))
// Admin
r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler))) r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler)))
} }
// ServeHTTP implements http.Handler. // ServeHTTP implements http.Handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// CORS
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
if req.Method == "OPTIONS" { if req.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
return return
} }
r.mux.ServeHTTP(w, req) r.mux.ServeHTTP(w, req)
} }
@@ -60,19 +117,18 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc { func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
header := req.Header.Get("Authorization") h := req.Header.Get("Authorization")
if header == "" || !strings.HasPrefix(strings.ToLower(header), "bearer ") { if h == "" || !strings.HasPrefix(strings.ToLower(h), "bearer ") {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid authorization"}) writeJSON(w, 401, M{"error": "unauthorized"})
return return
} }
token := header[7:] uid, uname, role, err := r.deps.AuthMgr.ValidateToken(h[7:])
userID, username, role, err := r.authMgr.ValidateToken(token)
if err != nil { if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"}) writeJSON(w, 401, M{"error": "invalid token"})
return return
} }
req.Header.Set("X-User-ID", userID) req.Header.Set("X-User-ID", uid)
req.Header.Set("X-Username", username) req.Header.Set("X-Username", uname)
req.Header.Set("X-Role", role) req.Header.Set("X-Role", role)
next(w, req) next(w, req)
} }
@@ -81,83 +137,209 @@ func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc {
func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc { func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
if req.Header.Get("X-Role") != "admin" { if req.Header.Get("X-Role") != "admin" {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "admin access required"}) writeJSON(w, 403, M{"error": "admin only"})
return return
} }
next(w, req) next(w, req)
} }
} }
// --- Public handlers --- // --- Handlers ---
func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) { func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "service": "cyrene-gateway"}) writeJSON(w, 200, M{"status": "ok", "service": "cyrene-gateway", "version": "2.0.0-dev"})
} }
func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" { if req.Method != "POST" {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST required"}) writeJSON(w, 405, M{"error": "POST required"})
return return
} }
var body struct { var body struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
if err := json.NewDecoder(req.Body).Decode(&body); err != nil { json.NewDecoder(req.Body).Decode(&body)
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
mgr, ok := r.authMgr.(*StubAuthManager) mgr, ok := r.deps.AuthMgr.(*StubAuthManager)
if !ok { if !ok {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "auth not configured"}) writeJSON(w, 500, M{"error": "auth not configured"})
return return
} }
token, err := mgr.Login(body.Username, body.Password) token, err := mgr.Login(body.Username, body.Password)
if err != nil { if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) writeJSON(w, 401, M{"error": "invalid credentials"})
return return
} }
writeJSON(w, http.StatusOK, map[string]string{"token": token}) writeJSON(w, 200, M{"token": token})
} }
func (r *Router) registerHandler(w http.ResponseWriter, _ *http.Request) { func (r *Router) registerHandler(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"message": "registration not implemented"}) writeJSON(w, 200, M{"message": "registration not implemented"})
} }
// --- Protected handlers --- // --- Sessions ---
func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) { func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) {
switch req.Method { switch req.Method {
case "GET": case "GET":
writeJSON(w, http.StatusOK, map[string]any{"sessions": []any{}}) writeJSON(w, 200, M{"sessions": []any{}})
case "POST": case "POST":
writeJSON(w, http.StatusCreated, map[string]string{"session_id": "stub-session"}) writeJSON(w, 201, M{"session_id": randID()})
default: default:
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) writeJSON(w, 405, M{"error": "method not allowed"})
} }
} }
func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) { func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) {
id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/") id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/")
if req.Method == "DELETE" { switch req.Method {
writeJSON(w, http.StatusNoContent, nil) case "DELETE":
return writeJSON(w, 204, nil)
case "GET":
writeJSON(w, 200, M{"session_id": id})
default:
writeJSON(w, 405, M{"error": "method not allowed"})
} }
writeJSON(w, http.StatusOK, map[string]string{"session_id": id})
} }
// --- Memory ---
func (r *Router) memorySearchHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
writeJSON(w, 405, M{"error": "GET required"})
return
}
if r.deps.MemorySvc == nil {
writeJSON(w, 503, M{"error": "memory service unavailable"})
return
}
q := req.URL.Query().Get("q")
uid := req.Header.Get("X-User-ID")
result, err := r.deps.MemorySvc.Search(req.Context(), uid, q, 10)
if err != nil {
writeJSON(w, 500, M{"error": err.Error()})
return
}
writeJSON(w, 200, M{"data": result})
}
func (r *Router) memoryHandler(w http.ResponseWriter, req *http.Request) {
if r.deps.MemorySvc == nil {
writeJSON(w, 503, M{"error": "memory service unavailable"})
return
}
// Extract ID from path: /api/v1/memory/{id}
path := strings.TrimPrefix(req.URL.Path, "/api/v1/memory")
path = strings.TrimPrefix(path, "/")
userID := req.Header.Get("X-User-ID")
switch {
case path == "" && req.Method == "GET":
result, _ := r.deps.MemorySvc.List(req.Context(), userID, 20)
writeJSON(w, 200, M{"data": result})
case path == "" && req.Method == "POST":
var body struct {
Content string `json:"content"`
Category string `json:"category"`
Priority float64 `json:"priority"`
}
json.NewDecoder(req.Body).Decode(&body)
if body.Category == "" { body.Category = "fact" }
mem, err := r.deps.MemorySvc.Add(req.Context(), userID, body.Content, body.Category, body.Priority)
if err != nil {
writeJSON(w, 500, M{"error": err.Error()})
return
}
writeJSON(w, 201, M{"data": mem})
case path != "" && req.Method == "DELETE":
r.deps.MemorySvc.Delete(req.Context(), path)
writeJSON(w, 204, nil)
case path != "" && req.Method == "GET":
mem, err := r.deps.MemorySvc.Get(req.Context(), path)
if err != nil {
writeJSON(w, 404, M{"error": "memory not found"})
return
}
writeJSON(w, 200, M{"data": mem})
default:
writeJSON(w, 405, M{"error": "method not allowed"})
}
}
// --- Voice ---
func (r *Router) voiceSTTHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
writeJSON(w, 405, M{"error": "POST required"})
return
}
if r.deps.VoiceSvc == nil {
writeJSON(w, 503, M{"error": "voice service unavailable"})
return
}
var body struct{ AudioData []byte `json:"audio_data"` }
json.NewDecoder(req.Body).Decode(&body)
text, err := r.deps.VoiceSvc.Recognize(req.Context(), body.AudioData)
if err != nil {
writeJSON(w, 500, M{"error": err.Error()})
return
}
writeJSON(w, 200, M{"text": text})
}
func (r *Router) voiceTTSHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
writeJSON(w, 405, M{"error": "POST required"})
return
}
if r.deps.VoiceSvc == nil {
writeJSON(w, 503, M{"error": "voice service unavailable"})
return
}
var body struct {
Text string `json:"text"`
Voice string `json:"voice"`
}
json.NewDecoder(req.Body).Decode(&body)
if body.Voice == "" { body.Voice = "zh-CN-XiaoxiaoNeural" }
audio, err := r.deps.VoiceSvc.Synthesize(req.Context(), body.Text, body.Voice)
if err != nil {
writeJSON(w, 500, M{"error": err.Error()})
return
}
writeJSON(w, 200, M{"audio_size": len(audio)})
}
// --- Admin ---
var startTime = time.Now()
func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) { func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, 200, M{
"dashboard": map[string]any{ "dashboard": M{
"status": "running", "status": "running",
"version": "2.0.0-dev", "version": "2.0.0-dev",
"uptime_sec": int(time.Since(startTime).Seconds()),
"services": M{
"memory": r.deps.MemorySvc != nil,
"voice": r.deps.VoiceSvc != nil,
"ai_core": r.deps.Orchestrator != nil,
},
}, },
}) })
} }
// --- Helpers --- // --- Helpers ---
type M = map[string]any
func writeJSON(w http.ResponseWriter, status int, data any) { func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
@@ -165,3 +347,14 @@ func writeJSON(w http.ResponseWriter, status int, data any) {
json.NewEncoder(w).Encode(data) json.NewEncoder(w).Encode(data)
} }
} }
func randID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func (r *Router) userID(req *http.Request) string { return req.Header.Get("X-User-ID") }
// Ensure unused imports don't cause issues
+5 -5
View File
@@ -8,7 +8,7 @@ import (
) )
func TestHealth(t *testing.T) { func TestHealth(t *testing.T) {
r := New(NewStubAuthManager()) r := New(Dependencies{AuthMgr: NewStubAuthManager()})
srv := httptest.NewServer(r) srv := httptest.NewServer(r)
defer srv.Close() defer srv.Close()
@@ -25,7 +25,7 @@ func TestHealth(t *testing.T) {
func TestLoginAndProtectedEndpoint(t *testing.T) { func TestLoginAndProtectedEndpoint(t *testing.T) {
auth := NewStubAuthManager() auth := NewStubAuthManager()
r := New(auth) r := New(Dependencies{AuthMgr: auth})
srv := httptest.NewServer(r) srv := httptest.NewServer(r)
defer srv.Close() defer srv.Close()
@@ -57,7 +57,7 @@ func TestLoginAndProtectedEndpoint(t *testing.T) {
func TestLoginWrongPassword(t *testing.T) { func TestLoginWrongPassword(t *testing.T) {
auth := NewStubAuthManager() auth := NewStubAuthManager()
r := New(auth) r := New(Dependencies{AuthMgr: auth})
srv := httptest.NewServer(r) srv := httptest.NewServer(r)
defer srv.Close() defer srv.Close()
@@ -75,7 +75,7 @@ func TestLoginWrongPassword(t *testing.T) {
func TestProtectedWithoutToken(t *testing.T) { func TestProtectedWithoutToken(t *testing.T) {
auth := NewStubAuthManager() auth := NewStubAuthManager()
r := New(auth) r := New(Dependencies{AuthMgr: auth})
srv := httptest.NewServer(r) srv := httptest.NewServer(r)
defer srv.Close() defer srv.Close()
@@ -115,7 +115,7 @@ func TestStubAuthManager(t *testing.T) {
} }
func TestCORSHeaders(t *testing.T) { func TestCORSHeaders(t *testing.T) {
r := New(NewStubAuthManager()) r := New(Dependencies{AuthMgr: NewStubAuthManager()})
srv := httptest.NewServer(r) srv := httptest.NewServer(r)
defer srv.Close() defer srv.Close()
+99 -54
View File
@@ -1,13 +1,16 @@
// Package ws provides WebSocket connection management for Cyrene. // Package ws provides WebSocket connection management.
package ws package ws
import ( import (
"context"
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"sync" "sync"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@@ -16,42 +19,50 @@ var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, 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. // Hub manages all active WebSocket connections.
type Hub struct { type Hub struct {
mu sync.RWMutex mu sync.RWMutex
clients map[*Client]bool clients map[*Client]bool
register chan *Client
register chan *Client unregister chan *Client
unregister chan *Client broadcast chan []byte
broadcast chan []byte orchestrator Orchestrator
} }
// Client represents a single WebSocket connection. // Client represents a single WebSocket connection.
type Client struct { type Client struct {
hub *Hub hub *Hub
conn *websocket.Conn conn *websocket.Conn
send chan []byte send chan []byte
UserID string UserID string
closed bool SessionID string
mu sync.Mutex closed bool
mu sync.Mutex
} }
// Message represents a WebSocket message envelope. // WSMessage represents a client-server message.
type Message struct { type WSMessage struct {
Type string `json:"type"` Type string `json:"type"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
SessionID string `json:"session_id,omitempty"` SessionID string `json:"session_id,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"` Timestamp int64 `json:"timestamp,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Devices []any `json:"devices,omitempty"`
} }
// NewHub creates and starts a new WebSocket hub. // NewHub creates a WebSocket hub with an optional orchestrator.
func NewHub() *Hub { func NewHub(orch Orchestrator) *Hub {
h := &Hub{ h := &Hub{
clients: make(map[*Client]bool), clients: make(map[*Client]bool),
register: make(chan *Client), register: make(chan *Client),
unregister: make(chan *Client), unregister: make(chan *Client),
broadcast: make(chan []byte, 256), broadcast: make(chan []byte, 256),
orchestrator: orch,
} }
go h.run() go h.run()
return h return h
@@ -60,28 +71,27 @@ func NewHub() *Hub {
func (h *Hub) run() { func (h *Hub) run() {
for { for {
select { select {
case client := <-h.register: case c := <-h.register:
h.mu.Lock() h.mu.Lock()
h.clients[client] = true h.clients[c] = true
n := len(h.clients)
h.mu.Unlock() h.mu.Unlock()
log.Printf("[ws] client connected (%d total)", len(h.clients)) log.Printf("[ws] client connected (total=%d)", n)
case client := <-h.unregister: case c := <-h.unregister:
h.mu.Lock() h.mu.Lock()
if _, ok := h.clients[client]; ok { if _, ok := h.clients[c]; ok {
delete(h.clients, client) delete(h.clients, c)
close(client.send) close(c.send)
} }
h.mu.Unlock() h.mu.Unlock()
log.Printf("[ws] client disconnected (%d remaining)", len(h.clients))
case message := <-h.broadcast: case msg := <-h.broadcast:
h.mu.RLock() h.mu.RLock()
for client := range h.clients { for c := range h.clients {
select { select {
case client.send <- message: case c.send <- msg:
default: default:
// Client too slow, skip
} }
} }
h.mu.RUnlock() h.mu.RUnlock()
@@ -97,20 +107,16 @@ func (h *Hub) HandleUpgrade(w http.ResponseWriter, r *http.Request) {
return return
} }
userID := r.URL.Query().Get("user_id") uid := r.URL.Query().Get("user_id")
if userID == "" { if uid == "" { uid = "anonymous" }
userID = "anonymous"
}
client := &Client{ client := &Client{
hub: h, hub: h,
conn: conn, conn: conn,
send: make(chan []byte, 64), send: make(chan []byte, 128),
UserID: userID, UserID: uid,
} }
h.register <- client h.register <- client
go client.writePump() go client.writePump()
go client.readPump() go client.readPump()
} }
@@ -122,38 +128,77 @@ func (c *Client) readPump() {
}() }()
for { for {
_, msgBytes, err := c.conn.ReadMessage() _, raw, err := c.conn.ReadMessage()
if err != nil { if err != nil {
break break
} }
var msg WSMessage
var msg Message if err := json.Unmarshal(raw, &msg); err != nil {
if err := json.Unmarshal(msgBytes, &msg); err != nil {
continue continue
} }
// Echo back for now (will route to AI-Core later) switch msg.Type {
reply := Message{ case "message":
Type: "response", c.handleChatMessage(msg)
Content: "echo: " + msg.Content, default:
SessionID: msg.SessionID, c.sendJSON(WSMessage{Type: "response", Content: "unknown message type: " + msg.Type})
} }
replyBytes, _ := json.Marshal(reply) }
c.send <- replyBytes }
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() { func (c *Client) writePump() {
defer c.conn.Close() defer c.conn.Close()
for msg := range c.send { for msg := range c.send {
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { c.mu.Lock()
err := c.conn.WriteMessage(websocket.TextMessage, msg)
c.mu.Unlock()
if err != nil {
return 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. // Broadcast sends a message to all connected clients.
func (h *Hub) Broadcast(msg Message) error { func (h *Hub) Broadcast(msg any) error {
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return err return err
+86
View File
@@ -0,0 +1,86 @@
package memory
import "context"
// APIAdapter adapts the memory Service to a simplified API interface.
// This bridges between the rich domain model and the Gateway's REST handler expectations.
type APIAdapter struct {
svc *Service
}
// APIEntry is the simplified memory entry for REST API responses.
type APIEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Content string `json:"content"`
Summary string `json:"summary"`
Keywords []string `json:"keywords"`
Category string `json:"category"`
Priority float64 `json:"priority"`
}
// NewAPIAdapter wraps a Service for REST API consumption.
func NewAPIAdapter(svc *Service) *APIAdapter {
return &APIAdapter{svc: svc}
}
func (a *APIAdapter) Search(ctx context.Context, userID, query string, limit int) (any, error) {
memories, err := a.svc.Search(ctx, SearchQuery{UserID: userID, Query: query, Limit: limit})
if err != nil {
return nil, err
}
result := make([]APIEntry, len(memories))
for i, m := range memories {
result[i] = toEntry(m)
}
return result, nil
}
func (a *APIAdapter) List(ctx context.Context, userID string, limit int) (any, error) {
memories, err := a.svc.List(ctx, userID, limit)
if err != nil {
return nil, err
}
result := make([]APIEntry, len(memories))
for i, m := range memories {
result[i] = toEntry(m)
}
return result, nil
}
func (a *APIAdapter) Add(ctx context.Context, userID, content, category string, priority float64) (any, error) {
m, err := a.svc.Add(ctx, userID, "", content, category, priority)
if err != nil {
return nil, err
}
e := toEntry(*m)
return &e, nil
}
func (a *APIAdapter) Get(ctx context.Context, id string) (any, error) {
m, err := a.svc.GetByID(ctx, id)
if err != nil {
return nil, err
}
e := toEntry(*m)
return &e, nil
}
func (a *APIAdapter) Delete(ctx context.Context, id string) error {
return a.svc.Delete(ctx, id)
}
func toEntry(m Memory) APIEntry {
return APIEntry{
ID: m.ID,
UserID: m.UserID,
Content: m.Content,
Summary: m.Summary,
Keywords: m.Keywords,
Category: m.Category,
Priority: m.Priority,
}
}
// Ensure we use context
var _ = context.Background