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
@@ -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
}