package tools // 故障录波(ToolBox 直连设备,经 MQTT 与设备交互;与云平台的 cloud_fault_record_* 是两套数据)。 // 操作由主服务的执行器收发 MQTT,状态保存在 Mongo(FaultRecordOperations);本服务只提交操作、读取状态与文件。 // 设备上的操作以长任务(kit.TaskDef)执行:任务 id 携带操作 id,状态直接取自操作记录。 import ( "context" "errors" "fmt" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "fwh-toolbox-server/agentmcp/kit" "fwh-toolbox-server/domain" "fwh-toolbox-server/dto" faultRecordFile "fwh-toolbox-server/function/FaultRecordFile" proto "fwh-toolbox-server/function/FaultRecordProtocol" mongoRepo "fwh-toolbox-server/repositories/mongo" "fwh-toolbox-server/services" ) const ( frEnvDesc = "服务器环境:prod(生产,默认)/ test(测试)" frDeviceKindDesc = "设备:aGate(默认)、aPowerPE、aPowerMPPT、aHub;站点有哪些设备、是否支持见 fault_record_devices" frDeviceSnDesc = "设备 SN;aGate 可不填(即站点 SN),其他设备必填(见 fault_record_devices)" // frSyncWait 普通调用(非任务方式)时等待设备回复的最长时间,低于单次调用 60 秒超时 frSyncWait = 45 * time.Second ) var frDeviceProps = map[string]map[string]any{ "env": {"enum": []string{"prod", "test"}}, "deviceKind": {"enum": []string{proto.DeviceKindAGate, proto.DeviceKindAPowerPE, proto.DeviceKindAPowerMPPT, proto.DeviceKindAHub}}, } // frStageMessages 进行中各阶段的说明(显示在平台的后台调用栏) var frStageMessages = map[domain.FaultRecordOpStatus]string{ domain.FaultRecordStatusPending: "等待发送到设备", domain.FaultRecordStatusSent: "已发送,等待设备确认", domain.FaultRecordStatusAccepted: "设备已接受,等待结果", domain.FaultRecordStatusReady: "设备已准备录波文件,等待上传", domain.FaultRecordStatusUploading: "设备正在上传录波文件", domain.FaultRecordStatusUploaded: "上传完成,正在下载", domain.FaultRecordStatusDownloaded: "文件已保存,正在解析", } // frFailMessages 失败终态的说明(写给模型) var frFailMessages = map[domain.FaultRecordOpStatus]string{ domain.FaultRecordStatusBusy: "设备正在处理其他命令,稍后重试", domain.FaultRecordStatusRejected: "设备拒绝了请求", domain.FaultRecordStatusTimeout: "等待设备超时(设备可能离线或无响应)", domain.FaultRecordStatusUploadFailed: "设备上传录波文件失败", domain.FaultRecordStatusDownloadFailed: "录波文件下载失败,可以用 fault_record_retry_download 重试下载", domain.FaultRecordStatusParseFailed: "录波文件已保存但解析失败,可以用 fault_record_export 导出原始文件", domain.FaultRecordStatusInterrupted: "服务重启时操作中断", domain.FaultRecordStatusGatewayLocked: "该网关已有其他录波操作在进行,等它结束后再试", domain.FaultRecordStatusError: "操作出错(如主服务 MQTT 不可用)", } type frDeviceArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"` DeviceKind string `json:"deviceKind,omitempty" jsonschema:"设备:aGate(默认)、aPowerPE、aPowerMPPT、aHub;站点有哪些设备、是否支持见 fault_record_devices"` DeviceSn string `json:"deviceSn,omitempty" jsonschema:"设备 SN;aGate 可不填(即站点 SN),其他设备必填(见 fault_record_devices)"` } // frRequest 由设备参数构造操作请求 func frRequest(a frDeviceArgs, kind domain.FaultRecordOpKind) (dto.FaultRecordCreateOpReq, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return dto.FaultRecordCreateOpReq{}, err } env, err := kit.Enum("env", a.Env, "prod", "prod", "test") if err != nil { return dto.FaultRecordCreateOpReq{}, err } devKind := a.DeviceKind if devKind == "" { devKind = proto.DeviceKindAGate } devType, ok := proto.DeviceKindProtocolType[devKind] if !ok || devType == "" { return dto.FaultRecordCreateOpReq{}, fmt.Errorf("不支持的设备:%s(可选 aGate、aPowerPE、aPowerMPPT、aHub)", devKind) } devSn := strings.TrimSpace(a.DeviceSn) if devKind != proto.DeviceKindAGate && devSn == "" { return dto.FaultRecordCreateOpReq{}, fmt.Errorf("%s 需要 deviceSn,可用 fault_record_devices 查看", devKind) } return dto.FaultRecordCreateOpReq{Env: env, GatewaySn: sn, DeviceType: devType, DeviceSn: devSn, Kind: string(kind)}, nil } // frSubmit 提交操作(由主服务执行),返回操作 id func frSubmit(ctx context.Context, req dto.FaultRecordCreateOpReq) (string, *mcp.CallToolResult) { id, err := services.SubmitFaultRecordOperation(ctx, req, ServiceAccount) if errors.Is(err, services.ErrFaultRecordInvalid) { return "", kit.Fail(err) } if err != nil { return "", kit.Upstream("ToolBox 故障录波", err) } return id, nil } // frOpView 写给模型的操作结果:逐项结果,不含原始报文 func frOpView(d *dto.FaultRecordOpDetail) map[string]any { out := map[string]any{ "operationId": d.ID, "kind": d.Kind, "env": d.Env, "gatewaySn": d.GatewaySn, "deviceType": d.DeviceType, "deviceSn": d.DeviceSn, "request": d.Summary, "status": d.Status, "operator": d.Operator, "createdAt": fmtBeijing(d.CreatedAt), } if d.Error != "" { out["error"] = d.Error } if d.TimeoutStage != "" { out["timeoutStage"] = d.TimeoutStage } if d.ReplyDesc != "" { out["reply"] = d.ReplyDesc } if len(d.Items) > 0 { items := make([]map[string]any, 0, len(d.Items)) for _, it := range d.Items { m := map[string]any{"name": it.Name, "ok": it.Ok, "result": it.ResultDesc} if it.Code != 0 { m["code"] = it.Code } if it.Id != 0 { m["id"] = it.Id } if it.SetVal != "" { m["setVal"] = it.SetVal } if it.Decoded != nil { m["value"] = it.Decoded } else if it.Raw != "" { m["raw"] = it.Raw } if it.DecodeErr != "" { m["decodeError"] = it.DecodeErr } items = append(items, m) } out["items"] = items } if d.HasFile { out["hasFile"] = true out["next"] = "录波数据:fault_record_data 查看故障信息与波形概要,fault_record_export 导出 CSV" } return out } // frTaskState 把操作记录翻译为任务状态 func frTaskState(d *dto.FaultRecordOpDetail) kit.TaskState { st := kit.TaskState{CreatedAt: d.CreatedAt} status := domain.FaultRecordOpStatus(d.Status) switch { case !status.Terminal(): st.Status = mcp.TaskStatusWorking st.Message = frStageMessages[status] if st.Message == "" { st.Message = d.Status } case status == domain.FaultRecordStatusDone || status == domain.FaultRecordStatusItemFailed: // item_failed:设备已回复,但部分子项失败,逐项结果中有说明 st.Status, st.Message = mcp.TaskStatusCompleted, "完成" if status == domain.FaultRecordStatusItemFailed { st.Message = "完成,部分子项失败" } st.Result = kit.Object(frOpView(d)) case status == domain.FaultRecordStatusCancelled: st.Status, st.Message = mcp.TaskStatusCancelled, "已取消" st.Result = kit.Failf("故障录波操作已取消(操作 id %s)。", d.ID) default: st.Status = mcp.TaskStatusFailed reason := frFailMessages[status] if reason == "" { reason = d.Status } st.Message = reason view := frOpView(d) view["reason"] = reason st.Result = kit.Object(view) st.Result.IsError = true } return st } // frStatus 读取操作并翻译为任务状态(kit.TaskDef.Status) func frStatus(ctx context.Context, ref kit.TaskRef) (kit.TaskState, error) { d, err := services.GetFaultRecordOperation(ctx, ref.Key) if errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) || errors.Is(err, services.ErrFaultRecordInvalid) { return kit.TaskState{}, kit.ErrTaskNotFound } if err != nil { return kit.TaskState{}, err } return frTaskState(d), nil } // frCancel 请求取消操作(执行器 1 秒内响应);已结束的操作无需取消 func frCancel(ctx context.Context, ref kit.TaskRef) error { err := services.CancelFaultRecordOperation(ctx, ref.Key) if errors.Is(err, services.ErrFaultRecordInvalid) { return nil } return err } // frWait 普通调用时等待操作结束(最多 frSyncWait),未结束时返回进度与操作 id func frWait(ctx context.Context, id string) *mcp.CallToolResult { deadline := time.Now().Add(frSyncWait) for { d, err := services.GetFaultRecordOperation(ctx, id) if err != nil { return kit.Upstream("ToolBox 故障录波", err) } st := frTaskState(d) if st.Status.IsTerminal() { return st.Result } if time.Now().After(deadline) { return kit.Object(map[string]any{"operationId": id, "status": d.Status, "message": st.Message + ";设备还没有回复完,稍后用 fault_record_operation_get 查看结果"}) } select { case <-ctx.Done(): return kit.Object(map[string]any{"operationId": id, "status": d.Status, "message": "等待中断,稍后用 fault_record_operation_get 查看结果"}) case <-time.After(time.Second): } } } // frSubmitted 普通调用提交耗时较长的操作后立即返回 func frSubmitted(id, what string) *mcp.CallToolResult { return kit.Object(map[string]any{"operationId": id, "status": "pending", "message": "已提交" + what + ",由主服务与设备交互;用 fault_record_operation_get 查看进度与结果"}) } // ---- fault_record_devices ---- type frDevicesArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` Env string `json:"env,omitempty" jsonschema:"服务器环境:prod(生产,默认)/ test(测试)"` } var FaultRecordDevices = kit.Def[frDevicesArgs]{ Name: "fault_record_devices", Title: "查询站点可做故障录波的设备", Description: "列出站点中可以做故障录波操作的设备:aGate、各 aPower-PE、带 MPPT 的 aPower-MPPT、各 aHub,以及站点是否在线。" + "返回 devices:kind(设备种类,作为其他 fault_record_* 工具的 deviceKind)、sn(作为 deviceSn)、model、supported(是否可操作)、note(不可操作的原因)。" + "对设备做读取、召唤、设置前先用本工具确认设备与 SN。", ReadOnly: true, Properties: map[string]map[string]any{"env": {"enum": []string{"prod", "test"}}}, Handler: func(ctx context.Context, a frDevicesArgs) (*mcp.CallToolResult, error) { sn, err := kit.Required("sn", a.SN) if err != nil { return kit.Fail(err), nil } env, err := kit.Enum("env", a.Env, "prod", "prod", "test") if err != nil { return kit.Fail(err), nil } d, err := services.GetFaultRecordSiteDevices(env, sn) if errors.Is(err, services.ErrFaultRecordInvalid) { return kit.Fail(err), nil } if err != nil { return kit.Upstream("FWH 平台设备信息接口", err), nil } return kit.Object(d), nil }, } // ---- fault_record_device_info ---- type frDeviceInfoArgs struct { frDeviceArgs Codes []int `json:"codes,omitempty" jsonschema:"要读取的信息类别:1 故障总体信息(含快照、可配通道当前值)、2 录波功能基本信息(含采样频率)、3 故障录波总目录(设备上已有的录波记录序号,召唤录波前先读);默认全部"` } var FaultRecordDeviceInfo = kit.TaskDef[frDeviceInfoArgs]{ Def: kit.Def[frDeviceInfoArgs]{ Name: "fault_record_device_info", Title: "读取设备故障录波信息", Description: "通过 MQTT 从设备实时读取故障录波相关信息(不修改设备):故障总体信息、录波功能基本信息、故障录波总目录。" + "召唤录波(fault_record_fetch)前用类别 3 查看设备上有哪些录波记录序号。设备通常几秒到几十秒内回复;" + "返回 items(逐项结果,value 为解析后的内容)。设备离线时会超时失败。", ReadOnly: true, Properties: mergeProps(frDeviceProps, map[string]map[string]any{"codes": {"items": map[string]any{"type": "integer", "enum": []int{1, 2, 3}}}}), Handler: func(ctx context.Context, a frDeviceInfoArgs) (*mcp.CallToolResult, error) { id, fail := frStartDeviceInfo(ctx, a) if fail != nil { return fail, nil } return frWait(ctx, id), nil }, }, Poll: 5 * time.Second, Start: func(ctx context.Context, a frDeviceInfoArgs) (kit.TaskStart, *mcp.CallToolResult) { id, fail := frStartDeviceInfo(ctx, a) return kit.TaskStart{Key: id, Flag: "op", Note: "已向设备发送读取请求,设备回复后平台会把结果发给你"}, fail }, Status: frStatus, Cancel: frCancel, } func frStartDeviceInfo(ctx context.Context, a frDeviceInfoArgs) (string, *mcp.CallToolResult) { req, err := frRequest(a.frDeviceArgs, domain.FaultRecordOpDeviceInfo) if err != nil { return "", kit.Fail(err) } req.CodeList = a.Codes if len(req.CodeList) == 0 { req.CodeList = []int{1, 2, 3} } return frSubmit(ctx, req) } // ---- fault_record_param_read ---- type frParamReadArgs struct { frDeviceArgs Ids []int `json:"ids,omitempty" jsonschema:"要读取的功能码:4 设备时间、7 录波开关;默认全部。采样频率(5)、可配采样通道(6)的当前值在 fault_record_device_info 的类别 2、1 中"` } var FaultRecordParamRead = kit.TaskDef[frParamReadArgs]{ Def: kit.Def[frParamReadArgs]{ Name: "fault_record_param_read", Title: "读取设备故障录波参数", Description: "通过 MQTT 从设备读取故障录波参数的当前值(不修改设备):设备时间(功能码 4)、录波开关(功能码 7)。" + "返回 items(逐项结果,value 为解析后的值)。设备离线时会超时失败。", ReadOnly: true, Properties: mergeProps(frDeviceProps, map[string]map[string]any{"ids": {"items": map[string]any{"type": "integer", "enum": []int{4, 7}}}}), Handler: func(ctx context.Context, a frParamReadArgs) (*mcp.CallToolResult, error) { id, fail := frStartParamRead(ctx, a) if fail != nil { return fail, nil } return frWait(ctx, id), nil }, }, Poll: 5 * time.Second, Start: func(ctx context.Context, a frParamReadArgs) (kit.TaskStart, *mcp.CallToolResult) { id, fail := frStartParamRead(ctx, a) return kit.TaskStart{Key: id, Flag: "op", Note: "已向设备发送读取请求,设备回复后平台会把结果发给你"}, fail }, Status: frStatus, Cancel: frCancel, } func frStartParamRead(ctx context.Context, a frParamReadArgs) (string, *mcp.CallToolResult) { req, err := frRequest(a.frDeviceArgs, domain.FaultRecordOpParamRead) if err != nil { return "", kit.Fail(err) } req.IdList = a.Ids if len(req.IdList) == 0 { req.IdList = []int{4, 7} } return frSubmit(ctx, req) } // ---- fault_record_operations ---- type frOperationsArgs struct { SN string `json:"sn,omitempty" jsonschema:"站点 SN;不填查全部站点"` Env string `json:"env,omitempty" jsonschema:"服务器环境:prod / test;不填查全部"` Kind string `json:"kind,omitempty" jsonschema:"操作类型,多个用逗号分隔:device_info(读取信息)、record_fetch(召唤录波)、param_read(读取参数)、param_write(设置参数)"` Page int `json:"page,omitempty" jsonschema:"页码,默认 1"` Size int `json:"size,omitempty" jsonschema:"每页条数,1~100,默认 20"` } var FaultRecordOperations = kit.Def[frOperationsArgs]{ Name: "fault_record_operations", Title: "查询故障录波操作记录", Description: "分页查询 ToolBox 对设备做过的故障录波操作(读取信息、召唤录波、读取/设置参数),按时间倒序,包括页面上的操作与 AI 助手的操作。" + "返回 total 与 items:id(操作 id,用于 fault_record_operation_get、fault_record_data、fault_record_export)、kind、gatewaySn、deviceSn、summary(请求摘要)、" + "status、operator、hasFile(是否有已保存的录波文件)、createdAt。", ReadOnly: true, Properties: map[string]map[string]any{"env": {"enum": []string{"prod", "test"}}}, Handler: func(ctx context.Context, a frOperationsArgs) (*mcp.CallToolResult, error) { size := kit.Limit(a.Size, 20, 100) page := kit.Page(a.Page) res, err := services.ListFaultRecordOperations(ctx, mongoRepo.FaultRecordOpFilter{ Env: a.Env, GatewaySn: strings.TrimSpace(a.SN), Kind: a.Kind, }, int64(page), int64(size)) if err != nil { return kit.Upstream("ToolBox 故障录波", err), nil } items := make([]map[string]any, 0, len(res.Data)) for _, s := range res.Data { m := map[string]any{ "id": s.ID, "env": s.Env, "kind": s.Kind, "gatewaySn": s.GatewaySn, "deviceType": s.DeviceType, "deviceSn": s.DeviceSn, "summary": s.Summary, "status": s.Status, "operator": s.Operator, "hasFile": s.HasFile, "createdAt": fmtBeijing(s.CreatedAt), } if s.Error != "" { m["error"] = s.Error } items = append(items, m) } return kit.List(items, int(res.Total), size, "用 page 翻页,或用 sn、kind 缩小范围"), nil }, } // ---- fault_record_operation_get ---- type frOperationGetArgs struct { ID string `json:"id" jsonschema:"操作 id(fault_record_operations 或各操作工具返回的 operationId)"` } var FaultRecordOperationGet = kit.Def[frOperationGetArgs]{ Name: "fault_record_operation_get", Title: "查询故障录波操作结果", Description: "查询一次故障录波操作的状态与逐项结果(设备回复的解析内容,不含原始报文)。" + "status 为 done 表示成功、item_failed 表示部分子项失败,其他结束状态见 error。召唤录波成功后 hasFile=true,可用 fault_record_data、fault_record_export 查看数据。", ReadOnly: true, Handler: func(ctx context.Context, a frOperationGetArgs) (*mcp.CallToolResult, error) { d, err := services.GetFaultRecordOperation(ctx, strings.TrimSpace(a.ID)) if errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) || errors.Is(err, services.ErrFaultRecordInvalid) { return kit.Failf("没有找到操作 %s,可以用 fault_record_operations 查看操作记录", a.ID), nil } if err != nil { return kit.Upstream("ToolBox 故障录波", err), nil } view := frOpView(d) if st := frTaskState(d); !st.Status.IsTerminal() { view["progress"] = st.Message } return kit.Object(view), nil }, } // ---- fault_record_data ---- var FaultRecordData = kit.Def[frOperationGetArgs]{ Name: "fault_record_data", Title: "查看故障录波数据概要", Description: "解析一次召唤录波(record_fetch)保存的录波文件,返回每条记录的故障信息与波形概要:" + "failure(故障时间、触发源、软件版本、快照通道等)、waveform(采样点数、触发前点数、采样频率,以及每个通道的 min、max 与触发点的值)。" + "不含逐点采样值;需要逐点分析时用 fault_record_export 导出 CSV。", ReadOnly: true, Handler: func(ctx context.Context, a frOperationGetArgs) (*mcp.CallToolResult, error) { v, err := services.GetFaultRecordRecord(ctx, strings.TrimSpace(a.ID)) if errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) || errors.Is(err, services.ErrFaultRecordInvalid) { return kit.Fail(err), nil } if err != nil { return kit.Upstream("ToolBox 故障录波文件", err), nil } records := make([]map[string]any, 0, len(v.Records)) for _, r := range v.Records { records = append(records, map[string]any{ "batch": r.Batch, "sn": r.Sn, "recordIndex": r.RecordIndex, "failure": r.Failure, "waveform": waveformSummary(r.Waveform), }) } return kit.Object(map[string]any{"operationId": v.OperationID, "records": records}), nil }, } // waveformSummary 波形概要:通道统计,不含逐点数据 func waveformSummary(w faultRecordFile.Waveform) map[string]any { channels := make([]map[string]any, 0, len(w.Channels)) for _, ch := range w.Channels { c := map[string]any{"channel": fmt.Sprintf("ch%d 0x%04X", ch.Number, ch.Meaning)} if ch.Configurable != nil { c["configurable"] = ch.Configurable.TypeName } if len(ch.Values) > 0 { lo, hi := ch.Values[0], ch.Values[0] for _, x := range ch.Values { lo, hi = min(lo, x), max(hi, x) } c["min"], c["max"] = lo, hi if w.PrePoints >= 0 && w.PrePoints < len(ch.Values) { c["atTrigger"] = ch.Values[w.PrePoints] } } channels = append(channels, c) } return map[string]any{"points": w.Points, "prePoints": w.PrePoints, "sampleFrequency": w.SampleFrequency, "channels": channels} } // ---- fault_record_export ---- var FaultRecordExport = kit.Def[frOperationGetArgs]{ Name: "fault_record_export", Title: "导出故障录波数据", Description: "把一次召唤录波保存的录波文件解析为 CSV(每条记录的故障信息、快照与逐点波形,打包为 zip),以文件形式保存到会话工作区。" + "返回文件路径;请用 Python 解压后用 pandas 分析、作图,不要把全文读入上下文。", ReadOnly: true, Handler: func(ctx context.Context, a frOperationGetArgs) (*mcp.CallToolResult, error) { name, data, err := services.ExportFaultRecordFile(ctx, strings.TrimSpace(a.ID), "csv") if errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) || errors.Is(err, services.ErrFaultRecordInvalid) { return kit.Fail(err), nil } if err != nil { return kit.Upstream("ToolBox 故障录波文件", err), nil } return kit.ExportFile("fwh://fault-record/"+name, "application/zip", data, "zip 内每条记录一组 CSV(故障信息、快照、逐点波形,通道列名为 ch<序号> 0x<含义码>)。"), nil }, } // ---- 写操作(ops):fault_record_fetch、fault_record_param_write、fault_record_cancel、fault_record_retry_download ---- type frFetchArgs struct { frDeviceArgs RecordIndexes []int `json:"recordIndexes" jsonschema:"要召唤的录波记录序号(从 fault_record_device_info 类别 3 的故障录波总目录中选),可多条,设备打成一个包上传"` } var FaultRecordFetch = kit.TaskDef[frFetchArgs]{ Def: kit.Def[frFetchArgs]{ Name: "fault_record_fetch", Title: "召唤设备故障录波", Description: "通过 MQTT 让设备上传指定的故障录波记录,下载后保存并解析。耗时通常几分钟(设备打包、上传、下载)。" + "以长任务方式执行:提交后立即返回,完成后平台会把结果发给你;之后用 fault_record_data 查看概要、fault_record_export 导出 CSV。" + "先用 fault_record_device_info(类别 3)确认记录序号。这是写操作,执行前会请求确认。", ReadOnly: false, Properties: mergeProps(frDeviceProps, map[string]map[string]any{"recordIndexes": {"minItems": 1, "items": map[string]any{"type": "integer", "minimum": 1}}}), Handler: func(ctx context.Context, a frFetchArgs) (*mcp.CallToolResult, error) { id, fail := frStartFetch(ctx, a) if fail != nil { return fail, nil } return frSubmitted(id, "召唤录波"), nil }, }, Poll: 15 * time.Second, Start: func(ctx context.Context, a frFetchArgs) (kit.TaskStart, *mcp.CallToolResult) { id, fail := frStartFetch(ctx, a) return kit.TaskStart{Key: id, Flag: "op", Note: "已提交召唤录波,设备上传与下载通常需要几分钟,完成后平台会把结果发给你"}, fail }, Status: frStatus, Cancel: frCancel, } func frStartFetch(ctx context.Context, a frFetchArgs) (string, *mcp.CallToolResult) { req, err := frRequest(a.frDeviceArgs, domain.FaultRecordOpRecordFetch) if err != nil { return "", kit.Fail(err) } req.RecordIndexes = a.RecordIndexes return frSubmit(ctx, req) } type frParamWriteArgs struct { frDeviceArgs Params []proto.ParaItem `json:"params" jsonschema:"要设置的参数:id 为功能码,setVal 为十六进制字节(可带空格)。1 清除设备故障波形(不可恢复,setVal 为空);2 采样通道配置(1 字节个数 + 每通道 2 字节);3 手动触发故障录波(setVal 为空);4 设备对时(7 字节:年 2B 大端、月、日、时、分、秒);5 采样频率(2 字节);6 可配采样通道(6 字节);7 录波开关(00 关、01 开)"` } var FaultRecordParamWrite = kit.TaskDef[frParamWriteArgs]{ Def: kit.Def[frParamWriteArgs]{ Name: "fault_record_param_write", Title: "设置设备故障录波参数", Description: "通过 MQTT 设置设备的故障录波参数或执行动作(清除波形、手动触发录波、对时、采样频率、通道配置、录波开关)。" + "会修改设备状态,其中清除波形不可恢复:调用前向用户说明要设置的设备、功能码与值。设置前可用 fault_record_param_read、fault_record_device_info 读取当前值。" + "返回逐项设置结果。这是破坏性写操作,每次执行前都会请求确认。", ReadOnly: false, Destructive: true, Properties: mergeProps(frDeviceProps, map[string]map[string]any{"params": {"minItems": 1}}), Handler: func(ctx context.Context, a frParamWriteArgs) (*mcp.CallToolResult, error) { id, fail := frStartParamWrite(ctx, a) if fail != nil { return fail, nil } return frWait(ctx, id), nil }, }, Poll: 5 * time.Second, Start: func(ctx context.Context, a frParamWriteArgs) (kit.TaskStart, *mcp.CallToolResult) { id, fail := frStartParamWrite(ctx, a) return kit.TaskStart{Key: id, Flag: "op", Note: "已向设备发送设置请求,设备回复后平台会把结果发给你"}, fail }, Status: frStatus, Cancel: frCancel, } func frStartParamWrite(ctx context.Context, a frParamWriteArgs) (string, *mcp.CallToolResult) { req, err := frRequest(a.frDeviceArgs, domain.FaultRecordOpParamWrite) if err != nil { return "", kit.Fail(err) } req.ParaList = a.Params return frSubmit(ctx, req) } var FaultRecordCancel = kit.Def[frOperationGetArgs]{ Name: "fault_record_cancel", Title: "取消故障录波操作", Description: "取消一次进行中的故障录波操作(在页面或其他会话中发起的也可以取消),执行器 1 秒内停止。已结束的操作无法取消。" + "本会话以长任务方式发起的操作,也可以在平台的后台调用栏中取消。这是写操作,执行前会请求确认。", ReadOnly: false, // 取消已取消的操作会报“已结束”,不会产生额外影响 Idempotent: true, Handler: func(ctx context.Context, a frOperationGetArgs) (*mcp.CallToolResult, error) { err := services.CancelFaultRecordOperation(ctx, strings.TrimSpace(a.ID)) if errors.Is(err, services.ErrFaultRecordInvalid) || errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) { return kit.Fail(err), nil } if err != nil { return kit.Upstream("ToolBox 故障录波", err), nil } return mcp.NewToolResultText("已请求取消操作 " + a.ID + ",用 fault_record_operation_get 确认状态。"), nil }, } var FaultRecordRetryDownload = kit.Def[frOperationGetArgs]{ Name: "fault_record_retry_download", Title: "重试下载故障录波文件", Description: "设备已上传录波文件、但下载失败或超时的召唤录波操作,重新下载(按 15 秒间隔重试到总超时)。" + "只在 fault_record_operation_get 显示上传已完成且下载失败时使用。这是写操作,执行前会请求确认。", ReadOnly: false, Handler: func(ctx context.Context, a frOperationGetArgs) (*mcp.CallToolResult, error) { err := services.RetryFaultRecordDownload(ctx, strings.TrimSpace(a.ID), ServiceAccount) if errors.Is(err, services.ErrFaultRecordInvalid) || errors.Is(err, mongoRepo.ErrFaultRecordOpNotFound) { return kit.Fail(err), nil } if err != nil { return kit.Upstream("ToolBox 故障录波", err), nil } return mcp.NewToolResultText("已重新提交下载,用 fault_record_operation_get 查看结果。"), nil }, } // mergeProps 合并多组字段补充约束 func mergeProps(groups ...map[string]map[string]any) map[string]map[string]any { out := map[string]map[string]any{} for _, g := range groups { for k, v := range g { out[k] = v } } return out }