diff --git a/internal/server/automatic_task_notifications.go b/internal/server/automatic_task_notifications.go index 9556ed1..80c0854 100644 --- a/internal/server/automatic_task_notifications.go +++ b/internal/server/automatic_task_notifications.go @@ -56,7 +56,7 @@ func (s *Server) notifyAutomaticTask(ctx context.Context, task store.AutomaticTa }, "\n"), Time: run.FinishedAt, Task: task, Run: run, } - for _, channel := range []string{"telegram", "bark", "email", "pushplus", "webhook"} { + for _, channel := range []string{"telegram", "bark", "email", "pushplus", "webhook", "wecom"} { setting, err := s.store.NotificationSetting(ctx, channel) if errors.Is(err, store.ErrNotFound) || (err == nil && !setting.Enabled) { continue @@ -88,6 +88,8 @@ func sendAutomaticTaskNotification(ctx context.Context, channel string, config m return sendPushplusTextNotification(ctx, config, message.Title, message.Text) case "webhook": return sendAutomaticTaskWebhook(ctx, config, message) + case "wecom": + return sendWecomNotification(ctx, config, wecomAutomaticTaskValues(message)) default: return fmt.Errorf("unsupported notification channel %q", channel) } diff --git a/internal/server/sms_notifications.go b/internal/server/sms_notifications.go index dac6ef1..7a6ada0 100644 --- a/internal/server/sms_notifications.go +++ b/internal/server/sms_notifications.go @@ -24,7 +24,7 @@ import ( const smsNotificationPollInterval = 2 * time.Second -var smsOnlyNotificationChannels = []string{"bark", "email", "pushplus", "webhook"} +var smsOnlyNotificationChannels = []string{"bark", "email", "pushplus", "webhook", "wecom"} type smsNotification struct { DeviceID string @@ -143,7 +143,7 @@ func (s *Server) smsNotificationConfig(ctx context.Context, channel string) (map func validateSMSNotificationConfig(channel string, config map[string]any) error { switch channel { - case "bark", "email", "webhook": + case "bark", "email", "webhook", "wecom": if err := validateNotificationTestConfig(channel, config); err != nil { return err } @@ -202,6 +202,8 @@ func sendSMSNotification(ctx context.Context, channel string, config map[string] return sendPushplusSMSNotification(ctx, config, message) case "webhook": return sendWebhookSMSNotification(ctx, config, message) + case "wecom": + return sendWecomNotification(ctx, config, wecomSMSValues(message)) default: return fmt.Errorf("unsupported SMS notification channel %q", channel) } diff --git a/internal/server/sms_notifications_test.go b/internal/server/sms_notifications_test.go index e1b3510..9d8e7d4 100644 --- a/internal/server/sms_notifications_test.go +++ b/internal/server/sms_notifications_test.go @@ -36,12 +36,45 @@ func TestRenderSMSWebhookTemplate(t *testing.T) { } } +func TestWecomSMSValuesIncludeRenderedSMSFields(t *testing.T) { + location := time.FixedZone("UTC+8", 8*60*60) + message := smsNotification{ + DeviceID: "device-1", DeviceName: "客厅", DeviceLabel: "EC20", + Number: "+447386", Time: time.Date(2026, 8, 8, 17, 25, 35, 0, location), Content: "hello", + } + values := wecomSMSValues(message) + if values["event"] != "sms.received" || values["title"] != "收到新短信" || values["message"] != message.Text() { + t.Fatalf("common values = %#v", values) + } + if values["content"] != "hello" || values["number"] != "+447386" || values["device_label"] != "EC20" || values["time"] != "2026-08-08 17:25:35" { + t.Fatalf("SMS values = %#v", values) + } +} + +func TestWecomAutomaticTaskValuesLeaveSMSFieldsEmpty(t *testing.T) { + values := wecomAutomaticTaskValues(automaticTaskNotification{ + Title: "自动任务执行成功", Text: "任务已完成", Time: time.Unix(1_700_000_000, 0), + }) + if values["event"] != "automatic_task.completed" || values["title"] != "自动任务执行成功" || values["message"] != "任务已完成" { + t.Fatalf("common values = %#v", values) + } + for _, name := range []string{"content", "number", "device_id", "device_name", "device_label", "time"} { + if values[name] != "" { + t.Fatalf("%s = %q, want empty", name, values[name]) + } + } +} + func TestValidateSMSNotificationConfig(t *testing.T) { valid := map[string]map[string]any{ "bark": {"urls": []any{"https://api.day.app/key"}}, "email": {"smtp_host": "smtp.example.com", "from_address": "from@example.com", "to_addresses": []any{"to@example.com"}}, "pushplus": {"token": "secret"}, "webhook": {"urls": []any{"https://example.com/hook"}}, + "wecom": { + "urls": []any{"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=secret"}, + "payload_template": `{"msgtype":"text","text":{"content":{{message}}}}`, + }, } for channel, config := range valid { if err := validateSMSNotificationConfig(channel, config); err != nil { diff --git a/internal/server/wecom_notification.go b/internal/server/wecom_notification.go index 7bb2230..5208070 100644 --- a/internal/server/wecom_notification.go +++ b/internal/server/wecom_notification.go @@ -64,6 +64,36 @@ func wecomTestValues(now time.Time) wecomTemplateValues { } } +func wecomSMSValues(message smsNotification) wecomTemplateValues { + return wecomTemplateValues{ + "event": "sms.received", + "title": "收到新短信", + "message": message.Text(), + "timestamp": message.Time.UTC().Format(time.RFC3339), + "content": message.Content, + "number": message.Number, + "device_id": message.DeviceID, + "device_name": message.DeviceName, + "device_label": message.DeviceLabel, + "time": message.Time.Local().Format("2006-01-02 15:04:05"), + } +} + +func wecomAutomaticTaskValues(message automaticTaskNotification) wecomTemplateValues { + return wecomTemplateValues{ + "event": "automatic_task.completed", + "title": message.Title, + "message": message.Text, + "timestamp": message.Time.UTC().Format(time.RFC3339), + "content": "", + "number": "", + "device_id": "", + "device_name": "", + "device_label": "", + "time": "", + } +} + func validateWecomNotificationConfig(config map[string]any) error { urls := configStrings(config, "urls") if len(urls) == 0 {