package tools import ( "context" "fmt" "strconv" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "fwh-toolbox-server/agentmcp/kit" "fwh-toolbox-server/dto" "fwh-toolbox-server/repositories" "fwh-toolbox-server/services" ) // ---- site_search ---- type siteSearchArgs struct { Keyword string `json:"keyword,omitempty" jsonschema:"搜索关键字,匹配 SN、站点名称等;不填则按默认排序列出"` Page int `json:"page,omitempty" jsonschema:"页码,从 1 开始,默认 1"` Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"` } var SiteSearch = kit.Def[siteSearchArgs]{ Name: "site_search", Title: "搜索站点", Description: "在 ToolBox 站点信息表中搜索站点(按 SN 片段、站点名称等关键字),分页返回。" + "返回字段:sn(aGate SN)、name、国家/州省/城市、激活状态与时间、在线状态、系统与固件版本、aPower 数量等(以实际返回为准,个人信息已脱敏)。" + "返回 total 为符合条件的总数,用 page 翻页。查看单个站点完整概况请用 site_get。", ReadOnly: true, Handler: func(ctx context.Context, a siteSearchArgs) (*mcp.CallToolResult, error) { page := kit.Page(a.Page) size := kit.Limit(a.Size, 20, 100) data, err := services.QuerySiteInfoDataList(dto.SiteInfoRequest{ Search: strings.TrimSpace(a.Keyword), PageNum: uint(page), PageSize: uint(size), }) if err != nil { return kit.Upstream("站点信息表", err), nil } if data == nil { return kit.Empty(""), nil } return kit.List(data.Data, int(data.Total), size, fmt.Sprintf("共 %d 条,当前第 %d 页,可用 page 翻页", data.Total, page)), nil }, } // ---- site_get ---- type siteGetArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` } var SiteGet = kit.Def[siteGetArgs]{ Name: "site_get", Title: "查询站点概况", Description: "查询单个站点的概况,合并三部分:info(站点信息表中的基本信息、版本、网络、激活状态等)、" + "mode(当前工作模式与相关设置)、currentAlarms(当前未结束的告警,最多 20 条)。" + "任一部分查询失败时在 errors 中说明,不影响其他部分。排查站点问题时通常先调用本工具。", ReadOnly: true, Handler: func(ctx context.Context, a siteGetArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } out := map[string]any{"sn": sn} var errs []string if info, err := services.GetSiteInfoData(sn); err != nil { errs = append(errs, "info: "+kit.UpstreamText("站点信息表", err)) } else { out["info"] = info } if mode, err := repositories.QuerySiteModeInfo(sn); err != nil { errs = append(errs, "mode: "+kit.UpstreamText("FWH 平台工作模式接口", err)) } else if mode != nil { out["mode"] = mode.Result } if alarms, err := repositories.QuerySiteAlarmList(sn); err != nil { errs = append(errs, "currentAlarms: "+kit.UpstreamText("FWH 平台实时告警接口", err)) } else if alarms != nil && alarms.Result != nil { list := *alarms.Result out["currentAlarmTotal"] = len(list) if len(list) > 20 { list = list[:20] } out["currentAlarms"] = list } if len(errs) > 0 { out["errors"] = errs } return kit.Object(out), nil }, } // ---- site_power_flow ---- type sitePowerFlowArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` Date string `json:"date" jsonschema:"日期,yyyy-MM-dd,站点当地日期"` } // powerFlowCols 功率流表格的列(功率单位 kW,SOC 单位 %,电量单位 kWh) var powerFlowCols = []string{"time", "pv_kw", "grid_kw", "apower_kw", "load_kw", "gen_kw", "soc_pct", "run_status", "kwh_total"} var SitePowerFlow = kit.Def[sitePowerFlowArgs]{ Name: "site_power_flow", Title: "查询站点单日功率流", Description: "查询单个站点某一天的功率流(约 5 分钟一个点),以 TSV 表格返回。" + "列:time(设备当地时间)、pv_kw(光伏)、grid_kw(电网,正为取电、负为馈网,以平台定义为准)、apower_kw(aPower,正为放电、负为充电,以平台定义为准)、" + "load_kw(家庭负载)、gen_kw(发电机)、soc_pct(电池 SOC,%)、run_status(0 待机 1 充电 2 放电 3 故障)、kwh_total(可用电量 kWh)。" + "需要多天数据时用 site_power_flow_export。", ReadOnly: true, Handler: func(ctx context.Context, a sitePowerFlowArgs) (*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 } rows, err := powerFlowRows(sn, *day) if err != nil { return kit.Upstream("FWH 平台功率流接口", err), nil } return kit.Table(powerFlowCols, rows, 0, "数据过多已截断,请用 site_power_flow_export 导出完整数据"), nil }, } // ---- site_power_flow_export ---- type sitePowerFlowExportArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` From string `json:"from" jsonschema:"开始日期,yyyy-MM-dd(含)"` To string `json:"to" jsonschema:"结束日期,yyyy-MM-dd(含),与开始日期相差不超过 31 天"` } var SitePowerFlowExport = kit.Def[sitePowerFlowExportArgs]{ Name: "site_power_flow_export", Title: "导出站点多日功率流", Description: "导出单个站点一段日期(最多 31 天)的功率流明细为 CSV 文件,列与 site_power_flow 相同,另加 date 列。" + "文件保存到会话工作区,返回文件信息与前 5 行预览;请用 Python(pandas)读取分析。某天查询失败时跳过并在摘要中说明。", ReadOnly: true, Handler: func(ctx context.Context, a sitePowerFlowExportArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } from, err := kit.ParseDate("from", a.From, false) if err != nil { return kit.Fail(err), nil } to, err := kit.ParseDate("to", a.To, false) if err != nil { return kit.Fail(err), nil } if to.Before(*from) { return kit.Failf("结束日期不能早于开始日期"), nil } if to.Sub(*from) > 30*24*time.Hour { return kit.Failf("日期范围不能超过 31 天,请分段导出"), nil } cols := append([]string{"date"}, powerFlowCols...) var rows [][]string var failed []string for d := *from; !d.After(*to); d = d.AddDate(0, 0, 1) { if ctx.Err() != nil { return kit.Failf("导出超时,已完成到 %s,请缩短日期范围", d.Format(kit.DateLayout)), nil } dayRows, err := powerFlowRows(sn, d) if err != nil { failed = append(failed, d.Format(kit.DateLayout)) continue } for _, r := range dayRows { rows = append(rows, append([]string{d.Format(kit.DateLayout)}, r...)) } } summary := fmt.Sprintf("站点 %s,%s ~ %s。", sn, from.Format(kit.DateLayout), to.Format(kit.DateLayout)) if len(failed) > 0 { summary += fmt.Sprintf("以下日期查询失败已跳过:%s。", strings.Join(failed, ", ")) } uri := fmt.Sprintf("fwh://export/site_power_flow/%s_%s_%s.csv", sn, from.Format("20060102"), to.Format("20060102")) return kit.Export(uri, cols, rows, summary), nil }, } // powerFlowRows 查询单日功率流并转为表格行(列顺序见 powerFlowCols)。 func powerFlowRows(sn string, day time.Time) ([][]string, error) { res, err := services.QuerySitePowerFlow(sn, day) if err != nil { return nil, err } if res == nil || res.Result == nil { return nil, nil } return PowerFlowToRows(res.Result), nil } // PowerFlowToRows 把平台的列式功率流数组转为行;数组长度不一致时缺失值留空。 func PowerFlowToRows(p *dto.SitePowerFlow) [][]string { n := len(p.DeviceTimeArray) rows := make([][]string, 0, n) for i := 0; i < n; i++ { rows = append(rows, []string{ p.DeviceTimeArray[i], f32At(p.PowerSunArray, i), f32At(p.PowerUtiArray, i), f32At(p.PowerFhpArray, i), f32At(p.PowerLoadArray, i), f32At(p.PowerGenArray, i), f32At(p.SocArray, i), uintAt(p.RunStatusArray, i), f32At(p.KwhTotalArray, i), }) } return rows } func f32At(a []float32, i int) string { if i >= len(a) { return "" } return strconv.FormatFloat(float64(a[i]), 'f', -1, 32) } func uintAt(a []uint, i int) string { if i >= len(a) { return "" } return strconv.FormatUint(uint64(a[i]), 10) }