From 551c6016ffafcb1ab14159667abf0db5edb5e4d4 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 28 Apr 2026 16:29:52 +0800 Subject: [PATCH] Expose graph node type catalog from agent --- agent/internal/httpapi/config_status_test.go | 45 +++++ agent/internal/httpapi/graph_node_types.go | 167 +++++++++++++++++++ agent/internal/httpapi/server.go | 1 + 3 files changed, 213 insertions(+) create mode 100644 agent/internal/httpapi/graph_node_types.go diff --git a/agent/internal/httpapi/config_status_test.go b/agent/internal/httpapi/config_status_test.go index 12ffad4..b42dfed 100644 --- a/agent/internal/httpapi/config_status_test.go +++ b/agent/internal/httpapi/config_status_test.go @@ -176,3 +176,48 @@ func TestHandleInfoIncludesCurrentConfigSummary(t *testing.T) { t.Fatalf("overlays = %#v", got["overlays"]) } } + +func TestHandleGraphNodeTypesListsRegisteredMediaNodes(t *testing.T) { + s := &Server{} + + req := httptest.NewRequest(http.MethodGet, "/v1/graph-node-types", nil) + rr := httptest.NewRecorder() + s.handleGraphNodeTypes(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("status code: got %d body=%s", rr.Code, rr.Body.String()) + } + var got struct { + Items []struct { + Type string `json:"type"` + Label string `json:"label"` + Category string `json:"category"` + Icon string `json:"icon"` + Description string `json:"description"` + Defaults map[string]any `json:"defaults"` + } `json:"items"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { + t.Fatalf("decode response: %v", err) + } + seen := map[string]bool{} + for _, item := range got.Items { + seen[item.Type] = true + if item.Label == "" || item.Category == "" || item.Icon == "" || item.Description == "" { + t.Fatalf("node type metadata incomplete: %#v", item) + } + if item.Defaults["type"] != item.Type { + t.Fatalf("defaults should include node type %q, got %#v", item.Type, item.Defaults) + } + } + for _, want := range []string{ + "input_rtsp", "input_file", "preprocess", "ai_scrfd", "ai_scrfd_sliding", + "ai_face_det", "ai_face_recog", "ai_yolo", "ai_shoe_det", "tracker", + "logic_gate", "event_fusion", "region_event", "action_recog", "det_post", + "osd", "publish", "storage", "alarm", "gate", "zlm_http", + } { + if !seen[want] { + t.Fatalf("missing registered node type %q in %#v", want, got.Items) + } + } +} diff --git a/agent/internal/httpapi/graph_node_types.go b/agent/internal/httpapi/graph_node_types.go new file mode 100644 index 0000000..83b138d --- /dev/null +++ b/agent/internal/httpapi/graph_node_types.go @@ -0,0 +1,167 @@ +package httpapi + +import "net/http" + +type graphNodeParam struct { + Key string `json:"key"` + Label string `json:"label"` + Type string `json:"type"` + Step string `json:"step,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Options []string `json:"options,omitempty"` +} + +type graphNodeTypeInfo struct { + Type string `json:"type"` + Label string `json:"label"` + Category string `json:"category"` + Icon string `json:"icon"` + Description string `json:"description"` + Defaults map[string]any `json:"defaults"` + Params []graphNodeParam `json:"params,omitempty"` +} + +func (s *Server) handleGraphNodeTypes(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + if !s.authorize(r, false) { + errorJSON(w, http.StatusUnauthorized, "unauthorized") + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "items": graphNodeTypesCatalog(), + }) +} + +func graphNodeTypesCatalog() []graphNodeTypeInfo { + return []graphNodeTypeInfo{ + nodeType("input_rtsp", "RTSP 输入", "输入", "camera", "从网络摄像机或流媒体地址读取视频流。", "source", map[string]any{"url": "${rtsp_url}"}, []graphNodeParam{ + textParam("url", "RTSP 地址", "${rtsp_url}"), + numberParam("fps", "输入帧率", "1"), + numberParam("width", "宽度", "1"), + numberParam("height", "高度", "1"), + boolParam("force_tcp", "强制 TCP"), + numberParam("reconnect_sec", "重连间隔秒", "1"), + }), + nodeType("input_file", "文件输入", "输入", "file", "从本地视频文件读取帧,常用于离线验证和回放。", "source", nil, []graphNodeParam{ + textParam("path", "文件路径", ""), + numberParam("fps", "回放帧率", "1"), + boolParam("loop", "循环播放"), + }), + nodeType("preprocess", "图像预处理", "处理", "adjust", "调整尺寸、格式和硬件加速路径,为后续推理或编码准备图像。", "filter", map[string]any{"dst_format": "rgb"}, []graphNodeParam{ + numberParam("dst_w", "输出宽度", "1"), + numberParam("dst_h", "输出高度", "1"), + selectParam("dst_format", "输出格式", []string{"rgb", "nv12", "bgr"}), + selectParam("resize_mode", "缩放方式", []string{"stretch", "letterbox"}), + boolParam("use_rga", "使用 RGA"), + textParam("rga_gate", "RGA 通道", ""), + }), + nodeType("ai_scrfd", "SCRFD 人脸检测", "AI 推理", "scan-face", "使用 SCRFD 模型做人脸检测,适合固定输入尺寸场景。", "filter", nil, faceDetParams()), + nodeType("ai_scrfd_sliding", "滑窗人脸检测", "AI 推理", "scan-face", "使用滑窗方式执行 SCRFD 人脸检测,适合高分辨率画面。", "filter", nil, faceDetParams()), + nodeType("ai_face_det", "人脸检测", "AI 推理", "face", "通用人脸检测节点,输出人脸框和质量信息。", "filter", nil, faceDetParams()), + nodeType("ai_face_recog", "人脸识别", "AI 推理", "face-id", "对检测到的人脸进行特征提取和人脸库匹配。", "filter", nil, []graphNodeParam{ + textParam("model_path", "模型路径", ""), + numberParam("infer_fps", "推理帧率", "0.1"), + boolParam("align", "人脸对齐"), + boolParam("emit_embedding", "输出特征"), + numberParam("max_faces", "最大人脸数", "1"), + }), + nodeType("ai_yolo", "YOLO 目标检测", "AI 推理", "target", "使用 YOLO 模型检测人员、PPE 或其他目标。", "filter", nil, []graphNodeParam{ + textParam("model_path", "模型路径", ""), + numberParam("infer_fps", "推理帧率", "0.1"), + numberParam("model_w", "模型宽度", "1"), + numberParam("model_h", "模型高度", "1"), + numberParam("conf", "置信度", "0.01"), + numberParam("nms", "NMS", "0.01"), + }), + nodeType("ai_shoe_det", "鞋靴检测", "AI 推理", "shoe", "检测鞋靴和工鞋相关目标,可配合逻辑节点判断违规。", "filter", nil, []graphNodeParam{ + textParam("model_path", "模型路径", ""), + numberParam("infer_fps", "推理帧率", "0.1"), + numberParam("conf", "置信度", "0.01"), + numberParam("nms", "NMS", "0.01"), + boolParam("append_detections", "追加检测结果"), + }), + nodeType("tracker", "目标跟踪", "处理", "route", "对检测目标分配跟踪 ID,保持跨帧目标状态。", "filter", nil, []graphNodeParam{ + textParam("mode", "跟踪模式", ""), + boolParam("per_class", "按类别跟踪"), + numberParam("high_th", "高阈值", "0.01"), + numberParam("low_th", "低阈值", "0.01"), + numberParam("iou_th", "IOU 阈值", "0.01"), + numberParam("max_age_ms", "最大保留毫秒", "1"), + }), + nodeType("logic_gate", "规则判断", "规则", "branch", "根据检测、跟踪或颜色分析结果进行业务规则判断。", "filter", nil, []graphNodeParam{ + textParam("mode", "逻辑模式", ""), + boolParam("debug", "调试输出"), + numberParam("anchor_class", "锚点类别", "1"), + numberParam("boots_class", "鞋靴类别", "1"), + numberParam("violation_class", "违规类别", "1"), + }), + nodeType("event_fusion", "事件融合", "规则", "merge", "融合多路事件,减少重复告警并形成更稳定的业务事件。", "filter", nil, nil), + nodeType("region_event", "区域事件", "规则", "region", "基于区域、越线或停留规则生成区域行为事件。", "filter", nil, nil), + nodeType("action_recog", "行为识别", "AI 推理", "activity", "识别人员行为或动作事件。", "filter", nil, nil), + nodeType("det_post", "检测后处理", "处理", "filter", "对检测结果做过滤、映射、合并或类别转换。", "filter", nil, nil), + nodeType("osd", "画面叠加", "输出", "overlay", "在视频帧上绘制检测框、文字、人脸识别和事件信息。", "filter", nil, []graphNodeParam{ + boolParam("draw_bbox", "绘制框"), + boolParam("draw_text", "绘制文字"), + boolParam("draw_face_det", "绘制人脸检测"), + boolParam("draw_face_recog", "绘制人脸识别"), + numberParam("line_width", "线宽", "0.1"), + numberParam("font_scale", "字体缩放", "0.1"), + }), + nodeType("publish", "视频输出", "输出", "broadcast", "编码并发布 RTSP、HLS 或其他视频输出。", "sink", nil, []graphNodeParam{ + selectParam("codec", "编码", []string{"h264", "h265"}), + numberParam("fps", "输出帧率", "1"), + numberParam("gop", "GOP", "1"), + numberParam("bitrate_kbps", "码率 kbps", "1"), + boolParam("use_mpp", "使用 MPP"), + boolParam("use_ffmpeg_mux", "FFmpeg 封装"), + }), + nodeType("storage", "本地存储", "输出", "database", "保存帧、事件或中间结果到本地存储。", "sink", nil, nil), + nodeType("alarm", "告警动作", "输出", "bell", "根据规则触发日志、抓图、录像片段、外部接口等动作。", "sink", nil, []graphNodeParam{ + numberParam("eval_fps", "评估帧率", "0.1"), + }), + nodeType("gate", "流控闸门", "系统", "gate", "控制流程分支或限流,保护下游节点。", "filter", nil, nil), + nodeType("zlm_http", "ZLMediaKit HTTP", "系统", "server", "提供 ZLMediaKit 相关 HTTP 文件服务能力。", "sink", nil, nil), + } +} + +func nodeType(t, label, category, icon, description, role string, defaults map[string]any, params []graphNodeParam) graphNodeTypeInfo { + if defaults == nil { + defaults = map[string]any{} + } + defaults["id"] = t + defaults["type"] = t + defaults["role"] = role + defaults["enable"] = true + return graphNodeTypeInfo{Type: t, Label: label, Category: category, Icon: icon, Description: description, Defaults: defaults, Params: params} +} + +func textParam(key, label, placeholder string) graphNodeParam { + return graphNodeParam{Key: key, Label: label, Type: "text", Placeholder: placeholder} +} + +func numberParam(key, label, step string) graphNodeParam { + return graphNodeParam{Key: key, Label: label, Type: "number", Step: step} +} + +func boolParam(key, label string) graphNodeParam { + return graphNodeParam{Key: key, Label: label, Type: "boolean"} +} + +func selectParam(key, label string, options []string) graphNodeParam { + return graphNodeParam{Key: key, Label: label, Type: "select", Options: options} +} + +func faceDetParams() []graphNodeParam { + return []graphNodeParam{ + textParam("model_path", "模型路径", ""), + numberParam("infer_fps", "推理帧率", "0.1"), + numberParam("model_w", "模型宽度", "1"), + numberParam("model_h", "模型高度", "1"), + numberParam("conf_thresh", "置信度", "0.01"), + numberParam("nms_thresh", "NMS", "0.01"), + numberParam("max_faces", "最大人脸数", "1"), + } +} diff --git a/agent/internal/httpapi/server.go b/agent/internal/httpapi/server.go index 6a30975..1a98844 100644 --- a/agent/internal/httpapi/server.go +++ b/agent/internal/httpapi/server.go @@ -138,6 +138,7 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st mux.HandleFunc("/v1/media-server/binary", s.handleMediaBinaryUpdate) mux.HandleFunc("/v1/media-server/binary/rollback", s.handleMediaBinaryRollback) mux.HandleFunc("/v1/agent/binary", s.handleAgentBinaryUpdate) + mux.HandleFunc("/v1/graph-node-types", s.handleGraphNodeTypes) mux.HandleFunc("/v1/graphs", s.handleGraphs) mux.HandleFunc("/v1/graphs/", s.handleGraphDetail) mux.HandleFunc("/v1/logs/recent", s.handleLogsRecent)