This commit is contained in:
2026-05-15 22:03:46 +08:00
parent e6a7565745
commit e047eacfdc
6 changed files with 1239 additions and 272 deletions
+116 -17
View File
@@ -36,9 +36,19 @@ func New(cfg *config.AdminConfig) *Server {
{
admin.GET("/api/stats", handleStats)
admin.GET("/api/clients", handleClients)
admin.GET("/api/client/:id", handleClientDetail)
admin.GET("/api/client/:id/subs", handleClientSubs)
}
r.GET("/admin/mqtt", serveAdmin)
r.GET("/admin/mqtt/client/:id", serveClientDetail)
r.GET("/", serveIndex)
// Meshtastic 消息查看
meshtastic := r.Group("/admin/meshtastic")
{
meshtastic.GET("/", serveMeshtastic)
meshtastic.GET("/api/messages", handleMessages)
}
r.GET("/admin/mqtt", serveIndex)
r.GET("/", serveHome)
addr := cfg.Port
if addr == "" {
@@ -90,23 +100,76 @@ func (s *Server) Enabled() bool { return s.enabled }
// serveIndex 返回管理页面
func serveIndex(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, indexHTML)
c.String(http.StatusOK, homeHTML)
}
// serveHome 主页(meshmap 占位)
func serveHome(c *gin.Context) {
// serveAdmin MQTT 管理页面
func serveAdmin(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, `<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>meshgo</title></head>
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f0f2f5">
<div style="text-align:center">
<h1 style="font-size:48px;color:#1677ff;margin-bottom:8px">meshmap</h1>
<p style="color:#888;margin-bottom:24px">Mesh Network Map · 网格拓扑可视化</p>
<a href="/admin/mqtt" style="display:inline-block;padding:10px 24px;background:#1677ff;color:#fff;text-decoration:none;border-radius:6px;font-size:14px">MQTT 管理面板</a>
</div>
</body>
</html>`)
c.String(http.StatusOK, adminHTML)
}
// serveClientDetail 客户端详情页面
func serveClientDetail(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, clientDetailHTML)
}
// serveMeshtastic Meshtastic 消息查看页面
func serveMeshtastic(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, meshtasticHTML)
}
// handleMessages 返回 msh 消息列表(JSON
func handleMessages(c *gin.Context) {
messages := stats.GetMessages()
pskIndex := stats.GetDefaultPSKIndex()
for i := range messages {
if isJsonTopic(messages[i].Topic) {
continue
}
dec, err := stats.TryDecryptMessage(messages[i].Payload, pskIndex)
if err != nil {
// 打印详细错误
env, parseErr := stats.ParseServiceEnvelopeDebug(messages[i].Payload)
if parseErr != nil {
log.Printf("[decrypt] FAIL psk=%d topic=%s parse_err=%v decrypt_err=%v payload_len=%d",
pskIndex, messages[i].Topic, parseErr, err, len(messages[i].Payload))
} else {
packetId := uint64(0)
from := uint32(0)
variant := 0
encryptedLen := 0
if env.Packet != nil {
packetId = env.Packet.Id
from = env.Packet.From
variant = env.Packet.WhichPayloadVariant
encryptedLen = len(env.Packet.Encrypted)
}
log.Printf("[decrypt] FAIL psk=%d topic=%s channel=%s packetId=%d from=0x%x variant=%d encrypted_len=%d err=%v",
pskIndex, messages[i].Topic, env.ChannelId, packetId, from, variant, encryptedLen, err)
}
messages[i].Decrypted = &stats.DecryptedMessage{
GatewayId: err.Error(),
}
} else if dec != nil {
messages[i].Decrypted = dec
log.Printf("[decrypt] OK topic=%s channel=%s packetId=%d from=0x%x port=%d payload_len=%d",
messages[i].Topic, dec.ChannelId, dec.PacketId, dec.From, dec.PortNum, len(dec.Payload))
}
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": messages,
})
}
func isJsonTopic(topic string) bool {
return len(topic) > 0 && (topic[len(topic)-1] == 'n' || topic[len(topic)-5:len(topic)] == "/json")
}
// handleStats 返回实时统计(JSON
@@ -126,6 +189,33 @@ func handleClients(c *gin.Context) {
})
}
// handleClientDetail 返回指定客户端详情(JSON)
func handleClientDetail(c *gin.Context) {
id := c.Param("id")
info := stats.GetClient(id)
if info == nil {
c.JSON(http.StatusOK, gin.H{
"code": 404,
"msg": "客户端不存在或已离线",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": info,
})
}
// handleClientSubs 返回指定客户端的订阅主题列表(JSON)
func handleClientSubs(c *gin.Context) {
id := c.Param("id")
subs := stats.GetClientSubs(id)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": subs,
})
}
// ---------------------------------------------------------------------------
// 认证中间件(预留扩展点)
// AuthMiddleware 根据 cfg.AuthType 选择对应认证策略
@@ -168,4 +258,13 @@ func basicAuth(user, pass string) gin.HandlerFunc {
// ---------------------------------------------------------------------------
//go:embed index.html
var indexHTML string
var homeHTML string
//go:embed admin_mqtt.html
var adminHTML string
//go:embed client_detail.html
var clientDetailHTML string
//go:embed meshtastic.html
var meshtasticHTML string