diff --git a/pkg/message/dtalk.go b/pkg/message/dtalk.go index fd0e0cf..d63d05b 100644 --- a/pkg/message/dtalk.go +++ b/pkg/message/dtalk.go @@ -62,6 +62,26 @@ func (t *Dtalk) SendMessageText(text string, at ...string) ([]byte, error) { "content": text, }, } + + // 添加@功能 + if len(at) > 0 { + atMobiles := []string{} + isAtAll := false + + for _, mobile := range at { + if mobile == "all" || mobile == "@all" { + isAtAll = true + } else { + atMobiles = append(atMobiles, mobile) + } + } + + msg["at"] = map[string]interface{}{ + "atMobiles": atMobiles, + "isAtAll": isAtAll, + } + } + resp, err := t.Request(msg) return resp, err } @@ -74,6 +94,26 @@ func (t *Dtalk) SendMessageMarkdown(title, text string, at ...string) ([]byte, e "text": text, }, } + + // 添加@功能 + if len(at) > 0 { + atMobiles := []string{} + isAtAll := false + + for _, mobile := range at { + if mobile == "all" || mobile == "@all" { + isAtAll = true + } else { + atMobiles = append(atMobiles, mobile) + } + } + + msg["at"] = map[string]interface{}{ + "atMobiles": atMobiles, + "isAtAll": isAtAll, + } + } + resp, err := t.Request(msg) return resp, err } diff --git a/pkg/message/qyweixin.go b/pkg/message/qyweixin.go index d2a4968..5e62152 100644 --- a/pkg/message/qyweixin.go +++ b/pkg/message/qyweixin.go @@ -53,10 +53,40 @@ func (t *QyWeiXin) Request(msg interface{}) ([]byte, error) { func (t *QyWeiXin) SendMessageText(text string, at ...string) ([]byte, error) { msg := map[string]interface{}{ "msgtype": "text", - "text": map[string]string{ + "text": map[string]interface{}{ "content": text, }, } + + // 添加@功能 + // 企业微信支持两种@方式: + // 1. mentioned_list: userid列表或"@all" + // 2. mentioned_mobile_list: 手机号列表 + if len(at) > 0 { + mentionedList := []string{} + mentionedMobileList := []string{} + + for _, item := range at { + if item == "@all" || item == "all" { + mentionedList = append(mentionedList, "@all") + } else if len(item) == 11 && item[0] == '1' { + // 判断是否为手机号(简单判断:11位且以1开头) + mentionedMobileList = append(mentionedMobileList, item) + } else { + // 否则当作userid处理 + mentionedList = append(mentionedList, item) + } + } + + textContent := msg["text"].(map[string]interface{}) + if len(mentionedList) > 0 { + textContent["mentioned_list"] = mentionedList + } + if len(mentionedMobileList) > 0 { + textContent["mentioned_mobile_list"] = mentionedMobileList + } + } + resp, err := t.Request(msg) return resp, err } @@ -64,11 +94,14 @@ func (t *QyWeiXin) SendMessageText(text string, at ...string) ([]byte, error) { func (t *QyWeiXin) SendMessageMarkdown(title, text string, at ...string) ([]byte, error) { msg := map[string]interface{}{ "msgtype": "markdown", - "markdown": map[string]string{ - "title": title, + "markdown": map[string]interface{}{ "content": text, }, } + + // 企业微信Markdown消息不支持@功能,但可以在内容中手动添加 + // 如果需要@功能,建议使用text类型 + resp, err := t.Request(msg) return resp, err } diff --git a/routers/api/v1/send_message.go b/routers/api/v1/send_message.go index 99736b5..d4e3ee7 100644 --- a/routers/api/v1/send_message.go +++ b/routers/api/v1/send_message.go @@ -23,6 +23,11 @@ type SendMessageReq struct { URL string `json:"url" label:"消息详情url地址"` MarkDown string `json:"markdown" label:"markdown内容"` Mode string `json:"mode" label:"是否异步发送"` + + // @提及相关参数 + AtMobiles []string `json:"at_mobiles" label:"@的手机号列表"` + AtUserIds []string `json:"at_user_ids" label:"@的用户ID列表"` + AtAll bool `json:"at_all" label:"是否@所有人"` } // DoSendMassage 外部调用发信接口 @@ -56,15 +61,18 @@ func DoSendMassage(c *gin.Context) { } msgService := send_message_service.SendMessageService{ - TaskID: taskID, - Title: req.Title, - Text: req.Text, - HTML: req.HTML, - URL: req.URL, - MarkDown: req.MarkDown, - CallerIp: c.ClientIP(), + TaskID: taskID, + Title: req.Title, + Text: req.Text, + HTML: req.HTML, + URL: req.URL, + MarkDown: req.MarkDown, + CallerIp: c.ClientIP(), + AtMobiles: req.AtMobiles, + AtUserIds: req.AtUserIds, + AtAll: req.AtAll, DefaultLogger: logrus.WithFields(logrus.Fields{ - //"prefix": "[Message Instance]", + "prefix": "[Send Instance]", }), } task, err := msgService.SendPreCheck() diff --git a/service/send_message_service/message_handler.go b/service/send_message_service/message_handler.go deleted file mode 100644 index f74247a..0000000 --- a/service/send_message_service/message_handler.go +++ /dev/null @@ -1,164 +0,0 @@ -package send_message_service - -import ( - "message-nest/models" - "message-nest/service/send_way_service" -) - -// 消息类型常量定义 -const ( - MessageTypeEmail = "Email" - MessageTypeDtalk = "Dtalk" - MessageTypeQyWeiXin = "QyWeiXin" - MessageTypeCustom = "Custom" - MessageTypeWeChatOFAccount = "WeChatOFAccount" - MessageTypeMessageNest = "MessageNest" -) - -// MessageHandler 消息处理器接口 -type MessageHandler interface { - // Send 发送消息 - // 返回:响应内容,错误信息 - Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) - - // GetType 返回该处理器支持的消息类型字符串 - GetType() string -} - -// MessageHandlerRegistry 消息处理器注册表 -type MessageHandlerRegistry struct { - handlers map[string]MessageHandler -} - -// NewMessageHandlerRegistry 创建新的消息处理器注册表 -func NewMessageHandlerRegistry() *MessageHandlerRegistry { - return &MessageHandlerRegistry{ - handlers: make(map[string]MessageHandler), - } -} - -// Register 注册消息处理器 -func (r *MessageHandlerRegistry) Register(handler MessageHandler) { - r.handlers[handler.GetType()] = handler -} - -// GetHandler 根据类型字符串获取对应的处理器 -func (r *MessageHandlerRegistry) GetHandler(wayType string) (MessageHandler, bool) { - handler, ok := r.handlers[wayType] - return handler, ok -} - -// 全局消息处理器注册表 -var globalRegistry = NewMessageHandlerRegistry() - -// init 初始化时注册所有处理器 -func init() { - globalRegistry.Register(&EmailHandler{}) - globalRegistry.Register(&DtalkHandler{}) - globalRegistry.Register(&QyWeiXinHandler{}) - globalRegistry.Register(&CustomHandler{}) - globalRegistry.Register(&WeChatOfAccountHandler{}) - globalRegistry.Register(&HostMessageHandler{}) -} - -// GetGlobalRegistry 获取全局注册表 -func GetGlobalRegistry() *MessageHandlerRegistry { - return globalRegistry -} - -// EmailHandler 邮箱消息处理器 -type EmailHandler struct{} - -func (h *EmailHandler) GetType() string { - return MessageTypeEmail -} - -func (h *EmailHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.WayDetailEmail) - if !ok { - return "", "类型转换失败" - } - es := EmailService{} - errMsg := es.SendTaskEmail(auth, ins, typeC, title, content) - return "", errMsg -} - -// DtalkHandler 钉钉消息处理器 -type DtalkHandler struct{} - -func (h *DtalkHandler) GetType() string { - return MessageTypeDtalk -} - -func (h *DtalkHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.WayDetailDTalk) - if !ok { - return "", "类型转换失败" - } - es := DtalkService{} - return es.SendDtalkMessage(auth, ins, typeC, title, content) -} - -// QyWeiXinHandler 企业微信消息处理器 -type QyWeiXinHandler struct{} - -func (h *QyWeiXinHandler) GetType() string { - return MessageTypeQyWeiXin -} - -func (h *QyWeiXinHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.WayDetailQyWeiXin) - if !ok { - return "", "类型转换失败" - } - es := QyWeiXinService{} - return es.SendQyWeiXinMessage(auth, ins, typeC, title, content) -} - -// CustomHandler 自定义webhook消息处理器 -type CustomHandler struct{} - -func (h *CustomHandler) GetType() string { - return MessageTypeCustom -} - -func (h *CustomHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.WayDetailCustom) - if !ok { - return "", "类型转换失败" - } - cs := CustomService{} - return cs.SendCustomMessage(auth, ins, typeC, title, content) -} - -// WeChatOfAccountHandler 微信公众号消息处理器 -type WeChatOfAccountHandler struct{} - -func (h *WeChatOfAccountHandler) GetType() string { - return MessageTypeWeChatOFAccount -} - -func (h *WeChatOfAccountHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.WeChatOFAccount) - if !ok { - return "", "类型转换失败" - } - cs := WeChatOfAccountService{} - return cs.SendWeChatOfAccountMessage(auth, ins, typeC, title, content, url) -} - -// HostMessageHandler 托管消息处理器 -type HostMessageHandler struct{} - -func (h *HostMessageHandler) GetType() string { - return MessageTypeMessageNest -} - -func (h *HostMessageHandler) Send(msgObj interface{}, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - auth, ok := msgObj.(send_way_service.MessageNest) - if !ok { - return "", "类型转换失败" - } - cs := HostMessageService{} - return cs.SendHostMessage(auth, ins, typeC, title, content) -} diff --git a/service/send_message_service/send_custom.go b/service/send_message_service/send_custom.go deleted file mode 100644 index 228ba28..0000000 --- a/service/send_message_service/send_custom.go +++ /dev/null @@ -1,29 +0,0 @@ -package send_message_service - -import ( - "encoding/json" - "fmt" - - "message-nest/models" - "message-nest/pkg/message" - "message-nest/service/send_way_service" - "strings" -) - -type CustomService struct { -} - -// SendCustomMessage 执行发送钉钉 -func (s *CustomService) SendCustomMessage(auth send_way_service.WayDetailCustom, ins models.SendTasksIns, typeC string, title string, content string) (string, string) { - errMsg := "" - cli := message.CustomWebhook{} - data, _ := json.Marshal(content) - dataStr := string(data) - dataStr = strings.Trim(dataStr, "\"") - bodyStr := strings.Replace(auth.Body, "TEXT", dataStr, -1) - res, err := cli.Request(auth.Webhook, bodyStr) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", err) - } - return string(res), errMsg -} diff --git a/service/send_message_service/send_dtalk.go b/service/send_message_service/send_dtalk.go deleted file mode 100644 index 0a8ca39..0000000 --- a/service/send_message_service/send_dtalk.go +++ /dev/null @@ -1,47 +0,0 @@ -package send_message_service - -import ( - "fmt" - "message-nest/models" - "message-nest/pkg/message" - "message-nest/service/send_ins_service" - "message-nest/service/send_way_service" -) - -type DtalkService struct { -} - -// SendDtalkMessage 执行发送钉钉 -func (s *DtalkService) SendDtalkMessage(auth send_way_service.WayDetailDTalk, ins models.SendTasksIns, typeC string, title string, content string) (string, string) { - insService := send_ins_service.SendTaskInsService{} - errStr, c := insService.ValidateDiffIns(ins) - if errStr != "" { - return errStr, "" - } - _, ok := c.(models.InsDtalkConfig) - if !ok { - return "钉钉config校验失败", "" - } - - errMsg := "" - var res []byte - var err error - cli := message.Dtalk{ - AccessToken: auth.AccessToken, - Secret: auth.Secret, - } - if typeC == "text" { - res, err = cli.SendMessageText(content) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType) - } - } else if typeC == "markdown" { - res, err = cli.SendMessageMarkdown(title, content) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType) - } - } else { - errMsg = fmt.Sprintf("未知的钉钉发送内容类型:%s", ins.ContentType) - } - return string(res), errMsg -} diff --git a/service/send_message_service/send_email.go b/service/send_message_service/send_email.go deleted file mode 100644 index 829b7b5..0000000 --- a/service/send_message_service/send_email.go +++ /dev/null @@ -1,37 +0,0 @@ -package send_message_service - -import ( - "fmt" - "message-nest/models" - "message-nest/pkg/message" - "message-nest/service/send_ins_service" - "message-nest/service/send_way_service" -) - -type EmailService struct { -} - -// SendTaskEmail 执行发送邮件 -func (s *EmailService) SendTaskEmail(auth send_way_service.WayDetailEmail, ins models.SendTasksIns, typeC string, title string, content string) string { - insService := send_ins_service.SendTaskInsService{} - errStr, c := insService.ValidateDiffIns(ins) - if errStr != "" { - return errStr - } - config, ok := c.(models.InsEmailConfig) - if !ok { - return "邮箱config校验失败" - } - - var emailer message.EmailMessage - errMsg := "" - emailer.Init(auth.Server, auth.Port, auth.Account, auth.Passwd) - if typeC == "text" { - errMsg = emailer.SendTextMessage(config.ToAccount, title, content) - } else if typeC == "html" { - errMsg = emailer.SendHtmlMessage(config.ToAccount, title, content) - } else { - errMsg = fmt.Sprintf("未知的邮件发送内容类型:%s", ins.ContentType) - } - return errMsg -} diff --git a/service/send_message_service/send_hosted_message.go b/service/send_message_service/send_hosted_message.go deleted file mode 100644 index 7c670ae..0000000 --- a/service/send_message_service/send_hosted_message.go +++ /dev/null @@ -1,36 +0,0 @@ -package send_message_service - -import ( - "message-nest/models" - "message-nest/service/hosted_message_service" - "message-nest/service/send_way_service" -) - -type HostMessageService struct { -} - -// SendHostMessage 执行托管消息记录 -func (s *HostMessageService) SendHostMessage( - auth send_way_service.MessageNest, - ins models.SendTasksIns, - typeC string, - title string, - content string) (string, string) { - - errMsg := "" - var res string - var err error - messageService := hosted_message_service.HostMessageService{ - Title: title, - Content: content, - Type: typeC, - } - err = messageService.Add() - if err != nil { - errMsg = err.Error() - res = "托管消息创建失败!" - } else { - res = "托管消息创建成功!" - } - return string(res), errMsg -} diff --git a/service/send_message_service/send_message.go b/service/send_message_service/send_message.go index 9981125..1874ff6 100644 --- a/service/send_message_service/send_message.go +++ b/service/send_message_service/send_message.go @@ -5,6 +5,7 @@ import ( "fmt" "message-nest/models" "message-nest/pkg/constant" + "message-nest/service/send_message_service/unified" "message-nest/service/send_task_service" "message-nest/service/send_way_service" "strings" @@ -33,6 +34,11 @@ type SendMessageService struct { MarkDown string CallerIp string + // @提及相关字段 + AtMobiles []string + AtUserIds []string + AtAll bool + Status int LogOutput []string @@ -151,19 +157,33 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) { continue } - // 使用注册的处理器发送消息 - registry := GetGlobalRegistry() - handler, ok := registry.GetHandler(way.Type) + // 使用新的Channel架构发送消息 + channelRegistry := unified.GetGlobalChannelRegistry() + channel, ok := channelRegistry.GetChannel(way.Type) if !ok { sm.LogsAndStatusMark(fmt.Sprintf("发送失败:未知渠道类型 %s 的发信实例: %s\n", way.Type, ins.ID), SendFail) continue } - res, errMsg := handler.Send(msgObj, ins.SendTasksIns, typeC, sm.Title, content, sm.URL) + // 构建统一消息内容(支持@功能) + unifiedContent := &unified.UnifiedMessageContent{ + Title: sm.Title, + Text: sm.Text, + HTML: sm.HTML, + Markdown: sm.MarkDown, + URL: sm.URL, + AtMobiles: sm.AtMobiles, + AtUserIds: sm.AtUserIds, + AtAll: sm.AtAll, + } + + // 使用 SendUnified 方法(自动格式转换和@功能支持) + res, errMsg := channel.SendUnified(msgObj, ins.SendTasksIns, unifiedContent) if res != "" { sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status) + } else { + sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg)) } - sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg)) } @@ -221,18 +241,18 @@ func (sm *SendMessageService) TransError(err string) string { // 先根据实例设置的类型取,取不到或者取到的是空,则使用text发送 func (sm *SendMessageService) GetSendMsg(ins models.SendTasksIns) (string, string) { data := map[string]string{} - data["text"] = sm.Text - data["html"] = sm.HTML - data["markdown"] = sm.MarkDown + data[unified.FormatTypeText] = sm.Text + data[unified.FormatTypeHTML] = sm.HTML + data[unified.FormatTypeMarkdown] = sm.MarkDown content, ok := data[strings.ToLower(ins.ContentType)] if !ok || len(content) == 0 { - content, ok := data["text"] + content, ok := data[unified.FormatTypeText] if !ok { logrus.Error("text节点数据为空!") - return "text", "" + return unified.FormatTypeText, "" } else { logrus.Error(fmt.Sprintf("没有找到%s对应的消息,使用text消息替代!", ins.ContentType)) - return "text", content + return unified.FormatTypeText, content } } else { return strings.ToLower(ins.ContentType), content diff --git a/service/send_message_service/send_qywexin.go b/service/send_message_service/send_qywexin.go deleted file mode 100644 index 029b5ce..0000000 --- a/service/send_message_service/send_qywexin.go +++ /dev/null @@ -1,46 +0,0 @@ -package send_message_service - -import ( - "fmt" - "message-nest/models" - "message-nest/pkg/message" - "message-nest/service/send_ins_service" - "message-nest/service/send_way_service" -) - -type QyWeiXinService struct { -} - -// SendDtalkMessage 执行发送钉钉 -func (s *QyWeiXinService) SendQyWeiXinMessage(auth send_way_service.WayDetailQyWeiXin, ins models.SendTasksIns, typeC string, title string, content string) (string, string) { - insService := send_ins_service.SendTaskInsService{} - errStr, c := insService.ValidateDiffIns(ins) - if errStr != "" { - return errStr, "" - } - _, ok := c.(models.InsQyWeiXinConfig) - if !ok { - return "企业微信config校验失败", "" - } - - errMsg := "" - var res []byte - var err error - cli := message.QyWeiXin{ - AccessToken: auth.AccessToken, - } - if typeC == "text" { - res, err = cli.SendMessageText(content) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType) - } - } else if typeC == "markdown" { - res, err = cli.SendMessageMarkdown(title, content) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType) - } - } else { - errMsg = fmt.Sprintf("未知的企业微信发送内容类型:%s", ins.ContentType) - } - return string(res), errMsg -} diff --git a/service/send_message_service/send_wechat_account.go b/service/send_message_service/send_wechat_account.go deleted file mode 100644 index 971dada..0000000 --- a/service/send_message_service/send_wechat_account.go +++ /dev/null @@ -1,42 +0,0 @@ -package send_message_service - -import ( - "fmt" - "message-nest/models" - "message-nest/pkg/message" - "message-nest/service/send_ins_service" - "message-nest/service/send_way_service" -) - -type WeChatOfAccountService struct { -} - -// SendWeChatOfAccountMessage 执行发送微信公众号模板消息 -func (s *WeChatOfAccountService) SendWeChatOfAccountMessage(auth send_way_service.WeChatOFAccount, ins models.SendTasksIns, typeC string, title string, content string, url string) (string, string) { - insService := send_ins_service.SendTaskInsService{} - errStr, c := insService.ValidateDiffIns(ins) - if errStr != "" { - return errStr, "" - } - config, ok := c.(models.InsWeChatAccountConfig) - if !ok { - return "微信公众号模板消息config校验失败", "" - } - var ( - err error - res string - errMsg string - ) - cli := message.WeChatOFAccount{ - AppID: auth.AppID, - AppSecret: auth.APPSecret, - TemplateID: auth.TempID, - ToUser: config.ToAccount, - URL: url, - } - res, err = cli.Send(title, content) - if err != nil { - errMsg = fmt.Sprintf("发送失败:%s", ins.ContentType) - } - return res, errMsg -} diff --git a/service/send_message_service/unified/channel_registry.go b/service/send_message_service/unified/channel_registry.go new file mode 100644 index 0000000..5579c87 --- /dev/null +++ b/service/send_message_service/unified/channel_registry.go @@ -0,0 +1,126 @@ +package unified + +import ( + "fmt" + "message-nest/service/send_message_service/unified/channels" + "sync" +) + +// 重导出channels包的类型,方便外部使用 +type ( + Channel = channels.Channel + UnifiedMessageContent = channels.UnifiedMessageContent +) + +// 重导出常量 +const ( + FormatTypeText = channels.FormatTypeText + FormatTypeHTML = channels.FormatTypeHTML + FormatTypeMarkdown = channels.FormatTypeMarkdown + + MessageTypeEmail = channels.MessageTypeEmail + MessageTypeDtalk = channels.MessageTypeDtalk + MessageTypeQyWeiXin = channels.MessageTypeQyWeiXin + MessageTypeCustom = channels.MessageTypeCustom + MessageTypeWeChatOFAccount = channels.MessageTypeWeChatOFAccount + MessageTypeMessageNest = channels.MessageTypeMessageNest +) + +// ChannelRegistry 渠道注册表 +type ChannelRegistry struct { + channels map[string]Channel + mu sync.RWMutex +} + +// NewChannelRegistry 创建渠道注册表 +func NewChannelRegistry() *ChannelRegistry { + return &ChannelRegistry{ + channels: make(map[string]Channel), + } +} + +// Register 注册渠道 +func (r *ChannelRegistry) Register(channel Channel) { + r.mu.Lock() + defer r.mu.Unlock() + r.channels[channel.GetType()] = channel +} + +// GetChannel 获取渠道 +func (r *ChannelRegistry) GetChannel(channelType string) (Channel, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + channel, ok := r.channels[channelType] + return channel, ok +} + +// GetAllChannels 获取所有渠道 +func (r *ChannelRegistry) GetAllChannels() map[string]Channel { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make(map[string]Channel, len(r.channels)) + for k, v := range r.channels { + result[k] = v + } + return result +} + +// ListChannels 列出所有渠道 +func (r *ChannelRegistry) ListChannels() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + types := make([]string, 0, len(r.channels)) + for t := range r.channels { + types = append(types, t) + } + return types +} + +var ( + globalChannelRegistry *ChannelRegistry + channelRegistryOnce sync.Once +) + +// GetGlobalChannelRegistry 获取全局渠道注册表(单例) +func GetGlobalChannelRegistry() *ChannelRegistry { + channelRegistryOnce.Do(func() { + globalChannelRegistry = NewChannelRegistry() + + // 注册所有渠道 + globalChannelRegistry.Register(channels.NewEmailChannel()) + globalChannelRegistry.Register(channels.NewDtalkChannel()) + globalChannelRegistry.Register(channels.NewQyWeiXinChannel()) + globalChannelRegistry.Register(channels.NewCustomChannel()) + globalChannelRegistry.Register(channels.NewWeChatOFAccountChannel()) + globalChannelRegistry.Register(channels.NewMessageNestChannel()) + }) + return globalChannelRegistry +} + +// GetChannelInfo 获取渠道信息(用于调试和文档) +func GetChannelInfo(channelType string) (string, error) { + registry := GetGlobalChannelRegistry() + channel, ok := registry.GetChannel(channelType) + if !ok { + return "", fmt.Errorf("未知的渠道类型: %s", channelType) + } + + info := fmt.Sprintf("渠道: %s\n支持格式: %v\n", + channel.GetType(), + channel.GetSupportedFormats()) + return info, nil +} + +// ListAllChannels 列出所有渠道信息 +func ListAllChannels() string { + registry := GetGlobalChannelRegistry() + allChannels := registry.GetAllChannels() + + result := "已注册的渠道:\n" + for channelType, channel := range allChannels { + result += fmt.Sprintf(" - %s: %v\n", channelType, channel.GetSupportedFormats()) + } + return result +} diff --git a/service/send_message_service/unified/channels/base.go b/service/send_message_service/unified/channels/base.go new file mode 100644 index 0000000..aa9f3af --- /dev/null +++ b/service/send_message_service/unified/channels/base.go @@ -0,0 +1,45 @@ +package channels + +import "message-nest/models" + +type Channel interface { + GetType() string + GetSupportedFormats() []string + FormatContent(content *UnifiedMessageContent) (formatType string, formattedContent string, err error) + SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) +} + +type BaseChannel struct { + channelType string + supportedFormats []string +} + +func NewBaseChannel(channelType string, supportedFormats []string) *BaseChannel { + return &BaseChannel{channelType: channelType, supportedFormats: supportedFormats} +} + +func (c *BaseChannel) GetType() string { return c.channelType } +func (c *BaseChannel) GetSupportedFormats() []string { return c.supportedFormats } + +func (c *BaseChannel) FormatContent(content *UnifiedMessageContent) (string, string, error) { + for _, formatType := range c.supportedFormats { + switch formatType { + case FormatTypeMarkdown: + if content.HasMarkdown() { + return FormatTypeMarkdown, content.Markdown, nil + } + case FormatTypeHTML: + if content.HasHTML() { + return FormatTypeHTML, content.HTML, nil + } + case FormatTypeText: + if content.HasText() { + return FormatTypeText, content.Text, nil + } + } + } + if content.HasText() { + return FormatTypeText, content.Text, nil + } + return FormatTypeText, "", nil +} diff --git a/service/send_message_service/unified/channels/custom.go b/service/send_message_service/unified/channels/custom.go new file mode 100644 index 0000000..4bcc21d --- /dev/null +++ b/service/send_message_service/unified/channels/custom.go @@ -0,0 +1,83 @@ +package channels + +import ( + "encoding/json" + "fmt" + "message-nest/models" + "message-nest/pkg/message" + "message-nest/service/send_way_service" + "regexp" + "strings" +) + +type CustomChannel struct{ *BaseChannel } + +func NewCustomChannel() *CustomChannel { + return &CustomChannel{BaseChannel: NewBaseChannel(MessageTypeCustom, []string{FormatTypeText})} +} + +func (c *CustomChannel) FormatContent(content *UnifiedMessageContent) (string, string, error) { + if content.HasText() { + return FormatTypeText, content.Text, nil + } + if content.HasMarkdown() { + return FormatTypeText, markdownToText(content.Markdown), nil + } + if content.HasHTML() { + return FormatTypeText, htmlToText(content.HTML), nil + } + return FormatTypeText, "", nil +} + +func (c *CustomChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + auth, ok := msgObj.(send_way_service.WayDetailCustom) + if !ok { + return "", "类型转换失败" + } + _, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + cli := message.CustomWebhook{} + data, _ := json.Marshal(formattedContent) + dataStr := strings.Trim(string(data), "\"") + bodyStr := strings.Replace(auth.Body, "TEXT", dataStr, -1) + res, err := cli.Request(auth.Webhook, bodyStr) + var errMsg string + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + return string(res), errMsg +} + +func markdownToText(md string) string { + t := md + t = regexp.MustCompile(`#{1,6}\s+`).ReplaceAllString(t, "") + t = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(t, "$1") + t = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(t, "$1") + t = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(t, "$1") + t = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(t, "$1") + t = regexp.MustCompile(`\[([^\]]+)\]\([^)]+\)`).ReplaceAllString(t, "$1") + t = regexp.MustCompile("```[^`]*```").ReplaceAllString(t, "") + t = regexp.MustCompile("`([^`]+)`").ReplaceAllString(t, "$1") + t = regexp.MustCompile(`(?m)^>\s+`).ReplaceAllString(t, "") + t = regexp.MustCompile(`(?m)^[\*\-\+]\s+`).ReplaceAllString(t, "") + t = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(t, "") + return strings.TrimSpace(t) +} + +func htmlToText(html string) string { + t := html + t = regexp.MustCompile(`(?i)]*>.*?`).ReplaceAllString(t, "") + t = regexp.MustCompile(`(?i)]*>.*?`).ReplaceAllString(t, "") + t = regexp.MustCompile(`(?i)`).ReplaceAllString(t, "\n") + t = regexp.MustCompile(`(?i)

