mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	feat: able to send alert message via message pusher (close #993)
This commit is contained in:
		@@ -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 进行部署
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,9 @@ var WeChatServerAddress = ""
 | 
			
		||||
var WeChatServerToken = ""
 | 
			
		||||
var WeChatAccountQRCodeImageURL = ""
 | 
			
		||||
 | 
			
		||||
var MessagePusherAddress = ""
 | 
			
		||||
var MessagePusherToken = ""
 | 
			
		||||
 | 
			
		||||
var TurnstileSiteKey = ""
 | 
			
		||||
var TurnstileSecretKey = ""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
							
								
								
									
										22
									
								
								common/message/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								common/message/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								common/message/message-pusher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								common/message/message-pusher.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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()))
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -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("<p>您好,你正在进行%s邮箱验证。</p>"+
 | 
			
		||||
		"<p>您的验证码为: <strong>%s</strong></p>"+
 | 
			
		||||
		"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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) {
 | 
			
		||||
		"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
 | 
			
		||||
		"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
 | 
			
		||||
		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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":
 | 
			
		||||
 
 | 
			
		||||
@@ -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,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.SysError("failed to send email" + err.Error())
 | 
			
		||||
 
 | 
			
		||||
@@ -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()))
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 Message Pusher
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以推送报警信息,
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://github.com/songquanpeng/message-pusher'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
              了解 Message Pusher
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='Message Pusher 推送地址'
 | 
			
		||||
              name='MessagePusherAddress'
 | 
			
		||||
              placeholder='例如:https://msgpusher.com/push/your_username'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.MessagePusherAddress}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='Message Pusher 访问凭证'
 | 
			
		||||
              name='MessagePusherToken'
 | 
			
		||||
              type='password'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.MessagePusherToken}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitMessagePusher}>
 | 
			
		||||
            保存 Message Pusher 设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 Turnstile
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user