| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- 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
- }
|