mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-17 16:06:38 +08:00
commit
af02cdc58b
@ -38,6 +38,46 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Playground(c *gin.Context) {
|
||||||
|
var openaiErr *dto.OpenAIErrorWithStatusCode
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if openaiErr != nil {
|
||||||
|
c.JSON(openaiErr.StatusCode, gin.H{
|
||||||
|
"error": openaiErr.Error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
playgroundRequest := &dto.PlayGroundRequest{}
|
||||||
|
err := common.UnmarshalBodyReusable(c, playgroundRequest)
|
||||||
|
if err != nil {
|
||||||
|
openaiErr = service.OpenAIErrorWrapperLocal(err, "unmarshal_request_failed", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if playgroundRequest.Model == "" {
|
||||||
|
openaiErr = service.OpenAIErrorWrapperLocal(errors.New("请选择模型"), "model_required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("original_model", playgroundRequest.Model)
|
||||||
|
group := playgroundRequest.Group
|
||||||
|
if group == "" {
|
||||||
|
group = c.GetString("group")
|
||||||
|
} else {
|
||||||
|
c.Set("group", group)
|
||||||
|
}
|
||||||
|
log.Printf("group: %s", group)
|
||||||
|
log.Printf("model: %s", playgroundRequest.Model)
|
||||||
|
channel, err := model.CacheGetRandomSatisfiedChannel(group, playgroundRequest.Model, 0)
|
||||||
|
if err != nil {
|
||||||
|
openaiErr = service.OpenAIErrorWrapperLocal(err, "get_playground_channel_failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
|
||||||
|
Relay(c)
|
||||||
|
}
|
||||||
|
|
||||||
func Relay(c *gin.Context) {
|
func Relay(c *gin.Context) {
|
||||||
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
||||||
requestId := c.GetString(common.RequestIdKey)
|
requestId := c.GetString(common.RequestIdKey)
|
||||||
|
@ -68,6 +68,7 @@ func setupLogin(user *model.User, c *gin.Context) {
|
|||||||
session.Set("username", user.Username)
|
session.Set("username", user.Username)
|
||||||
session.Set("role", user.Role)
|
session.Set("role", user.Role)
|
||||||
session.Set("status", user.Status)
|
session.Set("status", user.Status)
|
||||||
|
session.Set("group", user.Group)
|
||||||
err := session.Save()
|
err := session.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
6
dto/playground.go
Normal file
6
dto/playground.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type PlayGroundRequest struct {
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Group string `json:"group,omitempty"`
|
||||||
|
}
|
@ -121,6 +121,7 @@ func authHelper(c *gin.Context, minRole int) {
|
|||||||
c.Set("username", username)
|
c.Set("username", username)
|
||||||
c.Set("role", role)
|
c.Set("role", role)
|
||||||
c.Set("id", id)
|
c.Set("id", id)
|
||||||
|
c.Set("group", session.Get("group"))
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
|
relaycommon "one-api/relay/common"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -257,52 +258,57 @@ func decreaseTokenQuota(id int, quota int) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func PreConsumeTokenQuota(tokenId int, quota int) (userQuota int, err error) {
|
func PreConsumeTokenQuota(relayInfo *relaycommon.RelayInfo, quota int) (userQuota int, err error) {
|
||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return 0, errors.New("quota 不能为负数!")
|
return 0, errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
token, err := GetTokenById(tokenId)
|
if !relayInfo.IsPlayground {
|
||||||
|
token, err := GetTokenById(relayInfo.TokenId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if !token.UnlimitedQuota && token.RemainQuota < quota {
|
if !token.UnlimitedQuota && token.RemainQuota < quota {
|
||||||
return 0, errors.New("令牌额度不足")
|
return 0, errors.New("令牌额度不足")
|
||||||
}
|
}
|
||||||
userQuota, err = GetUserQuota(token.UserId)
|
}
|
||||||
|
userQuota, err = GetUserQuota(relayInfo.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if userQuota < quota {
|
if userQuota < quota {
|
||||||
return 0, errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
|
return 0, errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
|
||||||
}
|
}
|
||||||
err = DecreaseTokenQuota(tokenId, quota)
|
if !relayInfo.IsPlayground {
|
||||||
|
err = DecreaseTokenQuota(relayInfo.TokenId, quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = DecreaseUserQuota(token.UserId, quota)
|
}
|
||||||
|
err = DecreaseUserQuota(relayInfo.UserId, quota)
|
||||||
return userQuota - quota, err
|
return userQuota - quota, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuota int, sendEmail bool) (err error) {
|
func PostConsumeTokenQuota(relayInfo *relaycommon.RelayInfo, userQuota int, quota int, preConsumedQuota int, sendEmail bool) (err error) {
|
||||||
token, err := GetTokenById(tokenId)
|
|
||||||
|
|
||||||
if quota > 0 {
|
if quota > 0 {
|
||||||
err = DecreaseUserQuota(token.UserId, quota)
|
err = DecreaseUserQuota(relayInfo.UserId, quota)
|
||||||
} else {
|
} else {
|
||||||
err = IncreaseUserQuota(token.UserId, -quota)
|
err = IncreaseUserQuota(relayInfo.UserId, -quota)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !relayInfo.IsPlayground {
|
||||||
if quota > 0 {
|
if quota > 0 {
|
||||||
err = DecreaseTokenQuota(tokenId, quota)
|
err = DecreaseTokenQuota(relayInfo.TokenId, quota)
|
||||||
} else {
|
} else {
|
||||||
err = IncreaseTokenQuota(tokenId, -quota)
|
err = IncreaseTokenQuota(relayInfo.TokenId, -quota)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if sendEmail {
|
if sendEmail {
|
||||||
if (quota + preConsumedQuota) != 0 {
|
if (quota + preConsumedQuota) != 0 {
|
||||||
@ -310,7 +316,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
|
|||||||
noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
|
noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
|
||||||
if quotaTooLow || noMoreQuota {
|
if quotaTooLow || noMoreQuota {
|
||||||
go func() {
|
go func() {
|
||||||
email, err := GetUserEmail(token.UserId)
|
email, err := GetUserEmail(relayInfo.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("failed to fetch user email: " + err.Error())
|
common.SysError("failed to fetch user email: " + err.Error())
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ type RelayInfo struct {
|
|||||||
setFirstResponse bool
|
setFirstResponse bool
|
||||||
ApiType int
|
ApiType int
|
||||||
IsStream bool
|
IsStream bool
|
||||||
|
IsPlayground bool
|
||||||
RelayMode int
|
RelayMode int
|
||||||
UpstreamModelName string
|
UpstreamModelName string
|
||||||
OriginModelName string
|
OriginModelName string
|
||||||
@ -65,6 +66,11 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|||||||
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
||||||
Organization: c.GetString("channel_organization"),
|
Organization: c.GetString("channel_organization"),
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(c.Request.URL.Path, "/pg") {
|
||||||
|
info.IsPlayground = true
|
||||||
|
info.RequestURLPath = strings.TrimPrefix(info.RequestURLPath, "/pg")
|
||||||
|
info.RequestURLPath = "/v1" + info.RequestURLPath
|
||||||
|
}
|
||||||
if info.BaseUrl == "" {
|
if info.BaseUrl == "" {
|
||||||
info.BaseUrl = common.ChannelBaseURLs[channelType]
|
info.BaseUrl = common.ChannelBaseURLs[channelType]
|
||||||
}
|
}
|
||||||
@ -146,3 +152,20 @@ func GenTaskRelayInfo(c *gin.Context) *TaskRelayInfo {
|
|||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (info *TaskRelayInfo) ToRelayInfo() *RelayInfo {
|
||||||
|
return &RelayInfo{
|
||||||
|
ChannelType: info.ChannelType,
|
||||||
|
ChannelId: info.ChannelId,
|
||||||
|
TokenId: info.TokenId,
|
||||||
|
UserId: info.UserId,
|
||||||
|
Group: info.Group,
|
||||||
|
StartTime: info.StartTime,
|
||||||
|
ApiType: info.ApiType,
|
||||||
|
RelayMode: info.RelayMode,
|
||||||
|
UpstreamModelName: info.UpstreamModelName,
|
||||||
|
RequestURLPath: info.RequestURLPath,
|
||||||
|
ApiKey: info.ApiKey,
|
||||||
|
BaseUrl: info.BaseUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -42,7 +42,7 @@ const (
|
|||||||
|
|
||||||
func Path2RelayMode(path string) int {
|
func Path2RelayMode(path string) int {
|
||||||
relayMode := RelayModeUnknown
|
relayMode := RelayModeUnknown
|
||||||
if strings.HasPrefix(path, "/v1/chat/completions") {
|
if strings.HasPrefix(path, "/v1/chat/completions") || strings.HasPrefix(path, "/pg/chat/completions") {
|
||||||
relayMode = RelayModeChatCompletions
|
relayMode = RelayModeChatCompletions
|
||||||
} else if strings.HasPrefix(path, "/v1/completions") {
|
} else if strings.HasPrefix(path, "/v1/completions") {
|
||||||
relayMode = RelayModeCompletions
|
relayMode = RelayModeCompletions
|
||||||
|
@ -87,7 +87,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
preConsumedQuota = 0
|
preConsumedQuota = 0
|
||||||
}
|
}
|
||||||
if preConsumedQuota > 0 {
|
if preConsumedQuota > 0 {
|
||||||
userQuota, err = model.PreConsumeTokenQuota(relayInfo.TokenId, preConsumedQuota)
|
userQuota, err = model.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return service.OpenAIErrorWrapperLocal(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
return service.OpenAIErrorWrapperLocal(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
openaiErr := service.RelayErrorHandler(resp)
|
openaiErr := service.RelayErrorHandler(resp)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
@ -136,7 +136,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
|
|
||||||
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
||||||
if openaiErr != nil {
|
if openaiErr != nil {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
return openaiErr
|
return openaiErr
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
relaycommon "one-api/relay/common"
|
||||||
relayconstant "one-api/relay/constant"
|
relayconstant "one-api/relay/constant"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -146,6 +147,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
|||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
|
relayInfo := relaycommon.GenRelayInfo(c)
|
||||||
var swapFaceRequest dto.SwapFaceRequest
|
var swapFaceRequest dto.SwapFaceRequest
|
||||||
err := common.UnmarshalBodyReusable(c, &swapFaceRequest)
|
err := common.UnmarshalBodyReusable(c, &swapFaceRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -191,7 +193,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
|||||||
}
|
}
|
||||||
defer func(ctx context.Context) {
|
defer func(ctx context.Context) {
|
||||||
if mjResp.StatusCode == 200 && mjResp.Response.Code == 1 {
|
if mjResp.StatusCode == 200 && mjResp.Response.Code == 1 {
|
||||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, quota, 0, true)
|
err := model.PostConsumeTokenQuota(relayInfo, userQuota, quota, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error consuming token remain quota: " + err.Error())
|
common.SysError("error consuming token remain quota: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -356,6 +358,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
|||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
|
relayInfo := relaycommon.GenRelayInfo(c)
|
||||||
consumeQuota := true
|
consumeQuota := true
|
||||||
var midjRequest dto.MidjourneyRequest
|
var midjRequest dto.MidjourneyRequest
|
||||||
err := common.UnmarshalBodyReusable(c, &midjRequest)
|
err := common.UnmarshalBodyReusable(c, &midjRequest)
|
||||||
@ -495,7 +498,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
|||||||
|
|
||||||
defer func(ctx context.Context) {
|
defer func(ctx context.Context) {
|
||||||
if consumeQuota && midjResponseWithStatus.StatusCode == 200 {
|
if consumeQuota && midjResponseWithStatus.StatusCode == 200 {
|
||||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, quota, 0, true)
|
err := model.PostConsumeTokenQuota(relayInfo, userQuota, quota, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error consuming token remain quota: " + err.Error())
|
common.SysError("error consuming token remain quota: " + err.Error())
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
if resp != nil {
|
if resp != nil {
|
||||||
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
openaiErr := service.RelayErrorHandler(resp)
|
openaiErr := service.RelayErrorHandler(resp)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
@ -188,7 +188,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
|
|
||||||
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
||||||
if openaiErr != nil {
|
if openaiErr != nil {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
return openaiErr
|
return openaiErr
|
||||||
@ -266,7 +266,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if preConsumedQuota > 0 {
|
if preConsumedQuota > 0 {
|
||||||
userQuota, err = model.PreConsumeTokenQuota(relayInfo.TokenId, preConsumedQuota)
|
userQuota, err = model.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, service.OpenAIErrorWrapperLocal(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
return 0, 0, service.OpenAIErrorWrapperLocal(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
@ -274,11 +274,11 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
|
|||||||
return preConsumedQuota, userQuota, nil
|
return preConsumedQuota, userQuota, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func returnPreConsumedQuota(c *gin.Context, tokenId int, userQuota int, preConsumedQuota int) {
|
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
|
||||||
if preConsumedQuota != 0 {
|
if preConsumedQuota != 0 {
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
// return pre-consumed quota
|
// return pre-consumed quota
|
||||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, -preConsumedQuota, 0, false)
|
err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error return pre-consumed quota: " + err.Error())
|
common.SysError("error return pre-consumed quota: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -336,7 +336,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
|
|||||||
//}
|
//}
|
||||||
quotaDelta := quota - preConsumedQuota
|
quotaDelta := quota - preConsumedQuota
|
||||||
if quotaDelta != 0 {
|
if quotaDelta != 0 {
|
||||||
err := model.PostConsumeTokenQuota(relayInfo.TokenId, userQuota, quotaDelta, preConsumedQuota, true)
|
err := model.PostConsumeTokenQuota(relayInfo, userQuota, quotaDelta, preConsumedQuota, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ func RerankHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
|
|||||||
}
|
}
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
openaiErr := service.RelayErrorHandler(resp)
|
openaiErr := service.RelayErrorHandler(resp)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
@ -111,7 +111,7 @@ func RerankHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
|
|||||||
|
|
||||||
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
|
||||||
if openaiErr != nil {
|
if openaiErr != nil {
|
||||||
returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
|
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||||
// reset status code 重置状态码
|
// reset status code 重置状态码
|
||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
return openaiErr
|
return openaiErr
|
||||||
|
@ -111,7 +111,8 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
|
|||||||
defer func(ctx context.Context) {
|
defer func(ctx context.Context) {
|
||||||
// release quota
|
// release quota
|
||||||
if relayInfo.ConsumeQuota && taskErr == nil {
|
if relayInfo.ConsumeQuota && taskErr == nil {
|
||||||
err := model.PostConsumeTokenQuota(relayInfo.TokenId, userQuota, quota, 0, true)
|
|
||||||
|
err := model.PostConsumeTokenQuota(relayInfo.ToRelayInfo(), userQuota, quota, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error consuming token remain quota: " + err.Error())
|
common.SysError("error consuming token remain quota: " + err.Error())
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,11 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
modelsRouter.GET("", controller.ListModels)
|
modelsRouter.GET("", controller.ListModels)
|
||||||
modelsRouter.GET("/:model", controller.RetrieveModel)
|
modelsRouter.GET("/:model", controller.RetrieveModel)
|
||||||
}
|
}
|
||||||
|
playgroundRouter := router.Group("/pg")
|
||||||
|
playgroundRouter.Use(middleware.UserAuth())
|
||||||
|
{
|
||||||
|
playgroundRouter.POST("/chat/completions", controller.Playground)
|
||||||
|
}
|
||||||
relayV1Router := router.Group("/v1")
|
relayV1Router := router.Group("/v1")
|
||||||
relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||||
{
|
{
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-icons": "^2.46.1",
|
"@douyinfe/semi-icons": "^2.63.1",
|
||||||
"@douyinfe/semi-ui": "^2.55.3",
|
"@douyinfe/semi-ui": "^2.63.1",
|
||||||
"@visactor/react-vchart": "~1.8.8",
|
"@visactor/react-vchart": "~1.8.8",
|
||||||
"@visactor/vchart": "~1.8.8",
|
"@visactor/vchart": "~1.8.8",
|
||||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||||
@ -22,7 +22,8 @@
|
|||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.0.8",
|
||||||
"react-turnstile": "^1.0.5",
|
"react-turnstile": "^1.0.5",
|
||||||
"semantic-ui-offline": "^2.5.0",
|
"semantic-ui-offline": "^2.5.0",
|
||||||
"semantic-ui-react": "^2.1.3"
|
"semantic-ui-react": "^2.1.3",
|
||||||
|
"sse": "github:mpetazzoni/sse.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
4219
web/pnpm-lock.yaml
4219
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,7 @@ import { Layout } from '@douyinfe/semi-ui';
|
|||||||
import Midjourney from './pages/Midjourney';
|
import Midjourney from './pages/Midjourney';
|
||||||
import Pricing from './pages/Pricing/index.js';
|
import Pricing from './pages/Pricing/index.js';
|
||||||
import Task from "./pages/Task/index.js";
|
import Task from "./pages/Task/index.js";
|
||||||
|
import Playground from './components/Playground.js';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const Detail = lazy(() => import('./pages/Detail'));
|
const Detail = lazy(() => import('./pages/Detail'));
|
||||||
@ -100,6 +101,14 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/playground'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Playground />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/redemption'
|
path='/redemption'
|
||||||
element={
|
element={
|
||||||
|
@ -39,10 +39,10 @@ let buttons = [
|
|||||||
// icon: <IconHomeStroked />,
|
// icon: <IconHomeStroked />,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// text: '模型价格',
|
// text: 'Playground',
|
||||||
// itemKey: 'pricing',
|
// itemKey: 'playground',
|
||||||
// to: '/pricing',
|
// to: '/playground',
|
||||||
// icon: <IconNoteMoneyStroked />,
|
// // icon: <IconNoteMoneyStroked />,
|
||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
335
web/src/components/Playground.js
Normal file
335
web/src/components/Playground.js
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
import { API, getUserIdFromLocalStorage, showError } from '../helpers';
|
||||||
|
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { SSE } from 'sse';
|
||||||
|
|
||||||
|
const defaultMessage = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
id: '2',
|
||||||
|
createAt: 1715676751919,
|
||||||
|
content: "你好",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
id: '3',
|
||||||
|
createAt: 1715676751919,
|
||||||
|
content: "你好,请问有什么可以帮助您的吗?",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let id = 4;
|
||||||
|
function getId() {
|
||||||
|
return `${id++}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const Playground = () => {
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
group: '',
|
||||||
|
max_tokens: 0,
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [status, setStatus] = useState({});
|
||||||
|
const [systemPrompt, setSystemPrompt] = useState('You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.');
|
||||||
|
const [message, setMessage] = useState(defaultMessage);
|
||||||
|
const [models, setModels] = useState([]);
|
||||||
|
const [groups, setGroups] = useState([]);
|
||||||
|
|
||||||
|
const handleInputChange = (name, value) => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.get('expired')) {
|
||||||
|
showError('未登录或登录已过期,请重新登录!');
|
||||||
|
}
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
setStatus(status);
|
||||||
|
}
|
||||||
|
loadModels();
|
||||||
|
loadGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadModels = async () => {
|
||||||
|
let res = await API.get(`/api/user/models`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let localModelOptions = data.map((model) => ({
|
||||||
|
label: model,
|
||||||
|
value: model,
|
||||||
|
}));
|
||||||
|
setModels(localModelOptions);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGroups = async () => {
|
||||||
|
let res = await API.get(`/api/user/groups`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
// return data is a map, key is group name, value is group description
|
||||||
|
// label is group description, value is group name
|
||||||
|
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||||
|
label: data[group],
|
||||||
|
value: group,
|
||||||
|
}));
|
||||||
|
// handleInputChange('group', localGroupOptions[0].value);
|
||||||
|
|
||||||
|
if (localGroupOptions.length > 0) {
|
||||||
|
} else {
|
||||||
|
localGroupOptions = [{
|
||||||
|
label: '用户分组',
|
||||||
|
value: '',
|
||||||
|
}];
|
||||||
|
setGroups(localGroupOptions);
|
||||||
|
}
|
||||||
|
setGroups(localGroupOptions);
|
||||||
|
handleInputChange('group', localGroupOptions[0].value);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonOuterStyle = {
|
||||||
|
border: '1px solid var(--semi-color-border)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
margin: '0px 8px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSystemMessage = () => {
|
||||||
|
if (systemPrompt !== '') {
|
||||||
|
return {
|
||||||
|
role: 'system',
|
||||||
|
id: '1',
|
||||||
|
createAt: 1715676751919,
|
||||||
|
content: systemPrompt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleSSE = (payload) => {
|
||||||
|
let source = new SSE('/pg/chat/completions', {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"New-Api-User": getUserIdFromLocalStorage(),
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
payload: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
source.addEventListener("message", (e) => {
|
||||||
|
if (e.data !== "[DONE]") {
|
||||||
|
let payload = JSON.parse(e.data);
|
||||||
|
// console.log("Payload: ", payload);
|
||||||
|
if (payload.choices.length === 0) {
|
||||||
|
source.close();
|
||||||
|
completeMessage();
|
||||||
|
} else {
|
||||||
|
let text = payload.choices[0].delta.content;
|
||||||
|
generateMockResponse(text);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completeMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
source.addEventListener("error", (e) => {
|
||||||
|
generateMockResponse(e.data)
|
||||||
|
completeMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
source.addEventListener("readystatechange", (e) => {
|
||||||
|
if (e.readyState >= 2) {
|
||||||
|
if (source.status === undefined) {
|
||||||
|
source.close();
|
||||||
|
completeMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
source.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMessageSend = useCallback((content, attachment) => {
|
||||||
|
console.log("attachment: ", attachment);
|
||||||
|
setMessage((prevMessage) => {
|
||||||
|
const newMessage = [
|
||||||
|
...prevMessage,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: content,
|
||||||
|
createAt: Date.now(),
|
||||||
|
id: getId()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 将 getPayload 移到这里
|
||||||
|
const getPayload = () => {
|
||||||
|
let systemMessage = getSystemMessage();
|
||||||
|
let messages = newMessage.map((item) => {
|
||||||
|
return {
|
||||||
|
role: item.role,
|
||||||
|
content: item.content,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (systemMessage) {
|
||||||
|
messages.unshift(systemMessage);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
messages: messages,
|
||||||
|
stream: true,
|
||||||
|
model: inputs.model,
|
||||||
|
group: inputs.group,
|
||||||
|
max_tokens: inputs.max_tokens,
|
||||||
|
temperature: inputs.temperature,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用更新后的消息状态调用 handleSSE
|
||||||
|
handleSSE(getPayload());
|
||||||
|
newMessage.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
createAt: Date.now(),
|
||||||
|
id: getId(),
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
return newMessage;
|
||||||
|
});
|
||||||
|
}, [getSystemMessage]);
|
||||||
|
|
||||||
|
const completeMessage = useCallback(() => {
|
||||||
|
setMessage((prevMessage) => {
|
||||||
|
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||||
|
return [
|
||||||
|
...prevMessage.slice(0, -1),
|
||||||
|
{ ...lastMessage, status: 'complete' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const generateMockResponse = useCallback((content) => {
|
||||||
|
// console.log("Generate Mock Response: ", content);
|
||||||
|
setMessage((message) => {
|
||||||
|
const lastMessage = message[message.length - 1];
|
||||||
|
let newMessage = {...lastMessage};
|
||||||
|
if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') {
|
||||||
|
newMessage = {
|
||||||
|
...newMessage,
|
||||||
|
content: (lastMessage.content || '') + content,
|
||||||
|
status: 'incomplete'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [ ...message.slice(0, -1), newMessage ]
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{height: '100%'}}>
|
||||||
|
<Layout.Sider>
|
||||||
|
<Card style={commonOuterStyle}>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>分组:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
placeholder={'请选择分组'}
|
||||||
|
name='group'
|
||||||
|
required
|
||||||
|
selection
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('group', value);
|
||||||
|
}}
|
||||||
|
value={inputs.group}
|
||||||
|
autoComplete='new-password'
|
||||||
|
optionList={groups}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>模型:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
placeholder={'请选择模型'}
|
||||||
|
name='model'
|
||||||
|
required
|
||||||
|
selection
|
||||||
|
filter
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('model', value);
|
||||||
|
}}
|
||||||
|
value={inputs.model}
|
||||||
|
autoComplete='new-password'
|
||||||
|
optionList={models}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>Temperature:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
step={0.1}
|
||||||
|
min={0.1}
|
||||||
|
max={1}
|
||||||
|
value={inputs.temperature}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('temperature', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>MaxTokens:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder='MaxTokens'
|
||||||
|
name='max_tokens'
|
||||||
|
required
|
||||||
|
autoComplete='new-password'
|
||||||
|
defaultValue={0}
|
||||||
|
value={inputs.max_tokens}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('max_tokens', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>System:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
placeholder='System Prompt'
|
||||||
|
name='system'
|
||||||
|
required
|
||||||
|
autoComplete='new-password'
|
||||||
|
autosize
|
||||||
|
defaultValue={systemPrompt}
|
||||||
|
// value={systemPrompt}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSystemPrompt(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
</Layout.Sider>
|
||||||
|
<Layout.Content>
|
||||||
|
<div style={{height: '100%'}}>
|
||||||
|
<Chat
|
||||||
|
chatBoxRenderConfig={{
|
||||||
|
renderChatBoxAction: () => {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={commonOuterStyle}
|
||||||
|
chats={message}
|
||||||
|
onMessageSend={onMessageSend}
|
||||||
|
showClearContext
|
||||||
|
onClear={() => {
|
||||||
|
setMessage([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Playground;
|
790
web/src/components/SafetySetting.js
Normal file
790
web/src/components/SafetySetting.js
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
||||||
|
|
||||||
|
import { useTheme } from '../context/Theme';
|
||||||
|
|
||||||
|
const SafetySetting = () => {
|
||||||
|
let [inputs, setInputs] = useState({
|
||||||
|
PasswordLoginEnabled: '',
|
||||||
|
PasswordRegisterEnabled: '',
|
||||||
|
EmailVerificationEnabled: '',
|
||||||
|
GitHubOAuthEnabled: '',
|
||||||
|
GitHubClientId: '',
|
||||||
|
GitHubClientSecret: '',
|
||||||
|
Notice: '',
|
||||||
|
SMTPServer: '',
|
||||||
|
SMTPPort: '',
|
||||||
|
SMTPAccount: '',
|
||||||
|
SMTPFrom: '',
|
||||||
|
SMTPToken: '',
|
||||||
|
ServerAddress: '',
|
||||||
|
WorkerUrl: '',
|
||||||
|
WorkerValidKey: '',
|
||||||
|
EpayId: '',
|
||||||
|
EpayKey: '',
|
||||||
|
Price: 7.3,
|
||||||
|
MinTopUp: 1,
|
||||||
|
TopupGroupRatio: '',
|
||||||
|
PayAddress: '',
|
||||||
|
CustomCallbackAddress: '',
|
||||||
|
Footer: '',
|
||||||
|
WeChatAuthEnabled: '',
|
||||||
|
WeChatServerAddress: '',
|
||||||
|
WeChatServerToken: '',
|
||||||
|
WeChatAccountQRCodeImageURL: '',
|
||||||
|
TurnstileCheckEnabled: '',
|
||||||
|
TurnstileSiteKey: '',
|
||||||
|
TurnstileSecretKey: '',
|
||||||
|
RegisterEnabled: '',
|
||||||
|
EmailDomainRestrictionEnabled: '',
|
||||||
|
EmailAliasRestrictionEnabled: '',
|
||||||
|
SMTPSSLEnabled: '',
|
||||||
|
EmailDomainWhitelist: [],
|
||||||
|
// telegram login
|
||||||
|
TelegramOAuthEnabled: '',
|
||||||
|
TelegramBotToken: '',
|
||||||
|
TelegramBotName: '',
|
||||||
|
});
|
||||||
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
|
let [loading, setLoading] = useState(false);
|
||||||
|
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||||
|
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||||
|
const [showPasswordWarningModal, setShowPasswordWarningModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const getOptions = async () => {
|
||||||
|
const res = await API.get('/api/option/');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let newInputs = {};
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.key === 'TopupGroupRatio') {
|
||||||
|
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||||
|
}
|
||||||
|
newInputs[item.key] = item.value;
|
||||||
|
});
|
||||||
|
setInputs({
|
||||||
|
...newInputs,
|
||||||
|
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
|
||||||
|
});
|
||||||
|
setOriginInputs(newInputs);
|
||||||
|
|
||||||
|
setEmailDomainWhitelist(
|
||||||
|
newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||||
|
return { key: item, text: item, value: item };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getOptions().then();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {}, [inputs.EmailDomainWhitelist]);
|
||||||
|
|
||||||
|
const updateOption = async (key, value) => {
|
||||||
|
setLoading(true);
|
||||||
|
switch (key) {
|
||||||
|
case 'PasswordLoginEnabled':
|
||||||
|
case 'PasswordRegisterEnabled':
|
||||||
|
case 'EmailVerificationEnabled':
|
||||||
|
case 'GitHubOAuthEnabled':
|
||||||
|
case 'WeChatAuthEnabled':
|
||||||
|
case 'TelegramOAuthEnabled':
|
||||||
|
case 'TurnstileCheckEnabled':
|
||||||
|
case 'EmailDomainRestrictionEnabled':
|
||||||
|
case 'EmailAliasRestrictionEnabled':
|
||||||
|
case 'SMTPSSLEnabled':
|
||||||
|
case 'RegisterEnabled':
|
||||||
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const res = await API.put('/api/option/', {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (key === 'EmailDomainWhitelist') {
|
||||||
|
value = value.split(',');
|
||||||
|
}
|
||||||
|
if (key === 'Price') {
|
||||||
|
value = parseFloat(value);
|
||||||
|
}
|
||||||
|
setInputs((inputs) => ({
|
||||||
|
...inputs,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = async (e, { name, value }) => {
|
||||||
|
if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
|
||||||
|
// block disabling password login
|
||||||
|
setShowPasswordWarningModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
name === 'Notice' ||
|
||||||
|
(name.startsWith('SMTP') && name !== 'SMTPSSLEnabled') ||
|
||||||
|
name === 'ServerAddress' ||
|
||||||
|
name === 'WorkerUrl' ||
|
||||||
|
name === 'WorkerValidKey' ||
|
||||||
|
name === 'EpayId' ||
|
||||||
|
name === 'EpayKey' ||
|
||||||
|
name === 'Price' ||
|
||||||
|
name === 'PayAddress' ||
|
||||||
|
name === 'GitHubClientId' ||
|
||||||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'WeChatServerAddress' ||
|
||||||
|
name === 'WeChatServerToken' ||
|
||||||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
|
name === 'TurnstileSiteKey' ||
|
||||||
|
name === 'TurnstileSecretKey' ||
|
||||||
|
name === 'EmailDomainWhitelist' ||
|
||||||
|
name === 'TopupGroupRatio' ||
|
||||||
|
name === 'TelegramBotToken' ||
|
||||||
|
name === 'TelegramBotName'
|
||||||
|
) {
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
} else {
|
||||||
|
await updateOption(name, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitServerAddress = async () => {
|
||||||
|
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
||||||
|
await updateOption('ServerAddress', ServerAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitWorker = async () => {
|
||||||
|
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
||||||
|
await updateOption('WorkerUrl', WorkerUrl);
|
||||||
|
if (inputs.WorkerValidKey !== '') {
|
||||||
|
await updateOption('WorkerValidKey', inputs.WorkerValidKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitPayAddress = async () => {
|
||||||
|
if (inputs.ServerAddress === '') {
|
||||||
|
showError('请先填写服务器地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||||
|
if (!verifyJSON(inputs.TopupGroupRatio)) {
|
||||||
|
showError('充值分组倍率不是合法的 JSON 字符串');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
|
||||||
|
}
|
||||||
|
let PayAddress = removeTrailingSlash(inputs.PayAddress);
|
||||||
|
await updateOption('PayAddress', PayAddress);
|
||||||
|
if (inputs.EpayId !== '') {
|
||||||
|
await updateOption('EpayId', inputs.EpayId);
|
||||||
|
}
|
||||||
|
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
||||||
|
await updateOption('EpayKey', inputs.EpayKey);
|
||||||
|
}
|
||||||
|
await updateOption('Price', '' + inputs.Price);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSMTP = async () => {
|
||||||
|
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
||||||
|
await updateOption('SMTPServer', inputs.SMTPServer);
|
||||||
|
}
|
||||||
|
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
||||||
|
await updateOption('SMTPAccount', inputs.SMTPAccount);
|
||||||
|
}
|
||||||
|
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
|
||||||
|
await updateOption('SMTPFrom', inputs.SMTPFrom);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['SMTPPort'] !== inputs.SMTPPort &&
|
||||||
|
inputs.SMTPPort !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('SMTPPort', inputs.SMTPPort);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['SMTPToken'] !== inputs.SMTPToken &&
|
||||||
|
inputs.SMTPToken !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('SMTPToken', inputs.SMTPToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitEmailDomainWhitelist = async () => {
|
||||||
|
if (
|
||||||
|
originInputs['EmailDomainWhitelist'] !==
|
||||||
|
inputs.EmailDomainWhitelist.join(',') &&
|
||||||
|
inputs.SMTPToken !== ''
|
||||||
|
) {
|
||||||
|
await updateOption(
|
||||||
|
'EmailDomainWhitelist',
|
||||||
|
inputs.EmailDomainWhitelist.join(','),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitWeChat = async () => {
|
||||||
|
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
||||||
|
await updateOption(
|
||||||
|
'WeChatServerAddress',
|
||||||
|
removeTrailingSlash(inputs.WeChatServerAddress),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['WeChatAccountQRCodeImageURL'] !==
|
||||||
|
inputs.WeChatAccountQRCodeImageURL
|
||||||
|
) {
|
||||||
|
await updateOption(
|
||||||
|
'WeChatAccountQRCodeImageURL',
|
||||||
|
inputs.WeChatAccountQRCodeImageURL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
|
||||||
|
inputs.WeChatServerToken !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('WeChatServerToken', inputs.WeChatServerToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitGitHubOAuth = async () => {
|
||||||
|
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
||||||
|
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
|
||||||
|
inputs.GitHubClientSecret !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitTelegramSettings = async () => {
|
||||||
|
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
|
||||||
|
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
|
||||||
|
await updateOption('TelegramBotName', inputs.TelegramBotName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitTurnstile = async () => {
|
||||||
|
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
||||||
|
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
|
||||||
|
inputs.TurnstileSecretKey !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitNewRestrictedDomain = () => {
|
||||||
|
const localDomainList = inputs.EmailDomainWhitelist;
|
||||||
|
if (
|
||||||
|
restrictedDomainInput !== '' &&
|
||||||
|
!localDomainList.includes(restrictedDomainInput)
|
||||||
|
) {
|
||||||
|
setRestrictedDomainInput('');
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
||||||
|
});
|
||||||
|
setEmailDomainWhitelist([
|
||||||
|
...EmailDomainWhitelist,
|
||||||
|
{
|
||||||
|
key: restrictedDomainInput,
|
||||||
|
text: restrictedDomainInput,
|
||||||
|
value: restrictedDomainInput,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid columns={1}>
|
||||||
|
<Grid.Column>
|
||||||
|
<Form loading={loading} inverted={isDark}>
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
通用设置
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='服务器地址'
|
||||||
|
placeholder='例如:https://yourdomain.com'
|
||||||
|
value={inputs.ServerAddress}
|
||||||
|
name='ServerAddress'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitServerAddress}>
|
||||||
|
更新服务器地址
|
||||||
|
</Form.Button>
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
代理设置(支持 <a href='https://github.com/Calcium-Ion/new-api-worker' target='_blank' rel='noreferrer'>new-api-worker</a>)
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='Worker地址,不填写则不启用代理'
|
||||||
|
placeholder='例如:https://workername.yourdomain.workers.dev'
|
||||||
|
value={inputs.WorkerUrl}
|
||||||
|
name='WorkerUrl'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Worker密钥,根据你部署的 Worker 填写'
|
||||||
|
placeholder='例如:your_secret_key'
|
||||||
|
value={inputs.WorkerValidKey}
|
||||||
|
name='WorkerValidKey'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitWorker}>
|
||||||
|
更新Worker设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='支付地址,不填写则不启用在线支付'
|
||||||
|
placeholder='例如:https://yourdomain.com'
|
||||||
|
value={inputs.PayAddress}
|
||||||
|
name='PayAddress'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='易支付商户ID'
|
||||||
|
placeholder='例如:0001'
|
||||||
|
value={inputs.EpayId}
|
||||||
|
name='EpayId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='易支付商户密钥'
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
value={inputs.EpayKey}
|
||||||
|
name='EpayKey'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='回调地址,不填写则使用上方服务器地址作为回调地址'
|
||||||
|
placeholder='例如:https://yourdomain.com'
|
||||||
|
value={inputs.CustomCallbackAddress}
|
||||||
|
name='CustomCallbackAddress'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='充值价格(x元/美金)'
|
||||||
|
placeholder='例如:7,就是7元/美金'
|
||||||
|
value={inputs.Price}
|
||||||
|
name='Price'
|
||||||
|
min={0}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='最低充值美元数量(以美金为单位,如果使用额度请自行换算!)'
|
||||||
|
placeholder='例如:2,就是最低充值2$'
|
||||||
|
value={inputs.MinTopUp}
|
||||||
|
name='MinTopUp'
|
||||||
|
min={1}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.TextArea
|
||||||
|
label='充值分组倍率'
|
||||||
|
name='TopupGroupRatio'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.TopupGroupRatio}
|
||||||
|
placeholder='为一个 JSON 文本,键为组名称,值为倍率'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitPayAddress}>更新支付设置</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置登录注册
|
||||||
|
</Header>
|
||||||
|
<Form.Group inline>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.PasswordLoginEnabled === 'true'}
|
||||||
|
label='允许通过密码进行登录'
|
||||||
|
name='PasswordLoginEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
{showPasswordWarningModal && (
|
||||||
|
<Modal
|
||||||
|
open={showPasswordWarningModal}
|
||||||
|
onClose={() => setShowPasswordWarningModal(false)}
|
||||||
|
size={'tiny'}
|
||||||
|
style={{ maxWidth: '450px' }}
|
||||||
|
>
|
||||||
|
<Modal.Header>警告</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<p>
|
||||||
|
取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
|
||||||
|
</p>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={() => setShowPasswordWarningModal(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='yellow'
|
||||||
|
onClick={async () => {
|
||||||
|
setShowPasswordWarningModal(false);
|
||||||
|
await updateOption('PasswordLoginEnabled', 'false');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||||
|
label='允许通过密码进行注册'
|
||||||
|
name='PasswordRegisterEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.EmailVerificationEnabled === 'true'}
|
||||||
|
label='通过密码注册时需要进行邮箱验证'
|
||||||
|
name='EmailVerificationEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||||
|
label='允许通过 GitHub 账户登录 & 注册'
|
||||||
|
name='GitHubOAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.WeChatAuthEnabled === 'true'}
|
||||||
|
label='允许通过微信登录 & 注册'
|
||||||
|
name='WeChatAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.TelegramOAuthEnabled === 'true'}
|
||||||
|
label='允许通过 Telegram 进行登录'
|
||||||
|
name='TelegramOAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group inline>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.RegisterEnabled === 'true'}
|
||||||
|
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
|
||||||
|
name='RegisterEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.TurnstileCheckEnabled === 'true'}
|
||||||
|
label='启用 Turnstile 用户校验'
|
||||||
|
name='TurnstileCheckEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置邮箱域名白名单
|
||||||
|
<Header.Subheader>
|
||||||
|
用以防止恶意用户利用临时邮箱批量注册
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Checkbox
|
||||||
|
label='启用邮箱域名白名单'
|
||||||
|
name='EmailDomainRestrictionEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Checkbox
|
||||||
|
label='启用邮箱别名限制(例如:ab.cd@gmail.com)'
|
||||||
|
name='EmailAliasRestrictionEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
checked={inputs.EmailAliasRestrictionEnabled === 'true'}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={2}>
|
||||||
|
<Form.Dropdown
|
||||||
|
label='允许的邮箱域名'
|
||||||
|
placeholder='允许的邮箱域名'
|
||||||
|
name='EmailDomainWhitelist'
|
||||||
|
required
|
||||||
|
fluid
|
||||||
|
multiple
|
||||||
|
selection
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.EmailDomainWhitelist}
|
||||||
|
autoComplete='new-password'
|
||||||
|
options={EmailDomainWhitelist}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='添加新的允许的邮箱域名'
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={() => {
|
||||||
|
submitNewRestrictedDomain();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
submitNewRestrictedDomain();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoComplete='new-password'
|
||||||
|
placeholder='输入新的允许的邮箱域名'
|
||||||
|
value={restrictedDomainInput}
|
||||||
|
onChange={(e, { value }) => {
|
||||||
|
setRestrictedDomainInput(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitEmailDomainWhitelist}>
|
||||||
|
保存邮箱域名白名单设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 SMTP
|
||||||
|
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 服务器地址'
|
||||||
|
name='SMTPServer'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPServer}
|
||||||
|
placeholder='例如:smtp.qq.com'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 端口'
|
||||||
|
name='SMTPPort'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPPort}
|
||||||
|
placeholder='默认: 587'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 账户'
|
||||||
|
name='SMTPAccount'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPAccount}
|
||||||
|
placeholder='通常是邮箱地址'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 发送者邮箱'
|
||||||
|
name='SMTPFrom'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPFrom}
|
||||||
|
placeholder='通常和邮箱地址保持一致'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 访问凭证'
|
||||||
|
name='SMTPToken'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
checked={inputs.RegisterEnabled === 'true'}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Checkbox
|
||||||
|
label='启用SMTP SSL(465端口强制开启)'
|
||||||
|
name='SMTPSSLEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
checked={inputs.SMTPSSLEnabled === 'true'}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 GitHub OAuth App
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过 GitHub 进行登录注册,
|
||||||
|
<a
|
||||||
|
href='https://github.com/settings/developers'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的 GitHub OAuth App
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,Authorization callback URL 填{' '}
|
||||||
|
<code>{`${inputs.ServerAddress}/oauth/github`}</code>
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='GitHub Client ID'
|
||||||
|
name='GitHubClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.GitHubClientId}
|
||||||
|
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='GitHub Client Secret'
|
||||||
|
name='GitHubClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.GitHubClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitGitHubOAuth}>
|
||||||
|
保存 GitHub OAuth 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 WeChat Server
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过微信进行登录注册,
|
||||||
|
<a
|
||||||
|
href='https://github.com/songquanpeng/wechat-server'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
了解 WeChat Server
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='WeChat Server 服务器地址'
|
||||||
|
name='WeChatServerAddress'
|
||||||
|
placeholder='例如:https://yourdomain.com'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.WeChatServerAddress}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='WeChat Server 访问凭证'
|
||||||
|
name='WeChatServerToken'
|
||||||
|
type='password'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.WeChatServerToken}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='微信公众号二维码图片链接'
|
||||||
|
name='WeChatAccountQRCodeImageURL'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.WeChatAccountQRCodeImageURL}
|
||||||
|
placeholder='输入一个图片链接'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitWeChat}>
|
||||||
|
保存 WeChat Server 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 Telegram 登录
|
||||||
|
</Header>
|
||||||
|
<Form.Group inline>
|
||||||
|
<Form.Input
|
||||||
|
label='Telegram Bot Token'
|
||||||
|
name='TelegramBotToken'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.TelegramBotToken}
|
||||||
|
placeholder='输入你的 Telegram Bot Token'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Telegram Bot 名称'
|
||||||
|
name='TelegramBotName'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.TelegramBotName}
|
||||||
|
placeholder='输入你的 Telegram Bot 名称'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitTelegramSettings}>
|
||||||
|
保存 Telegram 登录设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 Turnstile
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持用户校验,
|
||||||
|
<a
|
||||||
|
href='https://dash.cloudflare.com/'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='Turnstile Site Key'
|
||||||
|
name='TurnstileSiteKey'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.TurnstileSiteKey}
|
||||||
|
placeholder='输入你注册的 Turnstile Site Key'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Turnstile Secret Key'
|
||||||
|
name='TurnstileSecretKey'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.TurnstileSecretKey}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitTurnstile}>
|
||||||
|
保存 Turnstile 设置
|
||||||
|
</Form.Button>
|
||||||
|
</Form>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSetting;
|
@ -15,7 +15,7 @@ import '../index.css';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
IconCalendarClock, IconChecklistStroked,
|
IconCalendarClock, IconChecklistStroked,
|
||||||
IconComment,
|
IconComment, IconCommentStroked,
|
||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconGift, IconHelpCircle,
|
IconGift, IconHelpCircle,
|
||||||
IconHistogram,
|
IconHistogram,
|
||||||
@ -63,6 +63,7 @@ const SiderBar = () => {
|
|||||||
detail: '/detail',
|
detail: '/detail',
|
||||||
pricing: '/pricing',
|
pricing: '/pricing',
|
||||||
task: '/task',
|
task: '/task',
|
||||||
|
playground: '/playground',
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerButtons = useMemo(
|
const headerButtons = useMemo(
|
||||||
@ -73,6 +74,12 @@ const SiderBar = () => {
|
|||||||
// to: '/',
|
// to: '/',
|
||||||
// icon: <IconHome />,
|
// icon: <IconHome />,
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
text: 'Playground',
|
||||||
|
itemKey: 'playground',
|
||||||
|
to: '/playground',
|
||||||
|
icon: <IconCommentStroked />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: '模型价格',
|
text: '模型价格',
|
||||||
itemKey: 'pricing',
|
itemKey: 'pricing',
|
||||||
|
@ -59,6 +59,12 @@ body {
|
|||||||
display: revert;
|
display: revert;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semi-chat {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.tableHiddle {
|
.tableHiddle {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,10 @@ export default defineConfig({
|
|||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/pg': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user