safesight/control/internal/service/auto_config.go
tian 98e375dc36 chore: 合并 safesight-control 仓库为 control/ 子目录
项目合并为单仓库:设备端(根) + 管理端(control/)。
两端 git 历史完整保留(git subtree 合并)。

git-subtree-dir: control
git-subtree-mainline: 923ab10d5c
git-subtree-split: 70738edb31
2026-08-02 11:13:26 +08:00

808 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"safesight-control/internal/storage"
)
// FeatureDef describes a detection feature that users can toggle on/off.
type FeatureDef struct {
Key string // unique key, e.g. "face", "shoe"
Label string // human-readable label
Templates []string // template names supporting this feature, ordered by coverage (broadest first)
}
// 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.
func NewFeatureRegistry() *FeatureRegistry {
defs := []FeatureDef{
{
Key: "face",
Label: "人脸识别",
Templates: []string{"std_workshop_face_recognition_shoe_alarm", "std_face_recognition_stream"},
},
{
Key: "shoe",
Label: "劳保鞋检测",
Templates: []string{"std_workshop_face_recognition_shoe_alarm", "std_workshoe_detection_stream"},
},
{
Key: "helmet",
Label: "安全帽检测",
Templates: []string{}, // 模型就绪后再补充
},
{
Key: "smoking",
Label: "抽烟检测",
Templates: []string{}, // 模型就绪后再补充
},
{
Key: "intrusion",
Label: "区域入侵",
Templates: []string{}, // 模板就绪后再补充
},
}
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]
}
return r
}
// FeatureKeys returns all registered feature keys in a stable order.
func (r *FeatureRegistry) FeatureKeys() []string {
keys := make([]string, 0, len(r.features))
for k := range r.features {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// 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]
}
// AllFeatures returns a copy of all feature definitions sorted by key.
func (r *FeatureRegistry) AllFeatures() []FeatureDef {
out := make([]FeatureDef, 0, len(r.features))
for _, key := range r.FeatureKeys() {
if f := r.features[key]; f != nil {
out = append(out, *f)
}
}
return out
}
// ResolveResult describes the outcome of template resolution.
type ResolveResult struct {
TemplateName string `json:"template_name"` // selected template name; may be a composed name when Composed is true
Composed bool `json:"composed"` // true when no single template covers all features and composition is needed
// internal fields for the template composer (unexported)
composePrimary string `json:"-"`
composeFeatures []string `json:"-"`
}
// ResolveTemplate picks the best template covering all requested features.
//
// Strategy:
// 1. Single-template: find the intersection of all features' template lists.
// Return the first (highest-coverage) match.
// 2. Composition needed: every feature has at least one template but no single
// template covers all. This case will be handled by the template composer
// later — for now we return the broadest template and set Composed=true.
// 3. Error: one or more features have no templates at all.
func (r *FeatureRegistry) ResolveTemplate(featureKeys []string) (*ResolveResult, error) {
if len(featureKeys) == 0 {
return nil, fmt.Errorf("未选择任何检测功能")
}
// Deduplicate and sort
seen := map[string]struct{}{}
cleaned := make([]string, 0, len(featureKeys))
for _, key := range featureKeys {
key = strings.TrimSpace(key)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if _, ok := r.features[key]; !ok {
return nil, fmt.Errorf("未知的检测功能: %q", key)
}
cleaned = append(cleaned, key)
}
if len(cleaned) == 0 {
return nil, fmt.Errorf("未选择任何检测功能")
}
sort.Strings(cleaned)
// Collect template lists for each feature.
lists := make([][]string, 0, len(cleaned))
for _, key := range cleaned {
def := r.features[key]
if len(def.Templates) == 0 {
return nil, fmt.Errorf("%s%s的模板尚未就绪", def.Label, def.Key)
}
lists = append(lists, def.Templates)
}
// Find intersection.
intersection := intersectTemplateLists(lists)
if len(intersection) > 0 {
// Pick the most specific template: the one covering the fewest features
// (i.e. listed by the fewest FeatureDefs), not the broadest.
// This ensures selecting only "face" doesn't pull in a face+shoe template.
best := intersection[0]
bestScore := r.templateFeatureCount(best)
for _, tpl := range intersection[1:] {
if score := r.templateFeatureCount(tpl); score < bestScore {
best = tpl
bestScore = score
}
}
return &ResolveResult{
TemplateName: best,
Composed: false,
}, nil
}
// No single template covers all features → composition needed.
// For now, pick the template that covers the most features.
primary := pickBroadestTemplate(lists)
composedName := "auto_" + strings.Join(cleaned, "_")
return &ResolveResult{
TemplateName: composedName,
Composed: true,
composePrimary: primary,
composeFeatures: append([]string(nil), cleaned...),
}, nil
}
// intersectTemplateLists returns templates that appear in every list, preserving
// the order of the first list.
func intersectTemplateLists(lists [][]string) []string {
if len(lists) == 0 {
return nil
}
// Count occurrences across all lists.
counts := map[string]int{}
for _, list := range lists {
lseen := map[string]struct{}{} // dedupe within one list
for _, t := range list {
if _, ok := lseen[t]; ok {
continue
}
lseen[t] = struct{}{}
counts[t]++
}
}
total := len(lists)
// Return in the order of the first list (which is sorted by coverage).
result := make([]string, 0)
seen := map[string]struct{}{}
for _, t := range lists[0] {
if counts[t] == total {
if _, ok := seen[t]; !ok {
seen[t] = struct{}{}
result = append(result, t)
}
}
}
return result
}
// pickBroadestTemplate returns the template that appears earliest in the
// first list (assumed to be sorted by coverage breadth).
func pickBroadestTemplate(lists [][]string) string {
if len(lists) == 0 || len(lists[0]) == 0 {
return ""
}
return lists[0][0]
}
// templateFeatureCount returns how many features list this template.
// Lower = more specific template.
func (r *FeatureRegistry) templateFeatureCount(templateName string) int {
count := 0
for _, def := range r.features {
for _, t := range def.Templates {
if t == templateName {
count++
break
}
}
}
return count
}
func (r *FeatureRegistry) ValidateFeatureKeys(keys []string) error {
seen := map[string]struct{}{}
for _, key := range keys {
key = strings.TrimSpace(key)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if _, ok := r.features[key]; !ok {
return fmt.Errorf("未知的检测功能: %q", key)
}
}
if len(seen) == 0 {
return fmt.Errorf("未选择任何检测功能")
}
return nil
}
// ────────────────────────────────────────────────────────────
// AutoConfigService — one-click configuration pipeline
// ────────────────────────────────────────────────────────────
// AutoConfigRequest is a single device's configuration intent from the console.
type AutoConfigRequest struct {
DeviceID string `json:"device_id"`
Features []string `json:"features"`
SourceNames []string `json:"source_names"`
Overlays []string `json:"overlays,omitempty"`
}
// AutoConfigResult reports what happened for a single device.
type AutoConfigResult struct {
DeviceID string `json:"device_id"`
TemplateName string `json:"template_name"`
Composed bool `json:"composed"`
UnitsCreated int `json:"units_created"`
ConfigSHA256 string `json:"config_sha256"`
TaskID string `json:"task_id,omitempty"`
Error string `json:"error,omitempty"`
}
// AutoConfigService orchestrates the full pipeline from user intent to deployed config.
type AutoConfigService struct {
preview *ConfigPreviewService
tasks *TaskService
registry *FeatureRegistry
configVersions *storage.ConfigVersionsRepo
}
// NewAutoConfigService creates a new AutoConfigService.
func NewAutoConfigService(preview *ConfigPreviewService, tasks *TaskService, registry *FeatureRegistry) *AutoConfigService {
return &AutoConfigService{
preview: preview,
tasks: tasks,
registry: registry,
}
}
func (s *AutoConfigService) SetConfigVersionsRepo(repo *storage.ConfigVersionsRepo) {
s.configVersions = repo
}
// BuildPipeline executes the full auto-config pipeline for a single device:
// 1. Resolve template from feature selection
// 2. If composed, generate the composed template
// 3. Create/update the scene template (profile)
// 4. Create/update recognition units for each video source
// 5. Create/update the device assignment
// 6. Render the final config
// 7. Create a deploy task
//
// If deploy is false, steps 6-7 are skipped (dry-run mode).
func (s *AutoConfigService) BuildPipeline(req AutoConfigRequest, deploy bool) (*AutoConfigResult, error) {
result := &AutoConfigResult{DeviceID: req.DeviceID}
// ---- 1. Resolve template ----
cleanSources := cleanSourceNames(req.SourceNames)
if len(cleanSources) == 0 {
result.Error = "未分配任何视频源"
return result, fmt.Errorf(result.Error)
}
resolveResult, err := s.registry.ResolveTemplate(req.Features)
if err != nil {
result.Error = err.Error()
return result, err
}
templateName := resolveResult.TemplateName
result.Composed = resolveResult.Composed
// ---- 1.5. Clone standard template to user-editable copy ----
if strings.HasPrefix(templateName, "std_") {
userName := strings.TrimPrefix(templateName, "std_")
if _, _, err := s.preview.readAssetJSON("templates", userName); err != nil {
raw, _, err := s.preview.readAssetJSON("templates", templateName)
if err != nil {
result.Error = fmt.Sprintf("读取标准模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
raw["name"] = userName
raw["description"] = "从 " + templateName + " 复制"
delete(raw, "source")
body, err := marshalConfigJSON(raw)
if err != nil {
result.Error = fmt.Sprintf("序列化模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
if err := s.preview.SaveTemplateAsset(userName, stringValue(raw["description"]), string(body)); err != nil {
result.Error = fmt.Sprintf("保存用户模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
}
templateName = userName
}
// ---- 2. Compose template if needed ----
if resolveResult.Composed {
composed, err := s.composeFromFeatureTemplates(req.Features, resolveResult.composePrimary, resolveResult.composeFeatures)
if err != nil {
result.Error = fmt.Sprintf("模板组合失败: %v", err)
return result, fmt.Errorf(result.Error)
}
body, err := marshalConfigJSON(composed)
if err != nil {
result.Error = fmt.Sprintf("序列化组合模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
if err := s.preview.SaveTemplateAsset(templateName, "自动组合: "+strings.Join(req.Features, "+"), string(body)); err != nil {
result.Error = fmt.Sprintf("保存组合模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
}
result.TemplateName = templateName
// ---- 3. Ensure profile (scene template) ----
profileName := "auto_" + req.DeviceID
editor := ConfigProfileEditor{
Name: profileName,
PrimaryTemplateName: templateName,
BusinessName: req.DeviceID + " 自动配置",
Description: "管控台一键生成",
}
// ---- 4. Create recognition units for each video source ----
editor.Instances = editor.Instances[:0] // clear previous instances on re-deploy
usedNames := map[string]bool{}
currentUnitRefs := make(map[string]bool)
for _, srcName := range cleanSources {
unitName := deriveSafeInstanceName(srcName)
baseName := unitName
for i := 1; usedNames[unitName]; i++ {
unitName = fmt.Sprintf("%s-%d", baseName, i)
}
usedNames[unitName] = true
channelNo := srcName
// Preserve user-set channelNo if already customized.
if existing, err := s.preview.GetRecognitionUnit(recognitionUnitRef(profileName, unitName)); err == nil && existing != nil {
if strings.TrimSpace(existing.OutputChannel) != "" && existing.OutputChannel != existing.Name && existing.OutputChannel != srcName {
channelNo = existing.OutputChannel
}
}
inst := ConfigProfileInstanceEditor{
Name: unitName,
Template: templateName,
VideoSourceRef: srcName,
DisplayName: srcName,
ChannelNo: channelNo,
InputBindings: map[string]InputBindingEditor{
"video_input_main": {VideoSourceRef: srcName},
},
OutputBindings: map[string]OutputBindingEditor{
"stream_output_main": {
PublishHLSPath: "./web/hls/" + unitName + "/index.m3u8",
PublishRTSPPort: "8555",
PublishRTSPPath: "/live/" + unitName,
ChannelNo: channelNo,
},
},
}
editor.Instances = append(editor.Instances, inst)
currentUnitRefs[recognitionUnitRef(profileName, unitName)] = true
}
if err := s.preview.SaveProfileEditor(editor); err != nil {
result.Error = fmt.Sprintf("保存场景模板失败: %v", err)
return result, fmt.Errorf(result.Error)
}
// ---- 4b. Save recognition units via the dedicated API ----
for _, inst := range editor.Instances {
unit := RecognitionUnitAsset{
SceneTemplateName: profileName,
Name: inst.Name,
DisplayName: inst.DisplayName,
VideoSourceRef: inst.VideoSourceRef,
OutputChannel: inst.ChannelNo,
RTSPPort: inst.PublishRTSPPort,
}
ref := recognitionUnitRef(profileName, inst.Name)
if err := s.preview.SaveRecognitionUnit(unit, ref); err != nil {
result.Error = fmt.Sprintf("保存识别单元 %s 失败: %v", inst.Name, err)
return result, fmt.Errorf(result.Error)
}
result.UnitsCreated++
}
// ---- 4c. Delete orphaned recognition units no longer in use ----
allUnits, _ := s.preview.ListRecognitionUnits()
for _, u := range allUnits {
if u.SceneTemplateName == profileName && !currentUnitRefs[u.Ref] {
_ = s.preview.DeleteRecognitionUnit(u.Ref)
}
}
// ---- 5. Device assignment ----
unitRefs := make([]string, 0, len(cleanSources))
for _, inst := range editor.Instances {
unitRefs = append(unitRefs, recognitionUnitRef(profileName, inst.Name))
}
assignment := DeviceAssignmentAsset{
DeviceID: req.DeviceID,
ProfileName: profileName,
Description: "管控台自动分配",
RecognitionUnits: unitRefs,
}
if err := s.preview.SaveDeviceAssignment(assignment); err != nil {
result.Error = fmt.Sprintf("保存设备分配失败: %v", err)
return result, fmt.Errorf(result.Error)
}
if !deploy {
preview, err := s.preview.RenderDeviceAssignment(req.DeviceID, req.Overlays)
if err != nil {
result.Error = fmt.Sprintf("配置渲染失败: %v", err)
return result, fmt.Errorf(result.Error)
}
result.ConfigSHA256 = preview.Sha256
return result, nil
}
// ---- 6 & 7. Render and deploy ----
preview, err := s.preview.RenderDeviceAssignment(req.DeviceID, req.Overlays)
if err != nil {
result.Error = fmt.Sprintf("配置渲染失败: %v", err)
return result, fmt.Errorf(result.Error)
}
result.ConfigSHA256 = preview.Sha256
if s.configVersions != nil {
_ = s.configVersions.Save(req.DeviceID, preview.Sha256, preview.JSON)
}
var configDoc any
if err := json.Unmarshal([]byte(preview.JSON), &configDoc); err != nil {
result.Error = fmt.Sprintf("配置 JSON 无效: %v", err)
return result, fmt.Errorf(result.Error)
}
if configMap, ok := configDoc.(map[string]any); ok {
InjectIntegrationsIntoConfig(configMap, s.preview)
}
task, err := s.tasks.CreateTask("config_apply", []string{req.DeviceID}, map[string]any{"config": configDoc})
if err != nil {
result.Error = fmt.Sprintf("创建下发任务失败: %v", err)
return result, fmt.Errorf(result.Error)
}
result.TaskID = task.ID
return result, nil
}
// BuildPipelineBatch executes the pipeline for multiple devices.
func (s *AutoConfigService) BuildPipelineBatch(requests []AutoConfigRequest, deploy bool) ([]AutoConfigResult, error) {
results := make([]AutoConfigResult, 0, len(requests))
var firstErr error
for _, req := range requests {
result, err := s.BuildPipeline(req, deploy)
if result != nil {
results = append(results, *result)
}
if err != nil && firstErr == nil {
firstErr = err
}
}
return results, firstErr
}
// composeFromFeatureTemplates loads templates for each feature and composes them.
func (s *AutoConfigService) composeFromFeatureTemplates(featureKeys []string, primaryName string, allFeatures []string) (map[string]any, error) {
primaryRaw, _, err := s.preview.readAssetJSON("templates", primaryName)
if err != nil {
return nil, fmt.Errorf("load primary template %q: %w", primaryName, err)
}
templates := []map[string]any{primaryRaw}
for _, key := range featureKeys {
if def := s.registry.Feature(key); def != nil {
for _, tplName := range def.Templates {
if tplName == primaryName {
continue
}
raw, _, err := s.preview.readAssetJSON("templates", tplName)
if err != nil {
continue
}
templates = append(templates, raw)
break
}
}
}
if len(templates) <= 1 {
return primaryRaw, nil
}
return ComposeTemplates(templates)
}
// ---- Device feature persistence ----
// GetDeviceFeatures returns the persisted feature keys for a device.
func (s *ConfigPreviewService) GetDeviceFeatures(deviceID string) ([]string, error) {
if s == nil || s.assets == nil {
return nil, nil
}
rec, err := s.assets.GetDeviceFeatures(deviceID)
if err != nil || rec == nil {
return nil, err
}
var features []string
if err := json.Unmarshal([]byte(rec.FeaturesJSON), &features); err != nil {
return nil, nil
}
return features, nil
}
// SaveDeviceFeatures persists the feature keys for a device.
func (s *ConfigPreviewService) SaveDeviceFeatures(deviceID string, features []string) error {
if s == nil || s.assets == nil {
return fmt.Errorf("asset repository is not configured")
}
if len(features) == 0 {
features = []string{}
}
body, err := json.Marshal(features)
if err != nil {
return err
}
return s.assets.SaveDeviceFeatures(deviceID, string(body))
}
// SaveDeviceOverlays persists the overlay names for a device.
func (s *ConfigPreviewService) SaveDeviceOverlays(deviceID string, overlays []string) error {
if s == nil || s.assets == nil {
return nil
}
if len(overlays) == 0 {
overlays = []string{}
}
body, err := json.Marshal(overlays)
if err != nil {
return err
}
return s.assets.SaveDeviceOverlays(deviceID, string(body))
}
// GetDeviceOverlays returns the persisted overlay names for a device.
func (s *ConfigPreviewService) GetDeviceOverlays(deviceID string) ([]string, error) {
if s == nil || s.assets == nil {
return nil, nil
}
rec, err := s.assets.GetDeviceOverlays(deviceID)
if err != nil || rec == "" {
return nil, err
}
var overlays []string
if err := json.Unmarshal([]byte(rec), &overlays); err != nil {
return nil, nil
}
return overlays, nil
}
func cleanSourceNames(names []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(names))
for _, n := range names {
n = strings.TrimSpace(n)
if n == "" {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
out = append(out, n)
}
sort.Strings(out)
return out
}
// deriveSafeInstanceName converts a user-facing video source name into a valid
// recognition unit instance name ([A-Za-z0-9_.-]+). If the source name is already
// valid, it's used as-is. Otherwise, a unique hash-based name is generated.
func deriveSafeInstanceName(srcName string) string {
// If already valid per validateConfigName rules, use as-is.
if safeConfigName.MatchString(srcName) {
return srcName
}
// For non-ASCII names, use a hash prefix to guarantee uniqueness
// (extracting digits/ASCII from Chinese names causes collisions e.g. "5跨主通道" and "5跨焙烧区" both → "5").
sum := sha256.Sum256([]byte(srcName))
return "src_" + strings.ToLower(hex.EncodeToString(sum[:4]))
}
// InjectIntegrationsIntoConfig injects alarm/storage URLs into the config.
func InjectIntegrationsIntoConfig(config map[string]any, preview *ConfigPreviewService) {
if preview == nil {
return
}
integrations, err := preview.ListIntegrationServices()
if err != nil {
return
}
var alarmCfg *AlarmServiceConfig
var storageCfg *ObjectStorageConfig
for _, item := range integrations {
if !item.Enabled {
continue
}
switch item.Type {
case "alarm_service":
if item.AlarmService != nil {
alarmCfg = item.AlarmService
}
case "object_storage":
if item.ObjectStorage != nil {
storageCfg = item.ObjectStorage
}
}
}
if alarmCfg == nil && storageCfg == nil {
return
}
templates, _ := config["templates"].(map[string]any)
if templates == nil {
return
}
for _, tpl := range templates {
tplMap, _ := tpl.(map[string]any)
if tplMap == nil {
continue
}
nodes, _ := tplMap["nodes"].([]any)
for _, n := range nodes {
node, _ := n.(map[string]any)
if node == nil || node["type"] != "alarm" {
continue
}
actions, _ := node["actions"].(map[string]any)
if actions == nil {
continue
}
if storageCfg != nil {
for _, key := range []string{"snapshot", "clip"} {
action, _ := actions[key].(map[string]any)
if action == nil {
action = map[string]any{}
actions[key] = action
}
upload, _ := action["upload"].(map[string]any)
if upload == nil {
upload = map[string]any{"type": "minio"}
action["upload"] = upload
}
if storageCfg.Endpoint != "" {
upload["endpoint"] = storageCfg.Endpoint
}
if storageCfg.Bucket != "" {
upload["bucket"] = storageCfg.Bucket
}
if storageCfg.AccessKey != "" {
upload["access_key"] = storageCfg.AccessKey
}
if storageCfg.SecretKey != "" {
upload["secret_key"] = storageCfg.SecretKey
}
}
}
if alarmCfg != nil {
extAPI, _ := actions["external_api"].(map[string]any)
if extAPI == nil {
extAPI = map[string]any{"enable": true}
actions["external_api"] = extAPI
}
if alarmCfg.GetTokenURL != "" {
extAPI["getTokenUrl"] = alarmCfg.GetTokenURL
}
if alarmCfg.PutMessageURL != "" {
extAPI["putMessageUrl"] = alarmCfg.PutMessageURL
}
if alarmCfg.TenantCode != "" {
extAPI["tenantCode"] = alarmCfg.TenantCode
}
setDefault(extAPI, "timeout_ms", float64(3000))
setDefault(extAPI, "include_media_url", true)
setDefault(extAPI, "token_header", "X-Access-Token")
setDefault(extAPI, "token_json_path", "responseBody.token")
setDefault(extAPI, "token_cache_sec", float64(1200))
}
// Always add agent alarm reporting (http action to local agent)
if _, ok := actions["http"]; !ok {
actions["http"] = map[string]any{
"enable": true,
"url": "http://127.0.0.1:9100/v1/alarms/report",
"method": "POST",
"timeout_ms": 3000,
}
}
}
}
}
func setDefault(m map[string]any, key string, val any) {
if _, ok := m[key]; !ok {
m[key] = val
}
}