mqttstats.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package tools
  2. // MQTT 报文统计(FWH iotcore-stat Elasticsearch,与 ToolBox“MQTT 报文管理 / MQTT 数据看板”同源)。
  3. // ES 按 UTC 日建索引:日期、时间参数一律按 UTC 解释。
  4. import (
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/mark3labs/mcp-go/mcp"
  13. "fwh-toolbox-server/agentmcp/kit"
  14. "fwh-toolbox-server/repositories"
  15. "fwh-toolbox-server/services"
  16. )
  17. const mqttEsSource = "FWH MQTT 报文统计(ES)"
  18. // esDate 把 yyyy-MM-dd 转为 ES 索引日期 yyyy.MM.dd
  19. func esDate(name, v string) (string, error) {
  20. t, err := kit.ParseDate(name, v, false)
  21. if err != nil {
  22. return "", err
  23. }
  24. return t.Format("2006.01.02"), nil
  25. }
  26. // ---- mqtt_traffic_trend ----
  27. type mqttTrendArgs struct {
  28. Month string `json:"month" jsonschema:"月份,yyyy-MM(UTC)"`
  29. }
  30. var MqttTrafficTrend = kit.Def[mqttTrendArgs]{
  31. Name: "mqtt_traffic_trend",
  32. Title: "查询 MQTT 报文月度趋势",
  33. Description: "查询全部站点某月的 MQTT 报文总量与每日总量(UTC 日,整月补齐,缺失的天为 0)。" +
  34. "用于观察报文量的整体趋势、定位异常的日期;某天哪些站点报文最多用 mqtt_traffic_sites。返回 month、total、days(date、total)。",
  35. ReadOnly: true,
  36. Handler: func(ctx context.Context, a mqttTrendArgs) (*mcp.CallToolResult, error) {
  37. t, err := time.Parse("2006-01", strings.TrimSpace(a.Month))
  38. if err != nil {
  39. return kit.Failf("参数 month 格式错误,需要 yyyy-MM:%s", a.Month), nil
  40. }
  41. total, err := services.GetMqttMonthlyTotal(t.Year(), int(t.Month()))
  42. if err != nil {
  43. return kit.Upstream(mqttEsSource, err), nil
  44. }
  45. days, err := services.GetMqttMonthlyDailyTrend(t.Year(), int(t.Month()))
  46. if err != nil {
  47. return kit.Upstream(mqttEsSource, err), nil
  48. }
  49. return kit.Object(map[string]any{"month": t.Format("2006-01"), "total": total.Total, "days": days}), nil
  50. },
  51. }
  52. // ---- mqtt_traffic_sites ----
  53. type mqttSitesArgs struct {
  54. Date string `json:"date" jsonschema:"日期,yyyy-MM-dd(UTC)"`
  55. Page int `json:"page,omitempty" jsonschema:"页码,默认 1"`
  56. Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"`
  57. }
  58. var MqttTrafficSites = kit.Def[mqttSitesArgs]{
  59. Name: "mqtt_traffic_sites",
  60. Title: "查询站点 MQTT 报文量排行",
  61. Description: "查询某一天(UTC)各站点的 MQTT 报文量,按报文量从多到少分页返回。用于找出报文量异常偏高的站点。" +
  62. "返回 siteCount(当天有报文的站点数)与 items(sn、total)。某站点各命令类型的分布用 mqtt_traffic_site_cmd。",
  63. ReadOnly: true,
  64. Handler: func(ctx context.Context, a mqttSitesArgs) (*mcp.CallToolResult, error) {
  65. date, err := esDate("date", a.Date)
  66. if err != nil {
  67. return kit.Fail(err), nil
  68. }
  69. page, size := kit.Page(a.Page), kit.Limit(a.Size, 20, 100)
  70. res, err := services.GetMqttSiteDailyTotals(date, page, size)
  71. if err != nil {
  72. return kit.Upstream(mqttEsSource, err), nil
  73. }
  74. items := make([]map[string]any, 0, len(res.Items))
  75. for _, it := range res.Items {
  76. items = append(items, map[string]any{"sn": it.EquipNo, "total": it.Total})
  77. }
  78. return kit.List(items, int(res.SiteCount), size, fmt.Sprintf("第 %d 页,按报文量降序,可用 page 翻页", page)), nil
  79. },
  80. }
  81. // ---- mqtt_traffic_site_cmd ----
  82. type mqttSiteCmdArgs struct {
  83. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  84. Date string `json:"date" jsonschema:"日期,yyyy-MM-dd(UTC)"`
  85. }
  86. var MqttTrafficSiteCmd = kit.Def[mqttSiteCmdArgs]{
  87. Name: "mqtt_traffic_site_cmd",
  88. Title: "查询站点 MQTT 命令类型分布",
  89. Description: "查询单个站点某一天(UTC)各 MQTT 命令类型(cmdType)的报文量,按报文量降序。用于分析某站点报文量异常的来源。" +
  90. "返回 cmdTypeCount 与 items(cmdType、total)。具体报文用 mqtt_message_search 或 mqtt_log_search。",
  91. ReadOnly: true,
  92. Handler: func(ctx context.Context, a mqttSiteCmdArgs) (*mcp.CallToolResult, error) {
  93. sn, err := kit.Required("sn", a.SN)
  94. if err != nil {
  95. return kit.Fail(err), nil
  96. }
  97. date, err := esDate("date", a.Date)
  98. if err != nil {
  99. return kit.Fail(err), nil
  100. }
  101. res, err := services.GetMqttSiteCmdTypeTotals(sn, date, 1, 200)
  102. if err != nil {
  103. return kit.Upstream(mqttEsSource, err), nil
  104. }
  105. return kit.List(res.Items, int(res.CmdTypeCount), 200, ""), nil
  106. },
  107. }
  108. // ---- mqtt_message_search ----
  109. type mqttMessageSearchArgs struct {
  110. SN string `json:"sn,omitempty" jsonschema:"站点 SN;不填查全部站点(此时建议指定 cmdType 并缩短时间范围)"`
  111. CmdType *int `json:"cmdType,omitempty" jsonschema:"MQTT 命令类型,如 0、101、105、107、201、211"`
  112. From string `json:"from" jsonschema:"开始时间,yyyy-MM-dd HH:mm:ss(UTC)"`
  113. To string `json:"to" jsonschema:"结束时间,同上;与开始时间相差不超过 7 天"`
  114. Limit int `json:"limit,omitempty" jsonschema:"最多返回条数,1~200,默认 50"`
  115. }
  116. var MqttMessageSearch = kit.Def[mqttMessageSearchArgs]{
  117. Name: "mqtt_message_search",
  118. Title: "检索 MQTT 报文(ES)",
  119. Description: "在 MQTT 报文统计库(ES)中按站点、命令类型、时间(UTC)检索逐条报文,按时间倒序。可以不指定站点,跨站点查找某类报文。" +
  120. "返回 total(命中数,可能为下界)与 items:time(UTC)、sn、cmdType,有内容时附 event(超过 1500 字截断)。" +
  121. "统计库主要保存报文的时间、站点与命令类型,多数报文没有 event 内容,适合统计与定位时间点;需要报文内容时用 mqtt_log_search(FWH 平台日志,时间为站点当地时间)。",
  122. ReadOnly: true,
  123. Handler: func(ctx context.Context, a mqttMessageSearchArgs) (*mcp.CallToolResult, error) {
  124. from, to, err := kit.ParseRange(a.From, a.To, 7*24*time.Hour)
  125. if err != nil {
  126. return kit.Fail(err), nil
  127. }
  128. // ParseRange 按本地时区解析;ES 的时间是 UTC,按字面值换为 UTC
  129. from = time.Date(from.Year(), from.Month(), from.Day(), from.Hour(), from.Minute(), from.Second(), 0, time.UTC)
  130. to = time.Date(to.Year(), to.Month(), to.Day(), to.Hour(), to.Minute(), to.Second(), 0, time.UTC)
  131. limit := kit.Limit(a.Limit, 50, 200)
  132. var sn *string
  133. if v := strings.TrimSpace(a.SN); v != "" {
  134. sn = &v
  135. }
  136. var cmd *int64
  137. if a.CmdType != nil {
  138. v := int64(*a.CmdType)
  139. cmd = &v
  140. }
  141. res, err := services.QueryMqttMessages(sn, cmd, &from, &to, 1, limit)
  142. if errors.Is(err, repositories.ErrEsDeepPaging) {
  143. return kit.Failf("命中的报文太多,请缩短时间范围或指定站点、命令类型。"), nil
  144. }
  145. if err != nil {
  146. return kit.Upstream(mqttEsSource, err), nil
  147. }
  148. items := make([]map[string]any, 0, len(res.Items))
  149. for _, m := range res.Items {
  150. item := map[string]any{"time": m.Timestamp, "sn": m.EquipNo, "cmdType": strconv.FormatInt(m.CmdType, 10)}
  151. if len(m.Event) > 0 {
  152. if b, err := json.Marshal(m.Event); err == nil {
  153. item["event"] = truncate(string(b), 1500)
  154. }
  155. }
  156. items = append(items, item)
  157. }
  158. return kit.List(items, int(res.Total), limit, "超过 limit 时请缩短时间范围或增加条件"), nil
  159. },
  160. }
  161. func truncate(s string, n int) string {
  162. r := []rune(s)
  163. if len(r) <= n {
  164. return s
  165. }
  166. return string(r[:n]) + "…(已截断)"
  167. }