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)
 | 
			
		||||
 | 
			
		||||
## 界面截图
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

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

 | 
			
		||||

 | 
			
		||||
 
 | 
			
		||||
@@ -143,6 +143,10 @@ const (
 | 
			
		||||
	RoleRootUser   = 100
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func IsValidateRole(role int) bool {
 | 
			
		||||
	return role == RoleGuestUser || role == RoleCommonUser || role == RoleAdminUser || role == RoleRootUser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	FileUploadPermission    = RoleGuestUser
 | 
			
		||||
	FileDownloadPermission  = RoleGuestUser
 | 
			
		||||
 
 | 
			
		||||
@@ -350,13 +350,14 @@ func GetCompletionRatio(name string) float64 {
 | 
			
		||||
		return 4.0 / 3.0
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4-turbo") || strings.HasPrefix(name, "gpt-4o") {
 | 
			
		||||
			return 3
 | 
			
		||||
		}
 | 
			
		||||
		return 2
 | 
			
		||||
	}
 | 
			
		||||
	if "o1" == name || strings.HasPrefix(name, "o1-") {
 | 
			
		||||
@@ -376,10 +377,7 @@ func GetCompletionRatio(name string) float64 {
 | 
			
		||||
		return 3
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "gemini-") {
 | 
			
		||||
		if strings.Contains(name, "flash") {
 | 
			
		||||
			return 4
 | 
			
		||||
		}
 | 
			
		||||
		return 3
 | 
			
		||||
		return 4
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "command") {
 | 
			
		||||
		switch name {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,3 +21,8 @@ func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
 | 
			
		||||
	UserUsableGroups = make(map[string]string)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GroupInUserUsableGroups(groupName string) bool {
 | 
			
		||||
	_, ok := UserUsableGroups[groupName]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,14 @@ package common
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	crand "crypto/rand"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"golang.org/x/net/proxy"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
@@ -147,24 +150,35 @@ func GetUUID() string {
 | 
			
		||||
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rand.Seed(time.Now().UnixNano())
 | 
			
		||||
	rand.New(rand.NewSource(time.Now().UnixNano()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GenerateKey() string {
 | 
			
		||||
	//rand.Seed(time.Now().UnixNano())
 | 
			
		||||
	key := make([]byte, 48)
 | 
			
		||||
	for i := 0; i < 16; i++ {
 | 
			
		||||
		key[i] = keyChars[rand.Intn(len(keyChars))]
 | 
			
		||||
	}
 | 
			
		||||
	uuid_ := GetUUID()
 | 
			
		||||
	for i := 0; i < 32; i++ {
 | 
			
		||||
		c := uuid_[i]
 | 
			
		||||
		if i%2 == 0 && c >= 'a' && c <= 'z' {
 | 
			
		||||
			c = c - 'a' + 'A'
 | 
			
		||||
func GenerateRandomCharsKey(length int) (string, error) {
 | 
			
		||||
	b := make([]byte, length)
 | 
			
		||||
	maxI := big.NewInt(int64(len(keyChars)))
 | 
			
		||||
 | 
			
		||||
	for i := range b {
 | 
			
		||||
		n, err := crand.Int(crand.Reader, maxI)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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{
 | 
			
		||||
		GitHubId: githubUser.Login,
 | 
			
		||||
	}
 | 
			
		||||
	// IsGitHubIdAlreadyTaken is unscoped
 | 
			
		||||
	if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
 | 
			
		||||
		// FillUserByGitHubId is scoped
 | 
			
		||||
		err := user.FillUserByGitHubId()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
@@ -121,6 +123,14 @@ func GitHubOAuth(c *gin.Context) {
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// if user.Id == 0 , user has been deleted
 | 
			
		||||
		if user.Id == 0 {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "用户已注销",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if common.RegisterEnabled {
 | 
			
		||||
			user.InviterId, _ = model.GetUserIdByAffCode(c.Query("aff"))
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,14 @@ func LinuxDoOAuth(c *gin.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if user.Id == 0 {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "用户已注销",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user.LinuxDoLevel = linuxdoUser.TrustLevel
 | 
			
		||||
		err = user.Update(false)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,7 @@ func GetStatus(c *gin.Context) {
 | 
			
		||||
			"default_collapse_sidebar": common.DefaultCollapseSidebar,
 | 
			
		||||
			"payment_enabled":          common.PaymentEnabled,
 | 
			
		||||
			"mj_notify_enabled":        constant.MjNotifyEnabled,
 | 
			
		||||
			"chats":                    constant.Chats,
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,58 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
 | 
			
		||||
	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) {
 | 
			
		||||
	relayMode := constant.Path2RelayMode(c.Request.URL.Path)
 | 
			
		||||
	requestId := c.GetString(common.RequestIdKey)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"sort"
 | 
			
		||||
@@ -48,6 +49,13 @@ func TelegramBind(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if user.Id == 0 {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "用户已注销",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user.TelegramId = telegramId
 | 
			
		||||
	if err := user.Update(false); err != nil {
 | 
			
		||||
		c.JSON(200, gin.H{
 | 
			
		||||
 
 | 
			
		||||
@@ -123,10 +123,19 @@ func AddToken(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		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{
 | 
			
		||||
		UserId:             c.GetInt("id"),
 | 
			
		||||
		Name:               token.Name,
 | 
			
		||||
		Key:                common.GenerateKey(),
 | 
			
		||||
		Key:                key,
 | 
			
		||||
		CreatedTime:        common.GetTimestamp(),
 | 
			
		||||
		AccessedTime:       common.GetTimestamp(),
 | 
			
		||||
		ExpiredTime:        token.ExpiredTime,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
@@ -67,6 +68,7 @@ func setupLogin(user *model.User, c *gin.Context) {
 | 
			
		||||
	session.Set("username", user.Username)
 | 
			
		||||
	session.Set("role", user.Role)
 | 
			
		||||
	session.Set("status", user.Status)
 | 
			
		||||
	session.Set("group", user.Group)
 | 
			
		||||
	session.Set("linuxdo_enable", user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel)
 | 
			
		||||
	err := session.Save()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -159,8 +161,9 @@ func Register(c *gin.Context) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
			"message": "数据库错误,请稍后重试",
 | 
			
		||||
		})
 | 
			
		||||
		common.SysError(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if exist {
 | 
			
		||||
@@ -200,11 +203,20 @@ func Register(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	// 生成默认令牌
 | 
			
		||||
	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{
 | 
			
		||||
			UserId:             insertedUser.Id, // 使用插入后的用户ID
 | 
			
		||||
			Name:               cleanUser.Username + "的初始令牌",
 | 
			
		||||
			Key:                common.GenerateKey(),
 | 
			
		||||
			Key:                key,
 | 
			
		||||
			CreatedTime:        common.GetTimestamp(),
 | 
			
		||||
			AccessedTime:       common.GetTimestamp(),
 | 
			
		||||
			ExpiredTime:        -1,     // 永不过期
 | 
			
		||||
@@ -311,7 +323,18 @@ func GenerateAccessToken(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		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 {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
@@ -631,6 +654,7 @@ func DeleteSelf(c *gin.Context) {
 | 
			
		||||
func CreateUser(c *gin.Context) {
 | 
			
		||||
	var user model.User
 | 
			
		||||
	err := json.NewDecoder(c.Request.Body).Decode(&user)
 | 
			
		||||
	user.Username = strings.TrimSpace(user.Username)
 | 
			
		||||
	if err != nil || user.Username == "" || user.Password == "" {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
@@ -678,8 +702,8 @@ func CreateUser(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ManageRequest struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Action   string `json:"action"`
 | 
			
		||||
	Id     int    `json:"id"`
 | 
			
		||||
	Action string `json:"action"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ManageUser Only admin user can do this
 | 
			
		||||
@@ -695,7 +719,7 @@ func ManageUser(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		Username: req.Username,
 | 
			
		||||
		Id: req.Id,
 | 
			
		||||
	}
 | 
			
		||||
	// Fill attributes
 | 
			
		||||
	model.DB.Unscoped().Where(&user).First(&user)
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,13 @@ func WeChatAuth(c *gin.Context) {
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if user.Id == 0 {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "用户已注销",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if common.RegisterEnabled {
 | 
			
		||||
			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"`
 | 
			
		||||
	Dimensions        int            `json:"dimensions,omitempty"`
 | 
			
		||||
	ParallelToolCalls bool           `json:"parallel_Tool_Calls,omitempty"`
 | 
			
		||||
	EncodingFormat    string         `json:"encoding_format,omitempty"`
 | 
			
		||||
	EncodingFormat    any         `json:"encoding_format,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAITools struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,17 @@ import (
 | 
			
		||||
	"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) {
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	username := session.Get("username")
 | 
			
		||||
@@ -31,6 +42,14 @@ func authHelper(c *gin.Context, minRole int) {
 | 
			
		||||
		}
 | 
			
		||||
		user := model.ValidateAccessToken(accessToken)
 | 
			
		||||
		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
 | 
			
		||||
			username = user.Username
 | 
			
		||||
			role = user.Role
 | 
			
		||||
@@ -101,9 +120,19 @@ func authHelper(c *gin.Context, minRole int) {
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		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("role", role)
 | 
			
		||||
	c.Set("id", id)
 | 
			
		||||
	c.Set("group", session.Get("group"))
 | 
			
		||||
	c.Set("use_access_token", useAccessToken)
 | 
			
		||||
	c.Next()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ func createRootAccountIfNeed() error {
 | 
			
		||||
			Role:        common.RoleRootUser,
 | 
			
		||||
			Status:      common.UserStatusEnabled,
 | 
			
		||||
			DisplayName: "Root User",
 | 
			
		||||
			AccessToken: common.GetUUID(),
 | 
			
		||||
			AccessToken: nil,
 | 
			
		||||
			Quota:       100000000,
 | 
			
		||||
		}
 | 
			
		||||
		DB.Create(&rootUser)
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(common.StripeUnitPrice, 'f', -1, 64)
 | 
			
		||||
	common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
 | 
			
		||||
	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 | 
			
		||||
	common.OptionMap["Chats"] = constant.Chats2JsonString()
 | 
			
		||||
	common.OptionMap["GitHubClientId"] = ""
 | 
			
		||||
	common.OptionMap["GitHubClientSecret"] = ""
 | 
			
		||||
	common.OptionMap["LinuxDoClientId"] = ""
 | 
			
		||||
@@ -252,6 +253,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		common.ServerAddress = value
 | 
			
		||||
	case "OutProxyUrl":
 | 
			
		||||
		common.OutProxyUrl = value
 | 
			
		||||
	case "Chats":
 | 
			
		||||
		err = constant.UpdateChatsByJsonString(value)
 | 
			
		||||
	case "StripeApiSecret":
 | 
			
		||||
		common.StripeApiSecret = value
 | 
			
		||||
	case "StripeWebhookSecret":
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	relaycommon "one-api/relay/common"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
@@ -256,51 +257,56 @@ func decreaseTokenQuota(id int, quota int) (err error) {
 | 
			
		||||
	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 {
 | 
			
		||||
		return 0, errors.New("quota 不能为负数!")
 | 
			
		||||
	}
 | 
			
		||||
	token, err := GetTokenById(tokenId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	if !relayInfo.IsPlayground {
 | 
			
		||||
		token, err := GetTokenById(relayInfo.TokenId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		if !token.UnlimitedQuota && token.RemainQuota < quota {
 | 
			
		||||
			return 0, errors.New("令牌额度不足")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !token.UnlimitedQuota && token.RemainQuota < quota {
 | 
			
		||||
		return 0, errors.New("令牌额度不足")
 | 
			
		||||
	}
 | 
			
		||||
	userQuota, err = GetUserQuota(token.UserId)
 | 
			
		||||
	userQuota, err = GetUserQuota(relayInfo.UserId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	if userQuota < quota {
 | 
			
		||||
		return 0, errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
 | 
			
		||||
	}
 | 
			
		||||
	err = DecreaseTokenQuota(tokenId, quota)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	if !relayInfo.IsPlayground {
 | 
			
		||||
		err = DecreaseTokenQuota(relayInfo.TokenId, quota)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	err = DecreaseUserQuota(token.UserId, quota)
 | 
			
		||||
	err = DecreaseUserQuota(relayInfo.UserId, quota)
 | 
			
		||||
	return userQuota - quota, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuota int, sendEmail bool) (err error) {
 | 
			
		||||
	token, err := GetTokenById(tokenId)
 | 
			
		||||
func PostConsumeTokenQuota(relayInfo *relaycommon.RelayInfo, userQuota int, quota int, preConsumedQuota int, sendEmail bool) (err error) {
 | 
			
		||||
 | 
			
		||||
	if quota > 0 {
 | 
			
		||||
		err = DecreaseUserQuota(token.UserId, quota)
 | 
			
		||||
		err = DecreaseUserQuota(relayInfo.UserId, quota)
 | 
			
		||||
	} else {
 | 
			
		||||
		err = IncreaseUserQuota(token.UserId, -quota)
 | 
			
		||||
		err = IncreaseUserQuota(relayInfo.UserId, -quota)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if quota > 0 {
 | 
			
		||||
		err = DecreaseTokenQuota(tokenId, quota)
 | 
			
		||||
	} else {
 | 
			
		||||
		err = IncreaseTokenQuota(tokenId, -quota)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	if !relayInfo.IsPlayground {
 | 
			
		||||
		if quota > 0 {
 | 
			
		||||
			err = DecreaseTokenQuota(relayInfo.TokenId, quota)
 | 
			
		||||
		} else {
 | 
			
		||||
			err = IncreaseTokenQuota(relayInfo.TokenId, -quota)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sendEmail {
 | 
			
		||||
@@ -309,7 +315,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
 | 
			
		||||
			noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
 | 
			
		||||
			if quotaTooLow || noMoreQuota {
 | 
			
		||||
				go func() {
 | 
			
		||||
					email, err := GetUserEmail(token.UserId)
 | 
			
		||||
					email, err := GetUserEmail(relayInfo.UserId)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						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"`
 | 
			
		||||
	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!
 | 
			
		||||
	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"`
 | 
			
		||||
	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
 | 
			
		||||
@@ -41,6 +41,17 @@ type User struct {
 | 
			
		||||
	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
 | 
			
		||||
func CheckUserExistOrDeleted(username string, email string) (bool, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
@@ -218,7 +229,7 @@ func (user *User) Insert(inviterId int) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	user.Quota = common.QuotaForNewUser
 | 
			
		||||
	user.AccessToken = common.GetUUID()
 | 
			
		||||
	//user.SetAccessToken(common.GetUUID())
 | 
			
		||||
	user.AffCode = common.GetRandomString(4)
 | 
			
		||||
	result := DB.Create(user)
 | 
			
		||||
	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,
 | 
			
		||||
	// it won’t be used to build query conditions
 | 
			
		||||
	password := user.Password
 | 
			
		||||
	if user.Username == "" || password == "" {
 | 
			
		||||
	username := strings.TrimSpace(user.Username)
 | 
			
		||||
	if username == "" || password == "" {
 | 
			
		||||
		return errors.New("用户名或密码为空")
 | 
			
		||||
	}
 | 
			
		||||
	// 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)
 | 
			
		||||
	if !okay || user.Status != common.UserStatusEnabled {
 | 
			
		||||
		return errors.New("用户名或密码错误,或用户已被封禁")
 | 
			
		||||
@@ -364,14 +376,6 @@ func (user *User) FillUserByWeChatId() error {
 | 
			
		||||
	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 {
 | 
			
		||||
	if user.TelegramId == "" {
 | 
			
		||||
		return errors.New("Telegram id 为空!")
 | 
			
		||||
@@ -384,27 +388,27 @@ func (user *User) FillUserByTelegramId() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
	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 {
 | 
			
		||||
	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 {
 | 
			
		||||
	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 {
 | 
			
		||||
	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
 | 
			
		||||
	return DB.Unscoped().Where("username = ?", username).Find(&User{}).RowsAffected == 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"one-api/dto"
 | 
			
		||||
	"one-api/relay/channel/claude"
 | 
			
		||||
	relaycommon "one-api/relay/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -31,11 +30,7 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
 | 
			
		||||
	if strings.HasPrefix(info.UpstreamModelName, "claude-3") {
 | 
			
		||||
		a.RequestMode = RequestModeMessage
 | 
			
		||||
	} else {
 | 
			
		||||
		a.RequestMode = RequestModeCompletion
 | 
			
		||||
	}
 | 
			
		||||
	a.RequestMode = RequestModeMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 err error
 | 
			
		||||
	if a.RequestMode == RequestModeCompletion {
 | 
			
		||||
		claudeReq = claude.RequestOpenAI2ClaudeComplete(*request)
 | 
			
		||||
	} else {
 | 
			
		||||
		claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request)
 | 
			
		||||
	}
 | 
			
		||||
	claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request)
 | 
			
		||||
 | 
			
		||||
	c.Set("request_model", request.Model)
 | 
			
		||||
	c.Set("converted_request", claudeReq)
 | 
			
		||||
	return claudeReq, err
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ type RelayInfo struct {
 | 
			
		||||
	setFirstResponse     bool
 | 
			
		||||
	ApiType              int
 | 
			
		||||
	IsStream             bool
 | 
			
		||||
	IsPlayground         bool
 | 
			
		||||
	RelayMode            int
 | 
			
		||||
	UpstreamModelName    string
 | 
			
		||||
	OriginModelName      string
 | 
			
		||||
@@ -65,6 +66,11 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
 | 
			
		||||
		ApiKey:            strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
 | 
			
		||||
		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 == "" {
 | 
			
		||||
		info.BaseUrl = common.ChannelBaseURLs[channelType]
 | 
			
		||||
	}
 | 
			
		||||
@@ -146,3 +152,20 @@ func GenTaskRelayInfo(c *gin.Context) *TaskRelayInfo {
 | 
			
		||||
	}
 | 
			
		||||
	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 {
 | 
			
		||||
	relayMode := RelayModeUnknown
 | 
			
		||||
	if strings.HasPrefix(path, "/v1/chat/completions") {
 | 
			
		||||
	if strings.HasPrefix(path, "/v1/chat/completions") || strings.HasPrefix(path, "/pg/chat/completions") {
 | 
			
		||||
		relayMode = RelayModeChatCompletions
 | 
			
		||||
	} else if strings.HasPrefix(path, "/v1/completions") {
 | 
			
		||||
		relayMode = RelayModeCompletions
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 | 
			
		||||
		preConsumedQuota = 0
 | 
			
		||||
	}
 | 
			
		||||
	if preConsumedQuota > 0 {
 | 
			
		||||
		userQuota, err = model.PreConsumeTokenQuota(relayInfo.TokenId, preConsumedQuota)
 | 
			
		||||
		userQuota, err = model.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			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")
 | 
			
		||||
	if resp != nil {
 | 
			
		||||
		if resp.StatusCode != http.StatusOK {
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
			openaiErr := service.RelayErrorHandler(resp)
 | 
			
		||||
			// reset status code 重置状态码
 | 
			
		||||
			service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
@@ -136,7 +136,7 @@ func AudioHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 | 
			
		||||
 | 
			
		||||
	usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
 | 
			
		||||
	if openaiErr != nil {
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
		// reset status code 重置状态码
 | 
			
		||||
		service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
		return openaiErr
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"one-api/constant"
 | 
			
		||||
	"one-api/dto"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	relaycommon "one-api/relay/common"
 | 
			
		||||
	relayconstant "one-api/relay/constant"
 | 
			
		||||
	"one-api/service"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -146,6 +147,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
	channelId := c.GetInt("channel_id")
 | 
			
		||||
	relayInfo := relaycommon.GenRelayInfo(c)
 | 
			
		||||
	var swapFaceRequest dto.SwapFaceRequest
 | 
			
		||||
	err := common.UnmarshalBodyReusable(c, &swapFaceRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -191,7 +193,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
 | 
			
		||||
	}
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		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 {
 | 
			
		||||
				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")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
	channelId := c.GetInt("channel_id")
 | 
			
		||||
	relayInfo := relaycommon.GenRelayInfo(c)
 | 
			
		||||
	consumeQuota := true
 | 
			
		||||
	var midjRequest dto.MidjourneyRequest
 | 
			
		||||
	err := common.UnmarshalBodyReusable(c, &midjRequest)
 | 
			
		||||
@@ -495,7 +498,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
 | 
			
		||||
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		if consumeQuota && midjResponseWithStatus.StatusCode == 200 {
 | 
			
		||||
			err := model.PostConsumeTokenQuota(tokenId, userQuota, quota, 0, true)
 | 
			
		||||
			err := model.PostConsumeTokenQuota(relayInfo, userQuota, quota, 0, true)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -178,7 +178,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 | 
			
		||||
	if resp != nil {
 | 
			
		||||
		relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
		if resp.StatusCode != http.StatusOK {
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
			openaiErr := service.RelayErrorHandler(resp)
 | 
			
		||||
			// reset status code 重置状态码
 | 
			
		||||
			service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
@@ -188,7 +188,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 | 
			
		||||
 | 
			
		||||
	usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
 | 
			
		||||
	if openaiErr != nil {
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
		// reset status code 重置状态码
 | 
			
		||||
		service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
		return openaiErr
 | 
			
		||||
@@ -275,7 +275,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if preConsumedQuota > 0 {
 | 
			
		||||
		userQuota, err = model.PreConsumeTokenQuota(relayInfo.TokenId, preConsumedQuota)
 | 
			
		||||
		userQuota, err = model.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
		go func(ctx context.Context) {
 | 
			
		||||
			// return pre-consumed quota
 | 
			
		||||
			err := model.PostConsumeTokenQuota(tokenId, userQuota, -preConsumedQuota, 0, false)
 | 
			
		||||
			err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				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
 | 
			
		||||
		if quotaDelta != 0 {
 | 
			
		||||
			err := model.PostConsumeTokenQuota(relayInfo.TokenId, userQuota, quotaDelta, preConsumedQuota, true)
 | 
			
		||||
			err := model.PostConsumeTokenQuota(relayInfo, userQuota, quotaDelta, preConsumedQuota, true)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				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.StatusCode != http.StatusOK {
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
			returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
			openaiErr := service.RelayErrorHandler(resp)
 | 
			
		||||
			// reset status code 重置状态码
 | 
			
		||||
			service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
@@ -111,7 +111,7 @@ func RerankHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
 | 
			
		||||
 | 
			
		||||
	usage, openaiErr := adaptor.DoResponse(c, resp, relayInfo)
 | 
			
		||||
	if openaiErr != nil {
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo.TokenId, userQuota, preConsumedQuota)
 | 
			
		||||
		returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
 | 
			
		||||
		// reset status code 重置状态码
 | 
			
		||||
		service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 | 
			
		||||
		return openaiErr
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,8 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		// release quota
 | 
			
		||||
		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 {
 | 
			
		||||
				common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,11 @@ func SetRelayRouter(router *gin.Engine) {
 | 
			
		||||
		modelsRouter.GET("", controller.ListModels)
 | 
			
		||||
		modelsRouter.GET("/:model", controller.RetrieveModel)
 | 
			
		||||
	}
 | 
			
		||||
	playgroundRouter := router.Group("/pg")
 | 
			
		||||
	playgroundRouter.Use(middleware.UserAuth())
 | 
			
		||||
	{
 | 
			
		||||
		playgroundRouter.POST("/chat/completions", controller.Playground)
 | 
			
		||||
	}
 | 
			
		||||
	relayV1Router := router.Group("/v1")
 | 
			
		||||
	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") {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@douyinfe/semi-icons": "^2.46.1",
 | 
			
		||||
    "@douyinfe/semi-ui": "^2.55.3",
 | 
			
		||||
    "@douyinfe/semi-icons": "^2.63.1",
 | 
			
		||||
    "@douyinfe/semi-ui": "^2.63.1",
 | 
			
		||||
    "@visactor/react-vchart": "~1.8.8",
 | 
			
		||||
    "@visactor/vchart": "~1.8.8",
 | 
			
		||||
    "@visactor/vchart-semi-theme": "~1.8.8",
 | 
			
		||||
@@ -22,7 +22,8 @@
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
    "react-turnstile": "^1.0.5",
 | 
			
		||||
    "semantic-ui-offline": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3"
 | 
			
		||||
    "semantic-ui-react": "^2.1.3",
 | 
			
		||||
    "sse": "github:mpetazzoni/sse.js"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "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 Pricing from './pages/Pricing/index.js';
 | 
			
		||||
import Task from './pages/Task/index.js';
 | 
			
		||||
import Playground from './components/Playground.js';
 | 
			
		||||
 | 
			
		||||
const Home = lazy(() => import('./pages/Home'));
 | 
			
		||||
const Detail = lazy(() => import('./pages/Detail'));
 | 
			
		||||
@@ -101,6 +102,14 @@ function App() {
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/playground'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Playground />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/redemption'
 | 
			
		||||
          element={
 | 
			
		||||
@@ -256,7 +265,7 @@ function App() {
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/chat'
 | 
			
		||||
          path='/chat/:id?'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <Chat />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
 | 
			
		||||
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 { setUserData } from '../helpers/data.js';
 | 
			
		||||
 | 
			
		||||
const GitHubOAuth = () => {
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
@@ -28,8 +29,10 @@ const GitHubOAuth = () => {
 | 
			
		||||
      } else {
 | 
			
		||||
        userDispatch({ type: 'login', payload: data });
 | 
			
		||||
        localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
        setUserData(data);
 | 
			
		||||
        updateAPI();
 | 
			
		||||
        showSuccess('登录成功!');
 | 
			
		||||
        navigate('/');
 | 
			
		||||
        navigate('/token');
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
 
 | 
			
		||||
@@ -39,10 +39,10 @@ let buttons = [
 | 
			
		||||
    // icon: <IconHomeStroked />,
 | 
			
		||||
  },
 | 
			
		||||
  // {
 | 
			
		||||
  //   text: '模型价格',
 | 
			
		||||
  //   itemKey: 'pricing',
 | 
			
		||||
  //   to: '/pricing',
 | 
			
		||||
  //   icon: <IconNoteMoneyStroked />,
 | 
			
		||||
  //   text: 'Playground',
 | 
			
		||||
  //   itemKey: 'playground',
 | 
			
		||||
  //   to: '/playground',
 | 
			
		||||
  //   // icon: <IconNoteMoneyStroked />,
 | 
			
		||||
  // },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,8 @@ const LoginForm = () => {
 | 
			
		||||
    if (success) {
 | 
			
		||||
      userDispatch({ type: 'login', payload: data });
 | 
			
		||||
      localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
      setUserData(data);
 | 
			
		||||
      updateAPI();
 | 
			
		||||
      navigate('/');
 | 
			
		||||
      showSuccess('登录成功!');
 | 
			
		||||
      setShowWeChatLoginModal(false);
 | 
			
		||||
@@ -151,6 +153,8 @@ const LoginForm = () => {
 | 
			
		||||
      userDispatch({ type: 'login', payload: data });
 | 
			
		||||
      localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
      showSuccess('登录成功!');
 | 
			
		||||
      setUserData(data);
 | 
			
		||||
      updateAPI();
 | 
			
		||||
      navigate('/');
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.
 | 
			
		||||
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
 | 
			
		||||
 | 
			
		||||
import { API, showError, showSuccess } from '../helpers';
 | 
			
		||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
 | 
			
		||||
 | 
			
		||||
const OperationSetting = () => {
 | 
			
		||||
  let [inputs, setInputs] = useState({
 | 
			
		||||
@@ -50,6 +51,7 @@ const OperationSetting = () => {
 | 
			
		||||
    DataExportInterval: 5,
 | 
			
		||||
    DefaultCollapseSidebar: false, // 默认折叠侧边栏
 | 
			
		||||
    RetryTimes: 0,
 | 
			
		||||
    Chats: '[]',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
@@ -131,6 +133,10 @@ const OperationSetting = () => {
 | 
			
		||||
        <Card style={{ marginTop: '10px' }}>
 | 
			
		||||
          <SettingsCreditLimit options={inputs} refresh={onRefresh} />
 | 
			
		||||
        </Card>
 | 
			
		||||
        {/* 聊天设置 */}
 | 
			
		||||
        <Card style={{ marginTop: '10px' }}>
 | 
			
		||||
          <SettingsChats options={inputs} refresh={onRefresh} />
 | 
			
		||||
        </Card>
 | 
			
		||||
        {/* 倍率设置 */}
 | 
			
		||||
        <Card style={{ marginTop: '10px' }}>
 | 
			
		||||
          <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,
 | 
			
		||||
  IconChecklistStroked,
 | 
			
		||||
  IconComment,
 | 
			
		||||
  IconCommentStroked,
 | 
			
		||||
  IconCreditCard,
 | 
			
		||||
  IconGift,
 | 
			
		||||
  IconHelpCircle,
 | 
			
		||||
@@ -42,11 +43,9 @@ const SiderBar = () => {
 | 
			
		||||
  const defaultIsCollapsed =
 | 
			
		||||
    isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
 | 
			
		||||
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
  const [selectedKeys, setSelectedKeys] = useState(['home']);
 | 
			
		||||
  const systemName = getSystemName();
 | 
			
		||||
  const logo = getLogo();
 | 
			
		||||
  const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
 | 
			
		||||
  const [chatItems, setChatItems] = useState([]);
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const setTheme = useSetTheme();
 | 
			
		||||
 | 
			
		||||
@@ -65,16 +64,17 @@ const SiderBar = () => {
 | 
			
		||||
    detail: '/detail',
 | 
			
		||||
    pricing: '/pricing',
 | 
			
		||||
    task: '/task',
 | 
			
		||||
    playground: '/playground',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const headerButtons = useMemo(
 | 
			
		||||
    () => [
 | 
			
		||||
      // {
 | 
			
		||||
      //   text: '首页',
 | 
			
		||||
      //   itemKey: 'home',
 | 
			
		||||
      //   to: '/',
 | 
			
		||||
      //   icon: <IconHome />,
 | 
			
		||||
      // },
 | 
			
		||||
      {
 | 
			
		||||
        text: 'Playground',
 | 
			
		||||
        itemKey: 'playground',
 | 
			
		||||
        to: '/playground',
 | 
			
		||||
        icon: <IconCommentStroked />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '模型价格',
 | 
			
		||||
        itemKey: 'pricing',
 | 
			
		||||
@@ -91,11 +91,12 @@ const SiderBar = () => {
 | 
			
		||||
      {
 | 
			
		||||
        text: '聊天',
 | 
			
		||||
        itemKey: 'chat',
 | 
			
		||||
        to: '/chat',
 | 
			
		||||
        // to: '/chat',
 | 
			
		||||
        items: chatItems,
 | 
			
		||||
        icon: <IconComment />,
 | 
			
		||||
        className: localStorage.getItem('chat_link')
 | 
			
		||||
          ? 'semi-navigation-item-normal'
 | 
			
		||||
          : 'tableHiddle',
 | 
			
		||||
        // className: localStorage.getItem('chat_link')
 | 
			
		||||
        //   ? 'semi-navigation-item-normal'
 | 
			
		||||
        //   : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '令牌',
 | 
			
		||||
@@ -177,6 +178,7 @@ const SiderBar = () => {
 | 
			
		||||
      localStorage.getItem('enable_drawing'),
 | 
			
		||||
      localStorage.getItem('enable_task'),
 | 
			
		||||
      localStorage.getItem('chat_link'),
 | 
			
		||||
      chatItems,
 | 
			
		||||
      isAdmin(),
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
@@ -207,6 +209,33 @@ const SiderBar = () => {
 | 
			
		||||
      localKey = 'home';
 | 
			
		||||
    }
 | 
			
		||||
    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 (
 | 
			
		||||
@@ -223,6 +252,27 @@ const SiderBar = () => {
 | 
			
		||||
        }}
 | 
			
		||||
        selectedKeys={selectedKeys}
 | 
			
		||||
        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 (
 | 
			
		||||
            <Link
 | 
			
		||||
              style={{ textDecoration: 'none' }}
 | 
			
		||||
@@ -236,15 +286,6 @@ const SiderBar = () => {
 | 
			
		||||
        onSelect={(key) => {
 | 
			
		||||
          setSelectedKeys([key.itemKey]);
 | 
			
		||||
        }}
 | 
			
		||||
        // header={{
 | 
			
		||||
        //   logo: (
 | 
			
		||||
        //     <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
        //   ),
 | 
			
		||||
        //   text: systemName,
 | 
			
		||||
        // }}
 | 
			
		||||
        // footer={{
 | 
			
		||||
        //   text: '© 2021 NekoAPI',
 | 
			
		||||
        // }}
 | 
			
		||||
        footer={
 | 
			
		||||
          <>
 | 
			
		||||
            {isMobile() && (
 | 
			
		||||
 
 | 
			
		||||
@@ -25,17 +25,6 @@ import {
 | 
			
		||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
 | 
			
		||||
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) {
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
@@ -88,28 +77,6 @@ function renderStatus(status, model_limits_enabled = false) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 = [
 | 
			
		||||
    {
 | 
			
		||||
      title: '名称',
 | 
			
		||||
@@ -177,149 +144,174 @@ const TokensTable = () => {
 | 
			
		||||
    {
 | 
			
		||||
      title: '',
 | 
			
		||||
      dataIndex: 'operate',
 | 
			
		||||
      render: (text, record, index) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Popover
 | 
			
		||||
            content={'sk-' + record.key}
 | 
			
		||||
            style={{ padding: 20 }}
 | 
			
		||||
            position='top'
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
			
		||||
              查看
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popover>
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='secondary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={async (text) => {
 | 
			
		||||
              await copyText('sk-' + record.key);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            复制
 | 
			
		||||
          </Button>
 | 
			
		||||
          <SplitButtonGroup
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            aria-label='项目操作按钮组'
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                onOpenLink('next', record.key);
 | 
			
		||||
              }}
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        let chats = localStorage.getItem('chats');
 | 
			
		||||
        let chatsArray = [];
 | 
			
		||||
        let chatLink = localStorage.getItem('chat_link');
 | 
			
		||||
        let mjLink = localStorage.getItem('chat_link2');
 | 
			
		||||
        let shouldUseCustom = true;
 | 
			
		||||
        if (chatLink) {
 | 
			
		||||
          shouldUseCustom = false;
 | 
			
		||||
          chatLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
 | 
			
		||||
          chatsArray.push({
 | 
			
		||||
            node: 'item',
 | 
			
		||||
            key: 'default',
 | 
			
		||||
            name: 'ChatGPT Next Web',
 | 
			
		||||
            onClick: () => {
 | 
			
		||||
              onOpenLink('default', chatLink, record);
 | 
			
		||||
            },
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        if (mjLink) {
 | 
			
		||||
          shouldUseCustom = false;
 | 
			
		||||
          mjLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
 | 
			
		||||
          chatsArray.push({
 | 
			
		||||
            node: 'item',
 | 
			
		||||
            key: 'mj',
 | 
			
		||||
            name: 'ChatGPT Next Midjourney',
 | 
			
		||||
            onClick: () => {
 | 
			
		||||
              onOpenLink('mj', mjLink, record);
 | 
			
		||||
            },
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        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>
 | 
			
		||||
            <Dropdown
 | 
			
		||||
              trigger='click'
 | 
			
		||||
              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 theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
			
		||||
                查看
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Popover>
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='secondary'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageToken(record.id, 'enable', record);
 | 
			
		||||
              onClick={async (text) => {
 | 
			
		||||
                await copyText('sk-' + record.key);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              启用
 | 
			
		||||
              复制
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='tertiary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setEditingToken(record);
 | 
			
		||||
              setShowEdit(true);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      ),
 | 
			
		||||
            <SplitButtonGroup
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              aria-label='项目操作按钮组'
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='light'
 | 
			
		||||
                style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  if (chatsArray.length === 0) {
 | 
			
		||||
                    showError('请联系管理员配置聊天链接');
 | 
			
		||||
                  } else {
 | 
			
		||||
                    onOpenLink(
 | 
			
		||||
                      '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 [searchToken, setSearchToken] = useState('');
 | 
			
		||||
  const [searching, setSearching] = useState(false);
 | 
			
		||||
  const [showTopUpModal, setShowTopUpModal] = useState(false);
 | 
			
		||||
  const [targetTokenIdx, setTargetTokenIdx] = useState(0);
 | 
			
		||||
  const [chats, setChats] = useState([]);
 | 
			
		||||
  const [editingToken, setEditingToken] = useState({
 | 
			
		||||
    id: undefined,
 | 
			
		||||
  });
 | 
			
		||||
@@ -379,16 +370,6 @@ const TokensTable = () => {
 | 
			
		||||
    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 () => {
 | 
			
		||||
    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 serverAddress = '';
 | 
			
		||||
    if (status) {
 | 
			
		||||
@@ -416,36 +398,39 @@ const TokensTable = () => {
 | 
			
		||||
      serverAddress = window.location.origin;
 | 
			
		||||
    }
 | 
			
		||||
    let encodedServerAddress = encodeURIComponent(serverAddress);
 | 
			
		||||
    const chatLink = localStorage.getItem('chat_link');
 | 
			
		||||
    const mjLink = localStorage.getItem('chat_link2');
 | 
			
		||||
    let defaultUrl;
 | 
			
		||||
 | 
			
		||||
    if (chatLink) {
 | 
			
		||||
      defaultUrl =
 | 
			
		||||
        chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    }
 | 
			
		||||
    let url;
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'ama':
 | 
			
		||||
        url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'opencat':
 | 
			
		||||
        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'lobe':
 | 
			
		||||
        url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'next-mj':
 | 
			
		||||
        url =
 | 
			
		||||
          mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        if (!chatLink) {
 | 
			
		||||
          showError('管理员未设置聊天链接');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        url = defaultUrl;
 | 
			
		||||
    }
 | 
			
		||||
    url = url.replace('{address}', encodedServerAddress);
 | 
			
		||||
    url = url.replace('{key}', 'sk-' + record.key);
 | 
			
		||||
    // console.log(url);
 | 
			
		||||
    // const chatLink = localStorage.getItem('chat_link');
 | 
			
		||||
    // const mjLink = localStorage.getItem('chat_link2');
 | 
			
		||||
    // let defaultUrl;
 | 
			
		||||
    //
 | 
			
		||||
    // if (chatLink) {
 | 
			
		||||
    //   defaultUrl =
 | 
			
		||||
    //     chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    // }
 | 
			
		||||
    // let url;
 | 
			
		||||
    // switch (type) {
 | 
			
		||||
    //   case 'ama':
 | 
			
		||||
    //     url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
 | 
			
		||||
    //     break;
 | 
			
		||||
    //   case 'opencat':
 | 
			
		||||
    //     url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
			
		||||
    //     break;
 | 
			
		||||
    //   case 'lobe':
 | 
			
		||||
    //     url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
 | 
			
		||||
    //     break;
 | 
			
		||||
    //   case 'next-mj':
 | 
			
		||||
    //     url =
 | 
			
		||||
    //       mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    //     break;
 | 
			
		||||
    //   default:
 | 
			
		||||
    //     if (!chatLink) {
 | 
			
		||||
    //       showError('管理员未设置聊天链接');
 | 
			
		||||
    //       return;
 | 
			
		||||
    //     }
 | 
			
		||||
    //     url = defaultUrl;
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    window.open(url, '_blank');
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,7 @@ const UsersTable = () => {
 | 
			
		||||
                title='确定?'
 | 
			
		||||
                okType={'warning'}
 | 
			
		||||
                onConfirm={() => {
 | 
			
		||||
                  manageUser(record.username, 'promote', record);
 | 
			
		||||
                  manageUser(record.id, 'promote', record);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button theme='light' type='warning' style={{ marginRight: 1 }}>
 | 
			
		||||
@@ -162,7 +162,7 @@ const UsersTable = () => {
 | 
			
		||||
                title='确定?'
 | 
			
		||||
                okType={'warning'}
 | 
			
		||||
                onConfirm={() => {
 | 
			
		||||
                  manageUser(record.username, 'demote', record);
 | 
			
		||||
                  manageUser(record.id, 'demote', record);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button
 | 
			
		||||
@@ -179,7 +179,7 @@ const UsersTable = () => {
 | 
			
		||||
                  type='warning'
 | 
			
		||||
                  style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    manageUser(record.username, 'disable', record);
 | 
			
		||||
                    manageUser(record.id, 'disable', record);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  禁用
 | 
			
		||||
@@ -190,7 +190,7 @@ const UsersTable = () => {
 | 
			
		||||
                  type='secondary'
 | 
			
		||||
                  style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    manageUser(record.username, 'enable', record);
 | 
			
		||||
                    manageUser(record.id, 'enable', record);
 | 
			
		||||
                  }}
 | 
			
		||||
                  disabled={record.status === 3}
 | 
			
		||||
                >
 | 
			
		||||
@@ -214,7 +214,7 @@ const UsersTable = () => {
 | 
			
		||||
                okType={'danger'}
 | 
			
		||||
                position={'left'}
 | 
			
		||||
                onConfirm={() => {
 | 
			
		||||
                  manageUser(record.username, 'delete', record).then(() => {
 | 
			
		||||
                  manageUser(record.id, 'delete', record).then(() => {
 | 
			
		||||
                    removeRecord(record.id);
 | 
			
		||||
                  });
 | 
			
		||||
                }}
 | 
			
		||||
@@ -303,9 +303,9 @@ const UsersTable = () => {
 | 
			
		||||
    fetchGroups().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const manageUser = async (username, action, record) => {
 | 
			
		||||
  const manageUser = async (userId, action, record) => {
 | 
			
		||||
    const res = await API.post('/api/user/manage', {
 | 
			
		||||
      username,
 | 
			
		||||
      id: userId,
 | 
			
		||||
      action,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { API, showError } from '../helpers';
 | 
			
		||||
 | 
			
		||||
async function fetchTokenKeys() {
 | 
			
		||||
  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;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      const activeTokens = data.filter((token) => token.status === 1);
 | 
			
		||||
@@ -38,9 +38,9 @@ function getServerAddress() {
 | 
			
		||||
  return serverAddress;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTokenKeys() {
 | 
			
		||||
export function useTokenKeys(id) {
 | 
			
		||||
  const [keys, setKeys] = useState([]);
 | 
			
		||||
  const [chatLink, setChatLink] = useState('');
 | 
			
		||||
  // const [chatLink, setChatLink] = useState('');
 | 
			
		||||
  const [serverAddress, setServerAddress] = useState('');
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(true);
 | 
			
		||||
 | 
			
		||||
@@ -55,9 +55,7 @@ export function useTokenKeys() {
 | 
			
		||||
      }
 | 
			
		||||
      setKeys(fetchedKeys);
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
 | 
			
		||||
      const link = localStorage.getItem('chat_link');
 | 
			
		||||
      setChatLink(link);
 | 
			
		||||
      // setChatLink(link);
 | 
			
		||||
 | 
			
		||||
      const address = getServerAddress();
 | 
			
		||||
      setServerAddress(address);
 | 
			
		||||
@@ -66,5 +64,5 @@ export function useTokenKeys() {
 | 
			
		||||
    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_task', data.enable_task);
 | 
			
		||||
  localStorage.setItem('enable_data_export', data.enable_data_export);
 | 
			
		||||
  localStorage.setItem('chats', JSON.stringify(data.chats));
 | 
			
		||||
  localStorage.setItem(
 | 
			
		||||
    'data_export_default_time',
 | 
			
		||||
    data.data_export_default_time,
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,17 @@ body {
 | 
			
		||||
  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 {
 | 
			
		||||
  display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,32 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
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 { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
 | 
			
		||||
  const { id } = useParams();
 | 
			
		||||
  const { keys, serverAddress, isLoading } = useTokenKeys(id);
 | 
			
		||||
 | 
			
		||||
  const comLink = (key) => {
 | 
			
		||||
    if (!chatLink || !serverAddress || !key) return '';
 | 
			
		||||
    return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
 | 
			
		||||
    // console.log('chatLink:', chatLink);
 | 
			
		||||
    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]) : '';
 | 
			
		||||
@@ -22,11 +41,7 @@ const ChatPage = () => {
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
          <h3 style={{ color: 'red' }}>
 | 
			
		||||
            当前没有可用的已启用令牌,请确认是否有令牌处于启用状态!
 | 
			
		||||
            <br />
 | 
			
		||||
            正在跳转......
 | 
			
		||||
          </h3>
 | 
			
		||||
          <Banner description={'正在跳转......'} type={'warning'} />
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </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 { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
 | 
			
		||||
import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  compareObjects,
 | 
			
		||||
  API,
 | 
			
		||||
@@ -74,6 +74,10 @@ export default function GeneralSettings(props) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Spin spinning={loading}>
 | 
			
		||||
        <Banner
 | 
			
		||||
          type='warning'
 | 
			
		||||
          description={'聊天链接功能已经弃用,请使用下方聊天设置功能'}
 | 
			
		||||
        />
 | 
			
		||||
        <Form
 | 
			
		||||
          values={inputs}
 | 
			
		||||
          getFormApi={(formAPI) => (refForm.current = formAPI)}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ const EditToken = (props) => {
 | 
			
		||||
    group,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
  // const [visible, setVisible] = useState(false);
 | 
			
		||||
  const [models, setModels] = useState({});
 | 
			
		||||
  const [models, setModels] = useState([]);
 | 
			
		||||
  const [groups, setGroups] = useState([]);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const handleInputChange = (name, value) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,10 @@ export default defineConfig({
 | 
			
		||||
        target: 'http://localhost:3000',
 | 
			
		||||
        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