feat: device capabilities from agent /v1/capabilities

Agent 新增 /v1/capabilities 接口,返回设备检测能力列表。
Managerd 控制台查询此接口,仅显示设备真实支持的功能。

- Agent: capabilities.go — 根据插件节点类型映射能力
- Managerd: pageConsole 查询 /v1/capabilities 过滤可选功能
- ConsoleFeature 增加 Description 字段,hover 显示说明
- 旧 agent 无此接口时回退为显示已持久化的功能
This commit is contained in:
tian 2026-05-12 13:52:53 +08:00
parent 286dcaa945
commit 700d964524
3 changed files with 96 additions and 15 deletions

View File

@ -19,6 +19,9 @@ type FeatureDef struct {
// FeatureRegistry centralises which templates cover which detection features.
type FeatureRegistry struct {
features map[string]*FeatureDef
// nodeTypeRequirements maps feature key → required media-server node types.
// If a device lacks any required node type, the feature is unavailable.
nodeTypeRequirements map[string][]string
}
// NewFeatureRegistry creates a registry with the built-in feature definitions.
@ -50,7 +53,16 @@ func NewFeatureRegistry() *FeatureRegistry {
Templates: []string{}, // 模板就绪后再补充
},
}
r := &FeatureRegistry{features: make(map[string]*FeatureDef, len(defs))}
r := &FeatureRegistry{
features: make(map[string]*FeatureDef, len(defs)),
nodeTypeRequirements: map[string][]string{
"face": {"ai_face_recog"}, // face recognition plugin
"shoe": {"ai_shoe_det"}, // shoe detection plugin
"helmet": {"ai_yolo"}, // YOLO for helmet (model pending)
"smoking": {"action_recog"}, // action recognition for smoking
"intrusion": {"region_event"}, // region-based intrusion
},
}
for i := range defs {
r.features[defs[i].Key] = &defs[i]
}
@ -68,6 +80,41 @@ func (r *FeatureRegistry) FeatureKeys() []string {
}
// Feature returns the definition for a feature key, or nil.
// AvailableFeatures returns the subset of registered features that are supported
// by the given device node types (from /v1/graph-node-types).
// If nodeTypes is nil/empty, all features are considered available (fallback for
// old agents that don't expose node types).
func (r *FeatureRegistry) AvailableFeatures(nodeTypes []string) []FeatureDef {
all := r.AllFeatures()
if len(nodeTypes) == 0 {
return all // no info → assume all available
}
typeSet := make(map[string]bool, len(nodeTypes))
for _, t := range nodeTypes {
typeSet[t] = true
}
out := make([]FeatureDef, 0, len(all))
for _, f := range all {
if r.featureAvailable(f.Key, typeSet) {
out = append(out, f)
}
}
return out
}
func (r *FeatureRegistry) featureAvailable(key string, nodeTypes map[string]bool) bool {
required, ok := r.nodeTypeRequirements[key]
if !ok || len(required) == 0 {
return true // no specific plugin requirement → always available
}
for _, t := range required {
if !nodeTypes[t] {
return false
}
}
return true
}
func (r *FeatureRegistry) Feature(key string) *FeatureDef {
return r.features[key]
}

View File

@ -909,13 +909,36 @@ func (u *UI) pageConsole(w http.ResponseWriter, r *http.Request) {
sourceNames[s.Name] = s.Name
}
// Feature definitions
featureDefs := []ConsoleFeature{
{Key: "face", Label: "人脸识别"},
{Key: "shoe", Label: "劳保鞋检测"},
{Key: "helmet", Label: "安全帽检测"},
{Key: "smoking", Label: "抽烟检测"},
{Key: "intrusion", Label: "区域入侵"},
// Query each device's capabilities from agent to filter available features.
deviceAvailableFeatures := map[string][]ConsoleFeature{}
if u.agent != nil {
for _, dev := range devices {
if dev == nil || !dev.Online || dev.DeviceID == "" {
continue
}
body, code, err := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/capabilities", nil)
if err != nil || code != 200 {
continue
}
var capsResp struct {
Capabilities []struct {
Key string `json:"key"`
Label string `json:"label"`
Available bool `json:"available"`
Description string `json:"description"`
} `json:"capabilities"`
}
if json.Unmarshal(body, &capsResp) != nil {
continue
}
features := make([]ConsoleFeature, 0, len(capsResp.Capabilities))
for _, c := range capsResp.Capabilities {
if c.Available {
features = append(features, ConsoleFeature{Key: c.Key, Label: c.Label, Description: c.Description, Enabled: false})
}
}
deviceAvailableFeatures[dev.DeviceID] = features
}
}
// Load persisted device features as the source of truth.
@ -1015,10 +1038,20 @@ func (u *UI) pageConsole(w http.ResponseWriter, r *http.Request) {
activeKeys = map[string]bool{}
}
// Features from device capabilities (filtered by agent /v1/capabilities).
// Fall back to persisted keys if agent doesn't respond.
available := deviceAvailableFeatures[dev.DeviceID]
var features []ConsoleFeature
for _, fd := range featureDefs {
f := ConsoleFeature{Key: fd.Key, Label: fd.Label, Enabled: activeKeys[fd.Key]}
features = append(features, f)
if len(available) > 0 {
for _, f := range available {
f.Enabled = activeKeys[f.Key]
features = append(features, f)
}
} else {
// Old agent: use persisted keys to show at least what's configured.
for key := range activeKeys {
features = append(features, ConsoleFeature{Key: key, Label: key, Enabled: true})
}
}
npu, diskUsage, temp, cpuUsage, memUsage := 0.0, 0.0, 0.0, 0.0, 0.0
@ -4695,7 +4728,8 @@ type ConsoleChannel struct {
}
type ConsoleFeature struct {
Key string
Label string
Enabled bool
Key string
Label string
Description string
Enabled bool
}

View File

@ -62,7 +62,7 @@
{{range $f := $cd.Features}}
<tr>
<td class="console-feature-check"><input type="checkbox" name="device_{{$cd.Device.DeviceID}}_feature" value="{{$f.Key}}"{{if $f.Enabled}} checked{{end}}></td>
<td class="console-feature-label">{{$f.Label}}</td>
<td class="console-feature-label" title="{{$f.Description}}">{{$f.Label}}</td>
</tr>
{{end}}
</table>