first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
package server
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
"sync"
"time"
"agent/internal/agent"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 4096,
// 本地单用户 Web,允许任意 Origin
CheckOrigin: func(r *http.Request) bool { return true },
}
// wsInbound 客户端 → 服务端消息。
// chat:在绑定会话上运行 Agent;switch:切换绑定会话;stop:停止当前生成;ping:保活。
type wsInbound struct {
Type string `json:"type"`
Content string `json:"content"`
SessionID string `json:"session_id"`
}
// wsConn 封装一个 WebSocket 连接:绑定一个会话、串行处理消息。
type wsConn struct {
server *Server
conn *websocket.Conn
sendMu sync.Mutex // 串行化 WriteJSON
sessionID string
unreg func() // 当前会话的 live 注销函数
runMu sync.Mutex
runCancel context.CancelFunc
}
func (s *Server) ws(c *gin.Context) {
sessionID := strings.TrimSpace(c.Query("session"))
if sessionID == "" || !sessionIDPattern.MatchString(sessionID) {
log.Printf("ws: 连接被拒(session 参数无效)session=%q", sessionID)
fail(c, http.StatusBadRequest, "缺少有效的 session 参数")
return
}
if _, ok := s.store.Get(sessionID); !ok {
log.Printf("ws: 连接被拒(会话不存在)session=%q", sessionID)
fail(c, http.StatusNotFound, "会话不存在")
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
client := &wsConn{server: s, conn: conn}
client.bind(sessionID)
client.readPump()
}
// sendJSON 安全地推送一个事件帧 {type, data}。
func (cl *wsConn) sendJSON(eventType string, data any) {
cl.sendMu.Lock()
defer cl.sendMu.Unlock()
_ = cl.conn.WriteJSON(map[string]any{"type": eventType, "data": data})
}
// bind 切换绑定会话:注销旧会话的 live 注册,注册新会话。
// 传入空串表示仅注销。
func (cl *wsConn) bind(sessionID string) {
if cl.unreg != nil {
cl.unreg()
cl.unreg = nil
}
cl.sessionID = sessionID
if sessionID == "" {
return
}
cl.unreg = cl.server.agent.RegisterLive(sessionID, func(event agent.Event) {
cl.sendJSON(event.Type, event.Data)
})
}
func (cl *wsConn) handleChat(content string) {
content = strings.TrimSpace(content)
if content == "" {
cl.sendJSON("error", map[string]any{"message": "消息内容不能为空"})
return
}
// 未配置 LLM API Key 时直接提示去配置,避免进入无效的模型调用。
// 环境变量 LLM_API_KEY(托管模式)与设置页 api-config.json 都计入已配置。
if !cl.server.apiCfg.IsAPIKeyConfigured() {
cl.sendJSON("error", map[string]any{"message": "尚未配置 LLM API,请先点击左下角设置按钮配置 API Key 与模型"})
return
}
cl.runMu.Lock()
if cl.runCancel != nil {
cl.runMu.Unlock()
cl.sendJSON("error", map[string]any{"message": "Agent 正在执行,请等待完成或先停止"})
return
}
ctx, cancel := context.WithCancel(context.Background())
cl.runCancel = cancel
cl.runMu.Unlock()
done := make(chan struct{})
go func() {
defer close(done)
_ = cl.server.agent.Run(ctx, cl.sessionID, content, func(event agent.Event) {
cl.sendJSON(event.Type, event.Data)
})
}()
go func() {
<-done
cl.runMu.Lock()
cl.runCancel = nil
cl.runMu.Unlock()
}()
}
func (cl *wsConn) handleStop() {
cl.runMu.Lock()
if cl.runCancel != nil {
cl.runCancel()
}
cl.runMu.Unlock()
}
func (cl *wsConn) readPump() {
defer func() {
cl.bind("") // 注销 live
_ = cl.conn.Close()
}()
cl.conn.SetReadLimit(1 << 20) // 1MB
_ = cl.conn.SetReadDeadline(time.Now().Add(90 * time.Second))
cl.conn.SetPongHandler(func(string) error {
_ = cl.conn.SetReadDeadline(time.Now().Add(90 * time.Second))
return nil
})
for {
// 任何收到消息(含客户端心跳 {type:"ping"})都刷新 90s 读超时,
// 否则浏览器只能发协议层 Pong,应用层 ping 不会触发 SetPongHandler,连接必死在第 90 秒
_ = cl.conn.SetReadDeadline(time.Now().Add(90 * time.Second))
_, raw, err := cl.conn.ReadMessage()
if err != nil {
return
}
var msg wsInbound
if err := json.Unmarshal(raw, &msg); err != nil {
continue
}
switch msg.Type {
case "chat":
cl.handleChat(msg.Content)
case "stop":
cl.handleStop()
case "switch":
if msg.SessionID != "" && sessionIDPattern.MatchString(msg.SessionID) {
if _, ok := cl.server.store.Get(msg.SessionID); ok {
cl.bind(msg.SessionID)
}
}
case "ping":
cl.sendJSON("pong", map[string]any{"time": time.Now().UnixMilli()})
default:
log.Printf("ws: unknown message type %q", msg.Type)
}
}
}