| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- package tools
- import (
- "context"
- "fmt"
- "regexp"
- "strconv"
- "strings"
- "time"
- "github.com/mark3labs/mcp-go/mcp"
- "fwh-toolbox-server/agentmcp/kit"
- "fwh-toolbox-server/domain"
- "fwh-toolbox-server/dto"
- "fwh-toolbox-server/repositories"
- "fwh-toolbox-server/services"
- )
- // ---- site_search ----
- type siteSearchArgs struct {
- Keyword string `json:"keyword,omitempty" jsonschema:"关键字,匹配 SN、站点名称、经销商/安装商、aPower SN、ICCID、版本、国家/州省/城市、备注等(不区分大小写);不填则只按筛选条件"`
- Country string `json:"country,omitempty" jsonschema:"国家:us(美国)、au(澳大利亚)"`
- Province string `json:"province,omitempty" jsonschema:"州/省(英文,包含匹配),如 California"`
- Online string `json:"online,omitempty" jsonschema:"网络状态:online(在线)、offline(离线)"`
- Active string `json:"active,omitempty" jsonschema:"激活状态:active(已激活)、inactive(未激活)"`
- RunStatus string `json:"runStatus,omitempty" jsonschema:"运行状态:standby(待机)、charging(充电)、discharging(放电)、fault(故障)"`
- ConnType string `json:"connType,omitempty" jsonschema:"联网方式:none(未连接)、eth0、eth1、wifi、4g"`
- Version string `json:"version,omitempty" jsonschema:"系统固件版本(包含匹配),如 V10R11B42"`
- ProtocolVer string `json:"protocolVer,omitempty" jsonschema:"协议版本(包含匹配),如 V1.12.45"`
- Hardware string `json:"hardware,omitempty" jsonschema:"aGate 系统硬件名称(包含匹配),如 System1.5"`
- ActiveFrom string `json:"activeFrom,omitempty" jsonschema:"激活时间起(含),yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss,北京时间"`
- ActiveTo string `json:"activeTo,omitempty" jsonschema:"激活时间止(含),格式同上"`
- Page int `json:"page,omitempty" jsonschema:"页码,从 1 开始,默认 1"`
- Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20;只需要数量时用 1,看 total"`
- }
- var (
- siteCountryIDs = map[string]int64{"us": 2, "au": 3}
- siteOnline = map[string]int64{"offline": 0, "online": 1}
- siteActive = map[string]int64{"inactive": 0, "active": 1}
- siteRunStatus = map[string]int64{"standby": 0, "charging": 1, "discharging": 2, "fault": 3}
- siteConnTypes = map[string]int64{"none": 0, "eth0": 1, "eth1": 2, "wifi": 3, "4g": 4}
- )
- var SiteSearch = kit.Def[siteSearchArgs]{
- Name: "site_search",
- Title: "搜索站点",
- Description: "在 ToolBox 站点信息表(约 6 万个站点,每天从 FWH 平台同步)中按关键字与条件搜索站点,分页返回;多个条件同时满足。" +
- "可按国家、州/省、在线、激活、运行状态、联网方式、固件/协议版本、aGate 硬件、激活时间筛选。统计数量时 size=1 看 total," +
- "如“美国在线的 System1.5 站点有多少”。返回字段:sn(aGate SN)、name、国家/州省/城市、激活状态与时间、在线状态、版本、aPower SN 等(个人信息已脱敏)。" +
- "数据是否最新见 site_info_sync_status;查看单个站点完整概况用 site_get。",
- ReadOnly: true,
- Properties: map[string]map[string]any{
- "country": {"enum": []string{"us", "au"}},
- "online": {"enum": []string{"online", "offline"}},
- "active": {"enum": []string{"active", "inactive"}},
- "runStatus": {"enum": []string{"standby", "charging", "discharging", "fault"}},
- "connType": {"enum": []string{"none", "eth0", "eth1", "wifi", "4g"}},
- },
- Handler: func(ctx context.Context, a siteSearchArgs) (*mcp.CallToolResult, error) {
- page := kit.Page(a.Page)
- size := kit.Limit(a.Size, 20, 100)
- filter, err := siteSearchFilter(a)
- if err != nil {
- return kit.Fail(err), nil
- }
- keyword := strings.TrimSpace(a.Keyword)
- data, err := services.QuerySiteInfoDataList(dto.SiteInfoRequest{
- Search: regexp.QuoteMeta(keyword), // 仓库按正则匹配,转义后按字面匹配
- Filter: filter,
- PageNum: uint(page),
- PageSize: uint(size),
- })
- if err != nil {
- return kit.Upstream("站点信息表", err), nil
- }
- if data == nil || data.Total == 0 {
- return kit.Empty("没有符合条件的站点。"), nil
- }
- return kit.List(data.Data, int(data.Total), size, fmt.Sprintf("共 %d 条,当前第 %d 页,可用 page 翻页或增加条件", data.Total, page)), nil
- },
- }
- // activeTimeBound 解析激活时间范围的一端:接受日期或日期时间;只给日期时,止时间取当天最后一秒
- func activeTimeBound(name, v string, end bool) (string, error) {
- v = strings.TrimSpace(v)
- if t, err := time.Parse(time.DateTime, v); err == nil {
- return t.Format(time.DateTime), nil
- }
- t, err := time.Parse(time.DateOnly, v)
- if err != nil {
- return "", fmt.Errorf("参数 %s 格式错误,需要 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss:%s", name, v)
- }
- if end {
- t = t.Add(24*time.Hour - time.Second)
- }
- return t.Format(time.DateTime), nil
- }
- // siteSearchFilter 把筛选参数转为站点信息表的过滤条件(全部为“且”);没有条件时返回 nil
- func siteSearchFilter(a siteSearchArgs) (*domain.SiteInfoFilter, error) {
- var conds []domain.SiteInfoFilterConfig
- eqInt := func(key string, v int64) {
- conds = append(conds, domain.SiteInfoFilterConfig{Key: key, Type: domain.FilterEqualInt, Para1: domain.FlexiblePara{Int: &v}})
- }
- contains := func(key, v string) {
- if v = strings.TrimSpace(v); v != "" {
- q := regexp.QuoteMeta(v)
- conds = append(conds, domain.SiteInfoFilterConfig{Key: key, Type: domain.FilterIncludeString, Para1: domain.FlexiblePara{String: &q}})
- }
- }
- enums := []struct {
- key, name, val string
- m map[string]int64
- }{
- {"countryId", "country", a.Country, siteCountryIDs},
- {"status", "online", a.Online, siteOnline},
- {"activeStatus", "active", a.Active, siteActive},
- {"runStatus", "runStatus", a.RunStatus, siteRunStatus},
- {"connType", "connType", a.ConnType, siteConnTypes},
- }
- for _, e := range enums {
- if e.val == "" {
- continue
- }
- v, ok := e.m[strings.ToLower(e.val)]
- if !ok {
- return nil, fmt.Errorf("参数 %s 的值 %q 无效", e.name, e.val)
- }
- eqInt(e.key, v)
- }
- contains("province", a.Province)
- contains("version", a.Version)
- contains("protocolVer", a.ProtocolVer)
- contains("realSysHdVersion", a.Hardware)
- if a.ActiveFrom != "" || a.ActiveTo != "" {
- // activeTime 以 "yyyy-MM-dd HH:mm:ss" 字符串保存,按字符串比较
- from, to := "0000-00-00 00:00:00", "9999-12-31 23:59:59"
- var err error
- if a.ActiveFrom != "" {
- if from, err = activeTimeBound("activeFrom", a.ActiveFrom, false); err != nil {
- return nil, err
- }
- }
- if a.ActiveTo != "" {
- if to, err = activeTimeBound("activeTo", a.ActiveTo, true); err != nil {
- return nil, err
- }
- }
- conds = append(conds, domain.SiteInfoFilterConfig{Key: "activeTime", Type: domain.FilterRangeTime,
- Para1: domain.FlexiblePara{String: &from}, Para2: &domain.FlexiblePara{String: &to}})
- }
- if len(conds) == 0 {
- return nil, nil
- }
- return &domain.SiteInfoFilter{Logic: "and", SubFilter: []domain.SiteInfoSubFilter{{Logic: "and", Config: conds}}}, 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)
- }
|