|
|
@@ -3,6 +3,7 @@ package tools
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
+ "regexp"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
@@ -10,6 +11,7 @@ import (
|
|
|
"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"
|
|
|
@@ -18,36 +20,145 @@ import (
|
|
|
// ---- 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"`
|
|
|
+ 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 站点信息表中搜索站点(按 SN 片段、站点名称等关键字),分页返回。" +
|
|
|
- "返回字段:sn(aGate SN)、name、国家/州省/城市、激活状态与时间、在线状态、系统与固件版本、aPower 数量等(以实际返回为准,个人信息已脱敏)。" +
|
|
|
- "返回 total 为符合条件的总数,用 page 翻页。查看单个站点完整概况请用 site_get。",
|
|
|
+ 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: strings.TrimSpace(a.Keyword),
|
|
|
+ Search: regexp.QuoteMeta(keyword), // 仓库按正则匹配,转义后按字面匹配
|
|
|
+ Filter: filter,
|
|
|
PageNum: uint(page),
|
|
|
PageSize: uint(size),
|
|
|
})
|
|
|
if err != nil {
|
|
|
return kit.Upstream("站点信息表", err), nil
|
|
|
}
|
|
|
- if data == nil {
|
|
|
- return kit.Empty(""), 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
|
|
|
+ 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 {
|