mirror of
				https://github.com/linux-do/new-api.git
				synced 2025-11-04 13:23:42 +08:00 
			
		
		
		
	merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
		
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
								
							@@ -1,6 +1,13 @@
 | 
			
		||||
<div align="center">
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
# New API
 | 
			
		||||
 | 
			
		||||
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
 | 
			
		||||
 | 
			
		||||
@@ -59,6 +66,7 @@
 | 
			
		||||
您可以在渠道中添加自定义模型gpt-4-gizmo-*或g-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
 | 
			
		||||
 | 
			
		||||
## 比原版One API多出的配置
 | 
			
		||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
 | 
			
		||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
 | 
			
		||||
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
 | 
			
		||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。
 | 
			
		||||
@@ -115,22 +123,13 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 | 
			
		||||
## Suno接口设置文档
 | 
			
		||||
[对接文档](Suno.md)
 | 
			
		||||
 | 
			
		||||
## 交流群
 | 
			
		||||
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="300">
 | 
			
		||||
 | 
			
		||||
## 界面截图
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

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

 | 
			
		||||

 | 
			
		||||

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

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

 | 
			
		||||
 | 
			
		||||
## 相关项目
 | 
			
		||||
 
 | 
			
		||||
@@ -376,6 +376,9 @@ func GetCompletionRatio(name string) float64 {
 | 
			
		||||
		return 3
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "gemini-") {
 | 
			
		||||
		if strings.Contains(name, "flash") {
 | 
			
		||||
			return 4
 | 
			
		||||
		}
 | 
			
		||||
		return 3
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "command") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								common/user_groups.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								common/user_groups.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var UserUsableGroups = map[string]string{
 | 
			
		||||
	"default": "默认分组",
 | 
			
		||||
	"vip":     "vip分组",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UserUsableGroups2JSONString() string {
 | 
			
		||||
	jsonBytes, err := json.Marshal(UserUsableGroups)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		SysError("error marshalling user groups: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return string(jsonBytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
 | 
			
		||||
	UserUsableGroups = make(map[string]string)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
 | 
			
		||||
}
 | 
			
		||||
@@ -133,6 +133,11 @@ func IntMax(a int, b int) int {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsIP(s string) bool {
 | 
			
		||||
	ip := net.ParseIP(s)
 | 
			
		||||
	return ip != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUUID() string {
 | 
			
		||||
	code := uuid.New().String()
 | 
			
		||||
	code = strings.Replace(code, "-", "", -1)
 | 
			
		||||
 
 | 
			
		||||
@@ -46,3 +46,6 @@ func InitEnv() {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 是否生成初始令牌,默认关闭。
 | 
			
		||||
var GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,3 +17,18 @@ func GetGroups(c *gin.Context) {
 | 
			
		||||
		"data":    groupNames,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUserGroups(c *gin.Context) {
 | 
			
		||||
	usableGroups := make(map[string]string)
 | 
			
		||||
	for groupName, _ := range common.GroupRatio {
 | 
			
		||||
		// UserUsableGroups contains the groups that the user can use
 | 
			
		||||
		if _, ok := common.UserUsableGroups[groupName]; ok {
 | 
			
		||||
			usableGroups[groupName] = common.UserUsableGroups[groupName]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "",
 | 
			
		||||
		"data":    usableGroups,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -137,31 +137,63 @@ func init() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ListModels(c *gin.Context) {
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	user, err := model.GetUserById(userId, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	models := model.GetGroupModels(user.Group)
 | 
			
		||||
	userOpenAiModels := make([]dto.OpenAIModels, 0)
 | 
			
		||||
	permission := getPermission()
 | 
			
		||||
	for _, s := range models {
 | 
			
		||||
		if _, ok := openAIModelsMap[s]; ok {
 | 
			
		||||
			userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
 | 
			
		||||
 | 
			
		||||
	modelLimitEnable := c.GetBool("token_model_limit_enabled")
 | 
			
		||||
	if modelLimitEnable {
 | 
			
		||||
		s, ok := c.Get("token_model_limit")
 | 
			
		||||
		var tokenModelLimit map[string]bool
 | 
			
		||||
		if ok {
 | 
			
		||||
			tokenModelLimit = s.(map[string]bool)
 | 
			
		||||
		} else {
 | 
			
		||||
			userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
 | 
			
		||||
				Id:         s,
 | 
			
		||||
				Object:     "model",
 | 
			
		||||
				Created:    1626777600,
 | 
			
		||||
				OwnedBy:    "custom",
 | 
			
		||||
				Permission: permission,
 | 
			
		||||
				Root:       s,
 | 
			
		||||
				Parent:     nil,
 | 
			
		||||
			tokenModelLimit = map[string]bool{}
 | 
			
		||||
		}
 | 
			
		||||
		for allowModel, _ := range tokenModelLimit {
 | 
			
		||||
			if _, ok := openAIModelsMap[allowModel]; ok {
 | 
			
		||||
				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
 | 
			
		||||
			} else {
 | 
			
		||||
				userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
 | 
			
		||||
					Id:         allowModel,
 | 
			
		||||
					Object:     "model",
 | 
			
		||||
					Created:    1626777600,
 | 
			
		||||
					OwnedBy:    "custom",
 | 
			
		||||
					Permission: permission,
 | 
			
		||||
					Root:       allowModel,
 | 
			
		||||
					Parent:     nil,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		userId := c.GetInt("id")
 | 
			
		||||
		userGroup, err := model.GetUserGroup(userId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "get user group failed",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		group := userGroup
 | 
			
		||||
		tokenGroup := c.GetString("token_group")
 | 
			
		||||
		if tokenGroup != "" {
 | 
			
		||||
			group = tokenGroup
 | 
			
		||||
		}
 | 
			
		||||
		models := model.GetGroupModels(group)
 | 
			
		||||
		for _, s := range models {
 | 
			
		||||
			if _, ok := openAIModelsMap[s]; ok {
 | 
			
		||||
				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
 | 
			
		||||
			} else {
 | 
			
		||||
				userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
 | 
			
		||||
					Id:         s,
 | 
			
		||||
					Object:     "model",
 | 
			
		||||
					Created:    1626777600,
 | 
			
		||||
					OwnedBy:    "custom",
 | 
			
		||||
					Permission: permission,
 | 
			
		||||
					Root:       s,
 | 
			
		||||
					Parent:     nil,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(200, gin.H{
 | 
			
		||||
 
 | 
			
		||||
@@ -7,18 +7,11 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetPricing(c *gin.Context) {
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	// if no login, get default group ratio
 | 
			
		||||
	groupRatio := common.GetGroupRatio("default")
 | 
			
		||||
	group, err := model.CacheGetUserGroup(userId)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		groupRatio = common.GetGroupRatio(group)
 | 
			
		||||
	}
 | 
			
		||||
	pricing := model.GetPricing(group)
 | 
			
		||||
	pricing := model.GetPricing()
 | 
			
		||||
	c.JSON(200, gin.H{
 | 
			
		||||
		"success":     true,
 | 
			
		||||
		"data":        pricing,
 | 
			
		||||
		"group_ratio": groupRatio,
 | 
			
		||||
		"group_ratio": common.GroupRatio,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -134,6 +134,8 @@ func AddToken(c *gin.Context) {
 | 
			
		||||
		UnlimitedQuota:     token.UnlimitedQuota,
 | 
			
		||||
		ModelLimitsEnabled: token.ModelLimitsEnabled,
 | 
			
		||||
		ModelLimits:        token.ModelLimits,
 | 
			
		||||
		AllowIps:           token.AllowIps,
 | 
			
		||||
		Group:              token.Group,
 | 
			
		||||
	}
 | 
			
		||||
	err = cleanToken.Insert()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -221,6 +223,8 @@ func UpdateToken(c *gin.Context) {
 | 
			
		||||
		cleanToken.UnlimitedQuota = token.UnlimitedQuota
 | 
			
		||||
		cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
 | 
			
		||||
		cleanToken.ModelLimits = token.ModelLimits
 | 
			
		||||
		cleanToken.AllowIps = token.AllowIps
 | 
			
		||||
		cleanToken.Group = token.Group
 | 
			
		||||
	}
 | 
			
		||||
	err = cleanToken.Update()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/constant"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LoginRequest struct {
 | 
			
		||||
@@ -187,6 +188,39 @@ func Register(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 获取插入后的用户ID
 | 
			
		||||
	var insertedUser model.User
 | 
			
		||||
	if err := model.DB.Where("username = ?", cleanUser.Username).First(&insertedUser).Error; err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "用户注册失败或用户ID获取失败",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 生成默认令牌
 | 
			
		||||
	if constant.GenerateDefaultToken {
 | 
			
		||||
		// 生成默认令牌
 | 
			
		||||
		token := model.Token{
 | 
			
		||||
			UserId:             insertedUser.Id, // 使用插入后的用户ID
 | 
			
		||||
			Name:               cleanUser.Username + "的初始令牌",
 | 
			
		||||
			Key:                common.GenerateKey(),
 | 
			
		||||
			CreatedTime:        common.GetTimestamp(),
 | 
			
		||||
			AccessedTime:       common.GetTimestamp(),
 | 
			
		||||
			ExpiredTime:        -1,     // 永不过期
 | 
			
		||||
			RemainQuota:        500000, // 示例额度
 | 
			
		||||
			UnlimitedQuota:     true,
 | 
			
		||||
			ModelLimitsEnabled: false,
 | 
			
		||||
		}
 | 
			
		||||
		if err := token.Insert(); err != nil {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "创建默认令牌失败",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "",
 | 
			
		||||
 
 | 
			
		||||
@@ -194,6 +194,8 @@ func TokenAuth() func(c *gin.Context) {
 | 
			
		||||
		} else {
 | 
			
		||||
			c.Set("token_model_limit_enabled", false)
 | 
			
		||||
		}
 | 
			
		||||
		c.Set("allow_ips", token.GetIpLimitsMap())
 | 
			
		||||
		c.Set("token_group", token.Group)
 | 
			
		||||
		if len(parts) > 1 {
 | 
			
		||||
			if model.IsAdmin(token.UserId) {
 | 
			
		||||
				c.Set("specific_channel_id", parts[1])
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,14 @@ type ModelRequest struct {
 | 
			
		||||
 | 
			
		||||
func Distribute() func(c *gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		allowIpsMap := c.GetStringMap("allow_ips")
 | 
			
		||||
		if len(allowIpsMap) != 0 {
 | 
			
		||||
			clientIp := c.ClientIP()
 | 
			
		||||
			if _, ok := allowIpsMap[clientIp]; !ok {
 | 
			
		||||
				abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		userId := c.GetInt("id")
 | 
			
		||||
		var channel *model.Channel
 | 
			
		||||
		channelId, ok := c.Get("specific_channel_id")
 | 
			
		||||
@@ -31,6 +39,20 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		userGroup, _ := model.CacheGetUserGroup(userId)
 | 
			
		||||
		tokenGroup := c.GetString("token_group")
 | 
			
		||||
		if tokenGroup != "" {
 | 
			
		||||
			// check common.UserUsableGroups[userGroup]
 | 
			
		||||
			if _, ok := common.UserUsableGroups[tokenGroup]; !ok {
 | 
			
		||||
				abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			// check group in common.GroupRatio
 | 
			
		||||
			if _, ok := common.GroupRatio[tokenGroup]; !ok {
 | 
			
		||||
				abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			userGroup = tokenGroup
 | 
			
		||||
		}
 | 
			
		||||
		c.Set("group", userGroup)
 | 
			
		||||
		if ok {
 | 
			
		||||
			id, err := strconv.Atoi(channelId.(string))
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,12 @@ func GetEnabledModels() []string {
 | 
			
		||||
	return models
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllEnableAbilities() []Ability {
 | 
			
		||||
	var abilities []Ability
 | 
			
		||||
	DB.Find(&abilities, "enabled = ?", true)
 | 
			
		||||
	return abilities
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPriority(group string, model string, retry int) (int, error) {
 | 
			
		||||
	groupCol := "`group`"
 | 
			
		||||
	trueVal := "1"
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,7 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
 | 
			
		||||
	common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
 | 
			
		||||
	common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
 | 
			
		||||
	common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
 | 
			
		||||
	common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
 | 
			
		||||
	common.OptionMap["TopUpLink"] = common.TopUpLink
 | 
			
		||||
	common.OptionMap["ChatLink"] = common.ChatLink
 | 
			
		||||
@@ -315,6 +316,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		err = common.UpdateModelRatioByJSONString(value)
 | 
			
		||||
	case "GroupRatio":
 | 
			
		||||
		err = common.UpdateGroupRatioByJSONString(value)
 | 
			
		||||
	case "UserUsableGroups":
 | 
			
		||||
		err = common.UpdateUserUsableGroupsByJSONString(value)
 | 
			
		||||
	case "CompletionRatio":
 | 
			
		||||
		err = common.UpdateCompletionRatioByJSONString(value)
 | 
			
		||||
	case "ModelPrice":
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,13 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Pricing struct {
 | 
			
		||||
	Available       bool     `json:"available"`
 | 
			
		||||
	ModelName       string   `json:"model_name"`
 | 
			
		||||
	QuotaType       int      `json:"quota_type"`
 | 
			
		||||
	ModelRatio      float64  `json:"model_ratio"`
 | 
			
		||||
	ModelPrice      float64  `json:"model_price"`
 | 
			
		||||
	OwnerBy         string   `json:"owner_by"`
 | 
			
		||||
	CompletionRatio float64  `json:"completion_ratio"`
 | 
			
		||||
	EnableGroup     []string `json:"enable_group,omitempty"`
 | 
			
		||||
	EnableGroup     []string `json:"enable_groups,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -23,40 +22,47 @@ var (
 | 
			
		||||
	updatePricingLock  sync.Mutex
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetPricing(group string) []Pricing {
 | 
			
		||||
func GetPricing() []Pricing {
 | 
			
		||||
	updatePricingLock.Lock()
 | 
			
		||||
	defer updatePricingLock.Unlock()
 | 
			
		||||
 | 
			
		||||
	if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
 | 
			
		||||
		updatePricing()
 | 
			
		||||
	}
 | 
			
		||||
	if group != "" {
 | 
			
		||||
		userPricingMap := make([]Pricing, 0)
 | 
			
		||||
		models := GetGroupModels(group)
 | 
			
		||||
		for _, pricing := range pricingMap {
 | 
			
		||||
			if !common.StringsContains(models, pricing.ModelName) {
 | 
			
		||||
				pricing.Available = false
 | 
			
		||||
			}
 | 
			
		||||
			userPricingMap = append(userPricingMap, pricing)
 | 
			
		||||
		}
 | 
			
		||||
		return userPricingMap
 | 
			
		||||
	}
 | 
			
		||||
	//if group != "" {
 | 
			
		||||
	//	userPricingMap := make([]Pricing, 0)
 | 
			
		||||
	//	models := GetGroupModels(group)
 | 
			
		||||
	//	for _, pricing := range pricingMap {
 | 
			
		||||
	//		if !common.StringsContains(models, pricing.ModelName) {
 | 
			
		||||
	//			pricing.Available = false
 | 
			
		||||
	//		}
 | 
			
		||||
	//		userPricingMap = append(userPricingMap, pricing)
 | 
			
		||||
	//	}
 | 
			
		||||
	//	return userPricingMap
 | 
			
		||||
	//}
 | 
			
		||||
	return pricingMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updatePricing() {
 | 
			
		||||
	//modelRatios := common.GetModelRatios()
 | 
			
		||||
	enabledModels := GetEnabledModels()
 | 
			
		||||
	allModels := make(map[string]int)
 | 
			
		||||
	for i, model := range enabledModels {
 | 
			
		||||
		allModels[model] = i
 | 
			
		||||
	enableAbilities := GetAllEnableAbilities()
 | 
			
		||||
	modelGroupsMap := make(map[string][]string)
 | 
			
		||||
	for _, ability := range enableAbilities {
 | 
			
		||||
		groups := modelGroupsMap[ability.Model]
 | 
			
		||||
		if groups == nil {
 | 
			
		||||
			groups = make([]string, 0)
 | 
			
		||||
		}
 | 
			
		||||
		if !common.StringsContains(groups, ability.Group) {
 | 
			
		||||
			groups = append(groups, ability.Group)
 | 
			
		||||
		}
 | 
			
		||||
		modelGroupsMap[ability.Model] = groups
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pricingMap = make([]Pricing, 0)
 | 
			
		||||
	for model, _ := range allModels {
 | 
			
		||||
	for model, groups := range modelGroupsMap {
 | 
			
		||||
		pricing := Pricing{
 | 
			
		||||
			Available: true,
 | 
			
		||||
			ModelName: model,
 | 
			
		||||
			ModelName:   model,
 | 
			
		||||
			EnableGroup: groups,
 | 
			
		||||
		}
 | 
			
		||||
		modelPrice, findPrice := common.GetModelPrice(model, false)
 | 
			
		||||
		if findPrice {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,10 +22,34 @@ type Token struct {
 | 
			
		||||
	UnlimitedQuota     bool           `json:"unlimited_quota" gorm:"default:false"`
 | 
			
		||||
	ModelLimitsEnabled bool           `json:"model_limits_enabled" gorm:"default:false"`
 | 
			
		||||
	ModelLimits        string         `json:"model_limits" gorm:"type:varchar(1024);default:''"`
 | 
			
		||||
	AllowIps           *string        `json:"allow_ips" gorm:"default:''"`
 | 
			
		||||
	UsedQuota          int            `json:"used_quota" gorm:"default:0"` // used quota
 | 
			
		||||
	Group              string         `json:"group" gorm:"default:''"`
 | 
			
		||||
	DeletedAt          gorm.DeletedAt `gorm:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (token *Token) GetIpLimitsMap() map[string]any {
 | 
			
		||||
	// delete empty spaces
 | 
			
		||||
	//split with \n
 | 
			
		||||
	ipLimitsMap := make(map[string]any)
 | 
			
		||||
	if token.AllowIps == nil {
 | 
			
		||||
		return ipLimitsMap
 | 
			
		||||
	}
 | 
			
		||||
	cleanIps := strings.ReplaceAll(*token.AllowIps, " ", "")
 | 
			
		||||
	if cleanIps == "" {
 | 
			
		||||
		return ipLimitsMap
 | 
			
		||||
	}
 | 
			
		||||
	ips := strings.Split(cleanIps, "\n")
 | 
			
		||||
	for _, ip := range ips {
 | 
			
		||||
		ip = strings.TrimSpace(ip)
 | 
			
		||||
		ip = strings.ReplaceAll(ip, ",", "")
 | 
			
		||||
		if common.IsIP(ip) {
 | 
			
		||||
			ipLimitsMap[ip] = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ipLimitsMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
 | 
			
		||||
	var tokens []*Token
 | 
			
		||||
	var err error
 | 
			
		||||
@@ -129,7 +153,8 @@ func (token *Token) Insert() error {
 | 
			
		||||
// Update Make sure your token's fields is completed, because this will update non-zero values
 | 
			
		||||
func (token *Token) Update() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits").Updates(token).Error
 | 
			
		||||
	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota",
 | 
			
		||||
		"model_limits_enabled", "model_limits", "allow_ips", "group").Updates(token).Error
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
			userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
 | 
			
		||||
			//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
 | 
			
		||||
			userRoute.GET("/logout", controller.Logout)
 | 
			
		||||
			userRoute.GET("/groups", controller.GetUserGroups)
 | 
			
		||||
 | 
			
		||||
			selfRoute := userRoute.Group("/")
 | 
			
		||||
			selfRoute.Use(middleware.UserAuth())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										405
									
								
								web/src/App.js
									
									
									
									
									
								
							
							
						
						
									
										405
									
								
								web/src/App.js
									
									
									
									
									
								
							@@ -21,11 +21,11 @@ import Redemption from './pages/Redemption';
 | 
			
		||||
import TopUp from './pages/TopUp';
 | 
			
		||||
import Log from './pages/Log';
 | 
			
		||||
import Chat from './pages/Chat';
 | 
			
		||||
import Chat2Link from './pages/Chat2Link';
 | 
			
		||||
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 Detail from './pages/Detail';
 | 
			
		||||
 | 
			
		||||
const Home = lazy(() => import('./pages/Home'));
 | 
			
		||||
const Detail = lazy(() => import('./pages/Detail'));
 | 
			
		||||
@@ -59,215 +59,224 @@ function App() {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
        <Routes>
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/'
 | 
			
		||||
            element={
 | 
			
		||||
    <>
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <Home />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/channel'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Channel />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/channel/edit/:id'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <EditChannel />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/channel/add'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <EditChannel />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/token'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Token />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/redemption'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Redemption />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/user'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <User />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/user/edit/:id'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <EditUser />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/user/edit'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <EditUser />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/user/reset'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <PasswordResetConfirm />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/login'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <LoginForm />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/register'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <RegisterForm />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/reset'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <PasswordResetForm />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/oauth/github'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <GitHubOAuth />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/oauth/linuxdo'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <LinuxDoOAuth />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/setting'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <Home />
 | 
			
		||||
                <Setting />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/channel'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Channel />
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/channel/edit/:id'
 | 
			
		||||
            element={
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/topup'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditChannel />
 | 
			
		||||
                <TopUp />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/channel/add'
 | 
			
		||||
            element={
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/log'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Log />
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/detail'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditChannel />
 | 
			
		||||
                <Detail />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/token'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Token />
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/redemption'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Redemption />
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/user'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <User />
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/user/edit/:id'
 | 
			
		||||
            element={
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/midjourney'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditUser />
 | 
			
		||||
                <Midjourney />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/user/edit'
 | 
			
		||||
            element={
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/task'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditUser />
 | 
			
		||||
                <Task />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/user/reset'
 | 
			
		||||
            element={
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/pricing'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <Pricing />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/about'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <About />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/chat'
 | 
			
		||||
          element={
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <Chat />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        {/* 方便使用chat2link直接跳转聊天... */}
 | 
			
		||||
        <Route
 | 
			
		||||
          path='/chat2link'
 | 
			
		||||
          element={
 | 
			
		||||
            <PrivateRoute>
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <PasswordResetConfirm />
 | 
			
		||||
                <Chat2Link />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/login'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <LoginForm />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/register'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <RegisterForm />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/reset'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <PasswordResetForm />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/oauth/github'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <GitHubOAuth />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/oauth/linuxdo'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <LinuxDoOAuth />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/setting'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Setting />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/topup'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <TopUp />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/log'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Log />
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/detail'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Detail />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/midjourney'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Midjourney />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/task'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Task />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/pricing'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <Pricing />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/about'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <About />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path='/chat'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <Chat />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route path='*' element={<NotFound />} />
 | 
			
		||||
        </Routes>
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
            </PrivateRoute>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <Route path='*' element={<NotFound />} />
 | 
			
		||||
      </Routes>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { getFooterHTML, getSystemName } from '../helpers';
 | 
			
		||||
import { Layout, Tooltip } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const Footer = () => {
 | 
			
		||||
const FooterBar = () => {
 | 
			
		||||
  const systemName = getSystemName();
 | 
			
		||||
  const [footer, setFooter] = useState(getFooterHTML());
 | 
			
		||||
  let remainCheckTimes = 5;
 | 
			
		||||
@@ -52,19 +52,17 @@ const Footer = () => {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Layout.Content style={{ textAlign: 'center' }}>
 | 
			
		||||
        {footer ? (
 | 
			
		||||
          <div
 | 
			
		||||
            className='custom-footer'
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: footer }}
 | 
			
		||||
          ></div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          defaultFooter
 | 
			
		||||
        )}
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
    <div style={{ textAlign: 'center' }}>
 | 
			
		||||
      {footer ? (
 | 
			
		||||
        <div
 | 
			
		||||
          className='custom-footer'
 | 
			
		||||
          dangerouslySetInnerHTML={{ __html: footer }}
 | 
			
		||||
        ></div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        defaultFooter
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Footer;
 | 
			
		||||
export default FooterBar;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { useSetTheme, useTheme } from '../context/Theme';
 | 
			
		||||
 | 
			
		||||
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
 | 
			
		||||
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
 | 
			
		||||
import '../index.css';
 | 
			
		||||
 | 
			
		||||
import fireworks from 'react-fireworks';
 | 
			
		||||
 | 
			
		||||
import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons';
 | 
			
		||||
import {
 | 
			
		||||
  IconHelpCircle,
 | 
			
		||||
  IconHome,
 | 
			
		||||
  IconHomeStroked,
 | 
			
		||||
  IconKey,
 | 
			
		||||
  IconNoteMoneyStroked,
 | 
			
		||||
  IconPriceTag,
 | 
			
		||||
  IconUser,
 | 
			
		||||
} from '@douyinfe/semi-icons';
 | 
			
		||||
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
 | 
			
		||||
import { stringToColor } from '../helpers/render';
 | 
			
		||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 | 
			
		||||
 | 
			
		||||
// HeaderBar Buttons
 | 
			
		||||
let headerButtons = [
 | 
			
		||||
@@ -22,6 +31,21 @@ let headerButtons = [
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
let buttons = [
 | 
			
		||||
  {
 | 
			
		||||
    text: '首页',
 | 
			
		||||
    itemKey: 'home',
 | 
			
		||||
    to: '/',
 | 
			
		||||
    // icon: <IconHomeStroked />,
 | 
			
		||||
  },
 | 
			
		||||
  // {
 | 
			
		||||
  //   text: '模型价格',
 | 
			
		||||
  //   itemKey: 'pricing',
 | 
			
		||||
  //   to: '/pricing',
 | 
			
		||||
  //   icon: <IconNoteMoneyStroked />,
 | 
			
		||||
  // },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('chat_link')) {
 | 
			
		||||
  headerButtons.splice(1, 0, {
 | 
			
		||||
    name: '聊天',
 | 
			
		||||
@@ -90,6 +114,7 @@ const HeaderBar = () => {
 | 
			
		||||
                about: '/about',
 | 
			
		||||
                login: '/login',
 | 
			
		||||
                register: '/register',
 | 
			
		||||
                home: '/',
 | 
			
		||||
              };
 | 
			
		||||
              return (
 | 
			
		||||
                <Link
 | 
			
		||||
@@ -103,6 +128,23 @@ const HeaderBar = () => {
 | 
			
		||||
            selectedKeys={[]}
 | 
			
		||||
            // items={headerButtons}
 | 
			
		||||
            onSelect={(key) => {}}
 | 
			
		||||
            header={
 | 
			
		||||
              isMobile()
 | 
			
		||||
                ? {
 | 
			
		||||
                    logo: (
 | 
			
		||||
                      <img
 | 
			
		||||
                        src={logo}
 | 
			
		||||
                        alt='logo'
 | 
			
		||||
                        style={{ marginRight: '0.75em' }}
 | 
			
		||||
                      />
 | 
			
		||||
                    ),
 | 
			
		||||
                  }
 | 
			
		||||
                : {
 | 
			
		||||
                    logo: <img src={logo} alt='logo' />,
 | 
			
		||||
                    text: systemName,
 | 
			
		||||
                  }
 | 
			
		||||
            }
 | 
			
		||||
            items={buttons}
 | 
			
		||||
            footer={
 | 
			
		||||
              <>
 | 
			
		||||
                {isNewYear && (
 | 
			
		||||
@@ -121,15 +163,19 @@ const HeaderBar = () => {
 | 
			
		||||
                  </Dropdown>
 | 
			
		||||
                )}
 | 
			
		||||
                <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checkedText='🌞'
 | 
			
		||||
                  size={'large'}
 | 
			
		||||
                  checked={theme === 'dark'}
 | 
			
		||||
                  uncheckedText='🌙'
 | 
			
		||||
                  onChange={(checked) => {
 | 
			
		||||
                    setTheme(checked);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                <>
 | 
			
		||||
                  {!isMobile() && (
 | 
			
		||||
                    <Switch
 | 
			
		||||
                      checkedText='🌞'
 | 
			
		||||
                      size={'large'}
 | 
			
		||||
                      checked={theme === 'dark'}
 | 
			
		||||
                      uncheckedText='🌙'
 | 
			
		||||
                      onChange={(checked) => {
 | 
			
		||||
                        setTheme(checked);
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                </>
 | 
			
		||||
                {userState.user ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Dropdown
 | 
			
		||||
@@ -155,7 +201,7 @@ const HeaderBar = () => {
 | 
			
		||||
                    <Nav.Item
 | 
			
		||||
                      itemKey={'login'}
 | 
			
		||||
                      text={'登录'}
 | 
			
		||||
                      icon={<IconKey />}
 | 
			
		||||
                      // icon={<IconKey />}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Nav.Item
 | 
			
		||||
                      itemKey={'register'}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
 | 
			
		||||
import { API, copy, showError, showSuccess } from '../helpers';
 | 
			
		||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Banner,
 | 
			
		||||
@@ -83,6 +83,7 @@ const ModelPricing = () => {
 | 
			
		||||
  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 | 
			
		||||
  const [modalImageUrl, setModalImageUrl] = useState('');
 | 
			
		||||
  const [isModalOpenurl, setIsModalOpenurl] = useState(false);
 | 
			
		||||
  const [selectedGroup, setSelectedGroup] = useState('default');
 | 
			
		||||
 | 
			
		||||
  const rowSelection = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
@@ -116,7 +117,8 @@ const ModelPricing = () => {
 | 
			
		||||
      title: '可用性',
 | 
			
		||||
      dataIndex: 'available',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return renderAvailable(text);
 | 
			
		||||
        // if record.enable_groups contains selectedGroup, then available is true
 | 
			
		||||
        return renderAvailable(record.enable_groups.includes(selectedGroup));
 | 
			
		||||
      },
 | 
			
		||||
      sorter: (a, b) => a.available - b.available,
 | 
			
		||||
    },
 | 
			
		||||
@@ -162,6 +164,44 @@ const ModelPricing = () => {
 | 
			
		||||
      },
 | 
			
		||||
      sorter: (a, b) => a.quota_type - b.quota_type,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '可用分组',
 | 
			
		||||
      dataIndex: 'enable_groups',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        // enable_groups is a string array
 | 
			
		||||
        return (
 | 
			
		||||
          <Space>
 | 
			
		||||
            {text.map((group) => {
 | 
			
		||||
              if (group === selectedGroup) {
 | 
			
		||||
                return (
 | 
			
		||||
                  <Tag color='blue' size='large' prefixIcon={<IconVerify />}>
 | 
			
		||||
                    {group}
 | 
			
		||||
                  </Tag>
 | 
			
		||||
                );
 | 
			
		||||
              } else {
 | 
			
		||||
                return (
 | 
			
		||||
                  <Tag
 | 
			
		||||
                    color='blue'
 | 
			
		||||
                    size='large'
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setSelectedGroup(group);
 | 
			
		||||
                      showInfo(
 | 
			
		||||
                        '当前查看的分组为:' +
 | 
			
		||||
                          group +
 | 
			
		||||
                          ',倍率为:' +
 | 
			
		||||
                          groupRatio[group],
 | 
			
		||||
                      );
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {group}
 | 
			
		||||
                  </Tag>
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            })}
 | 
			
		||||
          </Space>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: () => (
 | 
			
		||||
        <span style={{ display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
@@ -203,6 +243,8 @@ const ModelPricing = () => {
 | 
			
		||||
            <Text>
 | 
			
		||||
              补全:{record.quota_type === 0 ? completionRatio : '无'}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <br />
 | 
			
		||||
            <Text>分组:{groupRatio[selectedGroup]}</Text>
 | 
			
		||||
          </>
 | 
			
		||||
        );
 | 
			
		||||
        return <div>{content}</div>;
 | 
			
		||||
@@ -215,12 +257,13 @@ const ModelPricing = () => {
 | 
			
		||||
        let content = text;
 | 
			
		||||
        if (record.quota_type === 0) {
 | 
			
		||||
          // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
 | 
			
		||||
          let inputRatioPrice = record.model_ratio * 2 * record.group_ratio;
 | 
			
		||||
          let inputRatioPrice =
 | 
			
		||||
            record.model_ratio * 2 * groupRatio[selectedGroup];
 | 
			
		||||
          let completionRatioPrice =
 | 
			
		||||
            record.model_ratio *
 | 
			
		||||
            record.completion_ratio *
 | 
			
		||||
            2 *
 | 
			
		||||
            record.group_ratio;
 | 
			
		||||
            groupRatio[selectedGroup];
 | 
			
		||||
          content = (
 | 
			
		||||
            <>
 | 
			
		||||
              <Text>提示 ${inputRatioPrice} / 1M tokens</Text>
 | 
			
		||||
@@ -229,7 +272,7 @@ const ModelPricing = () => {
 | 
			
		||||
            </>
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          let price = parseFloat(text) * record.group_ratio;
 | 
			
		||||
          let price = parseFloat(text) * groupRatio[selectedGroup];
 | 
			
		||||
          content = <>模型价格:${price}</>;
 | 
			
		||||
        }
 | 
			
		||||
        return <div>{content}</div>;
 | 
			
		||||
@@ -240,12 +283,12 @@ const ModelPricing = () => {
 | 
			
		||||
  const [models, setModels] = useState([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  const [groupRatio, setGroupRatio] = useState(1);
 | 
			
		||||
  const [groupRatio, setGroupRatio] = useState({});
 | 
			
		||||
 | 
			
		||||
  const setModelsFormat = (models, groupRatio) => {
 | 
			
		||||
    for (let i = 0; i < models.length; i++) {
 | 
			
		||||
      models[i].key = models[i].model_name;
 | 
			
		||||
      models[i].group_ratio = groupRatio;
 | 
			
		||||
      models[i].group_ratio = groupRatio[models[i].model_name];
 | 
			
		||||
    }
 | 
			
		||||
    // sort by quota_type
 | 
			
		||||
    models.sort((a, b) => {
 | 
			
		||||
@@ -278,6 +321,7 @@ const ModelPricing = () => {
 | 
			
		||||
    const { success, message, data, group_ratio } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setGroupRatio(group_ratio);
 | 
			
		||||
      setSelectedGroup(userState.user ? userState.user.group : 'default');
 | 
			
		||||
      setModelsFormat(data, group_ratio);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -310,14 +354,14 @@ const ModelPricing = () => {
 | 
			
		||||
            type='success'
 | 
			
		||||
            fullMode={false}
 | 
			
		||||
            closeIcon='null'
 | 
			
		||||
            description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
 | 
			
		||||
            description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Banner
 | 
			
		||||
            type='warning'
 | 
			
		||||
            fullMode={false}
 | 
			
		||||
            closeIcon='null'
 | 
			
		||||
            description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
 | 
			
		||||
            description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <br />
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ const OperationSetting = () => {
 | 
			
		||||
    CompletionRatio: '',
 | 
			
		||||
    ModelPrice: '',
 | 
			
		||||
    GroupRatio: '',
 | 
			
		||||
    UserUsableGroups: '',
 | 
			
		||||
    TopUpLink: '',
 | 
			
		||||
    ChatLink: '',
 | 
			
		||||
    ChatLink2: '', // 添加的新状态变量
 | 
			
		||||
@@ -62,6 +63,7 @@ const OperationSetting = () => {
 | 
			
		||||
        if (
 | 
			
		||||
          item.key === 'ModelRatio' ||
 | 
			
		||||
          item.key === 'GroupRatio' ||
 | 
			
		||||
          item.key === 'UserUsableGroups' ||
 | 
			
		||||
          item.key === 'CompletionRatio' ||
 | 
			
		||||
          item.key === 'ModelPrice'
 | 
			
		||||
        ) {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import {
 | 
			
		||||
  IconComment,
 | 
			
		||||
  IconCreditCard,
 | 
			
		||||
  IconGift,
 | 
			
		||||
  IconHelpCircle,
 | 
			
		||||
  IconHistogram,
 | 
			
		||||
  IconHome,
 | 
			
		||||
  IconImage,
 | 
			
		||||
@@ -28,8 +29,10 @@ import {
 | 
			
		||||
  IconSetting,
 | 
			
		||||
  IconUser,
 | 
			
		||||
} from '@douyinfe/semi-icons';
 | 
			
		||||
import { Layout, Nav } from '@douyinfe/semi-ui';
 | 
			
		||||
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
 | 
			
		||||
import { setStatusData } from '../helpers/data.js';
 | 
			
		||||
import { stringToColor } from '../helpers/render.js';
 | 
			
		||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
 | 
			
		||||
 | 
			
		||||
// HeaderBar Buttons
 | 
			
		||||
 | 
			
		||||
@@ -44,6 +47,8 @@ const SiderBar = () => {
 | 
			
		||||
  const systemName = getSystemName();
 | 
			
		||||
  const logo = getLogo();
 | 
			
		||||
  const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const setTheme = useSetTheme();
 | 
			
		||||
 | 
			
		||||
  const routerMap = {
 | 
			
		||||
    home: '/',
 | 
			
		||||
@@ -64,11 +69,17 @@ const SiderBar = () => {
 | 
			
		||||
 | 
			
		||||
  const headerButtons = useMemo(
 | 
			
		||||
    () => [
 | 
			
		||||
      // {
 | 
			
		||||
      //   text: '首页',
 | 
			
		||||
      //   itemKey: 'home',
 | 
			
		||||
      //   to: '/',
 | 
			
		||||
      //   icon: <IconHome />,
 | 
			
		||||
      // },
 | 
			
		||||
      {
 | 
			
		||||
        text: '首页',
 | 
			
		||||
        itemKey: 'home',
 | 
			
		||||
        to: '/',
 | 
			
		||||
        icon: <IconHome />,
 | 
			
		||||
        text: '模型价格',
 | 
			
		||||
        itemKey: 'pricing',
 | 
			
		||||
        to: '/pricing',
 | 
			
		||||
        icon: <IconPriceTag />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '渠道',
 | 
			
		||||
@@ -105,12 +116,6 @@ const SiderBar = () => {
 | 
			
		||||
        to: '/topup',
 | 
			
		||||
        icon: <IconCreditCard />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '模型价格',
 | 
			
		||||
        itemKey: 'pricing',
 | 
			
		||||
        to: '/pricing',
 | 
			
		||||
        icon: <IconPriceTag />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '用户管理',
 | 
			
		||||
        itemKey: 'user',
 | 
			
		||||
@@ -206,48 +211,58 @@ const SiderBar = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <div style={{ height: '100%' }}>
 | 
			
		||||
          <Nav
 | 
			
		||||
            // bodyStyle={{ maxWidth: 200 }}
 | 
			
		||||
            style={{ maxWidth: 200 }}
 | 
			
		||||
            defaultIsCollapsed={
 | 
			
		||||
              isMobile() ||
 | 
			
		||||
              localStorage.getItem('default_collapse_sidebar') === 'true'
 | 
			
		||||
            }
 | 
			
		||||
            isCollapsed={isCollapsed}
 | 
			
		||||
            onCollapseChange={(collapsed) => {
 | 
			
		||||
              setIsCollapsed(collapsed);
 | 
			
		||||
            }}
 | 
			
		||||
            selectedKeys={selectedKeys}
 | 
			
		||||
            renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <Link
 | 
			
		||||
                  style={{ textDecoration: 'none' }}
 | 
			
		||||
                  to={routerMap[props.itemKey]}
 | 
			
		||||
                >
 | 
			
		||||
                  {itemElement}
 | 
			
		||||
                </Link>
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
            items={headerButtons}
 | 
			
		||||
            onSelect={(key) => {
 | 
			
		||||
              setSelectedKeys([key.itemKey]);
 | 
			
		||||
            }}
 | 
			
		||||
            header={{
 | 
			
		||||
              logo: (
 | 
			
		||||
                <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
              ),
 | 
			
		||||
              text: systemName,
 | 
			
		||||
            }}
 | 
			
		||||
            // footer={{
 | 
			
		||||
            //   text: '© 2021 NekoAPI',
 | 
			
		||||
            // }}
 | 
			
		||||
          >
 | 
			
		||||
            <Nav.Footer collapseButton={true}></Nav.Footer>
 | 
			
		||||
          </Nav>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Layout>
 | 
			
		||||
      <Nav
 | 
			
		||||
        style={{ maxWidth: 220, height: '100%' }}
 | 
			
		||||
        defaultIsCollapsed={
 | 
			
		||||
          isMobile() ||
 | 
			
		||||
          localStorage.getItem('default_collapse_sidebar') === 'true'
 | 
			
		||||
        }
 | 
			
		||||
        isCollapsed={isCollapsed}
 | 
			
		||||
        onCollapseChange={(collapsed) => {
 | 
			
		||||
          setIsCollapsed(collapsed);
 | 
			
		||||
        }}
 | 
			
		||||
        selectedKeys={selectedKeys}
 | 
			
		||||
        renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <Link
 | 
			
		||||
              style={{ textDecoration: 'none' }}
 | 
			
		||||
              to={routerMap[props.itemKey]}
 | 
			
		||||
            >
 | 
			
		||||
              {itemElement}
 | 
			
		||||
            </Link>
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
        items={headerButtons}
 | 
			
		||||
        onSelect={(key) => {
 | 
			
		||||
          setSelectedKeys([key.itemKey]);
 | 
			
		||||
        }}
 | 
			
		||||
        // header={{
 | 
			
		||||
        //   logo: (
 | 
			
		||||
        //     <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
        //   ),
 | 
			
		||||
        //   text: systemName,
 | 
			
		||||
        // }}
 | 
			
		||||
        // footer={{
 | 
			
		||||
        //   text: '© 2021 NekoAPI',
 | 
			
		||||
        // }}
 | 
			
		||||
        footer={
 | 
			
		||||
          <>
 | 
			
		||||
            {isMobile() && (
 | 
			
		||||
              <Switch
 | 
			
		||||
                checkedText='🌞'
 | 
			
		||||
                size={'small'}
 | 
			
		||||
                checked={theme === 'dark'}
 | 
			
		||||
                uncheckedText='🌙'
 | 
			
		||||
                onChange={(checked) => {
 | 
			
		||||
                  setTheme(checked);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <Nav.Footer collapseButton={true}></Nav.Footer>
 | 
			
		||||
      </Nav>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import {
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
import { renderGroup, renderQuota } from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
@@ -16,6 +16,7 @@ import {
 | 
			
		||||
  Modal,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Popover,
 | 
			
		||||
  Space,
 | 
			
		||||
  SplitButtonGroup,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
@@ -119,7 +120,14 @@ const TokensTable = () => {
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      key: 'status',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return <div>{renderStatus(text, record.model_limits_enabled)}</div>;
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space>
 | 
			
		||||
              {renderStatus(text, record.model_limits_enabled)}
 | 
			
		||||
              {renderGroup(record.group)}
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								web/src/components/fetchTokenKeys.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								web/src/components/fetchTokenKeys.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
// src/hooks/useTokenKeys.js
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { API, showError } from '../helpers';
 | 
			
		||||
 | 
			
		||||
async function fetchTokenKeys() {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await API.get('/api/token/?p=0&size=999');
 | 
			
		||||
    const { success, data } = response.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      const activeTokens = data.filter((token) => token.status === 1);
 | 
			
		||||
      return activeTokens.map((token) => token.key);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error('Failed to fetch token keys');
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error fetching token keys:', error);
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getServerAddress() {
 | 
			
		||||
  let status = localStorage.getItem('status');
 | 
			
		||||
  let serverAddress = '';
 | 
			
		||||
 | 
			
		||||
  if (status) {
 | 
			
		||||
    try {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      serverAddress = status.server_address || '';
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to parse status from localStorage:', error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!serverAddress) {
 | 
			
		||||
    serverAddress = window.location.origin;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return serverAddress;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTokenKeys() {
 | 
			
		||||
  const [keys, setKeys] = useState([]);
 | 
			
		||||
  const [chatLink, setChatLink] = useState('');
 | 
			
		||||
  const [serverAddress, setServerAddress] = useState('');
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(true);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const loadAllData = async () => {
 | 
			
		||||
      const fetchedKeys = await fetchTokenKeys();
 | 
			
		||||
      if (fetchedKeys.length === 0) {
 | 
			
		||||
        showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          window.location.href = '/token';
 | 
			
		||||
        }, 1500); // 延迟 1.5 秒后跳转
 | 
			
		||||
      }
 | 
			
		||||
      setKeys(fetchedKeys);
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
 | 
			
		||||
      const link = localStorage.getItem('chat_link');
 | 
			
		||||
      setChatLink(link);
 | 
			
		||||
 | 
			
		||||
      const address = getServerAddress();
 | 
			
		||||
      setServerAddress(address);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    loadAllData();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return { keys, chatLink, serverAddress, isLoading };
 | 
			
		||||
}
 | 
			
		||||
@@ -15,8 +15,8 @@ export function renderText(text, limit) {
 | 
			
		||||
export function renderGroup(group) {
 | 
			
		||||
  if (group === '') {
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag size='large' key='default'>
 | 
			
		||||
        unknown
 | 
			
		||||
      <Tag size='large' key='default' color='orange'>
 | 
			
		||||
        用户分组
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,12 @@ body {
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
  color: var(--semi-color-text-0) !important;
 | 
			
		||||
  background-color: var(--semi-color-bg-0) !important;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 767px) {
 | 
			
		||||
@@ -50,9 +51,9 @@ body {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-layout {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
/*.semi-layout {*/
 | 
			
		||||
/*  height: 100%;*/
 | 
			
		||||
/*}*/
 | 
			
		||||
 | 
			
		||||
.tableShow {
 | 
			
		||||
  display: revert;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom';
 | 
			
		||||
import App from './App';
 | 
			
		||||
import HeaderBar from './components/HeaderBar';
 | 
			
		||||
import Footer from './components/Footer';
 | 
			
		||||
import 'semantic-ui-offline/semantic.min.css';
 | 
			
		||||
import './index.css';
 | 
			
		||||
import { UserProvider } from './context/User';
 | 
			
		||||
@@ -13,35 +12,40 @@ import { StatusProvider } from './context/Status';
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
import SiderBar from './components/SiderBar';
 | 
			
		||||
import { ThemeProvider } from './context/Theme';
 | 
			
		||||
import FooterBar from './components/Footer';
 | 
			
		||||
 | 
			
		||||
// initialization
 | 
			
		||||
 | 
			
		||||
const root = ReactDOM.createRoot(document.getElementById('root'));
 | 
			
		||||
const { Sider, Content, Header } = Layout;
 | 
			
		||||
const { Sider, Content, Header, Footer } = Layout;
 | 
			
		||||
root.render(
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <StatusProvider>
 | 
			
		||||
      <UserProvider>
 | 
			
		||||
        <BrowserRouter>
 | 
			
		||||
          <ThemeProvider>
 | 
			
		||||
            <Layout>
 | 
			
		||||
              <Sider>
 | 
			
		||||
                <SiderBar />
 | 
			
		||||
              </Sider>
 | 
			
		||||
              <Layout>
 | 
			
		||||
                <Header>
 | 
			
		||||
                  <HeaderBar />
 | 
			
		||||
                </Header>
 | 
			
		||||
                <Content
 | 
			
		||||
                  style={{
 | 
			
		||||
                    padding: '24px',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <App />
 | 
			
		||||
                </Content>
 | 
			
		||||
                <Layout.Footer>
 | 
			
		||||
                  <Footer></Footer>
 | 
			
		||||
                </Layout.Footer>
 | 
			
		||||
            <Layout
 | 
			
		||||
              style={{
 | 
			
		||||
                height: '100vh',
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                flexDirection: 'column',
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Header>
 | 
			
		||||
                <HeaderBar />
 | 
			
		||||
              </Header>
 | 
			
		||||
              <Layout style={{ flex: 1, overflow: 'hidden' }}>
 | 
			
		||||
                <Sider>
 | 
			
		||||
                  <SiderBar />
 | 
			
		||||
                </Sider>
 | 
			
		||||
                <Layout>
 | 
			
		||||
                  <Content style={{ overflowY: 'auto', padding: '24px' }}>
 | 
			
		||||
                    <App />
 | 
			
		||||
                  </Content>
 | 
			
		||||
                  <Layout.Footer>
 | 
			
		||||
                    <FooterBar></FooterBar>
 | 
			
		||||
                  </Layout.Footer>
 | 
			
		||||
                </Layout>
 | 
			
		||||
              </Layout>
 | 
			
		||||
              <ToastContainer />
 | 
			
		||||
            </Layout>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,36 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const Chat = () => {
 | 
			
		||||
  const chatLink = localStorage.getItem('chat_link');
 | 
			
		||||
const ChatPage = () => {
 | 
			
		||||
  const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
  const comLink = (key) => {
 | 
			
		||||
    if (!chatLink || !serverAddress || !key) return '';
 | 
			
		||||
    return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
 | 
			
		||||
 | 
			
		||||
  return !isLoading && iframeSrc ? (
 | 
			
		||||
    <iframe
 | 
			
		||||
      src={chatLink}
 | 
			
		||||
      src={iframeSrc}
 | 
			
		||||
      style={{ width: '100%', height: '85vh', border: 'none' }}
 | 
			
		||||
      title='Token Frame'
 | 
			
		||||
    />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
          <h3 style={{ color: 'red' }}>
 | 
			
		||||
            当前没有可用的已启用令牌,请确认是否有令牌处于启用状态!
 | 
			
		||||
            <br />
 | 
			
		||||
            正在跳转......
 | 
			
		||||
          </h3>
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Chat;
 | 
			
		||||
export default ChatPage;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								web/src/pages/Chat2Link/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								web/src/pages/Chat2Link/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
 | 
			
		||||
 | 
			
		||||
const chat2page = () => {
 | 
			
		||||
  const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
 | 
			
		||||
 | 
			
		||||
  const comLink = (key) => {
 | 
			
		||||
    if (!chatLink || !serverAddress || !key) return '';
 | 
			
		||||
    return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (keys.length > 0) {
 | 
			
		||||
    const redirectLink = comLink(keys[0]);
 | 
			
		||||
    if (redirectLink) {
 | 
			
		||||
      window.location.href = redirectLink;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <h3>正在加载,请稍候...</h3>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default chat2page;
 | 
			
		||||
@@ -25,6 +25,7 @@ export default function SettingsMagnification(props) {
 | 
			
		||||
    ModelRatio: '',
 | 
			
		||||
    CompletionRatio: '',
 | 
			
		||||
    GroupRatio: '',
 | 
			
		||||
    UserUsableGroups: '',
 | 
			
		||||
  });
 | 
			
		||||
  const refForm = useRef();
 | 
			
		||||
  const [inputsRow, setInputsRow] = useState(inputs);
 | 
			
		||||
@@ -224,6 +225,33 @@ export default function SettingsMagnification(props) {
 | 
			
		||||
              />
 | 
			
		||||
            </Col>
 | 
			
		||||
          </Row>
 | 
			
		||||
          <Row gutter={16}>
 | 
			
		||||
            <Col span={16}>
 | 
			
		||||
              <Form.TextArea
 | 
			
		||||
                label={'用户可选分组'}
 | 
			
		||||
                extraText={''}
 | 
			
		||||
                placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
 | 
			
		||||
                field={'UserUsableGroups'}
 | 
			
		||||
                autosize={{ minRows: 6, maxRows: 12 }}
 | 
			
		||||
                trigger='blur'
 | 
			
		||||
                stopValidateWithError
 | 
			
		||||
                rules={[
 | 
			
		||||
                  {
 | 
			
		||||
                    validator: (rule, value) => {
 | 
			
		||||
                      return verifyJSON(value);
 | 
			
		||||
                    },
 | 
			
		||||
                    message: '不是合法的 JSON 字符串',
 | 
			
		||||
                  },
 | 
			
		||||
                ]}
 | 
			
		||||
                onChange={(value) =>
 | 
			
		||||
                  setInputs({
 | 
			
		||||
                    ...inputs,
 | 
			
		||||
                    UserUsableGroups: value,
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            </Col>
 | 
			
		||||
          </Row>
 | 
			
		||||
        </Form.Section>
 | 
			
		||||
      </Form>
 | 
			
		||||
      <Space>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import {
 | 
			
		||||
  SideSheet,
 | 
			
		||||
  Space,
 | 
			
		||||
  Spin,
 | 
			
		||||
  TextArea,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
@@ -34,6 +35,8 @@ const EditToken = (props) => {
 | 
			
		||||
    unlimited_quota: false,
 | 
			
		||||
    model_limits_enabled: false,
 | 
			
		||||
    model_limits: [],
 | 
			
		||||
    allow_ips: '',
 | 
			
		||||
    group: '',
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const {
 | 
			
		||||
@@ -43,9 +46,12 @@ const EditToken = (props) => {
 | 
			
		||||
    unlimited_quota,
 | 
			
		||||
    model_limits_enabled,
 | 
			
		||||
    model_limits,
 | 
			
		||||
    allow_ips,
 | 
			
		||||
    group,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
  // const [visible, setVisible] = useState(false);
 | 
			
		||||
  const [models, setModels] = useState({});
 | 
			
		||||
  const [groups, setGroups] = useState([]);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const handleInputChange = (name, value) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
@@ -86,6 +92,22 @@ const EditToken = (props) => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
      }));
 | 
			
		||||
      setGroups(localGroupOptions);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadToken = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    let res = await API.get(`/api/token/${props.editingToken.id}`);
 | 
			
		||||
@@ -118,6 +140,7 @@ const EditToken = (props) => {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    loadModels();
 | 
			
		||||
    loadGroups();
 | 
			
		||||
  }, [isEdit]);
 | 
			
		||||
 | 
			
		||||
  // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
 | 
			
		||||
@@ -374,6 +397,19 @@ const EditToken = (props) => {
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <div style={{ marginTop: 10 }}>
 | 
			
		||||
            <Typography.Text>IP白名单(请勿过度信任此功能)</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <TextArea
 | 
			
		||||
            label='IP白名单'
 | 
			
		||||
            name='allow_ips'
 | 
			
		||||
            placeholder={'允许的IP,一行一个'}
 | 
			
		||||
            onChange={(value) => {
 | 
			
		||||
              handleInputChange('allow_ips', value);
 | 
			
		||||
            }}
 | 
			
		||||
            value={inputs.allow_ips}
 | 
			
		||||
            style={{ fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 10, display: 'flex' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Checkbox
 | 
			
		||||
@@ -404,6 +440,31 @@ const EditToken = (props) => {
 | 
			
		||||
            optionList={models}
 | 
			
		||||
            disabled={!model_limits_enabled}
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 10 }}>
 | 
			
		||||
            <Typography.Text>令牌分组,默认为用户的分组</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          {groups.length > 0 ? (
 | 
			
		||||
            <Select
 | 
			
		||||
              style={{ marginTop: 8 }}
 | 
			
		||||
              placeholder={'令牌分组,默认为用户的分组'}
 | 
			
		||||
              name='gruop'
 | 
			
		||||
              required
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={(value) => {
 | 
			
		||||
                handleInputChange('group', value);
 | 
			
		||||
              }}
 | 
			
		||||
              value={inputs.group}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              optionList={groups}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Select
 | 
			
		||||
              style={{ marginTop: 8 }}
 | 
			
		||||
              placeholder={'管理员未设置用户可选分组'}
 | 
			
		||||
              name='gruop'
 | 
			
		||||
              disabled={true}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Spin>
 | 
			
		||||
      </SideSheet>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user