| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 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: "+err.Error())
- } else {
- out["info"] = info
- }
- if mode, err := repositories.QuerySiteModeInfo(sn); err != nil {
- errs = append(errs, "mode: "+err.Error())
- } else if mode != nil {
- out["mode"] = mode.Result
- }
- if alarms, err := repositories.QuerySiteAlarmList(sn); err != nil {
- errs = append(errs, "currentAlarms: "+err.Error())
- } 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)
- }
|