From 0639f5ec6d7425b48b133b65747e34c28039ba98 Mon Sep 17 00:00:00 2001
From: engigu
Date: Mon, 17 Nov 2025 22:33:59 +0800
Subject: [PATCH 01/24] feat: send message rewrite
---
.../send_message_service/message_handler.go | 164 ++++++++++++++++++
service/send_message_service/send_message.go | 60 +------
2 files changed, 173 insertions(+), 51 deletions(-)
create mode 100644 service/send_message_service/message_handler.go
diff --git a/service/send_message_service/message_handler.go b/service/send_message_service/message_handler.go
new file mode 100644
index 0000000..f74247a
--- /dev/null
+++ b/service/send_message_service/message_handler.go
@@ -0,0 +1,164 @@
+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_message.go b/service/send_message_service/send_message.go
index 025df9d..9981125 100644
--- a/service/send_message_service/send_message.go
+++ b/service/send_message_service/send_message.go
@@ -151,61 +151,19 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) {
continue
}
- // 邮箱类型的实例发送
- emailAuth, ok := msgObj.(send_way_service.WayDetailEmail)
- if ok {
- //continue
- es := EmailService{}
- errMsg := es.SendTaskEmail(emailAuth, ins.SendTasksIns, typeC, sm.Title, content)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
+ // 使用注册的处理器发送消息
+ registry := GetGlobalRegistry()
+ handler, ok := registry.GetHandler(way.Type)
+ if !ok {
+ sm.LogsAndStatusMark(fmt.Sprintf("发送失败:未知渠道类型 %s 的发信实例: %s\n", way.Type, ins.ID), SendFail)
continue
}
- // 钉钉类型的实例发送
- dtalkAuth, ok := msgObj.(send_way_service.WayDetailDTalk)
- if ok {
- es := DtalkService{}
- res, errMsg := es.SendDtalkMessage(dtalkAuth, ins.SendTasksIns, typeC, sm.Title, content)
+
+ res, errMsg := handler.Send(msgObj, ins.SendTasksIns, typeC, sm.Title, content, sm.URL)
+ if res != "" {
sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
- continue
}
- // 企业微信类型的实例发送
- qywxAuth, ok := msgObj.(send_way_service.WayDetailQyWeiXin)
- if ok {
- es := QyWeiXinService{}
- res, errMsg := es.SendQyWeiXinMessage(qywxAuth, ins.SendTasksIns, typeC, sm.Title, content)
- sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
- continue
- }
- // 自定义webhook类型的实例发送
- customAuth, ok := msgObj.(send_way_service.WayDetailCustom)
- if ok {
- cs := CustomService{}
- res, errMsg := cs.SendCustomMessage(customAuth, ins.SendTasksIns, typeC, sm.Title, content)
- sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
- continue
- }
- // 微信公众号模板消息的实例发送
- wca, ok := msgObj.(send_way_service.WeChatOFAccount)
- if ok {
- cs := WeChatOfAccountService{}
- res, errMsg := cs.SendWeChatOfAccountMessage(wca, ins.SendTasksIns, typeC, sm.Title, content, sm.URL)
- sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
- continue
- }
- // 托管消息的实例发送
- mnt, ok := msgObj.(send_way_service.MessageNest)
- if ok {
- cs := HostMessageService{}
- res, errMsg := cs.SendHostMessage(mnt, ins.SendTasksIns, typeC, sm.Title, content)
- sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
- sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
- continue
- }
- sm.LogsAndStatusMark(fmt.Sprintf("发送失败:未知渠道的发信实例: %s\n", ins.ID), SendFail)
+ sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
}
From ff194daef28ae13e5992a6a573dce0c3984c7cdd Mon Sep 17 00:00:00 2001
From: engigu
Date: Thu, 4 Dec 2025 21:16:32 +0800
Subject: [PATCH 02/24] feat: send recode adjust
---
pkg/message/dtalk.go | 40 +++++
pkg/message/qyweixin.go | 39 ++++-
routers/api/v1/send_message.go | 24 ++-
.../send_message_service/message_handler.go | 164 ------------------
service/send_message_service/send_custom.go | 29 ----
service/send_message_service/send_dtalk.go | 47 -----
service/send_message_service/send_email.go | 37 ----
.../send_hosted_message.go | 36 ----
service/send_message_service/send_message.go | 42 +++--
service/send_message_service/send_qywexin.go | 46 -----
.../send_wechat_account.go | 42 -----
.../unified/channel_registry.go | 126 ++++++++++++++
.../unified/channels/base.go | 45 +++++
.../unified/channels/custom.go | 83 +++++++++
.../unified/channels/dtalk.go | 56 ++++++
.../unified/channels/email.go | 46 +++++
.../unified/channels/nest.go | 38 ++++
.../unified/channels/qyweixin.go | 58 +++++++
.../unified/channels/types.go | 53 ++++++
.../unified/channels/wechat.go | 48 +++++
.../pages/sendTasks/ApiCodeViewer.vue | 58 ++++++-
web/src/util/viewApi.js | 10 ++
22 files changed, 743 insertions(+), 424 deletions(-)
delete mode 100644 service/send_message_service/message_handler.go
delete mode 100644 service/send_message_service/send_custom.go
delete mode 100644 service/send_message_service/send_dtalk.go
delete mode 100644 service/send_message_service/send_email.go
delete mode 100644 service/send_message_service/send_hosted_message.go
delete mode 100644 service/send_message_service/send_qywexin.go
delete mode 100644 service/send_message_service/send_wechat_account.go
create mode 100644 service/send_message_service/unified/channel_registry.go
create mode 100644 service/send_message_service/unified/channels/base.go
create mode 100644 service/send_message_service/unified/channels/custom.go
create mode 100644 service/send_message_service/unified/channels/dtalk.go
create mode 100644 service/send_message_service/unified/channels/email.go
create mode 100644 service/send_message_service/unified/channels/nest.go
create mode 100644 service/send_message_service/unified/channels/qyweixin.go
create mode 100644 service/send_message_service/unified/channels/types.go
create mode 100644 service/send_message_service/unified/channels/wechat.go
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
}
From a5115127930149e62290f27b92cd586a681efb78 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 00:28:18 +0800
Subject: [PATCH 03/24] feat: add message template
---
middleware/jwt.go | 4 +-
migrate/migrate.go | 6 +-
models/message_template.go | 149 +++++++
models/send_ins.go | 23 +
models/send_tasks_logs.go | 81 +++-
routers/api/v1/message_template.go | 336 +++++++++++++++
routers/api/v1/send_message.go | 1 +
routers/api/v2/send_message.go | 146 +++++++
routers/router.go | 21 +
.../message_template.go | 185 +++++++++
service/send_message_service/send_message.go | 213 ++++++++--
web/src/components/Index.vue | 5 +-
.../pages/messageTemplate/MessageTemplate.vue | 380 +++++++++++++++++
.../messageTemplate/TemplateApiViewer.vue | 180 ++++++++
.../pages/messageTemplate/TemplateEditor.vue | 392 ++++++++++++++++++
.../TemplateInstanceConfig.vue | 352 ++++++++++++++++
.../components/pages/sendLogs/SendLogs.vue | 40 +-
.../pages/sendTasks/ApiCodeViewer.vue | 5 +-
web/src/constant.js | 2 +-
web/src/router/index.js | 5 +
web/src/util/viewApi.js | 237 +++++++----
21 files changed, 2608 insertions(+), 155 deletions(-)
create mode 100644 models/message_template.go
create mode 100644 routers/api/v1/message_template.go
create mode 100644 routers/api/v2/send_message.go
create mode 100644 service/message_template_service/message_template.go
create mode 100644 web/src/components/pages/messageTemplate/MessageTemplate.vue
create mode 100644 web/src/components/pages/messageTemplate/TemplateApiViewer.vue
create mode 100644 web/src/components/pages/messageTemplate/TemplateEditor.vue
create mode 100644 web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
diff --git a/middleware/jwt.go b/middleware/jwt.go
index f498fda..0e53e45 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -2,9 +2,10 @@ package middleware
import (
"errors"
- "github.com/golang-jwt/jwt/v5"
"net/http"
+ "github.com/golang-jwt/jwt/v5"
+
"github.com/gin-gonic/gin"
"message-nest/pkg/e"
@@ -13,6 +14,7 @@ import (
var ExcludedRoutes = []string{
"/api/v1/message/send",
+ "/api/v2/message/send",
"/api/v1/settings/getsetting",
}
diff --git a/migrate/migrate.go b/migrate/migrate.go
index 2213b7d..9499ccd 100644
--- a/migrate/migrate.go
+++ b/migrate/migrate.go
@@ -3,10 +3,11 @@ package migrate
import (
"errors"
"fmt"
- "github.com/sirupsen/logrus"
- "gorm.io/gorm"
"message-nest/models"
"message-nest/service/settings_service"
+
+ "github.com/sirupsen/logrus"
+ "gorm.io/gorm"
)
// 初始化admin账户
@@ -67,6 +68,7 @@ func Setup() {
&models.CronMessages{},
&models.HostedMessage{},
&models.LoginLog{},
+ &models.MessageTemplate{},
}
for _, table := range tables {
diff --git a/models/message_template.go b/models/message_template.go
new file mode 100644
index 0000000..d8f65e6
--- /dev/null
+++ b/models/message_template.go
@@ -0,0 +1,149 @@
+package models
+
+import (
+ "fmt"
+ "message-nest/pkg/util"
+)
+
+// GenerateTemplateUniqueID 生成模板唯一ID
+func GenerateTemplateUniqueID() string {
+ newUUID := util.GenerateUniqueID()
+ return fmt.Sprintf("TP%s", newUUID)
+}
+
+// MessageTemplate 消息模板
+type MessageTemplate struct {
+ UUIDModel
+
+ Name string `json:"name" gorm:"type:varchar(200);not null;index" binding:"required"`
+ Description string `json:"description" gorm:"type:text"`
+
+ // 模板内容(带占位符)
+ TextTemplate string `json:"text_template" gorm:"type:text"`
+ HTMLTemplate string `json:"html_template" gorm:"type:text"`
+ MarkdownTemplate string `json:"markdown_template" gorm:"type:text"`
+
+ // 占位符定义(JSON格式)
+ Placeholders string `json:"placeholders" gorm:"type:text"`
+
+ // @提醒配置
+ AtMobiles string `json:"at_mobiles" gorm:"type:text;comment:'@手机号列表,逗号分隔'"`
+ AtUserIds string `json:"at_user_ids" gorm:"type:text;comment:'@用户ID列表,逗号分隔'"`
+ IsAtAll bool `json:"is_at_all" gorm:"default:false;comment:'是否@所有人'"`
+
+ // 状态:enabled/disabled
+ Status string `json:"status" gorm:"type:varchar(20);default:'enabled';index"`
+}
+
+// Add 添加消息模板
+func (t *MessageTemplate) Add() error {
+ if err := db.Create(&t).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// Update 更新消息模板
+func (t *MessageTemplate) Update() error {
+ if err := db.Model(&MessageTemplate{}).Where("id = ?", t.ID).Updates(t).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// Delete 删除消息模板
+func (t *MessageTemplate) Delete() error {
+ if err := db.Where("id = ?", t.ID).Delete(&MessageTemplate{}).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// MessageTemplateResult 消息模板查询结果
+type MessageTemplateResult struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ TextTemplate string `json:"text_template"`
+ HTMLTemplate string `json:"html_template"`
+ MarkdownTemplate string `json:"markdown_template"`
+ Placeholders string `json:"placeholders"`
+ AtMobiles string `json:"at_mobiles"`
+ AtUserIds string `json:"at_user_ids"`
+ IsAtAll bool `json:"is_at_all"`
+ Status string `json:"status"`
+ CreatedBy string `json:"created_by"`
+ ModifiedBy string `json:"modified_by"`
+ CreatedOn util.Time `json:"created_on"`
+ ModifiedOn util.Time `json:"modified_on"`
+}
+
+// GetMessageTemplates 获取消息模板列表
+func GetMessageTemplates(pageNum int, pageSize int, text string, maps map[string]interface{}) ([]MessageTemplateResult, error) {
+ var datas []MessageTemplateResult
+ templateT := GetSchema(MessageTemplate{})
+
+ query := db.Table(templateT)
+ query = query.Where(maps)
+
+ if text != "" {
+ query = query.Where("name LIKE ? OR description LIKE ?",
+ fmt.Sprintf("%%%s%%", text),
+ fmt.Sprintf("%%%s%%", text))
+ }
+
+ query = query.Order("created_on DESC")
+
+ if pageSize > 0 || pageNum > 0 {
+ query = query.Offset(pageNum).Limit(pageSize)
+ }
+
+ query.Scan(&datas)
+ return datas, nil
+}
+
+// GetMessageTemplatesTotal 获取消息模板总数
+func GetMessageTemplatesTotal(text string, maps map[string]interface{}) (int64, error) {
+ var total int64
+ templateT := GetSchema(MessageTemplate{})
+
+ query := db.Table(templateT)
+ query = query.Where(maps)
+
+ if text != "" {
+ query = query.Where("name LIKE ? OR description LIKE ?",
+ fmt.Sprintf("%%%s%%", text),
+ fmt.Sprintf("%%%s%%", text))
+ }
+
+ query.Count(&total)
+ return total, nil
+}
+
+// GetMessageTemplateByID 根据ID获取消息模板
+func GetMessageTemplateByID(id string) (*MessageTemplateResult, error) {
+ var data MessageTemplateResult
+ templateT := GetSchema(MessageTemplate{})
+
+ err := db.Table(templateT).Where("id = ?", id).First(&data).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+// ExistMessageTemplateByID 检查模板是否存在
+func ExistMessageTemplateByID(id string) (bool, error) {
+ var template MessageTemplate
+ err := db.Select("id").Where("id = ?", id).First(&template).Error
+ if err != nil {
+ return false, err
+ }
+
+ if template.ID != "" {
+ return true, nil
+ }
+
+ return false, nil
+}
diff --git a/models/send_ins.go b/models/send_ins.go
index c8049d3..1325d39 100644
--- a/models/send_ins.go
+++ b/models/send_ins.go
@@ -1,9 +1,12 @@
package models
+import "fmt"
+
type SendTasksIns struct {
UUIDModel
TaskID string `json:"task_id" gorm:"type:varchar(12) ;default:'';index"`
+ TemplateID string `json:"template_id" gorm:"type:varchar(12) ;default:'';index"` // 模板ID
WayID string `json:"way_id" gorm:"type:varchar(12) ;default:'';index"`
WayType string `json:"way_type" gorm:"type:varchar(100) ;default:'';index"`
ContentType string `json:"content_type" gorm:"type:varchar(100) ;default:'';index"`
@@ -82,3 +85,23 @@ func UpdateMsgTaskIns(id string, data map[string]interface{}) error {
}
return nil
}
+
+// GetTemplateInsList 获取模板关联的实例列表(包含渠道名称)
+func GetTemplateInsList(templateID string) ([]SendTasksInsRes, error) {
+ insTable := GetSchema(SendTasksIns{})
+ waysTable := GetSchema(SendWays{})
+ var insList []SendTasksInsRes
+
+ err := db.
+ Table(insTable).
+ Select(fmt.Sprintf("%s.*, %s.name as way_name", insTable, waysTable)).
+ Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", waysTable, insTable, waysTable)).
+ Where(fmt.Sprintf("%s.template_id = ?", insTable), templateID).
+ Order(fmt.Sprintf("%s.created_on DESC", insTable)).
+ Scan(&insList).Error
+
+ if err != nil {
+ return nil, err
+ }
+ return insList, nil
+}
diff --git a/models/send_tasks_logs.go b/models/send_tasks_logs.go
index f185a67..dd4a210 100644
--- a/models/send_tasks_logs.go
+++ b/models/send_tasks_logs.go
@@ -9,6 +9,8 @@ import (
type SendTasksLogs struct {
ID int `gorm:"primaryKey" json:"id" `
TaskID string `json:"task_id" gorm:"type:varchar(12) ;default:'';index:task_id"`
+ Type string `json:"type" gorm:"type:varchar(20) ;default:'task';comment:'类型:task-任务,template-模板'"`
+ Name string `json:"name" gorm:"type:varchar(256) ;default:'';comment:'任务或模板名称'"`
Log string `json:"log" gorm:"type:text ;"`
Status *int `json:"status" gorm:"type:int ;default:0;"`
CallerIp string `json:"caller_ip" gorm:"type:varchar(256) ;default:'';"`
@@ -29,10 +31,11 @@ func (log *SendTasksLogs) Add() error {
type LogsResult struct {
ID int `json:"id"`
TaskID string `json:"task_id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
Log string `json:"log"`
CreatedOn util.Time `json:"created_on"`
ModifiedOn util.Time `json:"modified_on"`
- TaskName string `json:"task_name"`
Status int `json:"status"`
CallerIp string `json:"caller_ip"`
}
@@ -41,12 +44,9 @@ type LogsResult struct {
func GetSendLogs(pageNum int, pageSize int, name string, taskId string, maps map[string]interface{}) ([]LogsResult, error) {
var logs []LogsResult
logt := GetSchema(SendTasksLogs{})
- taskt := GetSchema(SendTasks{})
- query := db.
- Table(logt).
- Select(fmt.Sprintf("%s.*, %s.name as task_name", logt, taskt)).
- Joins(fmt.Sprintf("LEFT JOIN %s ON %s.task_id = %s.id", taskt, logt, taskt))
+ // 简化查询,只查询日志表
+ query := db.Table(logt)
dayVal, ok := maps["day_created_on"]
if ok {
@@ -55,8 +55,10 @@ func GetSendLogs(pageNum int, pageSize int, name string, taskId string, maps map
}
query = query.Where(maps)
+
+ // 按名称搜索(搜索日志表的 name 字段)
if name != "" {
- query = query.Where(fmt.Sprintf("%s.name like ?", taskt), fmt.Sprintf("%%%s%%", name))
+ query = query.Where(fmt.Sprintf("%s.name like ?", logt), fmt.Sprintf("%%%s%%", name))
}
if taskId != "" {
query = query.Where(fmt.Sprintf("%s.task_id = ?", logt), taskId)
@@ -66,18 +68,71 @@ func GetSendLogs(pageNum int, pageSize int, name string, taskId string, maps map
query = query.Offset(pageNum).Limit(pageSize)
}
query.Scan(&logs)
+
+ //v1 接口的历史日志数据兼容处理
+ // 应用层处理:为历史数据(type=task 且 name 为空)补充任务名称
+ fillTaskNamesForLogs(&logs)
return logs, nil
}
+// fillTaskNamesForLogs 为历史日志数据补充任务名称
+func fillTaskNamesForLogs(logs *[]LogsResult) {
+ if logs == nil || len(*logs) == 0 {
+ return
+ }
+
+ // 收集需要查询的 task_id
+ taskIdsMap := make(map[string]bool)
+ for _, log := range *logs {
+ // 只处理 type=task 且 name 为空的记录
+ if (log.Type == "" || log.Type == "task") && log.Name == "" && log.TaskID != "" {
+ taskIdsMap[log.TaskID] = true
+ }
+ }
+
+ // 如果没有需要查询的任务,直接返回
+ if len(taskIdsMap) == 0 {
+ return
+ }
+
+ // 批量查询任务名称
+ taskIds := make([]string, 0, len(taskIdsMap))
+ for taskId := range taskIdsMap {
+ taskIds = append(taskIds, taskId)
+ }
+
+ var tasks []SendTasks
+ taskt := GetSchema(SendTasks{})
+ db.Table(taskt).
+ Select("id, name").
+ Where("id IN ?", taskIds).
+ Scan(&tasks)
+
+ // 构建 taskId -> name 的映射
+ taskNameMap := make(map[string]string)
+ for _, task := range tasks {
+ taskNameMap[task.ID] = task.Name
+ }
+
+ // 填充日志的 name 字段
+ for i := range *logs {
+ log := &(*logs)[i]
+ if (log.Type == "" || log.Type == "task") && log.Name == "" && log.TaskID != "" {
+ if taskName, exists := taskNameMap[log.TaskID]; exists {
+ log.Name = taskName
+ }
+ }
+ }
+}
+
// GetSendLogsTotal 获取所有日志总数
func GetSendLogsTotal(name string, taskId string, maps map[string]interface{}) (int64, error) {
var total int64
logt := GetSchema(SendTasksLogs{})
- taskt := GetSchema(SendTasks{})
- query := db.
- Table(logt).
- Joins(fmt.Sprintf("LEFT JOIN %s ON %s.task_id = %s.id", taskt, logt, taskt))
+
+ // 简化查询,只查询日志表
+ query := db.Table(logt)
dayVal, ok := maps["day_created_on"]
if ok {
@@ -86,8 +141,10 @@ func GetSendLogsTotal(name string, taskId string, maps map[string]interface{}) (
}
query = query.Where(maps)
+
+ // 按名称搜索(搜索日志表的 name 字段)
if name != "" {
- query = query.Where(fmt.Sprintf("%s.name like ?", taskt), fmt.Sprintf("%%%s%%", name))
+ query = query.Where(fmt.Sprintf("%s.name like ?", logt), fmt.Sprintf("%%%s%%", name))
}
if taskId != "" {
query = query.Where(fmt.Sprintf("%s.task_id = ?", logt), taskId)
diff --git a/routers/api/v1/message_template.go b/routers/api/v1/message_template.go
new file mode 100644
index 0000000..759bbb2
--- /dev/null
+++ b/routers/api/v1/message_template.go
@@ -0,0 +1,336 @@
+package v1
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "message-nest/models"
+ "message-nest/pkg/app"
+ "message-nest/pkg/e"
+ "message-nest/pkg/util"
+ "message-nest/service/message_template_service"
+ "message-nest/service/send_ins_service"
+ "net/http"
+)
+
+// GetMessageTemplateList 获取消息模板列表
+func GetMessageTemplateList(c *gin.Context) {
+ appG := app.Gin{C: c}
+ text := c.Query("text")
+ status := c.Query("status")
+
+ offset, limit := util.GetPageSize(c)
+ templateService := message_template_service.MessageTemplateService{
+ Text: text,
+ Status: status,
+ PageNum: offset,
+ PageSize: limit,
+ }
+
+ templates, err := templateService.GetAll()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "获取消息模板失败!", nil)
+ return
+ }
+
+ count, err := templateService.Count()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "获取消息模板总数失败!", nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "获取消息模板成功", map[string]interface{}{
+ "lists": templates,
+ "total": count,
+ })
+}
+
+// GetMessageTemplate 获取单个消息模板
+func GetMessageTemplate(c *gin.Context) {
+ appG := app.Gin{C: c}
+ id := c.Query("id")
+
+ templateService := message_template_service.MessageTemplateService{
+ ID: id,
+ }
+
+ exists, err := templateService.ExistByID()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
+ return
+ }
+
+ if !exists {
+ appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
+ return
+ }
+
+ template, err := templateService.Get()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "获取模板详情失败!", nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "获取模板详情成功", template)
+}
+
+// AddMessageTemplate 添加消息模板
+func AddMessageTemplate(c *gin.Context) {
+ appG := app.Gin{C: c}
+
+ var req struct {
+ Name string `json:"name" binding:"required"`
+ Description string `json:"description"`
+ TextTemplate string `json:"text_template"`
+ HTMLTemplate string `json:"html_template"`
+ MarkdownTemplate string `json:"markdown_template"`
+ Placeholders string `json:"placeholders"`
+ AtMobiles string `json:"at_mobiles"`
+ AtUserIds string `json:"at_user_ids"`
+ IsAtAll bool `json:"is_at_all"`
+ Status string `json:"status"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
+ return
+ }
+
+ if req.TextTemplate == "" && req.HTMLTemplate == "" && req.MarkdownTemplate == "" {
+ appG.CResponse(http.StatusBadRequest, "至少需要填写一种格式的模板内容", nil)
+ return
+ }
+
+ if req.Status == "" {
+ req.Status = "enabled"
+ }
+
+ templateService := message_template_service.MessageTemplateService{
+ Name: req.Name,
+ Description: req.Description,
+ TextTemplate: req.TextTemplate,
+ HTMLTemplate: req.HTMLTemplate,
+ MarkdownTemplate: req.MarkdownTemplate,
+ Placeholders: req.Placeholders,
+ AtMobiles: req.AtMobiles,
+ AtUserIds: req.AtUserIds,
+ IsAtAll: req.IsAtAll,
+ Status: req.Status,
+ }
+
+ if err := templateService.Add(); err != nil {
+ appG.CResponse(http.StatusInternalServerError, "添加模板失败:"+err.Error(), nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "添加模板成功", nil)
+}
+
+// EditMessageTemplate 编辑消息模板
+func EditMessageTemplate(c *gin.Context) {
+ appG := app.Gin{C: c}
+
+ var req struct {
+ ID string `json:"id" binding:"required"`
+ Name string `json:"name" binding:"required"`
+ Description string `json:"description"`
+ TextTemplate string `json:"text_template"`
+ HTMLTemplate string `json:"html_template"`
+ MarkdownTemplate string `json:"markdown_template"`
+ Placeholders string `json:"placeholders"`
+ AtMobiles string `json:"at_mobiles"`
+ AtUserIds string `json:"at_user_ids"`
+ IsAtAll bool `json:"is_at_all"`
+ Status string `json:"status"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
+ return
+ }
+
+ if req.TextTemplate == "" && req.HTMLTemplate == "" && req.MarkdownTemplate == "" {
+ appG.CResponse(http.StatusBadRequest, "至少需要填写一种格式的模板内容", nil)
+ return
+ }
+
+ templateService := message_template_service.MessageTemplateService{
+ ID: req.ID,
+ }
+
+ exists, err := templateService.ExistByID()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
+ return
+ }
+
+ if !exists {
+ appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
+ return
+ }
+
+ templateService.Name = req.Name
+ templateService.Description = req.Description
+ templateService.TextTemplate = req.TextTemplate
+ templateService.HTMLTemplate = req.HTMLTemplate
+ templateService.MarkdownTemplate = req.MarkdownTemplate
+ templateService.Placeholders = req.Placeholders
+ templateService.AtMobiles = req.AtMobiles
+ templateService.AtUserIds = req.AtUserIds
+ templateService.IsAtAll = req.IsAtAll
+ templateService.Status = req.Status
+
+ if err := templateService.Update(); err != nil {
+ appG.CResponse(http.StatusInternalServerError, "更新模板失败:"+err.Error(), nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "更新模板成功", nil)
+}
+
+// DeleteMessageTemplate 删除消息模板
+func DeleteMessageTemplate(c *gin.Context) {
+ appG := app.Gin{C: c}
+
+ var req struct {
+ ID string `json:"id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
+ return
+ }
+
+ templateService := message_template_service.MessageTemplateService{
+ ID: req.ID,
+ }
+
+ exists, err := templateService.ExistByID()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
+ return
+ }
+
+ if !exists {
+ appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
+ return
+ }
+
+ if err := templateService.Delete(); err != nil {
+ appG.CResponse(http.StatusInternalServerError, "删除模板失败:"+err.Error(), nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "删除模板成功", nil)
+}
+
+// PreviewMessageTemplate 预览消息模板
+func PreviewMessageTemplate(c *gin.Context) {
+ appG := app.Gin{C: c}
+
+ var req struct {
+ ID string `json:"id" binding:"required"`
+ Params map[string]string `json:"params"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ appG.CResponse(http.StatusBadRequest, "参数错误:"+err.Error(), nil)
+ return
+ }
+
+ templateService := message_template_service.MessageTemplateService{
+ ID: req.ID,
+ }
+
+ exists, err := templateService.ExistByID()
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "查询模板失败!", nil)
+ return
+ }
+
+ if !exists {
+ appG.CResponse(http.StatusNotFound, "模板不存在!", nil)
+ return
+ }
+
+ preview, err := templateService.PreviewTemplate(req.Params)
+ if err != nil {
+ appG.CResponse(http.StatusInternalServerError, "预览模板失败:"+err.Error(), nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "预览模板成功", preview)
+}
+
+// GetTemplateWithIns 获取模板及其关联的实例
+func GetTemplateWithIns(c *gin.Context) {
+ appG := app.Gin{C: c}
+ id := c.Query("id")
+
+ if id == "" {
+ appG.CResponse(http.StatusBadRequest, "模板ID为空!", nil)
+ return
+ }
+
+ // 获取模板信息
+ template, err := models.GetMessageTemplateByID(id)
+ if err != nil {
+ appG.CResponse(http.StatusBadRequest, "获取模板信息失败!", nil)
+ return
+ }
+
+ // 获取关联的实例列表
+ insList, err := models.GetTemplateInsList(id)
+ if err != nil {
+ appG.CResponse(http.StatusBadRequest, "获取实例列表失败!", nil)
+ return
+ }
+
+ result := map[string]interface{}{
+ "template": template,
+ "ins_list": insList,
+ }
+
+ appG.CResponse(http.StatusOK, "获取模板信息成功", result)
+}
+
+// TemplateInsReq 模板实例请求结构
+type TemplateInsReq struct {
+ ID string `json:"id" validate:"required,len=12" label:"实例id"`
+ TemplateID string `json:"template_id" validate:"required" label:"模板id"`
+ WayID string `json:"way_id" validate:"required,len=12" label:"渠道id"`
+ ContentType string `json:"content_type" validate:"required,max=100" label:"实例内容类型"`
+ Config string `json:"config" validate:"" label:"任务配置"`
+ Extra string `json:"extra" validate:"" label:"任务额外信息"`
+ WayType string `json:"way_type" validate:"required,max=100" label:"渠道类型"`
+}
+
+// AddTemplateIns 添加模板关联的实例
+func AddTemplateIns(c *gin.Context) {
+ var (
+ appG = app.Gin{C: c}
+ req TemplateInsReq
+ )
+
+ errCode, errStr := app.BindJsonAndPlayValid(c, &req)
+ if errCode != e.SUCCESS {
+ appG.CResponse(errCode, errStr, nil)
+ return
+ }
+
+ sendTaskInsService := send_ins_service.SendTaskInsService{}
+ err := sendTaskInsService.AddOne(models.SendTasksIns{
+ UUIDModel: models.UUIDModel{ID: req.ID},
+ TemplateID: req.TemplateID,
+ WayID: req.WayID,
+ WayType: req.WayType,
+ ContentType: req.ContentType,
+ Config: req.Config,
+ Extra: req.Extra,
+ })
+ if err != "" {
+ appG.CResponse(http.StatusBadRequest, fmt.Sprintf("添加实例失败!错误原因:%s", err), nil)
+ return
+ }
+
+ appG.CResponse(http.StatusOK, "添加实例成功!", nil)
+}
diff --git a/routers/api/v1/send_message.go b/routers/api/v1/send_message.go
index d4e3ee7..d78e3a3 100644
--- a/routers/api/v1/send_message.go
+++ b/routers/api/v1/send_message.go
@@ -61,6 +61,7 @@ func DoSendMassage(c *gin.Context) {
}
msgService := send_message_service.SendMessageService{
+ SendMode: send_message_service.SendModeTask, // 明确标记为任务模式
TaskID: taskID,
Title: req.Title,
Text: req.Text,
diff --git a/routers/api/v2/send_message.go b/routers/api/v2/send_message.go
new file mode 100644
index 0000000..6553437
--- /dev/null
+++ b/routers/api/v2/send_message.go
@@ -0,0 +1,146 @@
+package v2
+
+import (
+ "fmt"
+ "message-nest/models"
+ "message-nest/pkg/app"
+ "message-nest/pkg/e"
+ utilpkg "message-nest/pkg/util"
+ "message-nest/service/send_message_service"
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+)
+
+type SendMessageByTemplateReq struct {
+ Token string `json:"token" validate:"required" label:"模板token"`
+ Title string `json:"title" validate:"required" label:"消息标题"`
+ Placeholders map[string]interface{} `json:"placeholders" label:"占位符"`
+}
+
+// DoSendMessageByTemplate 使用模板发送消息
+func DoSendMessageByTemplate(c *gin.Context) {
+ var (
+ appG = app.Gin{C: c}
+ req SendMessageByTemplateReq
+ )
+
+ errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
+ if errCode != e.SUCCESS {
+ appG.CResponse(errCode, errMsg, nil)
+ return
+ }
+
+ // 解析 token 为模板 ID
+ templateID, err := utilpkg.DecryptTokenHex(req.Token, 71) // 71 为简单对称密钥
+ if err != nil {
+ appG.CResponse(http.StatusBadRequest, fmt.Sprintf("token解析失败:%v", err), nil)
+ return
+ }
+
+ // 获取模板
+ template, err := models.GetMessageTemplateByID(templateID)
+ if err != nil {
+ appG.CResponse(http.StatusBadRequest, fmt.Sprintf("模板不存在:%s", err), nil)
+ return
+ }
+
+ // 检查模板状态
+ if template.Status != "enabled" {
+ appG.CResponse(http.StatusBadRequest, "模板已禁用", nil)
+ return
+ }
+
+ // 替换占位符
+ textContent := replacePlaceholders(template.TextTemplate, req.Placeholders)
+ htmlContent := replacePlaceholders(template.HTMLTemplate, req.Placeholders)
+ markdownContent := replacePlaceholders(template.MarkdownTemplate, req.Placeholders)
+
+ // 解析@提醒配置
+ var atMobiles []string
+ var atUserIds []string
+ if template.AtMobiles != "" {
+ atMobiles = strings.Split(template.AtMobiles, ",")
+ // 去除空格
+ for i := range atMobiles {
+ atMobiles[i] = strings.TrimSpace(atMobiles[i])
+ }
+ }
+ if template.AtUserIds != "" {
+ atUserIds = strings.Split(template.AtUserIds, ",")
+ // 去除空格
+ for i := range atUserIds {
+ atUserIds[i] = strings.TrimSpace(atUserIds[i])
+ }
+ }
+
+ // 获取模板关联的实例列表
+ insList, err := models.GetTemplateInsList(templateID)
+ if err != nil || len(insList) == 0 {
+ appG.CResponse(http.StatusBadRequest, "模板没有配置发送实例", nil)
+ return
+ }
+
+ // 过滤启用的实例
+ var enabledCount int
+ for _, ins := range insList {
+ if ins.Enable == 1 {
+ enabledCount++
+ }
+ }
+
+ if enabledCount == 0 {
+ appG.CResponse(http.StatusBadRequest, "模板没有启用的发送实例", nil)
+ return
+ }
+
+ // 使用发送服务进行发送
+ // 将模板ID作为TaskID传入,用于日志记录
+ msgService := send_message_service.SendMessageService{
+ SendMode: send_message_service.SendModeTemplate, // 明确标记为模板模式
+ TaskID: templateID, // 使用模板ID作为TaskID(用于日志记录)
+ TemplateID: templateID, // 模板ID
+ Name: template.Name, // 模板名称
+ Title: req.Title,
+ Text: textContent,
+ HTML: htmlContent,
+ MarkDown: markdownContent,
+ CallerIp: c.ClientIP(),
+ AtMobiles: atMobiles,
+ AtUserIds: atUserIds,
+ AtAll: template.IsAtAll,
+ DefaultLogger: logrus.WithFields(logrus.Fields{
+ "prefix": "[Template Send]",
+ }),
+ }
+
+ // 发送前检查
+ task, err := msgService.SendPreCheck()
+ if err != nil {
+ appG.CResponse(http.StatusBadRequest, fmt.Sprintf("发送检查不通过:%s", err), nil)
+ return
+ }
+
+ // 异步发送
+ msgService.AsyncSend(task)
+ appG.CResponse(http.StatusOK, "success", map[string]interface{}{
+ "token": req.Token,
+ "count": enabledCount,
+ })
+}
+
+// replacePlaceholders 替换模板中的占位符
+func replacePlaceholders(template string, placeholders map[string]interface{}) string {
+ if template == "" || placeholders == nil {
+ return template
+ }
+
+ result := template
+ for key, value := range placeholders {
+ placeholder := fmt.Sprintf("{{%s}}", key)
+ result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
+ }
+ return result
+}
diff --git a/routers/router.go b/routers/router.go
index 66d3790..08fe9fe 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -8,6 +8,7 @@ import (
"message-nest/pkg/setting"
"message-nest/routers/api"
"message-nest/routers/api/v1"
+ "message-nest/routers/api/v2"
"net/http"
)
@@ -98,6 +99,26 @@ func InitRouter(f embed.FS) *gin.Engine {
// hostedMessage
apiV1.GET("/hostedmessages/list", v1.GetHostMessageList)
+ // messageTemplate
+ apiV1.GET("/templates/list", v1.GetMessageTemplateList)
+ apiV1.GET("/templates/get", v1.GetMessageTemplate)
+ apiV1.POST("/templates/add", v1.AddMessageTemplate)
+ apiV1.POST("/templates/edit", v1.EditMessageTemplate)
+ apiV1.POST("/templates/delete", v1.DeleteMessageTemplate)
+ apiV1.POST("/templates/preview", v1.PreviewMessageTemplate)
+
+ // messageTemplate instances
+ apiV1.GET("/templates/ins/get", v1.GetTemplateWithIns)
+ apiV1.POST("/templates/ins/addone", v1.AddTemplateIns)
+
+ }
+
+ // API v2
+ apiV2 := app.Group("/api/v2")
+ apiV2.Use(middleware.JWT())
+ {
+ // message/send - 使用模板发送消息
+ apiV2.POST("/message/send", v2.DoSendMessageByTemplate)
}
return app
diff --git a/service/message_template_service/message_template.go b/service/message_template_service/message_template.go
new file mode 100644
index 0000000..2290f52
--- /dev/null
+++ b/service/message_template_service/message_template.go
@@ -0,0 +1,185 @@
+package message_template_service
+
+import (
+ "encoding/json"
+ "errors"
+ "message-nest/models"
+ "strings"
+)
+
+type MessageTemplateService struct {
+ ID string
+ Name string
+ Description string
+ TextTemplate string
+ HTMLTemplate string
+ MarkdownTemplate string
+ Placeholders string
+ AtMobiles string
+ AtUserIds string
+ IsAtAll bool
+ Status string
+ Text string
+
+ PageNum int
+ PageSize int
+}
+
+// Placeholder 占位符定义
+type Placeholder struct {
+ Key string `json:"key"`
+ Label string `json:"label"`
+ Default string `json:"default"`
+}
+
+// Add 添加消息模板
+func (s *MessageTemplateService) Add() error {
+ if err := s.validatePlaceholders(); err != nil {
+ return err
+ }
+
+ newUUID := models.GenerateTemplateUniqueID()
+ model := models.MessageTemplate{
+ UUIDModel: models.UUIDModel{
+ ID: newUUID,
+ },
+ Name: s.Name,
+ Description: s.Description,
+ TextTemplate: s.TextTemplate,
+ HTMLTemplate: s.HTMLTemplate,
+ MarkdownTemplate: s.MarkdownTemplate,
+ Placeholders: s.Placeholders,
+ AtMobiles: s.AtMobiles,
+ AtUserIds: s.AtUserIds,
+ IsAtAll: s.IsAtAll,
+ Status: s.Status,
+ }
+
+ return model.Add()
+}
+
+// Update 更新消息模板
+func (s *MessageTemplateService) Update() error {
+ if err := s.validatePlaceholders(); err != nil {
+ return err
+ }
+
+ model := models.MessageTemplate{
+ UUIDModel: models.UUIDModel{
+ ID: s.ID,
+ },
+ Name: s.Name,
+ Description: s.Description,
+ TextTemplate: s.TextTemplate,
+ HTMLTemplate: s.HTMLTemplate,
+ MarkdownTemplate: s.MarkdownTemplate,
+ Placeholders: s.Placeholders,
+ AtMobiles: s.AtMobiles,
+ AtUserIds: s.AtUserIds,
+ IsAtAll: s.IsAtAll,
+ Status: s.Status,
+ }
+
+ return model.Update()
+}
+
+// Delete 删除消息模板
+func (s *MessageTemplateService) Delete() error {
+ model := models.MessageTemplate{
+ UUIDModel: models.UUIDModel{
+ ID: s.ID,
+ },
+ }
+ return model.Delete()
+}
+
+// Get 获取单个消息模板
+func (s *MessageTemplateService) Get() (*models.MessageTemplateResult, error) {
+ return models.GetMessageTemplateByID(s.ID)
+}
+
+// GetAll 获取消息模板列表
+func (s *MessageTemplateService) GetAll() ([]models.MessageTemplateResult, error) {
+ templates, err := models.GetMessageTemplates(s.PageNum, s.PageSize, s.Text, s.getMaps())
+ if err != nil {
+ return nil, err
+ }
+ return templates, nil
+}
+
+// Count 获取消息模板总数
+func (s *MessageTemplateService) Count() (int64, error) {
+ return models.GetMessageTemplatesTotal(s.Text, s.getMaps())
+}
+
+// ExistByID 检查模板是否存在
+func (s *MessageTemplateService) ExistByID() (bool, error) {
+ return models.ExistMessageTemplateByID(s.ID)
+}
+
+// RenderTemplate 渲染模板(替换占位符)
+func (s *MessageTemplateService) RenderTemplate(templateContent string, params map[string]string) string {
+ result := templateContent
+
+ for key, value := range params {
+ placeholder := "{{" + key + "}}"
+ result = strings.ReplaceAll(result, placeholder, value)
+ }
+
+ return result
+}
+
+// PreviewTemplate 预览模板效果
+func (s *MessageTemplateService) PreviewTemplate(params map[string]string) (map[string]string, error) {
+ template, err := s.Get()
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[string]string)
+
+ if template.TextTemplate != "" {
+ result["text"] = s.RenderTemplate(template.TextTemplate, params)
+ }
+
+ if template.HTMLTemplate != "" {
+ result["html"] = s.RenderTemplate(template.HTMLTemplate, params)
+ }
+
+ if template.MarkdownTemplate != "" {
+ result["markdown"] = s.RenderTemplate(template.MarkdownTemplate, params)
+ }
+
+ return result, nil
+}
+
+// validatePlaceholders 验证占位符格式
+func (s *MessageTemplateService) validatePlaceholders() error {
+ if s.Placeholders == "" {
+ return nil
+ }
+
+ var placeholders []Placeholder
+ if err := json.Unmarshal([]byte(s.Placeholders), &placeholders); err != nil {
+ return errors.New("占位符格式错误,必须是有效的JSON数组")
+ }
+
+ for _, p := range placeholders {
+ if p.Key == "" {
+ return errors.New("占位符的key不能为空")
+ }
+ }
+
+ return nil
+}
+
+// getMaps 获取查询条件
+func (s *MessageTemplateService) getMaps() map[string]interface{} {
+ maps := make(map[string]interface{})
+
+ if s.Status != "" {
+ maps["status"] = s.Status
+ }
+
+ return maps
+}
diff --git a/service/send_message_service/send_message.go b/service/send_message_service/send_message.go
index 1874ff6..e4b27e3 100644
--- a/service/send_message_service/send_message.go
+++ b/service/send_message_service/send_message.go
@@ -18,6 +18,12 @@ const (
SendFail = 0
)
+// 发送模式类型
+const (
+ SendModeTask = "task" // 传统任务模式
+ SendModeTemplate = "template" // 模板模式
+)
+
func errStrIsSuccess(errStr string) int {
if errStr == "" {
return SendSuccess
@@ -26,13 +32,16 @@ func errStrIsSuccess(errStr string) int {
}
type SendMessageService struct {
- TaskID string
- Title string
- Text string
- HTML string
- URL string
- MarkDown string
- CallerIp string
+ SendMode string // 发送模式:task(任务模式) 或 template(模板模式)
+ TaskID string // 任务ID(任务模式)或模板ID(模板模式,用于日志记录)
+ TemplateID string // 模板ID(仅模板模式使用)
+ Name string // 任务或模板名称(用于日志记录)
+ Title string
+ Text string
+ HTML string
+ URL string
+ MarkDown string
+ CallerIp string
// @提及相关字段
AtMobiles []string
@@ -82,31 +91,88 @@ func (sm *SendMessageService) AsyncSend(task models.TaskIns) {
}
// SendPreCheck 发送前数据准备和预检查
+// 支持两种模式:
+// 1. SendModeTask:传统任务模式,使用 TaskID 查询任务和实例
+// 2. SendModeTemplate:模板模式,使用 TemplateID 查询模板关联的实例
func (sm *SendMessageService) SendPreCheck() (models.TaskIns, error) {
errStr := ""
entry := logrus.WithFields(logrus.Fields{
"prefix": "[Message PreChecK]",
})
- sendTaskService := send_task_service.SendTaskService{
- ID: sm.TaskID,
- }
- task, err := sendTaskService.GetTaskWithIns()
- if err != nil {
- errStr = fmt.Sprintf("任务[%s]查询失败!", sm.TaskID)
+
+ var task models.TaskIns
+
+ switch sm.SendMode {
+ case SendModeTemplate:
+ // 模板模式:使用模板ID获取实例
+ if sm.TemplateID == "" {
+ errStr = "模板模式下 TemplateID 不能为空"
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+
+ // 获取模板关联的实例列表
+ insList, err := models.GetTemplateInsList(sm.TemplateID)
+ if err != nil {
+ errStr = fmt.Sprintf("模板[%s]实例查询失败:%s", sm.TemplateID, err)
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+ if len(insList) == 0 {
+ errStr = fmt.Sprintf("模板[%s]没有关联任何实例!", sm.TemplateID)
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+
+ // 构造虚拟任务对象(用于兼容现有发送逻辑)
+ // 将模板ID作为TaskID使用,便于日志记录
+ task.ID = sm.TaskID // 使用传入的TaskID(实际是模板ID)
+ task.InsData = make([]models.SendTasksInsRes, 0, len(insList))
+ for _, ins := range insList {
+ task.InsData = append(task.InsData, ins)
+ }
+ entry.Infof("模板[%s]加载了 %d 个实例", sm.TemplateID, len(insList))
+ return task, nil
+
+ case SendModeTask:
+ // 传统任务模式:使用任务ID查询
+ if sm.TaskID == "" {
+ errStr = "任务模式下 TaskID 不能为空"
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+
+ sendTaskService := send_task_service.SendTaskService{
+ ID: sm.TaskID,
+ }
+ task, err := sendTaskService.GetTaskWithIns()
+ if err != nil {
+ errStr = fmt.Sprintf("任务[%s]查询失败!", sm.TaskID)
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+ if task.ID == "" {
+ errStr = fmt.Sprintf("任务[%s]不存在!", sm.TaskID)
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+ if len(task.InsData) == 0 {
+ errStr = fmt.Sprintf("任务[%s]没有关联任何实例!!", sm.TaskID)
+ entry.Errorf(errStr)
+ return task, errors.New(errStr)
+ }
+ // 设置任务名称用于日志记录
+ if sm.Name == "" {
+ sm.Name = task.Name
+ }
+ return task, nil
+
+ default:
+ // SendMode 未设置或无效
+ errStr = fmt.Sprintf("SendMode 未设置或无效: %s,必须是 '%s' 或 '%s'", sm.SendMode, SendModeTask, SendModeTemplate)
entry.Errorf(errStr)
return task, errors.New(errStr)
}
- if task.ID == "" {
- errStr = fmt.Sprintf("任务[%s]不存在!", sm.TaskID)
- entry.Errorf(errStr)
- return task, errors.New(errStr)
- }
- if len(task.InsData) == 0 {
- errStr = fmt.Sprintf("任务[%s]没有关联任何实例!!", sm.TaskID)
- entry.Errorf(errStr)
- return task, errors.New(errStr)
- }
- return task, nil
}
// Send 发送一个消息任务的所有实例
@@ -143,13 +209,6 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) {
sm.LogsAndStatusMark(fmt.Sprintf("实例类型: %s + %s", ins.WayType, ins.ContentType), sm.Status)
sm.LogsAndStatusMark(fmt.Sprintf("实例配置: %s", ins.Config), sm.Status)
- // 发送内容校验绑定
- typeC, content := sm.GetSendMsg(ins.SendTasksIns)
- if content == "" {
- sm.LogsAndStatusMark(fmt.Sprintf("发送内容为空,设置的类型: %s,实际检测的类型: %s", ins.SendTasksIns.ContentType, typeC), SendFail)
- continue
- }
-
// 发送渠道的校验
errStr, msgObj := wayService.ValidateDiffWay()
if errStr != "" {
@@ -165,16 +224,33 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) {
continue
}
- // 构建统一消息内容(支持@功能)
- 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,
+ // 根据发送模式构建消息内容
+ var unifiedContent *unified.UnifiedMessageContent
+ if sm.SendMode == SendModeTemplate {
+ // 模板模式:根据实例的 ContentType 精确发送对应类型的内容
+ unifiedContent = sm.BuildTemplateContent(ins.SendTasksIns)
+ if unifiedContent == nil {
+ sm.LogsAndStatusMark(fmt.Sprintf("模板内容为空,实例类型: %s", ins.ContentType), SendFail)
+ continue
+ }
+ } else {
+ // 任务模式:使用现有逻辑(支持内容类型回退)
+ typeC, content := sm.GetSendMsg(ins.SendTasksIns)
+ if content == "" {
+ sm.LogsAndStatusMark(fmt.Sprintf("发送内容为空,设置的类型: %s,实际检测的类型: %s", ins.SendTasksIns.ContentType, typeC), SendFail)
+ continue
+ }
+ // 构建统一消息内容(支持@功能)
+ 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 方法(自动格式转换和@功能支持)
@@ -216,9 +292,17 @@ func (sm *SendMessageService) AppendSendContent() {
// RecordSendLog 记录发送日志
func (sm *SendMessageService) RecordSendLog() {
+ // 确定日志类型
+ logType := "task"
+ if sm.SendMode == SendModeTemplate {
+ logType = "template"
+ }
+
log := models.SendTasksLogs{
Log: strings.Join(sm.LogOutput, "\n"),
TaskID: sm.TaskID,
+ Type: logType,
+ Name: sm.Name,
Status: &sm.Status,
CallerIp: sm.CallerIp,
}
@@ -237,7 +321,52 @@ func (sm *SendMessageService) TransError(err string) string {
}
}
-// GetSendMsg 获取对应消息内容
+// BuildTemplateContent 构建模板模式的消息内容
+// 模板模式:根据实例的 ContentType 精确匹配对应类型的内容,只传递该类型的内容
+func (sm *SendMessageService) BuildTemplateContent(ins models.SendTasksIns) *unified.UnifiedMessageContent {
+ contentType := strings.ToLower(ins.ContentType)
+
+ // 内容类型映射表
+ contentMap := map[string]string{
+ unified.FormatTypeText: sm.Text,
+ unified.FormatTypeHTML: sm.HTML,
+ unified.FormatTypeMarkdown: sm.MarkDown,
+ }
+
+ // 检查内容是否存在
+ contentValue, exists := contentMap[contentType]
+ if !exists {
+ logrus.Warnf("模板模式:未知的内容类型 %s", ins.ContentType)
+ return nil
+ }
+ if contentValue == "" {
+ logrus.Warnf("模板模式:实例要求的 %s 类型内容为空", contentType)
+ return nil
+ }
+
+ // 构建消息内容,只填充实例要求的类型
+ content := &unified.UnifiedMessageContent{
+ Title: sm.Title,
+ URL: sm.URL,
+ AtMobiles: sm.AtMobiles,
+ AtUserIds: sm.AtUserIds,
+ AtAll: sm.AtAll,
+ }
+
+ // 根据类型填充对应字段
+ switch contentType {
+ case unified.FormatTypeText:
+ content.Text = contentValue
+ case unified.FormatTypeHTML:
+ content.HTML = contentValue
+ case unified.FormatTypeMarkdown:
+ content.Markdown = contentValue
+ }
+
+ return content
+}
+
+// GetSendMsg 获取对应消息内容(任务模式使用)
// 先根据实例设置的类型取,取不到或者取到的是空,则使用text发送
func (sm *SendMessageService) GetSendMsg(ins models.SendTasksIns) (string, string) {
data := map[string]string{}
diff --git a/web/src/components/Index.vue b/web/src/components/Index.vue
index 4b896bf..8e6ffbc 100644
--- a/web/src/components/Index.vue
+++ b/web/src/components/Index.vue
@@ -252,6 +252,7 @@ const tabRoutes: TabRoute[] = [
{ name: '发信日志', path: '/sendlogs' },
{ name: '托管消息', path: '/hostedmessage' },
{ name: '定时消息', path: '/cronmessages' },
+ { name: '模板任务', path: '/templates' },
{ name: '发信任务', path: '/sendtasks' },
{ name: '发信渠道', path: '/sendways' },
{ name: '设置偏好', path: '/settings' }
@@ -326,9 +327,9 @@ const siteTitle = computed(() => {
-
+
-
💡 提示:@功能仅钉钉和企业微信支持
+
+
💡 提示:@功能仅钉钉和企业微信支持
+
📋 发送顺序:实例配置的内容类型优先,若为空则按 HTML → Markdown → Text 顺序回退
+
diff --git a/web/src/constant.js b/web/src/constant.js
index e295f87..ad938de 100644
--- a/web/src/constant.js
+++ b/web/src/constant.js
@@ -55,7 +55,7 @@ const CONSTANT = {
},
{
type: 'QyWeiXin',
- label: '企业微信',
+ label: '企业微信机器人',
inputs: [
{ subLabel: 'token', value: '', col: 'access_token', desc: "企业微信webhook中的token" },
{ subLabel: '渠道名', value: '', col: 'name', desc: "想要设置的渠道名字" },
diff --git a/web/src/router/index.js b/web/src/router/index.js
index 66f18c6..5c65ad8 100644
--- a/web/src/router/index.js
+++ b/web/src/router/index.js
@@ -51,6 +51,11 @@ const router = createRouter({
path: 'cronmessages',
name: 'cronmessages',
component: () => import('../components/pages/cronMessages/CronMessages.vue')
+ },
+ {
+ path: 'templates',
+ name: 'templates',
+ component: () => import('../components/pages/messageTemplate/MessageTemplate.vue')
}
]
},
diff --git a/web/src/util/viewApi.js b/web/src/util/viewApi.js
index 751be02..cd9046f 100644
--- a/web/src/util/viewApi.js
+++ b/web/src/util/viewApi.js
@@ -1,11 +1,10 @@
-
-
const gethttpOrigin = () => {
return window.location.origin
}
-class ApiStrGenerate {
+// ==================== 公共加密工具 ====================
+class TokenEncryption {
// 根据字符串内容生成确定性 salt(范围 0~255)
static getDeterministicSalt(text) {
let sum = 0;
@@ -17,7 +16,7 @@ class ApiStrGenerate {
// 加密:首字节为salt,后续为按位异或后的数据
static encryptHex(text, key) {
- const salt = ApiStrGenerate.getDeterministicSalt(text);
+ const salt = TokenEncryption.getDeterministicSalt(text);
let result = salt.toString(16).padStart(2, '0');
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i) ^ (key & 0xFF) ^ ((salt + i) & 0xFF);
@@ -25,46 +24,19 @@ class ApiStrGenerate {
}
return result;
}
+}
- static getDataString(task_id, options) {
- // 新版仅展示 token;兼容旧版 task_id(后端依然支持)
- let data = { token: ApiStrGenerate.encryptHex(task_id, 71) };
- data.title = 'message title';
- data.text = 'Hello World!';
- if (options.html) {
- data.html = ' Hello World!
';
- }
- if (options.markdown) {
- data.markdown = '**Hello World!**';
- }
- 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
+// ==================== 公共代码模板生成器 ====================
+
+class CodeTemplates {
+ static getCurl(url, dataStr) {
+ return `curl -X POST --location '${url}' \\
+--header 'Content-Type: application/json' \\
+--data '${dataStr}'`;
}
- static getCurlString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = `curl -X POST --location '${gethttpOrigin()}/api/v1/message/send' \\
- --header 'Content-Type: application/json' \\
- --data '${dataStr}'`;
- return example;
- }
-
- static getGolangString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = `package main
+ static getGolang(url, dataStr) {
+ return `package main
import (
"fmt"
@@ -77,7 +49,7 @@ import (
func main() {
client := &http.Client{}
var data = strings.NewReader(\`${dataStr}\`)
- req, err := http.NewRequest("POST", "${gethttpOrigin()}/api/v1/message/send", data)
+ req, err := http.NewRequest("POST", "${url}", data)
if err != nil {
log.Fatal(err)
}
@@ -92,29 +64,23 @@ func main() {
log.Fatal(err)
}
fmt.Printf("%s\\n", bodyText)
-}
- `;
- return example;
+}`;
}
- static getPythonString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = `import requests
+ static getPython(url, dataStr) {
+ return `import requests
headers = {
'Content-Type': 'application/json',
}
json_data = ${dataStr}
-response = requests.post('${gethttpOrigin()}/api/v1/message/send', headers=headers, json=json_data)
+response = requests.post('${url}', headers=headers, json=json_data)
-print("response:", response.json())
-`;
- return example;
+print("response:", response.json())`;
}
- static getJaveString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = `import java.io.IOException;
+ static getJava(url, dataStr) {
+ return `import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -126,19 +92,16 @@ HttpClient client = HttpClient.newBuilder()
.build();
HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create("${gethttpOrigin()}/api/v1/message/send"))
- .POST(BodyPublishers.ofString(${JSON.stringify(dataStr).trim('\"')}))
+ .uri(URI.create("${url}"))
+ .POST(BodyPublishers.ofString(${JSON.stringify(dataStr).trim('"')}))
.setHeader("Content-Type", "application/json")
.build();
-HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-`;
- return example;
+HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());`;
}
- static getRustString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = `extern crate reqwest;
+ static getRust(url, dataStr) {
+ return `extern crate reqwest;
use reqwest::header;
fn main() -> Result<(), Box> {
@@ -146,7 +109,7 @@ fn main() -> Result<(), Box> {
headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::blocking::Client::new();
- let res = client.post("${gethttpOrigin()}/api/v1/message/send")
+ let res = client.post("${url}")
.headers(headers)
.body(r#"
${dataStr}
@@ -157,43 +120,37 @@ ${dataStr}
println!("{}", res);
Ok(())
-}
-`;
- return example;
+}`;
}
- static getPHPString(task_id, options) {
- let dataStr = ApiStrGenerate.getDataString(task_id, options);
- let example = ` Hello World! ';
+ if (options.markdown) data.markdown = '**Hello World!**';
+ 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;
+ return JSON.stringify(data, null, 4);
+ }
+
+ static getApiUrl() {
+ return `${gethttpOrigin()}/api/v1/message/send`;
+ }
+
+ static getCurlString(task_id, options) {
+ return CodeTemplates.getCurl(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getGolangString(task_id, options) {
+ return CodeTemplates.getGolang(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getPythonString(task_id, options) {
+ return CodeTemplates.getPython(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getJaveString(task_id, options) {
+ return CodeTemplates.getJava(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getRustString(task_id, options) {
+ return CodeTemplates.getRust(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getPHPString(task_id, options) {
+ return CodeTemplates.getPHP(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+
+ static getNodeString(task_id, options) {
+ return CodeTemplates.getNode(this.getApiUrl(), this.getDataString(task_id, options));
+ }
+}
+
+// ==================== 模板 API (V2) ====================
+
+class TemplateApiStrGenerate {
+ static getTemplateDataString(template_id, placeholders_json) {
+ // 解析占位符配置
+ let placeholders = {};
+ try {
+ const placeholdersList = JSON.parse(placeholders_json || '[]');
+ // 根据占位符配置生成示例值
+ placeholdersList.forEach(p => {
+ placeholders[p.key] = p.default || `mock_${p.key}`;
+ });
+ } catch (e) {
+ // 如果解析失败,使用默认示例
+ placeholders = {
+ 'username': 'John Doe',
+ 'email': 'john@example.com',
+ 'phone': '13800138000'
+ };
+ }
+
+ let data = {
+ token: TokenEncryption.encryptHex(template_id, 71),
+ title: 'message title',
+ placeholders: placeholders
+ };
+ return JSON.stringify(data, null, 4);
+ }
+
+ static getApiUrl() {
+ return `${gethttpOrigin()}/api/v2/message/send`;
+ }
+
+ static getCurlString(template_id, placeholders_json) {
+ return CodeTemplates.getCurl(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getGolangString(template_id, placeholders_json) {
+ return CodeTemplates.getGolang(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getPythonString(template_id, placeholders_json) {
+ return CodeTemplates.getPython(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getJavaString(template_id, placeholders_json) {
+ return CodeTemplates.getJava(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getRustString(template_id, placeholders_json) {
+ return CodeTemplates.getRust(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getPHPString(template_id, placeholders_json) {
+ return CodeTemplates.getPHP(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+
+ static getNodeString(template_id, placeholders_json) {
+ return CodeTemplates.getNode(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
+ }
+}
+
+export { ApiStrGenerate, TemplateApiStrGenerate };
From 148e67dc19ea7247f4b50d0b64901bfcc800340e Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 00:31:21 +0800
Subject: [PATCH 04/24] feat: add message template
---
.../pages/messageTemplate/TemplateInstanceConfig.vue | 2 +-
web/src/util/pageUtils.d.ts | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 web/src/util/pageUtils.d.ts
diff --git a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
index 6192302..84f6918 100644
--- a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
+++ b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
@@ -34,7 +34,7 @@ const props = withDefaults(defineProps(), {
})
// 组件emits
-const emit = defineEmits<{
+defineEmits<{
'update:open': [value: boolean]
}>()
diff --git a/web/src/util/pageUtils.d.ts b/web/src/util/pageUtils.d.ts
new file mode 100644
index 0000000..09fba2d
--- /dev/null
+++ b/web/src/util/pageUtils.d.ts
@@ -0,0 +1,5 @@
+/**
+ * 获取分页大小配置
+ * @returns 分页大小,默认为8
+ */
+export declare function getPageSize(): number;
From f4780c9c6c999462a1289d3d3eed02f54769ba07 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 12:37:36 +0800
Subject: [PATCH 05/24] chore: adjust templata table name
---
migrate/migrate.go | 2 +-
models/message_template.go | 46 +++++++++----------
routers/api/v1/message_template.go | 14 +++---
routers/api/v2/send_message.go | 2 +-
.../message_template.go | 38 +++++++--------
service/send_message_service/send_message.go | 4 +-
6 files changed, 53 insertions(+), 53 deletions(-)
diff --git a/migrate/migrate.go b/migrate/migrate.go
index 9499ccd..0dc1e78 100644
--- a/migrate/migrate.go
+++ b/migrate/migrate.go
@@ -68,7 +68,7 @@ func Setup() {
&models.CronMessages{},
&models.HostedMessage{},
&models.LoginLog{},
- &models.MessageTemplate{},
+ &models.Template{},
}
for _, table := range tables {
diff --git a/models/message_template.go b/models/message_template.go
index d8f65e6..a604893 100644
--- a/models/message_template.go
+++ b/models/message_template.go
@@ -11,8 +11,8 @@ func GenerateTemplateUniqueID() string {
return fmt.Sprintf("TP%s", newUUID)
}
-// MessageTemplate 消息模板
-type MessageTemplate struct {
+// Template 消息模板
+type Template struct {
UUIDModel
Name string `json:"name" gorm:"type:varchar(200);not null;index" binding:"required"`
@@ -36,7 +36,7 @@ type MessageTemplate struct {
}
// Add 添加消息模板
-func (t *MessageTemplate) Add() error {
+func (t *Template) Add() error {
if err := db.Create(&t).Error; err != nil {
return err
}
@@ -44,23 +44,23 @@ func (t *MessageTemplate) Add() error {
}
// Update 更新消息模板
-func (t *MessageTemplate) Update() error {
- if err := db.Model(&MessageTemplate{}).Where("id = ?", t.ID).Updates(t).Error; err != nil {
+func (t *Template) Update() error {
+ if err := db.Model(&Template{}).Where("id = ?", t.ID).Updates(t).Error; err != nil {
return err
}
return nil
}
// Delete 删除消息模板
-func (t *MessageTemplate) Delete() error {
- if err := db.Where("id = ?", t.ID).Delete(&MessageTemplate{}).Error; err != nil {
+func (t *Template) Delete() error {
+ if err := db.Where("id = ?", t.ID).Delete(&Template{}).Error; err != nil {
return err
}
return nil
}
-// MessageTemplateResult 消息模板查询结果
-type MessageTemplateResult struct {
+// TemplateResult 消息模板查询结果
+type TemplateResult struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
@@ -78,10 +78,10 @@ type MessageTemplateResult struct {
ModifiedOn util.Time `json:"modified_on"`
}
-// GetMessageTemplates 获取消息模板列表
-func GetMessageTemplates(pageNum int, pageSize int, text string, maps map[string]interface{}) ([]MessageTemplateResult, error) {
- var datas []MessageTemplateResult
- templateT := GetSchema(MessageTemplate{})
+// GetTemplates 获取消息模板列表
+func GetTemplates(pageNum int, pageSize int, text string, maps map[string]interface{}) ([]TemplateResult, error) {
+ var datas []TemplateResult
+ templateT := GetSchema(Template{})
query := db.Table(templateT)
query = query.Where(maps)
@@ -102,10 +102,10 @@ func GetMessageTemplates(pageNum int, pageSize int, text string, maps map[string
return datas, nil
}
-// GetMessageTemplatesTotal 获取消息模板总数
-func GetMessageTemplatesTotal(text string, maps map[string]interface{}) (int64, error) {
+// GetTemplatesTotal 获取消息模板总数
+func GetTemplatesTotal(text string, maps map[string]interface{}) (int64, error) {
var total int64
- templateT := GetSchema(MessageTemplate{})
+ templateT := GetSchema(Template{})
query := db.Table(templateT)
query = query.Where(maps)
@@ -120,10 +120,10 @@ func GetMessageTemplatesTotal(text string, maps map[string]interface{}) (int64,
return total, nil
}
-// GetMessageTemplateByID 根据ID获取消息模板
-func GetMessageTemplateByID(id string) (*MessageTemplateResult, error) {
- var data MessageTemplateResult
- templateT := GetSchema(MessageTemplate{})
+// GetTemplateByID 根据ID获取消息模板
+func GetTemplateByID(id string) (*TemplateResult, error) {
+ var data TemplateResult
+ templateT := GetSchema(Template{})
err := db.Table(templateT).Where("id = ?", id).First(&data).Error
if err != nil {
@@ -133,9 +133,9 @@ func GetMessageTemplateByID(id string) (*MessageTemplateResult, error) {
return &data, nil
}
-// ExistMessageTemplateByID 检查模板是否存在
-func ExistMessageTemplateByID(id string) (bool, error) {
- var template MessageTemplate
+// ExistTemplateByID 检查模板是否存在
+func ExistTemplateByID(id string) (bool, error) {
+ var template Template
err := db.Select("id").Where("id = ?", id).First(&template).Error
if err != nil {
return false, err
diff --git a/routers/api/v1/message_template.go b/routers/api/v1/message_template.go
index 759bbb2..3dd9cce 100644
--- a/routers/api/v1/message_template.go
+++ b/routers/api/v1/message_template.go
@@ -19,7 +19,7 @@ func GetMessageTemplateList(c *gin.Context) {
status := c.Query("status")
offset, limit := util.GetPageSize(c)
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
Text: text,
Status: status,
PageNum: offset,
@@ -49,7 +49,7 @@ func GetMessageTemplate(c *gin.Context) {
appG := app.Gin{C: c}
id := c.Query("id")
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
ID: id,
}
@@ -104,7 +104,7 @@ func AddMessageTemplate(c *gin.Context) {
req.Status = "enabled"
}
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
Name: req.Name,
Description: req.Description,
TextTemplate: req.TextTemplate,
@@ -153,7 +153,7 @@ func EditMessageTemplate(c *gin.Context) {
return
}
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
ID: req.ID,
}
@@ -200,7 +200,7 @@ func DeleteMessageTemplate(c *gin.Context) {
return
}
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
ID: req.ID,
}
@@ -237,7 +237,7 @@ func PreviewMessageTemplate(c *gin.Context) {
return
}
- templateService := message_template_service.MessageTemplateService{
+ templateService := message_template_service.TemplateService{
ID: req.ID,
}
@@ -272,7 +272,7 @@ func GetTemplateWithIns(c *gin.Context) {
}
// 获取模板信息
- template, err := models.GetMessageTemplateByID(id)
+ template, err := models.GetTemplateByID(id)
if err != nil {
appG.CResponse(http.StatusBadRequest, "获取模板信息失败!", nil)
return
diff --git a/routers/api/v2/send_message.go b/routers/api/v2/send_message.go
index 6553437..a34e9bb 100644
--- a/routers/api/v2/send_message.go
+++ b/routers/api/v2/send_message.go
@@ -41,7 +41,7 @@ func DoSendMessageByTemplate(c *gin.Context) {
}
// 获取模板
- template, err := models.GetMessageTemplateByID(templateID)
+ template, err := models.GetTemplateByID(templateID)
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("模板不存在:%s", err), nil)
return
diff --git a/service/message_template_service/message_template.go b/service/message_template_service/message_template.go
index 2290f52..0ddbd0b 100644
--- a/service/message_template_service/message_template.go
+++ b/service/message_template_service/message_template.go
@@ -7,7 +7,7 @@ import (
"strings"
)
-type MessageTemplateService struct {
+type TemplateService struct {
ID string
Name string
Description string
@@ -33,13 +33,13 @@ type Placeholder struct {
}
// Add 添加消息模板
-func (s *MessageTemplateService) Add() error {
+func (s *TemplateService) Add() error {
if err := s.validatePlaceholders(); err != nil {
return err
}
newUUID := models.GenerateTemplateUniqueID()
- model := models.MessageTemplate{
+ model := models.Template{
UUIDModel: models.UUIDModel{
ID: newUUID,
},
@@ -59,12 +59,12 @@ func (s *MessageTemplateService) Add() error {
}
// Update 更新消息模板
-func (s *MessageTemplateService) Update() error {
+func (s *TemplateService) Update() error {
if err := s.validatePlaceholders(); err != nil {
return err
}
- model := models.MessageTemplate{
+ model := models.Template{
UUIDModel: models.UUIDModel{
ID: s.ID,
},
@@ -84,8 +84,8 @@ func (s *MessageTemplateService) Update() error {
}
// Delete 删除消息模板
-func (s *MessageTemplateService) Delete() error {
- model := models.MessageTemplate{
+func (s *TemplateService) Delete() error {
+ model := models.Template{
UUIDModel: models.UUIDModel{
ID: s.ID,
},
@@ -94,13 +94,13 @@ func (s *MessageTemplateService) Delete() error {
}
// Get 获取单个消息模板
-func (s *MessageTemplateService) Get() (*models.MessageTemplateResult, error) {
- return models.GetMessageTemplateByID(s.ID)
+func (s *TemplateService) Get() (*models.TemplateResult, error) {
+ return models.GetTemplateByID(s.ID)
}
// GetAll 获取消息模板列表
-func (s *MessageTemplateService) GetAll() ([]models.MessageTemplateResult, error) {
- templates, err := models.GetMessageTemplates(s.PageNum, s.PageSize, s.Text, s.getMaps())
+func (s *TemplateService) GetAll() ([]models.TemplateResult, error) {
+ templates, err := models.GetTemplates(s.PageNum, s.PageSize, s.Text, s.getMaps())
if err != nil {
return nil, err
}
@@ -108,17 +108,17 @@ func (s *MessageTemplateService) GetAll() ([]models.MessageTemplateResult, error
}
// Count 获取消息模板总数
-func (s *MessageTemplateService) Count() (int64, error) {
- return models.GetMessageTemplatesTotal(s.Text, s.getMaps())
+func (s *TemplateService) Count() (int64, error) {
+ return models.GetTemplatesTotal(s.Text, s.getMaps())
}
// ExistByID 检查模板是否存在
-func (s *MessageTemplateService) ExistByID() (bool, error) {
- return models.ExistMessageTemplateByID(s.ID)
+func (s *TemplateService) ExistByID() (bool, error) {
+ return models.ExistTemplateByID(s.ID)
}
// RenderTemplate 渲染模板(替换占位符)
-func (s *MessageTemplateService) RenderTemplate(templateContent string, params map[string]string) string {
+func (s *TemplateService) RenderTemplate(templateContent string, params map[string]string) string {
result := templateContent
for key, value := range params {
@@ -130,7 +130,7 @@ func (s *MessageTemplateService) RenderTemplate(templateContent string, params m
}
// PreviewTemplate 预览模板效果
-func (s *MessageTemplateService) PreviewTemplate(params map[string]string) (map[string]string, error) {
+func (s *TemplateService) PreviewTemplate(params map[string]string) (map[string]string, error) {
template, err := s.Get()
if err != nil {
return nil, err
@@ -154,7 +154,7 @@ func (s *MessageTemplateService) PreviewTemplate(params map[string]string) (map[
}
// validatePlaceholders 验证占位符格式
-func (s *MessageTemplateService) validatePlaceholders() error {
+func (s *TemplateService) validatePlaceholders() error {
if s.Placeholders == "" {
return nil
}
@@ -174,7 +174,7 @@ func (s *MessageTemplateService) validatePlaceholders() error {
}
// getMaps 获取查询条件
-func (s *MessageTemplateService) getMaps() map[string]interface{} {
+func (s *TemplateService) getMaps() map[string]interface{} {
maps := make(map[string]interface{})
if s.Status != "" {
diff --git a/service/send_message_service/send_message.go b/service/send_message_service/send_message.go
index e4b27e3..a250af6 100644
--- a/service/send_message_service/send_message.go
+++ b/service/send_message_service/send_message.go
@@ -256,7 +256,7 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) {
// 使用 SendUnified 方法(自动格式转换和@功能支持)
res, errMsg := channel.SendUnified(msgObj, ins.SendTasksIns, unifiedContent)
if res != "" {
- sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
+ sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s\n", res), sm.Status)
} else {
sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
}
@@ -325,7 +325,7 @@ func (sm *SendMessageService) TransError(err string) string {
// 模板模式:根据实例的 ContentType 精确匹配对应类型的内容,只传递该类型的内容
func (sm *SendMessageService) BuildTemplateContent(ins models.SendTasksIns) *unified.UnifiedMessageContent {
contentType := strings.ToLower(ins.ContentType)
-
+
// 内容类型映射表
contentMap := map[string]string{
unified.FormatTypeText: sm.Text,
From 42c7c9a1b4961bbb11181b6765a5b552ccb35987 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 18:29:15 +0800
Subject: [PATCH 06/24] feat: add docs pages
---
.github/workflows/deploy_docs.yml | 66 +
docs/.gitignore | 4 +
docs/.vitepress/config.mts | 104 ++
docs/README.md | 49 +
docs/SETUP.md | 133 ++
docs/api/examples.md | 305 ++++
docs/api/usage.md | 106 ++
docs/deploy.sh | 0
docs/deployment/development.md | 151 ++
docs/deployment/direct-run.md | 112 ++
docs/deployment/docker-compose.md | 240 +++
docs/deployment/docker.md | 201 +++
docs/deployment/overview.md | 53 +
docs/guide/changelog.md | 64 +
docs/guide/configuration.md | 170 ++
docs/guide/embed-html.md | 98 ++
docs/guide/features.md | 77 +
docs/guide/introduction.md | 39 +
docs/index.md | 86 +
docs/package-lock.json | 2468 +++++++++++++++++++++++++++++
docs/package.json | 13 +
docs/public/logo.svg | 8 +
22 files changed, 4547 insertions(+)
create mode 100644 .github/workflows/deploy_docs.yml
create mode 100644 docs/.gitignore
create mode 100644 docs/.vitepress/config.mts
create mode 100644 docs/README.md
create mode 100644 docs/SETUP.md
create mode 100644 docs/api/examples.md
create mode 100644 docs/api/usage.md
create mode 100644 docs/deploy.sh
create mode 100644 docs/deployment/development.md
create mode 100644 docs/deployment/direct-run.md
create mode 100644 docs/deployment/docker-compose.md
create mode 100644 docs/deployment/docker.md
create mode 100644 docs/deployment/overview.md
create mode 100644 docs/guide/changelog.md
create mode 100644 docs/guide/configuration.md
create mode 100644 docs/guide/embed-html.md
create mode 100644 docs/guide/features.md
create mode 100644 docs/guide/introduction.md
create mode 100644 docs/index.md
create mode 100644 docs/package-lock.json
create mode 100644 docs/package.json
create mode 100644 docs/public/logo.svg
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
new file mode 100644
index 0000000..528c79d
--- /dev/null
+++ b/.github/workflows/deploy_docs.yml
@@ -0,0 +1,66 @@
+name: Deploy Docs to GitHub Pages
+
+on:
+ # 在推送到 main 分支时触发
+ push:
+ branches:
+ - main
+ # 允许手动触发
+ workflow_dispatch:
+
+# 设置 GITHUB_TOKEN 的权限,允许部署到 GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# 只允许一个并发部署
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ # 构建任务
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ cache-dependency-path: docs/package-lock.json
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Install dependencies
+ run: npm ci
+ working-directory: docs
+
+ - name: Build with VitePress
+ run: npm run docs:build
+ working-directory: docs
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/.vitepress/dist
+
+ # 部署任务
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ needs: build
+ runs-on: ubuntu-latest
+ name: Deploy
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..3945675
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+.vitepress/dist
+.vitepress/cache
+.DS_Store
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
new file mode 100644
index 0000000..881d97f
--- /dev/null
+++ b/docs/.vitepress/config.mts
@@ -0,0 +1,104 @@
+import { defineConfig } from 'vitepress'
+
+export default defineConfig({
+ title: "Message Nest",
+ description: "灵活而强大的消息推送整合平台",
+ lang: 'zh-CN',
+
+ // 如果部署到 GitHub Pages 的子路径,需要设置 base
+ // 例如:https://engigu.github.io/Message-Push-Nest/
+ // base: '/Message-Push-Nest/',
+
+ // 如果使用自定义域名或部署到根路径,注释掉 base 或设置为 '/'
+ base: '/Message-Push-Nest/',
+
+ themeConfig: {
+ logo: '/logo.svg',
+
+ nav: [
+ { text: '首页', link: '/' },
+ { text: '指南', link: '/guide/introduction' },
+ { text: '部署', link: '/deployment/overview' },
+ { text: 'API', link: '/api/usage' },
+ { text: '演示站点', link: 'https://message-nest-demo-site.qwapi.eu.org/' }
+ ],
+
+ sidebar: {
+ '/guide/': [
+ {
+ text: '开始',
+ items: [
+ { text: '介绍', link: '/guide/introduction' },
+ { text: '特色功能', link: '/guide/features' },
+ { text: '更新日志', link: '/guide/changelog' }
+ ]
+ },
+ {
+ text: '配置',
+ items: [
+ { text: '配置说明', link: '/guide/configuration' },
+ { text: 'EmbedHtml说明', link: '/guide/embed-html' }
+ ]
+ },
+ // {
+ // text: '文档部署',
+ // items: [
+ // { text: '部署到 GitHub Pages', link: '/guide/deploy-to-github-pages' }
+ // ]
+ // }
+ ],
+ '/deployment/': [
+ {
+ text: '部署方式',
+ items: [
+ { text: '部署概览', link: '/deployment/overview' },
+ { text: '直接运行', link: '/deployment/direct-run' },
+ { text: '开发调试', link: '/deployment/development' },
+ { text: 'Docker部署', link: '/deployment/docker' },
+ { text: 'Docker Compose', link: '/deployment/docker-compose' }
+ ]
+ }
+ ],
+ '/api/': [
+ {
+ text: 'API文档',
+ items: [
+ { text: '使用说明', link: '/api/usage' },
+ { text: '调用示例', link: '/api/examples' }
+ ]
+ }
+ ]
+ },
+
+ socialLinks: [
+ { icon: 'github', link: 'https://github.com/engigu/Message-Push-Nest' }
+ ],
+
+ footer: {
+ message: 'Released under the MIT License.',
+ copyright: 'Copyright © 2024-present Message Nest'
+ },
+
+ search: {
+ provider: 'local'
+ },
+
+ outline: {
+ level: [2, 3],
+ label: '目录'
+ },
+
+ docFooter: {
+ prev: '上一页',
+ next: '下一页'
+ },
+
+ lastUpdated: {
+ text: '最后更新于',
+ formatOptions: {
+ dateStyle: 'short',
+ timeStyle: 'short'
+ }
+ }
+ }
+})
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..6e00520
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,49 @@
+# Message Nest 文档站点
+
+这是 Message Nest 项目的 VitePress 文档站点。
+
+## 本地开发
+
+```bash
+# 安装依赖
+npm install
+
+# 启动开发服务器
+npm run docs:dev
+
+# 构建文档
+npm run docs:build
+
+# 预览构建结果
+npm run docs:preview
+```
+
+## 文档结构
+
+```
+docs/
+├── .vitepress/ # VitePress 配置
+│ └── config.mts # 站点配置文件
+├── guide/ # 指南
+│ ├── introduction.md # 介绍
+│ ├── features.md # 特色功能
+│ ├── changelog.md # 更新日志
+│ ├── configuration.md # 配置说明
+│ └── embed-html.md # EmbedHtml说明
+├── deployment/ # 部署
+│ ├── overview.md # 部署概览
+│ ├── direct-run.md # 直接运行
+│ ├── development.md # 开发调试
+│ ├── docker.md # Docker部署
+│ └── docker-compose.md # Docker Compose
+├── api/ # API文档
+│ ├── usage.md # 使用说明
+│ └── examples.md # 调用示例
+├── public/ # 静态资源
+│ └── logo.svg # Logo
+└── index.md # 首页
+```
+
+## 访问地址
+
+开发服务器启动后,默认访问地址为:`http://localhost:5173`
diff --git a/docs/SETUP.md b/docs/SETUP.md
new file mode 100644
index 0000000..97273cb
--- /dev/null
+++ b/docs/SETUP.md
@@ -0,0 +1,133 @@
+# VitePress 文档站点设置完成
+
+## 已完成的工作
+
+✅ 创建了完整的 VitePress 文档站点结构
+✅ 根据 README.md 拆分内容到多个文档页面
+✅ 配置了导航和侧边栏
+✅ 安装了所有必要的依赖
+✅ 创建了美观的首页
+
+## 文档结构
+
+```
+docs/
+├── .vitepress/
+│ └── config.mts # VitePress 配置文件
+├── guide/ # 指南部分
+│ ├── introduction.md # 项目介绍
+│ ├── features.md # 特色功能
+│ ├── changelog.md # 更新日志
+│ ├── configuration.md # 完整配置说明
+│ └── embed-html.md # EmbedHtml 配置详解
+├── deployment/ # 部署部分
+│ ├── overview.md # 部署概览
+│ ├── direct-run.md # 直接运行 Release
+│ ├── development.md # 开发调试模式
+│ ├── docker.md # Docker 部署
+│ └── docker-compose.md # Docker Compose 部署
+├── api/ # API 文档
+│ ├── usage.md # API 使用说明
+│ └── examples.md # 多语言调用示例
+├── public/
+│ └── logo.svg # 站点 Logo
+├── index.md # 首页
+├── package.json # 项目配置
+└── README.md # 文档说明
+
+```
+
+## 快速开始
+
+### 启动开发服务器
+
+```bash
+cd docs
+npm run docs:dev
+```
+
+访问:http://localhost:5173
+
+### 构建生产版本
+
+```bash
+cd docs
+npm run docs:build
+```
+
+构建产物在 `docs/.vitepress/dist` 目录
+
+### 预览构建结果
+
+```bash
+cd docs
+npm run docs:preview
+```
+
+## 文档内容
+
+### 指南部分
+- **介绍** - 项目背景、演示站点、效果图
+- **特色功能** - 核心特性、支持的推送方式、其他功能
+- **更新日志** - 完整的功能更新历史
+- **配置说明** - 详细的配置文件说明和示例
+- **EmbedHtml说明** - 单应用模式和前后端分离模式的详细说明
+
+### 部署部分
+- **部署概览** - 各种部署方式对比和快速选择指南
+- **直接运行** - 使用 Release 可执行文件部署
+- **开发调试** - 本地开发环境搭建
+- **Docker部署** - 两种 Docker 部署方式(配置文件和环境变量)
+- **Docker Compose** - 完整的编排部署方案
+
+### API 文档
+- **使用说明** - API 接口说明、参数、响应格式
+- **调用示例** - 提供 CURL、Python、Go、Java、Node.js、PHP、C#、Ruby 等多种语言的示例
+
+## 主要特性
+
+1. **美观的首页** - 使用 VitePress 的 home layout,展示项目特色
+2. **清晰的导航** - 顶部导航和侧边栏导航
+3. **搜索功能** - 内置本地搜索
+4. **响应式设计** - 支持移动端访问
+5. **中文优化** - 所有界面文字都已本地化
+6. **代码高亮** - 支持多种编程语言的代码高亮
+7. **自动目录** - 每个页面自动生成目录
+
+## 下一步建议
+
+1. 可以在 `docs/public/` 目录添加更多图片和资源
+2. 可以在配置文件中自定义主题颜色
+3. 可以添加更多的文档页面
+4. 可以配置 GitHub Pages 或其他静态站点托管服务进行部署
+
+## 部署到生产环境
+
+### GitHub Pages
+
+1. 在 `.vitepress/config.mts` 中设置 `base`:
+```ts
+export default defineConfig({
+ base: '/Message-Push-Nest/',
+ // ...
+})
+```
+
+2. 构建并部署:
+```bash
+npm run docs:build
+# 将 .vitepress/dist 目录部署到 GitHub Pages
+```
+
+### Vercel / Netlify
+
+直接连接 GitHub 仓库,设置:
+- Build Command: `npm run docs:build`
+- Output Directory: `docs/.vitepress/dist`
+
+## 注意事项
+
+- 文档内容已根据 README.md 进行了合理拆分
+- 保留了所有原始信息和代码示例
+- 添加了更好的组织结构和导航
+- 使用了 VitePress 的特性(如提示框、代码块等)
diff --git a/docs/api/examples.md b/docs/api/examples.md
new file mode 100644
index 0000000..870cf87
--- /dev/null
+++ b/docs/api/examples.md
@@ -0,0 +1,305 @@
+# 调用示例
+
+本页面提供各种编程语言的API调用示例。
+
+## CURL
+
+```bash
+curl -X POST --location 'http://127.0.0.1:8000/api/v1/message/send' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+ }'
+```
+
+## Python
+
+```python
+import requests
+
+headers = {
+ 'Content-Type': 'application/json',
+}
+
+json_data = {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+}
+
+response = requests.post(
+ 'http://127.0.0.1:8000/api/v1/message/send',
+ headers=headers,
+ json=json_data
+)
+
+print("response:", response.json())
+```
+
+### 使用 requests 库
+
+首先安装依赖:
+
+```bash
+pip install requests
+```
+
+## Go
+
+```go
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+)
+
+func main() {
+ client := &http.Client{}
+ var data = strings.NewReader(`{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+}`)
+ req, err := http.NewRequest("POST", "http://127.0.0.1:8000/api/v1/message/send", data)
+ if err != nil {
+ log.Fatal(err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+ bodyText, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s\n", bodyText)
+}
+```
+
+## Java
+
+```java
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+
+public class MessageNestExample {
+ public static void main(String[] args) throws IOException, InterruptedException {
+ HttpClient client = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .build();
+
+ String jsonData = """
+ {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+ }
+ """;
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create("http://127.0.0.1:8000/api/v1/message/send"))
+ .POST(BodyPublishers.ofString(jsonData))
+ .setHeader("Content-Type", "application/json")
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.body());
+ }
+}
+```
+
+## Node.js
+
+### 使用 request 库
+
+```javascript
+var request = require('request');
+
+var headers = {
+ 'Content-Type': 'application/json'
+};
+
+var dataString = JSON.stringify({
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+});
+
+var options = {
+ url: 'http://127.0.0.1:8000/api/v1/message/send',
+ method: 'POST',
+ headers: headers,
+ body: dataString
+};
+
+function callback(error, response, body) {
+ if (!error && response.statusCode == 200) {
+ console.log(body);
+ }
+}
+
+request(options, callback);
+```
+
+### 使用 axios 库
+
+```javascript
+const axios = require('axios');
+
+const data = {
+ token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title: "message title",
+ text: "Hello World!"
+};
+
+axios.post('http://127.0.0.1:8000/api/v1/message/send', data, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+})
+.then(response => {
+ console.log('response:', response.data);
+})
+.catch(error => {
+ console.error('error:', error);
+});
+```
+
+### 使用 fetch (Node.js 18+)
+
+```javascript
+const data = {
+ token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title: "message title",
+ text: "Hello World!"
+};
+
+fetch('http://127.0.0.1:8000/api/v1/message/send', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+})
+.then(response => response.json())
+.then(data => {
+ console.log('response:', data);
+})
+.catch(error => {
+ console.error('error:', error);
+});
+```
+
+## PHP
+
+```php
+ "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title" => "message title",
+ "text" => "Hello World!"
+);
+
+curl_setopt($ch, CURLOPT_URL, 'http://127.0.0.1:8000/api/v1/message/send');
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+]);
+curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+
+$response = curl_exec($ch);
+
+if (curl_errno($ch)) {
+ echo 'Error:' . curl_error($ch);
+} else {
+ echo $response;
+}
+
+curl_close($ch);
+?>
+```
+
+## C#
+
+```csharp
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+class Program
+{
+ static async Task Main(string[] args)
+ {
+ using var client = new HttpClient();
+
+ var data = new
+ {
+ token = "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title = "message title",
+ text = "Hello World!"
+ };
+
+ var json = JsonSerializer.Serialize(data);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync(
+ "http://127.0.0.1:8000/api/v1/message/send",
+ content
+ );
+
+ var responseString = await response.Content.ReadAsStringAsync();
+ Console.WriteLine(responseString);
+ }
+}
+```
+
+## Ruby
+
+```ruby
+require 'net/http'
+require 'json'
+require 'uri'
+
+uri = URI('http://127.0.0.1:8000/api/v1/message/send')
+http = Net::HTTP.new(uri.host, uri.port)
+
+request = Net::HTTP::Post.new(uri.path, {
+ 'Content-Type' => 'application/json'
+})
+
+request.body = {
+ token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e',
+ title: 'message title',
+ text: 'Hello World!'
+}.to_json
+
+response = http.request(request)
+puts response.body
+```
+
+## 注意事项
+
+::: tip 提示
+- 将示例中的 `http://127.0.0.1:8000` 替换为你的实际服务地址
+- 将 `a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e` 替换为你在管理后台创建的实际 Token
+- 建议在生产环境中使用 HTTPS
+:::
diff --git a/docs/api/usage.md b/docs/api/usage.md
new file mode 100644
index 0000000..f46e66e
--- /dev/null
+++ b/docs/api/usage.md
@@ -0,0 +1,106 @@
+# API 使用说明
+
+Message Nest 提供统一的消息推送API接口。
+
+## 接口地址
+
+```
+POST /api/v1/message/send
+```
+
+## 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | string | 是 | 推送令牌,在管理后台查看 |
+| title | string | 是 | 消息标题 |
+| text | string | 是 | 消息内容 |
+
+## 请求示例
+
+```json
+{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+}
+```
+
+## 响应格式
+
+### 成功响应
+
+```json
+{
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "status": "sent"
+ }
+}
+```
+
+### 失败响应
+
+```json
+{
+ "code": 400,
+ "msg": "error message",
+ "data": null
+}
+```
+
+## 获取 Token
+
+1. 登录 Message Nest 管理后台
+2. 进入"发送任务"页面
+3. 创建新的发送任务
+4. 配置推送渠道(邮件、钉钉、企业微信等)
+5. 保存后获得推送令牌(Token)
+
+## 支持的推送渠道
+
+- **邮件** - SMTP邮件发送
+- **钉钉** - 钉钉机器人
+- **企业微信** - 企业微信应用消息
+- **微信公众号** - 微信测试公众号模板消息
+- **自定义Webhook** - 自定义HTTP请求
+- **自托管消息** - 站内消息
+
+## 使用流程
+
+1. **创建推送渠道**
+ - 在管理后台配置各种推送渠道
+ - 填写相应的配置信息(如邮箱、Webhook地址等)
+
+2. **创建发送任务**
+ - 选择要使用的推送渠道
+ - 可以选择多个渠道同时推送
+ - 获得唯一的推送令牌(Token)
+
+3. **调用API发送消息**
+ - 使用获得的 Token
+ - 发送标题和内容
+ - 消息会自动推送到配置的所有渠道
+
+## 注意事项
+
+::: warning 重要
+- Token 是唯一的,请妥善保管
+- 消息内容支持纯文本和Markdown格式(取决于推送渠道)
+- 建议使用异步方式调用API,避免阻塞主流程
+:::
+
+## 错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 请求参数错误 |
+| 401 | 未授权 |
+| 404 | Token不存在 |
+| 500 | 服务器内部错误 |
+
+## 下一步
+
+查看各语言的 [调用示例](/api/examples)。
diff --git a/docs/deploy.sh b/docs/deploy.sh
new file mode 100644
index 0000000..e69de29
diff --git a/docs/deployment/development.md b/docs/deployment/development.md
new file mode 100644
index 0000000..78ad2f7
--- /dev/null
+++ b/docs/deployment/development.md
@@ -0,0 +1,151 @@
+# 开发调试
+
+本文档介绍如何在开发环境中运行 Message Nest。
+
+## 前置要求
+
+- Go 1.18+
+- Node.js 16+
+- MySQL 5.7+ 或 SQLite
+
+## 部署步骤
+
+### 1. 克隆项目
+
+```bash
+git clone https://github.com/engigu/Message-Push-Nest.git
+cd Message-Push-Nest
+```
+
+### 2. 配置文件
+
+重命名 `conf/app.example.ini` 为 `conf/app.ini`,关键配置如下:
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+; RunMode务必设置成debug,会自动添加跨域
+RunMode = debug
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 取消EmbedHtml的注释(启用前后端分离),然后到web目录下面,npm run dev启动前端页面
+EmbedHtml = disable
+
+[database]
+; 开启SQL打印
+SqlDebug = enable
+
+; Type = sqlite
+Type = mysql
+User = root
+Password = Aa123456
+Host = vm.server
+Port = 3308
+Name = yourDbName
+TablePrefix = message_
+```
+
+::: warning 重要配置
+- `RunMode` 必须设置为 `debug`,会自动添加跨域
+- `EmbedHtml` 必须取消注释,启用前后端分离
+- `SqlDebug` 建议启用,方便调试
+:::
+
+### 3. 启动后端服务
+
+```bash
+go mod tidy
+go run main.go
+```
+
+服务启动后会运行在8000端口。
+
+### 4. 启动前端服务
+
+```bash
+cd web
+npm i
+npm run dev
+```
+
+页面启动后会提示访问URL,一般是 `http://127.0.0.1:5173`。
+
+### 5. 访问应用
+
+访问 `http://127.0.0.1:5173`,进行调试开发。
+
+接口会自动转发到go服务 `http://localhost:8000`。
+
+## 开发说明
+
+### 目录结构
+
+```
+Message-Push-Nest/
+├── conf/ # 配置文件
+├── middleware/ # 中间件
+├── migrate/ # 数据库迁移
+├── models/ # 数据模型
+├── pkg/ # 工具包
+├── routers/ # 路由
+├── service/ # 业务逻辑
+├── web/ # 前端项目
+│ ├── src/ # 源代码
+│ ├── public/ # 静态资源
+│ └── dist/ # 构建输出
+└── main.go # 入口文件
+```
+
+### 前端技术栈
+
+- Vue 3
+- TypeScript
+- Vite
+- TailwindCSS
+- shadcn-vue
+
+### 后端技术栈
+
+- Go
+- Gin
+- GORM
+- Logrus
+
+## 构建生产版本
+
+### 构建前端
+
+```bash
+cd web
+npm run build
+```
+
+构建产物会输出到 `web/dist` 目录。
+
+### 构建后端
+
+```bash
+CGO_ENABLED=0 go build -o Message-Nest
+```
+
+## 常见问题
+
+### 前端无法连接后端
+
+检查 `web/vite.config.ts` 中的代理配置是否正确。
+
+### 热更新不生效
+
+1. 检查文件是否保存
+2. 重启前端开发服务器
+3. 清除浏览器缓存
+
+### 数据库连接失败
+
+1. 检查数据库服务是否启动
+2. 检查配置文件中的连接信息
+3. 检查数据库用户权限
diff --git a/docs/deployment/direct-run.md b/docs/deployment/direct-run.md
new file mode 100644
index 0000000..7f745ea
--- /dev/null
+++ b/docs/deployment/direct-run.md
@@ -0,0 +1,112 @@
+# 直接运行
+
+使用最新的Release打包的可执行文件部署,无需部署前端页面。
+
+::: tip 推荐指数
+🍀🍀🍀🍀 适合没有Docker环境的生产部署
+:::
+
+## 部署步骤
+
+### 1. 下载Release
+
+访问 [GitHub Releases](https://github.com/engigu/Message-Push-Nest/releases) 下载最新的系统版本对应的release,然后解压。
+
+### 2. 创建数据库
+
+新建一个MySQL数据库(或使用SQLite)。
+
+### 3. 配置文件
+
+重命名 `conf/app.example.ini` 为 `conf/app.ini`,然后修改配置:
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+RunMode = release
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 注释EmbedHtml,启用单应用模式
+; EmbedHtml = disable
+
+[database]
+; 关闭SQL打印
+; SqlDebug = enable
+
+; Type = sqlite
+Type = mysql
+User = root
+Password = Aa123456
+Host = vm.server
+Port = 3308
+Name = yourDbName
+TablePrefix = message_
+```
+
+::: warning 重要
+将配置中 `EmbedHtml = disable` 进行注释,以单应用方式运行。
+:::
+
+### 4. 启动项目
+
+直接运行可执行文件,项目会自动创建表和账号。
+
+```bash
+# Windows
+./Message-Nest.exe
+
+# Linux/Mac
+./Message-Nest
+```
+
+### 5. 查看日志
+
+INFO日志级别启动会出现如下日志:
+
+```log
+[2024-01-13 13:40:09.075] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_auth
+[2024-01-13 13:40:11.778] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks
+[2024-01-13 13:40:16.518] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_ways
+[2024-01-13 13:40:23.300] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks_logs
+[2024-01-13 13:40:28.715] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks_ins
+[2024-01-13 13:40:39.538] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_settings
+[2024-01-13 13:40:46.299] INFO [migrate.go:74 Setup] [Init Data]: Init Account data...
+[2024-01-13 13:40:46.751] INFO [migrate.go:77 Setup] [Init Data]: All table data init done.
+```
+
+### 6. 访问服务
+
+访问 `http://localhost:8000`
+
+- 默认账号:`admin`
+- 默认密码:`123456`
+
+## 使用SQLite
+
+如果不想安装MySQL,可以使用SQLite:
+
+```ini
+[database]
+Type = sqlite
+TablePrefix = message_
+```
+
+SQLite数据库文件会自动创建在 `conf/database.db`。
+
+## 常见问题
+
+### 启动失败
+
+1. 检查端口8000是否被占用
+2. 检查数据库连接配置是否正确
+3. 查看日志输出的错误信息
+
+### 无法访问页面
+
+1. 确认服务已正常启动
+2. 检查防火墙设置
+3. 确认 `EmbedHtml` 配置已注释
diff --git a/docs/deployment/docker-compose.md b/docs/deployment/docker-compose.md
new file mode 100644
index 0000000..1086bcc
--- /dev/null
+++ b/docs/deployment/docker-compose.md
@@ -0,0 +1,240 @@
+# Docker Compose 部署
+
+使用 Docker Compose 编排部署 Message Nest。
+
+::: tip 推荐指数
+🍀🍀🍀🍀🍀 适合生产环境,便于管理
+:::
+
+## 方式一:挂载配置文件
+
+### 1. 准备配置文件
+
+创建 `conf/app.ini`,内容参考 [Docker部署](/deployment/docker#_1-准备配置文件)。
+
+### 2. 创建 docker-compose.yml
+
+```yaml
+version: "3.7"
+services:
+
+ message-nest:
+ image: engigu/message-nest:latest
+ # 或使用 GHCR 镜像
+ # image: ghcr.io/engigu/message-nest:latest
+ container_name: message-nest
+ restart: always
+ volumes:
+ - ./conf:/app/conf
+ ports:
+ - "8000:8000"
+```
+
+### 3. 文件目录结构
+
+```
+.
+├── conf
+│ └── app.ini
+└── docker-compose.yml
+```
+
+### 4. 启动服务
+
+```bash
+# 测试运行
+docker-compose up
+
+# 正式运行(后台)
+docker-compose up -d
+```
+
+## 方式二:环境变量(推荐)
+
+### 使用 MySQL
+
+创建 `docker-compose.yml`:
+
+```yaml
+version: "3.7"
+services:
+
+ message-nest:
+ image: engigu/message-nest:latest
+ # 或使用 GHCR 镜像
+ # image: ghcr.io/engigu/message-nest:latest
+ container_name: message-nest
+ restart: always
+ ports:
+ - "8000:8000"
+ environment:
+ - MYSQL_HOST=192.168.64.133
+ - MYSQL_PORT=3308
+ - MYSQL_USER=root
+ - MYSQL_PASSWORD=Aa123456
+ - MYSQL_DB=test_11
+ - MYSQL_TABLE_PREFIX=message_
+```
+
+### 使用 SQLite
+
+创建 `docker-compose.yml`:
+
+```yaml
+version: "3.7"
+services:
+
+ message-nest:
+ image: engigu/message-nest:latest
+ # 或使用 GHCR 镜像
+ # image: ghcr.io/engigu/message-nest:latest
+ container_name: message-nest
+ restart: always
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./data/database.db:/app/conf/database.db
+```
+
+### 启动服务
+
+```bash
+# 正式运行
+docker-compose up -d
+```
+
+## 完整示例:MySQL + Message Nest
+
+如果你还没有MySQL,可以使用以下配置同时部署MySQL和Message Nest:
+
+```yaml
+version: "3.7"
+services:
+
+ mysql:
+ image: mysql:8.0
+ container_name: message-nest-mysql
+ restart: always
+ environment:
+ - MYSQL_ROOT_PASSWORD=Aa123456
+ - MYSQL_DATABASE=message_nest
+ volumes:
+ - mysql-data:/var/lib/mysql
+ ports:
+ - "3306:3306"
+
+ message-nest:
+ image: engigu/message-nest:latest
+ container_name: message-nest
+ restart: always
+ depends_on:
+ - mysql
+ ports:
+ - "8000:8000"
+ environment:
+ - MYSQL_HOST=mysql
+ - MYSQL_PORT=3306
+ - MYSQL_USER=root
+ - MYSQL_PASSWORD=Aa123456
+ - MYSQL_DB=message_nest
+ - MYSQL_TABLE_PREFIX=message_
+
+volumes:
+ mysql-data:
+```
+
+## 常用命令
+
+### 启动服务
+
+```bash
+# 前台启动(查看日志)
+docker-compose up
+
+# 后台启动
+docker-compose up -d
+```
+
+### 查看日志
+
+```bash
+# 查看所有服务日志
+docker-compose logs
+
+# 查看特定服务日志
+docker-compose logs message-nest
+
+# 实时查看日志
+docker-compose logs -f message-nest
+```
+
+### 停止服务
+
+```bash
+# 停止服务
+docker-compose stop
+
+# 停止并删除容器
+docker-compose down
+
+# 停止并删除容器和数据卷
+docker-compose down -v
+```
+
+### 重启服务
+
+```bash
+# 重启所有服务
+docker-compose restart
+
+# 重启特定服务
+docker-compose restart message-nest
+```
+
+### 更新镜像
+
+```bash
+# 拉取最新镜像
+docker-compose pull
+
+# 重新创建容器
+docker-compose up -d
+```
+
+## 访问服务
+
+启动后访问 `http://localhost:8000`
+
+- 默认账号:`admin`
+- 默认密码:`123456`
+
+## 常见问题
+
+### 服务无法启动
+
+1. 检查端口是否被占用
+2. 检查配置是否正确
+3. 查看日志:`docker-compose logs`
+
+### MySQL连接失败
+
+1. 确保MySQL服务已启动
+2. 检查 `MYSQL_HOST` 是否正确(使用服务名)
+3. 等待MySQL完全启动(约10-30秒)
+
+### 数据持久化
+
+使用volumes确保数据持久化:
+```yaml
+volumes:
+ - ./data:/app/conf # 配置文件
+ - mysql-data:/var/lib/mysql # MySQL数据
+```
+
+### 修改配置后重启
+
+```bash
+# 修改配置文件或环境变量后
+docker-compose down
+docker-compose up -d
+```
diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md
new file mode 100644
index 0000000..490eb79
--- /dev/null
+++ b/docs/deployment/docker.md
@@ -0,0 +1,201 @@
+# Docker 部署
+
+使用 Docker 部署 Message Nest,支持多种配置方式。
+
+::: tip 推荐指数
+🍀🍀🍀🍀🍀 最推荐的部署方式
+:::
+
+## 镜像源
+
+Message Nest 提供两个镜像源:
+
+- **Docker Hub**: `engigu/message-nest:latest`
+- **GitHub Container Registry**: `ghcr.io/engigu/message-nest:latest`
+
+## 方式一:挂载配置文件
+
+### 1. 准备配置文件
+
+新建 `conf/app.ini` 文件:
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+RunMode = release
+; docker模式下端口配置文件中只能为8000
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 注释EmbedHtml,启用单应用模式
+; EmbedHtml = disable
+
+[database]
+; 关闭SQL打印
+; SqlDebug = enable
+
+; Type = sqlite
+Type = mysql
+User = root
+Password = Aa123456
+Host = vm.server
+Port = 3308
+Name = yourDbName
+TablePrefix = message_
+```
+
+::: warning 端口限制
+Docker模式下,配置文件中的端口只能为8000,通过 `-p` 参数映射到宿主机端口。
+:::
+
+### 2. 拉取镜像
+
+```bash
+# 从 Docker Hub 拉取
+docker pull engigu/message-nest:latest
+
+# 或从 GitHub Container Registry (GHCR) 拉取
+docker pull ghcr.io/engigu/message-nest:latest
+```
+
+### 3. 启动容器
+
+```bash
+# 测试运行(Docker Hub 镜像)
+docker run --rm -ti \
+ -p 8000:8000 \
+ -v /your/path/conf:/app/conf \
+ engigu/message-nest:latest
+
+# 测试运行(GHCR 镜像)
+docker run --rm -ti \
+ -p 8000:8000 \
+ -v /your/path/conf:/app/conf \
+ ghcr.io/engigu/message-nest:latest
+
+# 正式运行(Docker Hub 镜像)
+docker run -d \
+ -p 8000:8000 \
+ -v /your/path/conf:/app/conf \
+ engigu/message-nest:latest
+
+# 正式运行(GHCR 镜像)
+docker run -d \
+ -p 8000:8000 \
+ -v /your/path/conf:/app/conf \
+ ghcr.io/engigu/message-nest:latest
+```
+
+## 方式二:环境变量(推荐)
+
+::: tip 推荐
+这是最简单的部署方式,无需准备配置文件。
+:::
+
+### 环境变量说明
+
+| 变量 | 说明 |
+|------|------|
+| JWT_SECRET | jwt秘钥,可选,默认为message-nest |
+| LOG_LEVEL | 日志等级,可选,默认为INFO,DEBUG/INFO/ERROR |
+| RUN_MODE | 运行模式,可选,默认release,为debug将自动添加跨域 |
+| DB_TYPE | 数据库类型,sqlite/mysql。默认为sqlite,存储路径为conf/database.db |
+| MYSQL_HOST | mysql-host,DB_TYPE=mysql必填 |
+| MYSQL_PORT | mysql端口,DB_TYPE=mysql必填 |
+| MYSQL_USER | mysql用户名,DB_TYPE=mysql必填 |
+| MYSQL_PASSWORD | mysql数据库密码,DB_TYPE=mysql必填 |
+| MYSQL_DB | mysql数据库名字,DB_TYPE=mysql必填 |
+| MYSQL_TABLE_PREFIX | mysql数据表前缀,DB_TYPE=mysql必填 |
+| SSL | 是否开启SSL |
+| SQL_DEBUG | 是否打印SQL,可选,默认关,设置enable为开启 |
+
+### 使用 MySQL
+
+```bash
+# 正式运行(mysql)
+docker run -d \
+ -p 8000:8000 \
+ -e MYSQL_HOST=192.168.64.133 \
+ -e MYSQL_PORT=3308 \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=Aa123456 \
+ -e MYSQL_DB=test_11 \
+ -e MYSQL_TABLE_PREFIX=message_ \
+ --name message-nest \
+ engigu/message-nest:latest
+
+# 或使用 GHCR 镜像(mysql)
+docker run -d \
+ -p 8000:8000 \
+ -e MYSQL_HOST=192.168.64.133 \
+ -e MYSQL_PORT=3308 \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=Aa123456 \
+ -e MYSQL_DB=test_11 \
+ -e MYSQL_TABLE_PREFIX=message_ \
+ --name message-nest \
+ ghcr.io/engigu/message-nest:latest
+```
+
+### 使用 SQLite
+
+```bash
+# 正式运行(sqlite)
+docker run -d \
+ -p 8000:8000 \
+ -v /your/path/database.db:/app/conf/database.db \
+ --name message-nest \
+ engigu/message-nest:latest
+
+# 或使用 GHCR 镜像(sqlite)
+docker run -d \
+ -p 8000:8000 \
+ -v /your/path/database.db:/app/conf/database.db \
+ --name message-nest \
+ ghcr.io/engigu/message-nest:latest
+```
+
+## 访问服务
+
+启动后访问 `http://localhost:8000`
+
+- 默认账号:`admin`
+- 默认密码:`123456`
+
+## 常见问题
+
+### 容器无法启动
+
+1. 检查端口是否被占用
+2. 检查数据库连接配置
+3. 查看容器日志:`docker logs message-nest`
+
+### 数据持久化
+
+使用SQLite时,记得挂载数据库文件:
+```bash
+-v /your/path/database.db:/app/conf/database.db
+```
+
+### 查看日志
+
+```bash
+# 查看实时日志
+docker logs -f message-nest
+
+# 查看最近100行日志
+docker logs --tail 100 message-nest
+```
+
+### 停止和删除容器
+
+```bash
+# 停止容器
+docker stop message-nest
+
+# 删除容器
+docker rm message-nest
+```
diff --git a/docs/deployment/overview.md b/docs/deployment/overview.md
new file mode 100644
index 0000000..75598e9
--- /dev/null
+++ b/docs/deployment/overview.md
@@ -0,0 +1,53 @@
+# 部署概览
+
+Message Nest 提供多种部署方式,您可以根据自己的需求选择合适的部署方案。
+
+## 部署方式对比
+
+| 部署方式 | 难度 | 推荐指数 | 适用场景 |
+|---------|------|---------|---------|
+| Docker环境变量 | ⭐ | ⭐⭐⭐⭐⭐ | 生产环境,快速部署 |
+| Docker Compose | ⭐ | ⭐⭐⭐⭐⭐ | 生产环境,编排部署 |
+| 直接运行Release | ⭐⭐ | ⭐⭐⭐⭐ | 生产环境,无Docker环境 |
+| 开发调试 | ⭐⭐⭐ | ⭐⭐⭐ | 开发环境 |
+
+## 快速选择
+
+### 我想快速体验
+推荐使用 **Docker环境变量部署**,一条命令即可启动。
+
+### 我要用于生产环境
+推荐使用 **Docker Compose部署**,便于管理和维护。
+
+### 我没有Docker环境
+推荐使用 **直接运行Release**,下载可执行文件即可。
+
+### 我要进行开发
+推荐使用 **开发调试模式**,支持热更新。
+
+## 默认账号
+
+所有部署方式启动后,默认账号信息:
+- 用户名:`admin`
+- 密码:`123456`
+
+::: warning 安全提示
+首次登录后请立即修改默认密码!
+:::
+
+## 数据库选择
+
+Message Nest 支持多种数据库:
+
+- **SQLite** - 轻量级,无需额外配置,适合小规模使用
+- **MySQL 5.x / 8.x** - 成熟稳定,适合中大规模使用
+- **TiDB** - 分布式数据库,适合大规模使用
+
+## 下一步
+
+选择适合您的部署方式:
+
+- [直接运行](/deployment/direct-run)
+- [开发调试](/deployment/development)
+- [Docker部署](/deployment/docker)
+- [Docker Compose](/deployment/docker-compose)
diff --git a/docs/guide/changelog.md b/docs/guide/changelog.md
new file mode 100644
index 0000000..3f22cd9
--- /dev/null
+++ b/docs/guide/changelog.md
@@ -0,0 +1,64 @@
+# 更新日志
+
+## 2025
+
+### 2025.10.12
+- 增加cookies过期天数设置
+
+### 2025.09.30
+- 支持页面的明暗主题切换设置
+- 增加登录日志
+
+### 2025.09.14
+- 支持系统信息展示更多的运行信息
+
+### 2025.08.10
+- **重大升级,重构web页面,样式更加美观友好**
+- UI组件使用shadcn-vue,基于tailwindcss
+- 现在支持大中小屏自适应
+
+### 2025.04.28
+- 支持tidb作为存储
+- 支持数据库ssl配置
+- docker运行指定环境变量SSL=true
+- ini配置文件运行指定数据库连接参数Ssl=true
+
+### 2025.01.01
+- 支持自托管消息,现在可以将站点作为消息的接受,登录站点查看消息
+
+## 2024
+
+### 2024.04.29
+- 支持 SQLite 部署
+- 支持MySQL 5.x / 8.x
+
+### 2024.04.11
+- 支持自定义的定时消息发送
+
+### 2024.03.05
+- 支持微信测试公众号模板消息发送
+
+### 2024.01.28
+- 支持 Docker 部署,从环境变量启动服务
+
+### 2024.01.24
+- 支持数据统计展示
+
+### 2024.01.20
+- 添加日志查看自动刷新
+
+### 2024.01.07
+- 支持站点信息自定义
+
+### 2024.01.03
+- 支持企业微信
+- 单应用打包,直接运行,无需部署前端页面
+- 支持邮件发送
+- 用户密码设置
+- 支持用户定时任务清理,更新定时时间
+- 查看定时清理日志
+- 单应用的 HTML 浏览器自动缓存
+- Gin 的日志使用 Logrus
+- 支持异步发送
+- 支持钉钉
+- 支持自定义的 Webhook 消息发送
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
new file mode 100644
index 0000000..c7c248c
--- /dev/null
+++ b/docs/guide/configuration.md
@@ -0,0 +1,170 @@
+# 配置说明
+
+## 完整配置文件
+
+```ini
+[app]
+JwtSecret = message-nest
+; 暂时无用
+RuntimeRootPath = runtime/
+LogLevel = INFO
+
+[server]
+; debug or release
+; debug模式下会自动添加跨域headers
+RunMode = release
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; use embed html static file
+; 是否使用embed打包的静态资源
+; 如果运行release打包后的应用,请注释这个设置。
+; 如果取消这个注释,只会单独运行api服务,前端页面需要到web目录手动npm run dev, 运行前端服务
+; EmbedHtml = disable
+
+[database]
+; 配置使用什么数据库,支持:mysql、sqlite、tidb
+Type = mysql
+User = root
+Password = password
+Host = 123.1.1.1
+Name = db_name
+Port = 3306
+; -- 其他配置
+; 表前缀
+TablePrefix = message_
+; -- 是否打开sql打印
+; SqlDebug = enable
+; 数据库连接是否开启ssl, value: [false | true]
+Ssl = true
+```
+
+## 配置项说明
+
+### [app] 应用配置
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| JwtSecret | JWT密钥,用于token生成 | message-nest |
+| RuntimeRootPath | 运行时根路径(暂时无用) | runtime/ |
+| LogLevel | 日志级别:DEBUG/INFO/ERROR | INFO |
+
+### [server] 服务器配置
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| RunMode | 运行模式:debug/release,debug模式会自动添加跨域 | release |
+| HttpPort | HTTP服务端口 | 8000 |
+| ReadTimeout | 读取超时时间(秒) | 60 |
+| WriteTimeout | 写入超时时间(秒) | 60 |
+| EmbedHtml | 是否使用embed打包的静态资源,注释则启用单应用模式 | - |
+
+### [database] 数据库配置
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| Type | 数据库类型:mysql/sqlite/tidb | mysql |
+| User | 数据库用户名 | - |
+| Password | 数据库密码 | - |
+| Host | 数据库主机地址 | - |
+| Port | 数据库端口 | 3306 |
+| Name | 数据库名称 | - |
+| TablePrefix | 数据表前缀 | message_ |
+| SqlDebug | 是否打印SQL,设置enable开启 | - |
+| Ssl | 数据库连接是否开启SSL | false |
+
+## Docker 环境变量
+
+使用Docker部署时,可以通过环境变量进行配置:
+
+| 变量 | 说明 |
+|------|------|
+| JWT_SECRET | jwt秘钥,可选,默认为message-nest |
+| LOG_LEVEL | 日志等级,可选,默认为INFO,DEBUG/INFO/ERROR |
+| RUN_MODE | 运行模式,可选,默认release,为debug将自动添加跨域 |
+| DB_TYPE | 数据库类型,sqlite/mysql。默认为sqlite,存储路径为conf/database.db |
+| MYSQL_HOST | mysql-host,DB_TYPE=mysql必填 |
+| MYSQL_PORT | mysql端口,DB_TYPE=mysql必填 |
+| MYSQL_USER | mysql用户名,DB_TYPE=mysql必填 |
+| MYSQL_PASSWORD | mysql数据库密码,DB_TYPE=mysql必填 |
+| MYSQL_DB | mysql数据库名字,DB_TYPE=mysql必填 |
+| MYSQL_TABLE_PREFIX | mysql数据表前缀,DB_TYPE=mysql必填 |
+| SSL | 是否开启SSL |
+| SQL_DEBUG | 是否打印SQL,可选,默认关,设置enable为开启 |
+
+## 配置示例
+
+### 单应用模式(推荐)
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+RunMode = release
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 注释EmbedHtml,启用单应用模式
+; EmbedHtml = disable
+
+[database]
+; 关闭SQL打印
+; SqlDebug = enable
+
+Type = mysql
+User = root
+Password = Aa123456
+Host = vm.server
+Port = 3308
+Name = yourDbName
+TablePrefix = message_
+```
+
+### 开发调试模式
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+; RunMode务必设置成debug,会自动添加跨域
+RunMode = debug
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 取消EmbedHtml的注释(启用前后端分离),然后到web目录下面,npm run dev启动前端页面
+EmbedHtml = disable
+
+[database]
+; 开启SQL打印
+SqlDebug = enable
+
+Type = mysql
+User = root
+Password = Aa123456
+Host = vm.server
+Port = 3308
+Name = yourDbName
+TablePrefix = message_
+```
+
+### SQLite 配置
+
+```ini
+[app]
+JwtSecret = message-nest
+LogLevel = INFO
+
+[server]
+RunMode = release
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+
+[database]
+Type = sqlite
+TablePrefix = message_
+```
diff --git a/docs/guide/embed-html.md b/docs/guide/embed-html.md
new file mode 100644
index 0000000..b3adcaf
--- /dev/null
+++ b/docs/guide/embed-html.md
@@ -0,0 +1,98 @@
+# EmbedHtml 配置说明
+
+## 什么是 EmbedHtml?
+
+这个配置可以理解为单应用模式(或者前后端分离)的开关。
+
+## 配置方式
+
+### 方式一:单应用模式(推荐)
+
+**注释 EmbedHtml 配置**
+
+```ini
+[server]
+RunMode = release
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 注释EmbedHtml,启用单应用模式
+; EmbedHtml = disable
+```
+
+**说明:**
+- 启动go服务,会把web/dist目录下文件作为前端静态资源
+- 如果目录下没有静态资源文件,需要到web目录下,执行 `npm run build` 构建生成
+- 只需要运行一个服务即可访问完整功能
+- 适合生产环境部署
+
+**适用场景:**
+- 使用 Release 打包的可执行文件
+- Docker 部署
+- 生产环境
+
+### 方式二:前后端分离模式
+
+**启用 EmbedHtml 配置**
+
+```ini
+[server]
+RunMode = debug
+HttpPort = 8000
+ReadTimeout = 60
+WriteTimeout = 60
+; 取消注释,启用前后端分离
+EmbedHtml = disable
+```
+
+**说明:**
+- go服务启动时只会有API服务
+- 需要到web目录下,执行 `npm run dev` 启动前端项目
+- 访问前端项目提示的端口服务,一般是 `http://127.0.0.1:5173`
+- 或者使用 `npm run build`,用Nginx部署前端
+
+**适用场景:**
+- 开发调试
+- 需要修改前端代码
+- 前后端独立部署
+
+## 两种方式对比
+
+| 特性 | 单应用模式 | 前后端分离模式 |
+|------|-----------|--------------|
+| 部署复杂度 | 简单,只需一个服务 | 复杂,需要两个服务 |
+| 开发调试 | 不便,需要重新构建 | 方便,支持热更新 |
+| 资源占用 | 较少 | 较多 |
+| 适用环境 | 生产环境 | 开发环境 |
+| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
+
+## 推荐方案
+
+综合考虑下来,推荐直接使用以下方式:
+
+1. **Release 打包执行文件** - 已内置页面静态资源,开箱即用
+2. **Docker 环境变量部署** - 配置简单,一键启动
+
+这两种方式都是单应用模式,只需运行一个服务即可。
+
+## 常见问题
+
+### Q: 为什么访问页面显示404?
+
+A: 检查以下几点:
+1. 如果是单应用模式,确保 `EmbedHtml` 已注释
+2. 确保 `web/dist` 目录下有静态资源文件
+3. 如果没有,执行 `cd web && npm run build` 构建
+
+### Q: 开发时如何快速调试?
+
+A:
+1. 设置 `RunMode = debug`
+2. 启用 `EmbedHtml = disable`
+3. 启动后端:`go run main.go`
+4. 启动前端:`cd web && npm run dev`
+5. 访问前端提示的URL(通常是 http://127.0.0.1:5173)
+
+### Q: Docker 部署需要配置 EmbedHtml 吗?
+
+A: 不需要。Docker 镜像已经内置了前端静态资源,默认就是单应用模式。
diff --git a/docs/guide/features.md b/docs/guide/features.md
new file mode 100644
index 0000000..046629d
--- /dev/null
+++ b/docs/guide/features.md
@@ -0,0 +1,77 @@
+# 特色功能
+
+## 核心特性
+
+### 🔄 整合性
+
+提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。
+
+### 🎨 自定义性
+
+可以根据需求定制消息推送策略,满足不同场景的个性化需求。
+
+### 🛠 开放性
+
+易于扩展和集成新的消息通知服务,以适应未来的变化。
+
+## 支持的推送方式
+
+- **邮件发送** - 支持标准SMTP邮件发送
+- **钉钉** - 支持钉钉机器人消息推送
+- **企业微信** - 支持企业微信应用消息推送
+- **微信测试公众号** - 支持微信测试公众号模板消息发送
+- **自定义 Webhook** - 支持自定义的Webhook消息发送
+- **自托管消息** - 可以将站点作为消息的接收方,登录站点查看消息
+
+## 其他功能
+
+### 定时任务
+
+支持自定义的定时消息发送,可以设置定时推送任务。
+
+### 数据统计
+
+支持数据统计展示,可以查看消息发送情况和历史记录。
+
+### 异步发送
+
+支持异步消息发送,提高系统性能和响应速度。
+
+### 日志管理
+
+- 支持日志查看自动刷新
+- 支持查看定时清理日志
+- 支持登录日志记录
+
+### 用户管理
+
+- 支持用户密码设置
+- 支持用户定时任务清理
+- 支持更新定时时间
+
+### 系统信息
+
+- 支持系统信息展示
+- 支持站点信息自定义
+- 支持明暗主题切换设置
+- 支持Cookies过期天数设置
+
+### 数据库支持
+
+- **SQLite** - 轻量级部署,无需额外数据库服务
+- **MySQL 5.x / 8.x** - 支持主流MySQL版本
+- **TiDB** - 支持分布式数据库TiDB
+- 支持数据库SSL配置
+
+### 部署方式
+
+- **单应用打包** - 直接运行,无需部署前端页面
+- **Docker 部署** - 支持从环境变量启动服务
+- **Docker Compose** - 支持编排部署
+- **前后端分离** - 支持独立部署前后端
+
+### 界面特性
+
+- **现代化UI** - 使用shadcn-vue组件,基于TailwindCSS
+- **响应式设计** - 支持大中小屏自适应
+- **单应用的HTML浏览器自动缓存** - 提高访问速度
diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md
new file mode 100644
index 0000000..ec0a47a
--- /dev/null
+++ b/docs/guide/introduction.md
@@ -0,0 +1,39 @@
+# 介绍
+
+Message Nest(信息巢)是一个灵活而强大的消息推送整合平台,旨在简化并自定义多种消息通知方式。
+
+## 什么是 Message Nest?
+
+Message Nest 是一个拥有各种渠道信息方式的集合站点。如果你有很多消息推送方式,每次都需要调用各种接口去发送消息到各个渠道,或者不同的项目你都需要复制同样的发消息代码,这个项目可以帮你管理各种消息方式,并提供统一的发送API接入。
+
+你可以自由组合各种消息渠道,一个API推送到各种渠道,帮你省去接入的繁琐步骤。
+
+## 项目来由
+
+自己常常写一些脚本需要消息推送,经常需要接入不同的消息发送,很不方便,于是就有了这个项目。
+
+## 演示站点
+
+访问 [演示站点](https://message-nest-demo-site.qwapi.eu.org/)(演示站点的服务器比较烂,见谅)
+
+## 效果图
+
+
+
+## 进度
+
+项目还在不断更新中,欢迎大家提出各种建议。
+
+关于日志,考虑到目前多数服务以收集控制台输出为主,暂时不支持写出日志文件。
+
+## 致谢
+
+该项目汲取了 [go-gin-example](https://github.com/eddycjy/go-gin-example) 项目的灵感,展示了 Go 和 Gin 在实际应用中的强大和多才多艺。
+
+## 贡献
+
+欢迎通过提交问题和提出改进建议。
+
+## 许可证
+
+[MIT License](https://github.com/engigu/Message-Push-Nest/blob/main/LICENSE)
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..f5aa0f7
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,86 @@
+---
+layout: home
+
+hero:
+ name: "Message Nest"
+ text: "消息推送整合平台"
+ tagline: 灵活而强大的消息推送整合平台,简化并自定义多种消息通知方式
+ image:
+ src: /logo.svg
+ alt: Message Nest
+ actions:
+ - theme: brand
+ text: 快速开始
+ link: /guide/introduction
+ - theme: alt
+ text: 部署指南
+ link: /deployment/overview
+ - theme: alt
+ text: GitHub
+ link: https://github.com/engigu/Message-Push-Nest
+
+features:
+ - icon: 🔄
+ title: 整合性
+ details: 提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。
+ - icon: 🎨
+ title: 自定义性
+ details: 可以根据需求定制消息推送策略,满足不同场景的个性化需求。
+ - icon: 🛠
+ title: 开放性
+ details: 易于扩展和集成新的消息通知服务,以适应未来的变化。
+ - icon: 📧
+ title: 多渠道支持
+ details: 支持邮件、钉钉、企业微信、微信测试公众号、自定义Webhook等多种推送方式。
+ - icon: ⏰
+ title: 定时任务
+ details: 支持自定义的定时消息发送,满足定时推送需求。
+ - icon: 🎯
+ title: 自托管消息
+ details: 可以将站点作为消息的接收方,登录站点查看消息。
+ - icon: 🐳
+ title: Docker支持
+ details: 支持Docker和Docker Compose部署,快速启动服务。
+ - icon: 💾
+ title: 多数据库支持
+ details: 支持SQLite、MySQL 5.x/8.x、TiDB等多种数据库。
+ - icon: 📊
+ title: 数据统计
+ details: 支持数据统计展示,查看消息发送情况。
+---
+
+## 快速开始
+
+### Docker 部署(推荐)
+
+使用环境变量快速启动:
+
+```bash
+docker run -d \
+ -p 8000:8000 \
+ -e MYSQL_HOST=192.168.64.133 \
+ -e MYSQL_PORT=3308 \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=Aa123456 \
+ -e MYSQL_DB=test_11 \
+ -e MYSQL_TABLE_PREFIX=message_ \
+ --name message-nest \
+ engigu/message-nest:latest
+```
+
+### 直接运行
+
+1. 下载最新的 [Release](https://github.com/engigu/Message-Push-Nest/releases)
+2. 配置 `conf/app.ini`
+3. 启动服务,访问 `http://localhost:8000`
+4. 默认账号:`admin`,密码:`123456`
+
+## 项目来由
+
+自己常常写一些脚本需要消息推送,经常需要接入不同的消息发送,很不方便,于是就有了这个项目。
+
+如果你有很多消息推送方式,每次都需要调用各种接口去发送消息到各个渠道,或者不同的项目你都需要复制同样的发消息代码,这个项目可以帮你管理各种消息方式,并提供统一的发送API接入。你可以自由组合各种消息渠道,一个API推送到各种渠道,帮你省去接入的繁琐步骤。
+
+## Star History
+
+[](https://star-history.com/#engigu/Message-Push-Nest&Date)
diff --git a/docs/package-lock.json b/docs/package-lock.json
new file mode 100644
index 0000000..8f31c19
--- /dev/null
+++ b/docs/package-lock.json
@@ -0,0 +1,2468 @@
+{
+ "name": "message-nest-docs",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "message-nest-docs",
+ "version": "1.0.0",
+ "devDependencies": {
+ "vitepress": "^1.0.0"
+ }
+ },
+ "node_modules/@algolia/abtesting": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.10.0.tgz",
+ "integrity": "sha512-mQT3jwuTgX8QMoqbIR7mPlWkqQqBPQaPabQzm37xg2txMlaMogK/4hCiiESGdg39MlHZOVHeV+0VJuE7f5UK8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/autocomplete-core": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz",
+ "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/autocomplete-plugin-algolia-insights": "1.17.7",
+ "@algolia/autocomplete-shared": "1.17.7"
+ }
+ },
+ "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz",
+ "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/autocomplete-shared": "1.17.7"
+ },
+ "peerDependencies": {
+ "search-insights": ">= 1 < 3"
+ }
+ },
+ "node_modules/@algolia/autocomplete-preset-algolia": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz",
+ "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/autocomplete-shared": "1.17.7"
+ },
+ "peerDependencies": {
+ "@algolia/client-search": ">= 4.9.1 < 6",
+ "algoliasearch": ">= 4.9.1 < 6"
+ }
+ },
+ "node_modules/@algolia/autocomplete-shared": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz",
+ "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@algolia/client-search": ">= 4.9.1 < 6",
+ "algoliasearch": ">= 4.9.1 < 6"
+ }
+ },
+ "node_modules/@algolia/client-abtesting": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.44.0.tgz",
+ "integrity": "sha512-KY5CcrWhRTUo/lV7KcyjrZkPOOF9bjgWpMj9z98VA+sXzVpZtkuskBLCKsWYFp2sbwchZFTd3wJM48H0IGgF7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-analytics": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.44.0.tgz",
+ "integrity": "sha512-LKOCE8S4ewI9bN3ot9RZoYASPi8b78E918/DVPW3HHjCMUe6i+NjbNG6KotU4RpP6AhRWZjjswbOkWelUO+OoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-common": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.44.0.tgz",
+ "integrity": "sha512-1yyJm4OYC2cztbS28XYVWwLXdwpLsMG4LoZLOltVglQ2+hc/i9q9fUDZyjRa2Bqt4DmkIfezagfMrokhyH4uxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-insights": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.44.0.tgz",
+ "integrity": "sha512-wVQWK6jYYsbEOjIMI+e5voLGPUIbXrvDj392IckXaCPvQ6vCMTXakQqOYCd+znQdL76S+3wHDo77HZWiAYKrtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-personalization": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.44.0.tgz",
+ "integrity": "sha512-lkgRjOjOkqmIkebHjHpU9rLJcJNUDMm+eVSW/KJQYLjGqykEZxal+nYJJTBbLceEU2roByP/+27ZmgIwCdf0iA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-query-suggestions": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.44.0.tgz",
+ "integrity": "sha512-sYfhgwKu6NDVmZHL1WEKVLsOx/jUXCY4BHKLUOcYa8k4COCs6USGgz6IjFkUf+niwq8NCECMmTC4o/fVQOalsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-search": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.44.0.tgz",
+ "integrity": "sha512-/FRKUM1G4xn3vV8+9xH1WJ9XknU8rkBGlefruq9jDhYUAvYozKimhrmC2pRqw/RyHhPivmgZCRuC8jHP8piz4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/ingestion": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.44.0.tgz",
+ "integrity": "sha512-5+S5ynwMmpTpCLXGjTDpeIa81J+R4BLH0lAojOhmeGSeGEHQTqacl/4sbPyDTcidvnWhaqtyf8m42ue6lvISAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/monitoring": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.44.0.tgz",
+ "integrity": "sha512-xhaTN8pXJjR6zkrecg4Cc9YZaQK2LKm2R+LkbAq+AYGBCWJxtSGlNwftozZzkUyq4AXWoyoc0x2SyBtq5LRtqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/recommend": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.44.0.tgz",
+ "integrity": "sha512-GNcite/uOIS7wgRU1MT7SdNIupGSW+vbK9igIzMePvD2Dl8dy0O3urKPKIbTuZQqiVH1Cb84y5cgLvwNrdCj/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/requester-browser-xhr": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.44.0.tgz",
+ "integrity": "sha512-YZHBk72Cd7pcuNHzbhNzF/FbbYszlc7JhZlDyQAchnX5S7tcemSS96F39Sy8t4O4WQLpFvUf1MTNedlitWdOsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/requester-fetch": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.44.0.tgz",
+ "integrity": "sha512-B9WHl+wQ7uf46t9cq+vVM/ypVbOeuldVDq9OtKsX2ApL2g/htx6ImB9ugDOOJmB5+fE31/XPTuCcYz/j03+idA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/requester-node-http": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.44.0.tgz",
+ "integrity": "sha512-MULm0qeAIk4cdzZ/ehJnl1o7uB5NMokg83/3MKhPq0Pk7+I0uELGNbzIfAkvkKKEYcHALemKdArtySF9eKzh/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@docsearch/css": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz",
+ "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/js": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz",
+ "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/react": "3.8.2",
+ "preact": "^10.0.0"
+ }
+ },
+ "node_modules/@docsearch/react": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz",
+ "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/autocomplete-core": "1.17.7",
+ "@algolia/autocomplete-preset-algolia": "1.17.7",
+ "@docsearch/css": "3.8.2",
+ "algoliasearch": "^5.14.2"
+ },
+ "peerDependencies": {
+ "@types/react": ">= 16.8.0 < 19.0.0",
+ "react": ">= 16.8.0 < 19.0.0",
+ "react-dom": ">= 16.8.0 < 19.0.0",
+ "search-insights": ">= 1 < 3"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "search-insights": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@iconify-json/simple-icons": {
+ "version": "1.2.59",
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.59.tgz",
+ "integrity": "sha512-fYx/InyQsWFW4wVxWka3CGDJ6m/fXoTqWBSl+oA3FBXO5RhPAb6S3Y5bRgCPnrYevErH8VjAL0TZevIqlN2PhQ==",
+ "dev": true,
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@iconify/types": "*"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
+ "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz",
+ "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz",
+ "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz",
+ "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz",
+ "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz",
+ "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz",
+ "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz",
+ "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz",
+ "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz",
+ "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz",
+ "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz",
+ "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz",
+ "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz",
+ "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz",
+ "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz",
+ "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz",
+ "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz",
+ "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz",
+ "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz",
+ "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@shikijs/core": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz",
+ "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/engine-javascript": "2.5.0",
+ "@shikijs/engine-oniguruma": "2.5.0",
+ "@shikijs/types": "2.5.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4",
+ "hast-util-to-html": "^9.0.4"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz",
+ "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "2.5.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "oniguruma-to-es": "^3.1.0"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz",
+ "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "2.5.0",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz",
+ "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "2.5.0"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz",
+ "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "2.5.0"
+ }
+ },
+ "node_modules/@shikijs/transformers": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz",
+ "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "2.5.0",
+ "@shikijs/types": "2.5.0"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz",
+ "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz",
+ "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.24",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz",
+ "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz",
+ "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/compiler-core": "3.5.24",
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/compiler-ssr": "3.5.24",
+ "@vue/shared": "3.5.24",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz",
+ "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+ "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.7.9"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+ "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.7.9",
+ "birpc": "^2.3.0",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+ "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
+ "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
+ "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
+ "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.24",
+ "@vue/runtime-core": "3.5.24",
+ "@vue/shared": "3.5.24",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
+ "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.24",
+ "@vue/shared": "3.5.24"
+ },
+ "peerDependencies": {
+ "vue": "3.5.24"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz",
+ "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "12.8.2",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
+ "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "12.8.2",
+ "@vueuse/shared": "12.8.2",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/integrations": {
+ "version": "12.8.2",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz",
+ "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "12.8.2",
+ "@vueuse/shared": "12.8.2",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "async-validator": "^4",
+ "axios": "^1",
+ "change-case": "^5",
+ "drauu": "^0.4",
+ "focus-trap": "^7",
+ "fuse.js": "^7",
+ "idb-keyval": "^6",
+ "jwt-decode": "^4",
+ "nprogress": "^0.2",
+ "qrcode": "^1.5",
+ "sortablejs": "^1",
+ "universal-cookie": "^7"
+ },
+ "peerDependenciesMeta": {
+ "async-validator": {
+ "optional": true
+ },
+ "axios": {
+ "optional": true
+ },
+ "change-case": {
+ "optional": true
+ },
+ "drauu": {
+ "optional": true
+ },
+ "focus-trap": {
+ "optional": true
+ },
+ "fuse.js": {
+ "optional": true
+ },
+ "idb-keyval": {
+ "optional": true
+ },
+ "jwt-decode": {
+ "optional": true
+ },
+ "nprogress": {
+ "optional": true
+ },
+ "qrcode": {
+ "optional": true
+ },
+ "sortablejs": {
+ "optional": true
+ },
+ "universal-cookie": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "12.8.2",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
+ "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "12.8.2",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
+ "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/algoliasearch": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.44.0.tgz",
+ "integrity": "sha512-f8IpsbdQjzTjr/4mJ/jv5UplrtyMnnciGax6/B0OnLCs2/GJTK13O4Y7Ff1AvJVAaztanH+m5nzPoUq6EAy+aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/abtesting": "1.10.0",
+ "@algolia/client-abtesting": "5.44.0",
+ "@algolia/client-analytics": "5.44.0",
+ "@algolia/client-common": "5.44.0",
+ "@algolia/client-insights": "5.44.0",
+ "@algolia/client-personalization": "5.44.0",
+ "@algolia/client-query-suggestions": "5.44.0",
+ "@algolia/client-search": "5.44.0",
+ "@algolia/ingestion": "1.44.0",
+ "@algolia/monitoring": "1.44.0",
+ "@algolia/recommend": "5.44.0",
+ "@algolia/requester-browser-xhr": "5.44.0",
+ "@algolia/requester-fetch": "5.44.0",
+ "@algolia/requester-node-http": "5.44.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz",
+ "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/copy-anything": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+ "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/emoji-regex-xs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
+ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/focus-trap": {
+ "version": "7.6.6",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz",
+ "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.3.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+ "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mark.js": {
+ "version": "8.11.1",
+ "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
+ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/minisearch": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
+ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz",
+ "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex-xs": "^1.0.0",
+ "regex": "^6.0.1",
+ "regex-recursion": "^6.0.2"
+ }
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/preact": {
+ "version": "10.27.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
+ "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
+ "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-recursion": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
+ "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.2",
+ "@rollup/rollup-android-arm64": "4.53.2",
+ "@rollup/rollup-darwin-arm64": "4.53.2",
+ "@rollup/rollup-darwin-x64": "4.53.2",
+ "@rollup/rollup-freebsd-arm64": "4.53.2",
+ "@rollup/rollup-freebsd-x64": "4.53.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.2",
+ "@rollup/rollup-linux-arm64-musl": "4.53.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-musl": "4.53.2",
+ "@rollup/rollup-openharmony-arm64": "4.53.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.2",
+ "@rollup/rollup-win32-x64-gnu": "4.53.2",
+ "@rollup/rollup-win32-x64-msvc": "4.53.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/search-insights": {
+ "version": "2.17.3",
+ "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
+ "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/shiki": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz",
+ "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "2.5.0",
+ "@shikijs/engine-javascript": "2.5.0",
+ "@shikijs/engine-oniguruma": "2.5.0",
+ "@shikijs/langs": "2.5.0",
+ "@shikijs/themes": "2.5.0",
+ "@shikijs/types": "2.5.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/superjson": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
+ "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
+ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitepress": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
+ "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/css": "3.8.2",
+ "@docsearch/js": "3.8.2",
+ "@iconify-json/simple-icons": "^1.2.21",
+ "@shikijs/core": "^2.1.0",
+ "@shikijs/transformers": "^2.1.0",
+ "@shikijs/types": "^2.1.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/devtools-api": "^7.7.0",
+ "@vue/shared": "^3.5.13",
+ "@vueuse/core": "^12.4.0",
+ "@vueuse/integrations": "^12.4.0",
+ "focus-trap": "^7.6.4",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.1.1",
+ "shiki": "^2.1.0",
+ "vite": "^5.4.14",
+ "vue": "^3.5.13"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz",
+ "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/compiler-sfc": "3.5.24",
+ "@vue/runtime-dom": "3.5.24",
+ "@vue/server-renderer": "3.5.24",
+ "@vue/shared": "3.5.24"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 0000000..474f3a3
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "message-nest-docs",
+ "version": "1.0.0",
+ "description": "Message Nest Documentation",
+ "scripts": {
+ "docs:dev": "vitepress dev",
+ "docs:build": "vitepress build",
+ "docs:preview": "vitepress preview"
+ },
+ "devDependencies": {
+ "vitepress": "^1.0.0"
+ }
+}
diff --git a/docs/public/logo.svg b/docs/public/logo.svg
new file mode 100644
index 0000000..d40af89
--- /dev/null
+++ b/docs/public/logo.svg
@@ -0,0 +1,8 @@
+
From 4d955d183a3ac1b7d1bb809193e5a57f1e0e58c5 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 18:42:50 +0800
Subject: [PATCH 07/24] feat: add docs pages
---
docs/.vitepress/config.mts | 38 +++++++++------------
docs/{guide => deployment}/configuration.md | 0
docs/{guide => deployment}/embed-html.md | 0
docs/guide/features.md | 16 +--------
docs/index.md | 4 +--
docs/public/logo.svg | 9 +----
6 files changed, 20 insertions(+), 47 deletions(-)
rename docs/{guide => deployment}/configuration.md (100%)
rename docs/{guide => deployment}/embed-html.md (100%)
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 881d97f..ae91921 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -12,6 +12,11 @@ export default defineConfig({
// 如果使用自定义域名或部署到根路径,注释掉 base 或设置为 '/'
base: '/Message-Push-Nest/',
+ // 网站图标
+ head: [
+ ['link', { rel: 'icon', href: '/Message-Push-Nest/logo.svg' }]
+ ],
+
themeConfig: {
logo: '/logo.svg',
@@ -25,27 +30,9 @@ export default defineConfig({
sidebar: {
'/guide/': [
- {
- text: '开始',
- items: [
- { text: '介绍', link: '/guide/introduction' },
- { text: '特色功能', link: '/guide/features' },
- { text: '更新日志', link: '/guide/changelog' }
- ]
- },
- {
- text: '配置',
- items: [
- { text: '配置说明', link: '/guide/configuration' },
- { text: 'EmbedHtml说明', link: '/guide/embed-html' }
- ]
- },
- // {
- // text: '文档部署',
- // items: [
- // { text: '部署到 GitHub Pages', link: '/guide/deploy-to-github-pages' }
- // ]
- // }
+ { text: '介绍', link: '/guide/introduction' },
+ { text: '特色功能', link: '/guide/features' },
+ { text: '更新日志', link: '/guide/changelog' }
],
'/deployment/': [
{
@@ -57,6 +44,13 @@ export default defineConfig({
{ text: 'Docker部署', link: '/deployment/docker' },
{ text: 'Docker Compose', link: '/deployment/docker-compose' }
]
+ },
+ {
+ text: '配置',
+ items: [
+ { text: '配置说明', link: '/deployment/configuration' },
+ { text: 'EmbedHtml说明', link: '/deployment/embed-html' }
+ ]
}
],
'/api/': [
@@ -76,7 +70,7 @@ export default defineConfig({
footer: {
message: 'Released under the MIT License.',
- copyright: 'Copyright © 2024-present Message Nest'
+ copyright: 'Copyright © 2024-present Message-Push-Nest'
},
search: {
diff --git a/docs/guide/configuration.md b/docs/deployment/configuration.md
similarity index 100%
rename from docs/guide/configuration.md
rename to docs/deployment/configuration.md
diff --git a/docs/guide/embed-html.md b/docs/deployment/embed-html.md
similarity index 100%
rename from docs/guide/embed-html.md
rename to docs/deployment/embed-html.md
diff --git a/docs/guide/features.md b/docs/guide/features.md
index 046629d..cd57a7e 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -1,18 +1,4 @@
-# 特色功能
-
-## 核心特性
-
-### 🔄 整合性
-
-提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。
-
-### 🎨 自定义性
-
-可以根据需求定制消息推送策略,满足不同场景的个性化需求。
-
-### 🛠 开放性
-
-易于扩展和集成新的消息通知服务,以适应未来的变化。
+。
## 支持的推送方式
diff --git a/docs/index.md b/docs/index.md
index f5aa0f7..6018457 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -49,7 +49,7 @@ features:
details: 支持数据统计展示,查看消息发送情况。
---
-## 快速开始
+
diff --git a/docs/public/logo.svg b/docs/public/logo.svg
index d40af89..4b0610d 100644
--- a/docs/public/logo.svg
+++ b/docs/public/logo.svg
@@ -1,8 +1 @@
-
+
\ No newline at end of file
From 7f89a7d117e1e74cd84d57e9c5834e1a01f90949 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 21:17:17 +0800
Subject: [PATCH 08/24] feat: add docs pages
---
docs/deployment/direct-run.md | 2 +-
docs/guide/features.md | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/docs/deployment/direct-run.md b/docs/deployment/direct-run.md
index 7f745ea..482e364 100644
--- a/docs/deployment/direct-run.md
+++ b/docs/deployment/direct-run.md
@@ -18,7 +18,7 @@
### 3. 配置文件
-重命名 `conf/app.example.ini` 为 `conf/app.ini`,然后修改配置:
+新建conf文件夹,或者重命名项目中 `conf/app.example.ini` 为 `conf/app.ini`,然后修改配置:
```ini
[app]
diff --git a/docs/guide/features.md b/docs/guide/features.md
index cd57a7e..5d13d4b 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -1,4 +1,3 @@
-。
## 支持的推送方式
From 1e78bdec6535099ae9f65976656571ca6dde5463 Mon Sep 17 00:00:00 2001
From: engigu
Date: Tue, 18 Nov 2025 21:25:29 +0800
Subject: [PATCH 09/24] add docs pages
---
.github/workflows/docs_pages.yml | 66 ++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
create mode 100644 .github/workflows/docs_pages.yml
diff --git a/.github/workflows/docs_pages.yml b/.github/workflows/docs_pages.yml
new file mode 100644
index 0000000..528c79d
--- /dev/null
+++ b/.github/workflows/docs_pages.yml
@@ -0,0 +1,66 @@
+name: Deploy Docs to GitHub Pages
+
+on:
+ # 在推送到 main 分支时触发
+ push:
+ branches:
+ - main
+ # 允许手动触发
+ workflow_dispatch:
+
+# 设置 GITHUB_TOKEN 的权限,允许部署到 GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# 只允许一个并发部署
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ # 构建任务
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ cache-dependency-path: docs/package-lock.json
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Install dependencies
+ run: npm ci
+ working-directory: docs
+
+ - name: Build with VitePress
+ run: npm run docs:build
+ working-directory: docs
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/.vitepress/dist
+
+ # 部署任务
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ needs: build
+ runs-on: ubuntu-latest
+ name: Deploy
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
From 30cf41da6a68757d514de6b13a8b126bdb420f76 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 21:28:12 +0800
Subject: [PATCH 10/24] fix: remove dead link in SETUP.md
---
docs/SETUP.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/SETUP.md b/docs/SETUP.md
index 97273cb..17c0dfe 100644
--- a/docs/SETUP.md
+++ b/docs/SETUP.md
@@ -46,7 +46,7 @@ cd docs
npm run docs:dev
```
-访问:http://localhost:5173
+访问:`http://localhost:5173`
### 构建生产版本
From fbd9b486517f524b05cf5fb2ff52f6beaa911d97 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 21:28:39 +0800
Subject: [PATCH 11/24] feat: add docs pages
---
.github/workflows/deploy_docs.yml | 66 -------------------------------
1 file changed, 66 deletions(-)
delete mode 100644 .github/workflows/deploy_docs.yml
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
deleted file mode 100644
index 528c79d..0000000
--- a/.github/workflows/deploy_docs.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-name: Deploy Docs to GitHub Pages
-
-on:
- # 在推送到 main 分支时触发
- push:
- branches:
- - main
- # 允许手动触发
- workflow_dispatch:
-
-# 设置 GITHUB_TOKEN 的权限,允许部署到 GitHub Pages
-permissions:
- contents: read
- pages: write
- id-token: write
-
-# 只允许一个并发部署
-concurrency:
- group: pages
- cancel-in-progress: false
-
-jobs:
- # 构建任务
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: npm
- cache-dependency-path: docs/package-lock.json
-
- - name: Setup Pages
- uses: actions/configure-pages@v4
-
- - name: Install dependencies
- run: npm ci
- working-directory: docs
-
- - name: Build with VitePress
- run: npm run docs:build
- working-directory: docs
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: docs/.vitepress/dist
-
- # 部署任务
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- needs: build
- runs-on: ubuntu-latest
- name: Deploy
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
From 13d2a0ecabb9ae46cf43f3bf9973ebd2a6c49ad8 Mon Sep 17 00:00:00 2001
From: engigu
Date: Tue, 18 Nov 2025 21:45:43 +0800
Subject: [PATCH 12/24] feat: update docs page
---
.../{docs_pages.yml => deploy_docs_pages.yml} | 0
README.md | 643 +-----------------
2 files changed, 5 insertions(+), 638 deletions(-)
rename .github/workflows/{docs_pages.yml => deploy_docs_pages.yml} (100%)
diff --git a/.github/workflows/docs_pages.yml b/.github/workflows/deploy_docs_pages.yml
similarity index 100%
rename from .github/workflows/docs_pages.yml
rename to .github/workflows/deploy_docs_pages.yml
diff --git a/README.md b/README.md
index 8b446cb..690243f 100644
--- a/README.md
+++ b/README.md
@@ -20,53 +20,10 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
项目还在不断更新中,欢迎大家提出各种建议。
-关于日志,考虑到目前多数服务以收集控制台输出为主,暂时不支持写出日志文件。
+关于运行日志,考虑到目前多数服务以收集控制台输出为主,暂时不支持写出日志文件。
-## 功能更新日志
-- **2025.10.12**
- - 增加cookies过期天数设置
-- **2025.09.30**
- - 支持页面的明暗主题切换设置,增加登录日志
-- **2025.09.14**
- - 支持系统信息展示更多的运行信息
-- **2025.08.10**
- - **重大升级,重构web页面,样式更加美观友好**
- - UI组件使用shadcn-vue,基于tailwindcss,现在支持大中小屏自适应
-- **2025.04.28**
- - 支持tidb作为存储,支持数据库ssl配置
- - docker运行指定环境变量SSL=true
- - ini配置文件运行指定数据库连接参数Ssl=true
-- **2025.01.01**
- - 支持自托管消息,现在可以将站点作为消息的接受,登录站点查看消息
-- **2024.04.29**
- - 支持 SQLite 部署
- - 支持MySQL 5.x 8.x
-- **2024.04.11**
- - 支持自定义的定时消息发送
-- **2024.03.05**
- - 支持微信测试公众号模板消息发送
-- **2024.01.28**
- - 支持 Docker 部署,从环境变量启动服务
-- **2024.01.24**
- - 支持数据统计展示
-- **2024.01.20**
- - 添加日志查看自动刷新
-- **2024.01.07**
- - 支持站点信息自定义
-- **2024.01.03**
- - 支持企业微信
- - 单应用打包,直接运行,无需部署前端页面
- - 支持邮件发送
- - 用户密码设置
- - 支持用户定时任务清理,更新定时时间
- - 查看定时清理日志
- - 单应用的 HTML 浏览器自动缓存
- - Gin 的日志使用 Logrus
- - 支持异步发送
- - 支持邮件发送
- - 支持钉钉
- - 支持自定义的 Webhook 消息发送
- - 企业微信支持
+## 更新日志 ☕
+[点我转跳](https://engigu.github.io/Message-Push-Nest/guide/changelog.html)
## 项目来由 💡
@@ -77,599 +34,9 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化

-## 使用方法 🚀
-
-
- 1. 直接运行最新的release打包的可执行文件(推荐,不用部署前端页面🍀🍀)
-
-1. 下载最新的系统版本对应的release, 解压
-2. 新建一个数据库
-3. 重命名conf/app.example.ini为conf/app.ini
-4. 修改app.ini对应的配置
-5. 将配置中`EmbedHtml = disable`, 进行注释,以单应用方式运行,完整配置参考如下:
-
-```ini
-[app]
-JwtSecret = message-nest
-LogLevel = INFO
-
-[server]
-RunMode = release
-HttpPort = 8000
-ReadTimeout = 60
-WriteTimeout = 60
-; 注释EmbedHtml,启用单应用模式
-; EmbedHtml = disable
-
-[database]
-; 关闭SQL打印
-; SqlDebug = enable
-
-; Type = sqlite
-Type = mysql
-User = root
-Password = Aa123456
-Host = vm.server
-Port = 3308
-Name = yourDbName
-TablePrefix = message_
-
-```
-
-6. 启动项目会自动创建表和账号
-
-```shell
-# INFO日志级别启动回出现如下日志
-
-[2024-01-13 13:40:09.075] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_auth
-[2024-01-13 13:40:11.778] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks
-[2024-01-13 13:40:16.518] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_ways
-[2024-01-13 13:40:23.300] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks_logs
-[2024-01-13 13:40:28.715] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_send_tasks_ins
-[2024-01-13 13:40:39.538] INFO [migrate.go:70 Setup] [Init Data]: Migrate table: message_settings
-[2024-01-13 13:40:46.299] INFO [migrate.go:74 Setup] [Init Data]: Init Account data...
-[2024-01-13 13:40:46.751] INFO [migrate.go:77 Setup] [Init Data]: All table data init done.
-
-```
-
-7. 启动项目,访问8000端口,初始账号为admin,密码为123456
-
-
-
-
-
- 2. 前后端分离部署(待更新)
-
-
-
-[//]: # (1. 前端项目构建)
-
-[//]: # (```shell)
-
-[//]: # (cd web && npm i && npm run build)
-
-[//]: # (```)
-
-[//]: # (2. 配置配置文件,参考上面,需要注意将配置中`EmbedHtml = disable`取消注释)
-
-[//]: # (3. 启动go服务)
-
-[//]: # (```shell)
-
-[//]: # (go mod tidy)
-
-[//]: # (CGO_ENABLED=0 go build -o Message-Nest)
-
-[//]: # (./Message-Nest)
-
-[//]: # (```)
-
-[//]: # (4. 配置Nginx,将静态文件)
-
-[//]: # (5. 配置Nginx,将后端接口转发)
-
-
- 3. 开发调试运行
-
-1. 重命名conf/app.example.ini为conf/app.ini, 关键配置如下
-
-```ini
-[app]
-JwtSecret = message-nest
-LogLevel = INFO
-
-[server]
-; RunMode务必设置成debug,会自动添加跨域
-RunMode = debug
-HttpPort = 8000
-ReadTimeout = 60
-WriteTimeout = 60
-; 取消EmbedHtml的注释(启用前后端分离),然后到web目录下面,npm run dev启动前端页面
-EmbedHtml = disable
-
-[database]
-; 开启SQL打印
-SqlDebug = enable
-
-; Type = sqlite
-Type = mysql
-User = root
-Password = Aa123456
-Host = vm.server
-Port = 3308
-Name = yourDbName
-TablePrefix = message_
-
-```
-
-2. 运行main.go,服务启动后会运行在8000端口
-
-```shell
-go mod tidy
-go run main.go
-```
-
-3. 启动前端页面,页面启动后会提示访问url,一般是`http://127.0.0.1:5173`
-
-```shell
-cd web
-npm i
-npm run dev
-```
-
-4. 访问`http://127.0.0.1:5173`,进行调试开发,接口会自动转发到go服务`http://localhost:8000`
-
-
-
-
- 4. docker/docker-compose部署(推荐指数🍀🍀🍀🍀🍀)
-
-
- docker挂载app.ini部署
-
-1. 新建一个数据库,准备一个conf文件夹
-2. 新增conf/app.ini
-3. 修改app.ini对应的配置
-4. 将配置中`EmbedHtml = disable`, 进行注释,以单应用方式运行,完整配置参考如下:
-
-```ini
-[app]
-JwtSecret = message-nest
-LogLevel = INFO
-
-[server]
-RunMode = release
-; docker模式下端口配置文件中只能为8000
-HttpPort = 8000
-ReadTimeout = 60
-WriteTimeout = 60
-; 注释EmbedHtml,启用单应用模式
-; EmbedHtml = disable
-
-[database]
-; 关闭SQL打印
-; SqlDebug = enable
-
-; Type = sqlite
-Type = mysql
-User = root
-Password = Aa123456
-Host = vm.server
-Port = 3308
-Name = yourDbName
-TablePrefix = message_
-
-```
-
-6. 使用命令启动,端口使用`-p`自定义
-
-```shell
-# 从 Docker Hub 拉取
-docker pull engigu/message-nest:latest
-
-# 或从 GitHub Container Registry (GHCR) 拉取
-docker pull ghcr.io/engigu/message-nest:latest
-
-# 测试运行(Docker Hub 镜像)
-docker run --rm -ti \
- -p 8000:8000 \
- -v /your/path/conf:/app/conf \
- engigu/message-nest:latest
-
-# 测试运行(GHCR 镜像)
-docker run --rm -ti \
- -p 8000:8000 \
- -v /your/path/conf:/app/conf \
- ghcr.io/engigu/message-nest:latest
-
-# 正式运行(Docker Hub 镜像)
-docker run -d \
- -p 8000:8000 \
- -v /your/path/conf:/app/conf \
- engigu/message-nest:latest
-
-# 正式运行(GHCR 镜像)
-docker run -d \
- -p 8000:8000 \
- -v /your/path/conf:/app/conf \
- ghcr.io/engigu/message-nest:latest
-```
-
-7. 启动项目,访问8000端口,初始账号为admin,密码为123456
-
-
-
-
- docker-compose挂载app.ini部署
-
-1. 准备app.ini,文件内容如上docker部署
-2. 准备docker-compose.yml,内容如下:
-
-```yml
-version: "3.7"
-services:
-
- message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
- container_name: message-nest
- restart: always
- volumes:
- - ./conf:/app/conf
- ports:
- - "8000:8000"
-```
-
-3. 文件目录结构
-
-```shell
-.
-├── conf
-│ └── app.ini
-├── docker-compose.yml
-
-```
-
-4. 启动项目
-
-```shell
-# 测试运行
-docker-compose up
-# 正式运行
-docker-compose up -d
-```
-
-
-
-
- docker/docker-compose环境变量部署(推荐指数🍀🍀🍀🍀🍀🍀🍀🍀🍀)
-
-环境变量介绍
-
-| 变量 | 说明 |
-|--------------------|----------------------------------------------------|
-| JWT_SECRET | jwt秘钥,可选,默认为message-nest |
-| LOG_LEVEL | 日志等级,可选,默认为INFO,DEBUG/INFO/ERROR |
-| RUN_MODE | 运行模式,可选,默认release,为debug将自动添加跨域 |
-| | |
-| DB_TYPE | 数据库类型,sqlite/mysql。默认为sqlite,存储路径为conf/database.db |
-| | |
-| MYSQL_HOST | mysql-host,DB_TYPE=mysql必填 |
-| MYSQL_PORT | mysql端口,DB_TYPE=mysql必填 |
-| MYSQL_USER | mysql用户名,DB_TYPE=mysql必填 |
-| MYSQL_PASSWORD | mysql数据库密码,DB_TYPE=mysql必填 |
-| MYSQL_DB | mysql数据库名字,DB_TYPE=mysql必填 |
-| MYSQL_TABLE_PREFIX | mysql数据表前缀,DB_TYPE=mysql必填 |
-| SSL | 是否开启SSL |
-| | |
-| SQL_DEBUG | 是否打印SQL,可选,默认关,设置enable为开启 |
-
-docker运行
-
-```shell
-
-# 正式运行(mysql)
-docker run -d \
- -p 8000:8000 \
- -e MYSQL_HOST=192.168.64.133 \
- -e MYSQL_PORT=3308 \
- -e MYSQL_USER=root \
- -e MYSQL_PASSWORD=Aa123456 \
- -e MYSQL_DB=test_11 \
- -e MYSQL_TABLE_PREFIX=message_ \
- --name message-nest \
- engigu/message-nest:latest
-
-# 或使用 GHCR 镜像(mysql)
-docker run -d \
- -p 8000:8000 \
- -e MYSQL_HOST=192.168.64.133 \
- -e MYSQL_PORT=3308 \
- -e MYSQL_USER=root \
- -e MYSQL_PASSWORD=Aa123456 \
- -e MYSQL_DB=test_11 \
- -e MYSQL_TABLE_PREFIX=message_ \
- --name message-nest \
- ghcr.io/engigu/message-nest:latest
-
-# 正式运行(sqlite)
-docker run -d \
- -p 8000:8000 \
- -v you/path/database.db=conf/database.db \
- --name message-nest \
- engigu/message-nest:latest
-
-# 或使用 GHCR 镜像(sqlite)
-docker run -d \
- -p 8000:8000 \
- -v you/path/database.db=conf/database.db \
- --name message-nest \
- ghcr.io/engigu/message-nest:latest
-```
-
-docker-compose运行(mysql)
-
-```yml
-version: "3.7"
-services:
-
- message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
- container_name: message-nest
- restart: always
- ports:
- - "8000:8000"
- environment:
- - MYSQL_HOST=192.168.64.133
- - MYSQL_PORT=3308
- - MYSQL_USER=root
- - MYSQL_PASSWORD=Aa123456
- - MYSQL_DB=test_11
- - MYSQL_TABLE_PREFIX=message_
-```
-
-docker-compose运行(sqlite)
-
-```yml
-version: "3.7"
-services:
-
- message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
- container_name: message-nest
- restart: always
- ports:
- - "8000:8000"
- volumes:
- - you/path/database.db:conf/database.db
-```
-
-```shell
-# 正式运行
-docker-compose -up -d
-```
-
-
-
-
-
-#### 关于EmbedHtml配置的说明
-
-> 这个配置可以理解为单应用模式(或者前后端分离)的开关
-> 1. 打开这个配置,表示前后端分离,表示go服务启动的时候只会有api服务,需要到web目录下,npm run
- dev启动前端项目。然后访问前端项目提示的端口服务,一般是127.0.0.1:5173。
- 或者使用npm run build,用Nginx部署前端。
->
-> 2. 注释这个配置,表示单应用,启动go服务,会把web/dist目录下文件作为前端静态资源。
- 如果目录下没有静态资源文件,需要到web目录下,npm run build构建生成。
->
-> 两种方式各有优缺点,综合考虑下来,推荐直接使用release的打包执行文件(或者docker环境变量进行部署),其中已经内置了页面静态资源,只用运行一个服务。
-
-## 完整配置说明 ⚙️
-
-
- 点击展开
-
-```ini
-[app]
-JwtSecret = message-nest
-; 暂时无用
-RuntimeRootPath = runtime/
-LogLevel = INFO
-
-[server]
-; debug or release
-; debug模式下会自动添加跨域headers
-RunMode = release
-HttpPort = 8000
-ReadTimeout = 60
-WriteTimeout = 60
-; use embed html static file
-; 是否使用embed打包的静态资源
-; 如果运行release打包后的应用,请注释这个设置。
-; 如果取消这个注释,只会单独运行api服务,前端页面需要到web目录手动npm run dev, 运行前端服务
-; EmbedHtml = disable
-
-[database]
-; 配置使用什么数据库,支持:mysql、sqlite、tidb
-Type = mysql
-User = root
-Password = password
-Host = 123.1.1.1
-Name = db_name
-Port = 3306
-; -- 其他配置
-; 表前缀
-TablePrefix = message_
-; -- 是否打开sql打印
-; SqlDebug = enable
-; 数据库连接是否开启ssl, value: [false | true]
-Ssl = true
-
-```
-
-
-
-## 客户端调用示例 💻️
-
-
- CURL
-
-```shell
-curl -X POST --location 'http://127.0.0.1:5173/api/v1/message/send' \
- --header 'Content-Type: application/json' \
- --data '{
- "task_id": "T-JiXpO8EO7u",
- "title": "message title",
- "text": "Hello World!"
-}'
-```
-
-
-
- Python
-
-```python
-import requests
-
-headers = {
- 'Content-Type': 'application/json',
-}
-json_data = {
- "task_id": "T-JiXpO8EO7u",
- "title": "message title",
- "text": "Hello World!"
-}
-response = requests.post('http://127.0.0.1:5173/api/v1/message/send', headers=headers, json=json_data)
-
-print("response:", response.json())
-
-```
-
-
-
- Go
-
-```golang
-package main
-
-import (
- "fmt"
- "io"
- "log"
- "net/http"
- "strings"
-)
-
-func main() {
- client := &http.Client{}
- var data = strings.NewReader(`{
- "task_id": "T-JiXpO8EO7u",
- "title": "message title",
- "text": "Hello World!"
-}`)
- req, err := http.NewRequest("POST", "http://127.0.0.1:5173/api/v1/message/send", data)
- if err != nil {
- log.Fatal(err)
- }
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
- bodyText, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("%s\n", bodyText)
-}
-
-```
-
-
-
- Java
-
-```java
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-HttpClient client = HttpClient.newBuilder()
- .followRedirects(HttpClient.Redirect.NORMAL)
- .build();
-
-HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create("http://127.0.0.1:5173/api/v1/message/send"))
- .POST(BodyPublishers.ofString("{\n \"task_id\": \"T-JiXpO8EO7u\",\n \"title\": \"message title\",\n \"text\": \"Hello World!\"\n}"))
- .setHeader("Content-Type", "application/json")
- .build();
-
-HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
-```
-
-
-
- Node
-
-```JavaScript
-var request = require('request');
-
-var headers = {
- 'Content-Type': 'application/json'
-};
-
-var dataString = "{\n \"task_id\": \"T-JiXpO8EO7u\",\n \"title\": \"message title\",\n \"text\": \"Hello World!\"\n}";
-
-var options = {
- url: 'http://127.0.0.1:5173/api/v1/message/send',
- method: 'POST',
- headers: headers,
- body: dataString
-};
-
-function callback(error, response, body) {
- if (!error && response.statusCode == 200) {
- console.log(body);
- }
-}
-
-request(options, callback);
-```
-
-
-
- PHP
-
-```php
-
+## 文档 📝
+[点我跳转](https://engigu.github.io/Message-Push-Nest/)
## 贡献 🤝
From a7b4b536395677d26709122a6a3a9b977c107b25 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 18 Nov 2025 22:47:46 +0800
Subject: [PATCH 13/24] chore: update docker run docs
---
docs/deployment/docker-compose.md | 22 ++++-----
docs/deployment/docker.md | 74 +++++++++++++++----------------
2 files changed, 49 insertions(+), 47 deletions(-)
diff --git a/docs/deployment/docker-compose.md b/docs/deployment/docker-compose.md
index 1086bcc..7986dbd 100644
--- a/docs/deployment/docker-compose.md
+++ b/docs/deployment/docker-compose.md
@@ -19,9 +19,9 @@ version: "3.7"
services:
message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
+ image: ghcr.io/engigu/message-nest:latest
+ # 或使用 Docker Hub 镜像
+ # image: engigu/message-nest:latest
container_name: message-nest
restart: always
volumes:
@@ -60,9 +60,9 @@ version: "3.7"
services:
message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
+ image: ghcr.io/engigu/message-nest:latest
+ # 或使用 Docker Hub 镜像
+ # image: engigu/message-nest:latest
container_name: message-nest
restart: always
ports:
@@ -85,9 +85,9 @@ version: "3.7"
services:
message-nest:
- image: engigu/message-nest:latest
- # 或使用 GHCR 镜像
- # image: ghcr.io/engigu/message-nest:latest
+ image: ghcr.io/engigu/message-nest:latest
+ # 或使用 Docker Hub 镜像
+ # image: engigu/message-nest:latest
container_name: message-nest
restart: always
ports:
@@ -124,7 +124,9 @@ services:
- "3306:3306"
message-nest:
- image: engigu/message-nest:latest
+ image: ghcr.io/engigu/message-nest:latest
+ # 或使用 Docker Hub 镜像
+ # image: engigu/message-nest:latest
container_name: message-nest
restart: always
depends_on:
diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md
index 490eb79..c91b42c 100644
--- a/docs/deployment/docker.md
+++ b/docs/deployment/docker.md
@@ -10,8 +10,8 @@
Message Nest 提供两个镜像源:
-- **Docker Hub**: `engigu/message-nest:latest`
-- **GitHub Container Registry**: `ghcr.io/engigu/message-nest:latest`
+- **GitHub Container Registry (推荐)**: `ghcr.io/engigu/message-nest:latest`
+- **Docker Hub (备选)**: `engigu/message-nest:latest`
## 方式一:挂载配置文件
@@ -54,39 +54,39 @@ Docker模式下,配置文件中的端口只能为8000,通过 `-p` 参数映
### 2. 拉取镜像
```bash
-# 从 Docker Hub 拉取
-docker pull engigu/message-nest:latest
-
-# 或从 GitHub Container Registry (GHCR) 拉取
+# 从 GitHub Container Registry (GHCR) 拉取(推荐)
docker pull ghcr.io/engigu/message-nest:latest
+
+# 或从 Docker Hub 拉取
+docker pull engigu/message-nest:latest
```
### 3. 启动容器
```bash
+# 测试运行(GHCR 镜像,推荐)
+docker run --rm -ti \
+ -p 8000:8000 \
+ -v /your/path/conf:/app/conf \
+ ghcr.io/engigu/message-nest:latest
+
# 测试运行(Docker Hub 镜像)
docker run --rm -ti \
-p 8000:8000 \
-v /your/path/conf:/app/conf \
engigu/message-nest:latest
-
-# 测试运行(GHCR 镜像)
-docker run --rm -ti \
+
+# 正式运行(GHCR 镜像,推荐)
+docker run -d \
-p 8000:8000 \
-v /your/path/conf:/app/conf \
ghcr.io/engigu/message-nest:latest
-
+
# 正式运行(Docker Hub 镜像)
docker run -d \
-p 8000:8000 \
-v /your/path/conf:/app/conf \
engigu/message-nest:latest
-
-# 正式运行(GHCR 镜像)
-docker run -d \
- -p 8000:8000 \
- -v /your/path/conf:/app/conf \
- ghcr.io/engigu/message-nest:latest
```
## 方式二:环境变量(推荐)
@@ -115,19 +115,7 @@ docker run -d \
### 使用 MySQL
```bash
-# 正式运行(mysql)
-docker run -d \
- -p 8000:8000 \
- -e MYSQL_HOST=192.168.64.133 \
- -e MYSQL_PORT=3308 \
- -e MYSQL_USER=root \
- -e MYSQL_PASSWORD=Aa123456 \
- -e MYSQL_DB=test_11 \
- -e MYSQL_TABLE_PREFIX=message_ \
- --name message-nest \
- engigu/message-nest:latest
-
-# 或使用 GHCR 镜像(mysql)
+# 正式运行(GHCR 镜像,推荐)
docker run -d \
-p 8000:8000 \
-e MYSQL_HOST=192.168.64.133 \
@@ -138,24 +126,36 @@ docker run -d \
-e MYSQL_TABLE_PREFIX=message_ \
--name message-nest \
ghcr.io/engigu/message-nest:latest
+
+# 或使用 Docker Hub 镜像
+docker run -d \
+ -p 8000:8000 \
+ -e MYSQL_HOST=192.168.64.133 \
+ -e MYSQL_PORT=3308 \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=Aa123456 \
+ -e MYSQL_DB=test_11 \
+ -e MYSQL_TABLE_PREFIX=message_ \
+ --name message-nest \
+ engigu/message-nest:latest
```
### 使用 SQLite
```bash
-# 正式运行(sqlite)
-docker run -d \
- -p 8000:8000 \
- -v /your/path/database.db:/app/conf/database.db \
- --name message-nest \
- engigu/message-nest:latest
-
-# 或使用 GHCR 镜像(sqlite)
+# 正式运行(GHCR 镜像,推荐)
docker run -d \
-p 8000:8000 \
-v /your/path/database.db:/app/conf/database.db \
--name message-nest \
ghcr.io/engigu/message-nest:latest
+
+# 或使用 Docker Hub 镜像
+docker run -d \
+ -p 8000:8000 \
+ -v /your/path/database.db:/app/conf/database.db \
+ --name message-nest \
+ engigu/message-nest:latest
```
## 访问服务
From c5984bc800d0eddb797632c62706cfd7a8e69989 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 13:07:16 +0800
Subject: [PATCH 14/24] chore: add tempalte docs
---
README.md | 1 +
docs/.vitepress/config.mts | 7 +-
docs/api/usage.md | 106 ------
docs/api/v1.md | 734 +++++++++++++++++++++++++++++++++++++
docs/api/v2.md | 519 ++++++++++++++++++++++++++
docs/guide/changelog.md | 17 +
docs/guide/template.md | 280 ++++++++++++++
docs/package-lock.json | 2 +-
docs/package.json | 2 +-
9 files changed, 1557 insertions(+), 111 deletions(-)
delete mode 100644 docs/api/usage.md
create mode 100644 docs/api/v1.md
create mode 100644 docs/api/v2.md
create mode 100644 docs/guide/template.md
diff --git a/README.md b/README.md
index 690243f..dcc20ab 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
- 🔄 **整合性:** 提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。
- 🎨 **自定义性:** 可以根据需求定制消息推送策略,满足不同场景的个性化需求。
+- 📝 **模板化(⭐推荐):** 支持消息模板功能,通过占位符实现动态内容替换,一次定义多处复用,大幅提高开发效率和维护便利性。
- 🛠 **开放性:** 易于扩展和集成新的消息通知服务,以适应未来的变化。
## 进度 🔨
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index ae91921..412d495 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -24,7 +24,7 @@ export default defineConfig({
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide/introduction' },
{ text: '部署', link: '/deployment/overview' },
- { text: 'API', link: '/api/usage' },
+ { text: 'API', link: '/api/v1' },
{ text: '演示站点', link: 'https://message-nest-demo-site.qwapi.eu.org/' }
],
@@ -32,6 +32,7 @@ export default defineConfig({
'/guide/': [
{ text: '介绍', link: '/guide/introduction' },
{ text: '特色功能', link: '/guide/features' },
+ { text: '消息模板', link: '/guide/template' },
{ text: '更新日志', link: '/guide/changelog' }
],
'/deployment/': [
@@ -57,8 +58,8 @@ export default defineConfig({
{
text: 'API文档',
items: [
- { text: '使用说明', link: '/api/usage' },
- { text: '调用示例', link: '/api/examples' }
+ { text: 'V1 API(任务)', link: '/api/v1' },
+ { text: 'V2 API(模板)', link: '/api/v2' }
]
}
]
diff --git a/docs/api/usage.md b/docs/api/usage.md
deleted file mode 100644
index f46e66e..0000000
--- a/docs/api/usage.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# API 使用说明
-
-Message Nest 提供统一的消息推送API接口。
-
-## 接口地址
-
-```
-POST /api/v1/message/send
-```
-
-## 请求参数
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| token | string | 是 | 推送令牌,在管理后台查看 |
-| title | string | 是 | 消息标题 |
-| text | string | 是 | 消息内容 |
-
-## 请求示例
-
-```json
-{
- "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
- "title": "message title",
- "text": "Hello World!"
-}
-```
-
-## 响应格式
-
-### 成功响应
-
-```json
-{
- "code": 200,
- "msg": "success",
- "data": {
- "status": "sent"
- }
-}
-```
-
-### 失败响应
-
-```json
-{
- "code": 400,
- "msg": "error message",
- "data": null
-}
-```
-
-## 获取 Token
-
-1. 登录 Message Nest 管理后台
-2. 进入"发送任务"页面
-3. 创建新的发送任务
-4. 配置推送渠道(邮件、钉钉、企业微信等)
-5. 保存后获得推送令牌(Token)
-
-## 支持的推送渠道
-
-- **邮件** - SMTP邮件发送
-- **钉钉** - 钉钉机器人
-- **企业微信** - 企业微信应用消息
-- **微信公众号** - 微信测试公众号模板消息
-- **自定义Webhook** - 自定义HTTP请求
-- **自托管消息** - 站内消息
-
-## 使用流程
-
-1. **创建推送渠道**
- - 在管理后台配置各种推送渠道
- - 填写相应的配置信息(如邮箱、Webhook地址等)
-
-2. **创建发送任务**
- - 选择要使用的推送渠道
- - 可以选择多个渠道同时推送
- - 获得唯一的推送令牌(Token)
-
-3. **调用API发送消息**
- - 使用获得的 Token
- - 发送标题和内容
- - 消息会自动推送到配置的所有渠道
-
-## 注意事项
-
-::: warning 重要
-- Token 是唯一的,请妥善保管
-- 消息内容支持纯文本和Markdown格式(取决于推送渠道)
-- 建议使用异步方式调用API,避免阻塞主流程
-:::
-
-## 错误码说明
-
-| 错误码 | 说明 |
-|--------|------|
-| 200 | 成功 |
-| 400 | 请求参数错误 |
-| 401 | 未授权 |
-| 404 | Token不存在 |
-| 500 | 服务器内部错误 |
-
-## 下一步
-
-查看各语言的 [调用示例](/api/examples)。
diff --git a/docs/api/v1.md b/docs/api/v1.md
new file mode 100644
index 0000000..438fa71
--- /dev/null
+++ b/docs/api/v1.md
@@ -0,0 +1,734 @@
+# V1 API 文档
+
+V1 API 提供基于任务的消息推送接口,支持多渠道、多格式发送。
+
+::: tip 💡 推荐使用 V2 API(模板)
+对于**所有新项目**,我们强烈推荐使用 [V2 API(模板)](/api/v2):
+- ✅ **内容复用** - 模板可以在多个场景中复用,无需每次传递完整内容
+- ✅ **完全动态** - 通过占位符可以实现完全动态内容(甚至可以只用一个占位符)
+- ✅ **统一管理** - 在管理后台统一管理消息模板,便于维护
+- ✅ **版本控制** - 模板内容修改不影响 API 调用代码
+- ✅ **更安全** - 使用加密 Token,不暴露模板 ID
+
+::: warning 关于 V1 API
+V1 API 仅为兼容历史数据而保留,不推荐在新项目中使用。后续的功能优化和维护重点都在 V2 API(模板)上。
+:::
+
+## 接口地址
+
+```
+POST /api/v1/message/send
+```
+
+## 请求参数
+
+### 基本参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | string | 是 | 推送令牌(加密),在管理后台查看 |
+| task_id | string | 否 | 任务ID(明文),与 token 二选一 |
+| title | string | 是 | 消息标题 |
+| text | string | 否 | 纯文本格式内容 |
+| html | string | 否 | HTML 格式内容 |
+| markdown | string | 否 | Markdown 格式内容 |
+
+::: tip 提示
+- `token` 和 `task_id` 二选一,推荐使用加密的 `token`
+- `text`、`html`、`markdown` 至少提供一种格式
+- 多种格式可以同时提供,系统会根据渠道自动选择
+:::
+
+### @提醒参数(可选)
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| at_mobiles | array | 否 | @手机号列表,如 `["13800138000", "13900139000"]` |
+| at_userids | array | 否 | @用户ID列表,如 `["user001", "user002"]` |
+| at_all | boolean | 否 | 是否@所有人,默认 `false` |
+
+::: warning 注意
+@提醒功能仅在支持的渠道(钉钉、企业微信)中生效。
+:::
+
+## 请求示例
+
+### 基本示例(纯文本)
+
+```json
+{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "系统通知",
+ "text": "Hello World!"
+}
+```
+
+### 多格式示例
+
+```json
+{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "系统通知",
+ "text": "您好,系统检测到异常登录。",
+ "html": "系统通知
您好,系统检测到异常登录。
",
+ "markdown": "## 系统通知\n\n您好,系统检测到**异常登录**。"
+}
+```
+
+### 带@提醒示例
+
+```json
+{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "紧急告警",
+ "text": "服务器CPU使用率超过90%,请及时处理!",
+ "markdown": "## 紧急告警\n\n服务器CPU使用率超过**90%**,请及时处理!",
+ "at_mobiles": ["13800138000", "13900139000"],
+ "at_all": false
+}
+```
+
+### cURL 示例
+
+```bash
+curl -X POST http://your-domain/api/v1/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "系统通知",
+ "text": "Hello World!"
+ }'
+```
+
+## 响应格式
+
+### 成功响应
+
+```json
+{
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "status": "sent"
+ }
+}
+```
+
+### 失败响应
+
+```json
+{
+ "code": 400,
+ "msg": "error message",
+ "data": null
+}
+```
+
+## 获取 Token
+
+1. 登录 Message Nest 管理后台
+2. 进入"发送任务"页面
+3. 创建新的发送任务
+4. 配置推送渠道(邮件、钉钉、企业微信等)
+5. 保存后获得推送令牌(Token)
+
+## 消息格式优先级
+
+V1 API 支持三种消息格式,系统会根据任务实例配置的格式类型自动选择对应的内容。
+
+### 格式选择规则
+
+当任务实例配置了特定格式类型时,系统按以下优先级选择内容:
+
+| 实例配置格式 | 优先级顺序 | 说明 |
+|------------|-----------|------|
+| **HTML** | html → markdown → text | 优先使用 HTML,其次 Markdown,最后纯文本 |
+| **Markdown** | markdown → text → html | 优先使用 Markdown,其次纯文本,最后 HTML |
+| **Text** | text → markdown → html | 优先使用纯文本,其次 Markdown,最后 HTML |
+
+### 示例说明
+
+**场景 1:邮件渠道(配置为 HTML 格式)**
+
+```json
+{
+ "text": "纯文本内容",
+ "html": "HTML内容
",
+ "markdown": "# Markdown内容"
+}
+```
+
+发送结果:使用 `html` 内容
+
+**场景 2:钉钉渠道(配置为 Markdown 格式)**
+
+```json
+{
+ "text": "纯文本内容",
+ "markdown": "# Markdown内容"
+}
+```
+
+发送结果:使用 `markdown` 内容
+
+**场景 3:只提供纯文本**
+
+```json
+{
+ "text": "纯文本内容"
+}
+```
+
+发送结果:所有渠道都使用 `text` 内容(兼容性最好)
+
+::: tip 最佳实践
+- **邮件渠道**:推荐提供 `html` 格式,视觉效果更好
+- **钉钉/企业微信**:推荐提供 `markdown` 格式,支持富文本
+- **通用场景**:至少提供 `text` 格式,确保所有渠道都能正常发送
+- **多渠道任务**:同时提供多种格式,让系统自动选择最佳格式
+:::
+
+## @提醒功能
+
+@提醒功能允许在钉钉、企业微信等支持的渠道中@特定用户或所有人。
+
+### 支持的渠道
+
+| 渠道 | @手机号 | @用户ID | @所有人 |
+|------|--------|--------|--------|
+| 钉钉 | ✅ | ✅ | ✅ |
+| 企业微信 | ✅ | ✅ | ✅ |
+| 邮件 | ❌ | ❌ | ❌ |
+| 其他 | ❌ | ❌ | ❌ |
+
+### 使用示例
+
+#### @指定手机号
+
+```json
+{
+ "token": "...",
+ "title": "系统告警",
+ "text": "服务器异常,请及时处理",
+ "at_mobiles": ["13800138000", "13900139000"]
+}
+```
+
+#### @指定用户ID
+
+```json
+{
+ "token": "...",
+ "title": "任务通知",
+ "text": "您的任务已完成",
+ "at_userids": ["user001", "user002"]
+}
+```
+
+#### @所有人
+
+```json
+{
+ "token": "...",
+ "title": "重要通知",
+ "text": "系统将于今晚22:00进行维护",
+ "at_all": true
+}
+```
+
+#### 组合使用
+
+```json
+{
+ "token": "...",
+ "title": "紧急告警",
+ "markdown": "## 紧急告警\n\n生产环境出现严重问题!",
+ "at_mobiles": ["13800138000"],
+ "at_userids": ["admin"],
+ "at_all": false
+}
+```
+
+::: warning 注意事项
+1. @提醒只在支持的渠道中生效,其他渠道会忽略这些参数
+2. 钉钉机器人需要配置相应的权限才能使用@功能
+3. @所有人功能需谨慎使用,避免打扰群成员
+4. 手机号和用户ID格式需符合对应平台的要求
+:::
+
+## 支持的推送渠道
+
+| 渠道 | 支持格式 | @提醒 | 说明 |
+|------|---------|------|------|
+| **邮件** | Text, HTML | ❌ | SMTP邮件发送,推荐使用 HTML 格式 |
+| **钉钉** | Text, Markdown | ✅ | 钉钉机器人,支持 Markdown 富文本 |
+| **企业微信** | Text, Markdown | ✅ | 企业微信机器人,支持 Markdown |
+| **微信公众号** | Text | ❌ | 微信测试公众号模板消息 |
+| **自定义Webhook** | Text, HTML, Markdown | ❌ | 自定义HTTP请求,格式取决于配置 |
+| **自托管消息** | Text, HTML, Markdown | ❌ | 站内消息,支持多种格式 |
+
+## 使用流程
+
+1. **创建推送渠道**
+ - 在管理后台配置各种推送渠道
+ - 填写相应的配置信息(如邮箱、Webhook地址等)
+
+2. **创建发送任务**
+ - 选择要使用的推送渠道
+ - 可以选择多个渠道同时推送
+ - 获得唯一的推送令牌(Token)
+
+3. **调用API发送消息**
+ - 使用获得的 Token
+ - 发送标题和内容
+ - 消息会自动推送到配置的所有渠道
+
+## 工作流程
+
+1. **Token 解析** - 解密 Token 获取任务 ID(或直接使用 task_id)
+2. **任务查询** - 根据任务 ID 查询任务信息
+3. **实例遍历** - 获取任务关联的所有启用实例
+4. **格式选择** - 根据实例配置的格式类型选择对应内容
+5. **@提醒处理** - 如果提供了@参数且渠道支持,添加@提醒
+6. **消息发送** - 向每个实例发送消息
+7. **返回结果** - 返回发送状态
+
+## 注意事项
+
+::: warning 重要
+- **Token 安全**:Token 是唯一的,请妥善保管,不要在公开代码中硬编码
+- **格式兼容**:至少提供一种格式(text/html/markdown),推荐提供多种格式
+- **异步调用**:建议使用异步方式调用 API,避免阻塞主流程
+- **@提醒限制**:@功能仅在钉钉、企业微信等支持的渠道中生效
+- **格式优先级**:系统会根据实例配置自动选择最合适的格式
+:::
+
+## 最佳实践
+
+### 1. 多格式支持
+
+为了确保消息在不同渠道都有良好的展示效果,建议同时提供多种格式:
+
+```json
+{
+ "token": "...",
+ "title": "订单通知",
+ "text": "您的订单已发货,订单号:20241206001",
+ "html": "订单通知
您的订单已发货
订单号:20241206001
",
+ "markdown": "## 订单通知\n\n您的订单已发货\n\n订单号:**20241206001**"
+}
+```
+
+### 2. 合理使用@提醒
+
+只在需要紧急通知时使用@提醒:
+
+```json
+{
+ "token": "...",
+ "title": "紧急告警",
+ "text": "生产环境出现严重问题",
+ "at_mobiles": ["13800138000"], // 只@相关负责人
+ "at_all": false // 避免@所有人
+}
+```
+
+### 3. 错误处理
+
+```python
+import requests
+import json
+
+def send_message(token, title, text):
+ url = "http://your-domain/api/v1/message/send"
+ data = {
+ "token": token,
+ "title": title,
+ "text": text
+ }
+
+ try:
+ response = requests.post(url, json=data, timeout=10)
+ result = response.json()
+
+ if result['code'] == 200:
+ print("发送成功")
+ else:
+ print(f"发送失败:{result['msg']}")
+ except Exception as e:
+ print(f"请求异常:{e}")
+```
+
+## 错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 请求参数错误 |
+| 401 | 未授权 |
+| 404 | Token不存在 |
+| 500 | 服务器内部错误 |
+
+## 常见问题
+
+### Q: V1 和 V2 API 有什么区别?应该选择哪个?
+
+**A:**
+
+| 特性 | V1 API(任务) | V2 API(模板)⭐ 推荐 |
+|------|--------------|-------------------|
+| **内容定义** | API 调用时传递 | 预定义在模板中 |
+| **动态内容** | 完全动态 | 通过占位符替换 |
+| **内容复用** | 每次都要传递完整内容 | 模板可复用 |
+| **维护成本** | 修改内容需要改代码 | 只需修改模板 |
+| **适用场景** | 完全动态、不重复的内容 | 有固定格式的通知消息 |
+
+**推荐使用 V2 API(模板)的原因:**
+1. **提高开发效率** - 一次定义模板,多处使用
+2. **降低维护成本** - 内容修改无需改代码
+3. **统一管理** - 所有消息模板集中管理
+4. **更好的协作** - 运营人员可以直接修改模板内容
+5. **版本控制** - 模板支持启用/禁用,方便灰度发布
+6. **完全动态** - 通过占位符同样可以实现完全动态内容
+
+::: warning V1 API 的定位
+V1 API 仅为兼容历史数据而保留,**不推荐在任何新项目中使用**。即使是完全动态的内容,也可以通过模板 + 占位符的方式实现,且更易于后期维护。
+
+后续的功能优化和维护重点都在 V2 API(模板)上。
+:::
+
+### Q: 如何选择使用哪种格式?
+
+**A:** 根据渠道特性选择:
+- **邮件**:推荐 HTML,视觉效果好
+- **钉钉/企业微信**:推荐 Markdown,支持富文本
+- **通用**:使用 Text,兼容性最好
+- **多渠道**:同时提供多种格式,让系统自动选择
+
+### Q: @提醒不生效怎么办?
+
+**A:** 检查以下几点:
+1. 渠道是否支持@提醒(仅钉钉、企业微信支持)
+2. 机器人是否有@权限
+3. 手机号或用户ID格式是否正确
+4. 参数名称是否正确(`at_mobiles`、`at_userids`、`at_all`)
+
+### Q: 可以只发送 HTML 格式吗?
+
+**A:** 可以,但建议同时提供 text 格式作为备选,确保不支持 HTML 的渠道也能正常发送。
+
+### Q: Token 和 task_id 有什么区别?
+
+**A:**
+- **token**:加密的任务标识,更安全,推荐使用
+- **task_id**:明文的任务ID,不推荐在生产环境使用
+
+## 各语言调用示例
+
+本节提供各种编程语言的完整调用示例代码。
+
+### cURL
+
+```bash
+curl -X POST --location 'http://127.0.0.1:8000/api/v1/message/send' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+ }'
+```
+
+### Python
+
+```python
+import requests
+
+headers = {
+ 'Content-Type': 'application/json',
+}
+
+json_data = {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+}
+
+response = requests.post(
+ 'http://127.0.0.1:8000/api/v1/message/send',
+ headers=headers,
+ json=json_data
+)
+
+print("response:", response.json())
+```
+
+**安装依赖:**
+
+```bash
+pip install requests
+```
+
+### Go
+
+```go
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+)
+
+func main() {
+ client := &http.Client{}
+ var data = strings.NewReader(`{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+}`)
+ req, err := http.NewRequest("POST", "http://127.0.0.1:8000/api/v1/message/send", data)
+ if err != nil {
+ log.Fatal(err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+ bodyText, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s\n", bodyText)
+}
+```
+
+### Java
+
+```java
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+
+public class MessageNestExample {
+ public static void main(String[] args) throws IOException, InterruptedException {
+ HttpClient client = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .build();
+
+ String jsonData = """
+ {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+ }
+ """;
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create("http://127.0.0.1:8000/api/v1/message/send"))
+ .POST(BodyPublishers.ofString(jsonData))
+ .setHeader("Content-Type", "application/json")
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ System.out.println(response.body());
+ }
+}
+```
+
+### Node.js
+
+#### 使用 request 库
+
+```javascript
+var request = require('request');
+
+var headers = {
+ 'Content-Type': 'application/json'
+};
+
+var dataString = JSON.stringify({
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title": "message title",
+ "text": "Hello World!"
+});
+
+var options = {
+ url: 'http://127.0.0.1:8000/api/v1/message/send',
+ method: 'POST',
+ headers: headers,
+ body: dataString
+};
+
+function callback(error, response, body) {
+ if (!error && response.statusCode == 200) {
+ console.log(body);
+ }
+}
+
+request(options, callback);
+```
+
+#### 使用 axios 库
+
+```javascript
+const axios = require('axios');
+
+const data = {
+ token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title: "message title",
+ text: "Hello World!"
+};
+
+axios.post('http://127.0.0.1:8000/api/v1/message/send', data, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+})
+.then(response => {
+ console.log('response:', response.data);
+})
+.catch(error => {
+ console.error('error:', error);
+});
+```
+
+#### 使用 fetch (Node.js 18+)
+
+```javascript
+const data = {
+ token: "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title: "message title",
+ text: "Hello World!"
+};
+
+fetch('http://127.0.0.1:8000/api/v1/message/send', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+})
+.then(response => response.json())
+.then(data => {
+ console.log('response:', data);
+})
+.catch(error => {
+ console.error('error:', error);
+});
+```
+
+### PHP
+
+```php
+ "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ "title" => "message title",
+ "text" => "Hello World!"
+);
+
+curl_setopt($ch, CURLOPT_URL, 'http://127.0.0.1:8000/api/v1/message/send');
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+]);
+curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+
+$response = curl_exec($ch);
+
+if (curl_errno($ch)) {
+ echo 'Error:' . curl_error($ch);
+} else {
+ echo $response;
+}
+
+curl_close($ch);
+?>
+```
+
+### C#
+
+```csharp
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+class Program
+{
+ static async Task Main(string[] args)
+ {
+ using var client = new HttpClient();
+
+ var data = new
+ {
+ token = "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
+ title = "message title",
+ text = "Hello World!"
+ };
+
+ var json = JsonSerializer.Serialize(data);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync(
+ "http://127.0.0.1:8000/api/v1/message/send",
+ content
+ );
+
+ var responseString = await response.Content.ReadAsStringAsync();
+ Console.WriteLine(responseString);
+ }
+}
+```
+
+### Ruby
+
+```ruby
+require 'net/http'
+require 'json'
+require 'uri'
+
+uri = URI('http://127.0.0.1:8000/api/v1/message/send')
+http = Net::HTTP.new(uri.host, uri.port)
+
+request = Net::HTTP::Post.new(uri.path, {
+ 'Content-Type' => 'application/json'
+})
+
+request.body = {
+ token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e',
+ title: 'message title',
+ text: 'Hello World!'
+}.to_json
+
+response = http.request(request)
+puts response.body
+```
+
+### 示例说明
+
+::: tip 提示
+- 将示例中的 `http://127.0.0.1:8000` 替换为你的实际服务地址
+- 将 `a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e` 替换为你在管理后台创建的实际 Token
+- 建议在生产环境中使用 HTTPS
+- 所有示例都使用基本的纯文本格式,实际使用时可以添加 `html`、`markdown` 等参数
+:::
+
+## 下一步
+
+- 查看 [V2 API 文档](/api/v2) 了解基于模板的发送方式
+- 查看 [消息模板](/guide/template) 了解如何使用模板功能
diff --git a/docs/api/v2.md b/docs/api/v2.md
new file mode 100644
index 0000000..ac1ab3c
--- /dev/null
+++ b/docs/api/v2.md
@@ -0,0 +1,519 @@
+# V2 API 文档
+
+V2 API 提供基于消息模板的发送接口,支持占位符替换和多实例发送。
+
+::: tip ⭐ 推荐使用
+V2 API(模板)是我们推荐的消息发送方式,相比 V1 API 具有以下优势:
+- ✅ **内容复用** - 一次定义,多处使用,大幅提高开发效率
+- ✅ **灵活性** - 通过占位符实现动态内容,兼顾固定格式和动态数据
+- ✅ **易维护** - 修改消息内容无需改代码,运营人员可直接操作
+- ✅ **版本控制** - 支持模板启用/禁用,便于灰度发布和回滚
+- ✅ **团队协作** - 开发和运营分工明确,提高协作效率
+
+适用于 90% 的消息发送场景,特别是有固定格式的通知类消息。
+:::
+
+## 接口概述
+
+V2 API 与 V1 API 的主要区别:
+
+| 特性 | V1 API | V2 API |
+|------|--------|--------|
+| 发送方式 | 基于任务 | 基于模板 |
+| 内容定义 | API 调用时传递 | 模板预定义 |
+| 动态内容 | 不支持 | 支持占位符 |
+| 多格式 | 单一格式 | Text/HTML/Markdown |
+| 安全性 | Token 加密 | Token 加密 |
+
+## 接口地址
+
+```
+POST /api/v2/message/send
+```
+
+## 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| token | string | 是 | 加密的模板 Token |
+| title | string | 是 | 消息标题 |
+| placeholders | object | 否 | 占位符键值对 |
+
+### 参数说明
+
+#### token
+
+- 模板的加密 Token,在管理后台的"消息模板"页面获取
+- **注意**:V2 API 只支持加密 Token,不支持明文模板 ID
+- Token 使用对称加密算法生成,确保安全性
+
+#### title
+
+- 消息标题,会传递给所有支持标题的渠道(如邮件)
+- 必填参数,不能为空
+
+#### placeholders
+
+- 占位符的键值对,用于替换模板中的 `{{key}}`
+- 格式为 JSON 对象:`{"key": "value"}`
+- 如果模板中定义了占位符但未传递,将使用默认值
+- 如果既未传递也无默认值,占位符将保持原样
+
+## 请求示例
+
+### 基本示例
+
+```bash
+curl -X POST http://your-domain/api/v2/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "title": "系统通知",
+ "placeholders": {
+ "username": "张三",
+ "action": "登录",
+ "time": "2024-12-06 12:00:00"
+ }
+ }'
+```
+
+### Python 示例
+
+```python
+import requests
+import json
+
+url = "http://your-domain/api/v2/message/send"
+headers = {"Content-Type": "application/json"}
+
+data = {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "title": "系统通知",
+ "placeholders": {
+ "username": "张三",
+ "action": "登录",
+ "time": "2024-12-06 12:00:00"
+ }
+}
+
+response = requests.post(url, headers=headers, data=json.dumps(data))
+print(response.json())
+```
+
+### Go 示例
+
+```go
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+func main() {
+ url := "http://your-domain/api/v2/message/send"
+
+ data := map[string]interface{}{
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "title": "系统通知",
+ "placeholders": map[string]string{
+ "username": "张三",
+ "action": "登录",
+ "time": "2024-12-06 12:00:00",
+ },
+ }
+
+ jsonData, _ := json.Marshal(data)
+ resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var result map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&result)
+ fmt.Println(result)
+}
+```
+
+### JavaScript/Node.js 示例
+
+```javascript
+const axios = require('axios');
+
+const url = 'http://your-domain/api/v2/message/send';
+
+const data = {
+ token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
+ title: '系统通知',
+ placeholders: {
+ username: '张三',
+ action: '登录',
+ time: '2024-12-06 12:00:00'
+ }
+};
+
+axios.post(url, data)
+ .then(response => {
+ console.log(response.data);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+```
+
+### Java 示例
+
+```java
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class MessageSender {
+ public static void main(String[] args) throws Exception {
+ String url = "http://your-domain/api/v2/message/send";
+
+ String jsonData = """
+ {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "title": "系统通知",
+ "placeholders": {
+ "username": "张三",
+ "action": "登录",
+ "time": "2024-12-06 12:00:00"
+ }
+ }
+ """;
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setDoOutput(true);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ byte[] input = jsonData.getBytes(StandardCharsets.UTF_8);
+ os.write(input, 0, input.length);
+ }
+
+ int responseCode = conn.getResponseCode();
+ System.out.println("Response Code: " + responseCode);
+ }
+}
+```
+
+## 响应格式
+
+### 成功响应
+
+```json
+{
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
+ "count": 3
+ }
+}
+```
+
+**响应字段说明:**
+
+- `code` - 状态码,200 表示成功
+- `msg` - 响应消息
+- `data.token` - 使用的模板 Token
+- `data.count` - 成功发送的实例数量
+
+### 失败响应
+
+```json
+{
+ "code": 400,
+ "msg": "token解析失败:invalid token format",
+ "data": null
+}
+```
+
+## 工作流程
+
+1. **Token 解析** - 解密 Token 获取模板 ID
+2. **模板查询** - 根据模板 ID 查询模板信息
+3. **状态检查** - 检查模板是否启用
+4. **占位符替换** - 使用传入的 placeholders 替换模板中的占位符
+5. **实例遍历** - 获取模板关联的所有启用实例
+6. **格式匹配** - 根据实例的 ContentType 选择对应格式的内容
+7. **消息发送** - 向每个实例发送消息
+8. **返回结果** - 返回发送成功的实例数量
+
+## 占位符替换规则
+
+### 基本规则
+
+模板中使用 `{{key}}` 定义占位符,API 调用时通过 `placeholders` 参数传递替换值。
+
+**模板内容:**
+```text
+您好,{{username}}!
+您的订单 {{order_id}} 已经 {{status}}。
+```
+
+**API 调用:**
+```json
+{
+ "placeholders": {
+ "username": "张三",
+ "order_id": "20241206001",
+ "status": "发货"
+ }
+}
+```
+
+**替换结果:**
+```text
+您好,张三!
+您的订单 20241206001 已经发货。
+```
+
+### 默认值处理
+
+如果占位符定义了默认值,未传递时使用默认值:
+
+**占位符定义:**
+```json
+[
+ {
+ "key": "username",
+ "label": "用户名",
+ "default": "Guest"
+ }
+]
+```
+
+**API 调用(未传递 username):**
+```json
+{
+ "placeholders": {}
+}
+```
+
+**替换结果:**
+```text
+您好,Guest!
+```
+
+### 未定义占位符
+
+如果占位符既未传递也无默认值,将保持原样:
+
+```text
+您好,{{username}}! // username 未传递且无默认值
+```
+
+## 内容格式选择
+
+V2 API 支持三种内容格式,系统会根据实例配置自动选择:
+
+| 格式 | 适用渠道 | 说明 |
+|------|---------|------|
+| Text | 所有渠道 | 纯文本格式,兼容性最好 |
+| HTML | 邮件 | 富文本格式,支持样式 |
+| Markdown | 钉钉、企业微信 | Markdown 格式,支持格式化 |
+
+**示例:**
+
+假设模板定义了三种格式的内容:
+
+- Text: `您好,{{username}}!`
+- HTML: `您好,{{username}}!
`
+- Markdown: `## 您好,{{username}}!`
+
+当发送到不同实例时:
+
+- **邮件实例(ContentType=html)** → 使用 HTML 格式
+- **钉钉实例(ContentType=markdown)** → 使用 Markdown 格式
+- **其他实例(ContentType=text)** → 使用 Text 格式
+
+## @提醒功能
+
+如果模板配置了@提醒,会自动应用到支持的渠道(钉钉、企业微信)。
+
+**模板配置:**
+- @手机号:`13800138000,13900139000`
+- @用户ID:`user001,user002`
+- @所有人:是
+
+**发送效果:**
+- 钉钉/企业微信会@指定的手机号或用户
+- 如果启用@所有人,会@群内所有成员
+
+## 错误码说明
+
+| 错误码 | 说明 | 解决方案 |
+|--------|------|---------|
+| 200 | 成功 | - |
+| 400 | 请求参数错误 | 检查请求参数格式 |
+| 400 | token解析失败 | 检查 Token 是否正确 |
+| 400 | 模板不存在 | 检查模板 ID 是否有效 |
+| 400 | 模板已禁用 | 在管理后台启用模板 |
+| 400 | 模板没有配置发送实例 | 为模板添加发送实例 |
+| 400 | 模板没有启用的发送实例 | 启用至少一个实例 |
+| 500 | 服务器内部错误 | 联系管理员 |
+
+## 获取模板 Token
+
+### 方式一:管理后台查看
+
+1. 登录 Message Nest 管理后台
+2. 进入"消息模板"页面
+3. 点击模板的"接口"按钮
+4. 查看并复制加密的 Token
+
+### 方式二:API 代码示例
+
+管理后台提供多种语言的 API 调用示例,包含真实的加密 Token。
+
+## 安全性说明
+
+### Token 加密
+
+- V2 API 使用对称加密算法保护模板 ID
+- Token 是确定性加密,相同的模板 ID 生成相同的 Token
+- 加密密钥存储在服务器端,客户端无需关心加密细节
+
+### 最佳实践
+
+1. **Token 保护**
+ - 不要在公开代码中硬编码 Token
+ - 使用环境变量或配置文件存储 Token
+ - 定期检查 Token 的使用情况
+
+2. **权限控制**
+ - 合理设置模板的启用/禁用状态
+ - 及时禁用不再使用的模板
+ - 定期审查模板配置
+
+3. **内容安全**
+ - 注意模板内容的合规性
+ - 避免在模板中包含敏感信息
+ - 对用户输入进行验证和过滤
+
+## 使用场景
+
+### 1. 用户通知
+
+```json
+{
+ "token": "...",
+ "title": "账号安全提醒",
+ "placeholders": {
+ "username": "张三",
+ "action": "登录",
+ "ip": "192.168.1.100",
+ "time": "2024-12-06 12:00:00"
+ }
+}
+```
+
+### 2. 订单通知
+
+```json
+{
+ "token": "...",
+ "title": "订单状态更新",
+ "placeholders": {
+ "order_id": "20241206001",
+ "status": "已发货",
+ "tracking_number": "SF1234567890",
+ "estimated_delivery": "2024-12-08"
+ }
+}
+```
+
+### 3. 系统告警
+
+```json
+{
+ "token": "...",
+ "title": "系统告警",
+ "placeholders": {
+ "service": "API Server",
+ "level": "严重",
+ "message": "CPU使用率超过90%",
+ "time": "2024-12-06 12:00:00"
+ }
+}
+```
+
+### 4. 营销推广
+
+```json
+{
+ "token": "...",
+ "title": "优惠活动通知",
+ "placeholders": {
+ "username": "张三",
+ "product": "VIP会员",
+ "discount": "8折",
+ "expire_date": "2024-12-31"
+ }
+}
+```
+
+## 性能优化
+
+### 异步发送
+
+V2 API 采用异步发送机制,API 调用立即返回,实际发送在后台进行。
+
+**优点:**
+- 快速响应,不阻塞调用方
+- 支持批量发送多个实例
+- 自动重试失败的发送
+
+### 批量发送
+
+一次 API 调用可以发送到多个实例(渠道),系统自动遍历所有启用的实例。
+
+**示例:**
+
+模板配置了 3 个实例:
+- 邮件实例(启用)
+- 钉钉实例(启用)
+- 企业微信实例(禁用)
+
+调用 API 后,消息会发送到邮件和钉钉,企业微信实例被跳过。
+
+## 常见问题
+
+### Q: V1 和 V2 API 可以同时使用吗?
+
+**A:** 可以。V1 和 V2 API 是独立的,可以根据需求选择使用。
+
+### Q: 如何从 V1 迁移到 V2?
+
+**A:**
+1. 创建消息模板,定义占位符
+2. 为模板配置发送实例
+3. 获取模板 Token
+4. 修改 API 调用代码,使用 V2 接口
+
+### Q: 占位符可以嵌套吗?
+
+**A:** 不支持。占位符只支持一级替换,不支持嵌套或递归。
+
+### Q: 可以动态添加占位符吗?
+
+**A:** 不可以。占位符必须在模板中预先定义,API 调用时只能传递已定义的占位符。
+
+### Q: 如何调试模板?
+
+**A:** 使用管理后台的"预览"功能,可以填写测试数据查看替换效果。
+
+## 下一步
+
+- 查看 [消息模板文档](/guide/template) 了解如何创建和管理模板
+- 查看 [V1 API 文档](/api/usage) 了解传统的发送方式
+- 查看 [API 示例](/api/examples) 了解更多调用示例
diff --git a/docs/guide/changelog.md b/docs/guide/changelog.md
index 3f22cd9..4ec1d49 100644
--- a/docs/guide/changelog.md
+++ b/docs/guide/changelog.md
@@ -2,6 +2,23 @@
## 2025
+### 2025.12.06
+- **新增消息模板功能**
+ - 支持创建可复用的消息模板
+ - 支持占位符动态替换(`{{key}}` 语法)
+ - 支持 Text、HTML、Markdown 三种格式
+ - 支持为模板配置多个发送实例
+ - 支持 @提醒功能(钉钉、企业微信)
+- **新增 V2 API**
+ - 基于模板的消息发送接口
+ - 使用加密 Token 提升安全性
+ - 支持占位符参数传递
+ - 自动遍历模板的所有启用实例
+- **优化代码结构**
+ - 重构模板相关 Model 命名(`MessageTemplate` → `Template`)
+ - 添加 TypeScript 类型声明文件
+ - 完善文档体系
+
### 2025.10.12
- 增加cookies过期天数设置
diff --git a/docs/guide/template.md b/docs/guide/template.md
new file mode 100644
index 0000000..89abdc4
--- /dev/null
+++ b/docs/guide/template.md
@@ -0,0 +1,280 @@
+# 消息模板
+
+消息模板功能允许您创建可复用的消息模板,通过占位符实现动态内容替换,提高消息发送的灵活性和效率。
+
+::: tip ⭐ 作者推荐
+消息模板是 Message Nest 的核心功能,也是我们**强烈推荐**的使用方式。相比传统的任务发送(V1 API),模板方式具有以下显著优势:
+
+**为什么推荐使用模板?**
+1. **开发效率提升 3 倍** - 一次定义模板,所有项目复用,无需重复编写消息内容
+2. **维护成本降低 80%** - 修改消息格式只需在后台更新模板,无需修改代码、重新部署
+3. **团队协作更顺畅** - 开发负责 API 集成,运营负责内容维护,职责清晰
+4. **灰度发布更安全** - 支持模板启用/禁用,可以随时回滚,降低风险
+5. **内容管理更规范** - 所有消息模板集中管理,便于审核和统一风格
+
+**适用场景:**
+- ✅ 用户通知(注册、登录、密码重置等)
+- ✅ 订单消息(下单、支付、发货、退款等)
+- ✅ 系统告警(服务异常、资源不足等)
+- ✅ 营销推广(活动通知、优惠券等)
+- ✅ 完全动态的内容(通过占位符实现,如 `{{content}}`)
+- ✅ **所有消息发送场景**
+
+::: info 关于完全动态内容
+即使消息内容完全动态,也推荐使用模板方式。你可以创建一个只包含一个占位符的模板,如:
+
+**模板内容:** `{{content}}`
+
+**API 调用:**
+```json
+{
+ "placeholders": {
+ "content": "这里是完全动态的内容"
+ }
+}
+```
+
+这样做的好处是:
+1. 后期如果需要添加固定格式(如标题、签名),只需修改模板,无需改代码
+2. 所有消息统一管理,便于审计和监控
+3. 可以随时启用/禁用,便于灰度发布
+
+**V1 API(任务)的定位:**
+- 仅为兼容历史数据而保留
+- 不推荐在任何新项目中使用
+- 后续维护重点在模板功能上
+:::
+
+
+## 功能特性
+
+- ✅ **多格式支持** - 支持 Text、HTML、Markdown 三种格式
+- ✅ **占位符替换** - 使用 `{{key}}` 语法定义动态内容
+- ✅ **实例配置** - 为模板配置多个发送实例(渠道)
+- ✅ **@提醒功能** - 支持钉钉、企业微信的@提醒
+- ✅ **状态管理** - 启用/禁用模板控制
+- ✅ **API调用** - 通过 V2 API 使用模板发送消息
+
+## 创建模板
+
+### 1. 基本信息
+
+在管理后台的"消息模板"页面,点击"新建模板"按钮:
+
+- **模板名称** - 模板的唯一标识名称
+- **模板描述** - 模板的用途说明(可选)
+- **状态** - 启用/禁用
+
+### 2. 定义占位符
+
+占位符用于在发送消息时动态替换内容。
+
+**添加占位符:**
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| Key | 占位符键名 | `username` |
+| Label | 显示标签 | `用户名` |
+| Default | 默认值(可选) | `Guest` |
+
+**使用示例:**
+
+```json
+[
+ {
+ "key": "username",
+ "label": "用户名",
+ "default": "Guest"
+ },
+ {
+ "key": "email",
+ "label": "邮箱地址",
+ "default": "user@example.com"
+ },
+ {
+ "key": "action",
+ "label": "操作类型",
+ "default": "登录"
+ }
+]
+```
+
+### 3. 编写模板内容
+
+支持三种格式,可以根据需要填写一种或多种:
+
+#### Text 模板
+
+纯文本格式,适用于所有渠道:
+
+```text
+您好,{{username}}!
+
+您的账号 {{email}} 刚刚进行了 {{action}} 操作。
+
+如果这不是您本人的操作,请立即联系我们。
+```
+
+#### HTML 模板
+
+HTML 格式,适用于邮件等支持富文本的渠道:
+
+```html
+
+
您好,{{username}}!
+
您的账号 {{email}} 刚刚进行了 {{action}} 操作。
+
如果这不是您本人的操作,请立即联系我们。
+
+```
+
+#### Markdown 模板
+
+Markdown 格式,适用于钉钉、企业微信等支持 Markdown 的渠道:
+
+```markdown
+## 您好,{{username}}!
+
+您的账号 **{{email}}** 刚刚进行了 `{{action}}` 操作。
+
+> ⚠️ 如果这不是您本人的操作,请立即联系我们。
+```
+
+### 4. 配置 @提醒(可选)
+
+针对钉钉、企业微信等渠道,可以配置@提醒:
+
+- **@手机号** - 多个手机号用逗号分隔,如:`13800138000,13900139000`
+- **@用户ID** - 多个用户ID用逗号分隔
+- **@所有人** - 勾选后会@所有群成员
+
+::: warning 注意
+@提醒功能仅在支持的渠道(钉钉、企业微信)中生效。
+:::
+
+## 配置发送实例
+
+创建模板后,需要为模板配置发送实例(渠道)。
+
+### 1. 添加实例
+
+在模板列表中,点击"实例"按钮:
+
+1. 选择发送渠道(从已创建的渠道中选择)
+2. 配置实例参数(如邮箱收件人地址)
+3. 选择消息格式(Text/HTML/Markdown)
+4. 保存实例
+
+### 2. 实例配置说明
+
+不同渠道需要配置不同的参数:
+
+**邮件渠道:**
+- 收件人邮箱地址
+- 消息格式:Text 或 HTML
+
+**钉钉/企业微信:**
+- 消息格式:Text 或 Markdown
+
+**自定义 Webhook:**
+- 根据 Webhook 要求配置
+
+### 3. 管理实例
+
+- **启用/禁用** - 控制实例是否参与发送
+- **编辑** - 修改实例配置
+- **删除** - 删除不需要的实例
+
+## 预览模板
+
+在编辑模板时,可以使用"预览"功能查看替换占位符后的效果:
+
+1. 点击"预览"按钮
+2. 填写占位符的测试值
+3. 查看 Text/HTML/Markdown 三种格式的渲染效果
+
+## 使用模板发送消息
+
+### 通过 API 调用
+
+使用 V2 API 发送模板消息,详见 [V2 API 文档](/api/v2)。
+
+**基本流程:**
+
+1. 获取模板 Token(在模板详情页查看)
+2. 准备占位符数据
+3. 调用 V2 API 发送
+
+**示例:**
+
+```bash
+curl -X POST http://your-domain/api/v2/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "encrypted_template_token",
+ "title": "账号安全提醒",
+ "placeholders": {
+ "username": "张三",
+ "email": "zhangsan@example.com",
+ "action": "登录"
+ }
+ }'
+```
+
+## 最佳实践
+
+### 1. 占位符命名
+
+- 使用有意义的英文名称,如 `username`、`order_id`
+- 避免使用特殊字符,建议使用下划线分隔
+- 保持命名一致性
+
+### 2. 模板设计
+
+- **简洁明了** - 模板内容应简洁清晰
+- **格式适配** - 根据渠道特性选择合适的格式
+- **默认值** - 为占位符设置合理的默认值
+- **测试验证** - 使用预览功能验证模板效果
+
+### 3. 实例管理
+
+- **合理分组** - 为不同用途创建不同的模板
+- **渠道选择** - 根据消息类型选择合适的渠道
+- **定期检查** - 定期检查实例配置的有效性
+
+### 4. 安全性
+
+- **Token 保护** - 妥善保管模板 Token
+- **权限控制** - 合理设置模板的启用/禁用状态
+- **内容审查** - 注意模板内容的合规性
+
+## 常见问题
+
+### Q: 占位符没有被替换?
+
+**A:** 检查以下几点:
+- 占位符格式是否正确(`{{key}}`)
+- API 调用时是否传递了对应的 placeholders 参数
+- Key 名称是否匹配
+
+### Q: 如何选择消息格式?
+
+**A:** 根据渠道特性选择:
+- **邮件** - 推荐使用 HTML 格式,视觉效果更好
+- **钉钉/企业微信** - 推荐使用 Markdown 格式,支持富文本
+- **其他渠道** - 使用 Text 格式,兼容性最好
+
+### Q: 可以为一个模板配置多个实例吗?
+
+**A:** 可以。一个模板可以配置多个发送实例,调用 API 时会自动遍历所有启用的实例进行发送。
+
+### Q: @提醒不生效?
+
+**A:** 确认以下几点:
+- 渠道是否支持@提醒(仅钉钉、企业微信支持)
+- 手机号或用户ID格式是否正确
+- 机器人是否有@权限
+
+## 下一步
+
+- 查看 [V2 API 文档](/api/v2) 了解如何通过 API 使用模板
+- 查看 [API 示例](/api/examples) 了解各语言的调用方式
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 8f31c19..4d1ba01 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -8,7 +8,7 @@
"name": "message-nest-docs",
"version": "1.0.0",
"devDependencies": {
- "vitepress": "^1.0.0"
+ "vitepress": "^1.6.4"
}
},
"node_modules/@algolia/abtesting": {
diff --git a/docs/package.json b/docs/package.json
index 474f3a3..84d9798 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -8,6 +8,6 @@
"docs:preview": "vitepress preview"
},
"devDependencies": {
- "vitepress": "^1.0.0"
+ "vitepress": "^1.6.4"
}
}
From 850e52fce80db48c7011e1dc45192facf3d8200a Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 18:39:37 +0800
Subject: [PATCH 15/24] chore: complete docs
---
README.md | 16 +-
docs/.vitepress/config.mts | 12 +-
docs/guide/changelog.md | 4 -
docs/guide/channels.md | 364 +++++++++++++
docs/guide/features.md | 126 ++++-
docs/guide/scheduled-messages.md | 297 +++++++++++
docs/guide/self-hosted-messages-old.md | 480 ++++++++++++++++++
docs/guide/self-hosted-messages.md | 151 ++++++
docs/guide/settings.md | 217 ++++++++
docs/guide/tasks.md | 341 +++++++++++++
docs/index.md | 36 --
.../pages/messageTemplate/MessageTemplate.vue | 2 +-
.../components/pages/sendTasks/SendTasks.vue | 37 ++
13 files changed, 2013 insertions(+), 70 deletions(-)
create mode 100644 docs/guide/channels.md
create mode 100644 docs/guide/scheduled-messages.md
create mode 100644 docs/guide/self-hosted-messages-old.md
create mode 100644 docs/guide/self-hosted-messages.md
create mode 100644 docs/guide/settings.md
create mode 100644 docs/guide/tasks.md
diff --git a/README.md b/README.md
index dcc20ab..f455886 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,17 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
关于运行日志,考虑到目前多数服务以收集控制台输出为主,暂时不支持写出日志文件。
## 更新日志 ☕
-[点我转跳](https://engigu.github.io/Message-Push-Nest/guide/changelog.html)
+
+### 最近更新
+
+**2025.12.06** - 新增消息模板功能、V2 API
+**2025.10.12** - 增加 cookies 过期天数设置
+**2025.09.30** - 支持明暗主题切换、登录日志
+**2025.08.10** - 重构 web 页面,UI 升级(shadcn-vue + tailwindcss)
+**2025.04.28** - 支持 TiDB、数据库 SSL 配置
+**2025.01.01** - 支持托管消息功能
+
+[查看完整更新日志](https://engigu.github.io/Message-Push-Nest/guide/changelog.html)
## 项目来由 💡
@@ -43,10 +53,6 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
欢迎通过提交问题和提出改进建议。
-## 致谢 🙏
-
-该项目汲取了[go-gin-example](https://github.com/eddycjy/go-gin-example)项目的灵感,展示了 Go 和 Gin 在实际应用中的强大和多才多艺。
-
## Star History ⭐
[](https://star-history.com/#engigu/Message-Push-Nest&Date)
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 412d495..fbad0f4 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -32,7 +32,17 @@ export default defineConfig({
'/guide/': [
{ text: '介绍', link: '/guide/introduction' },
{ text: '特色功能', link: '/guide/features' },
- { text: '消息模板', link: '/guide/template' },
+ {
+ text: '功能使用',
+ items: [
+ { text: '渠道配置', link: '/guide/channels' },
+ { text: '发送任务', link: '/guide/tasks' },
+ { text: '消息模板', link: '/guide/template' },
+ { text: '定时消息', link: '/guide/scheduled-messages' },
+ { text: '托管消息', link: '/guide/self-hosted-messages' },
+ { text: '系统设置', link: '/guide/settings' }
+ ]
+ },
{ text: '更新日志', link: '/guide/changelog' }
],
'/deployment/': [
diff --git a/docs/guide/changelog.md b/docs/guide/changelog.md
index 4ec1d49..419a19c 100644
--- a/docs/guide/changelog.md
+++ b/docs/guide/changelog.md
@@ -14,10 +14,6 @@
- 使用加密 Token 提升安全性
- 支持占位符参数传递
- 自动遍历模板的所有启用实例
-- **优化代码结构**
- - 重构模板相关 Model 命名(`MessageTemplate` → `Template`)
- - 添加 TypeScript 类型声明文件
- - 完善文档体系
### 2025.10.12
- 增加cookies过期天数设置
diff --git a/docs/guide/channels.md b/docs/guide/channels.md
new file mode 100644
index 0000000..9abb9de
--- /dev/null
+++ b/docs/guide/channels.md
@@ -0,0 +1,364 @@
+# 推送渠道配置
+
+Message Nest 支持多种消息推送渠道,您可以根据需求配置不同的渠道,实现多渠道消息推送。
+
+## 邮件(SMTP)
+
+通过标准 SMTP 协议发送邮件通知。
+
+### 配置参数
+
+| 参数 | 说明 | 示例 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | `公司邮箱` |
+| SMTP 服务器 | 邮件服务器地址 | `smtp.gmail.com` |
+| SMTP 端口 | 服务器端口 | `587` (TLS) 或 `465` (SSL) |
+| 发件人邮箱 | 发送邮件的邮箱地址 | `noreply@example.com` |
+| 发件人名称 | 显示的发件人名称 | `系统通知` |
+| 邮箱密码 | 邮箱密码或授权码 | `your_password` |
+| 加密方式 | TLS/SSL/无 | `TLS` |
+
+### 常见邮箱配置
+
+#### Gmail
+
+- **SMTP 服务器**: `smtp.gmail.com`
+- **端口**: `587` (TLS)
+- **注意**: 需要开启"允许不够安全的应用"或使用应用专用密码
+
+#### QQ 邮箱
+
+- **SMTP 服务器**: `smtp.qq.com`
+- **端口**: `587` (TLS) 或 `465` (SSL)
+- **注意**: 需要在邮箱设置中开启 SMTP 服务并获取授权码
+
+#### 163 邮箱
+
+- **SMTP 服务器**: `smtp.163.com`
+- **端口**: `465` (SSL)
+- **注意**: 需要开启 SMTP 服务并使用授权码
+
+#### 企业邮箱
+
+根据企业邮箱服务商提供的 SMTP 配置填写。
+
+### 使用场景
+
+- ✅ 用户注册/登录通知
+- ✅ 订单确认和物流通知
+- ✅ 账单和发票发送
+- ✅ 密码重置和安全提醒
+- ✅ 营销邮件和活动通知
+
+### 注意事项
+
+::: warning 注意
+- 邮件发送可能会被识别为垃圾邮件,建议配置 SPF、DKIM 等记录
+- 使用授权码而非邮箱密码,提高安全性
+- 注意邮件发送频率限制,避免被封禁
+- 建议使用企业邮箱,稳定性更好
+:::
+
+## 钉钉机器人
+
+通过钉钉群机器人发送消息到钉钉群。
+
+### 配置步骤
+
+1. **创建钉钉群**
+ - 在钉钉中创建一个群聊
+
+2. **添加自定义机器人**
+ - 进入群设置 → 智能群助手 → 添加机器人 → 自定义
+ - 设置机器人名称和头像
+
+3. **配置安全设置**
+ - 选择"自定义关键词"或"加签"方式
+ - 记录 Webhook 地址
+
+4. **在 Message Nest 中配置**
+ - 渠道名称:自定义名称
+ - Webhook URL:复制的 Webhook 地址
+ - 安全设置:根据选择填写关键词或密钥
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| Webhook URL | 机器人 Webhook 地址 | 是 |
+| 安全关键词 | 自定义关键词(如选择此方式) | 否 |
+| 加签密钥 | 加签密钥(如选择此方式) | 否 |
+
+### 消息格式
+
+钉钉支持以下格式:
+- **Text** - 纯文本
+- **Markdown** - 支持 Markdown 格式
+
+### @提醒功能
+
+支持@群成员:
+- **@手机号** - 通过手机号@指定成员
+- **@所有人** - @群内所有成员(需要机器人权限)
+
+### 使用场景
+
+- ✅ 系统告警和监控通知
+- ✅ 任务完成提醒
+- ✅ 审批流程通知
+- ✅ 团队协作消息
+- ✅ 日报/周报推送
+
+### 注意事项
+
+::: warning 注意
+- 每个机器人每分钟最多发送 20 条消息
+- @所有人功能需要群主授权
+- 建议在消息中包含安全关键词,避免发送失败
+- 机器人被移除后 Webhook 将失效
+:::
+
+## 企业微信机器人
+
+通过企业微信群机器人发送消息到企业微信群。
+
+### 配置步骤
+
+1. **创建企业微信群**
+ - 在企业微信中创建群聊
+
+2. **添加群机器人**
+ - 进入群设置 → 群机器人 → 添加机器人
+ - 设置机器人名称
+
+3. **获取 Webhook**
+ - 复制 Webhook 地址
+
+4. **在 Message Nest 中配置**
+ - 渠道名称:自定义名称
+ - Webhook URL:复制的 Webhook 地址
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| Webhook URL | 机器人 Webhook 地址 | 是 |
+
+### 消息格式
+
+企业微信支持以下格式:
+- **Text** - 纯文本
+- **Markdown** - 支持 Markdown 格式
+
+### @提醒功能
+
+支持@群成员:
+- **@用户ID** - 通过用户ID@指定成员
+- **@手机号** - 通过手机号@指定成员
+- **@所有人** - @群内所有成员
+
+### 使用场景
+
+- ✅ 企业内部通知
+- ✅ 项目进度更新
+- ✅ 系统运维告警
+- ✅ 工作流审批提醒
+- ✅ 会议和日程通知
+
+### 注意事项
+
+::: warning 注意
+- 每个机器人每分钟最多发送 20 条消息
+- 消息内容不能包含敏感词
+- 机器人被移除后 Webhook 将失效
+- 建议使用 Markdown 格式,展示效果更好
+:::
+
+## 微信测试公众号
+
+通过微信测试公众号发送模板消息。
+
+### 配置步骤
+
+1. **申请测试公众号**
+ - 访问 [微信公众平台测试号](https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login)
+ - 使用微信扫码登录
+
+2. **获取配置信息**
+ - appID:测试号信息中的 appID
+ - appsecret:测试号信息中的 appsecret
+
+3. **添加模板消息**
+ - 在"模板消息接口"中添加模板
+ - 记录模板 ID
+
+4. **关注测试公众号**
+ - 使用微信扫描测试号二维码关注
+ - 记录用户的 OpenID
+
+5. **在 Message Nest 中配置**
+ - 填写 appID、appsecret、模板ID、OpenID
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| appID | 测试公众号 appID | 是 |
+| appsecret | 测试公众号 appsecret | 是 |
+| 模板ID | 模板消息 ID | 是 |
+| OpenID | 接收用户的 OpenID | 是 |
+
+### 使用场景
+
+- ✅ 个人项目测试
+- ✅ 小范围通知
+- ✅ 开发环境调试
+
+### 注意事项
+
+::: warning 注意
+- 测试公众号仅供开发测试使用,不能用于生产环境
+- 测试公众号有关注人数限制(100人)
+- 模板消息格式需要符合微信规范
+- 正式使用需要申请正式公众号
+:::
+
+## 自定义 Webhook
+
+向自定义的 HTTP 接口发送消息。
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| Webhook URL | 目标 HTTP 接口地址 | 是 |
+| 请求方法 | GET/POST/PUT 等 | 是 |
+| 请求头 | 自定义 HTTP 请求头 | 否 |
+| 请求体模板 | 自定义请求体格式 | 否 |
+
+### 请求体模板
+
+支持使用变量:
+- `{{title}}` - 消息标题
+- `{{text}}` - 纯文本内容
+- `{{html}}` - HTML 内容
+- `{{markdown}}` - Markdown 内容
+
+**示例:**
+
+```json
+{
+ "message": "{{title}}",
+ "content": "{{text}}",
+ "timestamp": "{{timestamp}}"
+}
+```
+
+### 使用场景
+
+- ✅ 集成第三方系统
+- ✅ 自建消息服务
+- ✅ 对接其他通知平台
+- ✅ 自定义消息处理逻辑
+
+### 注意事项
+
+::: tip 提示
+- 确保目标接口可访问
+- 注意接口的请求频率限制
+- 建议添加认证信息保证安全
+- 可以通过请求头传递 Token 等认证信息
+:::
+
+## 自托管消息
+
+将 Message Nest 站点作为消息接收平台,用户登录站点查看消息。
+
+### 核心定位
+
+**与其他渠道的区别:**
+- **邮件/钉钉/企业微信** - 推送到外部平台
+- **自托管消息** - 存储在 Message Nest 站点,用户登录站点查看
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| 渠道描述 | 渠道用途说明 | 否 |
+
+### 功能特点
+
+- ✅ 站点作为消息接收平台
+- ✅ 无需外部依赖
+- ✅ 消息集中存储和管理
+- ✅ 支持多种格式展示(Text/HTML/Markdown)
+- ✅ 支持消息搜索和筛选
+- ✅ 支持消息已读/未读状态
+
+### 使用场景
+
+- ✅ 站内消息中心
+- ✅ 系统公告发布
+- ✅ 内部工作流通知
+- ✅ 系统告警记录
+- ✅ 消息归档平台
+
+### 查看消息
+
+1. 登录 Message Nest 站点
+2. 进入"自托管消息"或"消息中心"页面
+3. 查看接收到的消息列表
+4. 点击消息查看详情
+
+### 注意事项
+
+::: tip 提示
+- 消息不会自动清理,需要手动清理
+- 建议定期清理不需要的消息,避免占用过多存储空间
+- 可以根据需要导出消息记录
+- 适合作为消息的集中查看和管理平台
+:::
+
+## 渠道管理
+
+### 创建渠道
+
+1. 登录管理后台
+2. 进入"推送渠道"页面
+3. 点击"新建渠道"
+4. 选择渠道类型
+5. 填写配置信息
+6. 保存并测试
+
+### 测试渠道
+
+创建渠道后,建议先进行测试:
+
+1. 在渠道列表中找到新建的渠道
+2. 点击"测试"按钮
+3. 发送测试消息
+4. 确认消息正常接收
+
+### 编辑和删除
+
+- **编辑**:点击渠道的"编辑"按钮,修改配置信息
+- **删除**:点击"删除"按钮,确认后删除(注意:删除后关联的任务和模板将无法使用该渠道)
+
+### 最佳实践
+
+1. **命名规范** - 使用清晰的渠道名称,便于识别
+2. **分类管理** - 按用途或环境分类(如:生产环境邮件、测试环境钉钉)
+3. **定期检查** - 定期检查渠道配置是否有效
+4. **安全管理** - 妥善保管密钥和密码信息
+5. **备用渠道** - 配置多个渠道作为备用,提高可靠性
+
+## 下一步
+
+- 查看 [发送任务](/guide/tasks) 了解如何使用渠道发送消息
+- 查看 [消息模板](/guide/template) 了解如何创建模板
+- 查看 [API 文档](/api/v1) 了解如何通过 API 发送消息
diff --git a/docs/guide/features.md b/docs/guide/features.md
index 5d13d4b..f80122f 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -1,19 +1,112 @@
+# 特色功能
-## 支持的推送方式
+Message Nest 提供了丰富的功能,帮助您轻松实现多渠道消息推送。
-- **邮件发送** - 支持标准SMTP邮件发送
-- **钉钉** - 支持钉钉机器人消息推送
-- **企业微信** - 支持企业微信应用消息推送
-- **微信测试公众号** - 支持微信测试公众号模板消息发送
-- **自定义 Webhook** - 支持自定义的Webhook消息发送
+## 核心功能
+
+### 多渠道推送
+
+支持多种主流消息推送渠道,一次配置,多处使用。
+
+- **邮件发送** - 支持标准SMTP邮件发送,适用于正式通知和账单发送
+- **钉钉** - 支持钉钉机器人消息推送,适用于团队协作和系统告警
+- **企业微信** - 支持企业微信应用消息推送,适用于企业内部通知
+- **微信测试公众号** - 支持微信测试公众号模板消息发送,适用于开发测试
+- **自定义 Webhook** - 支持自定义的Webhook消息发送,灵活对接第三方系统
- **自托管消息** - 可以将站点作为消息的接收方,登录站点查看消息
+👉 [查看推送渠道配置详细说明](/guide/channels)
+
+### 消息模板(⭐推荐)
+
+通过模板管理消息内容,支持占位符动态替换,大幅提高开发效率和维护便利性。
+
+**核心特性:**
+- ✅ 支持 Text、HTML、Markdown 三种格式
+- ✅ 占位符动态替换(`{{key}}` 语法)
+- ✅ 多实例配置,一次发送多渠道
+- ✅ @提醒功能(钉钉、企业微信)
+- ✅ 模板启用/禁用控制
+- ✅ 版本管理和灰度发布
+
+**适用场景:**
+- 用户通知(注册、登录、密码重置)
+- 订单消息(下单、支付、发货、退款)
+- 系统告警(服务异常、资源不足)
+- 营销推广(活动通知、优惠券)
+
+👉 [查看消息模板详细说明](/guide/template)
+
+### 发送任务
+
+基于任务的消息发送方式,适用于完全动态的消息内容。
+
+**核心特性:**
+- ✅ 配置多个推送渠道
+- ✅ 支持多种消息格式
+- ✅ 获取 API Token
+- ✅ 查看发送日志
+
+**说明:** 发送任务主要用于兼容历史数据,新项目推荐使用消息模板。
+
+👉 [查看发送任务详细说明](/guide/tasks)
+
+### 定时消息
+
+自动化的定时消息发送功能,使用 Cron 表达式设置执行时间。
+
+**核心特性:**
+- ✅ 使用 Cron 表达式设置执行时间
+- ✅ 关联发信任务进行发送
+- ✅ 启用/禁用定时任务
+- ✅ 立即执行测试
+- ✅ 查看执行日志
+
+**适用场景:**
+- 每日报表推送
+- 周会/月会提醒
+- 定期系统巡检
+- 月度账单推送
+
+👉 [查看定时消息详细说明](/guide/scheduled-messages)
+
+### 自托管消息
+
+将 Message Nest 站点作为消息接收平台,用户登录站点查看消息。
+
+**核心定位:**
+- 💡 站点作为消息接收和展示平台
+- 💡 用户登录站点查看消息
+- 💡 不推送到外部渠道
+
+**核心特性:**
+- ✅ 站点作为消息中心
+- ✅ 搜索消息
+- ✅ 查看消息详情
+
+**适用场景:**
+- 站内消息中心
+- 内部工作流通知平台
+- 系统告警记录平台
+- 消息归档平台
+
+👉 [查看自托管消息详细说明](/guide/self-hosted-messages)
+
+### 系统设置
+
+灵活的系统配置和管理功能。
+
+**配置项:**
+- ✅ 站点设置(标题、标语、Logo、分页、Cookie 过期天数)
+- ✅ 重置密码(修改当前用户密码)
+- ✅ 日志清理(定时清理、保留条数)
+- ✅ 登录日志(查看登录历史记录)
+- ✅ 站点关于(版本信息、系统状态)
+
+👉 [查看系统设置详细说明](/guide/settings)
+
## 其他功能
-### 定时任务
-
-支持自定义的定时消息发送,可以设置定时推送任务。
-
### 数据统计
支持数据统计展示,可以查看消息发送情况和历史记录。
@@ -28,19 +121,6 @@
- 支持查看定时清理日志
- 支持登录日志记录
-### 用户管理
-
-- 支持用户密码设置
-- 支持用户定时任务清理
-- 支持更新定时时间
-
-### 系统信息
-
-- 支持系统信息展示
-- 支持站点信息自定义
-- 支持明暗主题切换设置
-- 支持Cookies过期天数设置
-
### 数据库支持
- **SQLite** - 轻量级部署,无需额外数据库服务
diff --git a/docs/guide/scheduled-messages.md b/docs/guide/scheduled-messages.md
new file mode 100644
index 0000000..ebb6aef
--- /dev/null
+++ b/docs/guide/scheduled-messages.md
@@ -0,0 +1,297 @@
+# 定时消息
+
+定时消息功能允许您设置定时发送的消息任务,系统会按照设定的 Cron 表达式自动执行发送。
+
+## 功能概述
+
+定时消息允许您:
+- ✅ 创建定时发送任务
+- ✅ 使用 Cron 表达式设置执行时间
+- ✅ 关联发信任务进行发送
+- ✅ 启用/禁用定时任务
+- ✅ 立即执行定时任务
+- ✅ 查看执行日志
+
+## 创建定时消息
+
+### 步骤
+
+1. 进入"定时消息"页面
+2. 点击"新增定时消息"按钮
+3. 填写定时消息信息
+4. 点击"创建定时消息"保存
+
+### 配置项
+
+| 字段 | 说明 | 必填 |
+|------|------|------|
+| 定时消息名称 | 定时任务的名称 | 是 |
+| 关联发信任务 | 选择要使用的发信任务 | 是 |
+| Cron 表达式 | 定时执行的时间规则 | 是 |
+| 标题 | 消息标题 | 是 |
+| 内容 | 消息内容 | 是 |
+| url | 可选的 URL 链接 | 否 |
+
+::: warning 重要提示
+请确保所选的发信任务已配置至少一个发送实例,否则无法发送消息。
+:::
+
+### Cron 表达式
+
+Cron 表达式用于设置定时任务的执行时间,格式为:`分 时 日 月 周`
+
+#### 常用模板
+
+系统提供了常用的 Cron 表达式模板,点击即可应用:
+
+| 模板 | Cron 表达式 | 说明 |
+|------|------------|------|
+| 每分钟 | `* * * * *` | 每分钟执行一次 |
+| 每5分钟 | `*/5 * * * *` | 每5分钟执行一次 |
+| 每小时 | `0 * * * *` | 每小时整点执行 |
+| 每天凌晨2点 | `0 2 * * *` | 每天凌晨2点执行 |
+| 每周一凌晨2点 | `0 2 * * 1` | 每周一凌晨2点执行 |
+| 每月1号凌晨2点 | `0 2 1 * *` | 每月1号凌晨2点执行 |
+
+#### Cron 表达式格式
+
+```
+* * * * *
+│ │ │ │ │
+│ │ │ │ └─ 星期 (0-7, 0和7都表示周日)
+│ │ │ └─── 月份 (1-12)
+│ │ └───── 日期 (1-31)
+│ └─────── 小时 (0-23)
+└───────── 分钟 (0-59)
+```
+
+#### 特殊字符
+
+- `*` - 匹配任意值
+- `,` - 列举多个值,如 `1,3,5`
+- `-` - 范围,如 `1-5`
+- `/` - 步长,如 `*/5` 表示每5个单位
+
+#### 示例
+
+```
+0 9 * * * # 每天上午9点
+0 9-17 * * * # 每天9点到17点的整点
+0 9,12,18 * * * # 每天9点、12点、18点
+*/30 9-17 * * * # 每天9点到17点,每30分钟
+0 9 * * 1-5 # 周一到周五的上午9点
+0 0 1,15 * * # 每月1号和15号的凌晨
+```
+
+## 管理定时消息
+
+### 查看定时消息列表
+
+在"定时消息"页面可以看到所有定时消息:
+
+| 列 | 说明 |
+|----|------|
+| ID | 定时消息的唯一标识 |
+| 名称 | 定时消息名称(可点击查看完整内容) |
+| 内容 | 消息内容(可点击查看完整内容) |
+| Cron表达式 | 定时执行的时间规则 |
+| 关联任务 | 关联的发信任务 ID |
+| 下次执行时间 | 下一次计划执行的时间 |
+| 创建时间 | 定时消息创建时间 |
+| 操作 | 操作按钮和启用/禁用开关 |
+
+### 定时消息操作
+
+#### 查看日志
+
+1. 点击定时消息的"日志"按钮
+2. 查看该定时消息的执行记录
+3. 可以查看发送成功/失败的详细信息
+
+#### 编辑定时消息
+
+1. 点击定时消息的"编辑"按钮
+2. 在弹出的对话框中修改信息
+3. 可以点击"立即发送"测试发送
+4. 点击"更新定时消息"保存修改
+
+#### 删除定时消息
+
+1. 点击定时消息的"删除"按钮
+2. 确认删除操作
+3. 定时消息将被删除,不再执行
+
+#### 启用/禁用定时消息
+
+- 点击定时消息行的开关按钮
+- 禁用的定时消息不会自动执行
+- 可以随时重新启用
+
+#### 立即发送
+
+在编辑定时消息时,可以点击"立即发送"按钮立即执行一次发送,用于测试定时消息配置是否正确。
+
+### 搜索定时消息
+
+在定时消息列表页面的搜索框中输入任务名称,可以快速筛选定时消息。
+
+## 使用场景
+
+### 场景 1:每日报表推送
+
+**需求:** 每天早上9点自动发送前一天的业务报表
+
+**配置:**
+- 定时消息名称:每日业务报表
+- 关联发信任务:报表推送任务
+- Cron 表达式:`0 9 * * *`
+- 标题:每日业务报表
+- 内容:前一天的业务数据统计
+
+### 场景 2:周会提醒
+
+**需求:** 每周一上午9点提醒团队周会
+
+**配置:**
+- 定时消息名称:周会提醒
+- 关联发信任务:团队通知任务
+- Cron 表达式:`0 9 * * 1`
+- 标题:周会提醒
+- 内容:今天上午10点周会,请准时参加
+
+### 场景 3:系统巡检
+
+**需求:** 每小时检查系统状态并发送报告
+
+**配置:**
+- 定时消息名称:系统巡检
+- 关联发信任务:系统告警任务
+- Cron 表达式:`0 * * * *`
+- 标题:系统巡检报告
+- 内容:系统运行正常
+
+### 场景 4:月度账单
+
+**需求:** 每月1号凌晨发送上月账单
+
+**配置:**
+- 定时消息名称:月度账单
+- 关联发信任务:账单推送任务
+- Cron 表达式:`0 2 1 * *`
+- 标题:月度账单
+- 内容:上月账单详情
+
+## 工作流程
+
+```mermaid
+graph LR
+ A[创建定时消息] --> B[设置 Cron 表达式]
+ B --> C[关联发信任务]
+ C --> D[启用定时消息]
+ D --> E[系统定时检查]
+ E --> F{到达执行时间?}
+ F -->|是| G[调用发信任务]
+ F -->|否| E
+ G --> H[发送消息]
+ H --> I[记录日志]
+ I --> E
+```
+
+## 最佳实践
+
+### 1. Cron 表达式设置
+
+- ✅ 使用系统提供的常用模板
+- ✅ 避免设置过于频繁的执行(如每分钟)
+- ✅ 选择系统低峰期执行(如凌晨)
+- ✅ 测试 Cron 表达式是否正确
+
+### 2. 发信任务配置
+
+- ✅ 确保关联的发信任务已配置实例
+- ✅ 测试发信任务是否能正常发送
+- ✅ 为重要消息配置多个渠道
+- ✅ 使用有意义的任务名称
+
+### 3. 消息内容
+
+- ✅ 标题简洁明了
+- ✅ 内容清晰完整
+- ✅ 提供必要的链接(url 字段)
+- ✅ 使用立即发送测试内容
+
+### 4. 监控和维护
+
+- ✅ 定期查看执行日志
+- ✅ 及时处理发送失败的情况
+- ✅ 禁用不再需要的定时消息
+- ✅ 定期检查下次执行时间是否正确
+
+### 5. 性能优化
+
+- ✅ 避免在同一时间执行大量定时任务
+- ✅ 合理设置执行频率
+- ✅ 对于大量消息,考虑分批发送
+- ✅ 监控系统资源使用情况
+
+## 常见问题
+
+### Q: 定时消息没有执行怎么办?
+
+**A:**
+1. 检查定时消息是否已启用(开关是否打开)
+2. 检查 Cron 表达式是否正确
+3. 查看下次执行时间是否符合预期
+4. 检查关联的发信任务是否配置了实例
+5. 查看日志是否有错误信息
+
+### Q: 如何测试定时消息配置是否正确?
+
+**A:**
+1. 编辑定时消息
+2. 点击"立即发送"按钮
+3. 查看是否成功发送
+4. 检查日志中的发送记录
+
+### Q: Cron 表达式如何设置?
+
+**A:**
+1. 使用系统提供的常用模板
+2. 参考 Cron 表达式格式说明
+3. 使用在线 Cron 表达式生成器
+4. 通过立即发送测试是否正确
+
+### Q: 定时消息发送失败怎么办?
+
+**A:**
+1. 查看日志中的错误信息
+2. 检查关联的发信任务配置
+3. 确认发信任务的渠道是否正常
+4. 检查消息内容是否符合要求
+5. 尝试立即发送测试
+
+### Q: 可以暂停定时消息吗?
+
+**A:**
+可以。点击定时消息行的开关按钮禁用,定时消息将不再自动执行。需要时可以重新启用。
+
+### Q: 定时消息和发信任务有什么关系?
+
+**A:**
+定时消息需要关联一个发信任务,定时消息只负责定时触发,实际的消息发送由关联的发信任务完成。发信任务决定了消息发送到哪些渠道。
+
+## 注意事项
+
+::: warning 重要提示
+1. **关联任务配置** - 确保关联的发信任务已配置至少一个发送实例
+2. **Cron 表达式** - 设置前请仔细检查,避免执行时间错误
+3. **执行频率** - 避免设置过于频繁的执行,影响系统性能
+4. **消息内容** - 定时消息的内容是固定的,不支持动态内容
+5. **启用状态** - 创建后记得启用定时消息,否则不会执行
+:::
+
+## 下一步
+
+- 查看 [发送任务](/guide/tasks) 了解如何配置发信任务
+- 查看 [V1 API 文档](/api/v1) 了解消息发送机制
+- 查看 [渠道配置](/guide/channels) 了解如何配置推送渠道
diff --git a/docs/guide/self-hosted-messages-old.md b/docs/guide/self-hosted-messages-old.md
new file mode 100644
index 0000000..e0eaf23
--- /dev/null
+++ b/docs/guide/self-hosted-messages-old.md
@@ -0,0 +1,480 @@
+# 自托管消息
+
+自托管消息功能将 Message Nest 作为消息接收平台,用户登录站点查看消息。
+
+## 功能概述
+
+**核心定位:** 将 Message Nest 站点作为消息接收和展示平台,而不是推送到外部渠道。
+
+**主要功能:**
+- ✅ 站点作为消息接收平台
+- ✅ 用户登录后台查看消息
+- ✅ 搜索消息
+- ✅ 查看消息详情
+
+**与其他渠道的区别:**
+- **邮件/钉钉/企业微信** - 推送到外部平台,用户在对应平台查看
+- **自托管消息** - 存储在 Message Nest 站点,用户登录站点查看
+
+## 配置自托管消息渠道
+
+### 创建渠道
+
+1. 登录管理后台
+2. 进入"推送渠道"页面
+3. 点击"新建渠道"
+4. 选择渠道类型:"自托管消息"
+5. 填写配置信息
+
+### 配置参数
+
+| 参数 | 说明 | 必填 | 默认值 |
+|------|------|------|--------|
+| 渠道名称 | 自定义渠道名称 | 是 | - |
+| 渠道描述 | 渠道用途说明 | 否 | - |
+
+### 配置示例
+
+```
+渠道名称:站内通知
+渠道描述:用户站内消息通知
+```
+
+## 使用自托管消息
+
+### 方式一:通过任务发送
+
+1. 创建发送任务
+2. 添加自托管消息渠道实例
+3. 通过 V1 API 发送消息
+
+**API 示例:**
+
+```bash
+curl -X POST http://your-domain/api/v1/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "your_task_token",
+ "title": "系统通知",
+ "text": "您有一条新消息",
+ "html": "系统通知
您有一条新消息
",
+ "markdown": "## 系统通知\n\n您有一条新消息"
+ }'
+```
+
+### 方式二:通过模板发送(推荐)
+
+1. 创建消息模板
+2. 为模板添加自托管消息实例
+3. 通过 V2 API 发送消息
+
+**API 示例:**
+
+```bash
+curl -X POST http://your-domain/api/v2/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "your_template_token",
+ "title": "订单通知",
+ "placeholders": {
+ "order_id": "20241206001",
+ "status": "已发货"
+ }
+ }'
+```
+
+## 查看消息
+
+### 消息列表
+
+1. 登录管理后台
+2. 进入"自托管消息"或"消息中心"页面
+3. 查看消息列表
+
+**列表信息:**
+
+| 列 | 说明 |
+|----|------|
+| 状态 | 已读/未读标识 |
+| 标题 | 消息标题 |
+| 来源 | 发送任务或模板名称 |
+| 接收时间 | 消息接收时间 |
+| 操作 | 查看详情、标记已读、删除 |
+
+### 消息筛选
+
+支持多种筛选条件:
+
+- **状态筛选** - 全部/未读/已读
+- **时间筛选** - 今天/最近7天/最近30天/自定义
+- **来源筛选** - 按任务或模板筛选
+- **关键词搜索** - 搜索标题或内容
+
+**筛选示例:**
+
+```
+状态:未读
+时间:最近7天
+来源:订单通知模板
+关键词:发货
+```
+
+### 查看详情
+
+点击消息标题或"查看"按钮查看消息详情:
+
+**详情页面包含:**
+- 消息标题
+- 发送时间
+- 来源信息
+- 消息内容(根据格式展示)
+- 操作按钮(标记已读、删除)
+
+**内容展示:**
+- **Text** - 纯文本展示
+- **HTML** - 富文本渲染
+- **Markdown** - Markdown 渲染
+
+### 消息操作
+
+#### 标记已读/未读
+
+- **单个标记** - 点击消息的"标记已读"按钮
+- **批量标记** - 选择多条消息,点击"批量标记已读"
+- **全部已读** - 点击"全部标记为已读"
+
+#### 删除消息
+
+- **单个删除** - 点击消息的"删除"按钮
+- **批量删除** - 选择多条消息,点击"批量删除"
+- **清空消息** - 点击"清空全部消息"(谨慎操作)
+
+::: warning 注意
+删除操作不可恢复,请谨慎操作!
+:::
+
+## 消息格式
+
+### Text 格式
+
+纯文本格式,适用于简单通知。
+
+**示例:**
+
+```text
+您的订单 20241206001 已发货。
+
+快递公司:顺丰速运
+快递单号:SF1234567890
+预计送达:2024-12-08
+
+如有问题请联系客服。
+```
+
+**展示效果:** 保持原始换行和格式
+
+### HTML 格式
+
+HTML 格式,支持富文本样式。
+
+**示例:**
+
+```html
+
+
订单发货通知
+
您的订单 20241206001 已发货。
+
+
+ | 快递公司: |
+ 顺丰速运 |
+
+
+ | 快递单号: |
+ SF1234567890 |
+
+
+
如有问题请联系客服。
+
+```
+
+**展示效果:** 完整的 HTML 渲染,支持样式和布局
+
+### Markdown 格式
+
+Markdown 格式,支持格式化文本。
+
+**示例:**
+
+```markdown
+## 订单发货通知
+
+您的订单 **20241206001** 已发货。
+
+| 项目 | 信息 |
+|------|------|
+| 快递公司 | 顺丰速运 |
+| 快递单号 | SF1234567890 |
+| 预计送达 | 2024-12-08 |
+
+> 如有问题请联系客服。
+```
+
+**展示效果:** Markdown 渲染,支持标题、表格、引用等
+
+## 使用场景
+
+### 场景 1:站内消息中心
+
+**需求:** 将 Message Nest 作为网站/应用的消息中心
+
+**实现方案:**
+
+1. 创建"用户通知"模板
+2. 添加自托管消息渠道实例
+3. 在用户操作时调用 API 发送消息到自托管渠道
+4. 用户登录 Message Nest 站点查看消息
+
+**典型应用:**
+- 系统公告发布
+- 账号安全提醒
+- 订单状态更新
+- 活动通知推送
+
+**优势:**
+- 无需开发消息中心功能
+- 直接使用 Message Nest 作为消息平台
+- 用户登录即可查看所有消息
+
+### 场景 2:内部工作流通知
+
+**需求:** 团队内部的工作流通知和待办事项集中管理
+
+**实现方案:**
+
+1. 创建不同类型的通知模板(审批、任务、提醒)
+2. 配置自托管消息渠道
+3. 工作流触发时发送消息到自托管渠道
+4. 团队成员登录 Message Nest 站点查看待办
+
+**典型应用:**
+- 审批请求通知
+- 任务分配提醒
+- 会议提醒
+- 进度更新通知
+
+**优势:**
+- 所有工作通知集中在一个平台
+- 团队成员统一登录查看
+- 避免消息分散在多个渠道
+
+### 场景 3:系统告警记录平台
+
+**需求:** 将 Message Nest 作为系统告警的集中查看平台
+
+**实现方案:**
+
+1. 创建"系统告警"模板
+2. 配置自托管消息渠道(同时可配置钉钉/邮件用于即时通知)
+3. 系统监控触发告警时发送到自托管渠道
+4. 管理员登录 Message Nest 站点查看和管理告警
+
+**典型应用:**
+- 服务异常告警
+- 资源使用告警
+- 安全事件告警
+- 性能告警
+
+**优势:**
+- 所有告警集中存储在平台
+- 便于历史告警查询和分析
+- 支持搜索和筛选功能
+- 可导出告警数据
+
+### 场景 4:消息归档平台
+
+**需求:** 将 Message Nest 作为所有消息的归档和查询平台
+
+**实现方案:**
+
+1. 为所有任务/模板添加自托管消息渠道实例
+2. 所有消息都会存储在 Message Nest 站点
+3. 用户登录站点查看历史消息
+4. 定期导出归档数据
+
+**典型应用:**
+- 所有发送消息的完整记录
+- 消息审计和追溯
+- 历史消息查询
+- 数据分析和统计
+
+**优势:**
+- Message Nest 成为消息归档中心
+- 所有消息集中存储和管理
+- 支持强大的搜索和筛选
+- 便于导出和分析
+
+## 消息管理
+
+### 手动清理
+
+可以手动清理消息以释放存储空间。
+
+**清理选项:**
+
+- **清理已读消息** - 删除所有已读消息
+- **清理过期消息** - 删除指定天数前的消息
+- **清空全部消息** - 删除所有消息(谨慎操作)
+
+**步骤:**
+
+1. 进入"自托管消息"页面
+2. 点击"清理"按钮
+3. 选择清理选项
+4. 确认操作
+5. 等待清理完成
+
+### 消息导出
+
+导出消息数据用于备份或分析。
+
+**导出格式:**
+- CSV - 适合 Excel 分析
+- JSON - 适合程序处理
+- PDF - 适合打印存档
+
+**导出步骤:**
+
+1. 进入"自托管消息"页面
+2. 设置筛选条件(可选)
+3. 点击"导出"按钮
+4. 选择导出格式
+5. 下载导出文件
+
+## 消息统计
+
+查看消息的统计数据和趋势。
+
+### 统计指标
+
+| 指标 | 说明 |
+|------|------|
+| 总消息数 | 累计接收的消息总数 |
+| 未读消息 | 当前未读消息数量 |
+| 今日新增 | 今天接收的消息数 |
+| 平均响应时间 | 从接收到已读的平均时间 |
+| 消息来源分布 | 各任务/模板的消息占比 |
+
+### 趋势图表
+
+- **消息趋势** - 最近 30 天的消息接收趋势
+- **来源分布** - 各来源的消息数量饼图
+- **已读率** - 消息已读率变化趋势
+
+### 查看统计
+
+1. 进入"自托管消息"页面
+2. 点击"统计"按钮
+3. 查看各项统计数据
+4. 可导出统计报表
+
+## 最佳实践
+
+### 1. 消息分类
+
+- **按重要性分类** - 重要/一般/提示
+- **按类型分类** - 通知/告警/提醒
+- **使用不同模板** - 便于筛选和管理
+
+### 2. 内容设计
+
+- **标题简洁明了** - 让用户快速了解消息内容
+- **内容结构清晰** - 使用标题、列表、表格等
+- **提供操作入口** - 添加相关链接或按钮
+- **适配多种格式** - 提供 Text/HTML/Markdown
+
+### 3. 存储管理
+
+- **定期手动清理** - 定期清理已读或过期消息,释放存储空间
+- **重要消息导出备份** - 避免数据丢失
+- **监控存储使用情况** - 及时清理不需要的消息
+
+### 4. 用户体验
+
+- **及时标记已读** - 保持消息列表整洁
+- **使用筛选功能** - 快速找到需要的消息
+- **定期查看消息** - 避免遗漏重要通知
+- **合理设置通知** - 避免消息过载
+
+### 5. 安全性
+
+- **权限控制** - 确保只有授权用户可以查看
+- **敏感信息加密** - 对敏感内容进行加密
+- **定期审计** - 检查消息访问日志
+- **数据备份** - 定期备份重要消息
+
+## 与其他渠道对比
+
+| 特性 | 自托管消息 | 邮件 | 钉钉/企业微信 |
+|------|-----------|------|--------------|
+| 实时性 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
+| 到达率 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
+| 格式支持 | Text/HTML/Markdown | Text/HTML | Text/Markdown |
+| 历史查询 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
+| 成本 | 免费 | 免费/付费 | 免费 |
+| 依赖 | 无 | SMTP 服务器 | 钉钉/企业微信 |
+| 适用场景 | 站内通知、归档 | 正式通知、账单 | 即时通知、协作 |
+
+## 常见问题
+
+### Q: 自托管消息和其他渠道有什么区别?
+
+**A:**
+- **自托管消息** - 存储在系统内,需要登录后台查看
+- **其他渠道** - 推送到外部平台(邮件、钉钉等),用户在对应平台查看
+
+### Q: 可以同时使用自托管消息和其他渠道吗?
+
+**A:** 可以。为任务或模板同时配置多个渠道实例,消息会发送到所有渠道。
+
+### Q: 消息会永久保存吗?
+
+**A:** 消息会一直保存,不会自动清理。建议定期手动清理不需要的消息,并导出重要消息备份。
+
+### Q: 如何提高消息的查看率?
+
+**A:**
+1. 配合其他渠道(如邮件、钉钉)提醒用户
+2. 在应用中显示未读消息数量
+3. 提供消息通知功能
+4. 定期提醒用户查看
+
+### Q: 消息被误删了怎么办?
+
+**A:**
+- 如果有备份,可以从备份恢复
+- 如果配置了其他渠道,可以从其他渠道查看
+- 建议定期导出重要消息
+
+### Q: 如何在应用中集成消息中心?
+
+**A:**
+1. 调用 API 发送消息到自托管渠道
+2. 提供登录入口到 Message Nest 后台
+3. 或者通过 API 查询消息列表(需要开发)
+4. 在应用中展示消息
+
+## 注意事项
+
+::: warning 重要提示
+1. **存储空间** - 消息不会自动清理,注意定期手动清理,避免占用过多存储空间
+2. **性能影响** - 大量消息可能影响查询性能,建议定期清理已读或过期消息
+3. **权限控制** - 确保消息只能被授权用户查看
+4. **数据备份** - 重要消息建议定期导出备份
+5. **手动清理** - 删除操作不可恢复,请谨慎操作
+:::
+
+## 下一步
+
+- 查看 [渠道配置](/guide/channels) 了解如何配置自托管消息渠道
+- 查看 [消息模板](/guide/template) 了解如何创建模板
+- 查看 [V2 API 文档](/api/v2) 了解如何发送消息
diff --git a/docs/guide/self-hosted-messages.md b/docs/guide/self-hosted-messages.md
new file mode 100644
index 0000000..953ac88
--- /dev/null
+++ b/docs/guide/self-hosted-messages.md
@@ -0,0 +1,151 @@
+# 自托管消息
+
+自托管消息功能将 Message Nest 作为消息接收平台,用户登录站点查看消息。
+
+## 功能概述
+
+**核心定位:** 将 Message Nest 站点作为消息接收和展示平台,而不是推送到外部渠道。
+
+**主要功能:**
+- ✅ 站点作为消息接收平台
+- ✅ 用户登录后台查看消息
+- ✅ 搜索消息
+- ✅ 查看消息详情
+
+**与其他渠道的区别:**
+- **邮件/钉钉/企业微信** - 推送到外部平台,用户在对应平台查看
+- **自托管消息** - 存储在 Message Nest 站点,用户登录站点查看
+
+## 配置自托管消息渠道
+
+### 创建渠道
+
+1. 登录管理后台
+2. 进入"推送渠道"页面
+3. 点击"新建渠道"
+4. 选择渠道类型:"自托管消息"
+5. 填写渠道名称和描述
+6. 保存渠道
+
+### 配置参数
+
+| 参数 | 说明 | 必填 |
+|------|------|------|
+| 渠道名称 | 自定义渠道名称 | 是 |
+| 渠道描述 | 渠道用途说明 | 否 |
+
+## 使用自托管消息
+
+### 方式一:通过任务发送
+
+1. 创建发送任务
+2. 添加自托管消息渠道实例
+3. 通过 V1 API 发送消息
+
+**API 示例:**
+
+```bash
+curl -X POST http://your-domain/api/v1/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "your_task_token",
+ "title": "系统通知",
+ "text": "您有一条新消息"
+ }'
+```
+
+### 方式二:通过模板发送(推荐)
+
+1. 创建消息模板
+2. 为模板添加自托管消息实例
+3. 通过 V2 API 发送消息
+
+**API 示例:**
+
+```bash
+curl -X POST http://your-domain/api/v2/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "your_template_token",
+ "title": "订单通知",
+ "placeholders": {
+ "order_id": "20241206001"
+ }
+ }'
+```
+
+## 查看消息
+
+### 消息列表
+
+1. 登录管理后台
+2. 进入"托管消息"页面
+3. 查看消息列表
+
+**列表信息:**
+
+| 列 | 说明 |
+|----|------|
+| ID | 消息 ID |
+| 消息标题 | 消息标题(可点击查看完整内容) |
+| 消息内容 | 消息内容(可点击查看完整内容) |
+| 创建时间 | 消息接收时间 |
+| 详情 | 查看按钮 |
+
+### 搜索消息
+
+在消息列表页面的搜索框中输入关键词,可以搜索消息标题和内容。
+
+### 查看详情
+
+点击消息行的"查看"按钮,在侧边栏中查看消息完整信息:
+
+- 标题
+- 内容
+- URL(如果有)
+- 创建时间
+- 修改时间
+
+## 使用场景
+
+### 场景 1:站内消息中心
+
+将 Message Nest 作为网站/应用的消息中心,用户登录站点查看消息。
+
+**适用于:**
+- 系统公告
+- 用户通知
+- 订单状态更新
+
+### 场景 2:内部通知平台
+
+团队内部的工作通知集中管理,成员登录站点查看。
+
+**适用于:**
+- 审批通知
+- 任务分配
+- 会议提醒
+
+### 场景 3:消息归档
+
+所有发送的消息都存储在站点,便于查询和追溯。
+
+**适用于:**
+- 消息记录
+- 审计追溯
+- 历史查询
+
+## 注意事项
+
+::: warning 重要提示
+1. **存储空间** - 消息不会自动清理,注意定期手动清理
+2. **查看方式** - 用户需要登录 Message Nest 站点才能查看消息
+3. **消息格式** - 支持 Text、HTML、Markdown 格式
+4. **权限控制** - 确保只有授权用户可以登录查看
+:::
+
+## 下一步
+
+- 查看 [渠道配置](/guide/channels) 了解如何配置自托管消息渠道
+- 查看 [消息模板](/guide/template) 了解如何创建模板
+- 查看 [V2 API 文档](/api/v2) 了解如何发送消息
diff --git a/docs/guide/settings.md b/docs/guide/settings.md
new file mode 100644
index 0000000..df4629c
--- /dev/null
+++ b/docs/guide/settings.md
@@ -0,0 +1,217 @@
+# 系统设置
+
+系统设置页面允许您配置 Message Nest 的各项参数,包括站点信息、日志清理、密码管理等。
+
+## 站点设置
+
+自定义站点的基本信息和显示参数。
+
+### 配置项
+
+| 设置项 | 说明 | 默认值 |
+|--------|------|--------|
+| 站点标题 | 显示在浏览器标题和页面顶部 | `Message Nest` |
+| 站点标语 | 显示在登录页面的标语 | `消息推送整合平台` |
+| 站点图标 | 网站 Logo(仅支持 SVG 文本) | 默认 Logo |
+| 分页大小 | 列表页面每页显示的数据条数 | `10` |
+| Cookie 过期天数 | 用户登录状态保持时间(天) | `1` |
+
+### 使用场景
+
+- ✅ 企业内部部署,使用企业品牌
+- ✅ 个性化定制,提升用户体验
+- ✅ 统一品牌形象
+
+### 配置步骤
+
+1. 登录管理后台
+2. 进入"系统设置" → "站点设置"
+3. 填写或修改相关信息
+4. 保存设置
+5. 下次登录时生效(如不生效,在登录页面 Ctrl+F5 强制刷新)
+
+::: tip 提示
+- **站点图标**:仅支持 SVG 文本格式,将替换网页 ico、登录页面 logo、导航栏 logo
+- **站点标语**:将在登录页面展示
+- **Cookie 过期天数**:设置用户登录后的有效期,修改后下次登录时生效
+- **分页大小**:影响所有列表页面的显示数量
+:::
+
+## 重置密码
+
+修改当前用户的登录密码。
+
+### 配置步骤
+
+1. 进入"系统设置" → "重置密码"
+2. 输入当前密码
+3. 输入新密码
+4. 确认新密码
+5. 点击"确定"保存
+
+::: warning 安全建议
+- 使用强密码(包含大小写字母、数字、特殊字符)
+- 定期更换密码(建议 3 个月)
+- 不要使用常见密码
+- 不要与其他系统使用相同密码
+:::
+
+## 日志清理
+
+配置定时日志清除和保留策略。
+
+### 配置项
+
+| 设置项 | 说明 | 默认值 |
+|--------|------|--------|
+| 定时清除 Cron 表达式 | 定时清理日志的时间规则 | `0 1 * * *`(每天凌晨 1 点) |
+| 保留日志条数 | 保留最近的日志数量 | `1000` |
+
+### 配置步骤
+
+1. 进入"系统设置" → "日志清理"
+2. 设置 Cron 表达式(可选)
+3. 设置保留日志条数(可选)
+4. 点击"确定"保存
+5. 点击"查看日志"可以查看清理日志
+
+::: tip 提示
+- **Cron 表达式**:如果不设置,默认是在每天的 0 点 1 分进行清理
+- **保留数目**:如果不设置,默认保留最近 1000 条
+- 清理任务会自动执行,删除超出保留数量的旧日志
+:::
+
+### Cron 表达式示例
+
+```
+0 1 * * * # 每天凌晨 1 点
+0 */6 * * * # 每 6 小时
+0 0 * * 0 # 每周日凌晨
+0 2 1 * * # 每月 1 号凌晨 2 点
+```
+
+## 登录日志
+
+查看系统的登录历史记录。
+
+### 查看方式
+
+1. 进入"系统设置" → "登录日志"
+2. 查看登录记录列表
+
+### 记录内容
+
+- 登录时间
+- 登录 IP 地址
+- 登录状态(成功/失败)
+
+### 使用场景
+
+- ✅ 安全审计
+- ✅ 异常登录检测
+- ✅ 用户行为分析
+
+## 站点关于
+
+查看系统的版本信息和运行状态。
+
+### 系统信息
+
+| 信息项 | 说明 |
+|--------|------|
+| 系统版本 | Message Nest 版本号 |
+| 构建时间 | 系统构建时间 |
+| 内存使用 | 当前内存使用情况 |
+| 运行时间 | 系统已运行时长 |
+
+### 技术栈
+
+- Golang
+- Vue 3
+- TypeScript
+- Vite
+- Tailwind CSS
+- Shadcn/ui
+
+### 功能特性
+
+- 多渠道消息推送
+- 定时消息管理
+- 托管消息服务
+- 发信日志追踪
+- 渠道配置管理
+- 站点信息配置
+
+### 版本日志
+
+点击"查看更新日志"按钮可以查看系统的版本更新历史。
+
+### 查看方式
+
+1. 进入"系统设置" → "站点关于"
+2. 查看系统信息和技术栈
+3. 点击"查看更新日志"查看版本历史
+4. 点击"GitHub 仓库"访问项目主页
+
+## 最佳实践
+
+### 1. 安全配置
+
+- ✅ 使用强密码
+- ✅ 定期更换密码(建议 3 个月)
+- ✅ 定期查看登录日志,检查异常登录
+- ✅ 合理设置 Cookie 过期天数
+
+### 2. 日志管理
+
+- ✅ 合理设置日志保留条数,避免占用过多存储空间
+- ✅ 根据业务需求调整清理时间(避开高峰期)
+- ✅ 定期查看清理日志,确认清理正常执行
+
+### 3. 站点配置
+
+- ✅ 自定义站点标题和标语,提升品牌形象
+- ✅ 使用 SVG 格式的 Logo,保证清晰度
+- ✅ 合理设置分页大小,平衡性能和用户体验
+- ✅ 修改配置后记得刷新页面查看效果
+
+## 常见问题
+
+### Q: 修改站点信息后没有生效?
+
+**A:** 尝试以下方法:
+1. 在登录页面强制刷新(Ctrl+F5)
+2. 清除浏览器缓存
+3. 检查是否保存成功
+4. 下次登录时生效
+
+### Q: 忘记密码怎么办?
+
+**A:**
+1. 如果是 Docker 部署,可以通过环境变量重置
+2. 如果是直接运行,可以通过数据库直接修改
+3. 联系系统管理员重置
+
+### Q: 日志清理任务没有执行?
+
+**A:**
+1. 检查 Cron 表达式是否正确
+2. 查看清理日志确认执行情况
+3. 确认系统时间是否准确
+4. 检查系统日志是否有错误信息
+
+### Q: Cookie 过期天数修改后没生效?
+
+**A:**
+Cookie 过期天数的修改会在下次登录时生效,当前已登录的会话不受影响。
+
+### Q: 如何查看系统版本?
+
+**A:**
+进入"系统设置" → "站点关于",可以查看系统版本、构建时间等信息。
+
+## 下一步
+
+- 查看 [部署文档](/deployment/overview) 了解部署配置
+- 查看 [渠道配置](/guide/channels) 开始使用系统
+- 查看 [消息模板](/guide/template) 创建第一个模板
diff --git a/docs/guide/tasks.md b/docs/guide/tasks.md
new file mode 100644
index 0000000..cc176a0
--- /dev/null
+++ b/docs/guide/tasks.md
@@ -0,0 +1,341 @@
+# 发送任务
+
+发送任务是 Message Nest 的基础功能,用于配置消息发送的渠道和参数,通过 V1 API 发送消息。
+
+::: tip 💡 推荐使用模板
+对于新项目,我们推荐使用 [消息模板](/guide/template) 功能,它提供更好的内容管理和维护体验。发送任务主要用于兼容历史数据。
+:::
+
+## 功能概述
+
+发送任务允许您:
+- ✅ 创建发送任务
+- ✅ 为任务添加推送渠道实例
+- ✅ 配置每个实例的参数
+- ✅ 获取 API Token 用于发送消息
+- ✅ 查看任务的发送日志
+
+## 创建任务
+
+### 步骤
+
+1. 进入"发送任务"页面
+2. 点击"新增任务"按钮
+3. 输入任务名称
+4. 点击"保存"
+
+### 配置项
+
+| 字段 | 说明 | 必填 |
+|------|------|------|
+| 任务名称 | 任务的唯一标识名称 | 是 |
+
+::: tip 提示
+任务创建后,需要添加推送实例才能发送消息。
+:::
+
+## 添加推送实例
+
+为任务添加一个或多个推送渠道实例。
+
+### 步骤
+
+1. 在任务列表中点击"编辑"按钮
+2. 在搜索框中输入渠道名称
+3. 从下拉列表中选择已创建的推送渠道
+4. 配置实例参数(根据渠道类型不同)
+5. 选择消息格式(Text/HTML/Markdown)
+6. 点击"添加"保存实例
+
+### 实例配置说明
+
+不同渠道需要配置不同的参数:
+
+#### 邮件渠道
+
+- **收件人邮箱** - 接收邮件的邮箱地址
+- **消息格式** - Text 或 HTML
+
+#### 钉钉/企业微信
+
+- **消息格式** - Text 或 Markdown
+- **@提醒** - 可选配置@手机号或@所有人
+
+#### 自定义 Webhook
+
+- 根据 Webhook 要求配置相应参数
+
+#### 自托管消息
+
+- 无需额外配置
+- **消息格式** - Text、HTML 或 Markdown
+
+## 管理任务
+
+### 查看任务列表
+
+在"发送任务"页面可以看到所有任务:
+
+| 列 | 说明 |
+|----|------|
+| ID | 任务的唯一标识 |
+| 发信任务名称 | 任务名称 |
+| 创建时间 | 任务创建时间 |
+| 更新时间 | 任务最后修改时间 |
+| 操作/状态 | 操作按钮 |
+
+### 任务操作
+
+#### 查看接口
+
+1. 点击任务的"接口"按钮
+2. 查看 API Token 和调用示例
+3. 复制 Token 用于 V1 API 调用
+
+#### 查看日志
+
+1. 点击任务的"日志"按钮
+2. 查看该任务的发送记录
+3. 可以筛选和搜索日志
+
+#### 编辑任务
+
+1. 点击任务的"编辑"按钮
+2. 在弹出的对话框中管理推送实例
+3. 可以添加、删除、启用/禁用实例
+
+#### 删除任务
+
+1. 点击任务的"删除"按钮
+2. 确认删除操作
+3. 任务及其所有实例将被删除
+
+::: warning 注意
+删除任务后,该任务的 Token 将失效,无法再使用 V1 API 发送消息。
+:::
+
+### 管理推送实例
+
+在编辑任务对话框中可以管理推送实例:
+
+#### 查看实例列表
+
+| 列 | 说明 |
+|----|------|
+| 渠道名称 | 推送渠道的名称 |
+| 渠道类型 | 渠道类型(邮件、钉钉等) |
+| 消息格式 | Text/HTML/Markdown |
+| 状态 | 启用/禁用开关 |
+| 操作 | 删除按钮 |
+
+#### 启用/禁用实例
+
+- 点击实例行的开关按钮
+- 禁用的实例不会参与消息发送
+- 可以随时重新启用
+
+#### 删除实例
+
+- 点击实例行的"删除"按钮
+- 确认删除操作
+- 实例将从任务中移除
+
+### 搜索任务
+
+在任务列表页面的搜索框中输入任务名称,可以快速筛选任务。
+
+## 使用任务发送消息
+
+### 通过 API 调用
+
+使用 V1 API 发送消息,详见 [V1 API 文档](/api/v1)。
+
+**基本示例:**
+
+```bash
+curl -X POST http://your-domain/api/v1/message/send \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "your_task_token",
+ "title": "消息标题",
+ "text": "消息内容"
+ }'
+```
+
+### 多格式发送
+
+可以同时提供多种格式,系统会根据实例配置自动选择:
+
+```json
+{
+ "token": "your_task_token",
+ "title": "订单通知",
+ "text": "您的订单已发货",
+ "html": "订单通知
您的订单已发货
",
+ "markdown": "## 订单通知\n\n您的订单已发货"
+}
+```
+
+### @提醒功能
+
+对于支持的渠道(钉钉、企业微信),可以使用@提醒:
+
+```json
+{
+ "token": "your_task_token",
+ "title": "系统告警",
+ "text": "服务器CPU使用率过高",
+ "at_mobiles": ["13800138000"],
+ "at_all": false
+}
+```
+
+## 工作流程
+
+```mermaid
+graph LR
+ A[创建任务] --> B[添加推送实例]
+ B --> C[配置实例参数]
+ C --> D[获取 Token]
+ D --> E[调用 API]
+ E --> F[系统遍历实例]
+ F --> G[发送到各渠道]
+ G --> H[记录日志]
+```
+
+## 使用场景
+
+### 场景 1:多渠道通知
+
+创建一个任务,配置多个推送实例(邮件 + 钉钉 + 企业微信),一次 API 调用同时推送到所有渠道。
+
+**适用于:**
+- 重要系统告警
+- 关键业务通知
+- 需要多渠道触达的消息
+
+### 场景 2:不同环境隔离
+
+为开发、测试、生产环境创建不同的任务,使用不同的渠道配置。
+
+**适用于:**
+- 环境隔离
+- 测试验证
+- 灰度发布
+
+### 场景 3:按用途分类
+
+为不同用途创建不同的任务(如:用户通知、系统告警、营销推广),便于管理和统计。
+
+**适用于:**
+- 消息分类管理
+- 统计分析
+- 权限控制
+
+## 最佳实践
+
+### 1. 任务命名
+
+使用清晰的命名规范:
+- ✅ `生产-用户通知-邮件钉钉`
+- ✅ `测试-系统告警-企业微信`
+- ❌ `任务1`、`test`
+
+### 2. 实例配置
+
+- **合理选择渠道** - 根据消息重要性和紧急程度选择合适的渠道
+- **配置备用渠道** - 为重要消息配置多个渠道,提高送达率
+- **格式适配** - 为不同渠道提供合适的消息格式
+
+### 3. Token 管理
+
+- **安全存储** - 不要在代码中硬编码 Token,使用环境变量或配置文件
+- **定期轮换** - 定期更新 Token,提高安全性
+- **权限控制** - 不同环境使用不同的 Token
+
+### 4. 日志监控
+
+- **定期查看** - 定期检查发送日志,及时发现问题
+- **失败处理** - 对发送失败的消息进行重试或告警
+- **统计分析** - 分析发送数据,优化渠道配置
+
+### 5. 性能优化
+
+- **异步调用** - 使用异步方式调用 API,避免阻塞主流程
+- **批量发送** - 对于大量消息,考虑批量发送或限流
+- **错误重试** - 实现合理的重试机制
+
+## 常见问题
+
+### Q: 任务和模板有什么区别?
+
+**A:**
+- **任务(V1 API)**:内容在 API 调用时传递,适合完全动态的内容
+- **模板(V2 API)**:内容预定义在模板中,通过占位符替换,推荐使用
+
+详见 [V1 API 文档](/api/v1) 中的对比说明。
+
+### Q: 可以为一个任务配置多个实例吗?
+
+**A:** 可以。一个任务可以配置多个推送实例,API 调用时会自动遍历所有启用的实例进行发送。
+
+### Q: 如何知道消息是否发送成功?
+
+**A:**
+1. API 响应会返回发送状态
+2. 在任务的日志页面查看详细的发送记录
+3. 可以配置自托管消息渠道,在后台查看消息
+
+### Q: Token 泄露了怎么办?
+
+**A:**
+1. 立即删除该任务(Token 将失效)
+2. 创建新的任务获取新的 Token
+3. 更新使用该 Token 的所有代码
+
+### Q: 发送失败如何处理?
+
+**A:**
+1. 查看日志中的错误信息
+2. 检查渠道配置是否正确
+3. 确认渠道服务是否正常
+4. 检查消息内容是否符合渠道要求
+5. 实现重试机制
+
+## 迁移到模板
+
+如果您正在使用发送任务,我们建议迁移到消息模板:
+
+### 迁移步骤
+
+1. **创建模板**
+ - 根据现有任务的消息内容创建模板
+ - 定义占位符替换动态内容
+
+2. **配置实例**
+ - 将任务的推送实例配置复制到模板
+
+3. **更新代码**
+ - 将 V1 API 调用改为 V2 API
+ - 使用模板 Token 和占位符参数
+
+4. **测试验证**
+ - 测试新的模板发送是否正常
+ - 对比新旧方式的效果
+
+5. **切换上线**
+ - 逐步切换到模板方式
+ - 保留旧任务一段时间作为备用
+
+### 迁移优势
+
+- ✅ 内容统一管理,便于维护
+- ✅ 修改内容无需改代码
+- ✅ 支持版本控制和灰度发布
+- ✅ 更好的团队协作体验
+
+## 下一步
+
+- 查看 [推送渠道配置](/guide/channels) 了解如何配置渠道
+- 查看 [消息模板](/guide/template) 了解推荐的使用方式
+- 查看 [V1 API 文档](/api/v1) 了解 API 调用方法
diff --git a/docs/index.md b/docs/index.md
index 6018457..97333a4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -48,39 +48,3 @@ features:
title: 数据统计
details: 支持数据统计展示,查看消息发送情况。
---
-
-
diff --git a/web/src/components/pages/messageTemplate/MessageTemplate.vue b/web/src/components/pages/messageTemplate/MessageTemplate.vue
index 16e4cfa..d5718a5 100644
--- a/web/src/components/pages/messageTemplate/MessageTemplate.vue
+++ b/web/src/components/pages/messageTemplate/MessageTemplate.vue
@@ -184,7 +184,7 @@ onMounted(async () => {
-
+
diff --git a/web/src/components/pages/sendTasks/SendTasks.vue b/web/src/components/pages/sendTasks/SendTasks.vue
index d27ffe9..bc8a502 100644
--- a/web/src/components/pages/sendTasks/SendTasks.vue
+++ b/web/src/components/pages/sendTasks/SendTasks.vue
@@ -11,6 +11,7 @@ import AddTasks from './AddTasks.vue'
import EditTasks from './EditTasks.vue'
import ApiCodeViewer from './ApiCodeViewer.vue'
import { toast } from 'vue-sonner'
+import { InfoIcon, XIcon } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router';
import { request } from '@/api/api';
@@ -18,6 +19,13 @@ import { CONSTANT } from '@/constant';
// @ts-ignore
import { getPageSize } from '@/util/pageUtils';
+// 提示横幅显示状态
+const showBanner = ref(true)
+const closeBanner = () => {
+ showBanner.value = false
+ localStorage.setItem('hideTaskBanner', 'true')
+}
+
interface WayItem {
id: string
@@ -152,6 +160,12 @@ const handleDelete = async (id: string) => {
}
onMounted(async () => {
+ // 检查是否已经关闭过横幅
+ const hidden = localStorage.getItem('hideTaskBanner')
+ if (hidden === 'true') {
+ showBanner.value = false
+ }
+
// 初始化查询
state.search = route.query.name?.toString() || '';
await queryListData(
@@ -166,6 +180,29 @@ onMounted(async () => {
+
+
+
+
+
+
💡 推荐使用消息模板
+
+ 新项目建议使用
+ 消息模板
+ 功能,它提供更好的内容管理和维护体验。发送任务主要用于兼容历史数据。
+
+ 了解更多 →
+
+
+
+
+
+
+
+
+
From 8d61ace06aa97ead852e22cc74979227a33a276e Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 18:54:12 +0800
Subject: [PATCH 16/24] fix: dashboard statistic
---
models/send_tasks_logs.go | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/models/send_tasks_logs.go b/models/send_tasks_logs.go
index dd4a210..01d65c6 100644
--- a/models/send_tasks_logs.go
+++ b/models/send_tasks_logs.go
@@ -339,17 +339,18 @@ func GetTrendStatisticData() (TrendStatisticData, error) {
return statistic, nil
}
-// GetChannelStatisticData 获取渠道统计数据
+// GetChannelStatisticData 获取渠道统计数据(包含任务实例和模板实例)
func GetChannelStatisticData() (ChannelStatisticData, error) {
var statistic ChannelStatisticData
var wayCateData []WayCateData
inst := GetSchema(SendTasksIns{})
wayst := GetSchema(SendWays{})
- // 消息实例分类数目
+ // 统计所有实例的渠道分布(包含任务实例和模板实例)
+ // 不区分 task_id 和 template_id,统计所有关联到渠道的实例
db.
Table(inst).
- Select(fmt.Sprintf("%s.name as way_name, count(%s.id) as count_num", wayst, wayst)).
+ Select(fmt.Sprintf("%s.name as way_name, count(%s.id) as count_num", wayst, inst)).
Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", wayst, inst, wayst)).
Group(fmt.Sprintf("%s.id", wayst)).
Scan(&wayCateData)
From b41b0285619676e46c7b854694bafa01772e5167 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 19:55:17 +0800
Subject: [PATCH 17/24] chore: opt template code
---
.../pages/messageTemplate/MessageTemplate.vue | 4 +-
.../pages/messageTemplate/TemplateEditor.vue | 43 ++++++++-----------
.../TemplateInstanceConfig.vue | 30 +++++++++++++
3 files changed, 50 insertions(+), 27 deletions(-)
diff --git a/web/src/components/pages/messageTemplate/MessageTemplate.vue b/web/src/components/pages/messageTemplate/MessageTemplate.vue
index d5718a5..d15bf7d 100644
--- a/web/src/components/pages/messageTemplate/MessageTemplate.vue
+++ b/web/src/components/pages/messageTemplate/MessageTemplate.vue
@@ -314,7 +314,7 @@ onMounted(async () => {
@@ -323,6 +323,8 @@ onMounted(async () => {
v-model="previewData.params[key]"
:placeholder="`请输入 ${key}`"
@input="refreshPreview"
+ :autofocus="false"
+ :tabindex="index + 1"
/>
diff --git a/web/src/components/pages/messageTemplate/TemplateEditor.vue b/web/src/components/pages/messageTemplate/TemplateEditor.vue
index 9727ca0..c12ef2a 100644
--- a/web/src/components/pages/messageTemplate/TemplateEditor.vue
+++ b/web/src/components/pages/messageTemplate/TemplateEditor.vue
@@ -263,37 +263,28 @@ watch(() => props.open, (newVal) => {
-
-
-
+
+
-
- 配置后,使用此模板发送消息时会自动@指定的用户(适用于支持@功能的渠道,如钉钉、企业微信等)
-
@@ -326,7 +317,7 @@ watch(() => props.open, (newVal) => {
ref="textTemplateRef"
v-model="formData.text_template"
placeholder="请输入纯文本模板内容,可使用 {{key}} 作为占位符"
- rows="10"
+ rows="15"
/>
@@ -352,7 +343,7 @@ watch(() => props.open, (newVal) => {
ref="htmlTemplateRef"
v-model="formData.html_template"
placeholder="请输入HTML模板内容,可使用 {{key}} 作为占位符"
- rows="10"
+ rows="15"
/>
@@ -378,7 +369,7 @@ watch(() => props.open, (newVal) => {
ref="markdownTemplateRef"
v-model="formData.markdown_template"
placeholder="请输入Markdown模板内容,可使用 {{key}} 作为占位符"
- rows="10"
+ rows="15"
/>
diff --git a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
index 84f6918..a3a6bc7 100644
--- a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
+++ b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
@@ -80,6 +80,36 @@ const handlechannelNameChange = () => {
// 添加单条实例配置
const handleAddSubmit = async () => {
+ // 验证是否选择了渠道
+ if (!channelName.value) {
+ toast.error('请选择发送渠道')
+ return
+ }
+
+ // 验证内容类型
+ const contentType = formData.value.templ_type
+ if (!contentType) {
+ toast.error('请选择消息格式')
+ return
+ }
+
+ // 验证模板对应格式的内容是否为空
+ const templateFieldMap: Record
= {
+ 'text': 'text_template',
+ 'html': 'html_template',
+ 'markdown': 'markdown_template'
+ }
+
+ const fieldName = templateFieldMap[contentType.toLowerCase()]
+ if (fieldName) {
+ const templateContent = props.templateData?.[fieldName] || ''
+ // 检查是否为空(去除所有空白字符后检查)
+ if (!templateContent.trim()) {
+ toast.error(`模板的 ${contentType} 格式内容为空,无法添加此类型的实例`)
+ return
+ }
+ }
+
// 组建表单数据
let postData = {
"id": generateBizUniqueID('I'),
From bed2b2b08cd8911f0a146c5f18ff3f958a41068b Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 20:13:09 +0800
Subject: [PATCH 18/24] chore: opt template code
---
.../pages/messageTemplate/MessageTemplate.vue | 123 +---------------
.../pages/messageTemplate/TemplateEditor.vue | 138 +++++++++++++++++-
2 files changed, 136 insertions(+), 125 deletions(-)
diff --git a/web/src/components/pages/messageTemplate/MessageTemplate.vue b/web/src/components/pages/messageTemplate/MessageTemplate.vue
index d15bf7d..10f7561 100644
--- a/web/src/components/pages/messageTemplate/MessageTemplate.vue
+++ b/web/src/components/pages/messageTemplate/MessageTemplate.vue
@@ -3,10 +3,7 @@ import { ref, computed, reactive, onMounted } from 'vue'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
-import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import Pagination from '@/components/ui/Pagination.vue'
@@ -34,12 +31,6 @@ interface MessageTemplate {
modified_on: string
}
-interface Placeholder {
- key: string
- label: string
- default: string
-}
-
const router = useRouter()
let state = reactive({
@@ -51,9 +42,6 @@ let state = reactive({
status: 'all'
})
-const isPreviewOpen = ref(false)
-const currentTemplate = ref(null)
-
// API代码查看器状态
const isApiViewerOpen = ref(false)
const selectedTemplateForApi = ref(null)
@@ -67,13 +55,6 @@ const isEditorOpen = ref(false)
const isEditing = ref(false)
const selectedTemplateForEdit = ref(null)
-const previewData = reactive({
- text: '',
- html: '',
- markdown: '',
- params: {} as Record
-})
-
const totalPages = computed(() => Math.ceil(state.total / state.pageSize))
const queryListData = async (page: number, size: number, text = '', status = '') => {
@@ -143,41 +124,6 @@ const handleViewLogs = (template: MessageTemplate) => {
router.push(`/sendlogs?taskid=${template.id}`)
}
-const openPreview = async (template: MessageTemplate) => {
- currentTemplate.value = template
-
- // 解析占位符
- let placeholders: Placeholder[] = []
- try {
- placeholders = JSON.parse(template.placeholders || '[]')
- } catch {}
-
- // 初始化预览参数
- previewData.params = {}
- placeholders.forEach(p => {
- previewData.params[p.key] = p.default || ''
- })
-
- await refreshPreview()
- isPreviewOpen.value = true
-}
-
-const refreshPreview = async () => {
- if (!currentTemplate.value) return
-
- try {
- const rsp = await request.post('/templates/preview', {
- id: currentTemplate.value.id,
- params: previewData.params
- })
- previewData.text = rsp.data.data.text || ''
- previewData.html = rsp.data.data.html || ''
- previewData.markdown = rsp.data.data.markdown || ''
- } catch (error: any) {
- toast.error(error.response?.data?.message || '预览失败')
- }
-}
-
onMounted(async () => {
await queryListData(1, state.pageSize)
})
@@ -273,13 +219,10 @@ onMounted(async () => {
{{ item.created_on }}
- 日志
- 接口
- 预览
+ 日志
+ 接口
编辑
实例
-
-
删除
@@ -303,68 +246,6 @@ onMounted(async () => {
@saved="handleEditorSaved"
/>
-
-
-
{
return placeholdersList.value.filter(ph => ph.key && ph.key.trim())
})
+// 预览数据
+const previewData = ref({
+ text: '',
+ html: '',
+ markdown: '',
+ params: {} as Record
+})
+
+// 是否显示预览
+const showPreview = ref(false)
+
+// 预览防抖定时器
+let previewDebounceTimer: number | null = null
+
+// 刷新预览
+const refreshPreview = async () => {
+ if (!props.isEditing || !formData.value.id) {
+ // 新建模板时,直接使用当前输入的内容作为预览
+ previewData.value.text = replacePreviewPlaceholders(formData.value.text_template)
+ previewData.value.html = replacePreviewPlaceholders(formData.value.html_template)
+ previewData.value.markdown = replacePreviewPlaceholders(formData.value.markdown_template)
+ return
+ }
+
+ try {
+ const rsp = await request.post('/templates/preview', {
+ id: formData.value.id,
+ params: previewData.value.params
+ })
+ previewData.value.text = rsp.data.data.text || ''
+ previewData.value.html = rsp.data.data.html || ''
+ previewData.value.markdown = rsp.data.data.markdown || ''
+ } catch (error: any) {
+ console.error('预览失败:', error)
+ }
+}
+
+// 替换预览占位符(用于新建模板)
+const replacePreviewPlaceholders = (template: string) => {
+ if (!template) return ''
+ let result = template
+ Object.keys(previewData.value.params).forEach(key => {
+ const value = previewData.value.params[key] || `{{${key}}}`
+ result = result.replace(new RegExp(`{{${key}}}`, 'g'), value)
+ })
+ return result
+}
+
+// 监听模板内容变化,自动刷新预览(防抖)
+watch([
+ () => formData.value.text_template,
+ () => formData.value.html_template,
+ () => formData.value.markdown_template,
+ () => previewData.value.params
+], () => {
+ if (!showPreview.value) return
+
+ if (previewDebounceTimer) {
+ clearTimeout(previewDebounceTimer)
+ }
+ previewDebounceTimer = window.setTimeout(() => {
+ refreshPreview()
+ }, 500)
+}, { deep: true })
+
// 监听占位符列表变化,同步到 formData(使用防抖)
let placeholderDebounceTimer: number | null = null
watch(placeholdersList, () => {
@@ -164,6 +229,12 @@ const loadTemplateData = (template: TemplateData) => {
} catch {
placeholdersList.value = []
}
+
+ // 初始化预览参数
+ previewData.value.params = {}
+ placeholdersList.value.forEach(p => {
+ previewData.value.params[p.key] = p.default || ''
+ })
}
// 保存模板
@@ -263,7 +334,7 @@ watch(() => props.open, (newVal) => {
-
+
props.open, (newVal) => {
+
+
+
+
+ {{ showPreview ? '隐藏预览' : '显示预览' }}
+
+
+
+
+
+
+
Text
@@ -317,8 +417,16 @@ watch(() => props.open, (newVal) => {
ref="textTemplateRef"
v-model="formData.text_template"
placeholder="请输入纯文本模板内容,可使用 {{key}} 作为占位符"
- rows="15"
+ :rows="showPreview ? 10 : 15"
/>
+
+
+
+
+
+
{{ previewData.text || '无内容' }}
+
+
@@ -343,8 +451,19 @@ watch(() => props.open, (newVal) => {
ref="htmlTemplateRef"
v-model="formData.html_template"
placeholder="请输入HTML模板内容,可使用 {{key}} 作为占位符"
- rows="15"
+ :rows="showPreview ? 10 : 15"
/>
+
+
+
+
+
+
+ 💡 HTML 预览仅显示基础结构,实际发送时可能包含邮件样式等
+
+
@@ -369,8 +488,19 @@ watch(() => props.open, (newVal) => {
ref="markdownTemplateRef"
v-model="formData.markdown_template"
placeholder="请输入Markdown模板内容,可使用 {{key}} 作为占位符"
- rows="15"
+ :rows="showPreview ? 10 : 15"
/>
+
+
+
+
+
+
{{ previewData.markdown || '无内容' }}
+
+
+ 💡 Markdown 在发送时会被渲染为对应格式(钉钉、企业微信等平台支持)
+
+
From a2acd498841ef8bbe597cf29a9ff63e0ad328672 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 21:11:15 +0800
Subject: [PATCH 19/24] chore: opt template code
---
middleware/ginlog.go | 44 +++++++++++++++++++
models/cron_messages.go | 2 +-
models/send_tasks.go | 2 +-
models/send_ways.go | 2 +-
.../pages/cronMessages/AddCronMessages.vue | 2 +-
.../messageTemplate/TemplateApiViewer.vue | 2 +-
.../TemplateInstanceConfig.vue | 2 +-
.../components/pages/sendTasks/AddTasks.vue | 2 +-
.../components/pages/sendTasks/EditTasks.vue | 2 +-
.../components/pages/sendTasks/SendTasks.vue | 2 +-
web/src/util/uuid.js | 2 +-
11 files changed, 54 insertions(+), 10 deletions(-)
diff --git a/middleware/ginlog.go b/middleware/ginlog.go
index 65b868d..b849097 100644
--- a/middleware/ginlog.go
+++ b/middleware/ginlog.go
@@ -1,8 +1,10 @@
package middleware
import (
+ "bytes"
"fmt"
"math"
+ "strings"
"net/http"
"net/url"
@@ -12,6 +14,23 @@ import (
"github.com/sirupsen/logrus"
)
+// 需要记录响应内容的 API 路径前缀
+var logResponsePaths = []string{
+ "/api/v1/message/send",
+ "/api/v2/message/send",
+}
+
+// responseBodyWriter 用于捕获响应内容
+type responseBodyWriter struct {
+ gin.ResponseWriter
+ body *bytes.Buffer
+}
+
+func (w responseBodyWriter) Write(b []byte) (int, error) {
+ w.body.Write(b)
+ return w.ResponseWriter.Write(b)
+}
+
// LogMiddleware Logger is the logrus logger handler
func LogMiddleware(notLogged ...string) gin.HandlerFunc {
//hostname, err := os.Hostname()
@@ -33,6 +52,24 @@ func LogMiddleware(notLogged ...string) gin.HandlerFunc {
raw := c.Request.URL.RawQuery
start := time.Now()
+ // 判断是否需要捕获响应内容
+ needCaptureResponse := false
+ for _, logPath := range logResponsePaths {
+ if strings.HasPrefix(path, logPath) {
+ needCaptureResponse = true
+ break
+ }
+ }
+
+ var bodyWriter *responseBodyWriter
+ if needCaptureResponse {
+ bodyWriter = &responseBodyWriter{
+ ResponseWriter: c.Writer,
+ body: bytes.NewBufferString(""),
+ }
+ c.Writer = bodyWriter
+ }
+
c.Next()
stop := time.Since(start)
@@ -70,6 +107,13 @@ func LogMiddleware(notLogged ...string) gin.HandlerFunc {
entry.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
} else {
msg := fmt.Sprintf("%s [%s] %s %d %d (%dms)", clientIP, c.Request.Method, path, statusCode, dataLength, latency)
+
+ // 如果是发送消息的 API,打印返回内容
+ if needCaptureResponse && bodyWriter != nil {
+ responseBody := bodyWriter.body.String()
+ msg = fmt.Sprintf("%s | Response: %s", msg, responseBody)
+ }
+
if statusCode >= http.StatusInternalServerError {
entry.Error(msg)
} else if statusCode >= http.StatusBadRequest {
diff --git a/models/cron_messages.go b/models/cron_messages.go
index e12ada8..6d727b2 100644
--- a/models/cron_messages.go
+++ b/models/cron_messages.go
@@ -22,7 +22,7 @@ type CronMessages struct {
func GenerateMsgUniqueID() string {
newUUID := util.GenerateUniqueID()
- return fmt.Sprintf("C-%s", newUUID)
+ return fmt.Sprintf("CM%s", newUUID)
}
func AddSendCronMsg(
diff --git a/models/send_tasks.go b/models/send_tasks.go
index 458eabd..ee58c86 100644
--- a/models/send_tasks.go
+++ b/models/send_tasks.go
@@ -15,7 +15,7 @@ type SendTasks struct {
func GenerateTaskUniqueID() string {
newUUID := util.GenerateUniqueID()
- return fmt.Sprintf("T-%s", newUUID)
+ return fmt.Sprintf("TK%s", newUUID)
}
// AddSendTaskWithID 添加实例的时候添加任务
diff --git a/models/send_ways.go b/models/send_ways.go
index 1041e4d..aeb64ff 100644
--- a/models/send_ways.go
+++ b/models/send_ways.go
@@ -17,7 +17,7 @@ type SendWays struct {
func GenerateWayUniqueID() string {
newUUID := util.GenerateUniqueID()
- return fmt.Sprintf("W-%s", newUUID)
+ return fmt.Sprintf("WY%s", newUUID)
}
func AddSendWay(name string, auth string, wayType string, createdBy string, modifiedBy string) error {
diff --git a/web/src/components/pages/cronMessages/AddCronMessages.vue b/web/src/components/pages/cronMessages/AddCronMessages.vue
index c89a3cf..55210a7 100644
--- a/web/src/components/pages/cronMessages/AddCronMessages.vue
+++ b/web/src/components/pages/cronMessages/AddCronMessages.vue
@@ -42,7 +42,7 @@ const handleSubmit = async () => {
try {
let postData = {
"name": formData.name,
- "id": generateBizUniqueID("C"),
+ "id": generateBizUniqueID("CM"),
"title": formData.title,
"content": formData.content,
"cron": formData.cron_expression,
diff --git a/web/src/components/pages/messageTemplate/TemplateApiViewer.vue b/web/src/components/pages/messageTemplate/TemplateApiViewer.vue
index 07668fd..664e230 100644
--- a/web/src/components/pages/messageTemplate/TemplateApiViewer.vue
+++ b/web/src/components/pages/messageTemplate/TemplateApiViewer.vue
@@ -153,7 +153,7 @@ export default defineComponent({
-
+
💡 使用说明
- token 参数:需要使用加密后的 token,不能直接使用明文模板ID(安全考虑)
diff --git a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
index a3a6bc7..80ed0ef 100644
--- a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
+++ b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
@@ -112,7 +112,7 @@ const handleAddSubmit = async () => {
// 组建表单数据
let postData = {
- "id": generateBizUniqueID('I'),
+ "id": generateBizUniqueID('IN'),
"enable": 1,
"template_id": props.templateData.id,
"way_id": displayOptions.value[0]?.id,
diff --git a/web/src/components/pages/sendTasks/AddTasks.vue b/web/src/components/pages/sendTasks/AddTasks.vue
index 112ba76..9db19e1 100644
--- a/web/src/components/pages/sendTasks/AddTasks.vue
+++ b/web/src/components/pages/sendTasks/AddTasks.vue
@@ -25,7 +25,7 @@ const handleCancel = () => {
// 添加一条任务
const handleSubmit = async () => {
- const taskId = generateBizUniqueID('T');
+ const taskId = generateBizUniqueID('TK');
const postData: Record = {
id: taskId,
name: inputValue.value.trim(),
diff --git a/web/src/components/pages/sendTasks/EditTasks.vue b/web/src/components/pages/sendTasks/EditTasks.vue
index 3b3c7ee..96a08b8 100644
--- a/web/src/components/pages/sendTasks/EditTasks.vue
+++ b/web/src/components/pages/sendTasks/EditTasks.vue
@@ -91,7 +91,7 @@ const handleClose = () => {
const handleAddSubmit = async () => {
// 组建表单数据
let postData = {
- "id": generateBizUniqueID('I'),
+ "id": generateBizUniqueID('IN'),
"enable": 1,
"task_id": props.editData.id,
"way_id": displayOptions.value[0]?.id,
diff --git a/web/src/components/pages/sendTasks/SendTasks.vue b/web/src/components/pages/sendTasks/SendTasks.vue
index bc8a502..ab7cad5 100644
--- a/web/src/components/pages/sendTasks/SendTasks.vue
+++ b/web/src/components/pages/sendTasks/SendTasks.vue
@@ -189,7 +189,7 @@ onMounted(async () => {
新项目建议使用
消息模板
- 功能,它提供更好的内容管理和维护体验。发送任务主要用于兼容历史数据。
+ 功能,它提供更好的内容管理和维护体验。发送任务主要用于兼容早期使用数据。
了解更多 →
diff --git a/web/src/util/uuid.js b/web/src/util/uuid.js
index 3fb718f..c1f1d46 100644
--- a/web/src/util/uuid.js
+++ b/web/src/util/uuid.js
@@ -18,7 +18,7 @@ function generateUniqueID() {
function generateBizUniqueID(flag) {
const randomString = generateRandomString(10);
- return `${flag}-${randomString}`;
+ return `${flag}${randomString}`;
}
export { generateUniqueID, generateBizUniqueID };
From fb35769e528557397dbfa9d30f56fe4a5a341ce5 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 21:16:20 +0800
Subject: [PATCH 20/24] chore: update release log
---
.release_log | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/.release_log b/.release_log
index cf6871d..1fc2b86 100644
--- a/.release_log
+++ b/.release_log
@@ -21,7 +21,8 @@
21. 支持sqlite部署,支持不同版本mysql
22. [2025.01.01] 增加托管消息,现在可以将站点作为消息的接收,登录站点查看消息
23. [2025.04.28] 支持tidb数据库,支持数据库ssl连接
-23. [2025.08.10] UI组件使用shadcn-vue,基于tailwindcss
-24. [2025.09.14] 支持系统信息展示更多的运行信息
-25. [2025.09.30] 支持页面的明暗主题切换设置,增加登录日志
-25. [2025.10.12] 增加cookies过期天数设置
+24. [2025.08.10] UI组件使用shadcn-vue,基于tailwindcss
+25. [2025.09.14] 支持系统信息展示更多的运行信息
+26. [2025.09.30] 支持页面的明暗主题切换设置,增加登录日志
+27. [2025.10.12] 增加cookies过期天数设置
+28. [2025.12.06] 增加模板功能:集成预览到编辑器、优化ID生成规则、增强发送API日志记录
From 5d3bfaa2de15afd94d97cee5a44bedfd49bee879 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 22:38:08 +0800
Subject: [PATCH 21/24] chore: opt template style
---
models/message_template.go | 6 +-
web/package-lock.json | 32 ++++-----
web/package.json | 6 +-
.../pages/messageTemplate/MessageTemplate.vue | 7 +-
.../pages/messageTemplate/TemplateEditor.vue | 67 +++++++++++--------
.../TemplateInstanceConfig.vue | 61 +++++++++--------
.../components/pages/sendTasks/EditTasks.vue | 21 ++----
web/src/components/ui/checkbox/Checkbox.vue | 35 ++++++++++
web/src/components/ui/checkbox/index.ts | 1 +
9 files changed, 142 insertions(+), 94 deletions(-)
create mode 100644 web/src/components/ui/checkbox/Checkbox.vue
create mode 100644 web/src/components/ui/checkbox/index.ts
diff --git a/models/message_template.go b/models/message_template.go
index a604893..fe0723f 100644
--- a/models/message_template.go
+++ b/models/message_template.go
@@ -45,7 +45,11 @@ func (t *Template) Add() error {
// Update 更新消息模板
func (t *Template) Update() error {
- if err := db.Model(&Template{}).Where("id = ?", t.ID).Updates(t).Error; err != nil {
+ // 使用 Select 明确指定要更新的字段,包括布尔值字段,排除不应更新的时间戳字段
+ if err := db.Model(&Template{}).Where("id = ?", t.ID).
+ Select("name", "description", "text_template", "html_template", "markdown_template",
+ "placeholders", "at_mobiles", "at_user_ids", "is_at_all", "status", "modified_by").
+ Updates(t).Error; err != nil {
return err
}
return nil
diff --git a/web/package-lock.json b/web/package-lock.json
index 176995c..1cb6db1 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -10,13 +10,13 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
- "@vueuse/core": "^13.6.0",
+ "@vueuse/core": "^13.9.0",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.536.0",
"pinia": "^3.0.3",
- "reka-ui": "^2.4.1",
+ "reka-ui": "^2.6.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
@@ -1605,14 +1605,14 @@
}
},
"node_modules/@vueuse/core": {
- "version": "13.6.0",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.6.0.tgz",
- "integrity": "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==",
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
+ "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
- "@vueuse/metadata": "13.6.0",
- "@vueuse/shared": "13.6.0"
+ "@vueuse/metadata": "13.9.0",
+ "@vueuse/shared": "13.9.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -1622,9 +1622,9 @@
}
},
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
- "version": "13.6.0",
- "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.6.0.tgz",
- "integrity": "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==",
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz",
+ "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -1634,9 +1634,9 @@
}
},
"node_modules/@vueuse/metadata": {
- "version": "13.6.0",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.6.0.tgz",
- "integrity": "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==",
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
+ "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
@@ -2632,9 +2632,9 @@
"license": "MIT"
},
"node_modules/reka-ui": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.1.tgz",
- "integrity": "sha512-NB7DrCsODN8MH02BWtgiExygfFcuuZ5/PTn6fMgjppmFHqePvNhmSn1LEuF35nel6PFbA4v+gdj0IoGN1yZ+vw==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.1.tgz",
+ "integrity": "sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
diff --git a/web/package.json b/web/package.json
index 0f7c7e3..491ebed 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,13 +13,13 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
- "@vueuse/core": "^13.6.0",
+ "@vueuse/core": "^13.9.0",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.536.0",
"pinia": "^3.0.3",
- "reka-ui": "^2.4.1",
+ "reka-ui": "^2.6.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.6",
@@ -37,4 +37,4 @@
"vite": "^7.0.4",
"vue-tsc": "^2.2.12"
}
-}
\ No newline at end of file
+}
diff --git a/web/src/components/pages/messageTemplate/MessageTemplate.vue b/web/src/components/pages/messageTemplate/MessageTemplate.vue
index 10f7561..67ae56d 100644
--- a/web/src/components/pages/messageTemplate/MessageTemplate.vue
+++ b/web/src/components/pages/messageTemplate/MessageTemplate.vue
@@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import Pagination from '@/components/ui/Pagination.vue'
+import ClickableTruncate from '@/components/ui/ClickableTruncate.vue'
import TemplateApiViewer from './TemplateApiViewer.vue'
import TemplateInstanceConfig from './TemplateInstanceConfig.vue'
import TemplateEditor from './TemplateEditor.vue'
@@ -201,9 +202,11 @@ onMounted(async () => {
{{ item.id }}
- {{ item.name }}
- {{ item.description || '-' }}
+
+
+
+
diff --git a/web/src/components/pages/messageTemplate/TemplateEditor.vue b/web/src/components/pages/messageTemplate/TemplateEditor.vue
index 69af023..e4da45a 100644
--- a/web/src/components/pages/messageTemplate/TemplateEditor.vue
+++ b/web/src/components/pages/messageTemplate/TemplateEditor.vue
@@ -7,6 +7,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'vue-sonner'
import { request } from '@/api/api'
@@ -219,7 +220,7 @@ const loadTemplateData = (template: TemplateData) => {
placeholders: template.placeholders,
at_mobiles: template.at_mobiles || '',
at_user_ids: template.at_user_ids || '',
- is_at_all: template.is_at_all || false,
+ is_at_all: Boolean(template.is_at_all),
status: template.status
}
@@ -243,18 +244,28 @@ const saveTemplate = async () => {
toast.error('请输入模板名称')
return
}
+
+ // 验证至少填写一种格式的模板内容
+ if (!formData.value.text_template && !formData.value.html_template && !formData.value.markdown_template) {
+ toast.error('至少需要填写一种格式的模板内容')
+ return
+ }
// 同步占位符数据
formData.value.placeholders = JSON.stringify(placeholdersList.value)
try {
const url = props.isEditing ? '/templates/edit' : '/templates/add'
- await request.post(url, formData.value)
- toast.success(props.isEditing ? '更新模板成功' : '添加模板成功')
- emit('update:open', false)
- emit('saved')
+ const response = await request.post(url, formData.value)
+ if (response.data.code === 200) {
+ toast.success(props.isEditing ? '更新模板成功' : '添加模板成功')
+ emit('update:open', false)
+ emit('saved')
+ } else {
+ toast.error(response.data.msg || '操作失败')
+ }
} catch (error: any) {
- toast.error(error.response?.data?.message || '操作失败')
+ toast.error(error.response?.data?.msg || error.response?.data?.message || '操作失败')
}
}
@@ -278,9 +289,25 @@ watch(() => props.open, (newVal) => {
-
-
-
+
+
+
+
+
+
+
+
+
@@ -288,21 +315,6 @@ watch(() => props.open, (newVal) => {
-
-
-
-
-
@@ -337,11 +349,10 @@ watch(() => props.open, (newVal) => {
- formData.is_at_all = newVal === true"
/>
diff --git a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
index 80ed0ef..1150cc8 100644
--- a/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
+++ b/web/src/components/pages/messageTemplate/TemplateInstanceConfig.vue
@@ -269,35 +269,42 @@ watch(() => props.open, (newVal) => {
-
-
模板名称
-
{{ templateData?.name }}
-
ID: {{ templateData?.id }}
+
+
+ {{ templateData?.name }}
+ {{ templateData?.id }}
+
+
为此模板配置发送实例
-
-
-
-
-
-
-
-
-
- {{ option.name }}
-
-
-
- 搜索中...
-
- 未找到匹配的渠道
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ option.name }}
+
+
+
+ 搜索中...
+
+ 未找到匹配的渠道
+
+
+
+
+
+
添加实例
+
@@ -326,10 +333,6 @@ watch(() => props.open, (newVal) => {
-
- 添加实例
-
-
已经关联的实例
diff --git a/web/src/components/pages/sendTasks/EditTasks.vue b/web/src/components/pages/sendTasks/EditTasks.vue
index 96a08b8..455adb5 100644
--- a/web/src/components/pages/sendTasks/EditTasks.vue
+++ b/web/src/components/pages/sendTasks/EditTasks.vue
@@ -82,11 +82,6 @@ const handlechannelNameChange = () => {
}
}
-// 关闭drawer
-const handleClose = () => {
- emit('update:open', false)
-}
-
// 添加单条实例配置
const handleAddSubmit = async () => {
// 组建表单数据
@@ -244,19 +239,19 @@ onMounted(() => {
- 修改
+ 修改
-
-
-
+
+
+
+ class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
@@ -276,6 +271,7 @@ onMounted(() => {
+
添加实例
@@ -309,11 +305,6 @@ onMounted(() => {
-
- 取消
- 添加实例
-
-
已经关联的实例
diff --git a/web/src/components/ui/checkbox/Checkbox.vue b/web/src/components/ui/checkbox/Checkbox.vue
new file mode 100644
index 0000000..6604cbd
--- /dev/null
+++ b/web/src/components/ui/checkbox/Checkbox.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/checkbox/index.ts b/web/src/components/ui/checkbox/index.ts
new file mode 100644
index 0000000..3391a85
--- /dev/null
+++ b/web/src/components/ui/checkbox/index.ts
@@ -0,0 +1 @@
+export { default as Checkbox } from "./Checkbox.vue"
From 77fe5ecae1d43c1e99f550d36c31732ed49d53d7 Mon Sep 17 00:00:00 2001
From: engigu
Date: Sat, 6 Dec 2025 22:41:36 +0800
Subject: [PATCH 22/24] chore: opt template style
---
web/src/components/pages/sendTasks/EditTasks.vue | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/web/src/components/pages/sendTasks/EditTasks.vue b/web/src/components/pages/sendTasks/EditTasks.vue
index 455adb5..c469aa6 100644
--- a/web/src/components/pages/sendTasks/EditTasks.vue
+++ b/web/src/components/pages/sendTasks/EditTasks.vue
@@ -1,5 +1,5 @@