mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-04 19:23:13 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe1f35fd1 | ||
|
|
972ac1ee0f | ||
|
|
0f95502b04 | ||
|
|
b58b1dc0ec | ||
|
|
05d9aa61df | ||
|
|
221894d972 | ||
|
|
50eab6b4e4 | ||
|
|
ed972eef06 | ||
|
|
c6ff785a83 | ||
|
|
2e734e0c37 |
@@ -126,6 +126,10 @@ const (
|
||||
RoleRootUser = 100
|
||||
)
|
||||
|
||||
func IsValidateRole(role int) bool {
|
||||
return role == RoleGuestUser || role == RoleCommonUser || role == RoleAdminUser || role == RoleRootUser
|
||||
}
|
||||
|
||||
var (
|
||||
FileUploadPermission = RoleGuestUser
|
||||
FileDownloadPermission = RoleGuestUser
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"html/template"
|
||||
"log"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os/exec"
|
||||
@@ -142,24 +145,35 @@ func GetUUID() string {
|
||||
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
func GenerateKey() string {
|
||||
//rand.Seed(time.Now().UnixNano())
|
||||
key := make([]byte, 48)
|
||||
for i := 0; i < 16; i++ {
|
||||
key[i] = keyChars[rand.Intn(len(keyChars))]
|
||||
}
|
||||
uuid_ := GetUUID()
|
||||
for i := 0; i < 32; i++ {
|
||||
c := uuid_[i]
|
||||
if i%2 == 0 && c >= 'a' && c <= 'z' {
|
||||
c = c - 'a' + 'A'
|
||||
func GenerateRandomCharsKey(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
maxI := big.NewInt(int64(len(keyChars)))
|
||||
|
||||
for i := range b {
|
||||
n, err := crand.Int(crand.Reader, maxI)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key[i+16] = c
|
||||
b[i] = keyChars[n.Int64()]
|
||||
}
|
||||
return string(key)
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func GenerateRandomKey(length int) (string, error) {
|
||||
bytes := make([]byte, length*3/4) // 对于48位的输出,这里应该是36
|
||||
if _, err := crand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GenerateKey() (string, error) {
|
||||
//rand.Seed(time.Now().UnixNano())
|
||||
return GenerateRandomCharsKey(48)
|
||||
}
|
||||
|
||||
func GetRandomInt(max int) int {
|
||||
|
||||
@@ -112,7 +112,9 @@ func GitHubOAuth(c *gin.Context) {
|
||||
user := model.User{
|
||||
GitHubId: githubUser.Login,
|
||||
}
|
||||
// IsGitHubIdAlreadyTaken is unscoped
|
||||
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
||||
// FillUserByGitHubId is scoped
|
||||
err := user.FillUserByGitHubId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -121,6 +123,14 @@ func GitHubOAuth(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// if user.Id == 0 , user has been deleted
|
||||
if user.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "用户已注销",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if common.RegisterEnabled {
|
||||
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"sort"
|
||||
@@ -48,6 +49,13 @@ func TelegramBind(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "用户已注销",
|
||||
})
|
||||
return
|
||||
}
|
||||
user.TelegramId = telegramId
|
||||
if err := user.Update(false); err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
|
||||
@@ -123,10 +123,19 @@ func AddToken(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
key, err := common.GenerateKey()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "生成令牌失败",
|
||||
})
|
||||
common.SysError("failed to generate token key: " + err.Error())
|
||||
return
|
||||
}
|
||||
cleanToken := model.Token{
|
||||
UserId: c.GetInt("id"),
|
||||
Name: token.Name,
|
||||
Key: common.GenerateKey(),
|
||||
Key: key,
|
||||
CreatedTime: common.GetTimestamp(),
|
||||
AccessedTime: common.GetTimestamp(),
|
||||
ExpiredTime: token.ExpiredTime,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -158,8 +159,9 @@ func Register(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
"message": "数据库错误,请稍后重试",
|
||||
})
|
||||
common.SysError(fmt.Sprintf("CheckUserExistOrDeleted error: %v", err))
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
@@ -199,11 +201,20 @@ func Register(c *gin.Context) {
|
||||
}
|
||||
// 生成默认令牌
|
||||
if constant.GenerateDefaultToken {
|
||||
key, err := common.GenerateKey()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "生成默认令牌失败",
|
||||
})
|
||||
common.SysError("failed to generate token key: " + err.Error())
|
||||
return
|
||||
}
|
||||
// 生成默认令牌
|
||||
token := model.Token{
|
||||
UserId: insertedUser.Id, // 使用插入后的用户ID
|
||||
Name: cleanUser.Username + "的初始令牌",
|
||||
Key: common.GenerateKey(),
|
||||
Key: key,
|
||||
CreatedTime: common.GetTimestamp(),
|
||||
AccessedTime: common.GetTimestamp(),
|
||||
ExpiredTime: -1, // 永不过期
|
||||
@@ -310,7 +321,18 @@ func GenerateAccessToken(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
user.AccessToken = common.GetUUID()
|
||||
// get rand int 28-32
|
||||
randI := common.GetRandomInt(4)
|
||||
key, err := common.GenerateRandomKey(29 + randI)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "生成失败",
|
||||
})
|
||||
common.SysError("failed to generate key: " + err.Error())
|
||||
return
|
||||
}
|
||||
user.SetAccessToken(key)
|
||||
|
||||
if model.DB.Where("access_token = ?", user.AccessToken).First(user).RowsAffected != 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -616,6 +638,7 @@ func DeleteSelf(c *gin.Context) {
|
||||
func CreateUser(c *gin.Context) {
|
||||
var user model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||
user.Username = strings.TrimSpace(user.Username)
|
||||
if err != nil || user.Username == "" || user.Password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@@ -663,8 +686,8 @@ func CreateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
type ManageRequest struct {
|
||||
Username string `json:"username"`
|
||||
Action string `json:"action"`
|
||||
Id int `json:"id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// ManageUser Only admin user can do this
|
||||
@@ -680,7 +703,7 @@ func ManageUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
Username: req.Username,
|
||||
Id: req.Id,
|
||||
}
|
||||
// Fill attributes
|
||||
model.DB.Unscoped().Where(&user).First(&user)
|
||||
|
||||
@@ -78,6 +78,13 @@ func WeChatAuth(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "用户已注销",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if common.RegisterEnabled {
|
||||
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
|
||||
@@ -10,6 +10,17 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validUserInfo(username string, role int) bool {
|
||||
// check username is empty
|
||||
if strings.TrimSpace(username) == "" {
|
||||
return false
|
||||
}
|
||||
if !common.IsValidateRole(role) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func authHelper(c *gin.Context, minRole int) {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
@@ -30,6 +41,14 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
}
|
||||
user := model.ValidateAccessToken(accessToken)
|
||||
if user != nil && user.Username != "" {
|
||||
if !validUserInfo(user.Username, user.Role) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,用户信息无效",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// Token is valid
|
||||
username = user.Username
|
||||
role = user.Role
|
||||
@@ -91,6 +110,14 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if !validUserInfo(username.(string), role.(int)) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,用户信息无效",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("username", username)
|
||||
c.Set("role", role)
|
||||
c.Set("id", id)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -32,7 +32,7 @@ func createRootAccountIfNeed() error {
|
||||
Role: common.RoleRootUser,
|
||||
Status: common.UserStatusEnabled,
|
||||
DisplayName: "Root User",
|
||||
AccessToken: common.GetUUID(),
|
||||
AccessToken: nil,
|
||||
Quota: 100000000,
|
||||
}
|
||||
DB.Create(&rootUser)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,7 @@ type User struct {
|
||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
|
||||
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
||||
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||
AccessToken *string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||
Quota int `json:"quota" gorm:"type:int;default:0"`
|
||||
UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
|
||||
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
|
||||
@@ -38,6 +38,17 @@ type User struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (user *User) GetAccessToken() string {
|
||||
if user.AccessToken == nil {
|
||||
return ""
|
||||
}
|
||||
return *user.AccessToken
|
||||
}
|
||||
|
||||
func (user *User) SetAccessToken(token string) {
|
||||
user.AccessToken = &token
|
||||
}
|
||||
|
||||
// CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil
|
||||
func CheckUserExistOrDeleted(username string, email string) (bool, error) {
|
||||
var user User
|
||||
@@ -201,7 +212,7 @@ func (user *User) Insert(inviterId int) error {
|
||||
}
|
||||
}
|
||||
user.Quota = common.QuotaForNewUser
|
||||
user.AccessToken = common.GetUUID()
|
||||
//user.SetAccessToken(common.GetUUID())
|
||||
user.AffCode = common.GetRandomString(4)
|
||||
result := DB.Create(user)
|
||||
if result.Error != nil {
|
||||
@@ -295,11 +306,12 @@ func (user *User) ValidateAndFill() (err error) {
|
||||
// that means if your field’s value is 0, '', false or other zero values,
|
||||
// it won’t be used to build query conditions
|
||||
password := user.Password
|
||||
if user.Username == "" || password == "" {
|
||||
username := strings.TrimSpace(user.Username)
|
||||
if username == "" || password == "" {
|
||||
return errors.New("用户名或密码为空")
|
||||
}
|
||||
// find buy username or email
|
||||
DB.Where("username = ? OR email = ?", user.Username, user.Username).First(user)
|
||||
DB.Where("username = ? OR email = ?", username, username).First(user)
|
||||
okay := common.ValidatePasswordAndHash(password, user.Password)
|
||||
if !okay || user.Status != common.UserStatusEnabled {
|
||||
return errors.New("用户名或密码错误,或用户已被封禁")
|
||||
@@ -339,14 +351,6 @@ func (user *User) FillUserByWeChatId() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) FillUserByUsername() error {
|
||||
if user.Username == "" {
|
||||
return errors.New("username 为空!")
|
||||
}
|
||||
DB.Where(User{Username: user.Username}).First(user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) FillUserByTelegramId() error {
|
||||
if user.TelegramId == "" {
|
||||
return errors.New("Telegram id 为空!")
|
||||
@@ -359,23 +363,19 @@ func (user *User) FillUserByTelegramId() error {
|
||||
}
|
||||
|
||||
func IsEmailAlreadyTaken(email string) bool {
|
||||
return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1
|
||||
return DB.Unscoped().Where("email = ?", email).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func IsWeChatIdAlreadyTaken(wechatId string) bool {
|
||||
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||
return DB.Unscoped().Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
||||
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func IsUsernameAlreadyTaken(username string) bool {
|
||||
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
||||
return DB.Unscoped().Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func IsTelegramIdAlreadyTaken(telegramId string) bool {
|
||||
return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
||||
return DB.Unscoped().Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
||||
}
|
||||
|
||||
func ResetUserPasswordByEmail(email string, password string) error {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess, updateAPI } from '../helpers';
|
||||
import { UserContext } from '../context/User';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
|
||||
const GitHubOAuth = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -23,8 +24,10 @@ const GitHubOAuth = () => {
|
||||
} else {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
setUserData(data);
|
||||
updateAPI()
|
||||
showSuccess('登录成功!');
|
||||
navigate('/');
|
||||
navigate('/token');
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -36,7 +36,7 @@ let buttons = [
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
icon: <IconHomeStroked />,
|
||||
// icon: <IconHomeStroked />,
|
||||
},
|
||||
// {
|
||||
// text: '模型价格',
|
||||
|
||||
@@ -71,6 +71,8 @@ const LoginForm = () => {
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
setUserData(data);
|
||||
updateAPI()
|
||||
navigate('/');
|
||||
showSuccess('登录成功!');
|
||||
setShowWeChatLoginModal(false);
|
||||
@@ -143,6 +145,8 @@ const LoginForm = () => {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
showSuccess('登录成功!');
|
||||
setUserData(data);
|
||||
updateAPI()
|
||||
navigate('/');
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -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,
|
||||
@@ -87,6 +87,7 @@ const ModelPricing = () => {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [selectedGroup, setSelectedGroup] = useState('default');
|
||||
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
@@ -120,7 +121,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,
|
||||
},
|
||||
@@ -166,6 +168,43 @@ 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'}}>
|
||||
@@ -201,6 +240,8 @@ const ModelPricing = () => {
|
||||
<Text>模型:{record.quota_type === 0 ? text : '无'}</Text>
|
||||
<br />
|
||||
<Text>补全:{record.quota_type === 0 ? completionRatio : '无'}</Text>
|
||||
<br />
|
||||
<Text>分组:{groupRatio[selectedGroup]}</Text>
|
||||
</>
|
||||
);
|
||||
return <div>{content}</div>;
|
||||
@@ -213,11 +254,11 @@ 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>
|
||||
@@ -226,7 +267,7 @@ const ModelPricing = () => {
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let price = parseFloat(text) * record.group_ratio;
|
||||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||
content = <>模型价格:${price}</>;
|
||||
}
|
||||
return <div>{content}</div>;
|
||||
@@ -237,12 +278,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) => {
|
||||
@@ -275,6 +316,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);
|
||||
@@ -307,14 +349,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/>
|
||||
|
||||
@@ -151,7 +151,7 @@ const UsersTable = () => {
|
||||
title='确定?'
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'promote', record);
|
||||
manageUser(record.id, 'promote', record);
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='warning' style={{ marginRight: 1 }}>
|
||||
@@ -162,7 +162,7 @@ const UsersTable = () => {
|
||||
title='确定?'
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'demote', record);
|
||||
manageUser(record.id, 'demote', record);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -179,7 +179,7 @@ const UsersTable = () => {
|
||||
type='warning'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageUser(record.username, 'disable', record);
|
||||
manageUser(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
@@ -190,7 +190,7 @@ const UsersTable = () => {
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageUser(record.username, 'enable', record);
|
||||
manageUser(record.id, 'enable', record);
|
||||
}}
|
||||
disabled={record.status === 3}
|
||||
>
|
||||
@@ -214,7 +214,7 @@ const UsersTable = () => {
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'delete', record).then(() => {
|
||||
manageUser(record.id, 'delete', record).then(() => {
|
||||
removeRecord(record.id);
|
||||
});
|
||||
}}
|
||||
@@ -303,9 +303,9 @@ const UsersTable = () => {
|
||||
fetchGroups().then();
|
||||
}, []);
|
||||
|
||||
const manageUser = async (username, action, record) => {
|
||||
const manageUser = async (userId, action, record) => {
|
||||
const res = await API.post('/api/user/manage', {
|
||||
username,
|
||||
id: userId,
|
||||
action,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
|
||||
@@ -15,8 +15,8 @@ export function renderText(text, limit) {
|
||||
export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default' color={stringToColor('default')}>
|
||||
default
|
||||
<Tag size='large' key='default' color='orange'>
|
||||
用户分组
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,150 +273,150 @@ const EditToken = (props) => {
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Input
|
||||
style={{marginTop: 20}}
|
||||
label='名称'
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
required={!isEdit}
|
||||
style={{ marginTop: 20 }}
|
||||
label='名称'
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
required={!isEdit}
|
||||
/>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
<DatePicker
|
||||
label='过期时间'
|
||||
name='expired_time'
|
||||
placeholder={'请选择过期时间'}
|
||||
onChange={(value) => handleInputChange('expired_time', value)}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
type='dateTime'
|
||||
label='过期时间'
|
||||
name='expired_time'
|
||||
placeholder={'请选择过期时间'}
|
||||
onChange={(value) => handleInputChange('expired_time', value)}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
type='dateTime'
|
||||
/>
|
||||
<div style={{marginTop: 20}}>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Space>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
永不过期
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
>
|
||||
一小时
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
一个月
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
type={'tertiary'}
|
||||
onClick={() => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
>
|
||||
一天
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider/>
|
||||
<Divider />
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
|
||||
}
|
||||
type={'warning'}
|
||||
description={
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
|
||||
}
|
||||
></Banner>
|
||||
<div style={{marginTop: 20}}>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{`额度${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{marginTop: 8}}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
type='number'
|
||||
// position={'top'}
|
||||
data={[
|
||||
{value: 500000, label: '1$'},
|
||||
{value: 5000000, label: '10$'},
|
||||
{value: 25000000, label: '50$'},
|
||||
{value: 50000000, label: '100$'},
|
||||
{value: 250000000, label: '500$'},
|
||||
{value: 500000000, label: '1000$'},
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
style={{ marginTop: 8 }}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
type='number'
|
||||
// position={'top'}
|
||||
data={[
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 5000000, label: '10$' },
|
||||
{ value: 25000000, label: '50$' },
|
||||
{ value: 50000000, label: '100$' },
|
||||
{ value: 250000000, label: '500$' },
|
||||
{ value: 500000000, label: '1000$' },
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
|
||||
{!isEdit && (
|
||||
<>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>新建数量</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{marginTop: 8}}
|
||||
label='数量'
|
||||
placeholder={'请选择或输入创建令牌的数量'}
|
||||
onChange={(value) => handleTokenCountChange(value)}
|
||||
onSelect={(value) => handleTokenCountChange(value)}
|
||||
value={tokenCount.toString()}
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
data={[
|
||||
{value: 10, label: '10个'},
|
||||
{value: 20, label: '20个'},
|
||||
{value: 30, label: '30个'},
|
||||
{value: 100, label: '100个'},
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>新建数量</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
label='数量'
|
||||
placeholder={'请选择或输入创建令牌的数量'}
|
||||
onChange={(value) => handleTokenCountChange(value)}
|
||||
onSelect={(value) => handleTokenCountChange(value)}
|
||||
value={tokenCount.toString()}
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
data={[
|
||||
{ value: 10, label: '10个' },
|
||||
{ value: 20, label: '20个' },
|
||||
{ value: 30, label: '30个' },
|
||||
{ value: 100, label: '100个' },
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
style={{marginTop: 8}}
|
||||
type={'warning'}
|
||||
onClick={() => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
style={{ marginTop: 8 }}
|
||||
type={'warning'}
|
||||
onClick={() => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
>
|
||||
{unlimited_quota ? '取消无限额度' : '设为无限额度'}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider/>
|
||||
<div style={{marginTop: 10}}>
|
||||
<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'}}
|
||||
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>
|
||||
<Checkbox
|
||||
name='model_limits_enabled'
|
||||
checked={model_limits_enabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange('model_limits_enabled', e.target.checked)
|
||||
}
|
||||
name='model_limits_enabled'
|
||||
checked={model_limits_enabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange('model_limits_enabled', e.target.checked)
|
||||
}
|
||||
></Checkbox>
|
||||
<Typography.Text>
|
||||
启用模型限制(非必要,不建议启用)
|
||||
@@ -425,27 +425,27 @@ const EditToken = (props) => {
|
||||
</div>
|
||||
|
||||
<Select
|
||||
style={{marginTop: 8}}
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_limits', value);
|
||||
}}
|
||||
value={inputs.model_limits}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
disabled={!model_limits_enabled}
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_limits', value);
|
||||
}}
|
||||
value={inputs.model_limits}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
disabled={!model_limits_enabled}
|
||||
/>
|
||||
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text>令牌分组,不选为默认分组</Typography.Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>令牌分组,默认为用户的分组</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
style={{marginTop: 8}}
|
||||
placeholder={'令牌分组,不选为默认分组'}
|
||||
{groups.length > 0 ?
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'令牌分组,默认为用户的分组'}
|
||||
name='gruop'
|
||||
required
|
||||
selection
|
||||
@@ -455,7 +455,14 @@ const EditToken = (props) => {
|
||||
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