diff --git a/internal/service/config_assets.go b/internal/service/config_assets.go index 9d85e38..34d4096 100644 --- a/internal/service/config_assets.go +++ b/internal/service/config_assets.go @@ -101,9 +101,8 @@ type TokenServiceConfig struct { } type AlarmServiceConfig struct { + GetTokenURL string `json:"get_token_url"` PutMessageURL string `json:"put_message_url"` - Username string `json:"username"` - Password string `json:"password"` TenantCode string `json:"tenant_code"` } @@ -1375,32 +1374,24 @@ func (s *ConfigPreviewService) SaveIntegrationServiceAsset(asset ConfigIntegrati 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, "get_token_url", asset.AlarmService.GetTokenURL) 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") + if strings.TrimSpace(stringValue(configMap["get_token_url"])) == "" && strings.TrimSpace(stringValue(configMap["put_message_url"])) == "" { + return fmt.Errorf("alarm service requires at least one of get_token_url or put_message_url") } + case "custom": + raw["config"] = asset.Raw default: return fmt.Errorf("unsupported third-party service type: %s", serviceType) } - raw["config"] = configMap + if serviceType != "custom" { + raw["config"] = configMap + } body, err := marshalConfigJSON(raw) if err != nil { return err @@ -1659,22 +1650,19 @@ func integrationServiceAssetFromRecord(record storage.IntegrationServiceRecord) 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{ + GetTokenURL: stringValue(sourceMap["get_token_url"]), PutMessageURL: stringValue(sourceMap["put_message_url"]), - Username: stringValue(sourceMap["username"]), - Password: stringValue(sourceMap["password"]), TenantCode: stringValue(sourceMap["tenant_code"]), } - item.AddressSummary = item.AlarmService.PutMessageURL + summary := strings.TrimSpace(item.AlarmService.PutMessageURL) + if summary == "" { + summary = item.AlarmService.GetTokenURL + } + item.AddressSummary = summary + case "custom": + item.AddressSummary = "自定义" } return item, nil } @@ -1719,10 +1707,10 @@ func integrationTypeLabel(v string) string { switch strings.TrimSpace(v) { case "object_storage": return "对象存储" - case "token_service": - return "认证服务" case "alarm_service": return "告警服务" + case "custom": + return "自定义" default: return strings.TrimSpace(v) } diff --git a/internal/service/config_preview.go b/internal/service/config_preview.go index 19f797f..5703732 100644 --- a/internal/service/config_preview.go +++ b/internal/service/config_preview.go @@ -359,7 +359,7 @@ func (s *ConfigPreviewService) resolveSceneBindings(raw map[string]any) (map[str expected string }{ {slot: "object_storage_main", expected: "object_storage"}, - {slot: "token_service_main", expected: "token_service"}, + {slot: "token_service_main", expected: "alarm_service"}, {slot: "alarm_service_main", expected: "alarm_service"}, } { serviceRef := bindingField(serviceBindings, binding.slot, "service_ref") @@ -405,26 +405,22 @@ func resolvedServiceBinding(asset *ConfigIntegrationServiceAsset) map[string]any "access_key": asset.ObjectStorage.AccessKey, "secret_key": asset.ObjectStorage.SecretKey, } - case "token_service": - if asset.TokenService == nil { - return nil - } - return map[string]any{ - "get_token_url": asset.TokenService.GetTokenURL, - "username": asset.TokenService.Username, - "password": asset.TokenService.Password, - "tenant_code": asset.TokenService.TenantCode, - } case "alarm_service": if asset.AlarmService == nil { return nil } return map[string]any{ + "get_token_url": asset.AlarmService.GetTokenURL, "put_message_url": asset.AlarmService.PutMessageURL, - "username": asset.AlarmService.Username, - "password": asset.AlarmService.Password, "tenant_code": asset.AlarmService.TenantCode, } + case "custom": + if cfg, ok := asset.Raw["config"]; ok { + if m, ok := cfg.(map[string]any); ok { + return m + } + } + return nil default: return nil } diff --git a/internal/service/config_preview_test.go b/internal/service/config_preview_test.go index 2fb396c..701de69 100644 --- a/internal/service/config_preview_test.go +++ b/internal/service/config_preview_test.go @@ -186,7 +186,7 @@ func TestConfigPreviewServiceRenderProfileEditorWritesResolvedBindings(t *testin t.Fatalf("SaveVideoSource: %v", err) } saveIntegrationServiceForPreviewTest(t, repo, "minio_main", "object_storage", `{"name":"minio_main","type":"object_storage","config":{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}}`) - saveIntegrationServiceForPreviewTest(t, repo, "token_main", "token_service", `{"name":"token_main","type":"token_service","config":{"get_token_url":"http://10.0.0.49:8080/api/getToken","tenant_code":"32"}}`) + saveIntegrationServiceForPreviewTest(t, repo, "token_main", "alarm_service", `{"name":"token_main","type":"alarm_service","config":{"get_token_url":"http://10.0.0.49:8080/api/getToken","tenant_code":"32"}}`) saveIntegrationServiceForPreviewTest(t, repo, "alarm_main", "alarm_service", `{"name":"alarm_main","type":"alarm_service","config":{"put_message_url":"http://10.0.0.49:8080/api/putMessage","tenant_code":"32"}}`) svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo) @@ -248,7 +248,7 @@ func TestConfigPreviewServiceRenderProfileEditorAllowsUnboundOptionalServiceSlot "source":"standard", "slots":{ "inputs":[{"name":"video_input_main","type":"video_source","required":true,"description":"主视频输入"}], - "services":[{"name":"token_service_main","type":"token_service","required":false,"description":"认证服务"}], + "services":[{"name":"token_service_main","type":"alarm_service","required":false,"description":"告警服务"}], "outputs":[{"name":"stream_output_main","type":"stream_publish","required":true,"description":"主视频输出"}] }, "template":{ diff --git a/internal/web/ui.go b/internal/web/ui.go index 85843cc..87d1062 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -131,6 +131,7 @@ type PageData struct { TemplateCloneDesc string TemplateCreateMode string OverlayDraftJSON string + IntegrationConfigDraft string AuditEntries []storage.AuditLogRecord PersistedConfig *storage.DeviceConfigStateRecord DBPath string @@ -419,8 +420,6 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic return "视频源" case "object_storage": return "对象存储" - case "token_service": - return "认证服务" case "alarm_service": return "告警服务" case "stream_publish": @@ -2468,6 +2467,16 @@ func (u *UI) pageAssetIntegrations(w http.ResponseWriter, r *http.Request) { } else { data.AssetIntegrationEditing = newMode || editMode } + if data.AssetIntegration != nil && data.AssetIntegration.Type == "custom" { + if configRaw, ok := data.AssetIntegration.Raw["config"]; ok { + if s, err := compactJSON(configRaw); err == nil { + data.IntegrationConfigDraft = s + } + } + if data.IntegrationConfigDraft == "" { + data.IntegrationConfigDraft = "{}" + } + } u.render(w, r, "assets", data) } @@ -2478,29 +2487,34 @@ func (u *UI) actionAssetIntegrationSave(w http.ResponseWriter, r *http.Request) } _ = r.ParseForm() enabled := strings.TrimSpace(r.FormValue("enabled")) == "1" || strings.EqualFold(strings.TrimSpace(r.FormValue("enabled")), "true") || strings.EqualFold(strings.TrimSpace(r.FormValue("enabled")), "on") + serviceType := strings.TrimSpace(r.FormValue("type")) asset := service.ConfigIntegrationServiceAsset{ Name: strings.TrimSpace(r.FormValue("name")), - Type: strings.TrimSpace(r.FormValue("type")), + Type: serviceType, Description: strings.TrimSpace(r.FormValue("description")), Enabled: enabled, - ObjectStorage: &service.ObjectStorageConfig{ - Endpoint: strings.TrimSpace(r.FormValue("endpoint")), - Bucket: strings.TrimSpace(r.FormValue("bucket")), - AccessKey: strings.TrimSpace(r.FormValue("access_key")), - SecretKey: strings.TrimSpace(r.FormValue("secret_key")), - }, - TokenService: &service.TokenServiceConfig{ - GetTokenURL: strings.TrimSpace(r.FormValue("get_token_url")), - Username: strings.TrimSpace(r.FormValue("username")), - Password: strings.TrimSpace(r.FormValue("password")), - TenantCode: strings.TrimSpace(r.FormValue("tenant_code")), - }, - AlarmService: &service.AlarmServiceConfig{ - PutMessageURL: strings.TrimSpace(r.FormValue("put_message_url")), - Username: strings.TrimSpace(r.FormValue("alarm_username")), - Password: strings.TrimSpace(r.FormValue("alarm_password")), - TenantCode: strings.TrimSpace(r.FormValue("alarm_tenant_code")), - }, + ObjectStorage: &service.ObjectStorageConfig{}, + AlarmService: &service.AlarmServiceConfig{}, + } + if serviceType == "custom" { + configRaw := strings.TrimSpace(r.FormValue("config_json")) + if configRaw == "" { + configRaw = "{}" + } + var raw map[string]any + if err := json.Unmarshal([]byte(configRaw), &raw); err != nil { + http.Redirect(w, r, "/ui/assets/integrations?error="+urlQueryEscape("自定义配置 JSON 格式不正确:"+err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) + return + } + asset.Raw = raw + } else { + asset.ObjectStorage.Endpoint = strings.TrimSpace(r.FormValue("endpoint")) + asset.ObjectStorage.Bucket = strings.TrimSpace(r.FormValue("bucket")) + asset.ObjectStorage.AccessKey = strings.TrimSpace(r.FormValue("access_key")) + asset.ObjectStorage.SecretKey = strings.TrimSpace(r.FormValue("secret_key")) + asset.AlarmService.GetTokenURL = strings.TrimSpace(r.FormValue("get_token_url")) + asset.AlarmService.PutMessageURL = strings.TrimSpace(r.FormValue("put_message_url")) + asset.AlarmService.TenantCode = strings.TrimSpace(r.FormValue("tenant_code")) } if err := u.preview.SaveIntegrationServiceAsset(asset); err != nil { http.Redirect(w, r, "/ui/assets/integrations?error="+urlQueryEscape(err.Error())+"&name="+url.PathEscape(asset.Name), http.StatusFound) diff --git a/internal/web/ui/templates/assets.html b/internal/web/ui/templates/assets.html index 3811e92..2055565 100644 --- a/internal/web/ui/templates/assets.html +++ b/internal/web/ui/templates/assets.html @@ -98,35 +98,40 @@ -
- - - - +
+

