tou.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "github.com/mark3labs/mcp-go/mcp"
  10. "fwh-toolbox-server/agentmcp/kit"
  11. "fwh-toolbox-server/dto"
  12. "fwh-toolbox-server/services"
  13. )
  14. func parseEnv(v string) (string, error) {
  15. return kit.Enum("env", v, "prod", "prod", "test")
  16. }
  17. // ---- tou_template_list ----
  18. type touTemplateListArgs struct {
  19. Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"`
  20. Search string `json:"search,omitempty" jsonschema:"搜索关键字(模版名称、电力公司等)"`
  21. Type *int `json:"type,omitempty" jsonschema:"模版类型:0 系统模版、1 API 模版、2 用户推荐;不填为全部"`
  22. Status *int `json:"status,omitempty" jsonschema:"发布状态:0 未发布、2 已发布;不填为全部"`
  23. Page int `json:"page,omitempty" jsonschema:"页码,默认 1"`
  24. Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"`
  25. }
  26. var TouTemplateList = kit.Def[touTemplateListArgs]{
  27. Name: "tou_template_list",
  28. Title: "查询 TOU 调度模版列表",
  29. Description: "分页查询 FWH 云平台的 TOU 调度模版(生产服或测试服)。返回 total 与 items(模版 ID、名称、类型、国家/地区、电力公司、电价类型、发布状态、更新时间等,以实际返回为准)。" +
  30. "查看某个模版的时段与调度模式用 tou_template_get。",
  31. ReadOnly: true,
  32. Handler: func(ctx context.Context, a touTemplateListArgs) (*mcp.CallToolResult, error) {
  33. env, err := parseEnv(a.Env)
  34. if err != nil {
  35. return kit.Fail(err), nil
  36. }
  37. page := kit.Page(a.Page)
  38. size := kit.Limit(a.Size, 20, 100)
  39. var data *dto.ResPage[interface{}]
  40. if env == "test" {
  41. data, err = services.FwhTestGetTouTemplateProList(uint(page), uint(size), a.Type, a.Status, strings.TrimSpace(a.Search))
  42. } else {
  43. data, err = services.GetTouTemplateProList(uint(page), uint(size), a.Type, a.Status, strings.TrimSpace(a.Search))
  44. }
  45. if err != nil {
  46. return kit.Upstream("FWH 平台 TOU 模版接口("+env+")", err), nil
  47. }
  48. if data == nil {
  49. return kit.Empty(""), nil
  50. }
  51. return kit.List(data.Data, int(data.Total), size, fmt.Sprintf("共 %d 条,当前第 %d 页,可用 page 翻页", data.Total, page)), nil
  52. },
  53. }
  54. // ---- tou_template_get ----
  55. type touTemplateGetArgs struct {
  56. Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"`
  57. ID int `json:"id" jsonschema:"模版 ID"`
  58. Raw bool `json:"raw,omitempty" jsonschema:"为 true 时返回云端原始详情(字段完整但较长),默认 false 返回整理后的结构"`
  59. }
  60. var TouTemplateGet = kit.Def[touTemplateGetArgs]{
  61. Name: "tou_template_get",
  62. Title: "查询 TOU 调度模版详情",
  63. Description: "查询单个 TOU 调度模版的详情,默认整理为:template(模版主体字段)与 strategies(按季节 × 日类型的策略)。" +
  64. "每个策略含 seasonName、month、dayName、ccociateSn、电价,以及 periods(时段):start、end、waveType(谷/平/峰/尖峰/低谷)、" +
  65. "solarPriority / loadPriority(原始值)、mode(识别出的调度模式:A~H、E/G、F/H、强制充电、强制放电、Zero Import、未设置、自定义)、params(设置了的扩展参数,-1 视为未设置)、issues(异常说明)。" +
  66. "优先级顺序:光伏 [市电, 负载, aPower],负载 [市电, 光伏, aPower]。raw=true 返回原始详情。",
  67. ReadOnly: true,
  68. Handler: func(ctx context.Context, a touTemplateGetArgs) (*mcp.CallToolResult, error) {
  69. env, err := parseEnv(a.Env)
  70. if err != nil {
  71. return kit.Fail(err), nil
  72. }
  73. if a.ID <= 0 {
  74. return kit.Failf("参数 id 必填且为正整数"), nil
  75. }
  76. var data *interface{}
  77. if env == "test" {
  78. data, err = services.FwhTestGetTouTemplateProDetailInfo(uint(a.ID))
  79. } else {
  80. data, err = services.GetTouTemplateProDetailInfo(uint(a.ID))
  81. }
  82. if err != nil {
  83. return kit.Upstream("FWH 平台 TOU 模版接口("+env+")", err), nil
  84. }
  85. if data == nil || *data == nil {
  86. return kit.Empty("模版不存在。"), nil
  87. }
  88. if a.Raw {
  89. return kit.Object(*data), nil
  90. }
  91. organized, ok := OrganizeTouDetail(*data)
  92. if !ok {
  93. // 结构与预期不符时退回原始详情,避免整理失败导致无结果
  94. return kit.Object(*data), nil
  95. }
  96. return kit.Object(organized), nil
  97. },
  98. }
  99. // ---- 详情整理与调度模式识别(规则见 docs/despatch-config/requirements.md 第 3 节) ----
  100. var waveTypeNames = map[int]string{0: "谷", 1: "平", 2: "峰", 3: "尖峰", 4: "低谷"}
  101. // touParamKeys 时段中的扩展参数(-1 为未设置的哨兵值)
  102. var touParamKeys = []string{
  103. "gridDischargeMax", "gridChargeMax", "chargePower", "dischargePower", "chargeMax", "dischargeMax",
  104. "gcaoMax", "gridFeedMax", "gridMax", "minDischargeSoc", "maxChargeSoc", "heatEnable", "powerOffApower",
  105. "rampTime", "offGrid", "solarCutoff",
  106. }
  107. // baseModes 基础模式:归一化后的光伏/负载优先级 → 模式
  108. var baseModes = []struct {
  109. Name, Solar, Load string
  110. }{
  111. {"A", "1,2,0", "1,3,2"},
  112. {"B", "1,2,0", "1,2,0"},
  113. {"C", "1,3,2", "1,2,0"},
  114. {"D", "1,3,2", "1,3,2"},
  115. {"E/G", "1,2,3", "1,2,0"},
  116. {"F/H", "1,2,0", "1,2,3"},
  117. }
  118. // TouPeriod 整理后的时段
  119. type TouPeriod struct {
  120. Start string `json:"start"`
  121. End string `json:"end"`
  122. WaveType string `json:"waveType,omitempty"`
  123. Name string `json:"name,omitempty"`
  124. SolarPriority string `json:"solarPriority"`
  125. LoadPriority string `json:"loadPriority"`
  126. Mode string `json:"mode"`
  127. Params map[string]any `json:"params,omitempty"`
  128. Issues []string `json:"issues,omitempty"`
  129. }
  130. // OrganizeTouDetail 把云端详情整理为 template + strategies;找不到 strategyList 时返回 false。
  131. func OrganizeTouDetail(detail any) (map[string]any, bool) {
  132. b, err := json.Marshal(detail)
  133. if err != nil {
  134. return nil, false
  135. }
  136. var root map[string]any
  137. if err := json.Unmarshal(b, &root); err != nil {
  138. return nil, false
  139. }
  140. // 兼容 { template, strategyList } 与 { ...模版字段, strategyList } 两种形态
  141. strategies, _ := root["strategyList"].([]any)
  142. template := root
  143. if t, ok := root["template"].(map[string]any); ok {
  144. template = t
  145. if strategies == nil {
  146. strategies, _ = t["strategyList"].([]any)
  147. }
  148. }
  149. if strategies == nil {
  150. return nil, false
  151. }
  152. tpl := map[string]any{}
  153. for k, v := range template {
  154. if k != "strategyList" {
  155. tpl[k] = v
  156. }
  157. }
  158. out := make([]map[string]any, 0, len(strategies))
  159. for _, s := range strategies {
  160. sm, ok := s.(map[string]any)
  161. if !ok {
  162. continue
  163. }
  164. st := map[string]any{}
  165. for _, k := range []string{"seasonName", "month", "dayName", "dayType", "ccociateType", "ccociateSn"} {
  166. if v, ok := sm[k]; ok {
  167. st[k] = v
  168. }
  169. }
  170. rates := map[string]any{}
  171. for k, v := range sm {
  172. if strings.HasPrefix(k, "eleticRate") || strings.HasPrefix(k, "eleticSell") {
  173. rates[k] = v
  174. }
  175. }
  176. if len(rates) > 0 {
  177. st["rates"] = rates
  178. }
  179. details, _ := sm["detailVoList"].([]any)
  180. periods := make([]TouPeriod, 0, len(details))
  181. for _, d := range details {
  182. if dm, ok := d.(map[string]any); ok {
  183. periods = append(periods, organizePeriod(dm))
  184. }
  185. }
  186. sort.SliceStable(periods, func(i, j int) bool { return periods[i].Start < periods[j].Start })
  187. st["periods"] = periods
  188. if issues := checkCoverage(periods); len(issues) > 0 {
  189. st["issues"] = issues
  190. }
  191. out = append(out, st)
  192. }
  193. return map[string]any{"template": tpl, "strategies": out}, true
  194. }
  195. func organizePeriod(dm map[string]any) TouPeriod {
  196. p := TouPeriod{
  197. Start: str(dm["startHourTime"]),
  198. End: str(dm["endHourTime"]),
  199. Name: str(dm["name"]),
  200. SolarPriority: str(dm["solarPriority"]),
  201. LoadPriority: str(dm["loadPriority"]),
  202. }
  203. if w, ok := num(dm["waveType"]); ok {
  204. if n, ok := waveTypeNames[int(w)]; ok {
  205. p.WaveType = n
  206. } else {
  207. p.WaveType = fmt.Sprintf("未知(%v)", w)
  208. }
  209. }
  210. params := map[string]any{}
  211. for _, k := range touParamKeys {
  212. v, ok := dm[k]
  213. if !ok || v == nil || str(v) == "" {
  214. continue
  215. }
  216. if n, isNum := num(v); isNum && n == -1 {
  217. continue
  218. }
  219. params[k] = v
  220. }
  221. if len(params) > 0 {
  222. p.Params = params
  223. }
  224. p.Mode, p.Issues = RecognizeMode(p.SolarPriority, p.LoadPriority, params)
  225. return p
  226. }
  227. // RecognizeMode 识别调度模式。params 只包含"设置了"的参数(有值且不为 -1)。
  228. func RecognizeMode(solar, load string, params map[string]any) (string, []string) {
  229. solar, load = strings.TrimSpace(solar), strings.TrimSpace(load)
  230. var issues []string
  231. // 1. 特殊模式按原始值精确匹配
  232. if solar == "9,9,9" && load == "9,9,9" {
  233. for k, want := range map[string]float64{"gridDischargeMax": 0.1, "gridFeedMax": 0.1} {
  234. if got, ok := num(params[k]); !ok || got != want {
  235. issues = append(issues, fmt.Sprintf("Zero Import 的 %s 应为 %v,实际为 %v", k, want, params[k]))
  236. }
  237. }
  238. sort.Strings(issues)
  239. return "Zero Import", issues
  240. }
  241. // 2. 两个优先级都为空
  242. if solar == "" && load == "" {
  243. _, hasCharge := params["chargePower"]
  244. _, hasDischarge := params["dischargePower"]
  245. switch {
  246. case hasCharge && !hasDischarge:
  247. return "强制充电", nil
  248. case hasDischarge && !hasCharge:
  249. return "强制放电", nil
  250. case !hasCharge && !hasDischarge:
  251. return "未设置", nil
  252. default:
  253. return "自定义", []string{"优先级为空但同时设置了 chargePower 和 dischargePower"}
  254. }
  255. }
  256. if solar == "" || load == "" {
  257. return "自定义", []string{"光伏与负载优先级只有一个为空"}
  258. }
  259. // 3. 归一化后匹配 A~H
  260. ns, ok1 := normalizePriority(solar)
  261. nl, ok2 := normalizePriority(load)
  262. if ok1 && ok2 {
  263. for _, m := range baseModes {
  264. if ns == m.Solar && nl == m.Load {
  265. return m.Name, nil
  266. }
  267. }
  268. }
  269. return "自定义", nil
  270. }
  271. // normalizePriority 稠密排名归一化:0 保持 0,非 0 值按从小到大映射为 1、2、3,相同值映射为同一个数。
  272. func normalizePriority(s string) (string, bool) {
  273. parts := strings.Split(s, ",")
  274. if len(parts) != 3 {
  275. return "", false
  276. }
  277. vals := make([]int, 3)
  278. uniq := map[int]bool{}
  279. for i, p := range parts {
  280. v, err := strconv.Atoi(strings.TrimSpace(p))
  281. if err != nil || v < 0 {
  282. return "", false
  283. }
  284. vals[i] = v
  285. if v != 0 {
  286. uniq[v] = true
  287. }
  288. }
  289. sorted := make([]int, 0, len(uniq))
  290. for v := range uniq {
  291. sorted = append(sorted, v)
  292. }
  293. sort.Ints(sorted)
  294. rank := map[int]int{}
  295. for i, v := range sorted {
  296. rank[v] = i + 1
  297. }
  298. out := make([]string, 3)
  299. for i, v := range vals {
  300. if v == 0 {
  301. out[i] = "0"
  302. } else {
  303. out[i] = strconv.Itoa(rank[v])
  304. }
  305. }
  306. return strings.Join(out, ","), true
  307. }
  308. // checkCoverage 检查时段是否首尾衔接覆盖 00:00~24:00。
  309. func checkCoverage(ps []TouPeriod) []string {
  310. if len(ps) == 0 {
  311. return []string{"没有时段"}
  312. }
  313. var issues []string
  314. if ps[0].Start != "00:00" {
  315. issues = append(issues, "第一个时段不是从 00:00 开始")
  316. }
  317. for i := 1; i < len(ps); i++ {
  318. if ps[i].Start != ps[i-1].End {
  319. issues = append(issues, fmt.Sprintf("%s 与 %s 之间不衔接(%s → %s)", ps[i-1].End, ps[i].Start, ps[i-1].End, ps[i].Start))
  320. }
  321. }
  322. if last := ps[len(ps)-1].End; last != "24:00" && last != "00:00" && last != "23:59" {
  323. issues = append(issues, "最后一个时段没有到 24:00")
  324. }
  325. return issues
  326. }
  327. func str(v any) string {
  328. switch t := v.(type) {
  329. case nil:
  330. return ""
  331. case string:
  332. return t
  333. case float64:
  334. return strconv.FormatFloat(t, 'f', -1, 64)
  335. default:
  336. return fmt.Sprint(t)
  337. }
  338. }
  339. func num(v any) (float64, bool) {
  340. switch t := v.(type) {
  341. case float64:
  342. return t, true
  343. case int:
  344. return float64(t), true
  345. case string:
  346. f, err := strconv.ParseFloat(strings.TrimSpace(t), 64)
  347. return f, err == nil
  348. }
  349. return 0, false
  350. }