feat: add notifier (#144)

* ♻️ refactor: email refactor

*  feat: add notifier
This commit is contained in:
Buer
2024-04-09 15:00:06 +08:00
committed by GitHub
parent 76d22f0572
commit a3719cd78a
33 changed files with 1386 additions and 188 deletions

View File

@@ -122,7 +122,7 @@ func updateAllChannelsBalance() error {
} else {
// err is nil & balance <= 0 means quota is used up
if balance <= 0 {
DisableChannel(channel.Id, channel.Name, "余额不足")
DisableChannel(channel.Id, channel.Name, "余额不足", true)
}
}
time.Sleep(common.RequestInterval)

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"one-api/common"
"one-api/common/notify"
"one-api/model"
"one-api/providers"
providers_base "one-api/providers/base"
@@ -130,28 +131,7 @@ func TestChannel(c *gin.Context) {
var testAllChannelsLock sync.Mutex
var testAllChannelsRunning bool = false
func notifyRootUser(subject string, content string) {
if common.RootUserEmail == "" {
common.RootUserEmail = model.GetRootUserEmail()
}
err := common.SendEmail(subject, common.RootUserEmail, content)
if err != nil {
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
}
}
// enable & notify
func enableChannel(channelId int, channelName string) {
model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled)
subject := fmt.Sprintf("通道「%s」#%d已被启用", channelName, channelId)
content := fmt.Sprintf("通道「%s」#%d已被启用", channelName, channelId)
notifyRootUser(subject, content)
}
func testAllChannels(notify bool) error {
if common.RootUserEmail == "" {
common.RootUserEmail = model.GetRootUserEmail()
}
func testAllChannels(isNotify bool) error {
testAllChannelsLock.Lock()
if testAllChannelsRunning {
testAllChannelsLock.Unlock()
@@ -168,33 +148,63 @@ func testAllChannels(notify bool) error {
disableThreshold = 10000000 // a impossible value
}
go func() {
var sendMessage string
for _, channel := range channels {
time.Sleep(common.RequestInterval)
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
sendMessage += fmt.Sprintf("**通道 %s (#%d) [%s]** : \n\n", channel.Name, channel.Id, channel.StatusToStr())
tik := time.Now()
err, openaiErr := testChannel(channel, "")
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
if milliseconds > disableThreshold {
err = fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
DisableChannel(channel.Id, channel.Name, err.Error())
}
if isChannelEnabled && ShouldDisableChannel(openaiErr, -1) {
DisableChannel(channel.Id, channel.Name, err.Error())
}
if !isChannelEnabled && shouldEnableChannel(err, openaiErr) {
enableChannel(channel.Id, channel.Name)
// 通道为禁用状态,并且还是请求错误 或者 响应时间超过阈值 直接跳过,也不需要更新响应时间。
if !isChannelEnabled {
if err != nil {
sendMessage += fmt.Sprintf("- 测试报错: %s \n\n- 无需改变状态,跳过\n\n", err.Error())
continue
}
if milliseconds > disableThreshold {
sendMessage += fmt.Sprintf("- 响应时间 %.2fs 超过阈值 %.2fs \n\n- 无需改变状态,跳过\n\n", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
continue
}
// 如果已被禁用,但是请求成功,需要判断是否需要恢复
// 手动禁用的通道,不会自动恢复
if shouldEnableChannel(err, openaiErr) {
if channel.Status == common.ChannelStatusAutoDisabled {
EnableChannel(channel.Id, channel.Name, false)
sendMessage += "- 已被启用 \n\n"
} else {
sendMessage += "- 手动禁用的通道,不会自动恢复 \n\n"
}
}
} else {
// 如果通道启用状态,但是返回了错误 或者 响应时间超过阈值,需要判断是否需要禁用
if milliseconds > disableThreshold {
sendMessage += fmt.Sprintf("- 响应时间 %.2fs 超过阈值 %.2fs \n\n- 禁用\n\n", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
DisableChannel(channel.Id, channel.Name, err.Error(), false)
continue
}
if ShouldDisableChannel(openaiErr, -1) {
sendMessage += fmt.Sprintf("- 已被禁用,原因:%s\n\n", err.Error())
DisableChannel(channel.Id, channel.Name, err.Error(), false)
continue
}
if err != nil {
sendMessage += fmt.Sprintf("- 测试报错: %s \n\n", err.Error())
continue
}
}
channel.UpdateResponseTime(milliseconds)
time.Sleep(common.RequestInterval)
sendMessage += fmt.Sprintf("- 测试完成,耗时 %.2fs\n\n", float64(milliseconds)/1000.0)
}
testAllChannelsLock.Lock()
testAllChannelsRunning = false
testAllChannelsLock.Unlock()
if notify {
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
if err != nil {
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
}
if isNotify {
notify.Send("通道测试完成", sendMessage)
}
}()
return nil

View File

@@ -4,8 +4,10 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/common/notify"
"one-api/model"
"one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
@@ -36,18 +38,62 @@ func ShouldDisableChannel(err *types.OpenAIError, statusCode int) bool {
return true
}
if err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
switch err.Type {
case "insufficient_quota":
return true
// https://docs.anthropic.com/claude/reference/errors
case "authentication_error":
return true
case "permission_error":
return true
case "forbidden":
return true
}
if err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
return true
}
if strings.HasPrefix(err.Message, "Your credit balance is too low") { // anthropic
return true
} else if strings.HasPrefix(err.Message, "This organization has been disabled.") {
return true
}
if strings.Contains(err.Message, "credit") {
return true
}
if strings.Contains(err.Message, "balance") {
return true
}
if strings.Contains(err.Message, "Access denied") {
return true
}
return false
}
// disable & notify
func DisableChannel(channelId int, channelName string, reason string) {
func DisableChannel(channelId int, channelName string, reason string, sendNotify bool) {
model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
if !sendNotify {
return
}
subject := fmt.Sprintf("通道「%s」#%d已被禁用", channelName, channelId)
content := fmt.Sprintf("通道「%s」#%d已被禁用原因%s", channelName, channelId, reason)
notifyRootUser(subject, content)
notify.Send(subject, content)
}
// enable & notify
func EnableChannel(channelId int, channelName string, sendNotify bool) {
model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled)
if !sendNotify {
return
}
subject := fmt.Sprintf("通道「%s」#%d已被启用", channelName, channelId)
content := fmt.Sprintf("通道「%s」#%d已被启用", channelName, channelId)
notify.Send(subject, content)
}
func RelayNotImplemented(c *gin.Context) {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/common/stmp"
"one-api/common/telegram"
"one-api/model"
"strings"
@@ -109,11 +110,7 @@ func SendEmailVerification(c *gin.Context) {
}
code := common.GenerateVerificationCode(6)
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
"<p>您的验证码为: <strong>%s</strong></p>"+
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
err := stmp.SendVerificationCodeEmail(email, code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -136,22 +133,29 @@ func SendPasswordResetEmail(c *gin.Context) {
})
return
}
if !model.IsEmailAlreadyTaken(email) {
user := &model.User{
Email: email,
}
if err := user.FillUserByEmail(); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该邮箱地址未注册",
})
return
}
userName := user.DisplayName
if userName == "" {
userName = user.Username
}
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
subject := fmt.Sprintf("%s密码重置", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
err := stmp.SendPasswordResetEmail(userName, email, link)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -173,6 +177,14 @@ type PasswordResetRequest struct {
func ResetPassword(c *gin.Context) {
var req PasswordResetRequest
err := json.NewDecoder(c.Request.Body).Decode(&req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
return
}
if req.Email == "" || req.Token == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@@ -696,9 +696,7 @@ func EmailBind(c *gin.Context) {
})
return
}
if user.Role == common.RoleRootUser {
common.RootUserEmail = email
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",