alarm.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package tools
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "time"
  8. "unicode/utf8"
  9. "github.com/mark3labs/mcp-go/mcp"
  10. "fwh-toolbox-server/agentmcp/kit"
  11. "fwh-toolbox-server/domain"
  12. "fwh-toolbox-server/repositories"
  13. "fwh-toolbox-server/services"
  14. )
  15. func parseCountry(v string) (domain.CountryType, error) {
  16. c, err := kit.Enum("country", v, "all", "all", "us", "au")
  17. if err != nil {
  18. return 0, err
  19. }
  20. switch c {
  21. case "us":
  22. return domain.CTRY_US, nil
  23. case "au":
  24. return domain.CTRY_AU, nil
  25. }
  26. return domain.CTRY_ALL, nil
  27. }
  28. // ---- alarm_current ----
  29. type snArgs struct {
  30. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  31. }
  32. var AlarmCurrent = kit.Def[snArgs]{
  33. Name: "alarm_current",
  34. Title: "查询站点当前告警",
  35. Description: "查询单个站点当前未结束的告警列表(FWH 平台实时告警)。" +
  36. "返回字段:alarmEqSn(告警设备 SN)、alarmCode / commonAlarmCode(告警码)、level(等级)、time(发生时间)、logName / logNameEn(告警名称)。",
  37. ReadOnly: true,
  38. Handler: func(ctx context.Context, a snArgs) (*mcp.CallToolResult, error) {
  39. sn, err := kit.Required("sn", a.SN)
  40. if err != nil {
  41. return kit.Fail(err), nil
  42. }
  43. res, err := repositories.QuerySiteAlarmList(sn)
  44. if err != nil {
  45. return kit.Upstream("FWH 平台实时告警接口", err), nil
  46. }
  47. if res == nil || res.Result == nil || len(*res.Result) == 0 {
  48. return kit.Empty("该站点当前没有未结束的告警。"), nil
  49. }
  50. return kit.List(*res.Result, -1, 200, ""), nil
  51. },
  52. }
  53. // ---- alarm_events ----
  54. type alarmEventsArgs struct {
  55. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  56. From string `json:"from" jsonschema:"开始时间,yyyy-MM-dd HH:mm:ss,站点当地时间"`
  57. To string `json:"to" jsonschema:"结束时间,同上;与开始时间相差不超过 31 天"`
  58. Kind string `json:"kind,omitempty" jsonschema:"alarm(只看告警)/ event(只看事件)/ all(默认)"`
  59. Limit int `json:"limit,omitempty" jsonschema:"最多返回条数,1~500,默认 100"`
  60. }
  61. // EventRow alarm_events 的精简行
  62. type EventRow struct {
  63. Kind string `json:"kind"` // 告警 / 事件
  64. LogTime string `json:"logTime"` // 设备时间
  65. StartTime string `json:"startTime,omitempty"`
  66. EndTime string `json:"endTime,omitempty"`
  67. Code string `json:"code,omitempty"`
  68. Name string `json:"name,omitempty"`
  69. Cmd string `json:"cmd,omitempty"`
  70. Data string `json:"data,omitempty"` // 原始 logData,超过 500 字截断
  71. }
  72. var AlarmEvents = kit.Def[alarmEventsArgs]{
  73. Name: "alarm_events",
  74. Title: "查询站点告警/事件记录",
  75. Description: "查询单个站点在一段时间内的告警和事件记录(FWH 平台日志),按时间倒序。用于排查某次故障前后发生了什么。" +
  76. "返回字段:kind(告警/事件)、logTime(设备当地时间)、startTime、endTime(告警结束时间,未结束为空)、code(告警码,事件为 0)、name、cmd、data(原始日志数据,超过 500 字截断)。" +
  77. "超过 limit 时 truncated=true,请缩短时间范围。时间范围较大时查询较慢。",
  78. ReadOnly: true,
  79. Handler: func(ctx context.Context, a alarmEventsArgs) (*mcp.CallToolResult, error) {
  80. sn, err := kit.Required("sn", a.SN)
  81. if err != nil {
  82. return kit.Fail(err), nil
  83. }
  84. from, to, err := kit.ParseRange(a.From, a.To, 31*24*time.Hour)
  85. if err != nil {
  86. return kit.Fail(err), nil
  87. }
  88. kind, err := kit.Enum("kind", a.Kind, "all", "all", "alarm", "event")
  89. if err != nil {
  90. return kit.Fail(err), nil
  91. }
  92. t := repositories.EventAndAlarm
  93. switch kind {
  94. case "alarm":
  95. t = repositories.Alarm
  96. case "event":
  97. t = repositories.Event
  98. }
  99. items, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, from, to, t)
  100. if err != nil {
  101. return kit.Upstream("FWH 平台事件告警日志接口", err), nil
  102. }
  103. rows := MapEventRows(items)
  104. return kit.List(rows, -1, kit.Limit(a.Limit, 100, 500), "结果已截断,请缩短时间范围或改用 kind 过滤"), nil
  105. },
  106. }
  107. // MapEventRows 转为精简行并按设备时间倒序。
  108. func MapEventRows(items []domain.EventAlarmItem) []EventRow {
  109. rows := make([]EventRow, 0, len(items))
  110. for _, it := range items {
  111. r := EventRow{
  112. Kind: "事件",
  113. LogTime: it.LogTime,
  114. StartTime: it.StartTime,
  115. Code: it.AlarmCode,
  116. Name: it.LogName,
  117. Cmd: strings.TrimSpace(it.CmdName + " " + it.CmdType),
  118. Data: clip(it.LogData, 500),
  119. }
  120. if it.LogType == "1" {
  121. r.Kind = "告警"
  122. }
  123. if it.EndTime != nil {
  124. r.EndTime = *it.EndTime
  125. }
  126. if r.Code == "0" {
  127. r.Code = ""
  128. }
  129. rows = append(rows, r)
  130. }
  131. sort.SliceStable(rows, func(i, j int) bool { return rows[i].LogTime > rows[j].LogTime })
  132. return rows
  133. }
  134. func clip(s string, n int) string {
  135. if utf8.RuneCountInString(s) <= n {
  136. return s
  137. }
  138. return string([]rune(s)[:n]) + "…"
  139. }
  140. // ---- alarm_snapshot ----
  141. type alarmSnapshotArgs struct {
  142. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  143. Date string `json:"date" jsonschema:"日期,yyyy-MM-dd"`
  144. }
  145. var AlarmSnapshot = kit.Def[alarmSnapshotArgs]{
  146. Name: "alarm_snapshot",
  147. Title: "查询站点告警快照",
  148. Description: "查询单个站点某一天的告警快照记录(告警发生时设备上传的快照文件信息)。" +
  149. "返回字段:id、fileName、fileUrl、时间等(以实际返回为准)。",
  150. ReadOnly: true,
  151. Handler: func(ctx context.Context, a alarmSnapshotArgs) (*mcp.CallToolResult, error) {
  152. sn, err := kit.Required("sn", a.SN)
  153. if err != nil {
  154. return kit.Fail(err), nil
  155. }
  156. day, err := kit.ParseDate("date", a.Date, false)
  157. if err != nil {
  158. return kit.Fail(err), nil
  159. }
  160. items, err := repositories.QueryAlarmSnapshotInfo(sn, day.Format(kit.DateLayout))
  161. if err != nil {
  162. return kit.Upstream("FWH 平台告警快照接口", err), nil
  163. }
  164. return kit.List(items, -1, 100, ""), nil
  165. },
  166. }
  167. // ---- alarm_stats ----
  168. type alarmStatsArgs struct {
  169. Source string `json:"source,omitempty" jsonschema:"daily(默认):ToolBox 每日告警统计,按告警码汇总站点数;realtime:FWH 平台当前告警统计"`
  170. Date string `json:"date,omitempty" jsonschema:"daily 时的统计日期 yyyy-MM-dd(北京时间),不填取最新"`
  171. Country string `json:"country,omitempty" jsonschema:"realtime 时的国家:all(默认)、us(美国)、au(澳大利亚)"`
  172. Page int `json:"page,omitempty" jsonschema:"realtime 时的页码,默认 1"`
  173. Top int `json:"top,omitempty" jsonschema:"daily 时返回告警码数量最多的前 N 个,1~200,默认 50"`
  174. }
  175. // CodeCount 告警码计数
  176. type CodeCount struct {
  177. Code string `json:"code"`
  178. Count uint `json:"count"`
  179. }
  180. var AlarmStats = kit.Def[alarmStatsArgs]{
  181. Name: "alarm_stats",
  182. Title: "查询全站告警统计",
  183. Description: "查询全量站点的告警统计。source=daily(默认):ToolBox 每日统计,返回 date、siteStatistics(有告警的站点数)、codes(按触发站点数倒序的告警码列表 code/count);" +
  184. "source=realtime:FWH 平台当前告警统计(按国家、分页),返回 total 与 records。" +
  185. "查看某个告警码涉及哪些站点用 alarm_sites_by_code。",
  186. ReadOnly: true,
  187. Handler: func(ctx context.Context, a alarmStatsArgs) (*mcp.CallToolResult, error) {
  188. src, err := kit.Enum("source", a.Source, "daily", "daily", "realtime")
  189. if err != nil {
  190. return kit.Fail(err), nil
  191. }
  192. if src == "realtime" {
  193. country, err := parseCountry(a.Country)
  194. if err != nil {
  195. return kit.Fail(err), nil
  196. }
  197. res, err := repositories.QueryAlarmStatistics(country, kit.Page(a.Page))
  198. if err != nil {
  199. return kit.Upstream("FWH 平台告警统计接口", err), nil
  200. }
  201. if res == nil || res.Data == nil {
  202. return kit.Empty(""), nil
  203. }
  204. return kit.List(res.Data.Records, res.Data.Total, 200, fmt.Sprintf("共 %d 页,可用 page 翻页", res.Data.PageTotal)), nil
  205. }
  206. day, err := kit.ParseDate("date", a.Date, true)
  207. if err != nil {
  208. return kit.Fail(err), nil
  209. }
  210. var datePtr *string
  211. if day != nil {
  212. s := day.Format(kit.DateLayout)
  213. datePtr = &s
  214. }
  215. dash, err := services.GetAlarmStatistics(datePtr)
  216. if err != nil {
  217. return kit.Upstream("ToolBox 告警统计", err), nil
  218. }
  219. if dash == nil {
  220. return kit.Empty("没有该日期的告警统计。"), nil
  221. }
  222. codes := SortCodeCounts(dash.CodeStatistics)
  223. top := kit.Limit(a.Top, 50, 200)
  224. total := len(codes)
  225. if len(codes) > top {
  226. codes = codes[:top]
  227. }
  228. return kit.Object(map[string]any{
  229. "date": dash.Date,
  230. "siteStatistics": dash.SiteStatistics,
  231. "codeTotal": total,
  232. "codes": codes,
  233. }), nil
  234. },
  235. }
  236. // SortCodeCounts 按计数倒序、告警码升序排列。
  237. func SortCodeCounts(m map[string]uint) []CodeCount {
  238. out := make([]CodeCount, 0, len(m))
  239. for k, v := range m {
  240. out = append(out, CodeCount{Code: k, Count: v})
  241. }
  242. sort.Slice(out, func(i, j int) bool {
  243. if out[i].Count != out[j].Count {
  244. return out[i].Count > out[j].Count
  245. }
  246. return out[i].Code < out[j].Code
  247. })
  248. return out
  249. }
  250. // ---- alarm_sites_by_code ----
  251. type alarmSitesByCodeArgs struct {
  252. Code string `json:"code" jsonschema:"告警码"`
  253. Country string `json:"country,omitempty" jsonschema:"国家:all(默认)、us(美国)、au(澳大利亚)"`
  254. Page int `json:"page,omitempty" jsonschema:"页码,默认 1"`
  255. }
  256. var AlarmSitesByCode = kit.Def[alarmSitesByCodeArgs]{
  257. Name: "alarm_sites_by_code",
  258. Title: "查询触发某告警码的站点",
  259. Description: "查询当前触发指定告警码的站点列表(FWH 平台实时告警,按国家、分页)。返回 total 与 items(站点 SN、告警时间等,以实际返回为准)。",
  260. ReadOnly: true,
  261. Handler: func(ctx context.Context, a alarmSitesByCodeArgs) (*mcp.CallToolResult, error) {
  262. code, err := kit.Required("code", a.Code)
  263. if err != nil {
  264. return kit.Fail(err), nil
  265. }
  266. country, err := parseCountry(a.Country)
  267. if err != nil {
  268. return kit.Fail(err), nil
  269. }
  270. res, err := repositories.QueryAlarmStatisticsFromCode(country, code, kit.Page(a.Page))
  271. if err != nil {
  272. return kit.Upstream("FWH 平台告警统计接口", err), nil
  273. }
  274. if res == nil || res.Data == nil {
  275. return kit.Empty(""), nil
  276. }
  277. return kit.List(res.Data.Records, res.Data.Total, 200, fmt.Sprintf("共 %d 页,可用 page 翻页", res.Data.PageTotal)), nil
  278. },
  279. }