package tools import ( "context" "encoding/json" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" "fwh-toolbox-server/agentmcp/kit" "fwh-toolbox-server/repositories" ) // ---- ems_param_lookup ---- type emsParamLookupArgs struct { Kind string `json:"kind" jsonschema:"参数类型:pid 或 vid"` IDs []int `json:"ids" jsonschema:"PID / VID 编号列表,最多 50 个"` } var EmsParamLookup = kit.Def[emsParamLookupArgs]{ Name: "ems_param_lookup", Title: "查询 EMS 参数含义", Description: "按 PID 或 VID 编号查询 EMS 参数的宏名与描述(来自 EMS 参数字典),用于解读日志、报文中的参数编号。" + "返回 items:id、macroName、description;未找到的编号放在 notFound。", ReadOnly: true, Handler: func(ctx context.Context, a emsParamLookupArgs) (*mcp.CallToolResult, error) { kind, err := kit.Enum("kind", a.Kind, "", "pid", "vid") if err != nil { return kit.Fail(err), nil } if kind == "" { return kit.Failf("参数 kind 必填:pid 或 vid"), nil } if len(a.IDs) == 0 || len(a.IDs) > 50 { return kit.Failf("参数 ids 需要 1~50 个编号"), nil } lookup := repositories.GetEmsparaPidInfo if kind == "vid" { lookup = repositories.GetEmsparaVidInfo } items := make([]map[string]any, 0, len(a.IDs)) var notFound []int for _, id := range a.IDs { if id < 0 { notFound = append(notFound, id) continue } name, desc, err := lookup(uint(id)) if err != nil { notFound = append(notFound, id) continue } items = append(items, map[string]any{"id": id, "macroName": name, "description": desc}) } return kit.Object(map[string]any{"kind": kind, "items": items, "notFound": notFound}), nil }, } // ---- plane_issue_search ---- type planeIssueSearchArgs struct { Query string `json:"query" jsonschema:"搜索关键字"` Limit int `json:"limit,omitempty" jsonschema:"最多返回条数,1~50,默认 20"` } var PlaneIssueSearch = kit.Def[planeIssueSearchArgs]{ Name: "plane_issue_search", Title: "搜索 Plane 工作项", Description: "在 Plane 项目管理系统中按关键字搜索工作项(问题、需求),只读。返回工作项 ID、项目、序号、名称、状态等(以 Plane 返回为准)。" + "查看详情与评论用 plane_issue_get。", ReadOnly: true, Handler: func(ctx context.Context, a planeIssueSearchArgs) (*mcp.CallToolResult, error) { q, err := kit.Required("query", a.Query) if err != nil { return kit.Fail(err), nil } raw, err := repositories.PlaneSearchWorkItems(q, kit.Limit(a.Limit, 20, 50), "") if err != nil { return kit.Upstream("Plane", err), nil } return rawJSON(raw), nil }, } // ---- plane_issue_get ---- type planeIssueGetArgs struct { ID string `json:"id,omitempty" jsonschema:"工作项 UUID;与 identifier 二选一"` Identifier string `json:"identifier,omitempty" jsonschema:"工作项标识,如 PROJ-42;与 id 二选一"` Comments *bool `json:"comments,omitempty" jsonschema:"是否附带评论,默认 true"` } var PlaneIssueGet = kit.Def[planeIssueGetArgs]{ Name: "plane_issue_get", Title: "查询 Plane 工作项详情", Description: "查询 Plane 工作项详情(按 UUID 或 PROJ-42 形式的标识),默认附带评论。返回 item(工作项字段)与 comments(评论列表),只读。", ReadOnly: true, Handler: func(ctx context.Context, a planeIssueGetArgs) (*mcp.CallToolResult, error) { var raw []byte var err error switch { case strings.TrimSpace(a.ID) != "": raw, err = repositories.PlaneGetWorkItem(strings.TrimSpace(a.ID)) case strings.TrimSpace(a.Identifier) != "": proj, seq, perr := parseIdentifier(a.Identifier) if perr != nil { return kit.Fail(perr), nil } raw, err = repositories.PlaneGetWorkItemByIdentifier(proj, seq) default: return kit.Failf("参数 id 与 identifier 必须填写一个"), nil } if err != nil { return kit.Upstream("Plane", err), nil } var item map[string]any if err := json.Unmarshal(raw, &item); err != nil { return kit.Failf("Plane 返回内容无法解析:%v", err), nil } out := map[string]any{"item": item} if a.Comments == nil || *a.Comments { if id, _ := item["id"].(string); id != "" { if cr, err := repositories.PlaneListWorkItemComments(id); err == nil { var comments any if json.Unmarshal(cr, &comments) == nil { out["comments"] = comments } } else { out["commentsError"] = err.Error() } } } return kit.Object(out), nil }, } // parseIdentifier 解析 PROJ-42 形式的标识。 func parseIdentifier(s string) (string, int, error) { s = strings.TrimSpace(s) i := strings.LastIndex(s, "-") if i <= 0 || i == len(s)-1 { return "", 0, fmt.Errorf("identifier 格式应为 PROJ-42:%s", s) } var seq int if _, err := fmt.Sscanf(s[i+1:], "%d", &seq); err != nil || seq <= 0 { return "", 0, fmt.Errorf("identifier 序号必须是正整数:%s", s) } return strings.ToUpper(s[:i]), seq, nil } // rawJSON 把上游返回的 JSON 字节作为对象输出(脱敏、去空值、限长)。 func rawJSON(raw []byte) *mcp.CallToolResult { var v any if err := json.Unmarshal(raw, &v); err != nil { return kit.Failf("返回内容无法解析:%v", err) } return kit.Object(v) } // ---- sn_parse ---- type snParseArgs struct { SN string `json:"sn" jsonschema:"设备 SN,标准长度 20 位,如 10060006A03X25310091"` } var SnParse = kit.Def[snParseArgs]{ Name: "sn_parse", Title: "解析设备 SN", Description: "解析 FranklinWH 设备 SN 的含义。格式:{产品代码 4 位}{硬件版本 4 位}{工厂 1 位}{版本标识 2 位}{预留 1 位}{年 2 位}{周 2 位}{序号}。" + "产品代码:1006 aGate、1005 aPower、1002 aHub/PowerLink、1003 MPPT。返回 productCode、productName、hwVersion、factory、versionFlag、year、week、serialNumber、summary。", ReadOnly: true, Handler: func(ctx context.Context, a snParseArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } r, err := ParseSN(sn) if err != nil { return kit.Fail(err), nil } return kit.Object(r), nil }, } // ParseSN 按 SN 规则拆分字段(规则来自旧 MCP 的 util_parse_sn)。 func ParseSN(sn string) (map[string]any, error) { sn = strings.TrimSpace(sn) if len(sn) < 20 { return nil, fmt.Errorf("SN 长度不足,标准长度为 20 位:%s", sn) } productCode := sn[0:4] productName := map[string]string{ "1006": "aGate (网关)", "1005": "aPower (储能电池)", "1002": "aHub / PowerLink", "1003": "MPPT", }[productCode] if productName == "" { productName = "未知产品" } hw, factory, verFlag, year, week, serial := sn[4:8], string(sn[8]), sn[9:11], sn[12:14], sn[14:16], sn[16:] return map[string]any{ "sn": sn, "productCode": productCode, "productName": productName, "hwVersion": hw, "factory": factory, "versionFlag": verFlag, "year": "20" + year, "week": week, "serialNumber": serial, "summary": fmt.Sprintf("%s | HW:%s | 工厂:%s | 20%s年第%s周 | 序号:%s", productName, hw, factory, year, week, serial), }, nil }