| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- package tools
- import (
- "context"
- "encoding/json"
- "fmt"
- "sort"
- "strconv"
- "strings"
- "github.com/mark3labs/mcp-go/mcp"
- "fwh-toolbox-server/agentmcp/kit"
- "fwh-toolbox-server/dto"
- "fwh-toolbox-server/services"
- )
- func parseEnv(v string) (string, error) {
- return kit.Enum("env", v, "prod", "prod", "test")
- }
- // ---- tou_template_list ----
- type touTemplateListArgs struct {
- Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"`
- Search string `json:"search,omitempty" jsonschema:"搜索关键字(模版名称、电力公司等)"`
- Type *int `json:"type,omitempty" jsonschema:"模版类型:0 系统模版、1 API 模版、2 用户推荐;不填为全部"`
- Status *int `json:"status,omitempty" jsonschema:"发布状态:0 未发布、2 已发布;不填为全部"`
- Page int `json:"page,omitempty" jsonschema:"页码,默认 1"`
- Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"`
- }
- var TouTemplateList = kit.Def[touTemplateListArgs]{
- Name: "tou_template_list",
- Title: "查询 TOU 调度模版列表",
- Description: "分页查询 FWH 云平台的 TOU 调度模版(生产服或测试服)。返回 total 与 items(模版 ID、名称、类型、国家/地区、电力公司、电价类型、发布状态、更新时间等,以实际返回为准)。" +
- "查看某个模版的时段与调度模式用 tou_template_get。",
- ReadOnly: true,
- Handler: func(ctx context.Context, a touTemplateListArgs) (*mcp.CallToolResult, error) {
- env, err := parseEnv(a.Env)
- if err != nil {
- return kit.Fail(err), nil
- }
- page := kit.Page(a.Page)
- size := kit.Limit(a.Size, 20, 100)
- var data *dto.ResPage[interface{}]
- if env == "test" {
- data, err = services.FwhTestGetTouTemplateProList(uint(page), uint(size), a.Type, a.Status, strings.TrimSpace(a.Search))
- } else {
- data, err = services.GetTouTemplateProList(uint(page), uint(size), a.Type, a.Status, strings.TrimSpace(a.Search))
- }
- if err != nil {
- return kit.Upstream("FWH 平台 TOU 模版接口("+env+")", 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
- },
- }
- // ---- tou_template_get ----
- type touTemplateGetArgs struct {
- Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"`
- ID int `json:"id" jsonschema:"模版 ID"`
- Raw bool `json:"raw,omitempty" jsonschema:"为 true 时返回云端原始详情(字段完整但较长),默认 false 返回整理后的结构"`
- }
- var TouTemplateGet = kit.Def[touTemplateGetArgs]{
- Name: "tou_template_get",
- Title: "查询 TOU 调度模版详情",
- Description: "查询单个 TOU 调度模版的详情,默认整理为:template(模版主体字段)与 strategies(按季节 × 日类型的策略)。" +
- "每个策略含 seasonName、month、dayName、ccociateSn、电价,以及 periods(时段):start、end、waveType(谷/平/峰/尖峰/低谷)、" +
- "solarPriority / loadPriority(原始值)、mode(识别出的调度模式:A~H、E/G、F/H、强制充电、强制放电、Zero Import、未设置、自定义)、params(设置了的扩展参数,-1 视为未设置)、issues(异常说明)。" +
- "优先级顺序:光伏 [市电, 负载, aPower],负载 [市电, 光伏, aPower]。raw=true 返回原始详情。",
- ReadOnly: true,
- Handler: func(ctx context.Context, a touTemplateGetArgs) (*mcp.CallToolResult, error) {
- env, err := parseEnv(a.Env)
- if err != nil {
- return kit.Fail(err), nil
- }
- if a.ID <= 0 {
- return kit.Failf("参数 id 必填且为正整数"), nil
- }
- var data *interface{}
- if env == "test" {
- data, err = services.FwhTestGetTouTemplateProDetailInfo(uint(a.ID))
- } else {
- data, err = services.GetTouTemplateProDetailInfo(uint(a.ID))
- }
- if err != nil {
- return kit.Upstream("FWH 平台 TOU 模版接口("+env+")", err), nil
- }
- if data == nil || *data == nil {
- return kit.Empty("模版不存在。"), nil
- }
- if a.Raw {
- return kit.Object(*data), nil
- }
- organized, ok := OrganizeTouDetail(*data)
- if !ok {
- // 结构与预期不符时退回原始详情,避免整理失败导致无结果
- return kit.Object(*data), nil
- }
- return kit.Object(organized), nil
- },
- }
- // ---- 详情整理与调度模式识别(规则见 docs/despatch-config/requirements.md 第 3 节) ----
- var waveTypeNames = map[int]string{0: "谷", 1: "平", 2: "峰", 3: "尖峰", 4: "低谷"}
- // touParamKeys 时段中的扩展参数(-1 为未设置的哨兵值)
- var touParamKeys = []string{
- "gridDischargeMax", "gridChargeMax", "chargePower", "dischargePower", "chargeMax", "dischargeMax",
- "gcaoMax", "gridFeedMax", "gridMax", "minDischargeSoc", "maxChargeSoc", "heatEnable", "powerOffApower",
- "rampTime", "offGrid", "solarCutoff",
- }
- // baseModes 基础模式:归一化后的光伏/负载优先级 → 模式
- var baseModes = []struct {
- Name, Solar, Load string
- }{
- {"A", "1,2,0", "1,3,2"},
- {"B", "1,2,0", "1,2,0"},
- {"C", "1,3,2", "1,2,0"},
- {"D", "1,3,2", "1,3,2"},
- {"E/G", "1,2,3", "1,2,0"},
- {"F/H", "1,2,0", "1,2,3"},
- }
- // TouPeriod 整理后的时段
- type TouPeriod struct {
- Start string `json:"start"`
- End string `json:"end"`
- WaveType string `json:"waveType,omitempty"`
- Name string `json:"name,omitempty"`
- SolarPriority string `json:"solarPriority"`
- LoadPriority string `json:"loadPriority"`
- Mode string `json:"mode"`
- Params map[string]any `json:"params,omitempty"`
- Issues []string `json:"issues,omitempty"`
- }
- // OrganizeTouDetail 把云端详情整理为 template + strategies;找不到 strategyList 时返回 false。
- func OrganizeTouDetail(detail any) (map[string]any, bool) {
- b, err := json.Marshal(detail)
- if err != nil {
- return nil, false
- }
- var root map[string]any
- if err := json.Unmarshal(b, &root); err != nil {
- return nil, false
- }
- // 兼容 { template, strategyList } 与 { ...模版字段, strategyList } 两种形态
- strategies, _ := root["strategyList"].([]any)
- template := root
- if t, ok := root["template"].(map[string]any); ok {
- template = t
- if strategies == nil {
- strategies, _ = t["strategyList"].([]any)
- }
- }
- if strategies == nil {
- return nil, false
- }
- tpl := map[string]any{}
- for k, v := range template {
- if k != "strategyList" {
- tpl[k] = v
- }
- }
- out := make([]map[string]any, 0, len(strategies))
- for _, s := range strategies {
- sm, ok := s.(map[string]any)
- if !ok {
- continue
- }
- st := map[string]any{}
- for _, k := range []string{"seasonName", "month", "dayName", "dayType", "ccociateType", "ccociateSn"} {
- if v, ok := sm[k]; ok {
- st[k] = v
- }
- }
- rates := map[string]any{}
- for k, v := range sm {
- if strings.HasPrefix(k, "eleticRate") || strings.HasPrefix(k, "eleticSell") {
- rates[k] = v
- }
- }
- if len(rates) > 0 {
- st["rates"] = rates
- }
- details, _ := sm["detailVoList"].([]any)
- periods := make([]TouPeriod, 0, len(details))
- for _, d := range details {
- if dm, ok := d.(map[string]any); ok {
- periods = append(periods, organizePeriod(dm))
- }
- }
- sort.SliceStable(periods, func(i, j int) bool { return periods[i].Start < periods[j].Start })
- st["periods"] = periods
- if issues := checkCoverage(periods); len(issues) > 0 {
- st["issues"] = issues
- }
- out = append(out, st)
- }
- return map[string]any{"template": tpl, "strategies": out}, true
- }
- func organizePeriod(dm map[string]any) TouPeriod {
- p := TouPeriod{
- Start: str(dm["startHourTime"]),
- End: str(dm["endHourTime"]),
- Name: str(dm["name"]),
- SolarPriority: str(dm["solarPriority"]),
- LoadPriority: str(dm["loadPriority"]),
- }
- if w, ok := num(dm["waveType"]); ok {
- if n, ok := waveTypeNames[int(w)]; ok {
- p.WaveType = n
- } else {
- p.WaveType = fmt.Sprintf("未知(%v)", w)
- }
- }
- params := map[string]any{}
- for _, k := range touParamKeys {
- v, ok := dm[k]
- if !ok || v == nil || str(v) == "" {
- continue
- }
- if n, isNum := num(v); isNum && n == -1 {
- continue
- }
- params[k] = v
- }
- if len(params) > 0 {
- p.Params = params
- }
- p.Mode, p.Issues = RecognizeMode(p.SolarPriority, p.LoadPriority, params)
- return p
- }
- // RecognizeMode 识别调度模式。params 只包含"设置了"的参数(有值且不为 -1)。
- func RecognizeMode(solar, load string, params map[string]any) (string, []string) {
- solar, load = strings.TrimSpace(solar), strings.TrimSpace(load)
- var issues []string
- // 1. 特殊模式按原始值精确匹配
- if solar == "9,9,9" && load == "9,9,9" {
- for k, want := range map[string]float64{"gridDischargeMax": 0.1, "gridFeedMax": 0.1} {
- if got, ok := num(params[k]); !ok || got != want {
- issues = append(issues, fmt.Sprintf("Zero Import 的 %s 应为 %v,实际为 %v", k, want, params[k]))
- }
- }
- sort.Strings(issues)
- return "Zero Import", issues
- }
- // 2. 两个优先级都为空
- if solar == "" && load == "" {
- _, hasCharge := params["chargePower"]
- _, hasDischarge := params["dischargePower"]
- switch {
- case hasCharge && !hasDischarge:
- return "强制充电", nil
- case hasDischarge && !hasCharge:
- return "强制放电", nil
- case !hasCharge && !hasDischarge:
- return "未设置", nil
- default:
- return "自定义", []string{"优先级为空但同时设置了 chargePower 和 dischargePower"}
- }
- }
- if solar == "" || load == "" {
- return "自定义", []string{"光伏与负载优先级只有一个为空"}
- }
- // 3. 归一化后匹配 A~H
- ns, ok1 := normalizePriority(solar)
- nl, ok2 := normalizePriority(load)
- if ok1 && ok2 {
- for _, m := range baseModes {
- if ns == m.Solar && nl == m.Load {
- return m.Name, nil
- }
- }
- }
- return "自定义", nil
- }
- // normalizePriority 稠密排名归一化:0 保持 0,非 0 值按从小到大映射为 1、2、3,相同值映射为同一个数。
- func normalizePriority(s string) (string, bool) {
- parts := strings.Split(s, ",")
- if len(parts) != 3 {
- return "", false
- }
- vals := make([]int, 3)
- uniq := map[int]bool{}
- for i, p := range parts {
- v, err := strconv.Atoi(strings.TrimSpace(p))
- if err != nil || v < 0 {
- return "", false
- }
- vals[i] = v
- if v != 0 {
- uniq[v] = true
- }
- }
- sorted := make([]int, 0, len(uniq))
- for v := range uniq {
- sorted = append(sorted, v)
- }
- sort.Ints(sorted)
- rank := map[int]int{}
- for i, v := range sorted {
- rank[v] = i + 1
- }
- out := make([]string, 3)
- for i, v := range vals {
- if v == 0 {
- out[i] = "0"
- } else {
- out[i] = strconv.Itoa(rank[v])
- }
- }
- return strings.Join(out, ","), true
- }
- // checkCoverage 检查时段是否首尾衔接覆盖 00:00~24:00。
- func checkCoverage(ps []TouPeriod) []string {
- if len(ps) == 0 {
- return []string{"没有时段"}
- }
- var issues []string
- if ps[0].Start != "00:00" {
- issues = append(issues, "第一个时段不是从 00:00 开始")
- }
- for i := 1; i < len(ps); i++ {
- if ps[i].Start != ps[i-1].End {
- issues = append(issues, fmt.Sprintf("%s 与 %s 之间不衔接(%s → %s)", ps[i-1].End, ps[i].Start, ps[i-1].End, ps[i].Start))
- }
- }
- if last := ps[len(ps)-1].End; last != "24:00" && last != "00:00" && last != "23:59" {
- issues = append(issues, "最后一个时段没有到 24:00")
- }
- return issues
- }
- func str(v any) string {
- switch t := v.(type) {
- case nil:
- return ""
- case string:
- return t
- case float64:
- return strconv.FormatFloat(t, 'f', -1, 64)
- default:
- return fmt.Sprint(t)
- }
- }
- func num(v any) (float64, bool) {
- switch t := v.(type) {
- case float64:
- return t, true
- case int:
- return float64(t), true
- case string:
- f, err := strconv.ParseFloat(strings.TrimSpace(t), 64)
- return f, err == nil
- }
- return 0, false
- }
|