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

|
||||||
|
|
||||||
# New API
|
# 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]
|
> [!NOTE]
|
||||||
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
||||||
|
|
||||||
@@ -59,6 +66,7 @@
|
|||||||
您可以在渠道中添加自定义模型gpt-4-gizmo-*或g-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
|
您可以在渠道中添加自定义模型gpt-4-gizmo-*或g-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
|
||||||
|
|
||||||
## 比原版One API多出的配置
|
## 比原版One API多出的配置
|
||||||
|
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
|
||||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
|
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
|
||||||
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
||||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。
|
- `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接口设置文档
|
||||||
[对接文档](Suno.md)
|
[对接文档](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
|
return 3
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(name, "gemini-") {
|
if strings.HasPrefix(name, "gemini-") {
|
||||||
|
if strings.Contains(name, "flash") {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(name, "command") {
|
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 {
|
func GetUUID() string {
|
||||||
code := uuid.New().String()
|
code := uuid.New().String()
|
||||||
code = strings.Replace(code, "-", "", -1)
|
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,
|
"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) {
|
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)
|
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
||||||
permission := getPermission()
|
permission := getPermission()
|
||||||
for _, s := range models {
|
|
||||||
if _, ok := openAIModelsMap[s]; ok {
|
modelLimitEnable := c.GetBool("token_model_limit_enabled")
|
||||||
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
|
if modelLimitEnable {
|
||||||
|
s, ok := c.Get("token_model_limit")
|
||||||
|
var tokenModelLimit map[string]bool
|
||||||
|
if ok {
|
||||||
|
tokenModelLimit = s.(map[string]bool)
|
||||||
} else {
|
} else {
|
||||||
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
tokenModelLimit = map[string]bool{}
|
||||||
Id: s,
|
}
|
||||||
Object: "model",
|
for allowModel, _ := range tokenModelLimit {
|
||||||
Created: 1626777600,
|
if _, ok := openAIModelsMap[allowModel]; ok {
|
||||||
OwnedBy: "custom",
|
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
|
||||||
Permission: permission,
|
} else {
|
||||||
Root: s,
|
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
||||||
Parent: nil,
|
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{
|
c.JSON(200, gin.H{
|
||||||
|
|||||||
@@ -7,18 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetPricing(c *gin.Context) {
|
func GetPricing(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
pricing := model.GetPricing()
|
||||||
// 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)
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": pricing,
|
"data": pricing,
|
||||||
"group_ratio": groupRatio,
|
"group_ratio": common.GroupRatio,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ func AddToken(c *gin.Context) {
|
|||||||
UnlimitedQuota: token.UnlimitedQuota,
|
UnlimitedQuota: token.UnlimitedQuota,
|
||||||
ModelLimitsEnabled: token.ModelLimitsEnabled,
|
ModelLimitsEnabled: token.ModelLimitsEnabled,
|
||||||
ModelLimits: token.ModelLimits,
|
ModelLimits: token.ModelLimits,
|
||||||
|
AllowIps: token.AllowIps,
|
||||||
|
Group: token.Group,
|
||||||
}
|
}
|
||||||
err = cleanToken.Insert()
|
err = cleanToken.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,6 +223,8 @@ func UpdateToken(c *gin.Context) {
|
|||||||
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
||||||
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
|
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
|
||||||
cleanToken.ModelLimits = token.ModelLimits
|
cleanToken.ModelLimits = token.ModelLimits
|
||||||
|
cleanToken.AllowIps = token.AllowIps
|
||||||
|
cleanToken.Group = token.Group
|
||||||
}
|
}
|
||||||
err = cleanToken.Update()
|
err = cleanToken.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"one-api/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -187,6 +188,39 @@ func Register(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
c.Set("token_model_limit_enabled", false)
|
c.Set("token_model_limit_enabled", false)
|
||||||
}
|
}
|
||||||
|
c.Set("allow_ips", token.GetIpLimitsMap())
|
||||||
|
c.Set("token_group", token.Group)
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
if model.IsAdmin(token.UserId) {
|
if model.IsAdmin(token.UserId) {
|
||||||
c.Set("specific_channel_id", parts[1])
|
c.Set("specific_channel_id", parts[1])
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ type ModelRequest struct {
|
|||||||
|
|
||||||
func Distribute() func(c *gin.Context) {
|
func Distribute() func(c *gin.Context) {
|
||||||
return 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")
|
userId := c.GetInt("id")
|
||||||
var channel *model.Channel
|
var channel *model.Channel
|
||||||
channelId, ok := c.Get("specific_channel_id")
|
channelId, ok := c.Get("specific_channel_id")
|
||||||
@@ -31,6 +39,20 @@ func Distribute() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
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)
|
c.Set("group", userGroup)
|
||||||
if ok {
|
if ok {
|
||||||
id, err := strconv.Atoi(channelId.(string))
|
id, err := strconv.Atoi(channelId.(string))
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ func GetEnabledModels() []string {
|
|||||||
return models
|
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) {
|
func getPriority(group string, model string, retry int) (int, error) {
|
||||||
groupCol := "`group`"
|
groupCol := "`group`"
|
||||||
trueVal := "1"
|
trueVal := "1"
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||||
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
|
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
|
||||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||||
|
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
|
||||||
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
|
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMap["ChatLink"] = common.ChatLink
|
common.OptionMap["ChatLink"] = common.ChatLink
|
||||||
@@ -315,6 +316,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
err = common.UpdateModelRatioByJSONString(value)
|
err = common.UpdateModelRatioByJSONString(value)
|
||||||
case "GroupRatio":
|
case "GroupRatio":
|
||||||
err = common.UpdateGroupRatioByJSONString(value)
|
err = common.UpdateGroupRatioByJSONString(value)
|
||||||
|
case "UserUsableGroups":
|
||||||
|
err = common.UpdateUserUsableGroupsByJSONString(value)
|
||||||
case "CompletionRatio":
|
case "CompletionRatio":
|
||||||
err = common.UpdateCompletionRatioByJSONString(value)
|
err = common.UpdateCompletionRatioByJSONString(value)
|
||||||
case "ModelPrice":
|
case "ModelPrice":
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Pricing struct {
|
type Pricing struct {
|
||||||
Available bool `json:"available"`
|
|
||||||
ModelName string `json:"model_name"`
|
ModelName string `json:"model_name"`
|
||||||
QuotaType int `json:"quota_type"`
|
QuotaType int `json:"quota_type"`
|
||||||
ModelRatio float64 `json:"model_ratio"`
|
ModelRatio float64 `json:"model_ratio"`
|
||||||
ModelPrice float64 `json:"model_price"`
|
ModelPrice float64 `json:"model_price"`
|
||||||
OwnerBy string `json:"owner_by"`
|
OwnerBy string `json:"owner_by"`
|
||||||
CompletionRatio float64 `json:"completion_ratio"`
|
CompletionRatio float64 `json:"completion_ratio"`
|
||||||
EnableGroup []string `json:"enable_group,omitempty"`
|
EnableGroup []string `json:"enable_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,40 +22,47 @@ var (
|
|||||||
updatePricingLock sync.Mutex
|
updatePricingLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPricing(group string) []Pricing {
|
func GetPricing() []Pricing {
|
||||||
updatePricingLock.Lock()
|
updatePricingLock.Lock()
|
||||||
defer updatePricingLock.Unlock()
|
defer updatePricingLock.Unlock()
|
||||||
|
|
||||||
if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
|
if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
|
||||||
updatePricing()
|
updatePricing()
|
||||||
}
|
}
|
||||||
if group != "" {
|
//if group != "" {
|
||||||
userPricingMap := make([]Pricing, 0)
|
// userPricingMap := make([]Pricing, 0)
|
||||||
models := GetGroupModels(group)
|
// models := GetGroupModels(group)
|
||||||
for _, pricing := range pricingMap {
|
// for _, pricing := range pricingMap {
|
||||||
if !common.StringsContains(models, pricing.ModelName) {
|
// if !common.StringsContains(models, pricing.ModelName) {
|
||||||
pricing.Available = false
|
// pricing.Available = false
|
||||||
}
|
// }
|
||||||
userPricingMap = append(userPricingMap, pricing)
|
// userPricingMap = append(userPricingMap, pricing)
|
||||||
}
|
// }
|
||||||
return userPricingMap
|
// return userPricingMap
|
||||||
}
|
//}
|
||||||
return pricingMap
|
return pricingMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePricing() {
|
func updatePricing() {
|
||||||
//modelRatios := common.GetModelRatios()
|
//modelRatios := common.GetModelRatios()
|
||||||
enabledModels := GetEnabledModels()
|
enableAbilities := GetAllEnableAbilities()
|
||||||
allModels := make(map[string]int)
|
modelGroupsMap := make(map[string][]string)
|
||||||
for i, model := range enabledModels {
|
for _, ability := range enableAbilities {
|
||||||
allModels[model] = i
|
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)
|
pricingMap = make([]Pricing, 0)
|
||||||
for model, _ := range allModels {
|
for model, groups := range modelGroupsMap {
|
||||||
pricing := Pricing{
|
pricing := Pricing{
|
||||||
Available: true,
|
ModelName: model,
|
||||||
ModelName: model,
|
EnableGroup: groups,
|
||||||
}
|
}
|
||||||
modelPrice, findPrice := common.GetModelPrice(model, false)
|
modelPrice, findPrice := common.GetModelPrice(model, false)
|
||||||
if findPrice {
|
if findPrice {
|
||||||
|
|||||||
@@ -22,10 +22,34 @@ type Token struct {
|
|||||||
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
|
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
|
||||||
ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"`
|
ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"`
|
||||||
ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"`
|
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
|
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
|
||||||
|
Group string `json:"group" gorm:"default:''"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
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) {
|
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
|
||||||
var tokens []*Token
|
var tokens []*Token
|
||||||
var err error
|
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
|
// Update Make sure your token's fields is completed, because this will update non-zero values
|
||||||
func (token *Token) Update() error {
|
func (token *Token) Update() error {
|
||||||
var err 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
|
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
|
||||||
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
||||||
userRoute.GET("/logout", controller.Logout)
|
userRoute.GET("/logout", controller.Logout)
|
||||||
|
userRoute.GET("/groups", controller.GetUserGroups)
|
||||||
|
|
||||||
selfRoute := userRoute.Group("/")
|
selfRoute := userRoute.Group("/")
|
||||||
selfRoute.Use(middleware.UserAuth())
|
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 TopUp from './pages/TopUp';
|
||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
|
import Chat2Link from './pages/Chat2Link';
|
||||||
import { Layout } from '@douyinfe/semi-ui';
|
import { Layout } from '@douyinfe/semi-ui';
|
||||||
import Midjourney from './pages/Midjourney';
|
import Midjourney from './pages/Midjourney';
|
||||||
import Pricing from './pages/Pricing/index.js';
|
import Pricing from './pages/Pricing/index.js';
|
||||||
import Task from './pages/Task/index.js';
|
import Task from './pages/Task/index.js';
|
||||||
// import Detail from './pages/Detail';
|
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const Detail = lazy(() => import('./pages/Detail'));
|
const Detail = lazy(() => import('./pages/Detail'));
|
||||||
@@ -59,215 +59,224 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Layout.Content>
|
<Routes>
|
||||||
<Routes>
|
<Route
|
||||||
<Route
|
path='/'
|
||||||
path='/'
|
element={
|
||||||
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>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<Home />
|
<Setting />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/channel'
|
<Route
|
||||||
element={
|
path='/topup'
|
||||||
<PrivateRoute>
|
element={
|
||||||
<Channel />
|
<PrivateRoute>
|
||||||
</PrivateRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path='/channel/edit/:id'
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Loading></Loading>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<EditChannel />
|
<TopUp />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/channel/add'
|
<Route
|
||||||
element={
|
path='/log'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Log />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/detail'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Loading></Loading>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<EditChannel />
|
<Detail />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/token'
|
<Route
|
||||||
element={
|
path='/midjourney'
|
||||||
<PrivateRoute>
|
element={
|
||||||
<Token />
|
<PrivateRoute>
|
||||||
</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>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<EditUser />
|
<Midjourney />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/user/edit'
|
<Route
|
||||||
element={
|
path='/task'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Loading></Loading>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<EditUser />
|
<Task />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/user/reset'
|
<Route
|
||||||
element={
|
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>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<PasswordResetConfirm />
|
<Chat2Link />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
</PrivateRoute>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path='/login'
|
<Route path='*' element={<NotFound />} />
|
||||||
element={
|
</Routes>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { getFooterHTML, getSystemName } from '../helpers';
|
import { getFooterHTML, getSystemName } from '../helpers';
|
||||||
import { Layout, Tooltip } from '@douyinfe/semi-ui';
|
import { Layout, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
const Footer = () => {
|
const FooterBar = () => {
|
||||||
const systemName = getSystemName();
|
const systemName = getSystemName();
|
||||||
const [footer, setFooter] = useState(getFooterHTML());
|
const [footer, setFooter] = useState(getFooterHTML());
|
||||||
let remainCheckTimes = 5;
|
let remainCheckTimes = 5;
|
||||||
@@ -52,19 +52,17 @@ const Footer = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Layout.Content style={{ textAlign: 'center' }}>
|
{footer ? (
|
||||||
{footer ? (
|
<div
|
||||||
<div
|
className='custom-footer'
|
||||||
className='custom-footer'
|
dangerouslySetInnerHTML={{ __html: footer }}
|
||||||
dangerouslySetInnerHTML={{ __html: footer }}
|
></div>
|
||||||
></div>
|
) : (
|
||||||
) : (
|
defaultFooter
|
||||||
defaultFooter
|
)}
|
||||||
)}
|
</div>
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default FooterBar;
|
||||||
|
|||||||
@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { useSetTheme, useTheme } from '../context/Theme';
|
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 '../index.css';
|
||||||
|
|
||||||
import fireworks from 'react-fireworks';
|
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 { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||||
import { stringToColor } from '../helpers/render';
|
import { stringToColor } from '../helpers/render';
|
||||||
|
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||||
|
|
||||||
// HeaderBar Buttons
|
// HeaderBar Buttons
|
||||||
let headerButtons = [
|
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')) {
|
if (localStorage.getItem('chat_link')) {
|
||||||
headerButtons.splice(1, 0, {
|
headerButtons.splice(1, 0, {
|
||||||
name: '聊天',
|
name: '聊天',
|
||||||
@@ -90,6 +114,7 @@ const HeaderBar = () => {
|
|||||||
about: '/about',
|
about: '/about',
|
||||||
login: '/login',
|
login: '/login',
|
||||||
register: '/register',
|
register: '/register',
|
||||||
|
home: '/',
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -103,6 +128,23 @@ const HeaderBar = () => {
|
|||||||
selectedKeys={[]}
|
selectedKeys={[]}
|
||||||
// items={headerButtons}
|
// items={headerButtons}
|
||||||
onSelect={(key) => {}}
|
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={
|
footer={
|
||||||
<>
|
<>
|
||||||
{isNewYear && (
|
{isNewYear && (
|
||||||
@@ -121,15 +163,19 @@ const HeaderBar = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
||||||
<Switch
|
<>
|
||||||
checkedText='🌞'
|
{!isMobile() && (
|
||||||
size={'large'}
|
<Switch
|
||||||
checked={theme === 'dark'}
|
checkedText='🌞'
|
||||||
uncheckedText='🌙'
|
size={'large'}
|
||||||
onChange={(checked) => {
|
checked={theme === 'dark'}
|
||||||
setTheme(checked);
|
uncheckedText='🌙'
|
||||||
}}
|
onChange={(checked) => {
|
||||||
/>
|
setTheme(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
{userState.user ? (
|
{userState.user ? (
|
||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -155,7 +201,7 @@ const HeaderBar = () => {
|
|||||||
<Nav.Item
|
<Nav.Item
|
||||||
itemKey={'login'}
|
itemKey={'login'}
|
||||||
text={'登录'}
|
text={'登录'}
|
||||||
icon={<IconKey />}
|
// icon={<IconKey />}
|
||||||
/>
|
/>
|
||||||
<Nav.Item
|
<Nav.Item
|
||||||
itemKey={'register'}
|
itemKey={'register'}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
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 {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
@@ -83,6 +83,7 @@ const ModelPricing = () => {
|
|||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState('default');
|
||||||
|
|
||||||
const rowSelection = useMemo(
|
const rowSelection = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -116,7 +117,8 @@ const ModelPricing = () => {
|
|||||||
title: '可用性',
|
title: '可用性',
|
||||||
dataIndex: 'available',
|
dataIndex: 'available',
|
||||||
render: (text, record, index) => {
|
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,
|
sorter: (a, b) => a.available - b.available,
|
||||||
},
|
},
|
||||||
@@ -162,6 +164,44 @@ const ModelPricing = () => {
|
|||||||
},
|
},
|
||||||
sorter: (a, b) => a.quota_type - b.quota_type,
|
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: () => (
|
title: () => (
|
||||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -203,6 +243,8 @@ const ModelPricing = () => {
|
|||||||
<Text>
|
<Text>
|
||||||
补全:{record.quota_type === 0 ? completionRatio : '无'}
|
补全:{record.quota_type === 0 ? completionRatio : '无'}
|
||||||
</Text>
|
</Text>
|
||||||
|
<br />
|
||||||
|
<Text>分组:{groupRatio[selectedGroup]}</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return <div>{content}</div>;
|
return <div>{content}</div>;
|
||||||
@@ -215,12 +257,13 @@ const ModelPricing = () => {
|
|||||||
let content = text;
|
let content = text;
|
||||||
if (record.quota_type === 0) {
|
if (record.quota_type === 0) {
|
||||||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||||||
let inputRatioPrice = record.model_ratio * 2 * record.group_ratio;
|
let inputRatioPrice =
|
||||||
|
record.model_ratio * 2 * groupRatio[selectedGroup];
|
||||||
let completionRatioPrice =
|
let completionRatioPrice =
|
||||||
record.model_ratio *
|
record.model_ratio *
|
||||||
record.completion_ratio *
|
record.completion_ratio *
|
||||||
2 *
|
2 *
|
||||||
record.group_ratio;
|
groupRatio[selectedGroup];
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
|
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
|
||||||
@@ -229,7 +272,7 @@ const ModelPricing = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let price = parseFloat(text) * record.group_ratio;
|
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||||
content = <>模型价格:${price}</>;
|
content = <>模型价格:${price}</>;
|
||||||
}
|
}
|
||||||
return <div>{content}</div>;
|
return <div>{content}</div>;
|
||||||
@@ -240,12 +283,12 @@ const ModelPricing = () => {
|
|||||||
const [models, setModels] = useState([]);
|
const [models, setModels] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
const [groupRatio, setGroupRatio] = useState(1);
|
const [groupRatio, setGroupRatio] = useState({});
|
||||||
|
|
||||||
const setModelsFormat = (models, groupRatio) => {
|
const setModelsFormat = (models, groupRatio) => {
|
||||||
for (let i = 0; i < models.length; i++) {
|
for (let i = 0; i < models.length; i++) {
|
||||||
models[i].key = models[i].model_name;
|
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
|
// sort by quota_type
|
||||||
models.sort((a, b) => {
|
models.sort((a, b) => {
|
||||||
@@ -278,6 +321,7 @@ const ModelPricing = () => {
|
|||||||
const { success, message, data, group_ratio } = res.data;
|
const { success, message, data, group_ratio } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setGroupRatio(group_ratio);
|
setGroupRatio(group_ratio);
|
||||||
|
setSelectedGroup(userState.user ? userState.user.group : 'default');
|
||||||
setModelsFormat(data, group_ratio);
|
setModelsFormat(data, group_ratio);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
@@ -310,14 +354,14 @@ const ModelPricing = () => {
|
|||||||
type='success'
|
type='success'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
closeIcon='null'
|
closeIcon='null'
|
||||||
description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
|
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Banner
|
<Banner
|
||||||
type='warning'
|
type='warning'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
closeIcon='null'
|
closeIcon='null'
|
||||||
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
|
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const OperationSetting = () => {
|
|||||||
CompletionRatio: '',
|
CompletionRatio: '',
|
||||||
ModelPrice: '',
|
ModelPrice: '',
|
||||||
GroupRatio: '',
|
GroupRatio: '',
|
||||||
|
UserUsableGroups: '',
|
||||||
TopUpLink: '',
|
TopUpLink: '',
|
||||||
ChatLink: '',
|
ChatLink: '',
|
||||||
ChatLink2: '', // 添加的新状态变量
|
ChatLink2: '', // 添加的新状态变量
|
||||||
@@ -62,6 +63,7 @@ const OperationSetting = () => {
|
|||||||
if (
|
if (
|
||||||
item.key === 'ModelRatio' ||
|
item.key === 'ModelRatio' ||
|
||||||
item.key === 'GroupRatio' ||
|
item.key === 'GroupRatio' ||
|
||||||
|
item.key === 'UserUsableGroups' ||
|
||||||
item.key === 'CompletionRatio' ||
|
item.key === 'CompletionRatio' ||
|
||||||
item.key === 'ModelPrice'
|
item.key === 'ModelPrice'
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
IconComment,
|
IconComment,
|
||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconGift,
|
IconGift,
|
||||||
|
IconHelpCircle,
|
||||||
IconHistogram,
|
IconHistogram,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconImage,
|
IconImage,
|
||||||
@@ -28,8 +29,10 @@ import {
|
|||||||
IconSetting,
|
IconSetting,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from '@douyinfe/semi-icons';
|
} 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 { setStatusData } from '../helpers/data.js';
|
||||||
|
import { stringToColor } from '../helpers/render.js';
|
||||||
|
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||||
|
|
||||||
// HeaderBar Buttons
|
// HeaderBar Buttons
|
||||||
|
|
||||||
@@ -44,6 +47,8 @@ const SiderBar = () => {
|
|||||||
const systemName = getSystemName();
|
const systemName = getSystemName();
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||||
|
const theme = useTheme();
|
||||||
|
const setTheme = useSetTheme();
|
||||||
|
|
||||||
const routerMap = {
|
const routerMap = {
|
||||||
home: '/',
|
home: '/',
|
||||||
@@ -64,11 +69,17 @@ const SiderBar = () => {
|
|||||||
|
|
||||||
const headerButtons = useMemo(
|
const headerButtons = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
// {
|
||||||
|
// text: '首页',
|
||||||
|
// itemKey: 'home',
|
||||||
|
// to: '/',
|
||||||
|
// icon: <IconHome />,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
text: '首页',
|
text: '模型价格',
|
||||||
itemKey: 'home',
|
itemKey: 'pricing',
|
||||||
to: '/',
|
to: '/pricing',
|
||||||
icon: <IconHome />,
|
icon: <IconPriceTag />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '渠道',
|
text: '渠道',
|
||||||
@@ -105,12 +116,6 @@ const SiderBar = () => {
|
|||||||
to: '/topup',
|
to: '/topup',
|
||||||
icon: <IconCreditCard />,
|
icon: <IconCreditCard />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: '模型价格',
|
|
||||||
itemKey: 'pricing',
|
|
||||||
to: '/pricing',
|
|
||||||
icon: <IconPriceTag />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: '用户管理',
|
text: '用户管理',
|
||||||
itemKey: 'user',
|
itemKey: 'user',
|
||||||
@@ -206,48 +211,58 @@ const SiderBar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout>
|
<Nav
|
||||||
<div style={{ height: '100%' }}>
|
style={{ maxWidth: 220, height: '100%' }}
|
||||||
<Nav
|
defaultIsCollapsed={
|
||||||
// bodyStyle={{ maxWidth: 200 }}
|
isMobile() ||
|
||||||
style={{ maxWidth: 200 }}
|
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||||
defaultIsCollapsed={
|
}
|
||||||
isMobile() ||
|
isCollapsed={isCollapsed}
|
||||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
onCollapseChange={(collapsed) => {
|
||||||
}
|
setIsCollapsed(collapsed);
|
||||||
isCollapsed={isCollapsed}
|
}}
|
||||||
onCollapseChange={(collapsed) => {
|
selectedKeys={selectedKeys}
|
||||||
setIsCollapsed(collapsed);
|
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||||
}}
|
return (
|
||||||
selectedKeys={selectedKeys}
|
<Link
|
||||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
style={{ textDecoration: 'none' }}
|
||||||
return (
|
to={routerMap[props.itemKey]}
|
||||||
<Link
|
>
|
||||||
style={{ textDecoration: 'none' }}
|
{itemElement}
|
||||||
to={routerMap[props.itemKey]}
|
</Link>
|
||||||
>
|
);
|
||||||
{itemElement}
|
}}
|
||||||
</Link>
|
items={headerButtons}
|
||||||
);
|
onSelect={(key) => {
|
||||||
}}
|
setSelectedKeys([key.itemKey]);
|
||||||
items={headerButtons}
|
}}
|
||||||
onSelect={(key) => {
|
// header={{
|
||||||
setSelectedKeys([key.itemKey]);
|
// logo: (
|
||||||
}}
|
// <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
header={{
|
// ),
|
||||||
logo: (
|
// text: systemName,
|
||||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
// }}
|
||||||
),
|
// footer={{
|
||||||
text: systemName,
|
// text: '© 2021 NekoAPI',
|
||||||
}}
|
// }}
|
||||||
// footer={{
|
footer={
|
||||||
// text: '© 2021 NekoAPI',
|
<>
|
||||||
// }}
|
{isMobile() && (
|
||||||
>
|
<Switch
|
||||||
<Nav.Footer collapseButton={true}></Nav.Footer>
|
checkedText='🌞'
|
||||||
</Nav>
|
size={'small'}
|
||||||
</div>
|
checked={theme === 'dark'}
|
||||||
</Layout>
|
uncheckedText='🌙'
|
||||||
|
onChange={(checked) => {
|
||||||
|
setTheme(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Nav.Footer collapseButton={true}></Nav.Footer>
|
||||||
|
</Nav>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
import { renderGroup, renderQuota } from '../helpers/render';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Popover,
|
Popover,
|
||||||
|
Space,
|
||||||
SplitButtonGroup,
|
SplitButtonGroup,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -119,7 +120,14 @@ const TokensTable = () => {
|
|||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
render: (text, record, index) => {
|
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) {
|
export function renderGroup(group) {
|
||||||
if (group === '') {
|
if (group === '') {
|
||||||
return (
|
return (
|
||||||
<Tag size='large' key='default'>
|
<Tag size='large' key='default' color='orange'>
|
||||||
unknown
|
用户分组
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ body {
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
color: var(--semi-color-text-0) !important;
|
color: var(--semi-color-text-0) !important;
|
||||||
background-color: var(--semi-color-bg-0) !important;
|
background-color: var(--semi-color-bg-0) !important;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
@@ -50,9 +51,9 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.semi-layout {
|
/*.semi-layout {*/
|
||||||
height: 100%;
|
/* height: 100%;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
.tableShow {
|
.tableShow {
|
||||||
display: revert;
|
display: revert;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import HeaderBar from './components/HeaderBar';
|
import HeaderBar from './components/HeaderBar';
|
||||||
import Footer from './components/Footer';
|
|
||||||
import 'semantic-ui-offline/semantic.min.css';
|
import 'semantic-ui-offline/semantic.min.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { UserProvider } from './context/User';
|
import { UserProvider } from './context/User';
|
||||||
@@ -13,35 +12,40 @@ import { StatusProvider } from './context/Status';
|
|||||||
import { Layout } from '@douyinfe/semi-ui';
|
import { Layout } from '@douyinfe/semi-ui';
|
||||||
import SiderBar from './components/SiderBar';
|
import SiderBar from './components/SiderBar';
|
||||||
import { ThemeProvider } from './context/Theme';
|
import { ThemeProvider } from './context/Theme';
|
||||||
|
import FooterBar from './components/Footer';
|
||||||
|
|
||||||
// initialization
|
// initialization
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
const { Sider, Content, Header } = Layout;
|
const { Sider, Content, Header, Footer } = Layout;
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<StatusProvider>
|
<StatusProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Layout>
|
<Layout
|
||||||
<Sider>
|
style={{
|
||||||
<SiderBar />
|
height: '100vh',
|
||||||
</Sider>
|
display: 'flex',
|
||||||
<Layout>
|
flexDirection: 'column',
|
||||||
<Header>
|
}}
|
||||||
<HeaderBar />
|
>
|
||||||
</Header>
|
<Header>
|
||||||
<Content
|
<HeaderBar />
|
||||||
style={{
|
</Header>
|
||||||
padding: '24px',
|
<Layout style={{ flex: 1, overflow: 'hidden' }}>
|
||||||
}}
|
<Sider>
|
||||||
>
|
<SiderBar />
|
||||||
<App />
|
</Sider>
|
||||||
</Content>
|
<Layout>
|
||||||
<Layout.Footer>
|
<Content style={{ overflowY: 'auto', padding: '24px' }}>
|
||||||
<Footer></Footer>
|
<App />
|
||||||
</Layout.Footer>
|
</Content>
|
||||||
|
<Layout.Footer>
|
||||||
|
<FooterBar></FooterBar>
|
||||||
|
</Layout.Footer>
|
||||||
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||||
|
import { Layout } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
const Chat = () => {
|
const ChatPage = () => {
|
||||||
const chatLink = localStorage.getItem('chat_link');
|
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
|
<iframe
|
||||||
src={chatLink}
|
src={iframeSrc}
|
||||||
style={{ width: '100%', height: '85vh', border: 'none' }}
|
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: '',
|
ModelRatio: '',
|
||||||
CompletionRatio: '',
|
CompletionRatio: '',
|
||||||
GroupRatio: '',
|
GroupRatio: '',
|
||||||
|
UserUsableGroups: '',
|
||||||
});
|
});
|
||||||
const refForm = useRef();
|
const refForm = useRef();
|
||||||
const [inputsRow, setInputsRow] = useState(inputs);
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
@@ -224,6 +225,33 @@ export default function SettingsMagnification(props) {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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.Section>
|
||||||
</Form>
|
</Form>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
SideSheet,
|
SideSheet,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
|
TextArea,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
@@ -34,6 +35,8 @@ const EditToken = (props) => {
|
|||||||
unlimited_quota: false,
|
unlimited_quota: false,
|
||||||
model_limits_enabled: false,
|
model_limits_enabled: false,
|
||||||
model_limits: [],
|
model_limits: [],
|
||||||
|
allow_ips: '',
|
||||||
|
group: '',
|
||||||
};
|
};
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const {
|
const {
|
||||||
@@ -43,9 +46,12 @@ const EditToken = (props) => {
|
|||||||
unlimited_quota,
|
unlimited_quota,
|
||||||
model_limits_enabled,
|
model_limits_enabled,
|
||||||
model_limits,
|
model_limits,
|
||||||
|
allow_ips,
|
||||||
|
group,
|
||||||
} = inputs;
|
} = inputs;
|
||||||
// const [visible, setVisible] = useState(false);
|
// const [visible, setVisible] = useState(false);
|
||||||
const [models, setModels] = useState({});
|
const [models, setModels] = useState({});
|
||||||
|
const [groups, setGroups] = useState([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleInputChange = (name, value) => {
|
const handleInputChange = (name, value) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [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 () => {
|
const loadToken = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let res = await API.get(`/api/token/${props.editingToken.id}`);
|
let res = await API.get(`/api/token/${props.editingToken.id}`);
|
||||||
@@ -118,6 +140,7 @@ const EditToken = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
loadModels();
|
loadModels();
|
||||||
|
loadGroups();
|
||||||
}, [isEdit]);
|
}, [isEdit]);
|
||||||
|
|
||||||
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
|
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
|
||||||
@@ -374,6 +397,19 @@ const EditToken = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<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' }}>
|
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -404,6 +440,31 @@ const EditToken = (props) => {
|
|||||||
optionList={models}
|
optionList={models}
|
||||||
disabled={!model_limits_enabled}
|
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>
|
</Spin>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user