package service import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "3588AdminBackend/internal/storage" ) var legacyBuiltinTemplateAliases = map[string]string{ "workshop_face_shoe_alarm": "std_workshop_face_recognition_shoe_alarm", "std_service_smoke_stream": "std_service_test_stream", } type ConfigTemplateAsset struct { Name string `json:"name"` Path string `json:"path"` Origin string `json:"origin"` ReadOnly bool `json:"read_only"` Description string `json:"description"` Source string `json:"source"` Slots TemplateSlotGroup `json:"slots"` NodeCount int `json:"node_count"` EdgeCount int `json:"edge_count"` MinIOEndpoint string `json:"minio_endpoint"` MinIOBucket string `json:"minio_bucket"` ExternalGetTokenURL string `json:"external_get_token_url"` ExternalPutMessageURL string `json:"external_put_message_url"` TenantCode string `json:"tenant_code"` AdvancedParams map[string]any `json:"advanced_params"` Raw map[string]any `json:"raw"` } type ConfigProfileAsset struct { Name string `json:"name"` Path string `json:"path"` Description string `json:"description"` BusinessName string `json:"business_name"` QueueSize int `json:"queue_size"` QueueStrategy string `json:"queue_strategy"` Instances []ConfigProfileInstanceAsset `json:"instances"` Raw map[string]any `json:"raw"` } type ConfigProfileInstanceAsset struct { Name string `json:"name"` Template string `json:"template"` VideoSourceRef string `json:"video_source_ref"` DisplayName string `json:"display_name"` DeviceCode string `json:"device_code"` SiteName string `json:"site_name"` PublishHLSPath string `json:"publish_hls_path"` PublishRTSPPort string `json:"publish_rtsp_port"` PublishRTSPPath string `json:"publish_rtsp_path"` ChannelNo string `json:"channel_no"` AdvancedParams map[string]any `json:"advanced_params"` } type ConfigOverlayAsset struct { Name string `json:"name"` Path string `json:"path"` Description string `json:"description"` OverrideTargets []string `json:"override_targets"` OverrideTargetNum int `json:"override_target_num"` Raw map[string]any `json:"raw"` } type ConfigIntegrationServiceAsset struct { Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` TypeLabel string `json:"type_label"` Description string `json:"description"` Enabled bool `json:"enabled"` AddressSummary string `json:"address_summary"` RefCount int `json:"ref_count"` ObjectStorage *ObjectStorageConfig `json:"object_storage,omitempty"` TokenService *TokenServiceConfig `json:"token_service,omitempty"` AlarmService *AlarmServiceConfig `json:"alarm_service,omitempty"` Raw map[string]any `json:"raw"` } type ObjectStorageConfig struct { Endpoint string `json:"endpoint"` Bucket string `json:"bucket"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` } type TokenServiceConfig struct { GetTokenURL string `json:"get_token_url"` Username string `json:"username"` Password string `json:"password"` TenantCode string `json:"tenant_code"` } type AlarmServiceConfig struct { PutMessageURL string `json:"put_message_url"` Username string `json:"username"` Password string `json:"password"` TenantCode string `json:"tenant_code"` } type ConfigVideoSourceAsset struct { Name string `json:"name"` Path string `json:"path"` SourceType string `json:"source_type"` SourceTypeLabel string `json:"source_type_label"` Area string `json:"area"` Description string `json:"description"` RefCount int `json:"ref_count"` SourceSummary string `json:"source_summary"` Config VideoSourceConfig `json:"config"` Raw map[string]any `json:"raw"` } type VideoSourceConfig struct { URL string `json:"url"` Resolution string `json:"resolution"` FrameSize string `json:"frame_size"` FPS string `json:"fps"` VideoFormat string `json:"video_format"` FocalLength string `json:"focal_length"` MountHeight string `json:"mount_height"` MountAngle string `json:"mount_angle"` } func (s *ConfigPreviewService) ListTemplateAssets() ([]ConfigTemplateAsset, error) { items := make([]ConfigTemplateAsset, 0) seen := map[string]bool{} if s != nil && s.assets != nil { records, err := s.assets.ListTemplates() if err != nil { return nil, err } for _, record := range records { name := strings.TrimSpace(record.Name) if name == "" || seen[name] || legacyBuiltinTemplateAliases[name] != "" { continue } readOnly := isStandardTemplateName(name) origin := "user" if readOnly { origin = "standard" } item, err := s.templateAssetFromRecord(record, origin, readOnly) if err != nil { continue } items = append(items, *item) seen[item.Name] = true } } sort.Slice(items, func(i, j int) bool { if items[i].ReadOnly != items[j].ReadOnly { return items[i].ReadOnly } return items[i].Name < items[j].Name }) return items, nil } func (s *ConfigPreviewService) GetTemplateAsset(name string) (*ConfigTemplateAsset, error) { name = canonicalTemplateAssetName(name) if err := validateConfigName(name); err != nil { return nil, err } if s != nil && s.assets != nil { record, err := s.assets.GetTemplate(name) if err != nil { return nil, err } if record != nil { readOnly := isStandardTemplateName(name) origin := "user" if readOnly { origin = "standard" } return s.templateAssetFromRecord(*record, origin, readOnly) } } return nil, os.ErrNotExist } func (s *ConfigPreviewService) SaveTemplateAsset(name string, description string, bodyJSON string) error { if s == nil || s.assets == nil { return fmt.Errorf("asset repository is not configured") } name = canonicalTemplateAssetName(name) if err := validateConfigName(name); err != nil { return err } if isStandardTemplateName(name) { return fmt.Errorf("standard template %q is read-only; please copy it before editing", name) } return s.assets.SaveTemplate(name, description, bodyJSON) } func (s *ConfigPreviewService) RenameTemplateAsset(oldName string, newName string, description string, bodyJSON string) error { if s == nil || s.assets == nil { return fmt.Errorf("asset repository is not configured") } oldName = canonicalTemplateAssetName(oldName) newName = canonicalTemplateAssetName(newName) if err := validateConfigName(oldName); err != nil { return err } if err := validateConfigName(newName); err != nil { return err } if isStandardTemplateName(oldName) { return fmt.Errorf("standard template %q is read-only; please copy it before editing", oldName) } if oldName != newName && isStandardTemplateName(newName) { return fmt.Errorf("standard template name %q is reserved", newName) } return s.assets.RenameTemplate(oldName, newName, description, bodyJSON) } func (s *ConfigPreviewService) DeleteTemplateAsset(name string) error { if s == nil || s.assets == nil { return fmt.Errorf("asset repository is not configured") } name = canonicalTemplateAssetName(name) if err := validateConfigName(name); err != nil { return err } if isStandardTemplateName(name) { return fmt.Errorf("standard template %q is read-only and cannot be deleted", name) } refs, err := s.profileNamesReferencingTemplate(name) if err != nil { return err } if len(refs) > 0 { return fmt.Errorf("template %q is used by business configs: %s", name, strings.Join(refs, ", ")) } return s.assets.DeleteTemplate(name) } func (s *ConfigPreviewService) ListProfileAssets() ([]ConfigProfileAsset, error) { sources, err := s.ListSources() if err != nil { return nil, err } items := make([]ConfigProfileAsset, 0, len(sources.Profiles)) for _, source := range sources.Profiles { item, err := s.GetProfileAsset(source.Name) if err != nil { continue } items = append(items, *item) } return items, nil } func (s *ConfigPreviewService) profileNamesReferencingTemplate(templateName string) ([]string, error) { if s == nil || s.assets == nil { return nil, nil } records, err := s.assets.ListProfiles() if err != nil { return nil, err } refs := make([]string, 0) for _, record := range records { if strings.TrimSpace(record.TemplateName) == templateName || strings.Contains(record.BodyJSON, `"`+"template"+`": "`+templateName+`"`) { refs = append(refs, record.Name) } } sort.Strings(refs) return refs, nil } func (s *ConfigPreviewService) GetProfileAsset(name string) (*ConfigProfileAsset, error) { raw, path, err := s.readAssetJSON("profiles", name) if err != nil { return nil, err } queueMap, _ := raw["queue"].(map[string]any) instancesRaw, _ := raw["instances"].([]any) instances := make([]ConfigProfileInstanceAsset, 0, len(instancesRaw)) for _, item := range instancesRaw { instanceMap, _ := item.(map[string]any) paramsMap, _ := instanceMap["params"].(map[string]any) sceneMeta, _ := instanceMap["scene_meta"].(map[string]any) inputBindings, _ := instanceMap["input_bindings"].(map[string]any) outputBindings, _ := instanceMap["output_bindings"].(map[string]any) advanced := cloneMap(paramsMap) for _, key := range []string{ "display_name", "device_code", "site_name", "video_source_ref", "publish_hls_path", "publish_rtsp_port", "publish_rtsp_path", "channel_no", } { delete(advanced, key) } if len(advanced) == 0 { advanced = nil } instances = append(instances, ConfigProfileInstanceAsset{ Name: stringValue(instanceMap["name"]), Template: stringValue(instanceMap["template"]), VideoSourceRef: bindingField(inputBindings, "video_input_main", "video_source_ref"), DisplayName: stringValue(sceneMeta["display_name"]), DeviceCode: stringValue(sceneMeta["device_code"]), SiteName: stringValue(sceneMeta["site_name"]), PublishHLSPath: bindingField(outputBindings, "stream_output_main", "publish_hls_path"), PublishRTSPPort: valueString(bindingAny(outputBindings, "stream_output_main", "publish_rtsp_port")), PublishRTSPPath: bindingField(outputBindings, "stream_output_main", "publish_rtsp_path"), ChannelNo: bindingField(outputBindings, "stream_output_main", "channel_no"), AdvancedParams: advanced, }) } return &ConfigProfileAsset{ Name: firstString(raw["name"], name), Path: path, Description: stringValue(raw["description"]), BusinessName: stringValue(raw["business_name"]), QueueSize: intValue(queueMap["size"]), QueueStrategy: stringValue(queueMap["strategy"]), Instances: instances, Raw: raw, }, nil } func (s *ConfigPreviewService) ListOverlayAssets() ([]ConfigOverlayAsset, error) { sources, err := s.ListSources() if err != nil { return nil, err } items := make([]ConfigOverlayAsset, 0, len(sources.Overlays)) for _, source := range sources.Overlays { item, err := s.GetOverlayAsset(source.Name) if err != nil { continue } items = append(items, *item) } return items, nil } func (s *ConfigPreviewService) GetOverlayAsset(name string) (*ConfigOverlayAsset, error) { raw, path, err := s.readAssetJSON("overlays", name) if err != nil { return nil, err } targets := make([]string, 0) if overrides, ok := raw["instance_overrides"].(map[string]any); ok { for key := range overrides { targets = append(targets, key) } sort.Strings(targets) } return &ConfigOverlayAsset{ Name: name, Path: path, Description: stringValue(raw["description"]), OverrideTargets: targets, OverrideTargetNum: len(targets), Raw: raw, }, nil } func (s *ConfigPreviewService) ListIntegrationServices() ([]ConfigIntegrationServiceAsset, error) { if s == nil || s.assets == nil { return nil, nil } records, err := s.assets.ListIntegrationServices() if err != nil { return nil, err } items := make([]ConfigIntegrationServiceAsset, 0, len(records)) for _, record := range records { item, err := integrationServiceAssetFromRecord(record) if err != nil { continue } if refs, err := s.profileNamesReferencingIntegrationService(item.Name); err == nil { item.RefCount = len(refs) } items = append(items, *item) } sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) return items, nil } func (s *ConfigPreviewService) ListVideoSources() ([]ConfigVideoSourceAsset, error) { if s == nil || s.assets == nil { return nil, nil } records, err := s.assets.ListVideoSources() if err != nil { return nil, err } items := make([]ConfigVideoSourceAsset, 0, len(records)) for _, record := range records { item, err := videoSourceAssetFromRecord(record) if err != nil { continue } if refs, err := s.profileNamesReferencingVideoSource(item.Name); err == nil { item.RefCount = len(refs) } items = append(items, *item) } sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) return items, nil } func (s *ConfigPreviewService) GetVideoSource(name string) (*ConfigVideoSourceAsset, error) { if s == nil || s.assets == nil { return nil, fmt.Errorf("基础配置仓库未初始化") } if err := validateVideoSourceName(name); err != nil { return nil, err } record, err := s.assets.GetVideoSource(name) if err != nil { return nil, err } if record == nil { return nil, os.ErrNotExist } item, err := videoSourceAssetFromRecord(*record) if err != nil { return nil, err } if refs, err := s.profileNamesReferencingVideoSource(item.Name); err == nil { item.RefCount = len(refs) } return item, nil } func (s *ConfigPreviewService) SaveVideoSourceAsset(asset ConfigVideoSourceAsset) error { if s == nil || s.assets == nil { return fmt.Errorf("基础配置仓库未初始化") } name := strings.TrimSpace(asset.Name) if err := validateVideoSourceName(name); err != nil { return fmt.Errorf("视频源名称不合法:%w", err) } sourceType := strings.TrimSpace(asset.SourceType) if sourceType == "" { return fmt.Errorf("视频源类型不能为空") } urlValue := strings.TrimSpace(asset.Config.URL) if urlValue == "" { return fmt.Errorf("视频源地址不能为空") } raw := map[string]any{ "name": name, "source_type": sourceType, "area": strings.TrimSpace(asset.Area), "description": strings.TrimSpace(asset.Description), "config": map[string]any{}, } configMap := raw["config"].(map[string]any) setAnyString(configMap, "url", asset.Config.URL) setAnyString(configMap, "resolution", asset.Config.Resolution) setAnyString(configMap, "frame_size", asset.Config.FrameSize) setAnyString(configMap, "fps", asset.Config.FPS) setAnyString(configMap, "video_format", asset.Config.VideoFormat) setAnyString(configMap, "focal_length", asset.Config.FocalLength) setAnyString(configMap, "mount_height", asset.Config.MountHeight) setAnyString(configMap, "mount_angle", asset.Config.MountAngle) body, err := marshalConfigJSON(raw) if err != nil { return err } return s.assets.SaveVideoSource(name, sourceType, strings.TrimSpace(asset.Area), strings.TrimSpace(asset.Description), string(body)) } func (s *ConfigPreviewService) DeleteVideoSource(name string) error { if s == nil || s.assets == nil { return fmt.Errorf("基础配置仓库未初始化") } if err := validateVideoSourceName(name); err != nil { return err } refs, err := s.profileNamesReferencingVideoSource(name) if err != nil { return err } if len(refs) > 0 { return fmt.Errorf("视频源 %q 已被场景配置引用:%s", name, strings.Join(refs, ", ")) } return s.assets.DeleteVideoSource(name) } func (s *ConfigPreviewService) GetIntegrationService(name string) (*ConfigIntegrationServiceAsset, error) { if s == nil || s.assets == nil { return nil, fmt.Errorf("asset repository is not configured") } if err := validateConfigName(name); err != nil { return nil, err } record, err := s.assets.GetIntegrationService(name) if err != nil { return nil, err } if record == nil { return nil, os.ErrNotExist } item, err := integrationServiceAssetFromRecord(*record) if err != nil { return nil, err } if refs, err := s.profileNamesReferencingIntegrationService(item.Name); err == nil { item.RefCount = len(refs) } return item, nil } func (s *ConfigPreviewService) DeleteIntegrationService(name string) error { if s == nil || s.assets == nil { return fmt.Errorf("asset repository is not configured") } if err := validateConfigName(name); err != nil { return err } refs, err := s.profileNamesReferencingIntegrationService(name) if err != nil { return err } if len(refs) > 0 { return fmt.Errorf("third-party service %q is used by scene configs: %s", name, strings.Join(refs, ", ")) } return s.assets.DeleteIntegrationService(name) } func (s *ConfigPreviewService) SaveIntegrationServiceAsset(asset ConfigIntegrationServiceAsset) error { if s == nil || s.assets == nil { return fmt.Errorf("asset repository is not configured") } name := strings.TrimSpace(asset.Name) if err := validateConfigName(name); err != nil { return fmt.Errorf("invalid third-party service name: %w", err) } serviceType := strings.TrimSpace(asset.Type) if serviceType == "" { return fmt.Errorf("third-party service type is required") } raw := map[string]any{ "name": name, "type": serviceType, "description": strings.TrimSpace(asset.Description), "enabled": asset.Enabled, } configMap := map[string]any{} switch serviceType { case "object_storage": if asset.ObjectStorage == nil { return fmt.Errorf("object storage config is required") } setAnyString(configMap, "endpoint", asset.ObjectStorage.Endpoint) setAnyString(configMap, "bucket", asset.ObjectStorage.Bucket) setAnyString(configMap, "access_key", asset.ObjectStorage.AccessKey) setAnyString(configMap, "secret_key", asset.ObjectStorage.SecretKey) for _, key := range []string{"endpoint", "bucket", "access_key", "secret_key"} { if strings.TrimSpace(stringValue(configMap[key])) == "" { return fmt.Errorf("object storage %s is required", key) } } case "token_service": if asset.TokenService == nil { return fmt.Errorf("token service config is required") } setAnyString(configMap, "get_token_url", asset.TokenService.GetTokenURL) setAnyString(configMap, "username", asset.TokenService.Username) setAnyString(configMap, "password", asset.TokenService.Password) setAnyString(configMap, "tenant_code", asset.TokenService.TenantCode) if strings.TrimSpace(stringValue(configMap["get_token_url"])) == "" { return fmt.Errorf("token service get_token_url is required") } case "alarm_service": if asset.AlarmService == nil { return fmt.Errorf("alarm service config is required") } setAnyString(configMap, "put_message_url", asset.AlarmService.PutMessageURL) setAnyString(configMap, "username", asset.AlarmService.Username) setAnyString(configMap, "password", asset.AlarmService.Password) setAnyString(configMap, "tenant_code", asset.AlarmService.TenantCode) if strings.TrimSpace(stringValue(configMap["put_message_url"])) == "" { return fmt.Errorf("alarm service put_message_url is required") } default: return fmt.Errorf("unsupported third-party service type: %s", serviceType) } raw["config"] = configMap body, err := marshalConfigJSON(raw) if err != nil { return err } return s.assets.SaveIntegrationService(name, serviceType, strings.TrimSpace(asset.Description), asset.Enabled, string(body)) } func (s *ConfigPreviewService) profileNamesReferencingIntegrationService(name string) ([]string, error) { if s == nil || s.assets == nil { return nil, nil } records, err := s.assets.ListProfiles() if err != nil { return nil, err } name = strings.TrimSpace(name) if name == "" { return nil, nil } refs := make([]string, 0) for _, record := range records { var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { continue } if raw == nil { continue } instances, _ := raw["instances"].([]any) for _, item := range instances { instanceMap, _ := item.(map[string]any) serviceBindings, _ := instanceMap["service_bindings"].(map[string]any) found := false for _, slot := range []string{"object_storage_main", "token_service_main", "alarm_service_main"} { if strings.TrimSpace(bindingField(serviceBindings, slot, "service_ref")) == name { refs = append(refs, record.Name) found = true break } } if found { break } } } sort.Strings(refs) return refs, nil } func (s *ConfigPreviewService) profileNamesReferencingVideoSource(name string) ([]string, error) { if s == nil || s.assets == nil { return nil, nil } records, err := s.assets.ListProfiles() if err != nil { return nil, err } name = strings.TrimSpace(name) if name == "" { return nil, nil } refs := make([]string, 0) for _, record := range records { var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { continue } instances, _ := raw["instances"].([]any) for _, item := range instances { instanceMap, _ := item.(map[string]any) inputBindings, _ := instanceMap["input_bindings"].(map[string]any) if strings.TrimSpace(bindingField(inputBindings, "video_input_main", "video_source_ref")) == name { refs = append(refs, record.Name) break } } } sort.Strings(refs) return refs, nil } func (s *ConfigPreviewService) readAssetJSON(kind string, name string) (map[string]any, string, error) { if s == nil || s.assets == nil { return nil, "", fmt.Errorf("asset repository is not configured") } raw, path, ok, err := s.readRepoAssetJSON(kind, name) if err != nil { return nil, "", err } if ok { return raw, path, nil } return nil, "", os.ErrNotExist } func (s *ConfigPreviewService) readRepoAssetJSON(kind string, name string) (map[string]any, string, bool, error) { if err := validateConfigName(name); err != nil { return nil, "", false, err } var ( record *storage.AssetRecord err error ) switch kind { case "templates": record, err = s.assets.GetTemplate(name) case "profiles": record, err = s.assets.GetProfile(name) case "overlays": record, err = s.assets.GetOverlay(name) default: return nil, "", false, fmt.Errorf("unsupported asset kind: %s", kind) } if err != nil { return nil, "", true, err } if record == nil { return nil, "", false, nil } var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { return nil, "", true, err } if raw == nil { raw = map[string]any{} } if strings.TrimSpace(record.Description) != "" { raw["description"] = record.Description } if kind == "profiles" { if strings.TrimSpace(record.TemplateName) != "" { raw["primary_template_name"] = record.TemplateName } if strings.TrimSpace(record.BusinessName) != "" && stringValue(raw["business_name"]) == "" { raw["business_name"] = record.BusinessName } } return raw, repoAssetPath(kind, name), true, nil } func cloneMap(in map[string]any) map[string]any { if len(in) == 0 { return map[string]any{} } out := make(map[string]any, len(in)) for k, v := range in { out[k] = v } return out } func bindingAny(bindings map[string]any, slot string, field string) any { entry, _ := bindings[slot].(map[string]any) return entry[field] } func (s *ConfigPreviewService) templateAssetFromRecord(record storage.AssetRecord, origin string, readOnly bool) (*ConfigTemplateAsset, error) { var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { return nil, err } if raw == nil { raw = map[string]any{} } if strings.TrimSpace(record.Description) != "" { raw["description"] = record.Description } return buildTemplateAsset(raw, repoAssetPath("templates", record.Name), origin, readOnly), nil } func buildTemplateAsset(raw map[string]any, path string, origin string, readOnly bool) *ConfigTemplateAsset { templateMap, _ := raw["template"].(map[string]any) paramsMap, _ := raw["params"].(map[string]any) nodes, _ := templateMap["nodes"].([]any) edges, _ := templateMap["edges"].([]any) slots, _ := parseTemplateSlots(raw) advanced := cloneMap(paramsMap) for _, key := range []string{ "minio_endpoint", "minio_bucket", "external_get_token_url", "external_put_message_url", "tenant_code", } { delete(advanced, key) } if len(advanced) == 0 { advanced = nil } return &ConfigTemplateAsset{ Name: firstString(raw["name"], filepath.Base(strings.TrimSuffix(path, filepath.Ext(path)))), Path: path, Origin: origin, ReadOnly: readOnly, Description: stringValue(raw["description"]), Source: stringValue(raw["source"]), Slots: slots, NodeCount: len(nodes), EdgeCount: len(edges), MinIOEndpoint: stringValue(paramsMap["minio_endpoint"]), MinIOBucket: stringValue(paramsMap["minio_bucket"]), ExternalGetTokenURL: stringValue(paramsMap["external_get_token_url"]), ExternalPutMessageURL: stringValue(paramsMap["external_put_message_url"]), TenantCode: valueString(paramsMap["tenant_code"]), AdvancedParams: advanced, Raw: raw, } } func integrationServiceAssetFromRecord(record storage.IntegrationServiceRecord) (*ConfigIntegrationServiceAsset, error) { var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { return nil, err } if raw == nil { raw = map[string]any{} } configMap, _ := raw["config"].(map[string]any) sourceMap := raw if len(configMap) > 0 { sourceMap = configMap } item := &ConfigIntegrationServiceAsset{ Name: firstString(raw["name"], record.Name), Path: repoAssetPath("integration_services", record.Name), Type: firstString(record.ServiceType, stringValue(raw["type"])), Description: firstString(raw["description"], record.Description), Enabled: boolValue(raw["enabled"], record.Enabled), Raw: raw, } item.TypeLabel = integrationTypeLabel(item.Type) switch item.Type { case "object_storage": item.ObjectStorage = &ObjectStorageConfig{ Endpoint: stringValue(sourceMap["endpoint"]), Bucket: stringValue(sourceMap["bucket"]), AccessKey: firstString(sourceMap["access_key"], stringValue(sourceMap["minio_access_key"])), SecretKey: firstString(sourceMap["secret_key"], stringValue(sourceMap["minio_secret_key"])), } item.AddressSummary = strings.TrimSpace(strings.Trim(strings.Join([]string{item.ObjectStorage.Endpoint, item.ObjectStorage.Bucket}, " / "), " /")) case "token_service": item.TokenService = &TokenServiceConfig{ GetTokenURL: stringValue(sourceMap["get_token_url"]), Username: stringValue(sourceMap["username"]), Password: stringValue(sourceMap["password"]), TenantCode: stringValue(sourceMap["tenant_code"]), } item.AddressSummary = item.TokenService.GetTokenURL case "alarm_service": item.AlarmService = &AlarmServiceConfig{ PutMessageURL: stringValue(sourceMap["put_message_url"]), Username: stringValue(sourceMap["username"]), Password: stringValue(sourceMap["password"]), TenantCode: stringValue(sourceMap["tenant_code"]), } item.AddressSummary = item.AlarmService.PutMessageURL } return item, nil } func videoSourceAssetFromRecord(record storage.VideoSourceRecord) (*ConfigVideoSourceAsset, error) { var raw map[string]any if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil { return nil, err } if raw == nil { raw = map[string]any{} } configMap, _ := raw["config"].(map[string]any) sourceMap := raw if len(configMap) > 0 { sourceMap = configMap } item := &ConfigVideoSourceAsset{ Name: firstString(raw["name"], record.Name), Path: repoAssetPath("video_sources", record.Name), SourceType: firstString(record.SourceType, stringValue(raw["source_type"])), Area: firstString(raw["area"], record.Area), Description: firstString(raw["description"], record.Description), SourceTypeLabel: videoSourceTypeLabel(firstString(record.SourceType, stringValue(raw["source_type"]))), Config: VideoSourceConfig{ URL: stringValue(sourceMap["url"]), Resolution: stringValue(sourceMap["resolution"]), FrameSize: stringValue(sourceMap["frame_size"]), FPS: valueString(sourceMap["fps"]), VideoFormat: stringValue(sourceMap["video_format"]), FocalLength: stringValue(sourceMap["focal_length"]), MountHeight: stringValue(sourceMap["mount_height"]), MountAngle: stringValue(sourceMap["mount_angle"]), }, Raw: raw, } item.SourceSummary = item.Config.URL return item, nil } func integrationTypeLabel(v string) string { switch strings.TrimSpace(v) { case "object_storage": return "对象存储" case "token_service": return "认证服务" case "alarm_service": return "告警服务" default: return strings.TrimSpace(v) } } func videoSourceTypeLabel(v string) string { switch strings.TrimSpace(v) { case "rtsp": return "RTSP" case "rtmp": return "RTMP" case "file": return "文件" case "usb_camera": return "USB 摄像头" default: return strings.TrimSpace(v) } } func validateVideoSourceName(name string) error { name = strings.TrimSpace(name) if name == "" { return fmt.Errorf("不能为空") } if strings.Contains(name, "..") { return fmt.Errorf("不能包含 '..'") } if strings.ContainsAny(name, `/\`) { return fmt.Errorf("不能包含 / 或 \\") } return nil } func (s *ConfigPreviewService) templateIsBuiltin(name string) bool { return isStandardTemplateName(name) } func isStandardTemplateName(name string) bool { name = canonicalTemplateAssetName(name) return strings.HasPrefix(strings.TrimSpace(name), "std_") } func canonicalTemplateAssetName(name string) string { name = strings.TrimSpace(name) if next := strings.TrimSpace(legacyBuiltinTemplateAliases[name]); next != "" { return next } return name } func stringValue(v any) string { if s, ok := v.(string); ok { return strings.TrimSpace(s) } return "" } func valueString(v any) string { switch value := v.(type) { case string: return strings.TrimSpace(value) case float64: if float64(int(value)) == value { return fmt.Sprintf("%d", int(value)) } return fmt.Sprintf("%v", value) case int: return fmt.Sprintf("%d", value) case int64: return fmt.Sprintf("%d", value) default: return "" } } func boolValue(v any, fallback bool) bool { switch value := v.(type) { case bool: return value case float64: return value != 0 case int: return value != 0 case int64: return value != 0 case string: value = strings.TrimSpace(strings.ToLower(value)) return value == "1" || value == "true" || value == "yes" || value == "on" default: return fallback } } func firstString(v any, fallback string) string { if got := stringValue(v); got != "" { return got } return fallback } func intValue(v any) int { switch value := v.(type) { case int: return value case int64: return int(value) case float64: return int(value) default: return 0 } }