feat: send recode adjust

This commit is contained in:
engigu
2025-12-04 21:16:32 +08:00
parent 0639f5ec6d
commit ff194daef2
22 changed files with 743 additions and 424 deletions
+40
View File
@@ -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
}
+36 -3
View File
@@ -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
}
+16 -8
View File
@@ -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()
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
+31 -11
View File
@@ -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
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)<script[^>]*>.*?</script>`).ReplaceAllString(t, "")
t = regexp.MustCompile(`(?i)<style[^>]*>.*?</style>`).ReplaceAllString(t, "")
t = regexp.MustCompile(`(?i)<br\s*/?>`).ReplaceAllString(t, "\n")
t = regexp.MustCompile(`(?i)</p>`).ReplaceAllString(t, "\n")
t = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(t, "")
t = strings.ReplaceAll(t, "&nbsp;", " ")
t = strings.ReplaceAll(t, "&lt;", "<")
t = strings.ReplaceAll(t, "&gt;", ">")
t = strings.ReplaceAll(t, "&amp;", "&")
t = strings.ReplaceAll(t, "&quot;", "\"")
t = regexp.MustCompile(`\n{3,}`).ReplaceAllString(t, "\n\n")
return strings.TrimSpace(t)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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 }
@@ -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
}
@@ -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({
</div>
</div> -->
<!-- 可选参数 -->
<div class="border rounded-lg p-4 space-y-3">
<h3 class="font-semibold text-sm">可选参数</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showHtml" class="rounded">
<span class="text-sm">HTML</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showMarkdown" class="rounded">
<span class="text-sm">Markdown</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showUrl" class="rounded">
<span class="text-sm">URL</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showAtMobiles" class="rounded">
<span class="text-sm">@手机号</span>
<Badge variant="secondary" class="text-xs"></Badge>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showAtUserIds" class="rounded">
<span class="text-sm">@用户ID</span>
<Badge variant="secondary" class="text-xs"></Badge>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showAtAll" class="rounded">
<span class="text-sm">@所有人</span>
<Badge variant="secondary" class="text-xs"></Badge>
</label>
</div>
<p class="text-xs text-gray-500">💡 提示@功能仅钉钉和企业微信支持</p>
</div>
<!-- 代码示例 -->
<div class="space-y-4">
<h3 class="font-semibold">代码示例</h3>
+10
View File
@@ -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
}