diff --git a/flashduty_test.go b/flashduty_test.go index 93c1574..621c7d7 100644 --- a/flashduty_test.go +++ b/flashduty_test.go @@ -507,6 +507,15 @@ func TestNullableRequestFieldsSendExplicitZero(t *testing.T) { at: []string{"feishu_app_card_v2_table_enabled"}, want: false, }, + { + // Clearing a channel is an explicit empty string. It must survive + // the encoder: dropping it would silently mean "leave as is". + name: "template update clears a channel", + path: "/template/update", + body: &TemplateUpdateRequest{TemplateID: "t-1", TemplateName: "t", DingtalkApp: String("")}, + at: []string{"dingtalk_app"}, + want: "", + }, } for _, tt := range tests { @@ -560,11 +569,19 @@ func TestNullableRequestFieldsOmitUnsetValues(t *testing.T) { present: []string{`"scopes"`}, }, { - name: "template update leaves the feishu card table alone", - path: "/template/update", - body: &TemplateUpdateRequest{TemplateID: "t-1", TemplateName: "t"}, - absent: []string{`"feishu_app_card_v2_table_enabled"`}, - present: []string{`"template_id":"t-1"`}, + // An update that touches one channel must not carry the others: + // on the wire, a channel that is present is a channel that gets + // written. + name: "template update carries only the channel it changes", + path: "/template/update", + body: &TemplateUpdateRequest{TemplateID: "t-1", TemplateName: "t", Feishu: String("{{ .title }}")}, + absent: []string{ + `"feishu_app_card_v2_table_enabled"`, `"description"`, `"team_id"`, + `"email"`, `"sms"`, `"voice"`, `"dingtalk"`, `"wecom"`, `"feishu_app"`, + `"dingtalk_app"`, `"wecom_app"`, `"slack_app"`, `"teams_app"`, + `"telegram"`, `"slack"`, `"zoom"`, + }, + present: []string{`"template_id":"t-1"`, `"feishu":"{{ .title }}"`}, }, } diff --git a/models_gen.go b/models_gen.go index 4a07798..afac118 100644 --- a/models_gen.go +++ b/models_gen.go @@ -9813,46 +9813,46 @@ type TemplateListResponse struct { // TemplateUpdateRequest is generated from the Flashduty OpenAPI schema. type TemplateUpdateRequest struct { - // Free-form description. Up to 500 characters. - Description string `json:"description,omitempty" toon:"description,omitempty"` - // DingTalk robot message template source. - Dingtalk string `json:"dingtalk,omitempty" toon:"dingtalk,omitempty"` - // DingTalk app message template source. - DingtalkApp string `json:"dingtalk_app,omitempty" toon:"dingtalk_app,omitempty"` - // Email body template source (Go `html/template` syntax). - Email string `json:"email,omitempty" toon:"email,omitempty"` - // Feishu robot message template source. - Feishu string `json:"feishu,omitempty" toon:"feishu,omitempty"` - // Feishu app message template source. - FeishuApp string `json:"feishu_app,omitempty" toon:"feishu_app,omitempty"` + // Free-form description. Up to 500 characters. Omit to keep the current content; send an empty string to clear it. + Description *string `json:"description,omitempty" toon:"description,omitempty"` + // DingTalk robot message template source. Omit to keep the current content; send an empty string to clear it. + Dingtalk *string `json:"dingtalk,omitempty" toon:"dingtalk,omitempty"` + // DingTalk app message template source. Omit to keep the current content; send an empty string to clear it. + DingtalkApp *string `json:"dingtalk_app,omitempty" toon:"dingtalk_app,omitempty"` + // Email body template source (Go `html/template` syntax). Omit to keep the current content; send an empty string to clear it. + Email *string `json:"email,omitempty" toon:"email,omitempty"` + // Feishu robot message template source. Omit to keep the current content; send an empty string to clear it. + Feishu *string `json:"feishu,omitempty" toon:"feishu,omitempty"` + // Feishu app message template source. Omit to keep the current content; send an empty string to clear it. + FeishuApp *string `json:"feishu_app,omitempty" toon:"feishu_app,omitempty"` // When set, enable or disable table rendering for alert labels in Feishu app cards. Omit to keep the existing setting. FeishuAppCardV2TableEnabled *bool `json:"feishu_app_card_v2_table_enabled,omitempty" toon:"feishu_app_card_v2_table_enabled,omitempty"` // Incident card fields hidden per IM app type. IncidentCardHiddenFields IncidentCardHiddenFields `json:"incident_card_hidden_fields,omitempty" toon:"incident_card_hidden_fields,omitempty"` - // Slack robot message template source. - Slack string `json:"slack,omitempty" toon:"slack,omitempty"` - // Slack app message template source. - SlackApp string `json:"slack_app,omitempty" toon:"slack_app,omitempty"` - // SMS template source (Go `text/template` syntax). - SMS string `json:"sms,omitempty" toon:"sms,omitempty"` - // Team scope. 0 for account-wide. - TeamID int64 `json:"team_id,omitempty" toon:"team_id,omitempty"` - // Microsoft Teams app message template source. - TeamsApp string `json:"teams_app,omitempty" toon:"teams_app,omitempty"` - // Telegram bot message template source. - Telegram string `json:"telegram,omitempty" toon:"telegram,omitempty"` + // Slack robot message template source. Omit to keep the current content; send an empty string to clear it. + Slack *string `json:"slack,omitempty" toon:"slack,omitempty"` + // Slack app message template source. Omit to keep the current content; send an empty string to clear it. + SlackApp *string `json:"slack_app,omitempty" toon:"slack_app,omitempty"` + // SMS template source (Go `text/template` syntax). Omit to keep the current content; send an empty string to clear it. + SMS *string `json:"sms,omitempty" toon:"sms,omitempty"` + // Team scope. 0 for account-wide. Omit to keep the template's current team. + TeamID *int64 `json:"team_id,omitempty" toon:"team_id,omitempty"` + // Microsoft Teams app message template source. Omit to keep the current content; send an empty string to clear it. + TeamsApp *string `json:"teams_app,omitempty" toon:"teams_app,omitempty"` + // Telegram bot message template source. Omit to keep the current content; send an empty string to clear it. + Telegram *string `json:"telegram,omitempty" toon:"telegram,omitempty"` // Target template ID; obtain it from `POST /template/list`. TemplateID string `json:"template_id" toon:"template_id"` // Template name. 1–39 characters. TemplateName string `json:"template_name" toon:"template_name"` - // Voice call script template source. - Voice string `json:"voice,omitempty" toon:"voice,omitempty"` - // WeCom robot message template source. - Wecom string `json:"wecom,omitempty" toon:"wecom,omitempty"` - // WeCom app message template source. - WecomApp string `json:"wecom_app,omitempty" toon:"wecom_app,omitempty"` - // Zoom bot message template source. - Zoom string `json:"zoom,omitempty" toon:"zoom,omitempty"` + // Voice call script template source. Omit to keep the current content; send an empty string to clear it. + Voice *string `json:"voice,omitempty" toon:"voice,omitempty"` + // WeCom robot message template source. Omit to keep the current content; send an empty string to clear it. + Wecom *string `json:"wecom,omitempty" toon:"wecom,omitempty"` + // WeCom app message template source. Omit to keep the current content; send an empty string to clear it. + WecomApp *string `json:"wecom_app,omitempty" toon:"wecom_app,omitempty"` + // Zoom bot message template source. Omit to keep the current content; send an empty string to clear it. + Zoom *string `json:"zoom,omitempty" toon:"zoom,omitempty"` } // TimeFilter is generated from the Flashduty OpenAPI schema. diff --git a/notification_templates.go b/notification_templates.go index d6b22d8..a66c58b 100644 --- a/notification_templates.go +++ b/notification_templates.go @@ -74,7 +74,7 @@ func (s *NotificationTemplatesService) WriteDelete(ctx context.Context, req *Tem // Update a template. // -// Replace the content of every channel on an existing template. +// Update an existing template. Only the fields present in the request are written: a channel you omit keeps its current content, and an explicit empty string clears it. // // API: POST /template/update (template-write-update). func (s *NotificationTemplatesService) WriteUpdate(ctx context.Context, req *TemplateUpdateRequest) (*Response, error) { diff --git a/openapi/openapi.en.json b/openapi/openapi.en.json index f1e3828..3d4e496 100644 --- a/openapi/openapi.en.json +++ b/openapi/openapi.en.json @@ -9171,12 +9171,12 @@ "post": { "operationId": "template-write-update", "summary": "Update a template", - "description": "Replace the content of every channel on an existing template.", + "description": "Update an existing template. Only the fields present in the request are written: a channel you omit keeps its current content, and an explicit empty string clears it.", "tags": [ "On-call/Notification templates" ], "x-mint": { - "content": "## Restrictions\n\n| Aspect | Value |\n| ------ | ----- |\n| Rate limits | **1,000 requests/minute**; **50 requests/second** per account |\n| Permissions | **Templates Manage** (`on-call`) |\n\n## Usage\n\n- Every channel field in the request overwrites the stored value — send an empty string to clear a channel.\n- The caller needs data-permission on the template's team; otherwise the response is `AccessDenied`.\n- Every call is recorded in the account audit log. Don't put secrets in request fields.", + "content": "## Restrictions\n\n| Aspect | Value |\n| ------ | ----- |\n| Rate limits | **1,000 requests/minute**; **50 requests/second** per account |\n| Permissions | **Templates Manage** (`on-call`) |\n\n## Usage\n\n- Only the fields present in the request are written. A channel you omit keeps its current content; send it as an empty string to clear it.\n- The caller needs data-permission on the template's team; otherwise the response is `AccessDenied`.\n- Every call is recorded in the account audit log. Don't put secrets in request fields.", "href": "/en/api-reference/on-call/notification-templates/template-write-update", "metadata": { "sidebarTitle": "Update a template" @@ -39972,10 +39972,12 @@ "example": "6605a1b2c3d4e5f6a7b8c9d0" }, "team_id": { - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "int64", - "description": "Team scope. 0 for account-wide.", - "default": 0 + "description": "Team scope. 0 for account-wide. Omit to keep the template's current team." }, "template_name": { "type": "string", @@ -39984,37 +39986,61 @@ "description": "Template name. 1–39 characters." }, "description": { - "type": "string", + "type": [ + "string", + "null" + ], "maxLength": 500, - "description": "Free-form description. Up to 500 characters." + "description": "Free-form description. Up to 500 characters. Omit to keep the current content; send an empty string to clear it." }, "email": { - "type": "string", - "description": "Email body template source (Go `html/template` syntax)." + "type": [ + "string", + "null" + ], + "description": "Email body template source (Go `html/template` syntax). Omit to keep the current content; send an empty string to clear it." }, "sms": { - "type": "string", - "description": "SMS template source (Go `text/template` syntax)." + "type": [ + "string", + "null" + ], + "description": "SMS template source (Go `text/template` syntax). Omit to keep the current content; send an empty string to clear it." }, "voice": { - "type": "string", - "description": "Voice call script template source." + "type": [ + "string", + "null" + ], + "description": "Voice call script template source. Omit to keep the current content; send an empty string to clear it." }, "dingtalk": { - "type": "string", - "description": "DingTalk robot message template source." + "type": [ + "string", + "null" + ], + "description": "DingTalk robot message template source. Omit to keep the current content; send an empty string to clear it." }, "wecom": { - "type": "string", - "description": "WeCom robot message template source." + "type": [ + "string", + "null" + ], + "description": "WeCom robot message template source. Omit to keep the current content; send an empty string to clear it." }, "feishu": { - "type": "string", - "description": "Feishu robot message template source." + "type": [ + "string", + "null" + ], + "description": "Feishu robot message template source. Omit to keep the current content; send an empty string to clear it." }, "feishu_app": { - "type": "string", - "description": "Feishu app message template source." + "type": [ + "string", + "null" + ], + "description": "Feishu app message template source. Omit to keep the current content; send an empty string to clear it." }, "feishu_app_card_v2_table_enabled": { "type": [ @@ -40024,32 +40050,53 @@ "description": "When set, enable or disable table rendering for alert labels in Feishu app cards. Omit to keep the existing setting." }, "dingtalk_app": { - "type": "string", - "description": "DingTalk app message template source." + "type": [ + "string", + "null" + ], + "description": "DingTalk app message template source. Omit to keep the current content; send an empty string to clear it." }, "wecom_app": { - "type": "string", - "description": "WeCom app message template source." + "type": [ + "string", + "null" + ], + "description": "WeCom app message template source. Omit to keep the current content; send an empty string to clear it." }, "slack_app": { - "type": "string", - "description": "Slack app message template source." + "type": [ + "string", + "null" + ], + "description": "Slack app message template source. Omit to keep the current content; send an empty string to clear it." }, "teams_app": { - "type": "string", - "description": "Microsoft Teams app message template source." + "type": [ + "string", + "null" + ], + "description": "Microsoft Teams app message template source. Omit to keep the current content; send an empty string to clear it." }, "telegram": { - "type": "string", - "description": "Telegram bot message template source." + "type": [ + "string", + "null" + ], + "description": "Telegram bot message template source. Omit to keep the current content; send an empty string to clear it." }, "slack": { - "type": "string", - "description": "Slack robot message template source." + "type": [ + "string", + "null" + ], + "description": "Slack robot message template source. Omit to keep the current content; send an empty string to clear it." }, "zoom": { - "type": "string", - "description": "Zoom bot message template source." + "type": [ + "string", + "null" + ], + "description": "Zoom bot message template source. Omit to keep the current content; send an empty string to clear it." }, "incident_card_hidden_fields": { "$ref": "#/components/schemas/IncidentCardHiddenFields", diff --git a/openapi/openapi.zh.json b/openapi/openapi.zh.json index 5da4385..64abe1a 100644 --- a/openapi/openapi.zh.json +++ b/openapi/openapi.zh.json @@ -9171,12 +9171,12 @@ "post": { "operationId": "template-write-update", "summary": "更新模板", - "description": "替换指定模板在所有通道上的内容。", + "description": "更新指定模板。仅写入请求中出现的字段:省略的通道保持当前内容,显式传空字符串表示清空该通道。", "tags": [ "On-call/通知模板" ], "x-mint": { - "content": "## 限制说明\n\n| 项目 | 说明 |\n| ---- | ---- |\n| 速率限制 | 每个账户 **1,000 次/分钟**;**50 次/秒** |\n| 权限要求 | **模板管理**(`on-call`) |\n\n## 使用说明\n\n- 请求中的每个通道字段会覆盖存储值——想清空某通道时,把该字段设置为空字符串即可。\n- 调用者必须对目标模板所属团队拥有数据权限,否则返回 `AccessDenied`。\n- 每次调用都会记录到账户审计日志,请不要把敏感信息放在请求字段中。", + "content": "## 限制说明\n\n| 项目 | 说明 |\n| ---- | ---- |\n| 速率限制 | 每个账户 **1,000 次/分钟**;**50 次/秒** |\n| 权限要求 | **模板管理**(`on-call`) |\n\n## 使用说明\n\n- 仅写入请求中出现的字段:省略的通道保持当前内容;想清空某通道,把该字段显式设置为空字符串。\n- 调用者必须对目标模板所属团队拥有数据权限,否则返回 `AccessDenied`。\n- 每次调用都会记录到账户审计日志,请不要把敏感信息放在请求字段中。", "href": "/zh/api-reference/on-call/notification-templates/template-write-update", "metadata": { "sidebarTitle": "更新模板" @@ -39963,10 +39963,12 @@ "example": "6605a1b2c3d4e5f6a7b8c9d0" }, "team_id": { - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "int64", - "description": "团队归属。0 表示账户全局共享。", - "default": 0 + "description": "团队归属。0 表示账户全局共享。省略时保持模板当前的团队归属。" }, "template_name": { "type": "string", @@ -39975,37 +39977,61 @@ "description": "模板名称,长度 1–39 个字符。" }, "description": { - "type": "string", + "type": [ + "string", + "null" + ], "maxLength": 500, - "description": "自定义描述。最多 500 字符。" + "description": "自定义描述。最多 500 字符。省略时保持当前内容;传空字符串表示清空。" }, "email": { - "type": "string", - "description": "邮件正文模板源(Go `html/template` 语法)。" + "type": [ + "string", + "null" + ], + "description": "邮件正文模板源(Go `html/template` 语法)。省略时保持当前内容;传空字符串表示清空。" }, "sms": { - "type": "string", - "description": "短信模板源(Go `text/template` 语法)。" + "type": [ + "string", + "null" + ], + "description": "短信模板源(Go `text/template` 语法)。省略时保持当前内容;传空字符串表示清空。" }, "voice": { - "type": "string", - "description": "语音呼叫脚本模板源。" + "type": [ + "string", + "null" + ], + "description": "语音呼叫脚本模板源。省略时保持当前内容;传空字符串表示清空。" }, "dingtalk": { - "type": "string", - "description": "钉钉群机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "钉钉群机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "wecom": { - "type": "string", - "description": "企业微信群机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "企业微信群机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "feishu": { - "type": "string", - "description": "飞书群机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "飞书群机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "feishu_app": { - "type": "string", - "description": "飞书应用消息模板源。" + "type": [ + "string", + "null" + ], + "description": "飞书应用消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "feishu_app_card_v2_table_enabled": { "type": [ @@ -40015,32 +40041,53 @@ "description": "设置后开启或关闭飞书应用卡片中的告警标签表格渲染;省略时保持当前设置。" }, "dingtalk_app": { - "type": "string", - "description": "钉钉应用消息模板源。" + "type": [ + "string", + "null" + ], + "description": "钉钉应用消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "wecom_app": { - "type": "string", - "description": "企业微信应用消息模板源。" + "type": [ + "string", + "null" + ], + "description": "企业微信应用消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "slack_app": { - "type": "string", - "description": "Slack 应用消息模板源。" + "type": [ + "string", + "null" + ], + "description": "Slack 应用消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "teams_app": { - "type": "string", - "description": "Microsoft Teams 应用消息模板源。" + "type": [ + "string", + "null" + ], + "description": "Microsoft Teams 应用消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "telegram": { - "type": "string", - "description": "Telegram 机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "Telegram 机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "slack": { - "type": "string", - "description": "Slack 机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "Slack 机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "zoom": { - "type": "string", - "description": "Zoom 机器人消息模板源。" + "type": [ + "string", + "null" + ], + "description": "Zoom 机器人消息模板源。省略时保持当前内容;传空字符串表示清空。" }, "incident_card_hidden_fields": { "$ref": "#/components/schemas/IncidentCardHiddenFields",