mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-28 04:13:42 +08:00
Compare commits
6 Commits
v0.6.2-alp
...
v0.6.2-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
358920c858 | ||
|
|
1ea598c773 | ||
|
|
796be42487 | ||
|
|
5b50eb94e5 | ||
|
|
71c61365eb | ||
|
|
b09f979b80 |
@@ -106,6 +106,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。
|
23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。
|
||||||
|
24. 配合 [Message Pusher](https://github.com/songquanpeng/message-pusher) 可将报警信息推送到多种 App 上。
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
@@ -375,7 +376,7 @@ graph LR
|
|||||||
16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。
|
16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。
|
||||||
17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。
|
17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。
|
||||||
18. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。
|
18. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。
|
||||||
19. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。
|
19. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。
|
||||||
20. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。
|
20. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。
|
||||||
21. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。
|
21. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ var WeChatServerAddress = ""
|
|||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
|
||||||
|
var MessagePusherAddress = ""
|
||||||
|
var MessagePusherToken = ""
|
||||||
|
|
||||||
var TurnstileSiteKey = ""
|
var TurnstileSiteKey = ""
|
||||||
var TurnstileSecretKey = ""
|
var TurnstileSecretKey = ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package common
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func SendEmail(subject string, receiver string, content string) error {
|
func SendEmail(subject string, receiver string, content string) error {
|
||||||
|
if receiver == "" {
|
||||||
|
return fmt.Errorf("receiver is empty")
|
||||||
|
}
|
||||||
if config.SMTPFrom == "" { // for compatibility
|
if config.SMTPFrom == "" { // for compatibility
|
||||||
config.SMTPFrom = config.SMTPAccount
|
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
|
||||||
|
}
|
||||||
@@ -148,6 +148,26 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddNewMissingRatio(oldRatio string) string {
|
||||||
|
newRatio := make(map[string]float64)
|
||||||
|
err := json.Unmarshal([]byte(oldRatio), &newRatio)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError("error unmarshalling old ratio: " + err.Error())
|
||||||
|
return oldRatio
|
||||||
|
}
|
||||||
|
for k, v := range DefaultModelRatio {
|
||||||
|
if _, ok := newRatio[k]; !ok {
|
||||||
|
newRatio[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(newRatio)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError("error marshalling new ratio: " + err.Error())
|
||||||
|
return oldRatio
|
||||||
|
}
|
||||||
|
return string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
func ModelRatio2JSONString() string {
|
func ModelRatio2JSONString() string {
|
||||||
jsonBytes, err := json.Marshal(ModelRatio)
|
jsonBytes, err := json.Marshal(ModelRatio)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ func UpdateChannelBalance(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateAllChannelsBalance() error {
|
func updateAllChannelsBalance() error {
|
||||||
channels, err := model.GetAllChannels(0, 0, true)
|
channels, err := model.GetAllChannels(0, 0, "all")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/songquanpeng/one-api/common"
|
"github.com/songquanpeng/one-api/common"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"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/middleware"
|
||||||
"github.com/songquanpeng/one-api/model"
|
"github.com/songquanpeng/one-api/model"
|
||||||
"github.com/songquanpeng/one-api/monitor"
|
"github.com/songquanpeng/one-api/monitor"
|
||||||
@@ -149,7 +150,7 @@ func TestChannel(c *gin.Context) {
|
|||||||
var testAllChannelsLock sync.Mutex
|
var testAllChannelsLock sync.Mutex
|
||||||
var testAllChannelsRunning bool = false
|
var testAllChannelsRunning bool = false
|
||||||
|
|
||||||
func testAllChannels(notify bool) error {
|
func testChannels(notify bool, scope string) error {
|
||||||
if config.RootUserEmail == "" {
|
if config.RootUserEmail == "" {
|
||||||
config.RootUserEmail = model.GetRootUserEmail()
|
config.RootUserEmail = model.GetRootUserEmail()
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,7 @@ func testAllChannels(notify bool) error {
|
|||||||
}
|
}
|
||||||
testAllChannelsRunning = true
|
testAllChannelsRunning = true
|
||||||
testAllChannelsLock.Unlock()
|
testAllChannelsLock.Unlock()
|
||||||
channels, err := model.GetAllChannels(0, 0, true)
|
channels, err := model.GetAllChannels(0, 0, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -192,7 +193,7 @@ func testAllChannels(notify bool) error {
|
|||||||
testAllChannelsRunning = false
|
testAllChannelsRunning = false
|
||||||
testAllChannelsLock.Unlock()
|
testAllChannelsLock.Unlock()
|
||||||
if notify {
|
if notify {
|
||||||
err := common.SendEmail("通道测试完成", config.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
err := message.Notify(message.ByAll, "通道测试完成", "", "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
||||||
}
|
}
|
||||||
@@ -201,8 +202,12 @@ func testAllChannels(notify bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllChannels(c *gin.Context) {
|
func TestChannels(c *gin.Context) {
|
||||||
err := testAllChannels(true)
|
scope := c.Query("scope")
|
||||||
|
if scope == "" {
|
||||||
|
scope = "all"
|
||||||
|
}
|
||||||
|
err := testChannels(true, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -221,7 +226,7 @@ func AutomaticallyTestChannels(frequency int) {
|
|||||||
for {
|
for {
|
||||||
time.Sleep(time.Duration(frequency) * time.Minute)
|
time.Sleep(time.Duration(frequency) * time.Minute)
|
||||||
logger.SysLog("testing all channels")
|
logger.SysLog("testing all channels")
|
||||||
_ = testAllChannels(false)
|
_ = testChannels(false, "all")
|
||||||
logger.SysLog("channel test finished")
|
logger.SysLog("channel test finished")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
if p < 0 {
|
if p < 0 {
|
||||||
p = 0
|
p = 0
|
||||||
}
|
}
|
||||||
channels, err := model.GetAllChannels(p*config.ItemsPerPage, config.ItemsPerPage, false)
|
channels, err := model.GetAllChannels(p*config.ItemsPerPage, config.ItemsPerPage, "limited")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/songquanpeng/one-api/common"
|
"github.com/songquanpeng/one-api/common"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
|
"github.com/songquanpeng/one-api/common/message"
|
||||||
"github.com/songquanpeng/one-api/model"
|
"github.com/songquanpeng/one-api/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -110,7 +111,7 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||||
"<p>您的验证码为: <strong>%s</strong></p>"+
|
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, code, common.VerificationValidMinutes)
|
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, code, common.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := message.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -149,7 +150,7 @@ func SendPasswordResetEmail(c *gin.Context) {
|
|||||||
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||||
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, link, link, common.VerificationValidMinutes)
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, link, link, common.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := message.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
type Channel struct {
|
type Channel struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Type int `json:"type" gorm:"default:0"`
|
Type int `json:"type" gorm:"default:0"`
|
||||||
Key string `json:"key" gorm:"not null;index"`
|
Key string `json:"key" gorm:"type:text"`
|
||||||
Status int `json:"status" gorm:"default:1"`
|
Status int `json:"status" gorm:"default:1"`
|
||||||
Name string `json:"name" gorm:"index"`
|
Name string `json:"name" gorm:"index"`
|
||||||
Weight *uint `json:"weight" gorm:"default:0"`
|
Weight *uint `json:"weight" gorm:"default:0"`
|
||||||
@@ -32,23 +32,22 @@ type Channel struct {
|
|||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, scope string) ([]*Channel, error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
var err error
|
var err error
|
||||||
if selectAll {
|
switch scope {
|
||||||
|
case "all":
|
||||||
err = DB.Order("id desc").Find(&channels).Error
|
err = DB.Order("id desc").Find(&channels).Error
|
||||||
} else {
|
case "disabled":
|
||||||
|
err = DB.Order("id desc").Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Find(&channels).Error
|
||||||
|
default:
|
||||||
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
||||||
}
|
}
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
||||||
keyCol := "`key`"
|
err = DB.Omit("key").Where("id = ? or name LIKE ?", helper.String2Int(keyword), keyword+"%").Find(&channels).Error
|
||||||
if common.UsingPostgreSQL {
|
|
||||||
keyCol = `"key"`
|
|
||||||
}
|
|
||||||
err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", helper.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error
|
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ func InitOptionMap() {
|
|||||||
config.OptionMap["WeChatServerAddress"] = ""
|
config.OptionMap["WeChatServerAddress"] = ""
|
||||||
config.OptionMap["WeChatServerToken"] = ""
|
config.OptionMap["WeChatServerToken"] = ""
|
||||||
config.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
config.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
|
config.OptionMap["MessagePusherAddress"] = ""
|
||||||
|
config.OptionMap["MessagePusherToken"] = ""
|
||||||
config.OptionMap["TurnstileSiteKey"] = ""
|
config.OptionMap["TurnstileSiteKey"] = ""
|
||||||
config.OptionMap["TurnstileSecretKey"] = ""
|
config.OptionMap["TurnstileSecretKey"] = ""
|
||||||
config.OptionMap["QuotaForNewUser"] = strconv.Itoa(config.QuotaForNewUser)
|
config.OptionMap["QuotaForNewUser"] = strconv.Itoa(config.QuotaForNewUser)
|
||||||
@@ -79,6 +81,9 @@ func InitOptionMap() {
|
|||||||
func loadOptionsFromDatabase() {
|
func loadOptionsFromDatabase() {
|
||||||
options, _ := AllOption()
|
options, _ := AllOption()
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
|
if option.Key == "ModelRatio" {
|
||||||
|
option.Value = common.AddNewMissingRatio(option.Value)
|
||||||
|
}
|
||||||
err := updateOptionMap(option.Key, option.Value)
|
err := updateOptionMap(option.Key, option.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.SysError("failed to update option map: " + err.Error())
|
logger.SysError("failed to update option map: " + err.Error())
|
||||||
@@ -179,6 +184,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
config.WeChatServerToken = value
|
config.WeChatServerToken = value
|
||||||
case "WeChatAccountQRCodeImageURL":
|
case "WeChatAccountQRCodeImageURL":
|
||||||
config.WeChatAccountQRCodeImageURL = value
|
config.WeChatAccountQRCodeImageURL = value
|
||||||
|
case "MessagePusherAddress":
|
||||||
|
config.MessagePusherAddress = value
|
||||||
|
case "MessagePusherToken":
|
||||||
|
config.MessagePusherToken = value
|
||||||
case "TurnstileSiteKey":
|
case "TurnstileSiteKey":
|
||||||
config.TurnstileSiteKey = value
|
config.TurnstileSiteKey = value
|
||||||
case "TurnstileSecretKey":
|
case "TurnstileSecretKey":
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
"github.com/songquanpeng/one-api/common/helper"
|
"github.com/songquanpeng/one-api/common/helper"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/common/message"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ func PreConsumeTokenQuota(tokenId int, quota int) (err error) {
|
|||||||
}
|
}
|
||||||
if email != "" {
|
if email != "" {
|
||||||
topUpLink := fmt.Sprintf("%s/topup", config.ServerAddress)
|
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))
|
fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.SysError("failed to send email" + err.Error())
|
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"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/common/message"
|
||||||
"github.com/songquanpeng/one-api/model"
|
"github.com/songquanpeng/one-api/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyRootUser(subject string, content string) {
|
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 == "" {
|
if config.RootUserEmail == "" {
|
||||||
config.RootUserEmail = model.GetRootUserEmail()
|
config.RootUserEmail = model.GetRootUserEmail()
|
||||||
}
|
}
|
||||||
err := common.SendEmail(subject, config.RootUserEmail, content)
|
err := message.SendEmail(subject, config.RootUserEmail, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,16 @@ func ShouldDisableChannel(err *relaymodel.Error, statusCode int) bool {
|
|||||||
if statusCode == http.StatusUnauthorized {
|
if statusCode == http.StatusUnauthorized {
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
if err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -101,6 +110,9 @@ func RelayErrorHandler(resp *http.Response) (ErrorWithStatusCode *relaymodel.Err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if config.DebugEnabled {
|
||||||
|
logger.SysLog(fmt.Sprintf("error happened, status code: %d, response: \n%s", resp.StatusCode, string(responseBody)))
|
||||||
|
}
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.GET("/search", controller.SearchChannels)
|
channelRoute.GET("/search", controller.SearchChannels)
|
||||||
channelRoute.GET("/models", controller.ListModels)
|
channelRoute.GET("/models", controller.ListModels)
|
||||||
channelRoute.GET("/:id", controller.GetChannel)
|
channelRoute.GET("/:id", controller.GetChannel)
|
||||||
channelRoute.GET("/test", controller.TestAllChannels)
|
channelRoute.GET("/test", controller.TestChannels)
|
||||||
channelRoute.GET("/test/:id", controller.TestChannel)
|
channelRoute.GET("/test/:id", controller.TestChannel)
|
||||||
channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
|
channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
|
||||||
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
||||||
|
|||||||
@@ -240,11 +240,11 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testAllChannels = async () => {
|
const testChannels = async (scope) => {
|
||||||
const res = await API.get(`/api/channel/test`);
|
const res = await API.get(`/api/channel/test?scope=${scope}`);
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
|
showInfo('已成功开始测试通道,请刷新页面查看结果。');
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -529,9 +529,12 @@ const ChannelsTable = () => {
|
|||||||
<Button size='small' as={Link} to='/channel/add' loading={loading}>
|
<Button size='small' as={Link} to='/channel/add' loading={loading}>
|
||||||
添加新的渠道
|
添加新的渠道
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='small' loading={loading} onClick={testAllChannels}>
|
<Button size='small' loading={loading} onClick={()=>{testChannels("all")}}>
|
||||||
测试所有渠道
|
测试所有渠道
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size='small' loading={loading} onClick={()=>{testChannels("disabled")}}>
|
||||||
|
测试禁用渠道
|
||||||
|
</Button>
|
||||||
{/*<Button size='small' onClick={updateAllChannelsBalance}*/}
|
{/*<Button size='small' onClick={updateAllChannelsBalance}*/}
|
||||||
{/* loading={loading || updatingBalance}>更新已启用渠道余额</Button>*/}
|
{/* loading={loading || updatingBalance}>更新已启用渠道余额</Button>*/}
|
||||||
<Popup
|
<Popup
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ const PasswordResetForm = () => {
|
|||||||
const [disableButton, setDisableButton] = useState(false);
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(30);
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
if (status.turnstile_check) {
|
||||||
|
setTurnstileEnabled(true);
|
||||||
|
setTurnstileSiteKey(status.turnstile_site_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let countdownInterval = null;
|
let countdownInterval = null;
|
||||||
if (disableButton && countdown > 0) {
|
if (disableButton && countdown > 0) {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const SystemSetting = () => {
|
|||||||
WeChatServerAddress: '',
|
WeChatServerAddress: '',
|
||||||
WeChatServerToken: '',
|
WeChatServerToken: '',
|
||||||
WeChatAccountQRCodeImageURL: '',
|
WeChatAccountQRCodeImageURL: '',
|
||||||
|
MessagePusherAddress: '',
|
||||||
|
MessagePusherToken: '',
|
||||||
TurnstileCheckEnabled: '',
|
TurnstileCheckEnabled: '',
|
||||||
TurnstileSiteKey: '',
|
TurnstileSiteKey: '',
|
||||||
TurnstileSecretKey: '',
|
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 () => {
|
const submitGitHubOAuth = async () => {
|
||||||
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
||||||
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
||||||
@@ -496,6 +513,42 @@ const SystemSetting = () => {
|
|||||||
保存 WeChat Server 设置
|
保存 WeChat Server 设置
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
<Divider />
|
<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'>
|
<Header as='h3'>
|
||||||
配置 Turnstile
|
配置 Turnstile
|
||||||
<Header.Subheader>
|
<Header.Subheader>
|
||||||
|
|||||||
Reference in New Issue
Block a user