feat(push-gateway): config 扩展 + kafka consumer + ws handler + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 16:03:17 +08:00
parent 5a88c8b45d
commit 9fd7c018c2
8 changed files with 630 additions and 36 deletions

View File

@@ -6,8 +6,12 @@
// - WebSocket /ws: JWT RS256 validated via shared-go/jwks (caching JWKS from
// iam /.well-known/jwks.json, refreshed every 5 minutes). DevMode accepts
// the literal "dev-token" returning a synthetic dev-user subject.
// - /internal/*: X-Internal-Token header matched against INTERNAL_API_TOKEN
// (ARB-015 §17.3 / president §7.2). DevMode skips the check.
// - /internal/*: X-Internal-Key header matched against PUSH_INTERNAL_TOKEN
// (ARB-013 alignment with msg). X-Internal-Token is accepted as a
// backward-compat alias. DevMode skips the check.
//
// Request body field naming: msg sends userId (camelCase); legacy callers may
// still send user_id (snake_case). Both are accepted by /internal/push.
//
// Heartbeat (RFC 6455 control frames, not text messages):
// - Client sends Ping every 30s; gorilla/websocket auto-replies with Pong.
@@ -40,8 +44,12 @@ import (
)
// internalTokenHeader is the canonical header for /internal/* authentication
// (ARB-015 §17.3 / president §7.2). Was X-Internal-Key in pre-P5 builds.
const internalTokenHeader = "X-Internal-Token"
// (ARB-013 alignment with msg). legacyInternalTokenHeader is accepted as a
// backward-compat alias for existing callers.
const (
internalTokenHeader = "X-Internal-Key"
legacyInternalTokenHeader = "X-Internal-Token"
)
// readBufferSize / writeBufferSize tune gorilla/websocket's internal buffers.
const (
@@ -244,16 +252,20 @@ func (h *Handler) readerLoop(c *hub.Connection) {
// PushHandler implements POST /internal/push: directed push to a single user.
// Response carries delivered + online so msg (ai10) can decide offline fallback.
//
// Request body accepts both camelCase (userId, msg's preferred format) and
// snake_case (user_id, legacy format) field names for the user identifier.
func (h *Handler) PushHandler(c *gin.Context) {
if !h.checkInternalToken(c) {
return
}
var req struct {
UserID string `json:"user_id" binding:"required"`
Event string `json:"event" binding:"required"`
Data map[string]any `json:"data"`
TTL *int `json:"ttl,omitempty"`
UserID string `json:"userId"` // camelCase (msg canonical, ARB-013)
UserIDLeg string `json:"user_id"` // snake_case (legacy)
Event string `json:"event" binding:"required"`
Data map[string]any `json:"data"`
TTL *int `json:"ttl,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, pushResponse{
@@ -262,6 +274,17 @@ func (h *Handler) PushHandler(c *gin.Context) {
})
return
}
// Prefer userId (camelCase); fall back to user_id (snake_case, legacy).
if req.UserID == "" {
req.UserID = req.UserIDLeg
}
if req.UserID == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, pushResponse{
Success: false,
Error: &errBody{Code: "PUSH_INVALID_REQUEST", Message: "userId (or user_id) is required"},
})
return
}
message, err := buildMessage(req.Event, req.Data)
if err != nil {
@@ -399,8 +422,10 @@ func (h *Handler) OnlineHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"online": false, "instances": []string{}})
}
// checkInternalToken validates the X-Internal-Token header. Returns false when
// the request has been aborted (caller should return immediately).
// checkInternalToken validates the internal token header. The canonical header
// is X-Internal-Key (ARB-013 alignment with msg); X-Internal-Token is accepted
// as a backward-compat alias. Returns false when the request has been aborted
// (caller should return immediately).
func (h *Handler) checkInternalToken(c *gin.Context) bool {
if h.cfg.DevMode {
return true
@@ -412,7 +437,11 @@ func (h *Handler) checkInternalToken(c *gin.Context) bool {
})
return false
}
if c.GetHeader(internalTokenHeader) != h.cfg.InternalAPIToken {
token := c.GetHeader(internalTokenHeader)
if token == "" {
token = c.GetHeader(legacyInternalTokenHeader)
}
if token != h.cfg.InternalAPIToken {
c.AbortWithStatusJSON(http.StatusUnauthorized, pushResponse{
Success: false,
Error: &errBody{Code: "PUSH_UNAUTHORIZED", Message: "invalid or missing internal token"},