3588AdminBackend/internal/service/config_assets.go

1018 lines
32 KiB
Go

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