From 5b0ad9a04ecc26cb8471eb1d0cd0efabbb5ab6c9 Mon Sep 17 00:00:00 2001 From: alick-liming Date: Mon, 12 Oct 2026 12:00:04 +0800 Subject: [PATCH] fix(integrations): sync public API contract and skill commands --- go.mod | 2 +- go.sum | 4 +- internal/cli/integration_contract_test.go | 137 ++++++++++++++++++++++ internal/cli/zz_generated_integrations.go | 127 ++++++++++++++------ skills/flashduty/SKILL.md | 5 +- skills/flashduty/reference/channel.md | 2 +- skills/flashduty/reference/integration.md | 107 +++++++++++++++++ 7 files changed, 339 insertions(+), 45 deletions(-) create mode 100644 internal/cli/integration_contract_test.go create mode 100644 skills/flashduty/reference/integration.md diff --git a/go.mod b/go.mod index 0db0f08..1dd831c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli go 1.26.0 require ( - github.com/flashcatcloud/go-flashduty v0.15.14 + github.com/flashcatcloud/go-flashduty v0.15.13-0.20261012034738-577bf0504904 github.com/mattn/go-runewidth v0.0.30 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index 11bf3d1..454b10f 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/flashcatcloud/go-flashduty v0.15.14 h1:185WAkgahlRnLBk3jI6kYL2t8IoH5YtQ9Pd4ckEPRWM= -github.com/flashcatcloud/go-flashduty v0.15.14/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk= +github.com/flashcatcloud/go-flashduty v0.15.13-0.20261012034738-577bf0504904 h1:5ldIZhq/PKdJaZ/CX5Pafa5hJCNcYIWFFJz2R9R+Xp0= +github.com/flashcatcloud/go-flashduty v0.15.13-0.20261012034738-577bf0504904/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= diff --git a/internal/cli/integration_contract_test.go b/internal/cli/integration_contract_test.go new file mode 100644 index 0000000..6c86bfb --- /dev/null +++ b/internal/cli/integration_contract_test.go @@ -0,0 +1,137 @@ +package cli + +import ( + "encoding/json" + "reflect" + "testing" +) + +func integrationContractFixture() map[string]any { + return map[string]any{ + "integration_id": 42, "data_source_id": 42, "account_id": 100, "exclusive_data_source_id": 0, + "name": "Contract fixture", "description": "", "team_id": 7, + "category": "im", "plugin_type": "slack_app", "plugin_type_name": "Slack", "plugin_id": 9, + "ref_id": "a_100", "status": "enabled", "created_at": 1700000000, "updated_at": 1700000001, "last_time": 0, + "creator_id": 11, "updated_by": 12, "no_editable": false, "editable": true, + "settings": map[string]any{"headers": map[string]any{"Authorization": "fixture-not-a-real-secret"}}, + "integration_key": "fixture-not-a-real-key", + } +} + +func TestIntegrationListPreservesConfigurationAndPermissions(t *testing.T) { + saveAndResetGlobals(t) + stub := newGFStub(t) + fixture := integrationContractFixture() + stub.data = map[string]any{"p": 1, "limit": 100, "total": 1, "items": []any{fixture}} + out, err := execCommand("integration", "list", "--json") + if err != nil { + t.Fatal(err) + } + var result map[string]any + if err := json.Unmarshal([]byte(out), &result); err != nil { + t.Fatal(err) + } + items, ok := result["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("unexpected list output: %s", out) + } + item := items[0].(map[string]any) + for field := range fixture { + if _, ok := item[field]; !ok { + t.Errorf("list dropped backend field %s", field) + } + } + for _, field := range []string{"settings", "integration_key", "editable", "no_editable"} { + if !reflect.DeepEqual(item[field], fixture[field]) { + t.Errorf("%s changed: got %#v, want %#v", field, item[field], fixture[field]) + } + } + if stub.lastPath != "/integration/list" { + t.Fatalf("request path = %s", stub.lastPath) + } +} + +func TestIntegrationInfoPreservesConfigurationAndTestResult(t *testing.T) { + for _, withTest := range []bool{false, true} { + t.Run(map[bool]string{false: "absent test", true: "failed test"}[withTest], func(t *testing.T) { + saveAndResetGlobals(t) + stub := newGFStub(t) + fixture := integrationContractFixture() + for _, field := range []string{"account_id", "data_source_id", "exclusive_data_source_id"} { + delete(fixture, field) + } + if withTest { + fixture["test"] = map[string]any{"success": false, "message": "fixture plugin test failed"} + } + stub.data = fixture + out, err := execCommand("integration", "info", "42", "--json") + if err != nil { + t.Fatal(err) + } + var result map[string]any + if err := json.Unmarshal([]byte(out), &result); err != nil { + t.Fatal(err) + } + for _, field := range []string{"settings", "integration_key", "plugin_id", "editable", "creator_id", "updated_by", "no_editable"} { + if _, ok := result[field]; !ok { + t.Errorf("info dropped backend field %s", field) + } + } + if !reflect.DeepEqual(result["settings"], fixture["settings"]) || result["integration_key"] != fixture["integration_key"] { + t.Errorf("info changed configuration or key: %s", out) + } + if !reflect.DeepEqual(result["test"], fixture["test"]) { + t.Errorf("test result changed: %s", out) + } + if !withTest { + if _, ok := result["test"]; ok { + t.Errorf("absent test was fabricated: %s", out) + } + } + }) + } +} + +func TestIntegrationCreatePreservesKeyAndPluginTest(t *testing.T) { + for _, mode := range []string{"data", "flags"} { + t.Run(mode, func(t *testing.T) { + saveAndResetGlobals(t) + stub := newGFStub(t) + stub.data = map[string]any{"integration_id": 42, "integration_key": "fixture-key", "test": map[string]any{"success": false, "message": "fixture plugin test failed"}} + args := []string{"integration", "create", "--json", "--data", `{"plugin_type":"standard.alert","integration_key":"00000000000000000000000000000000100","is_test":true}`} + if mode == "flags" { + args = []string{"integration", "create", "--json", "--plugin-type", "standard.alert", "--integration-key", "00000000000000000000000000000000100", "--is-test"} + } + out, err := execCommand(args...) + if err != nil { + t.Fatal(err) + } + if stub.lastBody["integration_key"] != "00000000000000000000000000000000100" || stub.lastBody["is_test"] != true { + t.Errorf("create dropped key or test flag: %#v", stub.lastBody) + } + var result map[string]any + if err := json.Unmarshal([]byte(out), &result); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(result["test"], stub.data.(map[string]any)["test"]) { + t.Errorf("create lost failed plugin test: %s", out) + } + }) + } +} + +func TestIntegrationUpdatePreservesTestAndExplicitEmptyHeaders(t *testing.T) { + saveAndResetGlobals(t) + stub := newGFStub(t) + _, err := execCommand("integration", "update", "42", "--team-id", "0", "--is-test", "--data", `{"settings":{"headers":{}}}`) + if err != nil { + t.Fatal(err) + } + if stub.lastBody["is_test"] != true || stub.lastBody["team_id"] != float64(0) { + t.Errorf("update lost test flag or explicit team clear: %#v", stub.lastBody) + } + want := map[string]any{"headers": map[string]any{}} + if !reflect.DeepEqual(stub.lastBody["settings"], want) { + t.Errorf("update changed explicit empty headers: %#v", stub.lastBody) + } +} diff --git a/internal/cli/zz_generated_integrations.go b/internal/cli/zz_generated_integrations.go index ac8b856..f675504 100644 --- a/internal/cli/zz_generated_integrations.go +++ b/internal/cli/zz_generated_integrations.go @@ -67,7 +67,7 @@ func genIntegrationsIntegrationAPIReadInfoCmd() *cobra.Command { Short: "Get integration detail", Long: `Get integration detail. -Return one integration, including its settings with sensitive values masked. +Get a public integration with its stored settings and decrypted integration key. API: POST /integration/info (integration-api-read-info) @@ -75,19 +75,28 @@ Request fields: --integration-id int (required) — Integration ID. (min 1) Response fields ('data' envelope is unwrapped — these fields are at the top level): - - category (any) (required) - - created_at (any) (required) - - description (any) (required) - - integration_id (any) (required) - - last_time (any) (required) - - name (any) (required) - - plugin_type (any) (required) - - plugin_type_name (any) (required) - - ref_id (any) (required) - - settings (object) (required) — Type-specific configuration. Sensitive values (endpoint, headers, secrets, passwords) are returned masked as '******'. - - status (any) (required) - - team_id (any) (required) - - updated_at (any) (required) + - category (string) (required) — Category the integration belongs to: 'event.alert' alert events, 'event.change' change events, 'im' IM bots, 'webhook' custom webhooks. [event.alert, event.change, im, webhook] + - created_at (string) (required) — Unix timestamp in seconds when the integration was created. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - creator_id (integer) (required) — ID of the member who created the integration. + - description (string) (required) — Free-form description. + - editable (boolean) (required) — Whether the current caller passes the integration object-permission check; write calls also require Integrations Manage. + - integration_id (integer) (required) — Integration ID. + - integration_key (string) (required) — Decrypted integration key for inbound pushes. Empty when no key is stored; distinct from the account 'app_key'. + - last_time (string) (required) — Unix timestamp in seconds of the most recent event received. '0' when no event has arrived yet. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - name (string) (required) — Integration name. + - no_editable (boolean) (required) — Read-only flag. When true, the public APIs do not allow changes to this integration. + - plugin_id (integer) (required) — Internal integration type ID; use 'plugin_type', not this ID, when creating an integration. + - plugin_type (string) (required) — Integration type, for example 'standard.alert' or 'zabbix.alert'. + - plugin_type_name (string) (required) — Display name of the integration type, in the language of the request. + - ref_id (string) (required) — Source reference ID, normally 'a_' for an account-level integration; legacy 'w_' references may also exist. Channel-owned 'c_' integrations are excluded. + - settings (object) (required) — Stored type-specific configuration, including credential values without masking; matches the legacy '/datasource/*' response behavior. + - status (string) (required) — Lifecycle status: 'enabled' while the integration accepts events, 'disabled' when it is paused. [enabled, disabled] + - team_id (integer) (required) — ID of the team that owns the integration. '0' when it is not assigned to a team. + - test (object) — Plugin test result, returned only when a test is performed. + - message (string) (required) — Test failure message; empty on success. + - success (boolean) (required) — Whether the plugin test succeeded. + - updated_at (string) (required) — Unix timestamp in seconds when the integration was last updated. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - updated_by (integer) (required) — ID of the member who last updated the integration. `, Args: requireBodyFieldOrExactArg("integration_id", "integration-id"), Example: ` flashduty integration info --data '{"integration_id":6113996590131}'`, @@ -142,7 +151,7 @@ func genIntegrationsIntegrationAPIReadListCmd() *cobra.Command { Short: "List integrations", Long: `List integrations. -List the account's alert-source and change-source integrations. +List public integrations with their stored settings, decrypted keys, and edit permissions. API: POST /integration/list (integration-api-read-list) @@ -163,18 +172,28 @@ Request fields: Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - items (array) (required) — Integrations on the current page. + - account_id (integer) (required) — ID of the account that owns the integration. - category (string) (required) — Category the integration belongs to: 'event.alert' alert events, 'event.change' change events, 'im' IM bots, 'webhook' custom webhooks. [event.alert, event.change, im, webhook] - created_at (string) (required) — Unix timestamp in seconds when the integration was created. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - creator_id (integer) (required) — ID of the member who created the integration. + - data_source_id (integer) (required) — Legacy datasource ID, equal to 'integration_id'. - description (string) (required) — Free-form description. + - editable (boolean) (required) — Whether the current caller passes the integration object-permission check; write calls also require Integrations Manage. + - exclusive_data_source_id (integer) (required) — Legacy compatibility field; '0' for items returned by this public-integration list. - integration_id (integer) (required) — Integration ID. + - integration_key (string) (required) — Decrypted integration key for inbound pushes. Empty when no key is stored; distinct from the account 'app_key'. - last_time (string) (required) — Unix timestamp in seconds of the most recent event received. '0' when no event has arrived yet. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. - name (string) (required) — Integration name. + - no_editable (boolean) (required) — Read-only flag. When true, the public APIs do not allow changes to this integration. + - plugin_id (integer) (required) — Internal integration type ID; use 'plugin_type', not this ID, when creating an integration. - plugin_type (string) (required) — Integration type, for example 'standard.alert' or 'zabbix.alert'. - plugin_type_name (string) (required) — Display name of the integration type, in the language of the request. - - ref_id (string) (required) — Source reference ID: 'a_'-prefixed for an account-scoped integration, 'c_'-prefixed when it is shared into a channel, 'w_'-prefixed on legacy workspace-scoped integrations. + - ref_id (string) (required) — Source reference ID, normally 'a_' for an account-level integration; legacy 'w_' references may also exist. Channel-owned 'c_' integrations are excluded. + - settings (object) (required) — Stored type-specific configuration, including credential values without masking; matches the legacy '/datasource/*' response behavior. - status (string) (required) — Lifecycle status: 'enabled' while the integration accepts events, 'disabled' when it is paused. [enabled, disabled] - team_id (integer) (required) — ID of the team that owns the integration. '0' when it is not assigned to a team. - updated_at (string) (required) — Unix timestamp in seconds when the integration was last updated. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - updated_by (integer) (required) — ID of the member who last updated the integration. - limit (integer) (required) — Page size echoed back. - p (integer) (required) — Page number echoed back. - total (integer) (required) — Total number of matching integrations. @@ -345,6 +364,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genIntegrationsIntegrationAPIWriteCreateCmd() *cobra.Command { var dataJSON string var fDescription string + var fIntegrationKey string + var fIsTest bool var fName string var fPluginType string var fTeamID int64 @@ -353,28 +374,39 @@ func genIntegrationsIntegrationAPIWriteCreateCmd() *cobra.Command { Short: "Create integration", Long: `Create integration. -Create an integration for an alert source or change source. +Create a public integration for a supported alert, change, IM, or webhook type. API: POST /integration/create (integration-api-write-create) Request fields: --description string — Free-form description, at most 499 characters. (≤499 chars) - --name string — Integration name. 2–49 characters. (2-49 chars) + --integration-key string — Optional pre-generated key: 35 characters, ending with the account ID modulo 1000 padded to three digits; it must not already be in use. Omit to generate a key. (35-35 chars) + --is-test bool — Run the plugin test during creation; false when omitted. Inspect the returned 'test.success' for the test outcome. + --name string — Integration name, 2–49 characters when nonempty; defaults to the integration type name when omitted or empty. (2-49 chars) --plugin-type string (required) — Integration type. Must be one listed by 'POST /integration/type/list' with 'supports_api_create: true'. - --team-id int — Owning team ID. (min 1) + --team-id int — Owning team ID; omit or use '0' for no team. A nonzero ID must belong to the current account. (min 0) settings (object, via --data) — Type-specific configuration; the accepted keys depend on 'plugin_type'. Response fields ('data' envelope is unwrapped — these fields are at the top level): - integration_id (integer) (required) — ID of the new integration. - - integration_key (string) (required) — Key used to authenticate inbound pushes to this integration. Returned here only; fetch a new one with 'POST /integration/key/rotate'. + - integration_key (string) (required) — Integration key for inbound pushes; also readable from '/integration/list' and '/integration/info'. + - test (object) — Plugin test result, returned only when a test is performed. + - message (string) (required) — Test failure message; empty on success. + - success (boolean) (required) — Whether the plugin test succeeded. `, - Example: ` flashduty integration create --data '{"description":"Alerts pushed by the production Prometheus stack","name":"Prod metrics alerts","plugin_type":"standard.alert","settings":{"severity_mapping":[{"P1":"Critical"}]},"team_id":1467226103121}'`, + Example: ` flashduty integration create --data '{"description":"Alerts pushed by the production Prometheus stack","is_test":true,"name":"Prod metrics alerts","plugin_type":"standard.alert","settings":{"field_mapping":{},"value_mapping":{}},"team_id":1467226103121}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { body, err := genAssembleBody(dataJSON, func(body map[string]any) error { if cmd.Flags().Changed("description") { body["description"] = fDescription } + if cmd.Flags().Changed("integration-key") { + body["integration_key"] = fIntegrationKey + } + if cmd.Flags().Changed("is-test") { + body["is_test"] = fIsTest + } if cmd.Flags().Changed("name") { body["name"] = fName } @@ -402,9 +434,11 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }, } cmd.Flags().StringVar(&fDescription, "description", "", "Free-form description, at most 499 characters. (≤499 chars)") - cmd.Flags().StringVar(&fName, "name", "", "Integration name. 2–49 characters. (2-49 chars)") + cmd.Flags().StringVar(&fIntegrationKey, "integration-key", "", "Optional pre-generated key: 35 characters, ending with the account ID modulo 1000 padded to three digits; it must not already be in use. Omit to generate a key. (35-35 chars)") + cmd.Flags().BoolVar(&fIsTest, "is-test", false, "Run the plugin test during creation; false when omitted. Inspect the returned 'test.success' for the test outcome.") + cmd.Flags().StringVar(&fName, "name", "", "Integration name, 2–49 characters when nonempty; defaults to the integration type name when omitted or empty. (2-49 chars)") cmd.Flags().StringVar(&fPluginType, "plugin-type", "", "Integration type. Must be one listed by 'POST /integration/type/list' with 'supports_api_create: true'. (required)") - cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "Owning team ID. (min 1)") + cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "Owning team ID; omit or use '0' for no team. A nonzero ID must belong to the current account. (min 0)") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; positional arguments and typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } @@ -581,7 +615,7 @@ Request fields: --integration-id int (required) — Integration ID. (min 1) Response fields ('data' envelope is unwrapped — these fields are at the top level): - - integration_key (string) (required) — The new key. The previous key stops working immediately; this value cannot be read again later. + - integration_key (string) (required) — New integration key; the previous key stops working immediately. Also readable from '/integration/list' and '/integration/info'. `, Args: requireBodyFieldOrExactArg("integration_id", "integration-id"), Example: ` flashduty integration key-rotate --data '{"integration_id":6113996590131}'`, @@ -620,6 +654,7 @@ func genIntegrationsIntegrationAPIWriteUpdateCmd() *cobra.Command { var dataJSON string var fDescription string var fIntegrationID int64 + var fIsTest bool var fName string var fTeamID int64 cmd := &cobra.Command{ @@ -634,27 +669,37 @@ API: POST /integration/update (integration-api-write-update) Request fields: --description string — New description, at most 499 characters. (≤499 chars) --integration-id int (required) — Integration ID. (min 1) + --is-test bool — Run the plugin test when submitting 'settings'; no test is performed for a metadata-only update. --name string — New name, 2–49 characters. (2-49 chars) --team-id int — New owning team ID; '0' clears the team assignment. (min 0) - settings (object, via --data) — Replacement configuration for the integration type. Sensitive entries left out, or sent back as the masked '******', keep their stored value. + settings (object, via --data) — Replacement type-specific configuration. Known sensitive fields omitted or sent as '******' keep their stored value; an explicit empty headers object clears headers. Response fields ('data' envelope is unwrapped — these fields are at the top level): - - category (any) (required) - - created_at (any) (required) - - description (any) (required) - - integration_id (any) (required) - - last_time (any) (required) - - name (any) (required) - - plugin_type (any) (required) - - plugin_type_name (any) (required) - - ref_id (any) (required) - - settings (object) (required) — Type-specific configuration. Sensitive values (endpoint, headers, secrets, passwords) are returned masked as '******'. - - status (any) (required) - - team_id (any) (required) - - updated_at (any) (required) + - category (string) (required) — Category the integration belongs to: 'event.alert' alert events, 'event.change' change events, 'im' IM bots, 'webhook' custom webhooks. [event.alert, event.change, im, webhook] + - created_at (string) (required) — Unix timestamp in seconds when the integration was created. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - creator_id (integer) (required) — ID of the member who created the integration. + - description (string) (required) — Free-form description. + - editable (boolean) (required) — Whether the current caller passes the integration object-permission check; write calls also require Integrations Manage. + - integration_id (integer) (required) — Integration ID. + - integration_key (string) (required) — Decrypted integration key for inbound pushes. Empty when no key is stored; distinct from the account 'app_key'. + - last_time (string) (required) — Unix timestamp in seconds of the most recent event received. '0' when no event has arrived yet. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - name (string) (required) — Integration name. + - no_editable (boolean) (required) — Read-only flag. When true, the public APIs do not allow changes to this integration. + - plugin_id (integer) (required) — Internal integration type ID; use 'plugin_type', not this ID, when creating an integration. + - plugin_type (string) (required) — Integration type, for example 'standard.alert' or 'zabbix.alert'. + - plugin_type_name (string) (required) — Display name of the integration type, in the language of the request. + - ref_id (string) (required) — Source reference ID, normally 'a_' for an account-level integration; legacy 'w_' references may also exist. Channel-owned 'c_' integrations are excluded. + - settings (object) (required) — Stored type-specific configuration, including credential values without masking; matches the legacy '/datasource/*' response behavior. + - status (string) (required) — Lifecycle status: 'enabled' while the integration accepts events, 'disabled' when it is paused. [enabled, disabled] + - team_id (integer) (required) — ID of the team that owns the integration. '0' when it is not assigned to a team. + - test (object) — Plugin test result, returned only when a test is performed. + - message (string) (required) — Test failure message; empty on success. + - success (boolean) (required) — Whether the plugin test succeeded. + - updated_at (string) (required) — Unix timestamp in seconds when the integration was last updated. CLI '--json' renders this as an RFC3339 string in the process's local timezone (NOT UTC, and NOT the wire integer); an unset value renders as null. + - updated_by (integer) (required) — ID of the member who last updated the integration. `, Args: requireBodyFieldOrExactArg("integration_id", "integration-id"), - Example: ` flashduty integration update --data '{"integration_id":6113996590131,"name":"Prod metrics alerts (primary)","team_id":1467226103121}'`, + Example: ` flashduty integration update --data '{"integration_id":6113996590131,"is_test":true,"name":"Prod metrics alerts (primary)","settings":{"field_mapping":{},"value_mapping":{}},"team_id":1467226103121}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { body, err := genAssembleBody(dataJSON, func(body map[string]any) error { @@ -667,6 +712,9 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le if cmd.Flags().Changed("integration-id") { body["integration_id"] = fIntegrationID } + if cmd.Flags().Changed("is-test") { + body["is_test"] = fIsTest + } if cmd.Flags().Changed("name") { body["name"] = fName } @@ -692,6 +740,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le } cmd.Flags().StringVar(&fDescription, "description", "", "New description, at most 499 characters. (≤499 chars)") cmd.Flags().Int64Var(&fIntegrationID, "integration-id", 0, "Integration ID. (required) (min 1)") + cmd.Flags().BoolVar(&fIsTest, "is-test", false, "Run the plugin test when submitting 'settings'; no test is performed for a metadata-only update.") cmd.Flags().StringVar(&fName, "name", "", "New name, 2–49 characters. (2-49 chars)") cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "New owning team ID; '0' clears the team assignment. (min 0)") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; positional arguments and typed flags override its fields. Accepts inline JSON, or - to read stdin.") diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index 5b3c05b..44d7c92 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -1,7 +1,7 @@ --- name: flashduty version: "3.0" -description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, on-call, monitors, automations, RUM, sourcemaps, members. `flashduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance." +description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, integrations, on-call, monitors, automations, RUM, sourcemaps, members. `flashduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance." allowed-tools: bash, read hidden: true # internal-only: withheld from skills.sh public discovery (Safari embeds this skill directly). --- @@ -76,7 +76,8 @@ Some asks span several commands. For those the skill ships a script that fetches | datasource 数据源 = a system Flashmonit queries (Prometheus / Loki / SQL / SLS) / connect a datasource 连接数据源 / SLS project / logstore / Redis / MongoDB / Kafka / database overview 数据库概览 / locks 锁 / slowlog 慢日志 | **`reference/monit-datasource.md`** | | automation / 自动化 / 定时 AI SRE / scheduled AI task / daily brief / weekly report / webhook trigger / POST trigger / chat-created automation | **`reference/automation.md`** | | metric/log query / 指标查询 / 日志查询 / PromQL / LogsQL / SQL / trend 趋势 / log clustering 日志聚类 / datasource RCA 数据源排查 | **`reference/monit-query.md`** | -| channel / 协作空间 / collaboration space / 频道 / integration 集成 / 告警来源 alert source / alert grouping 告警分组 | **`reference/channel.md`** | +| public integration / 公共集成 / 集成中心 / integration 集成 / alert source 告警来源 / integration key 集成 Key / IM bot / webhook configuration | **`reference/integration.md`** | +| channel / 协作空间 / collaboration space / 频道 / channel-owned dedicated integration 协作空间专属集成 / alert grouping 告警分组 | **`reference/channel.md`** | | dispatch rule 分派策略 / 分派规则 / escalation rule 升级规则 / notify layers 通知层级 / who gets paged | **`reference/escalation.md`** | | silence 静默 / 屏蔽 / inhibit 抑制 / drop rule 丢弃 / noise reduction 降噪 / maintenance silence 维护窗口静默 | **`reference/noise.md`** | | enrichment / 数据加工 / 富化 / label mapping 字段映射 / extraction 提取 / mapping schema 集成 schema | **`reference/enrichment.md`** | diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 521a937..8533e53 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -133,5 +133,5 @@ Escalation, silence, inhibit and unsubscribe rules live on their own cards: `ref - **`channel-id` can be passed positionally or via `--channel-id` — both work** on every verb of this card (`info`, `infos`, `update`, `delete`, `disable`, `enable`). The fence heading `### verb ` shows the shorter positional form; the flag is an equally valid alternative, and if both are given the flag value wins. - **`channel create` requires `--channel-name` and `--team-id`** even though they are not marked `required` in the flag list — the server rejects the request without them. -- **`--plugin-ids` subscribes integrations that already exist; no `flashduty` verb creates or configures one.** Creating an integration (email, webhook, an alert source) and reading its inbound address/token live in the console only — they are not part of the open API this CLI speaks, so they are absent from the command tree by design. When the user asks how to *connect* something, answer from the product documentation and point at the console; do not dump and grep the command tree looking for a verb that cannot exist. `channel info` likewise reports the channel's own fields, not integration credentials. +- **`--plugin-ids` subscribes integrations that already exist.** Public integration creation, configuration, and inbound keys are available through the `integration` commands; read `reference/integration.md`. Channel-owned dedicated integrations are outside that public API surface and are managed in the console. `channel info` reports the channel's own fields, not integration credentials. - **`delete` on a channel is irreversible** — all rules within it (escalation, silence, inhibit, drop) are also removed. Confirm the `channel-id` against `list` before proceeding. diff --git a/skills/flashduty/reference/integration.md b/skills/flashduty/reference/integration.md new file mode 100644 index 0000000..c46e12d --- /dev/null +++ b/skills/flashduty/reference/integration.md @@ -0,0 +1,107 @@ +# flashduty integration — command card + +Prereq: read `SKILL.md`. These commands manage **public/account-level integrations**, including supported alert, change, IM, and webhook types. They do not manage channel-owned dedicated integrations (`c_` references); use the console for those. For channel configuration and subscriptions, read `reference/channel.md`. + +## Read before changing configuration + +- Start with `type-list` and `list`. Create only a `plugin_type` whose type entry has `supports_api_create: true`. +- List/info return stored `settings` and decrypted `integration_key` without masking. Prefer `--fields` projections for discovery; do not paste secrets into a report or unrelated tool. An integration key authenticates inbound pushes; it is not the account App Key used by this CLI. +- `editable` describes object permission; writes also require Integrations Manage. `no_editable: true` marks a read-only integration. Do not infer write authorization from read access. +- Before update/delete or key rotation, get per-target confirmation and save `info` to the protected workspace backup described in `SKILL.md`. Confirm references before deletion. Rotating the key invalidates the old key immediately; update the sender configuration as part of the approved change. +- Metadata-only updates preserve omitted fields. `--team-id 0` deliberately clears team ownership. Submitted `settings` replace type-specific configuration; known sensitive fields omitted or set to `******` preserve their stored value. An explicit `headers: {}` clears headers. +- `--is-test` on create, or on update with `settings`, returns plugin test results. Inspect `test.success` and `test.message`: HTTP success is not plugin-test success. An absent `test` means no test result was returned, not a successful test. + +## Discovery + +```bash +flashduty integration type-list --category event.alert --output-format toon +flashduty integration list --fields integration_id,name,plugin_type,team_id,editable,no_editable --output-format toon +flashduty integration info 42 --fields integration_id,name,plugin_type,team_id,editable,no_editable --output-format toon +``` + +## Approved configuration changes + +Use the exact type-specific settings provided for the target integration; do not guess credential keys. `--integration-key` is optional on create; omitting it generates a key. If provided, it must satisfy the account suffix, length, and uniqueness checks. + +```bash +# Only after confirmation; inspect the returned integration_id/key and test result. +flashduty integration create --plugin-type standard.alert --name "Production alerts" --is-test --data '{"settings":{}}' --json +# Only after backing up integration 42 and confirming this rename. +flashduty integration update 42 --name "Production alerts (primary)" --json +``` + +Do not add `--is-test` to a metadata-only update expecting a plugin test; settings must be submitted for an update test. Disable/enable preserve configuration. Delete may fail with `ReferenceExist` when other objects still reference the integration. + + + +### create +Create integration +- `--description` string — Free-form description, at most 499 characters. (≤499 chars) +- `--integration-key` string — Optional pre-generated key: 35 characters, ending with the account ID modulo 1000 padded to three digits; it must not already be in use. Omit to generate a key. (35-35 chars) +- `--is-test` bool — Run the plugin test during creation; false when omitted. Inspect the returned 'test.success' for the test outcome. +- `--name` string — Integration name, 2–49 characters when nonempty; defaults to the integration type name when omitted or empty. (2-49 chars) +- `--plugin-type` string (required) — Integration type. Must be one listed by 'POST /integration/type/list' with 'supports_api_create: true'. +- `--team-id` int64 — Owning team ID; omit or use '0' for no team. A nonzero ID must belong to the current account. (min 0) +- body-only (`--data`): settings (object) +- response: single object (`data` unwrapped to the top level) — fields: integration_id (integer); integration_key (string); test (object) + +### delete +Delete integration +- `` (positional, required) int64 — Integration ID. (min 1) + +### disable +Disable integration +- `` (positional, required) int64 — Integration ID. (min 1) + +### enable +Enable integration +- `` (positional, required) int64 — Integration ID. (min 1) + +### info +Get integration detail +- `` (positional, required) int64 — Integration ID. (min 1) +- response: single object (`data` unwrapped to the top level) — fields: category (string); created_at (string); creator_id (integer); description (string); editable (boolean); integration_id (integer); integration_key (string); last_time (string); name (string); no_editable (boolean); plugin_id (integer); plugin_type (string); plugin_type_name (string); ref_id (string); settings (object); status (string); team_id (integer); test (object); updated_at (string); updated_by (integer) + +### key-rotate +Rotate integration key +- `` (positional, required) int64 — Integration ID. (min 1) +- response: single object (`data` unwrapped to the top level) — fields: integration_key (string) + +### list +List integrations +- `--asc` bool — Sort ascending when true, descending when false. +- `--category` string — Filter by category. Accepts a comma-separated list. +- `--is-my-team` bool — Limit the result to integrations owned by your teams. +- `--limit` int64 — Page size. Defaults to 100, maximum 100. (1-100) +- `--name` string — Filter by integration name. +- `--orderby` string — Sort field. Defaults to 'created_at'; 'plugin_type' is sorted by the underlying plugin. · enum: created_at | updated_at | name | plugin_type | status +- `--page` int64 — Page number, 1-based. (min 1) +- `--plugin-type` string — Filter by integration type. Accepts a comma-separated list. +- `--ref-ids` stringSlice — Filter by source reference IDs. Each value must start with 'c_' (channel), 'a_' (account) or 'w_'. +- `--search-after-ctx` string +- `--status` string — Filter by status. Accepts a comma-separated list. +- `--team-ids` intSlice — Filter by team IDs. With 'is_my_team', the values narrow that set further. +- `--type` string — Deprecated. Merged into 'plugin_type' when both are set. +- response: `{items: [...], limit, p, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: account_id (integer); category (string); created_at (string); creator_id (integer); data_source_id (integer); description (string); editable (boolean); exclusive_data_source_id (integer); integration_id (integer); integration_key (string); last_time (string); name (string); no_editable (boolean); plugin_id (integer); plugin_type (string); plugin_type_name (string); ref_id (string); settings (object); status (string); team_id (integer); updated_at (string); updated_by (integer) + +### type-list +List integration types +- `--asc` bool — Sort ascending when 'true' (the default); descending when 'false'. +- `--category` string — Filter by category. Accepts a comma-separated list, for example 'event.alert,event.change'. +- `--limit` int64 — Page size. Defaults to 20, maximum 100. (1-100) +- `--orderby` string — Sort field. When omitted, types are returned in console ranking order. · enum: id | created_at | updated_at | name | type +- `--page` int64 — Page number, 1-based. (min 1) +- `--search-after-ctx` string +- response: `{items: [...], limit, p, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: category (string); plugin_type (string); plugin_type_logo_url (string); plugin_type_name (string); status (string); supports_api_create (boolean) + +### update +Update integration +- `--description` string — New description, at most 499 characters. (≤499 chars) +- `` (positional, required) int64 — Integration ID. (min 1) +- `--is-test` bool — Run the plugin test when submitting 'settings'; no test is performed for a metadata-only update. +- `--name` string — New name, 2–49 characters. (2-49 chars) +- `--team-id` int64 — New owning team ID; '0' clears the team assignment. (min 0) +- body-only (`--data`): settings (object) +- response: same shape as `info ` above + +