Browse Source

feat:ems_param_lookup的ids定为0~65535整数数组,工具框架支持补充参数schema约束

- kit.Def 新增 Properties:在生成的字段 schema 上补充枚举、取值范围、数组长度,服务端按补充后的 schema 校验
- ems_param_lookup:ids 为非空整数数组(0~65535,1~50 个,与快照 XID 的 uint16 编号一致),kind 限定 pid / vid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 5 days ago
parent
commit
5bbdfac548

+ 1 - 1
deploy/agent-assistant/skills/fwh-assistant/SKILL.md

@@ -25,7 +25,7 @@ description: FranklinWH(FWH)储能业务数据助手。只要问题涉及 Fr
 | 故障录波 | `fault_record_list`、`fault_record_detail`、`fault_record_callable`、`fault_record_call_history` |
 | 调度 | `tou_template_list`、`tou_template_get`(`env=prod/test`,详情含调度模式识别) |
 | SIM | `sim_get`(自动判断平台)、`sim_search`、`sim_usage_monthly` |
-| 其他 | `ems_param_lookup`(PID/VID 宏名与描述)、`plane_issue_search`、`plane_issue_get`、`sn_parse` |
+| 其他 | `ems_param_lookup`(PID/VID 宏名与描述,编号为十进制整数)、`plane_issue_search`、`plane_issue_get`、`sn_parse` |
 
 参数以工具自身的描述为准,不要臆造参数或工具名。写操作工具(`mcp__gateway__fwhops__*`)只在部分配置中提供,见 references/write-operations.md;没有这些工具时如实告诉用户无法修改数据。
 

+ 2 - 2
docs/ai-assistant-agentserver/agent-mcp.md

