diff --git a/README.md b/README.md index 96c2d708..1cb30591 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 +24. 配合 [Message Pusher](https://github.com/songquanpeng/message-pusher) 可将报警信息推送到多种 App 上。 ## 部署 ### 基于 Docker 进行部署 diff --git a/common/config/config.go b/common/config/config.go index 4d391aac..53af824f 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -70,6 +70,9 @@ var WeChatServerAddress = "" var WeChatServerToken = "" var WeChatAccountQRCodeImageURL = "" +var MessagePusherAddress = "" +var MessagePusherToken = "" + var TurnstileSiteKey = "" var TurnstileSecretKey = "" diff --git a/common/email.go b/common/message/email.go similarity index 96% rename from common/email.go rename to common/message/email.go index 2689da6a..b06782db 100644 --- a/common/email.go +++ b/common/message/email.go @@ -1,4 +1,4 @@ -package common +package message import ( "crypto/rand" @@ -12,6 +12,9 @@ import ( ) func SendEmail(subject string, receiver string, content string) error { + if receiver == "" { + return fmt.Errorf("receiver is empty") + } if config.SMTPFrom == "" { // for compatibility config.SMTPFrom = config.SMTPAccount } diff --git a/common/message/main.go b/common/message/main.go new file mode 100644 index 00000000..5ce82a64 --- /dev/null +++ b/common/message/main.go @@ -0,0 +1,22 @@ +package message + +import ( + "fmt" + "github.com/songquanpeng/one-api/common/config" +) + +const ( + ByAll = "all" + ByEmail = "email" + ByMessagePusher = "message_pusher" +) + +func Notify(by string, title string, description string, content string) error { + if by == ByEmail { + return SendEmail(title, config.RootUserEmail, content) + } + if by == ByMessagePusher { + return SendMessage(title, description, content) + } + return fmt.Errorf("unknown notify method: %s", by) +} diff --git a/common/message/message-pusher.go b/common/message/message-pusher.go new file mode 100644 index 00000000..69949b4b --- /dev/null +++ b/common/message/message-pusher.go @@ -0,0 +1,53 @@ +package message + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/songquanpeng/one-api/common/config" + "net/http" +) + +type request struct { + Title string `json:"title"` + Description string `json:"description"` + Content string `json:"content"` + URL string `json:"url"` + Channel string `json:"channel"` + Token string `json:"token"` +} + +type response struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func SendMessage(title string, description string, content string) error { + if config.MessagePusherAddress == "" { + return errors.New("message pusher address is not set") + } + req := request{ + Title: title, + Description: description, + Content: content, + Token: config.MessagePusherToken, + } + data, err := json.Marshal(req) + if err != nil { + return err + } + resp, err := http.Post(config.MessagePusherAddress, + "application/json", bytes.NewBuffer(data)) + if err != nil { + return err + } + var res response + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return err + } + if !res.Success { + return errors.New(res.Message) + } + return nil +} diff --git a/controller/channel-test.go b/controller/channel-test.go index d00e5e5d..f709a6df 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -8,6 +8,7 @@ import ( "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/middleware" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/monitor" @@ -192,7 +193,7 @@ func testChannels(notify bool, scope string) error { testAllChannelsRunning = false testAllChannelsLock.Unlock() if notify { - err := common.SendEmail("通道测试完成", config.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") + err := message.Notify(message.ByAll, "通道测试完成", "", "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") if err != nil { logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error())) } diff --git a/controller/misc.go b/controller/misc.go index 036bdbd1..f27fdb12 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/model" "net/http" "strings" @@ -110,7 +111,7 @@ func SendEmailVerification(c *gin.Context) { content := fmt.Sprintf("

您好,你正在进行%s邮箱验证。

"+ "

您的验证码为: %s

"+ "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

", config.SystemName, code, common.VerificationValidMinutes) - err := common.SendEmail(subject, email, content) + err := message.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -149,7 +150,7 @@ func SendPasswordResetEmail(c *gin.Context) { "

点击 此处 进行密码重置。

"+ "

如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:
%s

"+ "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

", config.SystemName, link, link, common.VerificationValidMinutes) - err := common.SendEmail(subject, email, content) + err := message.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/main.go b/main.go index 96603066..e9673118 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/middleware" "github.com/songquanpeng/one-api/model" @@ -87,6 +88,7 @@ func main() { logger.SysLog("metric enabled, will disable channel if too much request failed") } openai.InitTokenEncoders() + _ = message.SendMessage("One API", "", fmt.Sprintf("One API %s started", common.Version)) // Initialize HTTP server server := gin.New() diff --git a/model/option.go b/model/option.go index 6002c795..f58a9ca3 100644 --- a/model/option.go +++ b/model/option.go @@ -57,6 +57,8 @@ func InitOptionMap() { config.OptionMap["WeChatServerAddress"] = "" config.OptionMap["WeChatServerToken"] = "" config.OptionMap["WeChatAccountQRCodeImageURL"] = "" + config.OptionMap["MessagePusherAddress"] = "" + config.OptionMap["MessagePusherToken"] = "" config.OptionMap["TurnstileSiteKey"] = "" config.OptionMap["TurnstileSecretKey"] = "" config.OptionMap["QuotaForNewUser"] = strconv.Itoa(config.QuotaForNewUser) @@ -179,6 +181,10 @@ func updateOptionMap(key string, value string) (err error) { config.WeChatServerToken = value case "WeChatAccountQRCodeImageURL": config.WeChatAccountQRCodeImageURL = value + case "MessagePusherAddress": + config.MessagePusherAddress = value + case "MessagePusherToken": + config.MessagePusherToken = value case "TurnstileSiteKey": config.TurnstileSiteKey = value case "TurnstileSecretKey": diff --git a/model/token.go b/model/token.go index d0a0648a..c4669e0b 100644 --- a/model/token.go +++ b/model/token.go @@ -7,6 +7,7 @@ import ( "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/common/message" "gorm.io/gorm" ) @@ -213,7 +214,7 @@ func PreConsumeTokenQuota(tokenId int, quota int) (err error) { } if email != "" { topUpLink := fmt.Sprintf("%s/topup", config.ServerAddress) - err = common.SendEmail(prompt, email, + err = message.SendEmail(prompt, email, fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s", prompt, userQuota, topUpLink, topUpLink)) if err != nil { logger.SysError("failed to send email" + err.Error()) diff --git a/monitor/channel.go b/monitor/channel.go index 12394913..597ab11a 100644 --- a/monitor/channel.go +++ b/monitor/channel.go @@ -5,14 +5,23 @@ import ( "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/model" ) func notifyRootUser(subject string, content string) { + if config.MessagePusherAddress != "" { + err := message.SendMessage(subject, content, content) + if err != nil { + logger.SysError(fmt.Sprintf("failed to send message: %s", err.Error())) + } else { + return + } + } if config.RootUserEmail == "" { config.RootUserEmail = model.GetRootUserEmail() } - err := common.SendEmail(subject, config.RootUserEmail, content) + err := message.SendEmail(subject, config.RootUserEmail, content) if err != nil { logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error())) } diff --git a/web/default/src/components/SystemSetting.js b/web/default/src/components/SystemSetting.js index 7b34ce5b..09b98665 100644 --- a/web/default/src/components/SystemSetting.js +++ b/web/default/src/components/SystemSetting.js @@ -22,6 +22,8 @@ const SystemSetting = () => { WeChatServerAddress: '', WeChatServerToken: '', WeChatAccountQRCodeImageURL: '', + MessagePusherAddress: '', + MessagePusherToken: '', TurnstileCheckEnabled: '', TurnstileSiteKey: '', TurnstileSecretKey: '', @@ -183,6 +185,21 @@ const SystemSetting = () => { } }; + const submitMessagePusher = async () => { + if (originInputs['MessagePusherAddress'] !== inputs.MessagePusherAddress) { + await updateOption( + 'MessagePusherAddress', + removeTrailingSlash(inputs.MessagePusherAddress) + ); + } + if ( + originInputs['MessagePusherToken'] !== inputs.MessagePusherToken && + inputs.MessagePusherToken !== '' + ) { + await updateOption('MessagePusherToken', inputs.MessagePusherToken); + } + }; + const submitGitHubOAuth = async () => { if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { await updateOption('GitHubClientId', inputs.GitHubClientId); @@ -496,6 +513,42 @@ const SystemSetting = () => { 保存 WeChat Server 设置 +
+ 配置 Message Pusher + + 用以推送报警信息, + + 点击此处 + + 了解 Message Pusher + +
+ + + + + + 保存 Message Pusher 设置 + +
配置 Turnstile