Initialize LineUp app server
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AgentModule is the app-server-facing control plane for external Agent
|
||||
// adapters. Message delivery remains in MessageModule; this module makes the
|
||||
// adapter's liveness and delivery cursor observable without exposing Hermes.
|
||||
type AgentModule struct {
|
||||
vp *viper.Viper
|
||||
mu sync.RWMutex
|
||||
entries map[string]agentHeartbeat
|
||||
}
|
||||
|
||||
type agentHeartbeat struct {
|
||||
Status string `json:"status"`
|
||||
LastMessageSeq uint64 `json:"last_message_seq"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type agentHeartbeatRequest struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
LastMessageSeq uint64 `json:"last_message_seq"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
func NewAgentModule(vp *viper.Viper) *AgentModule {
|
||||
return &AgentModule{vp: vp, entries: make(map[string]agentHeartbeat)}
|
||||
}
|
||||
|
||||
func (m *AgentModule) RegisterRoutes(r *gin.Engine) {
|
||||
r.GET("/agents/:uid/health", m.handleHealth)
|
||||
r.POST("/agents/:uid/heartbeat", m.handleHeartbeat)
|
||||
}
|
||||
|
||||
func (m *AgentModule) configuredSecret() string {
|
||||
return m.vp.GetString("agent.shared_secret")
|
||||
}
|
||||
|
||||
func (m *AgentModule) authorize(c *gin.Context) bool {
|
||||
secret := m.configuredSecret()
|
||||
if secret == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Agent heartbeat 未配置 shared_secret"})
|
||||
return false
|
||||
}
|
||||
provided := c.GetHeader("X-LineUp-Agent-Secret")
|
||||
if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Agent heartbeat 认证失败"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *AgentModule) handleHeartbeat(c *gin.Context) {
|
||||
if !m.authorize(c) {
|
||||
return
|
||||
}
|
||||
var req agentHeartbeatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: status必填"})
|
||||
return
|
||||
}
|
||||
entry := agentHeartbeat{
|
||||
Status: req.Status, LastMessageSeq: req.LastMessageSeq, Detail: req.Detail, UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.entries[c.Param("uid")] = entry
|
||||
m.mu.Unlock()
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "updated_at": entry.UpdatedAt})
|
||||
}
|
||||
|
||||
func (m *AgentModule) handleHealth(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
m.mu.RLock()
|
||||
entry, found := m.entries[uid]
|
||||
m.mu.RUnlock()
|
||||
if !found {
|
||||
c.JSON(http.StatusOK, gin.H{"uid": uid, "status": "offline", "known": false})
|
||||
return
|
||||
}
|
||||
// A worker normally reports once per poll. Keep a generous threshold so a
|
||||
// slow Hermes turn does not look dead while it is still being supervised.
|
||||
online := time.Since(entry.UpdatedAt) <= 45*time.Second
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"uid": uid, "status": entry.Status, "known": true, "online": online,
|
||||
"last_message_seq": entry.LastMessageSeq, "detail": entry.Detail, "updated_at": entry.UpdatedAt,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user