site.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package tools
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/mark3labs/mcp-go/mcp"
  9. "fwh-toolbox-server/agentmcp/kit"
  10. "fwh-toolbox-server/dto"
  11. "fwh-toolbox-server/repositories"
  12. "fwh-toolbox-server/services"
  13. )
  14. // ---- site_search ----
  15. type siteSearchArgs struct {
  16. Keyword string `json:"keyword,omitempty" jsonschema:"搜索关键字,匹配 SN、站点名称等;不填则按默认排序列出"`
  17. Page int `json:"page,omitempty" jsonschema:"页码,从 1 开始,默认 1"`
  18. Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"`
  19. }
  20. var SiteSearch = kit.Def[siteSearchArgs]{
  21. Name: "site_search",
  22. Title: "搜索站点",
  23. Description: "在 ToolBox 站点信息表中搜索站点(按 SN 片段、站点名称等关键字),分页返回。" +
  24. "返回字段:sn(aGate SN)、name、国家/州省/城市、激活状态与时间、在线状态、系统与固件版本、aPower 数量等(以实际返回为准,个人信息已脱敏)。" +
  25. "返回 total 为符合条件的总数,用 page 翻页。查看单个站点完整概况请用 site_get。",
  26. ReadOnly: true,
  27. Handler: func(ctx context.Context, a siteSearchArgs) (*mcp.CallToolResult, error) {
  28. page := kit.Page(a.Page)
  29. size := kit.Limit(a.Size, 20, 100)
  30. data, err := services.QuerySiteInfoDataList(dto.SiteInfoRequest{
  31. Search: strings.TrimSpace(a.Keyword),
  32. PageNum: uint(page),
  33. PageSize: uint(size),
  34. })
  35. if err != nil {
  36. return kit.Upstream("站点信息表", err), nil
  37. }
  38. if data == nil {
  39. return kit.Empty(""), nil
  40. }
  41. return kit.List(data.Data, int(data.Total), size, fmt.Sprintf("共 %d 条,当前第 %d 页,可用 page 翻页", data.Total, page)), nil
  42. },
  43. }
  44. // ---- site_get ----
  45. type siteGetArgs struct {
  46. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  47. }
  48. var SiteGet = kit.Def[siteGetArgs]{
  49. Name: "site_get",
  50. Title: "查询站点概况",
  51. Description: "查询单个站点的概况,合并三部分:info(站点信息表中的基本信息、版本、网络、激活状态等)、" +
  52. "mode(当前工作模式与相关设置)、currentAlarms(当前未结束的告警,最多 20 条)。" +
  53. "任一部分查询失败时在 errors 中说明,不影响其他部分。排查站点问题时通常先调用本工具。",
  54. ReadOnly: true,
  55. Handler: func(ctx context.Context, a siteGetArgs) (*mcp.CallToolResult, error) {
  56. sn, err := kit.Required("sn", a.SN)
  57. if err != nil {
  58. return kit.Fail(err), nil
  59. }
  60. out := map[string]any{"sn": sn}
  61. var errs []string
  62. if info, err := services.GetSiteInfoData(sn); err != nil {
  63. errs = append(errs, "info: "+err.Error())
  64. } else {
  65. out["info"] = info
  66. }
  67. if mode, err := repositories.QuerySiteModeInfo(sn); err != nil {
  68. errs = append(errs, "mode: "+err.Error())
  69. } else if mode != nil {
  70. out["mode"] = mode.Result
  71. }
  72. if alarms, err := repositories.QuerySiteAlarmList(sn); err != nil {
  73. errs = append(errs, "currentAlarms: "+err.Error())
  74. } else if alarms != nil && alarms.Result != nil {
  75. list := *alarms.Result
  76. out["currentAlarmTotal"] = len(list)
  77. if len(list) > 20 {
  78. list = list[:20]
  79. }
  80. out["currentAlarms"] = list
  81. }
  82. if len(errs) > 0 {
  83. out["errors"] = errs
  84. }
  85. return kit.Object(out), nil
  86. },
  87. }
  88. // ---- site_power_flow ----
  89. type sitePowerFlowArgs struct {
  90. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  91. Date string `json:"date" jsonschema:"日期,yyyy-MM-dd,站点当地日期"`
  92. }
  93. // powerFlowCols 功率流表格的列(功率单位 kW,SOC 单位 %,电量单位 kWh)
  94. var powerFlowCols = []string{"time", "pv_kw", "grid_kw", "apower_kw", "load_kw", "gen_kw", "soc_pct", "run_status", "kwh_total"}
  95. var SitePowerFlow = kit.Def[sitePowerFlowArgs]{
  96. Name: "site_power_flow",
  97. Title: "查询站点单日功率流",
  98. Description: "查询单个站点某一天的功率流(约 5 分钟一个点),以 TSV 表格返回。" +
  99. "列:time(设备当地时间)、pv_kw(光伏)、grid_kw(电网,正为取电、负为馈网,以平台定义为准)、apower_kw(aPower,正为放电、负为充电,以平台定义为准)、" +
  100. "load_kw(家庭负载)、gen_kw(发电机)、soc_pct(电池 SOC,%)、run_status(0 待机 1 充电 2 放电 3 故障)、kwh_total(可用电量 kWh)。" +
  101. "需要多天数据时用 site_power_flow_export。",
  102. ReadOnly: true,
  103. Handler: func(ctx context.Context, a sitePowerFlowArgs) (*mcp.CallToolResult, error) {
  104. sn, err := kit.Required("sn", a.SN)
  105. if err != nil {
  106. return kit.Fail(err), nil
  107. }
  108. day, err := kit.ParseDate("date", a.Date, false)
  109. if err != nil {
  110. return kit.Fail(err), nil
  111. }
  112. rows, err := powerFlowRows(sn, *day)
  113. if err != nil {
  114. return kit.Upstream("FWH 平台功率流接口", err), nil
  115. }
  116. return kit.Table(powerFlowCols, rows, 0, "数据过多已截断,请用 site_power_flow_export 导出完整数据"), nil
  117. },
  118. }
  119. // ---- site_power_flow_export ----
  120. type sitePowerFlowExportArgs struct {
  121. SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"`
  122. From string `json:"from" jsonschema:"开始日期,yyyy-MM-dd(含)"`
  123. To string `json:"to" jsonschema:"结束日期,yyyy-MM-dd(含),与开始日期相差不超过 31 天"`
  124. }
  125. var SitePowerFlowExport = kit.Def[sitePowerFlowExportArgs]{
  126. Name: "site_power_flow_export",
  127. Title: "导出站点多日功率流",
  128. Description: "导出单个站点一段日期(最多 31 天)的功率流明细为 CSV 文件,列与 site_power_flow 相同,另加 date 列。" +
  129. "文件保存到会话工作区,返回文件信息与前 5 行预览;请用 Python(pandas)读取分析。某天查询失败时跳过并在摘要中说明。",
  130. ReadOnly: true,
  131. Handler: func(ctx context.Context, a sitePowerFlowExportArgs) (*mcp.CallToolResult, error) {
  132. sn, err := kit.Required("sn", a.SN)
  133. if err != nil {
  134. return kit.Fail(err), nil
  135. }
  136. from, err := kit.ParseDate("from", a.From, false)
  137. if err != nil {
  138. return kit.Fail(err), nil
  139. }
  140. to, err := kit.ParseDate("to", a.To, false)
  141. if err != nil {
  142. return kit.Fail(err), nil
  143. }
  144. if to.Before(*from) {
  145. return kit.Failf("结束日期不能早于开始日期"), nil
  146. }
  147. if to.Sub(*from) > 30*24*time.Hour {
  148. return kit.Failf("日期范围不能超过 31 天,请分段导出"), nil
  149. }
  150. cols := append([]string{"date"}, powerFlowCols...)
  151. var rows [][]string
  152. var failed []string
  153. for d := *from; !d.After(*to); d = d.AddDate(0, 0, 1) {
  154. if ctx.Err() != nil {
  155. return kit.Failf("导出超时,已完成到 %s,请缩短日期范围", d.Format(kit.DateLayout)), nil
  156. }
  157. dayRows, err := powerFlowRows(sn, d)
  158. if err != nil {
  159. failed = append(failed, d.Format(kit.DateLayout))
  160. continue
  161. }
  162. for _, r := range dayRows {
  163. rows = append(rows, append([]string{d.Format(kit.DateLayout)}, r...))
  164. }
  165. }
  166. summary := fmt.Sprintf("站点 %s,%s ~ %s。", sn, from.Format(kit.DateLayout), to.Format(kit.DateLayout))
  167. if len(failed) > 0 {
  168. summary += fmt.Sprintf("以下日期查询失败已跳过:%s。", strings.Join(failed, ", "))
  169. }
  170. uri := fmt.Sprintf("fwh://export/site_power_flow/%s_%s_%s.csv", sn, from.Format("20060102"), to.Format("20060102"))
  171. return kit.Export(uri, cols, rows, summary), nil
  172. },
  173. }
  174. // powerFlowRows 查询单日功率流并转为表格行(列顺序见 powerFlowCols)。
  175. func powerFlowRows(sn string, day time.Time) ([][]string, error) {
  176. res, err := services.QuerySitePowerFlow(sn, day)
  177. if err != nil {
  178. return nil, err
  179. }
  180. if res == nil || res.Result == nil {
  181. return nil, nil
  182. }
  183. return PowerFlowToRows(res.Result), nil
  184. }
  185. // PowerFlowToRows 把平台的列式功率流数组转为行;数组长度不一致时缺失值留空。
  186. func PowerFlowToRows(p *dto.SitePowerFlow) [][]string {
  187. n := len(p.DeviceTimeArray)
  188. rows := make([][]string, 0, n)
  189. for i := 0; i < n; i++ {
  190. rows = append(rows, []string{
  191. p.DeviceTimeArray[i],
  192. f32At(p.PowerSunArray, i),
  193. f32At(p.PowerUtiArray, i),
  194. f32At(p.PowerFhpArray, i),
  195. f32At(p.PowerLoadArray, i),
  196. f32At(p.PowerGenArray, i),
  197. f32At(p.SocArray, i),
  198. uintAt(p.RunStatusArray, i),
  199. f32At(p.KwhTotalArray, i),
  200. })
  201. }
  202. return rows
  203. }
  204. func f32At(a []float32, i int) string {
  205. if i >= len(a) {
  206. return ""
  207. }
  208. return strconv.FormatFloat(float64(a[i]), 'f', -1, 32)
  209. }
  210. func uintAt(a []uint, i int) string {
  211. if i >= len(a) {
  212. return ""
  213. }
  214. return strconv.FormatUint(uint64(a[i]), 10)
  215. }