@@ -130,7 +130,7 @@ var AlarmEvents = kit.Def[alarmEventsArgs]{
 
 处理函数只做三件事:校验与换算参数、调用 `repositories`/`services`、把上游数据映射成精简的行结构。字段映射集中写,丢弃对分析无用的字段,统一时间格式与单位。
 
-schema 由 mcp-go 依赖的 `google/jsonschema-go` 生成:`jsonschema` 标签的内容就是字段描述(不支持 `key=value` 选项),没有 `omitempty` 的字段为必填。枚举、取值范围写进描述,并在处理函数中校验(`kit.Enum`、`kit.Limit` 等辅助函数);需要在 schema 中声明枚举时,在 `Def.Register` 里对生成的 schema 做补充。AgentServer 网关只接受 `type: object` 的 inputSchema,测试中要检查。
+schema 由 mcp-go 依赖的 `google/jsonschema-go` 生成:`jsonschema` 标签的内容就是字段描述(不支持 `key=value` 选项),没有 `omitempty` 的字段为必填。枚举、取值范围写进描述,并在处理函数中校验(`kit.Enum`、`kit.Limit` 等辅助函数);需要在 schema 中声明枚举、取值范围或数组长度时,用 `Def.Properties` 对生成的字段 schema 做补充(合并进对应字段,服务端按补充后的 schema 校验);Go 的切片字段默认生成可空类型(`null | array`),必填数组要用它改为 `array`。AgentServer 网关只接受 `type: object` 的 inputSchema,测试中要检查。
 
 ### 3.3 结果构造
 
@@ -197,7 +197,7 @@ schema 由 mcp-go 依赖的 `google/jsonschema-go` 生成:`jsonschema` 标签
 | SIM | `sim_get` | 按 ICCID 查询,自动判断平台(SmartHub / Acceleronix) | SIM 卡管理服务 |
 | | `sim_search` | 按状态、平台、关键字搜索 | 同上 |
 | | `sim_usage_monthly` | 单卡某月每日用量 | 同上 |
-| EMS 参数 | `ems_param_lookup` | 按 PID / VID 查询宏名与描述 | emspara |
+| EMS 参数 | `ems_param_lookup` | 按 PID / VID 查询宏名与描述;`ids` 为十进制整数数组(0~65535,1~50 个,与快照 XID 的 `uint16` 编号一致),schema 中声明约束由服务端校验 | emspara |
 | Plane | `plane_issue_search` | 按关键字、状态、项目搜索工作项(只读) | Plane V1 |
 | | `plane_issue_get` | 工作项详情(含最近评论) | Plane V1 |
 | 工具 | `sn_parse` | 解析设备 SN 的含义(型号、产地、批次等) | 旧 `util_parse_sn` 的规则(拷贝) |

+ 33 - 1
service/agentmcp/kit/tool.go

@@ -6,6 +6,8 @@ package kit
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"strings"
 
 	"github.com/mark3labs/mcp-go/mcp"
@@ -40,6 +42,9 @@ type Def[T any] struct {
 	Description string // 用途、适用场景、返回字段与单位
 	ReadOnly    bool
 	Handler     func(ctx context.Context, args T) (*mcp.CallToolResult, error)
+	// Properties 可选:补充生成的 schema 中各字段的约束(枚举、取值范围、数组长度等),
+	// key 为字段名,value 合并进该字段的 schema(同名键覆盖)。服务端按补充后的 schema 校验参数。
+	Properties map[string]map[string]any
 }
 
 func (d Def[T]) Meta() Meta {
@@ -47,7 +52,7 @@ func (d Def[T]) Meta() Meta {
 }
 
 func (d Def[T]) Tool() mcp.Tool {
-	return mcp.NewTool(d.Name,
+	t := mcp.NewTool(d.Name,
 		mcp.WithDescription(d.Description),
 		mcp.WithInputSchema[T](),
 		mcp.WithTitleAnnotation(d.Title),
@@ -56,6 +61,33 @@ func (d Def[T]) Tool() mcp.Tool {
 		mcp.WithIdempotentHintAnnotation(d.ReadOnly),
 		mcp.WithOpenWorldHintAnnotation(false),
 	)
+	if len(d.Properties) > 0 {
+		t.RawInputSchema = patchProperties(t.RawInputSchema, d.Name, d.Properties)
+	}
+	return t
+}
+
+// patchProperties 把 patch 合并进 schema.properties 中的对应字段;字段不存在视为定义错误,直接 panic(启动即暴露)。
+func patchProperties(raw json.RawMessage, tool string, patch map[string]map[string]any) json.RawMessage {
+	var schema map[string]any
+	if err := json.Unmarshal(raw, &schema); err != nil {
+		panic(fmt.Sprintf("%s: 解析 inputSchema 失败: %v", tool, err))
+	}
+	props, _ := schema["properties"].(map[string]any)
+	for name, extra := range patch {
+		field, ok := props[name].(map[string]any)
+		if !ok {
+			panic(fmt.Sprintf("%s: inputSchema 中没有字段 %s", tool, name))
+		}
+		for k, v := range extra {
+			field[k] = v
+		}
+	}
+	out, err := json.Marshal(schema)
+	if err != nil {
+		panic(fmt.Sprintf("%s: 序列化 inputSchema 失败: %v", tool, err))
+	}
+	return out
 }
 
 func (d Def[T]) Register(s *server.MCPServer) {

+ 57 - 0
service/agentmcp/kit/tool_test.go

@@ -0,0 +1,57 @@
+package kit
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+type patchArgs struct {
+	Kind string `json:"kind" jsonschema:"类型"`
+	IDs  []int  `json:"ids" jsonschema:"编号"`
+}
+
+func TestDefProperties(t *testing.T) {
+	d := Def[patchArgs]{
+		Name: "patch_test", Title: "补充 schema", Description: "测试", ReadOnly: true,
+		Handler: func(context.Context, patchArgs) (*mcp.CallToolResult, error) { return nil, nil },
+		Properties: map[string]map[string]any{
+			"kind": {"enum": []string{"pid", "vid"}},
+			"ids":  {"type": "array", "minItems": 1, "items": map[string]any{"type": "integer", "minimum": 0}},
+		},
+	}
+	var schema struct {
+		Properties map[string]map[string]any `json:"properties"`
+		Required   []string                  `json:"required"`
+	}
+	if err := json.Unmarshal(d.Tool().RawInputSchema, &schema); err != nil {
+		t.Fatal(err)
+	}
+	ids := schema.Properties["ids"]
+	if ids["type"] != "array" {
+		t.Errorf("ids.type 应为 array(不再可空),实际 %v", ids["type"])
+	}
+	if ids["description"] != "编号" {
+		t.Errorf("补充约束不应覆盖生成的描述,实际 %v", ids["description"])
+	}
+	if items, _ := ids["items"].(map[string]any); items["minimum"] != float64(0) {
+		t.Errorf("ids.items.minimum 未生效:%v", ids["items"])
+	}
+	if enum, _ := schema.Properties["kind"]["enum"].([]any); len(enum) != 2 {
+		t.Errorf("kind.enum 未生效:%v", schema.Properties["kind"])
+	}
+	if len(schema.Required) != 2 {
+		t.Errorf("required 应保持不变,实际 %v", schema.Required)
+	}
+}
+
+func TestDefPropertiesUnknownField(t *testing.T) {
+	defer func() {
+		if recover() == nil {
+			t.Error("补充不存在的字段应 panic")
+		}
+	}()
+	Def[patchArgs]{Name: "patch_bad", Properties: map[string]map[string]any{"nope": {"type": "string"}}}.Tool()
+}

+ 14 - 3
service/agentmcp/tools/misc.go

@@ -16,15 +16,26 @@ import (
 
 type emsParamLookupArgs struct {
 	Kind string `json:"kind" jsonschema:"参数类型:pid 或 vid"`
-	IDs  []int  `json:"ids" jsonschema:"PID / VID 编号列表,最多 50 个"`
+	IDs  []int  `json:"ids" jsonschema:"PID / VID 编号列表(十进制整数,0~65535),1~50 个"`
 }
 
+// PID / VID 编号:设备快照 XID 的 id 字段为 uint16(function/SnapFile),
+// 字典来自 EMS 头文件中的总序号(十进制,utils/emspara/updateCmd),Redis 以十进制字符串为 key。
 var EmsParamLookup = kit.Def[emsParamLookupArgs]{
 	Name:  "ems_param_lookup",
 	Title: "查询 EMS 参数含义",
-	Description: "按 PID 或 VID 编号查询 EMS 参数的宏名与描述(来自 EMS 参数字典),用于解读日志、报文中的参数编号。" +
-		"返回 items:id、macroName、description;未找到的编号放在 notFound。",
+	Description: "按 PID 或 VID 编号查询 EMS 参数的宏名与描述(来自 EMS 参数字典),用于解读日志、报文、告警快照中的参数编号。" +
+		"编号为十进制整数(快照中 xid 的 id),十六进制需先换算。返回 items:id、macroName、description;未找到的编号放在 notFound。",
 	ReadOnly: true,
+	Properties: map[string]map[string]any{
+		"kind": {"enum": []string{"pid", "vid"}},
+		"ids": {
+			"type":     "array",
+			"minItems": 1,
+			"maxItems": 50,
+			"items":    map[string]any{"type": "integer", "minimum": 0, "maximum": 65535},
+		},
+	},
 	Handler: func(ctx context.Context, a emsParamLookupArgs) (*mcp.CallToolResult, error) {
 		kind, err := kit.Enum("kind", a.Kind, "", "pid", "vid")
 		if err != nil {