对象存储配置

+
+ + + + +
-
- - - - +
+

告警服务配置

+
+ + + +
-
- - - - +
+

自定义配置

+
+ +
@@ -139,21 +144,38 @@
服务类型{{.AssetIntegration.TypeLabel}}
描述{{if .AssetIntegration.Description}}{{.AssetIntegration.Description}}{{else}}-{{end}}
启用状态{{if .AssetIntegration.Enabled}}启用{{else}}停用{{end}}
+ {{if eq .AssetIntegration.Type "object_storage"}}
对象存储地址{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.Endpoint}}{{.AssetIntegration.ObjectStorage.Endpoint}}{{else}}-{{end}}
Bucket{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.Bucket}}{{.AssetIntegration.ObjectStorage.Bucket}}{{else}}-{{end}}
Access Key{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.AccessKey}}{{.AssetIntegration.ObjectStorage.AccessKey}}{{else}}-{{end}}
Secret Key{{if and .AssetIntegration.ObjectStorage .AssetIntegration.ObjectStorage.SecretKey}}{{.AssetIntegration.ObjectStorage.SecretKey}}{{else}}-{{end}}
-
Token 获取地址{{if and .AssetIntegration.TokenService .AssetIntegration.TokenService.GetTokenURL}}{{.AssetIntegration.TokenService.GetTokenURL}}{{else}}-{{end}}
-
认证用户名{{if and .AssetIntegration.TokenService .AssetIntegration.TokenService.Username}}{{.AssetIntegration.TokenService.Username}}{{else}}-{{end}}
-
认证租户编码{{if and .AssetIntegration.TokenService .AssetIntegration.TokenService.TenantCode}}{{.AssetIntegration.TokenService.TenantCode}}{{else}}-{{end}}
+ {{end}} + {{if eq .AssetIntegration.Type "alarm_service"}} +
Token 获取地址{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.GetTokenURL}}{{.AssetIntegration.AlarmService.GetTokenURL}}{{else}}-{{end}}
消息上报地址{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.PutMessageURL}}{{.AssetIntegration.AlarmService.PutMessageURL}}{{else}}-{{end}}
-
告警用户名{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.Username}}{{.AssetIntegration.AlarmService.Username}}{{else}}-{{end}}
-
告警租户编码{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.TenantCode}}{{.AssetIntegration.AlarmService.TenantCode}}{{else}}-{{end}}
+
租户编码{{if and .AssetIntegration.AlarmService .AssetIntegration.AlarmService.TenantCode}}{{.AssetIntegration.AlarmService.TenantCode}}{{else}}-{{end}}
+ {{end}}
{{end}}
+ +{{if and (not .AssetIntegrationEditing) (eq .AssetIntegration.Type "custom")}} +
+ {{icon "tech"}}原始 JSON +
{{json .AssetIntegration.Raw}}
+
{{end}} +{{end}} + + {{else if eq .AssetTab "video-sources"}}
diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index 40c2288..67baa50 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -2327,11 +2327,11 @@ func TestUI_AssetIntegrationsPageShowsStructuredServices(t *testing.T) { defer store.Close() repo := storage.NewAssetsRepo(store.DB()) mustSaveIntegrationService(t, repo, "minio_main", "object_storage", `{"name":"minio_main","type":"object_storage","description":"主对象存储","config":{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}}`) - mustSaveIntegrationService(t, repo, "token_main", "token_service", `{"name":"token_main","type":"token_service","description":"认证","config":{"get_token_url":"http://10.0.0.49:8080/api/getToken","tenant_code":"32"}}`) + mustSaveIntegrationService(t, repo, "alarm_main", "alarm_service", `{"name":"alarm_main","type":"alarm_service","description":"告警","config":{"get_token_url":"http://10.0.0.49:8080/api/getToken","put_message_url":"http://10.0.0.49:8080/api/putMessage","tenant_code":"32"}}`) ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo) mustImportAssetsForUI(t, ui.preview) - req := httptest.NewRequest(http.MethodGet, "/ui/assets/integrations", nil) + req := httptest.NewRequest(http.MethodGet, "/ui/assets/integrations?name=alarm_main", nil) rr := httptest.NewRecorder() ui.pageAssetIntegrations(rr, req) @@ -2343,15 +2343,15 @@ func TestUI_AssetIntegrationsPageShowsStructuredServices(t *testing.T) { "编辑", "删除", "minio_main", - "token_main", + "alarm_main", "对象存储", - "认证服务", + "告警服务", "http://10.0.0.49:9000 / myminio", "1", "查看模式", - `minio_main`, + `alarm_main`, `data-nav-row`, - `data-nav-href="/ui/assets/integrations?name=minio_main"`, + `data-nav-href="/ui/assets/integrations?name=alarm_main"`, `class="selected"`, } { if !strings.Contains(body, want) {