package tools import ( "context" "fmt" "sort" "strings" "time" "unicode/utf8" "github.com/mark3labs/mcp-go/mcp" "fwh-toolbox-server/agentmcp/kit" "fwh-toolbox-server/domain" "fwh-toolbox-server/repositories" "fwh-toolbox-server/services" ) func parseCountry(v string) (domain.CountryType, error) { c, err := kit.Enum("country", v, "all", "all", "us", "au") if err != nil { return 0, err } switch c { case "us": return domain.CTRY_US, nil case "au": return domain.CTRY_AU, nil } return domain.CTRY_ALL, nil } // ---- alarm_current ---- type snArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` } var AlarmCurrent = kit.Def[snArgs]{ Name: "alarm_current", Title: "查询站点当前告警", Description: "查询单个站点当前未结束的告警列表(FWH 平台实时告警)。" + "返回字段:alarmEqSn(告警设备 SN)、alarmCode / commonAlarmCode(告警码)、level(等级)、time(发生时间)、logName / logNameEn(告警名称)。", ReadOnly: true, Handler: func(ctx context.Context, a snArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } res, err := repositories.QuerySiteAlarmList(sn) if err != nil { return kit.Upstream("FWH 平台实时告警接口", err), nil } if res == nil || res.Result == nil || len(*res.Result) == 0 { return kit.Empty("该站点当前没有未结束的告警。"), nil } return kit.List(*res.Result, -1, 200, ""), nil }, } // ---- alarm_events ---- type alarmEventsArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` From string `json:"from" jsonschema:"开始时间,yyyy-MM-dd HH:mm:ss,站点当地时间"` To string `json:"to" jsonschema:"结束时间,同上;与开始时间相差不超过 31 天"` Kind string `json:"kind,omitempty" jsonschema:"alarm(只看告警)/ event(只看事件)/ all(默认)"` Limit int `json:"limit,omitempty" jsonschema:"最多返回条数,1~500,默认 100"` } // EventRow alarm_events 的精简行 type EventRow struct { Kind string `json:"kind"` // 告警 / 事件 LogTime string `json:"logTime"` // 设备时间 StartTime string `json:"startTime,omitempty"` EndTime string `json:"endTime,omitempty"` Code string `json:"code,omitempty"` Name string `json:"name,omitempty"` Cmd string `json:"cmd,omitempty"` Data string `json:"data,omitempty"` // 原始 logData,超过 500 字截断 } var AlarmEvents = kit.Def[alarmEventsArgs]{ Name: "alarm_events", Title: "查询站点告警/事件记录", Description: "查询单个站点在一段时间内的告警和事件记录(FWH 平台日志),按时间倒序。用于排查某次故障前后发生了什么。" + "返回字段:kind(告警/事件)、logTime(设备当地时间)、startTime、endTime(告警结束时间,未结束为空)、code(告警码,事件为 0)、name、cmd、data(原始日志数据,超过 500 字截断)。" + "超过 limit 时 truncated=true,请缩短时间范围。时间范围较大时查询较慢。", ReadOnly: true, Handler: func(ctx context.Context, a alarmEventsArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } from, to, err := kit.ParseRange(a.From, a.To, 31*24*time.Hour) if err != nil { return kit.Fail(err), nil } kind, err := kit.Enum("kind", a.Kind, "all", "all", "alarm", "event") if err != nil { return kit.Fail(err), nil } t := repositories.EventAndAlarm switch kind { case "alarm": t = repositories.Alarm case "event": t = repositories.Event } items, err := repositories.QueryEventOrAlarmListOnTimeRange(sn, from, to, t) if err != nil { return kit.Upstream("FWH 平台事件告警日志接口", err), nil } rows := MapEventRows(items) return kit.List(rows, -1, kit.Limit(a.Limit, 100, 500), "结果已截断,请缩短时间范围或改用 kind 过滤"), nil }, } // MapEventRows 转为精简行并按设备时间倒序。 func MapEventRows(items []domain.EventAlarmItem) []EventRow { rows := make([]EventRow, 0, len(items)) for _, it := range items { r := EventRow{ Kind: "事件", LogTime: it.LogTime, StartTime: it.StartTime, Code: it.AlarmCode, Name: it.LogName, Cmd: strings.TrimSpace(it.CmdName + " " + it.CmdType), Data: clip(it.LogData, 500), } if it.LogType == "1" { r.Kind = "告警" } if it.EndTime != nil { r.EndTime = *it.EndTime } if r.Code == "0" { r.Code = "" } rows = append(rows, r) } sort.SliceStable(rows, func(i, j int) bool { return rows[i].LogTime > rows[j].LogTime }) return rows } func clip(s string, n int) string { if utf8.RuneCountInString(s) <= n { return s } return string([]rune(s)[:n]) + "…" } // ---- alarm_snapshot ---- type alarmSnapshotArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` Date string `json:"date" jsonschema:"日期,yyyy-MM-dd"` } var AlarmSnapshot = kit.Def[alarmSnapshotArgs]{ Name: "alarm_snapshot", Title: "查询站点告警快照", Description: "查询单个站点某一天的告警快照记录(告警发生时设备上传的快照文件信息)。" + "返回字段:id、fileName、fileUrl、时间等(以实际返回为准)。", ReadOnly: true, Handler: func(ctx context.Context, a alarmSnapshotArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } day, err := kit.ParseDate("date", a.Date, false) if err != nil { return kit.Fail(err), nil } items, err := repositories.QueryAlarmSnapshotInfo(sn, day.Format(kit.DateLayout)) if err != nil { return kit.Upstream("FWH 平台告警快照接口", err), nil } return kit.List(items, -1, 100, ""), nil }, } // ---- alarm_stats ---- type alarmStatsArgs struct { Source string `json:"source,omitempty" jsonschema:"daily(默认):ToolBox 每日告警统计,按告警码汇总站点数;realtime:FWH 平台当前告警统计"` Date string `json:"date,omitempty" jsonschema:"daily 时的统计日期 yyyy-MM-dd(北京时间),不填取最新"` Country string `json:"country,omitempty" jsonschema:"realtime 时的国家:all(默认)、us(美国)、au(澳大利亚)"` Page int `json:"page,omitempty" jsonschema:"realtime 时的页码,默认 1"` Top int `json:"top,omitempty" jsonschema:"daily 时返回告警码数量最多的前 N 个,1~200,默认 50"` } // CodeCount 告警码计数 type CodeCount struct { Code string `json:"code"` Count uint `json:"count"` } var AlarmStats = kit.Def[alarmStatsArgs]{ Name: "alarm_stats", Title: "查询全站告警统计", Description: "查询全量站点的告警统计。source=daily(默认):ToolBox 每日统计,返回 date、siteStatistics(有告警的站点数)、codes(按触发站点数倒序的告警码列表 code/count);" + "source=realtime:FWH 平台当前告警统计(按国家、分页),返回 total 与 records。" + "查看某个告警码涉及哪些站点用 alarm_sites_by_code。", ReadOnly: true, Handler: func(ctx context.Context, a alarmStatsArgs) (*mcp.CallToolResult, error) { src, err := kit.Enum("source", a.Source, "daily", "daily", "realtime") if err != nil { return kit.Fail(err), nil } if src == "realtime" { country, err := parseCountry(a.Country) if err != nil { return kit.Fail(err), nil } res, err := repositories.QueryAlarmStatistics(country, kit.Page(a.Page)) if err != nil { return kit.Upstream("FWH 平台告警统计接口", err), nil } if res == nil || res.Data == nil { return kit.Empty(""), nil } return kit.List(res.Data.Records, res.Data.Total, 200, fmt.Sprintf("共 %d 页,可用 page 翻页", res.Data.PageTotal)), nil } day, err := kit.ParseDate("date", a.Date, true) if err != nil { return kit.Fail(err), nil } var datePtr *string if day != nil { s := day.Format(kit.DateLayout) datePtr = &s } dash, err := services.GetAlarmStatistics(datePtr) if err != nil { return kit.Upstream("ToolBox 告警统计", err), nil } if dash == nil { return kit.Empty("没有该日期的告警统计。"), nil } codes := SortCodeCounts(dash.CodeStatistics) top := kit.Limit(a.Top, 50, 200) total := len(codes) if len(codes) > top { codes = codes[:top] } return kit.Object(map[string]any{ "date": dash.Date, "siteStatistics": dash.SiteStatistics, "codeTotal": total, "codes": codes, }), nil }, } // SortCodeCounts 按计数倒序、告警码升序排列。 func SortCodeCounts(m map[string]uint) []CodeCount { out := make([]CodeCount, 0, len(m)) for k, v := range m { out = append(out, CodeCount{Code: k, Count: v}) } sort.Slice(out, func(i, j int) bool { if out[i].Count != out[j].Count { return out[i].Count > out[j].Count } return out[i].Code < out[j].Code }) return out } // ---- alarm_sites_by_code ---- type alarmSitesByCodeArgs struct { Code string `json:"code" jsonschema:"告警码"` Country string `json:"country,omitempty" jsonschema:"国家:all(默认)、us(美国)、au(澳大利亚)"` Page int `json:"page,omitempty" jsonschema:"页码,默认 1"` } var AlarmSitesByCode = kit.Def[alarmSitesByCodeArgs]{ Name: "alarm_sites_by_code", Title: "查询触发某告警码的站点", Description: "查询当前触发指定告警码的站点列表(FWH 平台实时告警,按国家、分页)。返回 total 与 items(站点 SN、告警时间等,以实际返回为准)。", ReadOnly: true, Handler: func(ctx context.Context, a alarmSitesByCodeArgs) (*mcp.CallToolResult, error) { code, err := kit.Required("code", a.Code) if err != nil { return kit.Fail(err), nil } country, err := parseCountry(a.Country) if err != nil { return kit.Fail(err), nil } res, err := repositories.QueryAlarmStatisticsFromCode(country, code, kit.Page(a.Page)) if err != nil { return kit.Upstream("FWH 平台告警统计接口", err), nil } if res == nil || res.Data == nil { return kit.Empty(""), nil } return kit.List(res.Data.Records, res.Data.Total, 200, fmt.Sprintf("共 %d 页,可用 page 翻页", res.Data.PageTotal)), nil }, }