`).ReplaceAllString(t, "\n") + t = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(t, "") + t = strings.ReplaceAll(t, " ", " ") + t = strings.ReplaceAll(t, "<", "<") + t = strings.ReplaceAll(t, ">", ">") + t = strings.ReplaceAll(t, "&", "&") + t = strings.ReplaceAll(t, """, "\"") + t = regexp.MustCompile(`\n{3,}`).ReplaceAllString(t, "\n\n") + return strings.TrimSpace(t) +} diff --git a/service/send_message_service/unified/channels/dtalk.go b/service/send_message_service/unified/channels/dtalk.go new file mode 100644 index 0000000..7eedd2a --- /dev/null +++ b/service/send_message_service/unified/channels/dtalk.go @@ -0,0 +1,56 @@ +package channels + +import ( + "fmt" + "message-nest/models" + "message-nest/pkg/message" + "message-nest/service/send_ins_service" + "message-nest/service/send_way_service" +) + +type DtalkChannel struct{ *BaseChannel } + +func NewDtalkChannel() *DtalkChannel { + return &DtalkChannel{BaseChannel: NewBaseChannel(MessageTypeDtalk, []string{FormatTypeMarkdown, FormatTypeText})} +} + +func (c *DtalkChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + auth, ok := msgObj.(send_way_service.WayDetailDTalk) + if !ok { + return "", "类型转换失败" + } + insService := send_ins_service.SendTaskInsService{} + errStr, configInterface := insService.ValidateDiffIns(ins) + if errStr != "" { + return errStr, "" + } + _, ok = configInterface.(models.InsDtalkConfig) + if !ok { + return "钉钉config校验失败", "" + } + contentType, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + atMobiles := content.GetAtMobiles() + if content.IsAtAll() { + atMobiles = append(atMobiles, "all") + } + cli := message.Dtalk{AccessToken: auth.AccessToken, Secret: auth.Secret} + var res []byte + var errMsg string + if contentType == FormatTypeText { + res, err = cli.SendMessageText(formattedContent, atMobiles...) + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + } else if contentType == FormatTypeMarkdown { + res, err = cli.SendMessageMarkdown(content.Title, formattedContent, atMobiles...) + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + } else { + errMsg = fmt.Sprintf("未知的钉钉发送内容类型:%s", contentType) + } + return string(res), errMsg +} diff --git a/service/send_message_service/unified/channels/email.go b/service/send_message_service/unified/channels/email.go new file mode 100644 index 0000000..4926d8c --- /dev/null +++ b/service/send_message_service/unified/channels/email.go @@ -0,0 +1,46 @@ +package channels + +import ( + "fmt" + "message-nest/models" + "message-nest/pkg/message" + "message-nest/service/send_ins_service" + "message-nest/service/send_way_service" +) + +type EmailChannel struct{ *BaseChannel } + +func NewEmailChannel() *EmailChannel { + return &EmailChannel{BaseChannel: NewBaseChannel(MessageTypeEmail, []string{FormatTypeHTML, FormatTypeText})} +} + +func (c *EmailChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + auth, ok := msgObj.(send_way_service.WayDetailEmail) + if !ok { + return "", "类型转换失败" + } + insService := send_ins_service.SendTaskInsService{} + errStr, configInterface := insService.ValidateDiffIns(ins) + if errStr != "" { + return "", errStr + } + config, ok := configInterface.(models.InsEmailConfig) + if !ok { + return "", "邮箱config校验失败" + } + contentType, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + var emailer message.EmailMessage + emailer.Init(auth.Server, auth.Port, auth.Account, auth.Passwd) + var errMsg string + if contentType == FormatTypeText { + errMsg = emailer.SendTextMessage(config.ToAccount, content.Title, formattedContent) + } else if contentType == FormatTypeHTML { + errMsg = emailer.SendHtmlMessage(config.ToAccount, content.Title, formattedContent) + } else { + errMsg = fmt.Sprintf("未知的邮件发送内容类型:%s", contentType) + } + return "", errMsg +} diff --git a/service/send_message_service/unified/channels/nest.go b/service/send_message_service/unified/channels/nest.go new file mode 100644 index 0000000..17fb182 --- /dev/null +++ b/service/send_message_service/unified/channels/nest.go @@ -0,0 +1,38 @@ +package channels + +import ( + "message-nest/models" + "message-nest/service/hosted_message_service" + "message-nest/service/send_way_service" +) + +type MessageNestChannel struct{ *BaseChannel } + +func NewMessageNestChannel() *MessageNestChannel { + return &MessageNestChannel{BaseChannel: NewBaseChannel(MessageTypeMessageNest, []string{FormatTypeMarkdown, FormatTypeHTML, FormatTypeText})} +} + +func (c *MessageNestChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + _, ok := msgObj.(send_way_service.MessageNest) + if !ok { + return "", "类型转换失败" + } + contentType, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + messageService := hosted_message_service.HostMessageService{ + Title: content.Title, + Content: formattedContent, + Type: contentType, + } + err = messageService.Add() + var res, errMsg string + if err != nil { + errMsg = err.Error() + res = "托管消息创建失败!" + } else { + res = "托管消息创建成功!" + } + return res, errMsg +} diff --git a/service/send_message_service/unified/channels/qyweixin.go b/service/send_message_service/unified/channels/qyweixin.go new file mode 100644 index 0000000..50377ef --- /dev/null +++ b/service/send_message_service/unified/channels/qyweixin.go @@ -0,0 +1,58 @@ +package channels + +import ( + "fmt" + "message-nest/models" + "message-nest/pkg/message" + "message-nest/service/send_ins_service" + "message-nest/service/send_way_service" +) + +type QyWeiXinChannel struct{ *BaseChannel } + +func NewQyWeiXinChannel() *QyWeiXinChannel { + return &QyWeiXinChannel{BaseChannel: NewBaseChannel(MessageTypeQyWeiXin, []string{FormatTypeMarkdown, FormatTypeText})} +} + +func (c *QyWeiXinChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + auth, ok := msgObj.(send_way_service.WayDetailQyWeiXin) + if !ok { + return "", "类型转换失败" + } + insService := send_ins_service.SendTaskInsService{} + errStr, configInterface := insService.ValidateDiffIns(ins) + if errStr != "" { + return errStr, "" + } + _, ok = configInterface.(models.InsQyWeiXinConfig) + if !ok { + return "企业微信config校验失败", "" + } + contentType, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + atList := []string{} + atList = append(atList, content.GetAtUserIds()...) + atList = append(atList, content.GetAtMobiles()...) + if content.IsAtAll() { + atList = append(atList, "@all") + } + cli := message.QyWeiXin{AccessToken: auth.AccessToken} + var res []byte + var errMsg string + if contentType == FormatTypeText { + res, err = cli.SendMessageText(formattedContent, atList...) + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + } else if contentType == FormatTypeMarkdown { + res, err = cli.SendMessageMarkdown(content.Title, formattedContent, atList...) + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + } else { + errMsg = fmt.Sprintf("未知的企业微信发送内容类型:%s", contentType) + } + return string(res), errMsg +} diff --git a/service/send_message_service/unified/channels/types.go b/service/send_message_service/unified/channels/types.go new file mode 100644 index 0000000..909b891 --- /dev/null +++ b/service/send_message_service/unified/channels/types.go @@ -0,0 +1,53 @@ +package channels + +// 消息格式类型常量 +const ( + FormatTypeText = "text" + FormatTypeHTML = "html" + FormatTypeMarkdown = "markdown" +) + +// 消息类型常量 +const ( + MessageTypeEmail = "Email" + MessageTypeDtalk = "Dtalk" + MessageTypeQyWeiXin = "QyWeiXin" + MessageTypeCustom = "Custom" + MessageTypeWeChatOFAccount = "WeChatOFAccount" + MessageTypeMessageNest = "MessageNest" +) + +// UnifiedMessageContent 统一的消息内容结构 +type UnifiedMessageContent struct { + Title string + URL string + Text string + HTML string + Markdown string + AtMobiles []string + AtUserIds []string + AtAll bool + Summary string + ImageURL string + Extra map[string]interface{} +} + +func (m *UnifiedMessageContent) HasText() bool { return m.Text != "" } +func (m *UnifiedMessageContent) HasHTML() bool { return m.HTML != "" } +func (m *UnifiedMessageContent) HasMarkdown() bool { return m.Markdown != "" } + +func (m *UnifiedMessageContent) GetAtMobiles() []string { + if m.AtMobiles == nil { + return []string{} + } + return m.AtMobiles +} + +func (m *UnifiedMessageContent) GetAtUserIds() []string { + if m.AtUserIds == nil { + return []string{} + } + return m.AtUserIds +} + +func (m *UnifiedMessageContent) IsAtAll() bool { return m.AtAll } diff --git a/service/send_message_service/unified/channels/wechat.go b/service/send_message_service/unified/channels/wechat.go new file mode 100644 index 0000000..6743ee9 --- /dev/null +++ b/service/send_message_service/unified/channels/wechat.go @@ -0,0 +1,48 @@ +package channels + +import ( + "fmt" + "message-nest/models" + "message-nest/pkg/message" + "message-nest/service/send_ins_service" + "message-nest/service/send_way_service" +) + +type WeChatOFAccountChannel struct{ *BaseChannel } + +func NewWeChatOFAccountChannel() *WeChatOFAccountChannel { + return &WeChatOFAccountChannel{BaseChannel: NewBaseChannel(MessageTypeWeChatOFAccount, []string{FormatTypeText})} +} + +func (c *WeChatOFAccountChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) { + auth, ok := msgObj.(send_way_service.WeChatOFAccount) + if !ok { + return "", "类型转换失败" + } + insService := send_ins_service.SendTaskInsService{} + errStr, configInterface := insService.ValidateDiffIns(ins) + if errStr != "" { + return errStr, "" + } + config, ok := configInterface.(models.InsWeChatAccountConfig) + if !ok { + return "微信公众号模板消息config校验失败", "" + } + _, formattedContent, err := c.FormatContent(content) + if err != nil { + return "", err.Error() + } + cli := message.WeChatOFAccount{ + AppID: auth.AppID, + AppSecret: auth.APPSecret, + TemplateID: auth.TempID, + ToUser: config.ToAccount, + URL: content.URL, + } + res, err := cli.Send(content.Title, formattedContent) + var errMsg string + if err != nil { + errMsg = fmt.Sprintf("发送失败:%s", err.Error()) + } + return res, errMsg +} diff --git a/web/src/components/pages/sendTasks/ApiCodeViewer.vue b/web/src/components/pages/sendTasks/ApiCodeViewer.vue index 183857d..59641a2 100644 --- a/web/src/components/pages/sendTasks/ApiCodeViewer.vue +++ b/web/src/components/pages/sendTasks/ApiCodeViewer.vue @@ -42,6 +42,14 @@ export default defineComponent({ // 当前选中的标签 const activeTab = ref('curl') + // 可选参数选项 + const showHtml = ref(false) + const showMarkdown = ref(false) + const showUrl = ref(false) + const showAtMobiles = ref(false) + const showAtUserIds = ref(false) + const showAtAll = ref(false) + // 代码语言选项 const codeLanguages = [ { value: 'curl', label: 'cURL', icon: '🌐' }, @@ -56,7 +64,14 @@ export default defineComponent({ // 生成API代码示例 const generateApiCode = (language: string) => { const taskId = props.taskData?.id || 'TASK_ID' - const options = { html: false, markdown: false, url: false } + const options = { + html: showHtml.value, + markdown: showMarkdown.value, + url: showUrl.value, + at_mobiles: showAtMobiles.value, + at_user_ids: showAtUserIds.value, + at_all: showAtAll.value + } switch (language) { case 'curl': @@ -91,6 +106,12 @@ export default defineComponent({ return { handleUpdateOpen, activeTab, + showHtml, + showMarkdown, + showUrl, + showAtMobiles, + showAtUserIds, + showAtAll, codeLanguages, generateApiCode, copyToClipboard @@ -122,6 +143,41 @@ export default defineComponent({ --> + +
+

可选参数

+
+ + + + + + +
+

💡 提示:@功能仅钉钉和企业微信支持

+
+

代码示例

diff --git a/web/src/util/viewApi.js b/web/src/util/viewApi.js index 188619f..751be02 100644 --- a/web/src/util/viewApi.js +++ b/web/src/util/viewApi.js @@ -40,6 +40,16 @@ class ApiStrGenerate { if (options.url) { data.url = 'https://github.com'; } + // @提及功能参数(可选) + if (options.at_mobiles) { + data.at_mobiles = ['13800138000', '13900139000']; + } + if (options.at_user_ids) { + data.at_user_ids = ['zhangsan', 'lisi']; + } + if (options.at_all) { + data.at_all = true; + } let dataStr = JSON.stringify(data, null, 4); return dataStr }