mirror of
				https://github.com/linux-do/new-api.git
				synced 2025-11-04 21:33:41 +08:00 
			
		
		
		
	merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
		@@ -124,10 +124,11 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 | 
				
			|||||||
[对接文档](Suno.md)
 | 
					[对接文档](Suno.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 界面截图
 | 
					## 界面截图
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||

 | 
					 | 
				
			||||||
夜间模式  
 | 
					夜间模式  
 | 
				
			||||||

 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -143,6 +143,10 @@ const (
 | 
				
			|||||||
	RoleRootUser   = 100
 | 
						RoleRootUser   = 100
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func IsValidateRole(role int) bool {
 | 
				
			||||||
 | 
						return role == RoleGuestUser || role == RoleCommonUser || role == RoleAdminUser || role == RoleRootUser
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	FileUploadPermission    = RoleGuestUser
 | 
						FileUploadPermission    = RoleGuestUser
 | 
				
			||||||
	FileDownloadPermission  = RoleGuestUser
 | 
						FileDownloadPermission  = RoleGuestUser
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -350,13 +350,14 @@ func GetCompletionRatio(name string) float64 {
 | 
				
			|||||||
		return 4.0 / 3.0
 | 
							return 4.0 / 3.0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if strings.HasPrefix(name, "gpt-4") && name != "gpt-4-all" && name != "gpt-4-gizmo-*" {
 | 
						if strings.HasPrefix(name, "gpt-4") && name != "gpt-4-all" && name != "gpt-4-gizmo-*" {
 | 
				
			||||||
		if strings.HasPrefix(name, "gpt-4o-mini") || "gpt-4o-2024-08-06" == name {
 | 
							if strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4-turbo") || "gpt-4o-2024-05-13" == name {
 | 
				
			||||||
 | 
								return 3
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if strings.HasPrefix(name, "gpt-4o") {
 | 
				
			||||||
			return 4
 | 
								return 4
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4-turbo") || strings.HasPrefix(name, "gpt-4o") {
 | 
					 | 
				
			||||||
			return 3
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return 2
 | 
							return 2
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if "o1" == name || strings.HasPrefix(name, "o1-") {
 | 
						if "o1" == name || strings.HasPrefix(name, "o1-") {
 | 
				
			||||||
@@ -376,10 +377,7 @@ func GetCompletionRatio(name string) float64 {
 | 
				
			|||||||
		return 3
 | 
							return 3
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if strings.HasPrefix(name, "gemini-") {
 | 
						if strings.HasPrefix(name, "gemini-") {
 | 
				
			||||||
		if strings.Contains(name, "flash") {
 | 
							return 4
 | 
				
			||||||
			return 4
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return 3
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if strings.HasPrefix(name, "command") {
 | 
						if strings.HasPrefix(name, "command") {
 | 
				
			||||||
		switch name {
 | 
							switch name {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,3 +21,8 @@ func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
 | 
				
			|||||||
	UserUsableGroups = make(map[string]string)
 | 
						UserUsableGroups = make(map[string]string)
 | 
				
			||||||
	return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
 | 
						return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GroupInUserUsableGroups(groupName string) bool {
 | 
				
			||||||
 | 
						_, ok := UserUsableGroups[groupName]
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,11 +3,14 @@ package common
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						crand "crypto/rand"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"github.com/google/uuid"
 | 
						"github.com/google/uuid"
 | 
				
			||||||
	"golang.org/x/net/proxy"
 | 
						"golang.org/x/net/proxy"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
 | 
						"math/big"
 | 
				
			||||||
	"math/rand"
 | 
						"math/rand"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
@@ -147,24 +150,35 @@ func GetUUID() string {
 | 
				
			|||||||
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 | 
					const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	rand.Seed(time.Now().UnixNano())
 | 
						rand.New(rand.NewSource(time.Now().UnixNano()))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GenerateKey() string {
 | 
					func GenerateRandomCharsKey(length int) (string, error) {
 | 
				
			||||||
	//rand.Seed(time.Now().UnixNano())
 | 
						b := make([]byte, length)
 | 
				
			||||||
	key := make([]byte, 48)
 | 
						maxI := big.NewInt(int64(len(keyChars)))
 | 
				
			||||||
	for i := 0; i < 16; i++ {
 | 
					
 | 
				
			||||||
		key[i] = keyChars[rand.Intn(len(keyChars))]
 | 
						for i := range b {
 | 
				
			||||||
	}
 | 
							n, err := crand.Int(crand.Reader, maxI)
 | 
				
			||||||
	uuid_ := GetUUID()
 | 
							if err != nil {
 | 
				
			||||||
	for i := 0; i < 32; i++ {
 | 
								return "", err
 | 
				
			||||||
		c := uuid_[i]
 | 
					 | 
				
			||||||
		if i%2 == 0 && c >= 'a' && c <= 'z' {
 | 
					 | 
				
			||||||
			c = c - 'a' + 'A'
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		key[i+16] = c
 | 
							b[i] = keyChars[n.Int64()]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return string(key)
 | 
					
 | 
				
			||||||
 | 
						return string(b), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GenerateRandomKey(length int) (string, error) {
 | 
				
			||||||
 | 
						bytes := make([]byte, length*3/4) // 对于48位的输出,这里应该是36
 | 
				
			||||||
 | 
						if _, err := crand.Read(bytes); err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return base64.StdEncoding.EncodeToString(bytes), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GenerateKey() (string, error) {
 | 
				
			||||||
 | 
						//rand.Seed(time.Now().UnixNano())
 | 
				
			||||||
 | 
						return GenerateRandomCharsKey(48)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetRandomInt(max int) int {
 | 
					func GetRandomInt(max int) int {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								constant/chat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								constant/chat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					package constant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"one-api/common"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Chats = []map[string]string{
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							"ChatGPT Next Web 官方示例": "https://app.nextchat.dev/#/?settings={\"key\":\"{key}\",\"url\":\"{address}\"}",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							"Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							"AMA 问天": "ama://set-api-key?server={address}&key={key}",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							"OpenCat": "opencat://team/join?domain={address}&token={key}",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UpdateChatsByJsonString(jsonString string) error {
 | 
				
			||||||
 | 
						Chats = make([]map[string]string, 0)
 | 
				
			||||||
 | 
						return json.Unmarshal([]byte(jsonString), &Chats)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Chats2JsonString() string {
 | 
				
			||||||
 | 
						jsonBytes, err := json.Marshal(Chats)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							common.SysError("error marshalling chats: " + err.Error())
 | 
				
			||||||
 | 
							return "[]"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return string(jsonBytes)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -112,7 +112,9 @@ func GitHubOAuth(c *gin.Context) {
 | 
				
			|||||||
	user := model.User{
 | 
						user := model.User{
 | 
				
			||||||
		GitHubId: githubUser.Login,
 | 
							GitHubId: githubUser.Login,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// IsGitHubIdAlreadyTaken is unscoped
 | 
				
			||||||
	if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
 | 
						if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
 | 
				
			||||||
 | 
							// FillUserByGitHubId is scoped
 | 
				
			||||||
		err := user.FillUserByGitHubId()
 | 
							err := user.FillUserByGitHubId()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			c.JSON(http.StatusOK, gin.H{
 | 
								c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
@@ -121,6 +123,14 @@ func GitHubOAuth(c *gin.Context) {
 | 
				
			|||||||
			})
 | 
								})
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// if user.Id == 0 , user has been deleted
 | 
				
			||||||
 | 
							if user.Id == 0 {
 | 
				
			||||||
 | 
								c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
									"success": false,
 | 
				
			||||||
 | 
									"message": "用户已注销",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if common.RegisterEnabled {
 | 
							if common.RegisterEnabled {
 | 
				
			||||||
			user.InviterId, _ = model.GetUserIdByAffCode(c.Query("aff"))
 | 
								user.InviterId, _ = model.GetUserIdByAffCode(c.Query("aff"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -132,6 +132,14 @@ func LinuxDoOAuth(c *gin.Context) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if user.Id == 0 {
 | 
				
			||||||
 | 
								c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
									"success": false,
 | 
				
			||||||
 | 
									"message": "用户已注销",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		user.LinuxDoLevel = linuxdoUser.TrustLevel
 | 
							user.LinuxDoLevel = linuxdoUser.TrustLevel
 | 
				
			||||||
		err = user.Update(false)
 | 
							err = user.Update(false)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,7 @@ func GetStatus(c *gin.Context) {
 | 
				
			|||||||
			"default_collapse_sidebar": common.DefaultCollapseSidebar,
 | 
								"default_collapse_sidebar": common.DefaultCollapseSidebar,
 | 
				
			||||||
			"payment_enabled":          common.PaymentEnabled,
 | 
								"payment_enabled":          common.PaymentEnabled,
 | 
				
			||||||
			"mj_notify_enabled":        constant.MjNotifyEnabled,
 | 
								"mj_notify_enabled":        constant.MjNotifyEnabled,
 | 
				
			||||||
 | 
								"chats":                    constant.Chats,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,58 @@ 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,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						useAccessToken := c.GetBool("use_access_token")
 | 
				
			||||||
 | 
						if useAccessToken {
 | 
				
			||||||
 | 
							openaiErr = service.OpenAIErrorWrapperLocal(errors.New("暂不支持使用 access token"), "access_token_not_supported", http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
						userGroup := c.GetString("group")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if group == "" {
 | 
				
			||||||
 | 
							group = userGroup
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if !common.GroupInUserUsableGroups(group) && group != userGroup {
 | 
				
			||||||
 | 
								openaiErr = service.OpenAIErrorWrapperLocal(errors.New("无权访问该分组"), "group_not_allowed", http.StatusForbidden)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.Set("group", group)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Set("token_name", "playground-"+group)
 | 
				
			||||||
 | 
						channel, err := model.CacheGetRandomSatisfiedChannel(group, playgroundRequest.Model, 0)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", group, playgroundRequest.Model)
 | 
				
			||||||
 | 
							openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"crypto/sha256"
 | 
						"crypto/sha256"
 | 
				
			||||||
	"encoding/hex"
 | 
						"encoding/hex"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
	"one-api/common"
 | 
						"one-api/common"
 | 
				
			||||||
	"one-api/model"
 | 
						"one-api/model"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
@@ -48,6 +49,13 @@ func TelegramBind(c *gin.Context) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if user.Id == 0 {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
								"success": false,
 | 
				
			||||||
 | 
								"message": "用户已注销",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	user.TelegramId = telegramId
 | 
						user.TelegramId = telegramId
 | 
				
			||||||
	if err := user.Update(false); err != nil {
 | 
						if err := user.Update(false); err != nil {
 | 
				
			||||||
		c.JSON(200, gin.H{
 | 
							c.JSON(200, gin.H{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -123,10 +123,19 @@ func AddToken(c *gin.Context) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						key, err := common.GenerateKey()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
								"success": false,
 | 
				
			||||||
 | 
								"message": "生成令牌失败",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							common.SysError("failed to generate token key: " + err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	cleanToken := model.Token{
 | 
						cleanToken := model.Token{
 | 
				
			||||||
		UserId:             c.GetInt("id"),
 | 
							UserId:             c.GetInt("id"),
 | 
				
			||||||
		Name:               token.Name,
 | 
							Name:               token.Name,
 | 
				
			||||||
		Key:                common.GenerateKey(),
 | 
							Key:                key,
 | 
				
			||||||
		CreatedTime:        common.GetTimestamp(),
 | 
							CreatedTime:        common.GetTimestamp(),
 | 
				
			||||||
		AccessedTime:       common.GetTimestamp(),
 | 
							AccessedTime:       common.GetTimestamp(),
 | 
				
			||||||
		ExpiredTime:        token.ExpiredTime,
 | 
							ExpiredTime:        token.ExpiredTime,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import (
 | 
				
			|||||||
	"one-api/common"
 | 
						"one-api/common"
 | 
				
			||||||
	"one-api/model"
 | 
						"one-api/model"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gin-contrib/sessions"
 | 
						"github.com/gin-contrib/sessions"
 | 
				
			||||||
@@ -67,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)
 | 
				
			||||||
	session.Set("linuxdo_enable", user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel)
 | 
						session.Set("linuxdo_enable", user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel)
 | 
				
			||||||
	err := session.Save()
 | 
						err := session.Save()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -159,8 +161,9 @@ func Register(c *gin.Context) {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.JSON(http.StatusOK, gin.H{
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
			"success": false,
 | 
								"success": false,
 | 
				
			||||||
			"message": err.Error(),
 | 
								"message": "数据库错误,请稍后重试",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
							common.SysError(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if exist {
 | 
						if exist {
 | 
				
			||||||
@@ -200,11 +203,20 @@ func Register(c *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	// 生成默认令牌
 | 
						// 生成默认令牌
 | 
				
			||||||
	if constant.GenerateDefaultToken {
 | 
						if constant.GenerateDefaultToken {
 | 
				
			||||||
 | 
							key, err := common.GenerateKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
									"success": false,
 | 
				
			||||||
 | 
									"message": "生成默认令牌失败",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								common.SysError("failed to generate token key: " + err.Error())
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		// 生成默认令牌
 | 
							// 生成默认令牌
 | 
				
			||||||
		token := model.Token{
 | 
							token := model.Token{
 | 
				
			||||||
			UserId:             insertedUser.Id, // 使用插入后的用户ID
 | 
								UserId:             insertedUser.Id, // 使用插入后的用户ID
 | 
				
			||||||
			Name:               cleanUser.Username + "的初始令牌",
 | 
								Name:               cleanUser.Username + "的初始令牌",
 | 
				
			||||||
			Key:                common.GenerateKey(),
 | 
								Key:                key,
 | 
				
			||||||
			CreatedTime:        common.GetTimestamp(),
 | 
								CreatedTime:        common.GetTimestamp(),
 | 
				
			||||||
			AccessedTime:       common.GetTimestamp(),
 | 
								AccessedTime:       common.GetTimestamp(),
 | 
				
			||||||
			ExpiredTime:        -1,     // 永不过期
 | 
								ExpiredTime:        -1,     // 永不过期
 | 
				
			||||||
@@ -311,7 +323,18 @@ func GenerateAccessToken(c *gin.Context) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.AccessToken = common.GetUUID()
 | 
						// get rand int 28-32
 | 
				
			||||||
 | 
						randI := common.GetRandomInt(4)
 | 
				
			||||||
 | 
						key, err := common.GenerateRandomKey(29 + randI)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
								"success": false,
 | 
				
			||||||
 | 
								"message": "生成失败",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							common.SysError("failed to generate key: " + err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user.SetAccessToken(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 {
 | 
						if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 {
 | 
				
			||||||
		c.JSON(http.StatusOK, gin.H{
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
@@ -631,6 +654,7 @@ func DeleteSelf(c *gin.Context) {
 | 
				
			|||||||
func CreateUser(c *gin.Context) {
 | 
					func CreateUser(c *gin.Context) {
 | 
				
			||||||
	var user model.User
 | 
						var user model.User
 | 
				
			||||||
	err := json.NewDecoder(c.Request.Body).Decode(&user)
 | 
						err := json.NewDecoder(c.Request.Body).Decode(&user)
 | 
				
			||||||
 | 
						user.Username = strings.TrimSpace(user.Username)
 | 
				
			||||||
	if err != nil || user.Username == "" || user.Password == "" {
 | 
						if err != nil || user.Username == "" || user.Password == "" {
 | 
				
			||||||
		c.JSON(http.StatusOK, gin.H{
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
			"success": false,
 | 
								"success": false,
 | 
				
			||||||
@@ -678,8 +702,8 @@ func CreateUser(c *gin.Context) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ManageRequest struct {
 | 
					type ManageRequest struct {
 | 
				
			||||||
	Username string `json:"username"`
 | 
						Id     int    `json:"id"`
 | 
				
			||||||
	Action   string `json:"action"`
 | 
						Action string `json:"action"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ManageUser Only admin user can do this
 | 
					// ManageUser Only admin user can do this
 | 
				
			||||||
@@ -695,7 +719,7 @@ func ManageUser(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user := model.User{
 | 
						user := model.User{
 | 
				
			||||||
		Username: req.Username,
 | 
							Id: req.Id,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// Fill attributes
 | 
						// Fill attributes
 | 
				
			||||||
	model.DB.Unscoped().Where(&user).First(&user)
 | 
						model.DB.Unscoped().Where(&user).First(&user)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,6 +78,13 @@ func WeChatAuth(c *gin.Context) {
 | 
				
			|||||||
			})
 | 
								})
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if user.Id == 0 {
 | 
				
			||||||
 | 
								c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
									"success": false,
 | 
				
			||||||
 | 
									"message": "用户已注销",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if common.RegisterEnabled {
 | 
							if common.RegisterEnabled {
 | 
				
			||||||
			user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
 | 
								user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -34,7 +34,7 @@ type GeneralOpenAIRequest struct {
 | 
				
			|||||||
	TopLogProbs       int            `json:"top_logprobs,omitempty"`
 | 
						TopLogProbs       int            `json:"top_logprobs,omitempty"`
 | 
				
			||||||
	Dimensions        int            `json:"dimensions,omitempty"`
 | 
						Dimensions        int            `json:"dimensions,omitempty"`
 | 
				
			||||||
	ParallelToolCalls bool           `json:"parallel_Tool_Calls,omitempty"`
 | 
						ParallelToolCalls bool           `json:"parallel_Tool_Calls,omitempty"`
 | 
				
			||||||
	EncodingFormat    string         `json:"encoding_format,omitempty"`
 | 
						EncodingFormat    any         `json:"encoding_format,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OpenAITools struct {
 | 
					type OpenAITools struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,17 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func validUserInfo(username string, role int) bool {
 | 
				
			||||||
 | 
						// check username is empty
 | 
				
			||||||
 | 
						if strings.TrimSpace(username) == "" {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !common.IsValidateRole(role) {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func authHelper(c *gin.Context, minRole int) {
 | 
					func authHelper(c *gin.Context, minRole int) {
 | 
				
			||||||
	session := sessions.Default(c)
 | 
						session := sessions.Default(c)
 | 
				
			||||||
	username := session.Get("username")
 | 
						username := session.Get("username")
 | 
				
			||||||
@@ -31,6 +42,14 @@ func authHelper(c *gin.Context, minRole int) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		user := model.ValidateAccessToken(accessToken)
 | 
							user := model.ValidateAccessToken(accessToken)
 | 
				
			||||||
		if user != nil && user.Username != "" {
 | 
							if user != nil && user.Username != "" {
 | 
				
			||||||
 | 
								if !validUserInfo(user.Username, user.Role) {
 | 
				
			||||||
 | 
									c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
										"success": false,
 | 
				
			||||||
 | 
										"message": "无权进行此操作,用户信息无效",
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									c.Abort()
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			// Token is valid
 | 
								// Token is valid
 | 
				
			||||||
			username = user.Username
 | 
								username = user.Username
 | 
				
			||||||
			role = user.Role
 | 
								role = user.Role
 | 
				
			||||||
@@ -101,9 +120,19 @@ func authHelper(c *gin.Context, minRole int) {
 | 
				
			|||||||
		c.Abort()
 | 
							c.Abort()
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if !validUserInfo(username.(string), role.(int)) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
								"success": false,
 | 
				
			||||||
 | 
								"message": "无权进行此操作,用户信息无效",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							c.Abort()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	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.Set("use_access_token", useAccessToken)
 | 
				
			||||||
	c.Next()
 | 
						c.Next()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ func createRootAccountIfNeed() error {
 | 
				
			|||||||
			Role:        common.RoleRootUser,
 | 
								Role:        common.RoleRootUser,
 | 
				
			||||||
			Status:      common.UserStatusEnabled,
 | 
								Status:      common.UserStatusEnabled,
 | 
				
			||||||
			DisplayName: "Root User",
 | 
								DisplayName: "Root User",
 | 
				
			||||||
			AccessToken: common.GetUUID(),
 | 
								AccessToken: nil,
 | 
				
			||||||
			Quota:       100000000,
 | 
								Quota:       100000000,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		DB.Create(&rootUser)
 | 
							DB.Create(&rootUser)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,6 +70,7 @@ func InitOptionMap() {
 | 
				
			|||||||
	common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(common.StripeUnitPrice, 'f', -1, 64)
 | 
						common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(common.StripeUnitPrice, 'f', -1, 64)
 | 
				
			||||||
	common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
 | 
						common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
 | 
				
			||||||
	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 | 
						common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 | 
				
			||||||
 | 
						common.OptionMap["Chats"] = constant.Chats2JsonString()
 | 
				
			||||||
	common.OptionMap["GitHubClientId"] = ""
 | 
						common.OptionMap["GitHubClientId"] = ""
 | 
				
			||||||
	common.OptionMap["GitHubClientSecret"] = ""
 | 
						common.OptionMap["GitHubClientSecret"] = ""
 | 
				
			||||||
	common.OptionMap["LinuxDoClientId"] = ""
 | 
						common.OptionMap["LinuxDoClientId"] = ""
 | 
				
			||||||
@@ -252,6 +253,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
				
			|||||||
		common.ServerAddress = value
 | 
							common.ServerAddress = value
 | 
				
			||||||
	case "OutProxyUrl":
 | 
						case "OutProxyUrl":
 | 
				
			||||||
		common.OutProxyUrl = value
 | 
							common.OutProxyUrl = value
 | 
				
			||||||
 | 
						case "Chats":
 | 
				
			||||||
 | 
							err = constant.UpdateChatsByJsonString(value)
 | 
				
			||||||
	case "StripeApiSecret":
 | 
						case "StripeApiSecret":
 | 
				
			||||||
		common.StripeApiSecret = value
 | 
							common.StripeApiSecret = value
 | 
				
			||||||
	case "StripeWebhookSecret":
 | 
						case "StripeWebhookSecret":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
	"one-api/common"
 | 
						"one-api/common"
 | 
				
			||||||
 | 
						relaycommon "one-api/relay/common"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -256,51 +257,56 @@ 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 {
 | 
				
			||||||
	if err != nil {
 | 
							token, err := GetTokenById(relayInfo.TokenId)
 | 
				
			||||||
		return 0, err
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !token.UnlimitedQuota && token.RemainQuota < quota {
 | 
				
			||||||
 | 
								return 0, errors.New("令牌额度不足")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !token.UnlimitedQuota && token.RemainQuota < quota {
 | 
						userQuota, err = GetUserQuota(relayInfo.UserId)
 | 
				
			||||||
		return 0, errors.New("令牌额度不足")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	userQuota, err = GetUserQuota(token.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 {
 | 
				
			||||||
	if err != nil {
 | 
							err = DecreaseTokenQuota(relayInfo.TokenId, quota)
 | 
				
			||||||
		return 0, err
 | 
							if err != nil {
 | 
				
			||||||
 | 
								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 quota > 0 {
 | 
						if !relayInfo.IsPlayground {
 | 
				
			||||||
		err = DecreaseTokenQuota(tokenId, quota)
 | 
							if quota > 0 {
 | 
				
			||||||
	} else {
 | 
								err = DecreaseTokenQuota(relayInfo.TokenId, quota)
 | 
				
			||||||
		err = IncreaseTokenQuota(tokenId, -quota)
 | 
							} else {
 | 
				
			||||||
	}
 | 
								err = IncreaseTokenQuota(relayInfo.TokenId, -quota)
 | 
				
			||||||
	if err != nil {
 | 
							}
 | 
				
			||||||
		return err
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if sendEmail {
 | 
						if sendEmail {
 | 
				
			||||||
@@ -309,7 +315,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())
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ type User struct {
 | 
				
			|||||||
	WeChatId         string         `json:"wechat_id" gorm:"column:wechat_id;index"`
 | 
						WeChatId         string         `json:"wechat_id" gorm:"column:wechat_id;index"`
 | 
				
			||||||
	TelegramId       string         `json:"telegram_id" gorm:"column:telegram_id;index"`
 | 
						TelegramId       string         `json:"telegram_id" gorm:"column:telegram_id;index"`
 | 
				
			||||||
	VerificationCode string         `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database!
 | 
						VerificationCode string         `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database!
 | 
				
			||||||
	AccessToken      string         `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
 | 
						AccessToken      *string        `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
 | 
				
			||||||
	Quota            int            `json:"quota" gorm:"type:int;default:0"`
 | 
						Quota            int            `json:"quota" gorm:"type:int;default:0"`
 | 
				
			||||||
	UsedQuota        int            `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
 | 
						UsedQuota        int            `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
 | 
				
			||||||
	RequestCount     int            `json:"request_count" gorm:"type:int;default:0;"`               // request number
 | 
						RequestCount     int            `json:"request_count" gorm:"type:int;default:0;"`               // request number
 | 
				
			||||||
@@ -41,6 +41,17 @@ type User struct {
 | 
				
			|||||||
	DeletedAt        gorm.DeletedAt `gorm:"index"`
 | 
						DeletedAt        gorm.DeletedAt `gorm:"index"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (user *User) GetAccessToken() string {
 | 
				
			||||||
 | 
						if user.AccessToken == nil {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return *user.AccessToken
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (user *User) SetAccessToken(token string) {
 | 
				
			||||||
 | 
						user.AccessToken = &token
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil
 | 
					// CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil
 | 
				
			||||||
func CheckUserExistOrDeleted(username string, email string) (bool, error) {
 | 
					func CheckUserExistOrDeleted(username string, email string) (bool, error) {
 | 
				
			||||||
	var user User
 | 
						var user User
 | 
				
			||||||
@@ -218,7 +229,7 @@ func (user *User) Insert(inviterId int) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Quota = common.QuotaForNewUser
 | 
						user.Quota = common.QuotaForNewUser
 | 
				
			||||||
	user.AccessToken = common.GetUUID()
 | 
						//user.SetAccessToken(common.GetUUID())
 | 
				
			||||||
	user.AffCode = common.GetRandomString(4)
 | 
						user.AffCode = common.GetRandomString(4)
 | 
				
			||||||
	result := DB.Create(user)
 | 
						result := DB.Create(user)
 | 
				
			||||||
	if result.Error != nil {
 | 
						if result.Error != nil {
 | 
				
			||||||
@@ -312,11 +323,12 @@ func (user *User) ValidateAndFill() (err error) {
 | 
				
			|||||||
	// that means if your field’s value is 0, '', false or other zero values,
 | 
						// that means if your field’s value is 0, '', false or other zero values,
 | 
				
			||||||
	// it won’t be used to build query conditions
 | 
						// it won’t be used to build query conditions
 | 
				
			||||||
	password := user.Password
 | 
						password := user.Password
 | 
				
			||||||
	if user.Username == "" || password == "" {
 | 
						username := strings.TrimSpace(user.Username)
 | 
				
			||||||
 | 
						if username == "" || password == "" {
 | 
				
			||||||
		return errors.New("用户名或密码为空")
 | 
							return errors.New("用户名或密码为空")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// find buy username or email
 | 
						// find buy username or email
 | 
				
			||||||
	DB.Where("username = ? OR email = ?", user.Username, user.Username).First(user)
 | 
						DB.Where("username = ? OR email = ?", username, username).First(user)
 | 
				
			||||||
	okay := common.ValidatePasswordAndHash(password, user.Password)
 | 
						okay := common.ValidatePasswordAndHash(password, user.Password)
 | 
				
			||||||
	if !okay || user.Status != common.UserStatusEnabled {
 | 
						if !okay || user.Status != common.UserStatusEnabled {
 | 
				
			||||||
		return errors.New("用户名或密码错误,或用户已被封禁")
 | 
							return errors.New("用户名或密码错误,或用户已被封禁")
 | 
				
			||||||
@@ -364,14 +376,6 @@ func (user *User) FillUserByWeChatId() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (user *User) FillUserByUsername() error {
 | 
					 | 
				
			||||||
	if user.Username == "" {
 | 
					 | 
				
			||||||
		return errors.New("username 为空!")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	DB.Where(User{Username: user.Username}).First(user)
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (user *User) FillUserByTelegramId() error {
 | 
					func (user *User) FillUserByTelegramId() error {
 | 
				
			||||||
	if user.TelegramId == "" {
 | 
						if user.TelegramId == "" {
 | 
				
			||||||
		return errors.New("Telegram id 为空!")
 | 
							return errors.New("Telegram id 为空!")
 | 
				
			||||||
@@ -384,27 +388,27 @@ func (user *User) FillUserByTelegramId() error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsEmailAlreadyTaken(email string) bool {
 | 
					func IsEmailAlreadyTaken(email string) bool {
 | 
				
			||||||
	return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("email = ?", email).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsWeChatIdAlreadyTaken(wechatId string) bool {
 | 
					func IsWeChatIdAlreadyTaken(wechatId string) bool {
 | 
				
			||||||
	return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsGitHubIdAlreadyTaken(githubId string) bool {
 | 
					func IsGitHubIdAlreadyTaken(githubId string) bool {
 | 
				
			||||||
	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsLinuxDoIdAlreadyTaken(linuxdoId string) bool {
 | 
					func IsLinuxDoIdAlreadyTaken(linuxdoId string) bool {
 | 
				
			||||||
	return DB.Where("linuxdo_id = ?", linuxdoId).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("linuxdo_id = ?", linuxdoId).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsUsernameAlreadyTaken(username string) bool {
 | 
					func IsUsernameAlreadyTaken(username string) bool {
 | 
				
			||||||
	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("username = ?", username).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IsTelegramIdAlreadyTaken(telegramId string) bool {
 | 
					func IsTelegramIdAlreadyTaken(telegramId string) bool {
 | 
				
			||||||
	return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
 | 
						return DB.Unscoped().Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ResetUserPasswordByEmail(email string, password string) error {
 | 
					func ResetUserPasswordByEmail(email string, password string) error {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"one-api/dto"
 | 
						"one-api/dto"
 | 
				
			||||||
	"one-api/relay/channel/claude"
 | 
						"one-api/relay/channel/claude"
 | 
				
			||||||
	relaycommon "one-api/relay/common"
 | 
						relaycommon "one-api/relay/common"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
@@ -31,11 +30,7 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
 | 
					func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
 | 
				
			||||||
	if strings.HasPrefix(info.UpstreamModelName, "claude-3") {
 | 
						a.RequestMode = RequestModeMessage
 | 
				
			||||||
		a.RequestMode = RequestModeMessage
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		a.RequestMode = RequestModeCompletion
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 | 
					func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 | 
				
			||||||
@@ -53,11 +48,8 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var claudeReq *claude.ClaudeRequest
 | 
						var claudeReq *claude.ClaudeRequest
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	if a.RequestMode == RequestModeCompletion {
 | 
						claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request)
 | 
				
			||||||
		claudeReq = claude.RequestOpenAI2ClaudeComplete(*request)
 | 
					
 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Set("request_model", request.Model)
 | 
						c.Set("request_model", request.Model)
 | 
				
			||||||
	c.Set("converted_request", claudeReq)
 | 
						c.Set("converted_request", claudeReq)
 | 
				
			||||||
	return claudeReq, err
 | 
						return claudeReq, err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
@@ -275,7 +275,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)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -283,11 +283,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())
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -345,7 +345,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())
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,6 +73,15 @@ func ShouldDisableChannel(channelType int, err *relaymodel.OpenAIErrorWithStatus
 | 
				
			|||||||
	} else if strings.HasPrefix(err.Error.Message, "Permission denied") {
 | 
						} else if strings.HasPrefix(err.Error.Message, "Permission denied") {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if strings.Contains(err.Error.Message, "The security token included in the request is invalid") { // anthropic
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} else if strings.Contains(err.Error.Message, "Operation not allowed") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} else if strings.Contains(err.Error.Message, "Your account is not authorized") {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4752
									
								
								web/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4752
									
								
								web/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -26,6 +26,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'));
 | 
				
			||||||
@@ -101,6 +102,14 @@ function App() {
 | 
				
			|||||||
            </PrivateRoute>
 | 
					            </PrivateRoute>
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					        <Route
 | 
				
			||||||
 | 
					          path='/playground'
 | 
				
			||||||
 | 
					          element={
 | 
				
			||||||
 | 
					            <PrivateRoute>
 | 
				
			||||||
 | 
					              <Playground />
 | 
				
			||||||
 | 
					            </PrivateRoute>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <Route
 | 
					        <Route
 | 
				
			||||||
          path='/redemption'
 | 
					          path='/redemption'
 | 
				
			||||||
          element={
 | 
					          element={
 | 
				
			||||||
@@ -256,7 +265,7 @@ function App() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <Route
 | 
					        <Route
 | 
				
			||||||
          path='/chat'
 | 
					          path='/chat/:id?'
 | 
				
			||||||
          element={
 | 
					          element={
 | 
				
			||||||
            <Suspense fallback={<Loading></Loading>}>
 | 
					            <Suspense fallback={<Loading></Loading>}>
 | 
				
			||||||
              <Chat />
 | 
					              <Chat />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
import React, { useContext, useEffect, useState } from 'react';
 | 
					import React, { useContext, useEffect, useState } from 'react';
 | 
				
			||||||
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
 | 
					import { Dimmer, Loader, Segment } from 'semantic-ui-react';
 | 
				
			||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
 | 
					import { useNavigate, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
import { API, showError, showSuccess } from '../helpers';
 | 
					import { API, showError, showSuccess, updateAPI } from '../helpers';
 | 
				
			||||||
import { UserContext } from '../context/User';
 | 
					import { UserContext } from '../context/User';
 | 
				
			||||||
 | 
					import { setUserData } from '../helpers/data.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const GitHubOAuth = () => {
 | 
					const GitHubOAuth = () => {
 | 
				
			||||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
					  const [searchParams, setSearchParams] = useSearchParams();
 | 
				
			||||||
@@ -28,8 +29,10 @@ const GitHubOAuth = () => {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        userDispatch({ type: 'login', payload: data });
 | 
					        userDispatch({ type: 'login', payload: data });
 | 
				
			||||||
        localStorage.setItem('user', JSON.stringify(data));
 | 
					        localStorage.setItem('user', JSON.stringify(data));
 | 
				
			||||||
 | 
					        setUserData(data);
 | 
				
			||||||
 | 
					        updateAPI();
 | 
				
			||||||
        showSuccess('登录成功!');
 | 
					        showSuccess('登录成功!');
 | 
				
			||||||
        navigate('/');
 | 
					        navigate('/token');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      showError(message);
 | 
					      showError(message);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 />,
 | 
				
			||||||
  // },
 | 
					  // },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,6 +79,8 @@ const LoginForm = () => {
 | 
				
			|||||||
    if (success) {
 | 
					    if (success) {
 | 
				
			||||||
      userDispatch({ type: 'login', payload: data });
 | 
					      userDispatch({ type: 'login', payload: data });
 | 
				
			||||||
      localStorage.setItem('user', JSON.stringify(data));
 | 
					      localStorage.setItem('user', JSON.stringify(data));
 | 
				
			||||||
 | 
					      setUserData(data);
 | 
				
			||||||
 | 
					      updateAPI();
 | 
				
			||||||
      navigate('/');
 | 
					      navigate('/');
 | 
				
			||||||
      showSuccess('登录成功!');
 | 
					      showSuccess('登录成功!');
 | 
				
			||||||
      setShowWeChatLoginModal(false);
 | 
					      setShowWeChatLoginModal(false);
 | 
				
			||||||
@@ -151,6 +153,8 @@ const LoginForm = () => {
 | 
				
			|||||||
      userDispatch({ type: 'login', payload: data });
 | 
					      userDispatch({ type: 'login', payload: data });
 | 
				
			||||||
      localStorage.setItem('user', JSON.stringify(data));
 | 
					      localStorage.setItem('user', JSON.stringify(data));
 | 
				
			||||||
      showSuccess('登录成功!');
 | 
					      showSuccess('登录成功!');
 | 
				
			||||||
 | 
					      setUserData(data);
 | 
				
			||||||
 | 
					      updateAPI();
 | 
				
			||||||
      navigate('/');
 | 
					      navigate('/');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      showError(message);
 | 
					      showError(message);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.
 | 
				
			|||||||
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
 | 
					import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { API, showError, showSuccess } from '../helpers';
 | 
					import { API, showError, showSuccess } from '../helpers';
 | 
				
			||||||
 | 
					import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const OperationSetting = () => {
 | 
					const OperationSetting = () => {
 | 
				
			||||||
  let [inputs, setInputs] = useState({
 | 
					  let [inputs, setInputs] = useState({
 | 
				
			||||||
@@ -50,6 +51,7 @@ const OperationSetting = () => {
 | 
				
			|||||||
    DataExportInterval: 5,
 | 
					    DataExportInterval: 5,
 | 
				
			||||||
    DefaultCollapseSidebar: false, // 默认折叠侧边栏
 | 
					    DefaultCollapseSidebar: false, // 默认折叠侧边栏
 | 
				
			||||||
    RetryTimes: 0,
 | 
					    RetryTimes: 0,
 | 
				
			||||||
 | 
					    Chats: '[]',
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let [loading, setLoading] = useState(false);
 | 
					  let [loading, setLoading] = useState(false);
 | 
				
			||||||
@@ -131,6 +133,10 @@ const OperationSetting = () => {
 | 
				
			|||||||
        <Card style={{ marginTop: '10px' }}>
 | 
					        <Card style={{ marginTop: '10px' }}>
 | 
				
			||||||
          <SettingsCreditLimit options={inputs} refresh={onRefresh} />
 | 
					          <SettingsCreditLimit options={inputs} refresh={onRefresh} />
 | 
				
			||||||
        </Card>
 | 
					        </Card>
 | 
				
			||||||
 | 
					        {/* 聊天设置 */}
 | 
				
			||||||
 | 
					        <Card style={{ marginTop: '10px' }}>
 | 
				
			||||||
 | 
					          <SettingsChats options={inputs} refresh={onRefresh} />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
        {/* 倍率设置 */}
 | 
					        {/* 倍率设置 */}
 | 
				
			||||||
        <Card style={{ marginTop: '10px' }}>
 | 
					        <Card style={{ marginTop: '10px' }}>
 | 
				
			||||||
          <SettingsMagnification options={inputs} refresh={onRefresh} />
 | 
					          <SettingsMagnification options={inputs} refresh={onRefresh} />
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										357
									
								
								web/src/components/Playground.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										357
									
								
								web/src/components/Playground.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,357 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					          if (text) {
 | 
				
			||||||
 | 
					            generateMockResponse(text);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        completeMessage();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    source.addEventListener('error', (e) => {
 | 
				
			||||||
 | 
					      generateMockResponse(e.data);
 | 
				
			||||||
 | 
					      completeMessage('error');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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: parseInt(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((status = 'complete') => {
 | 
				
			||||||
 | 
					    // console.log("Complete Message: ", status)
 | 
				
			||||||
 | 
					    setMessage((prevMessage) => {
 | 
				
			||||||
 | 
					      const lastMessage = prevMessage[prevMessage.length - 1];
 | 
				
			||||||
 | 
					      // only change the status if the last message is not complete and not error
 | 
				
			||||||
 | 
					      if (lastMessage.status === 'complete' || lastMessage.status === 'error') {
 | 
				
			||||||
 | 
					        return prevMessage;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
							
								
								
									
										796
									
								
								web/src/components/SafetySetting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										796
									
								
								web/src/components/SafetySetting.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,796 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
@@ -17,6 +17,7 @@ import {
 | 
				
			|||||||
  IconCalendarClock,
 | 
					  IconCalendarClock,
 | 
				
			||||||
  IconChecklistStroked,
 | 
					  IconChecklistStroked,
 | 
				
			||||||
  IconComment,
 | 
					  IconComment,
 | 
				
			||||||
 | 
					  IconCommentStroked,
 | 
				
			||||||
  IconCreditCard,
 | 
					  IconCreditCard,
 | 
				
			||||||
  IconGift,
 | 
					  IconGift,
 | 
				
			||||||
  IconHelpCircle,
 | 
					  IconHelpCircle,
 | 
				
			||||||
@@ -42,11 +43,9 @@ const SiderBar = () => {
 | 
				
			|||||||
  const defaultIsCollapsed =
 | 
					  const defaultIsCollapsed =
 | 
				
			||||||
    isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
 | 
					    isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let navigate = useNavigate();
 | 
					 | 
				
			||||||
  const [selectedKeys, setSelectedKeys] = useState(['home']);
 | 
					  const [selectedKeys, setSelectedKeys] = useState(['home']);
 | 
				
			||||||
  const systemName = getSystemName();
 | 
					 | 
				
			||||||
  const logo = getLogo();
 | 
					 | 
				
			||||||
  const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
 | 
					  const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
 | 
				
			||||||
 | 
					  const [chatItems, setChatItems] = useState([]);
 | 
				
			||||||
  const theme = useTheme();
 | 
					  const theme = useTheme();
 | 
				
			||||||
  const setTheme = useSetTheme();
 | 
					  const setTheme = useSetTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,16 +64,17 @@ const SiderBar = () => {
 | 
				
			|||||||
    detail: '/detail',
 | 
					    detail: '/detail',
 | 
				
			||||||
    pricing: '/pricing',
 | 
					    pricing: '/pricing',
 | 
				
			||||||
    task: '/task',
 | 
					    task: '/task',
 | 
				
			||||||
 | 
					    playground: '/playground',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const headerButtons = useMemo(
 | 
					  const headerButtons = useMemo(
 | 
				
			||||||
    () => [
 | 
					    () => [
 | 
				
			||||||
      // {
 | 
					      {
 | 
				
			||||||
      //   text: '首页',
 | 
					        text: 'Playground',
 | 
				
			||||||
      //   itemKey: 'home',
 | 
					        itemKey: 'playground',
 | 
				
			||||||
      //   to: '/',
 | 
					        to: '/playground',
 | 
				
			||||||
      //   icon: <IconHome />,
 | 
					        icon: <IconCommentStroked />,
 | 
				
			||||||
      // },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        text: '模型价格',
 | 
					        text: '模型价格',
 | 
				
			||||||
        itemKey: 'pricing',
 | 
					        itemKey: 'pricing',
 | 
				
			||||||
@@ -91,11 +91,12 @@ const SiderBar = () => {
 | 
				
			|||||||
      {
 | 
					      {
 | 
				
			||||||
        text: '聊天',
 | 
					        text: '聊天',
 | 
				
			||||||
        itemKey: 'chat',
 | 
					        itemKey: 'chat',
 | 
				
			||||||
        to: '/chat',
 | 
					        // to: '/chat',
 | 
				
			||||||
 | 
					        items: chatItems,
 | 
				
			||||||
        icon: <IconComment />,
 | 
					        icon: <IconComment />,
 | 
				
			||||||
        className: localStorage.getItem('chat_link')
 | 
					        // className: localStorage.getItem('chat_link')
 | 
				
			||||||
          ? 'semi-navigation-item-normal'
 | 
					        //   ? 'semi-navigation-item-normal'
 | 
				
			||||||
          : 'tableHiddle',
 | 
					        //   : 'tableHiddle',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        text: '令牌',
 | 
					        text: '令牌',
 | 
				
			||||||
@@ -177,6 +178,7 @@ const SiderBar = () => {
 | 
				
			|||||||
      localStorage.getItem('enable_drawing'),
 | 
					      localStorage.getItem('enable_drawing'),
 | 
				
			||||||
      localStorage.getItem('enable_task'),
 | 
					      localStorage.getItem('enable_task'),
 | 
				
			||||||
      localStorage.getItem('chat_link'),
 | 
					      localStorage.getItem('chat_link'),
 | 
				
			||||||
 | 
					      chatItems,
 | 
				
			||||||
      isAdmin(),
 | 
					      isAdmin(),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -207,6 +209,33 @@ const SiderBar = () => {
 | 
				
			|||||||
      localKey = 'home';
 | 
					      localKey = 'home';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    setSelectedKeys([localKey]);
 | 
					    setSelectedKeys([localKey]);
 | 
				
			||||||
 | 
					    let chatLink = localStorage.getItem('chat_link');
 | 
				
			||||||
 | 
					    if (!chatLink) {
 | 
				
			||||||
 | 
					      let chats = localStorage.getItem('chats');
 | 
				
			||||||
 | 
					      if (chats) {
 | 
				
			||||||
 | 
					        // console.log(chats);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          chats = JSON.parse(chats);
 | 
				
			||||||
 | 
					          if (Array.isArray(chats)) {
 | 
				
			||||||
 | 
					            let chatItems = [];
 | 
				
			||||||
 | 
					            for (let i = 0; i < chats.length; i++) {
 | 
				
			||||||
 | 
					              let chat = {};
 | 
				
			||||||
 | 
					              for (let key in chats[i]) {
 | 
				
			||||||
 | 
					                chat.text = key;
 | 
				
			||||||
 | 
					                chat.itemKey = 'chat' + i;
 | 
				
			||||||
 | 
					                chat.to = '/chat/' + i;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              // setRouterMap({ ...routerMap, chat: '/chat/' + i })
 | 
				
			||||||
 | 
					              chatItems.push(chat);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            setChatItems(chatItems);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          console.error(e);
 | 
				
			||||||
 | 
					          showError('聊天数据解析失败');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -223,6 +252,27 @@ const SiderBar = () => {
 | 
				
			|||||||
        }}
 | 
					        }}
 | 
				
			||||||
        selectedKeys={selectedKeys}
 | 
					        selectedKeys={selectedKeys}
 | 
				
			||||||
        renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
 | 
					        renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
 | 
				
			||||||
 | 
					          let chatLink = localStorage.getItem('chat_link');
 | 
				
			||||||
 | 
					          if (!chatLink) {
 | 
				
			||||||
 | 
					            let chats = localStorage.getItem('chats');
 | 
				
			||||||
 | 
					            if (chats) {
 | 
				
			||||||
 | 
					              chats = JSON.parse(chats);
 | 
				
			||||||
 | 
					              if (Array.isArray(chats) && chats.length > 0) {
 | 
				
			||||||
 | 
					                for (let i = 0; i < chats.length; i++) {
 | 
				
			||||||
 | 
					                  routerMap['chat' + i] = '/chat/' + i;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (chats.length > 1) {
 | 
				
			||||||
 | 
					                  // delete /chat
 | 
				
			||||||
 | 
					                  if (routerMap['chat']) {
 | 
				
			||||||
 | 
					                    delete routerMap['chat'];
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  // rename /chat to /chat/0
 | 
				
			||||||
 | 
					                  routerMap['chat'] = '/chat/0';
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <Link
 | 
					            <Link
 | 
				
			||||||
              style={{ textDecoration: 'none' }}
 | 
					              style={{ textDecoration: 'none' }}
 | 
				
			||||||
@@ -236,15 +286,6 @@ const SiderBar = () => {
 | 
				
			|||||||
        onSelect={(key) => {
 | 
					        onSelect={(key) => {
 | 
				
			||||||
          setSelectedKeys([key.itemKey]);
 | 
					          setSelectedKeys([key.itemKey]);
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        // header={{
 | 
					 | 
				
			||||||
        //   logo: (
 | 
					 | 
				
			||||||
        //     <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
					 | 
				
			||||||
        //   ),
 | 
					 | 
				
			||||||
        //   text: systemName,
 | 
					 | 
				
			||||||
        // }}
 | 
					 | 
				
			||||||
        // footer={{
 | 
					 | 
				
			||||||
        //   text: '© 2021 NekoAPI',
 | 
					 | 
				
			||||||
        // }}
 | 
					 | 
				
			||||||
        footer={
 | 
					        footer={
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            {isMobile() && (
 | 
					            {isMobile() && (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,17 +25,6 @@ import {
 | 
				
			|||||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
 | 
					import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
 | 
				
			||||||
import EditToken from '../pages/Token/EditToken';
 | 
					import EditToken from '../pages/Token/EditToken';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const COPY_OPTIONS = [
 | 
					 | 
				
			||||||
  { key: 'next', text: 'ChatGPT Next Web', value: 'next' },
 | 
					 | 
				
			||||||
  { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
 | 
					 | 
				
			||||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const OPEN_LINK_OPTIONS = [
 | 
					 | 
				
			||||||
  { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
 | 
					 | 
				
			||||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function renderTimestamp(timestamp) {
 | 
					function renderTimestamp(timestamp) {
 | 
				
			||||||
  return <>{timestamp2string(timestamp)}</>;
 | 
					  return <>{timestamp2string(timestamp)}</>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -88,28 +77,6 @@ function renderStatus(status, model_limits_enabled = false) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TokensTable = () => {
 | 
					const TokensTable = () => {
 | 
				
			||||||
  const link_menu = [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      node: 'item',
 | 
					 | 
				
			||||||
      key: 'next',
 | 
					 | 
				
			||||||
      name: 'ChatGPT Next Web',
 | 
					 | 
				
			||||||
      onClick: () => {
 | 
					 | 
				
			||||||
        onOpenLink('next');
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    { node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      node: 'item',
 | 
					 | 
				
			||||||
      key: 'next-mj',
 | 
					 | 
				
			||||||
      name: 'ChatGPT Web & Midjourney',
 | 
					 | 
				
			||||||
      value: 'next-mj',
 | 
					 | 
				
			||||||
      onClick: () => {
 | 
					 | 
				
			||||||
        onOpenLink('next-mj');
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' },
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const columns = [
 | 
					  const columns = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      title: '名称',
 | 
					      title: '名称',
 | 
				
			||||||
@@ -177,149 +144,174 @@ const TokensTable = () => {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      title: '',
 | 
					      title: '',
 | 
				
			||||||
      dataIndex: 'operate',
 | 
					      dataIndex: 'operate',
 | 
				
			||||||
      render: (text, record, index) => (
 | 
					      render: (text, record, index) => {
 | 
				
			||||||
        <div>
 | 
					        let chats = localStorage.getItem('chats');
 | 
				
			||||||
          <Popover
 | 
					        let chatsArray = [];
 | 
				
			||||||
            content={'sk-' + record.key}
 | 
					        let chatLink = localStorage.getItem('chat_link');
 | 
				
			||||||
            style={{ padding: 20 }}
 | 
					        let mjLink = localStorage.getItem('chat_link2');
 | 
				
			||||||
            position='top'
 | 
					        let shouldUseCustom = true;
 | 
				
			||||||
          >
 | 
					        if (chatLink) {
 | 
				
			||||||
            <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
					          shouldUseCustom = false;
 | 
				
			||||||
              查看
 | 
					          chatLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
 | 
				
			||||||
            </Button>
 | 
					          chatsArray.push({
 | 
				
			||||||
          </Popover>
 | 
					            node: 'item',
 | 
				
			||||||
          <Button
 | 
					            key: 'default',
 | 
				
			||||||
            theme='light'
 | 
					            name: 'ChatGPT Next Web',
 | 
				
			||||||
            type='secondary'
 | 
					            onClick: () => {
 | 
				
			||||||
            style={{ marginRight: 1 }}
 | 
					              onOpenLink('default', chatLink, record);
 | 
				
			||||||
            onClick={async (text) => {
 | 
					            },
 | 
				
			||||||
              await copyText('sk-' + record.key);
 | 
					          });
 | 
				
			||||||
            }}
 | 
					        }
 | 
				
			||||||
          >
 | 
					        if (mjLink) {
 | 
				
			||||||
            复制
 | 
					          shouldUseCustom = false;
 | 
				
			||||||
          </Button>
 | 
					          mjLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
 | 
				
			||||||
          <SplitButtonGroup
 | 
					          chatsArray.push({
 | 
				
			||||||
            style={{ marginRight: 1 }}
 | 
					            node: 'item',
 | 
				
			||||||
            aria-label='项目操作按钮组'
 | 
					            key: 'mj',
 | 
				
			||||||
          >
 | 
					            name: 'ChatGPT Next Midjourney',
 | 
				
			||||||
            <Button
 | 
					            onClick: () => {
 | 
				
			||||||
              theme='light'
 | 
					              onOpenLink('mj', mjLink, record);
 | 
				
			||||||
              style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
 | 
					            },
 | 
				
			||||||
              onClick={() => {
 | 
					          });
 | 
				
			||||||
                onOpenLink('next', record.key);
 | 
					        }
 | 
				
			||||||
              }}
 | 
					        if (shouldUseCustom) {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            // console.log(chats);
 | 
				
			||||||
 | 
					            chats = JSON.parse(chats);
 | 
				
			||||||
 | 
					            // check chats is array
 | 
				
			||||||
 | 
					            if (Array.isArray(chats)) {
 | 
				
			||||||
 | 
					              for (let i = 0; i < chats.length; i++) {
 | 
				
			||||||
 | 
					                let chat = {};
 | 
				
			||||||
 | 
					                chat.node = 'item';
 | 
				
			||||||
 | 
					                // c is a map
 | 
				
			||||||
 | 
					                // chat.key = chats[i].name;
 | 
				
			||||||
 | 
					                // console.log(chats[i])
 | 
				
			||||||
 | 
					                for (let key in chats[i]) {
 | 
				
			||||||
 | 
					                  if (chats[i].hasOwnProperty(key)) {
 | 
				
			||||||
 | 
					                    chat.key = i;
 | 
				
			||||||
 | 
					                    chat.name = key;
 | 
				
			||||||
 | 
					                    chat.onClick = () => {
 | 
				
			||||||
 | 
					                      onOpenLink(key, chats[i][key], record);
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                chatsArray.push(chat);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            console.log(e);
 | 
				
			||||||
 | 
					            showError('聊天链接配置错误,请联系管理员');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <Popover
 | 
				
			||||||
 | 
					              content={'sk-' + record.key}
 | 
				
			||||||
 | 
					              style={{ padding: 20 }}
 | 
				
			||||||
 | 
					              position='top'
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              聊天
 | 
					              <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
				
			||||||
            </Button>
 | 
					                查看
 | 
				
			||||||
            <Dropdown
 | 
					              </Button>
 | 
				
			||||||
              trigger='click'
 | 
					            </Popover>
 | 
				
			||||||
              position='bottomRight'
 | 
					 | 
				
			||||||
              menu={[
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                  node: 'item',
 | 
					 | 
				
			||||||
                  key: 'next',
 | 
					 | 
				
			||||||
                  disabled: !localStorage.getItem('chat_link'),
 | 
					 | 
				
			||||||
                  name: 'ChatGPT Next Web',
 | 
					 | 
				
			||||||
                  onClick: () => {
 | 
					 | 
				
			||||||
                    onOpenLink('next', record.key);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                  node: 'item',
 | 
					 | 
				
			||||||
                  key: 'next-mj',
 | 
					 | 
				
			||||||
                  disabled: !localStorage.getItem('chat_link2'),
 | 
					 | 
				
			||||||
                  name: 'ChatGPT Web & Midjourney',
 | 
					 | 
				
			||||||
                  onClick: () => {
 | 
					 | 
				
			||||||
                    onOpenLink('next-mj', record.key);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                // {
 | 
					 | 
				
			||||||
                //   node: 'item',
 | 
					 | 
				
			||||||
                //   key: 'lobe',
 | 
					 | 
				
			||||||
                //   name: 'Lobe Chat',
 | 
					 | 
				
			||||||
                //   onClick: () => {
 | 
					 | 
				
			||||||
                //     onOpenLink('lobe', record.key);
 | 
					 | 
				
			||||||
                //   },
 | 
					 | 
				
			||||||
                // },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                  node: 'item',
 | 
					 | 
				
			||||||
                  key: 'ama',
 | 
					 | 
				
			||||||
                  name: 'AMA 问天(BotGem)',
 | 
					 | 
				
			||||||
                  onClick: () => {
 | 
					 | 
				
			||||||
                    onOpenLink('ama', record.key);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                  node: 'item',
 | 
					 | 
				
			||||||
                  key: 'opencat',
 | 
					 | 
				
			||||||
                  name: 'OpenCat',
 | 
					 | 
				
			||||||
                  onClick: () => {
 | 
					 | 
				
			||||||
                    onOpenLink('opencat', record.key);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ]}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <Button
 | 
					 | 
				
			||||||
                style={{
 | 
					 | 
				
			||||||
                  padding: '8px 4px',
 | 
					 | 
				
			||||||
                  color: 'rgba(var(--semi-teal-7), 1)',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                type='primary'
 | 
					 | 
				
			||||||
                icon={<IconTreeTriangleDown />}
 | 
					 | 
				
			||||||
              ></Button>
 | 
					 | 
				
			||||||
            </Dropdown>
 | 
					 | 
				
			||||||
          </SplitButtonGroup>
 | 
					 | 
				
			||||||
          <Popconfirm
 | 
					 | 
				
			||||||
            title='确定是否要删除此令牌?'
 | 
					 | 
				
			||||||
            content='此修改将不可逆'
 | 
					 | 
				
			||||||
            okType={'danger'}
 | 
					 | 
				
			||||||
            position={'left'}
 | 
					 | 
				
			||||||
            onConfirm={() => {
 | 
					 | 
				
			||||||
              manageToken(record.id, 'delete', record).then(() => {
 | 
					 | 
				
			||||||
                removeRecord(record.key);
 | 
					 | 
				
			||||||
              });
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
					 | 
				
			||||||
              删除
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          </Popconfirm>
 | 
					 | 
				
			||||||
          {record.status === 1 ? (
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              theme='light'
 | 
					 | 
				
			||||||
              type='warning'
 | 
					 | 
				
			||||||
              style={{ marginRight: 1 }}
 | 
					 | 
				
			||||||
              onClick={async () => {
 | 
					 | 
				
			||||||
                manageToken(record.id, 'disable', record);
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              禁用
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          ) : (
 | 
					 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
              theme='light'
 | 
					              theme='light'
 | 
				
			||||||
              type='secondary'
 | 
					              type='secondary'
 | 
				
			||||||
              style={{ marginRight: 1 }}
 | 
					              style={{ marginRight: 1 }}
 | 
				
			||||||
              onClick={async () => {
 | 
					              onClick={async (text) => {
 | 
				
			||||||
                manageToken(record.id, 'enable', record);
 | 
					                await copyText('sk-' + record.key);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              启用
 | 
					              复制
 | 
				
			||||||
            </Button>
 | 
					            </Button>
 | 
				
			||||||
          )}
 | 
					            <SplitButtonGroup
 | 
				
			||||||
          <Button
 | 
					              style={{ marginRight: 1 }}
 | 
				
			||||||
            theme='light'
 | 
					              aria-label='项目操作按钮组'
 | 
				
			||||||
            type='tertiary'
 | 
					            >
 | 
				
			||||||
            style={{ marginRight: 1 }}
 | 
					              <Button
 | 
				
			||||||
            onClick={() => {
 | 
					                theme='light'
 | 
				
			||||||
              setEditingToken(record);
 | 
					                style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
 | 
				
			||||||
              setShowEdit(true);
 | 
					                onClick={() => {
 | 
				
			||||||
            }}
 | 
					                  if (chatsArray.length === 0) {
 | 
				
			||||||
          >
 | 
					                    showError('请联系管理员配置聊天链接');
 | 
				
			||||||
            编辑
 | 
					                  } else {
 | 
				
			||||||
          </Button>
 | 
					                    onOpenLink(
 | 
				
			||||||
        </div>
 | 
					                      'default',
 | 
				
			||||||
      ),
 | 
					                      chats[0][Object.keys(chats[0])[0]],
 | 
				
			||||||
 | 
					                      record,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                聊天
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					              <Dropdown
 | 
				
			||||||
 | 
					                trigger='click'
 | 
				
			||||||
 | 
					                position='bottomRight'
 | 
				
			||||||
 | 
					                menu={chatsArray}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    padding: '8px 4px',
 | 
				
			||||||
 | 
					                    color: 'rgba(var(--semi-teal-7), 1)',
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  type='primary'
 | 
				
			||||||
 | 
					                  icon={<IconTreeTriangleDown />}
 | 
				
			||||||
 | 
					                ></Button>
 | 
				
			||||||
 | 
					              </Dropdown>
 | 
				
			||||||
 | 
					            </SplitButtonGroup>
 | 
				
			||||||
 | 
					            <Popconfirm
 | 
				
			||||||
 | 
					              title='确定是否要删除此令牌?'
 | 
				
			||||||
 | 
					              content='此修改将不可逆'
 | 
				
			||||||
 | 
					              okType={'danger'}
 | 
				
			||||||
 | 
					              position={'left'}
 | 
				
			||||||
 | 
					              onConfirm={() => {
 | 
				
			||||||
 | 
					                manageToken(record.id, 'delete', record).then(() => {
 | 
				
			||||||
 | 
					                  removeRecord(record.key);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
				
			||||||
 | 
					                删除
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </Popconfirm>
 | 
				
			||||||
 | 
					            {record.status === 1 ? (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                theme='light'
 | 
				
			||||||
 | 
					                type='warning'
 | 
				
			||||||
 | 
					                style={{ marginRight: 1 }}
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  manageToken(record.id, 'disable', record);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                禁用
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                theme='light'
 | 
				
			||||||
 | 
					                type='secondary'
 | 
				
			||||||
 | 
					                style={{ marginRight: 1 }}
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  manageToken(record.id, 'enable', record);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                启用
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              theme='light'
 | 
				
			||||||
 | 
					              type='tertiary'
 | 
				
			||||||
 | 
					              style={{ marginRight: 1 }}
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                setEditingToken(record);
 | 
				
			||||||
 | 
					                setShowEdit(true);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              编辑
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -333,8 +325,7 @@ const TokensTable = () => {
 | 
				
			|||||||
  const [searchKeyword, setSearchKeyword] = useState('');
 | 
					  const [searchKeyword, setSearchKeyword] = useState('');
 | 
				
			||||||
  const [searchToken, setSearchToken] = useState('');
 | 
					  const [searchToken, setSearchToken] = useState('');
 | 
				
			||||||
  const [searching, setSearching] = useState(false);
 | 
					  const [searching, setSearching] = useState(false);
 | 
				
			||||||
  const [showTopUpModal, setShowTopUpModal] = useState(false);
 | 
					  const [chats, setChats] = useState([]);
 | 
				
			||||||
  const [targetTokenIdx, setTargetTokenIdx] = useState(0);
 | 
					 | 
				
			||||||
  const [editingToken, setEditingToken] = useState({
 | 
					  const [editingToken, setEditingToken] = useState({
 | 
				
			||||||
    id: undefined,
 | 
					    id: undefined,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -379,16 +370,6 @@ const TokensTable = () => {
 | 
				
			|||||||
    setLoading(false);
 | 
					    setLoading(false);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onPaginationChange = (e, { activePage }) => {
 | 
					 | 
				
			||||||
    (async () => {
 | 
					 | 
				
			||||||
      if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
 | 
					 | 
				
			||||||
        // In this case we have to load more data and then append them.
 | 
					 | 
				
			||||||
        await loadTokens(activePage - 1);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      setActivePage(activePage);
 | 
					 | 
				
			||||||
    })();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const refresh = async () => {
 | 
					  const refresh = async () => {
 | 
				
			||||||
    await loadTokens(activePage - 1);
 | 
					    await loadTokens(activePage - 1);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -405,7 +386,8 @@ const TokensTable = () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onOpenLink = async (type, key) => {
 | 
					  const onOpenLink = async (type, url, record) => {
 | 
				
			||||||
 | 
					    // console.log(type, url, key);
 | 
				
			||||||
    let status = localStorage.getItem('status');
 | 
					    let status = localStorage.getItem('status');
 | 
				
			||||||
    let serverAddress = '';
 | 
					    let serverAddress = '';
 | 
				
			||||||
    if (status) {
 | 
					    if (status) {
 | 
				
			||||||
@@ -416,36 +398,39 @@ const TokensTable = () => {
 | 
				
			|||||||
      serverAddress = window.location.origin;
 | 
					      serverAddress = window.location.origin;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let encodedServerAddress = encodeURIComponent(serverAddress);
 | 
					    let encodedServerAddress = encodeURIComponent(serverAddress);
 | 
				
			||||||
    const chatLink = localStorage.getItem('chat_link');
 | 
					    url = url.replace('{address}', encodedServerAddress);
 | 
				
			||||||
    const mjLink = localStorage.getItem('chat_link2');
 | 
					    url = url.replace('{key}', 'sk-' + record.key);
 | 
				
			||||||
    let defaultUrl;
 | 
					    // console.log(url);
 | 
				
			||||||
 | 
					    // const chatLink = localStorage.getItem('chat_link');
 | 
				
			||||||
    if (chatLink) {
 | 
					    // const mjLink = localStorage.getItem('chat_link2');
 | 
				
			||||||
      defaultUrl =
 | 
					    // let defaultUrl;
 | 
				
			||||||
        chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
					    //
 | 
				
			||||||
    }
 | 
					    // if (chatLink) {
 | 
				
			||||||
    let url;
 | 
					    //   defaultUrl =
 | 
				
			||||||
    switch (type) {
 | 
					    //     chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
				
			||||||
      case 'ama':
 | 
					    // }
 | 
				
			||||||
        url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
 | 
					    // let url;
 | 
				
			||||||
        break;
 | 
					    // switch (type) {
 | 
				
			||||||
      case 'opencat':
 | 
					    //   case 'ama':
 | 
				
			||||||
        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
					    //     url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
 | 
				
			||||||
        break;
 | 
					    //     break;
 | 
				
			||||||
      case 'lobe':
 | 
					    //   case 'opencat':
 | 
				
			||||||
        url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
 | 
					    //     url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
				
			||||||
        break;
 | 
					    //     break;
 | 
				
			||||||
      case 'next-mj':
 | 
					    //   case 'lobe':
 | 
				
			||||||
        url =
 | 
					    //     url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
 | 
				
			||||||
          mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
					    //     break;
 | 
				
			||||||
        break;
 | 
					    //   case 'next-mj':
 | 
				
			||||||
      default:
 | 
					    //     url =
 | 
				
			||||||
        if (!chatLink) {
 | 
					    //       mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
				
			||||||
          showError('管理员未设置聊天链接');
 | 
					    //     break;
 | 
				
			||||||
          return;
 | 
					    //   default:
 | 
				
			||||||
        }
 | 
					    //     if (!chatLink) {
 | 
				
			||||||
        url = defaultUrl;
 | 
					    //       showError('管理员未设置聊天链接');
 | 
				
			||||||
    }
 | 
					    //       return;
 | 
				
			||||||
 | 
					    //     }
 | 
				
			||||||
 | 
					    //     url = defaultUrl;
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.open(url, '_blank');
 | 
					    window.open(url, '_blank');
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -151,7 +151,7 @@ const UsersTable = () => {
 | 
				
			|||||||
                title='确定?'
 | 
					                title='确定?'
 | 
				
			||||||
                okType={'warning'}
 | 
					                okType={'warning'}
 | 
				
			||||||
                onConfirm={() => {
 | 
					                onConfirm={() => {
 | 
				
			||||||
                  manageUser(record.username, 'promote', record);
 | 
					                  manageUser(record.id, 'promote', record);
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <Button theme='light' type='warning' style={{ marginRight: 1 }}>
 | 
					                <Button theme='light' type='warning' style={{ marginRight: 1 }}>
 | 
				
			||||||
@@ -162,7 +162,7 @@ const UsersTable = () => {
 | 
				
			|||||||
                title='确定?'
 | 
					                title='确定?'
 | 
				
			||||||
                okType={'warning'}
 | 
					                okType={'warning'}
 | 
				
			||||||
                onConfirm={() => {
 | 
					                onConfirm={() => {
 | 
				
			||||||
                  manageUser(record.username, 'demote', record);
 | 
					                  manageUser(record.id, 'demote', record);
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <Button
 | 
					                <Button
 | 
				
			||||||
@@ -179,7 +179,7 @@ const UsersTable = () => {
 | 
				
			|||||||
                  type='warning'
 | 
					                  type='warning'
 | 
				
			||||||
                  style={{ marginRight: 1 }}
 | 
					                  style={{ marginRight: 1 }}
 | 
				
			||||||
                  onClick={async () => {
 | 
					                  onClick={async () => {
 | 
				
			||||||
                    manageUser(record.username, 'disable', record);
 | 
					                    manageUser(record.id, 'disable', record);
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  禁用
 | 
					                  禁用
 | 
				
			||||||
@@ -190,7 +190,7 @@ const UsersTable = () => {
 | 
				
			|||||||
                  type='secondary'
 | 
					                  type='secondary'
 | 
				
			||||||
                  style={{ marginRight: 1 }}
 | 
					                  style={{ marginRight: 1 }}
 | 
				
			||||||
                  onClick={async () => {
 | 
					                  onClick={async () => {
 | 
				
			||||||
                    manageUser(record.username, 'enable', record);
 | 
					                    manageUser(record.id, 'enable', record);
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                  disabled={record.status === 3}
 | 
					                  disabled={record.status === 3}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
@@ -214,7 +214,7 @@ const UsersTable = () => {
 | 
				
			|||||||
                okType={'danger'}
 | 
					                okType={'danger'}
 | 
				
			||||||
                position={'left'}
 | 
					                position={'left'}
 | 
				
			||||||
                onConfirm={() => {
 | 
					                onConfirm={() => {
 | 
				
			||||||
                  manageUser(record.username, 'delete', record).then(() => {
 | 
					                  manageUser(record.id, 'delete', record).then(() => {
 | 
				
			||||||
                    removeRecord(record.id);
 | 
					                    removeRecord(record.id);
 | 
				
			||||||
                  });
 | 
					                  });
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
@@ -303,9 +303,9 @@ const UsersTable = () => {
 | 
				
			|||||||
    fetchGroups().then();
 | 
					    fetchGroups().then();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const manageUser = async (username, action, record) => {
 | 
					  const manageUser = async (userId, action, record) => {
 | 
				
			||||||
    const res = await API.post('/api/user/manage', {
 | 
					    const res = await API.post('/api/user/manage', {
 | 
				
			||||||
      username,
 | 
					      id: userId,
 | 
				
			||||||
      action,
 | 
					      action,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const { success, message } = res.data;
 | 
					    const { success, message } = res.data;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ import { API, showError } from '../helpers';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function fetchTokenKeys() {
 | 
					async function fetchTokenKeys() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const response = await API.get('/api/token/?p=0&size=999');
 | 
					    const response = await API.get('/api/token/?p=0&size=100');
 | 
				
			||||||
    const { success, data } = response.data;
 | 
					    const { success, data } = response.data;
 | 
				
			||||||
    if (success) {
 | 
					    if (success) {
 | 
				
			||||||
      const activeTokens = data.filter((token) => token.status === 1);
 | 
					      const activeTokens = data.filter((token) => token.status === 1);
 | 
				
			||||||
@@ -38,9 +38,9 @@ function getServerAddress() {
 | 
				
			|||||||
  return serverAddress;
 | 
					  return serverAddress;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useTokenKeys() {
 | 
					export function useTokenKeys(id) {
 | 
				
			||||||
  const [keys, setKeys] = useState([]);
 | 
					  const [keys, setKeys] = useState([]);
 | 
				
			||||||
  const [chatLink, setChatLink] = useState('');
 | 
					  // const [chatLink, setChatLink] = useState('');
 | 
				
			||||||
  const [serverAddress, setServerAddress] = useState('');
 | 
					  const [serverAddress, setServerAddress] = useState('');
 | 
				
			||||||
  const [isLoading, setIsLoading] = useState(true);
 | 
					  const [isLoading, setIsLoading] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -55,9 +55,7 @@ export function useTokenKeys() {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      setKeys(fetchedKeys);
 | 
					      setKeys(fetchedKeys);
 | 
				
			||||||
      setIsLoading(false);
 | 
					      setIsLoading(false);
 | 
				
			||||||
 | 
					      // setChatLink(link);
 | 
				
			||||||
      const link = localStorage.getItem('chat_link');
 | 
					 | 
				
			||||||
      setChatLink(link);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const address = getServerAddress();
 | 
					      const address = getServerAddress();
 | 
				
			||||||
      setServerAddress(address);
 | 
					      setServerAddress(address);
 | 
				
			||||||
@@ -66,5 +64,5 @@ export function useTokenKeys() {
 | 
				
			|||||||
    loadAllData();
 | 
					    loadAllData();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { keys, chatLink, serverAddress, isLoading };
 | 
					  return { keys, serverAddress, isLoading };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ export function setStatusData(data) {
 | 
				
			|||||||
  localStorage.setItem('enable_drawing', data.enable_drawing);
 | 
					  localStorage.setItem('enable_drawing', data.enable_drawing);
 | 
				
			||||||
  localStorage.setItem('enable_task', data.enable_task);
 | 
					  localStorage.setItem('enable_task', data.enable_task);
 | 
				
			||||||
  localStorage.setItem('enable_data_export', data.enable_data_export);
 | 
					  localStorage.setItem('enable_data_export', data.enable_data_export);
 | 
				
			||||||
 | 
					  localStorage.setItem('chats', JSON.stringify(data.chats));
 | 
				
			||||||
  localStorage.setItem(
 | 
					  localStorage.setItem(
 | 
				
			||||||
    'data_export_default_time',
 | 
					    'data_export_default_time',
 | 
				
			||||||
    data.data_export_default_time,
 | 
					    data.data_export_default_time,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,6 +59,17 @@ body {
 | 
				
			|||||||
  display: revert;
 | 
					  display: revert;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.semi-chat {
 | 
				
			||||||
 | 
					  padding-top: 0 !important;
 | 
				
			||||||
 | 
					  padding-bottom: 0 !important;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.semi-chat-chatBox-content {
 | 
				
			||||||
 | 
					  min-width: auto;
 | 
				
			||||||
 | 
					  word-break: break-word;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tableHiddle {
 | 
					.tableHiddle {
 | 
				
			||||||
  display: none !important;
 | 
					  display: none !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,32 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React, { useEffect } from 'react';
 | 
				
			||||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
 | 
					import { useTokenKeys } from '../../components/fetchTokenKeys';
 | 
				
			||||||
import { Layout } from '@douyinfe/semi-ui';
 | 
					import { Banner, Layout } from '@douyinfe/semi-ui';
 | 
				
			||||||
 | 
					import { useParams } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChatPage = () => {
 | 
					const ChatPage = () => {
 | 
				
			||||||
  const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
 | 
					  const { id } = useParams();
 | 
				
			||||||
 | 
					  const { keys, serverAddress, isLoading } = useTokenKeys(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const comLink = (key) => {
 | 
					  const comLink = (key) => {
 | 
				
			||||||
    if (!chatLink || !serverAddress || !key) return '';
 | 
					    // console.log('chatLink:', chatLink);
 | 
				
			||||||
    return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
 | 
					    if (!serverAddress || !key) return '';
 | 
				
			||||||
 | 
					    let link = localStorage.getItem('chat_link');
 | 
				
			||||||
 | 
					    if (link) {
 | 
				
			||||||
 | 
					      link = `${link}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
 | 
				
			||||||
 | 
					    } else if (id) {
 | 
				
			||||||
 | 
					      let chats = localStorage.getItem('chats');
 | 
				
			||||||
 | 
					      if (chats) {
 | 
				
			||||||
 | 
					        chats = JSON.parse(chats);
 | 
				
			||||||
 | 
					        if (Array.isArray(chats) && chats.length > 0) {
 | 
				
			||||||
 | 
					          for (let k in chats[id]) {
 | 
				
			||||||
 | 
					            link = chats[id][k];
 | 
				
			||||||
 | 
					            link = link.replace('{address}', encodeURIComponent(serverAddress));
 | 
				
			||||||
 | 
					            link = link.replace('{key}', 'sk-' + key);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return link;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
 | 
					  const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
 | 
				
			||||||
@@ -22,11 +41,7 @@ const ChatPage = () => {
 | 
				
			|||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <Layout>
 | 
					      <Layout>
 | 
				
			||||||
        <Layout.Header>
 | 
					        <Layout.Header>
 | 
				
			||||||
          <h3 style={{ color: 'red' }}>
 | 
					          <Banner description={'正在跳转......'} type={'warning'} />
 | 
				
			||||||
            当前没有可用的已启用令牌,请确认是否有令牌处于启用状态!
 | 
					 | 
				
			||||||
            <br />
 | 
					 | 
				
			||||||
            正在跳转......
 | 
					 | 
				
			||||||
          </h3>
 | 
					 | 
				
			||||||
        </Layout.Header>
 | 
					        </Layout.Header>
 | 
				
			||||||
      </Layout>
 | 
					      </Layout>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										162
									
								
								web/src/pages/Setting/Operation/SettingsChats.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								web/src/pages/Setting/Operation/SettingsChats.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					import React, { useEffect, useState, useRef } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Banner,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Col,
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  Popconfirm,
 | 
				
			||||||
 | 
					  Row,
 | 
				
			||||||
 | 
					  Space,
 | 
				
			||||||
 | 
					  Spin,
 | 
				
			||||||
 | 
					} from '@douyinfe/semi-ui';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  compareObjects,
 | 
				
			||||||
 | 
					  API,
 | 
				
			||||||
 | 
					  showError,
 | 
				
			||||||
 | 
					  showSuccess,
 | 
				
			||||||
 | 
					  showWarning,
 | 
				
			||||||
 | 
					  verifyJSON,
 | 
				
			||||||
 | 
					  verifyJSONPromise,
 | 
				
			||||||
 | 
					} from '../../../helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SettingsChats(props) {
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [inputs, setInputs] = useState({
 | 
				
			||||||
 | 
					    Chats: '[]',
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const refForm = useRef();
 | 
				
			||||||
 | 
					  const [inputsRow, setInputsRow] = useState(inputs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function onSubmit() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      console.log('Starting validation...');
 | 
				
			||||||
 | 
					      await refForm.current
 | 
				
			||||||
 | 
					        .validate()
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          console.log('Validation passed');
 | 
				
			||||||
 | 
					          const updateArray = compareObjects(inputs, inputsRow);
 | 
				
			||||||
 | 
					          if (!updateArray.length) return showWarning('你似乎并没有修改什么');
 | 
				
			||||||
 | 
					          const requestQueue = updateArray.map((item) => {
 | 
				
			||||||
 | 
					            let value = '';
 | 
				
			||||||
 | 
					            if (typeof inputs[item.key] === 'boolean') {
 | 
				
			||||||
 | 
					              value = String(inputs[item.key]);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              value = inputs[item.key];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return API.put('/api/option/', {
 | 
				
			||||||
 | 
					              key: item.key,
 | 
				
			||||||
 | 
					              value,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          setLoading(true);
 | 
				
			||||||
 | 
					          Promise.all(requestQueue)
 | 
				
			||||||
 | 
					            .then((res) => {
 | 
				
			||||||
 | 
					              if (requestQueue.length === 1) {
 | 
				
			||||||
 | 
					                if (res.includes(undefined)) return;
 | 
				
			||||||
 | 
					              } else if (requestQueue.length > 1) {
 | 
				
			||||||
 | 
					                if (res.includes(undefined))
 | 
				
			||||||
 | 
					                  return showError('部分保存失败,请重试');
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              showSuccess('保存成功');
 | 
				
			||||||
 | 
					              props.refresh();
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(() => {
 | 
				
			||||||
 | 
					              showError('保存失败,请重试');
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .finally(() => {
 | 
				
			||||||
 | 
					              setLoading(false);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Validation failed:', error);
 | 
				
			||||||
 | 
					          showError('请检查输入');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showError('请检查输入');
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function resetModelRatio() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      let res = await API.post(`/api/option/rest_model_ratio`);
 | 
				
			||||||
 | 
					      // return {success, message}
 | 
				
			||||||
 | 
					      if (res.data.success) {
 | 
				
			||||||
 | 
					        showSuccess(res.data.message);
 | 
				
			||||||
 | 
					        props.refresh();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        showError(res.data.message);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showError(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const currentInputs = {};
 | 
				
			||||||
 | 
					    for (let key in props.options) {
 | 
				
			||||||
 | 
					      if (Object.keys(inputs).includes(key)) {
 | 
				
			||||||
 | 
					        if (key === 'Chats') {
 | 
				
			||||||
 | 
					          const obj = JSON.parse(props.options[key]);
 | 
				
			||||||
 | 
					          currentInputs[key] = JSON.stringify(obj, null, 2);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          currentInputs[key] = props.options[key];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setInputs(currentInputs);
 | 
				
			||||||
 | 
					    setInputsRow(structuredClone(currentInputs));
 | 
				
			||||||
 | 
					    refForm.current.setValues(currentInputs);
 | 
				
			||||||
 | 
					  }, [props.options]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Spin spinning={loading}>
 | 
				
			||||||
 | 
					      <Form
 | 
				
			||||||
 | 
					        values={inputs}
 | 
				
			||||||
 | 
					        getFormApi={(formAPI) => (refForm.current = formAPI)}
 | 
				
			||||||
 | 
					        style={{ marginBottom: 15 }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Form.Section text={'令牌聊天设置'}>
 | 
				
			||||||
 | 
					          <Banner
 | 
				
			||||||
 | 
					            type='warning'
 | 
				
			||||||
 | 
					            description={
 | 
				
			||||||
 | 
					              '必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <Banner
 | 
				
			||||||
 | 
					            type='info'
 | 
				
			||||||
 | 
					            description={
 | 
				
			||||||
 | 
					              '链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <Form.TextArea
 | 
				
			||||||
 | 
					            label={'聊天配置'}
 | 
				
			||||||
 | 
					            extraText={''}
 | 
				
			||||||
 | 
					            placeholder={'为一个 JSON 文本'}
 | 
				
			||||||
 | 
					            field={'Chats'}
 | 
				
			||||||
 | 
					            autosize={{ minRows: 6, maxRows: 12 }}
 | 
				
			||||||
 | 
					            trigger='blur'
 | 
				
			||||||
 | 
					            stopValidateWithError
 | 
				
			||||||
 | 
					            rules={[
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                validator: (rule, value) => {
 | 
				
			||||||
 | 
					                  return verifyJSON(value);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                message: '不是合法的 JSON 字符串',
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					            onChange={(value) =>
 | 
				
			||||||
 | 
					              setInputs({
 | 
				
			||||||
 | 
					                ...inputs,
 | 
				
			||||||
 | 
					                Chats: value,
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Form.Section>
 | 
				
			||||||
 | 
					      </Form>
 | 
				
			||||||
 | 
					      <Space>
 | 
				
			||||||
 | 
					        <Button onClick={onSubmit}>保存聊天设置</Button>
 | 
				
			||||||
 | 
					      </Space>
 | 
				
			||||||
 | 
					    </Spin>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import React, { useEffect, useState, useRef } from 'react';
 | 
					import React, { useEffect, useState, useRef } from 'react';
 | 
				
			||||||
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
 | 
					import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  compareObjects,
 | 
					  compareObjects,
 | 
				
			||||||
  API,
 | 
					  API,
 | 
				
			||||||
@@ -74,6 +74,10 @@ export default function GeneralSettings(props) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Spin spinning={loading}>
 | 
					      <Spin spinning={loading}>
 | 
				
			||||||
 | 
					        <Banner
 | 
				
			||||||
 | 
					          type='warning'
 | 
				
			||||||
 | 
					          description={'聊天链接功能已经弃用,请使用下方聊天设置功能'}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <Form
 | 
					        <Form
 | 
				
			||||||
          values={inputs}
 | 
					          values={inputs}
 | 
				
			||||||
          getFormApi={(formAPI) => (refForm.current = formAPI)}
 | 
					          getFormApi={(formAPI) => (refForm.current = formAPI)}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,7 +50,7 @@ const EditToken = (props) => {
 | 
				
			|||||||
    group,
 | 
					    group,
 | 
				
			||||||
  } = inputs;
 | 
					  } = inputs;
 | 
				
			||||||
  // const [visible, setVisible] = useState(false);
 | 
					  // const [visible, setVisible] = useState(false);
 | 
				
			||||||
  const [models, setModels] = useState({});
 | 
					  const [models, setModels] = useState([]);
 | 
				
			||||||
  const [groups, setGroups] = useState([]);
 | 
					  const [groups, setGroups] = useState([]);
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const handleInputChange = (name, value) => {
 | 
					  const handleInputChange = (name, value) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1261
									
								
								web/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										1261
									
								
								web/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user