mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-17 16:06:38 +08:00
merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
commit
5ffb520363
@ -229,6 +229,8 @@ const (
|
|||||||
ChannelTypeLingYiWanWu = 31
|
ChannelTypeLingYiWanWu = 31
|
||||||
ChannelTypeAws = 33
|
ChannelTypeAws = 33
|
||||||
ChannelTypeCohere = 34
|
ChannelTypeCohere = 34
|
||||||
|
|
||||||
|
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
|
@ -64,8 +64,6 @@ var DefaultModelRatio = map[string]float64{
|
|||||||
"text-search-ada-doc-001": 10,
|
"text-search-ada-doc-001": 10,
|
||||||
"text-moderation-stable": 0.1,
|
"text-moderation-stable": 0.1,
|
||||||
"text-moderation-latest": 0.1,
|
"text-moderation-latest": 0.1,
|
||||||
"dall-e-2": 8,
|
|
||||||
"dall-e-3": 16,
|
|
||||||
"claude-instant-1": 0.4, // $0.8 / 1M tokens
|
"claude-instant-1": 0.4, // $0.8 / 1M tokens
|
||||||
"claude-2.0": 4, // $8 / 1M tokens
|
"claude-2.0": 4, // $8 / 1M tokens
|
||||||
"claude-2.1": 4, // $8 / 1M tokens
|
"claude-2.1": 4, // $8 / 1M tokens
|
||||||
@ -120,6 +118,8 @@ var DefaultModelRatio = map[string]float64{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var DefaultModelPrice = map[string]float64{
|
var DefaultModelPrice = map[string]float64{
|
||||||
|
"dall-e-2": 0.02,
|
||||||
|
"dall-e-3": 0.04,
|
||||||
"gpt-4-gizmo-*": 0.1,
|
"gpt-4-gizmo-*": 0.1,
|
||||||
"g-*": 0.1,
|
"g-*": 0.1,
|
||||||
"mj_imagine": 0.1,
|
"mj_imagine": 0.1,
|
||||||
@ -142,6 +142,13 @@ var DefaultModelPrice = map[string]float64{
|
|||||||
var modelPrice map[string]float64 = nil
|
var modelPrice map[string]float64 = nil
|
||||||
var modelRatio map[string]float64 = nil
|
var modelRatio map[string]float64 = nil
|
||||||
|
|
||||||
|
var CompletionRatio map[string]float64 = nil
|
||||||
|
var DefaultCompletionRatio = map[string]float64{
|
||||||
|
"gpt-4-gizmo-*": 2,
|
||||||
|
"g-*": 2,
|
||||||
|
"gpt-4-all": 2,
|
||||||
|
}
|
||||||
|
|
||||||
func ModelPrice2JSONString() string {
|
func ModelPrice2JSONString() string {
|
||||||
if modelPrice == nil {
|
if modelPrice == nil {
|
||||||
modelPrice = DefaultModelPrice
|
modelPrice = DefaultModelPrice
|
||||||
@ -158,7 +165,8 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
|
|||||||
return json.Unmarshal([]byte(jsonStr), &modelPrice)
|
return json.Unmarshal([]byte(jsonStr), &modelPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetModelPrice(name string, printErr bool) float64 {
|
// GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false
|
||||||
|
func GetModelPrice(name string, printErr bool) (float64, bool) {
|
||||||
if modelPrice == nil {
|
if modelPrice == nil {
|
||||||
modelPrice = DefaultModelPrice
|
modelPrice = DefaultModelPrice
|
||||||
}
|
}
|
||||||
@ -172,9 +180,16 @@ func GetModelPrice(name string, printErr bool) float64 {
|
|||||||
if printErr {
|
if printErr {
|
||||||
SysError("model price not found: " + name)
|
SysError("model price not found: " + name)
|
||||||
}
|
}
|
||||||
return -1
|
return -1, false
|
||||||
}
|
}
|
||||||
return price
|
return price, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModelPrices() map[string]float64 {
|
||||||
|
if modelPrice == nil {
|
||||||
|
modelPrice = DefaultModelPrice
|
||||||
|
}
|
||||||
|
return modelPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModelRatio2JSONString() string {
|
func ModelRatio2JSONString() string {
|
||||||
@ -210,7 +225,35 @@ func GetModelRatio(name string) float64 {
|
|||||||
return ratio
|
return ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetModelRatios() map[string]float64 {
|
||||||
|
if modelRatio == nil {
|
||||||
|
modelRatio = DefaultModelRatio
|
||||||
|
}
|
||||||
|
return modelRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompletionRatio2JSONString() string {
|
||||||
|
if CompletionRatio == nil {
|
||||||
|
CompletionRatio = DefaultCompletionRatio
|
||||||
|
}
|
||||||
|
jsonBytes, err := json.Marshal(CompletionRatio)
|
||||||
|
if err != nil {
|
||||||
|
SysError("error marshalling completion ratio: " + err.Error())
|
||||||
|
}
|
||||||
|
return string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCompletionRatioByJSONString(jsonStr string) error {
|
||||||
|
CompletionRatio = make(map[string]float64)
|
||||||
|
return json.Unmarshal([]byte(jsonStr), &CompletionRatio)
|
||||||
|
}
|
||||||
|
|
||||||
func GetCompletionRatio(name string) float64 {
|
func GetCompletionRatio(name string) float64 {
|
||||||
|
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
||||||
|
name = "gpt-4-gizmo-*"
|
||||||
|
} else if strings.HasPrefix(name, "g-") {
|
||||||
|
name = "g-*"
|
||||||
|
}
|
||||||
if strings.HasPrefix(name, "gpt-3.5") {
|
if strings.HasPrefix(name, "gpt-3.5") {
|
||||||
if strings.HasSuffix(name, "0125") {
|
if strings.HasSuffix(name, "0125") {
|
||||||
return 3
|
return 3
|
||||||
@ -224,7 +267,7 @@ func GetCompletionRatio(name string) float64 {
|
|||||||
|
|
||||||
return 1.333333
|
return 1.333333
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(name, "gpt-4") {
|
if strings.HasPrefix(name, "gpt-4") && name != "gpt-4-all" && name != "gpt-4-gizmo-*" {
|
||||||
if strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4-turbo") || strings.HasPrefix(name, "gpt-4o") {
|
if strings.HasSuffix(name, "preview") || strings.HasPrefix(name, "gpt-4-turbo") || strings.HasPrefix(name, "gpt-4o") {
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
@ -258,7 +301,21 @@ func GetCompletionRatio(name string) float64 {
|
|||||||
}
|
}
|
||||||
switch name {
|
switch name {
|
||||||
case "llama2-70b-4096":
|
case "llama2-70b-4096":
|
||||||
return 0.8 / 0.7
|
return 0.8 / 0.64
|
||||||
|
case "llama3-8b-8192":
|
||||||
|
return 2
|
||||||
|
case "llama3-70b-8192":
|
||||||
|
return 0.79 / 0.59
|
||||||
|
}
|
||||||
|
if ratio, ok := CompletionRatio[name]; ok {
|
||||||
|
return ratio
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCompletionRatios() map[string]float64 {
|
||||||
|
if CompletionRatio == nil {
|
||||||
|
CompletionRatio = DefaultCompletionRatio
|
||||||
|
}
|
||||||
|
return CompletionRatio
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -241,3 +242,19 @@ func RandomSleep() {
|
|||||||
// Sleep for 0-3000 ms
|
// Sleep for 0-3000 ms
|
||||||
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapToJsonStr(m map[string]interface{}) string {
|
||||||
|
bytes, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapToJsonStrFloat(m map[string]float64) string {
|
||||||
|
bytes, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
@ -53,7 +53,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openaiErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
meta := relaycommon.GenRelayInfo(c)
|
meta := relaycommon.GenRelayInfo(c)
|
||||||
apiType := constant.ChannelType2APIType(channel.Type)
|
apiType, _ := constant.ChannelType2APIType(channel.Type)
|
||||||
adaptor := relay.GetAdaptor(apiType)
|
adaptor := relay.GetAdaptor(apiType)
|
||||||
if adaptor == nil {
|
if adaptor == nil {
|
||||||
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
|
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
|
||||||
@ -208,7 +208,7 @@ func testAllChannels(notify bool) error {
|
|||||||
if isChannelEnabled && service.ShouldDisableChannel(openaiErr, -1) && ban {
|
if isChannelEnabled && service.ShouldDisableChannel(openaiErr, -1) && ban {
|
||||||
service.DisableChannel(channel.Id, channel.Name, err.Error())
|
service.DisableChannel(channel.Id, channel.Name, err.Error())
|
||||||
}
|
}
|
||||||
if !isChannelEnabled && service.ShouldEnableChannel(err, openaiErr) {
|
if !isChannelEnabled && service.ShouldEnableChannel(err, openaiErr, channel.Status) {
|
||||||
service.EnableChannel(channel.Id, channel.Name)
|
service.EnableChannel(channel.Id, channel.Name)
|
||||||
}
|
}
|
||||||
channel.UpdateResponseTime(milliseconds)
|
channel.UpdateResponseTime(milliseconds)
|
||||||
|
@ -149,7 +149,7 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if common.EmailAliasRestrictionEnabled {
|
if common.EmailAliasRestrictionEnabled {
|
||||||
containsSpecialSymbols := strings.Contains(localPart, "+") || strings.Count(localPart, ".") > 1
|
containsSpecialSymbols := strings.Contains(localPart, "+") || strings.Contains(localPart, ".")
|
||||||
if containsSpecialSymbols {
|
if containsSpecialSymbols {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
@ -4,49 +4,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/relay"
|
"one-api/relay"
|
||||||
"one-api/relay/channel/ai360"
|
"one-api/relay/channel/ai360"
|
||||||
"one-api/relay/channel/moonshot"
|
|
||||||
"one-api/relay/channel/lingyiwanwu"
|
"one-api/relay/channel/lingyiwanwu"
|
||||||
|
"one-api/relay/channel/moonshot"
|
||||||
|
relaycommon "one-api/relay/common"
|
||||||
relayconstant "one-api/relay/constant"
|
relayconstant "one-api/relay/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://platform.openai.com/docs/api-reference/models/list
|
// https://platform.openai.com/docs/api-reference/models/list
|
||||||
|
|
||||||
type OpenAIModelPermission struct {
|
var openAIModels []dto.OpenAIModels
|
||||||
Id string `json:"id"`
|
var openAIModelsMap map[string]dto.OpenAIModels
|
||||||
Object string `json:"object"`
|
var channelId2Models map[int][]string
|
||||||
Created int `json:"created"`
|
|
||||||
AllowCreateEngine bool `json:"allow_create_engine"`
|
|
||||||
AllowSampling bool `json:"allow_sampling"`
|
|
||||||
AllowLogprobs bool `json:"allow_logprobs"`
|
|
||||||
AllowSearchIndices bool `json:"allow_search_indices"`
|
|
||||||
AllowView bool `json:"allow_view"`
|
|
||||||
AllowFineTuning bool `json:"allow_fine_tuning"`
|
|
||||||
Organization string `json:"organization"`
|
|
||||||
Group *string `json:"group"`
|
|
||||||
IsBlocking bool `json:"is_blocking"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenAIModels struct {
|
func getPermission() []dto.OpenAIModelPermission {
|
||||||
Id string `json:"id"`
|
var permission []dto.OpenAIModelPermission
|
||||||
Object string `json:"object"`
|
permission = append(permission, dto.OpenAIModelPermission{
|
||||||
Created int `json:"created"`
|
|
||||||
OwnedBy string `json:"owned_by"`
|
|
||||||
Permission []OpenAIModelPermission `json:"permission"`
|
|
||||||
Root string `json:"root"`
|
|
||||||
Parent *string `json:"parent"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var openAIModels []OpenAIModels
|
|
||||||
var openAIModelsMap map[string]OpenAIModels
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var permission []OpenAIModelPermission
|
|
||||||
permission = append(permission, OpenAIModelPermission{
|
|
||||||
Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
|
Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
|
||||||
Object: "model_permission",
|
Object: "model_permission",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
@ -60,7 +38,12 @@ func init() {
|
|||||||
Group: nil,
|
Group: nil,
|
||||||
IsBlocking: false,
|
IsBlocking: false,
|
||||||
})
|
})
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
|
permission := getPermission()
|
||||||
for i := 0; i < relayconstant.APITypeDummy; i++ {
|
for i := 0; i < relayconstant.APITypeDummy; i++ {
|
||||||
if i == relayconstant.APITypeAIProxyLibrary {
|
if i == relayconstant.APITypeAIProxyLibrary {
|
||||||
continue
|
continue
|
||||||
@ -69,7 +52,7 @@ func init() {
|
|||||||
channelName := adaptor.GetChannelName()
|
channelName := adaptor.GetChannelName()
|
||||||
modelNames := adaptor.GetModelList()
|
modelNames := adaptor.GetModelList()
|
||||||
for _, modelName := range modelNames {
|
for _, modelName := range modelNames {
|
||||||
openAIModels = append(openAIModels, OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
@ -81,18 +64,18 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, modelName := range ai360.ModelList {
|
for _, modelName := range ai360.ModelList {
|
||||||
openAIModels = append(openAIModels, OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: "360",
|
OwnedBy: ai360.ChannelName,
|
||||||
Permission: permission,
|
Permission: permission,
|
||||||
Root: modelName,
|
Root: modelName,
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, modelName := range moonshot.ModelList {
|
for _, modelName := range moonshot.ModelList {
|
||||||
openAIModels = append(openAIModels, OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
@ -103,7 +86,7 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, modelName := range lingyiwanwu.ModelList {
|
for _, modelName := range lingyiwanwu.ModelList {
|
||||||
openAIModels = append(openAIModels, OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
@ -114,7 +97,7 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
for modelName, _ := range constant.MidjourneyModel2Action {
|
for modelName, _ := range constant.MidjourneyModel2Action {
|
||||||
openAIModels = append(openAIModels, OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
@ -124,10 +107,21 @@ func init() {
|
|||||||
Parent: nil,
|
Parent: nil,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]OpenAIModels)
|
openAIModelsMap = make(map[string]dto.OpenAIModels)
|
||||||
for _, model := range openAIModels {
|
for _, model := range openAIModels {
|
||||||
openAIModelsMap[model.Id] = model
|
openAIModelsMap[model.Id] = model
|
||||||
}
|
}
|
||||||
|
channelId2Models = make(map[int][]string)
|
||||||
|
for i := 1; i <= common.ChannelTypeDummy; i++ {
|
||||||
|
apiType, success := relayconstant.ChannelType2APIType(i)
|
||||||
|
if !success || apiType == relayconstant.APITypeAIProxyLibrary {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta := &relaycommon.RelayInfo{ChannelType: i}
|
||||||
|
adaptor := relay.GetAdaptor(apiType)
|
||||||
|
adaptor.Init(meta, dto.GeneralOpenAIRequest{})
|
||||||
|
channelId2Models[i] = adaptor.GetModelList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListModels(c *gin.Context) {
|
func ListModels(c *gin.Context) {
|
||||||
@ -141,22 +135,40 @@ func ListModels(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
models := model.GetGroupModels(user.Group)
|
models := model.GetGroupModels(user.Group)
|
||||||
userOpenAiModels := make([]OpenAIModels, 0)
|
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
||||||
|
permission := getPermission()
|
||||||
for _, s := range models {
|
for _, s := range models {
|
||||||
if _, ok := openAIModelsMap[s]; ok {
|
if _, ok := openAIModelsMap[s]; ok {
|
||||||
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
|
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{
|
||||||
"object": "list",
|
"success": true,
|
||||||
"data": userOpenAiModels,
|
"data": userOpenAiModels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChannelListModels(c *gin.Context) {
|
func ChannelListModels(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"object": "list",
|
"success": true,
|
||||||
"data": openAIModels,
|
"data": openAIModels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DashboardListModels(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": channelId2Models,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,3 +188,18 @@ func RetrieveModel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPricing(c *gin.Context) {
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
user, _ := model.GetUserById(userId, true)
|
||||||
|
groupRatio := common.GetGroupRatio("default")
|
||||||
|
if user != nil {
|
||||||
|
groupRatio = common.GetGroupRatio(user.Group)
|
||||||
|
}
|
||||||
|
pricing := model.GetPricing(user, openAIModels)
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": pricing,
|
||||||
|
"group_ratio": groupRatio,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
37
dto/pricing.go
Normal file
37
dto/pricing.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type OpenAIModelPermission struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
AllowCreateEngine bool `json:"allow_create_engine"`
|
||||||
|
AllowSampling bool `json:"allow_sampling"`
|
||||||
|
AllowLogprobs bool `json:"allow_logprobs"`
|
||||||
|
AllowSearchIndices bool `json:"allow_search_indices"`
|
||||||
|
AllowView bool `json:"allow_view"`
|
||||||
|
AllowFineTuning bool `json:"allow_fine_tuning"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Group *string `json:"group"`
|
||||||
|
IsBlocking bool `json:"is_blocking"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIModels struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
OwnedBy string `json:"owned_by"`
|
||||||
|
Permission []OpenAIModelPermission `json:"permission"`
|
||||||
|
Root string `json:"root"`
|
||||||
|
Parent *string `json:"parent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelPricing 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"`
|
||||||
|
}
|
@ -74,6 +74,17 @@ func authHelper(c *gin.Context, minRole int) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryUserAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
if id != nil {
|
||||||
|
c.Set("id", id)
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func UserAuth() func(c *gin.Context) {
|
func UserAuth() func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
authHelper(c, common.RoleCommonUser)
|
authHelper(c, common.RoleCommonUser)
|
||||||
|
@ -29,6 +29,13 @@ func GetGroupModels(group string) []string {
|
|||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetEnabledModels() []string {
|
||||||
|
var models []string
|
||||||
|
// Find distinct models
|
||||||
|
DB.Table("abilities").Where("enabled = ?", true).Distinct("model").Pluck("model", &models)
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
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"
|
||||||
|
@ -24,6 +24,7 @@ type Log struct {
|
|||||||
IsStream bool `json:"is_stream" gorm:"default:false"`
|
IsStream bool `json:"is_stream" gorm:"default:false"`
|
||||||
ChannelId int `json:"channel" gorm:"index"`
|
ChannelId int `json:"channel" gorm:"index"`
|
||||||
TokenId int `json:"token_id" gorm:"default:0;index"`
|
TokenId int `json:"token_id" gorm:"default:0;index"`
|
||||||
|
Other string `json:"other"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -57,12 +58,13 @@ func RecordLog(userId int, logType int, content string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int, isStream bool) {
|
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int, isStream bool, other map[string]interface{}) {
|
||||||
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
|
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
|
||||||
if !common.LogConsumeEnabled {
|
if !common.LogConsumeEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username, _ := CacheGetUsername(userId)
|
username, _ := CacheGetUsername(userId)
|
||||||
|
otherStr := common.MapToJsonStr(other)
|
||||||
log := &Log{
|
log := &Log{
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
Username: username,
|
Username: username,
|
||||||
@ -78,6 +80,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
|
|||||||
TokenId: tokenId,
|
TokenId: tokenId,
|
||||||
UseTime: useTimeSeconds,
|
UseTime: useTimeSeconds,
|
||||||
IsStream: isStream,
|
IsStream: isStream,
|
||||||
|
Other: otherStr,
|
||||||
}
|
}
|
||||||
err := DB.Create(log).Error
|
err := DB.Create(log).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -88,6 +88,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["CompletionRatio"] = common.CompletionRatio2JSONString()
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMap["ChatLink"] = common.ChatLink
|
common.OptionMap["ChatLink"] = common.ChatLink
|
||||||
common.OptionMap["ChatLink2"] = common.ChatLink2
|
common.OptionMap["ChatLink2"] = common.ChatLink2
|
||||||
@ -305,6 +306,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 "CompletionRatio":
|
||||||
|
err = common.UpdateCompletionRatioByJSONString(value)
|
||||||
case "ModelPrice":
|
case "ModelPrice":
|
||||||
err = common.UpdateModelPriceByJSONString(value)
|
err = common.UpdateModelPriceByJSONString(value)
|
||||||
case "TopUpLink":
|
case "TopUpLink":
|
||||||
|
72
model/pricing.go
Normal file
72
model/pricing.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/dto"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pricingMap []dto.ModelPricing
|
||||||
|
lastGetPricingTime time.Time
|
||||||
|
updatePricingLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPricing(user *User, openAIModels []dto.OpenAIModels) []dto.ModelPricing {
|
||||||
|
updatePricingLock.Lock()
|
||||||
|
defer updatePricingLock.Unlock()
|
||||||
|
|
||||||
|
if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
|
||||||
|
updatePricing(openAIModels)
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
userPricingMap := make([]dto.ModelPricing, 0)
|
||||||
|
models := GetGroupModels(user.Group)
|
||||||
|
for _, pricing := range pricingMap {
|
||||||
|
if !common.StringsContains(models, pricing.ModelName) {
|
||||||
|
pricing.Available = false
|
||||||
|
}
|
||||||
|
userPricingMap = append(userPricingMap, pricing)
|
||||||
|
}
|
||||||
|
return userPricingMap
|
||||||
|
}
|
||||||
|
return pricingMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePricing(openAIModels []dto.OpenAIModels) {
|
||||||
|
modelRatios := common.GetModelRatios()
|
||||||
|
enabledModels := GetEnabledModels()
|
||||||
|
allModels := make(map[string]string)
|
||||||
|
for _, openAIModel := range openAIModels {
|
||||||
|
if common.StringsContains(enabledModels, openAIModel.Id) {
|
||||||
|
allModels[openAIModel.Id] = openAIModel.OwnedBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for model, _ := range modelRatios {
|
||||||
|
if common.StringsContains(enabledModels, model) {
|
||||||
|
if _, ok := allModels[model]; !ok {
|
||||||
|
allModels[model] = "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pricingMap = make([]dto.ModelPricing, 0)
|
||||||
|
for model, ownerBy := range allModels {
|
||||||
|
pricing := dto.ModelPricing{
|
||||||
|
Available: true,
|
||||||
|
ModelName: model,
|
||||||
|
OwnerBy: ownerBy,
|
||||||
|
}
|
||||||
|
modelPrice, findPrice := common.GetModelPrice(model, false)
|
||||||
|
if findPrice {
|
||||||
|
pricing.ModelPrice = modelPrice
|
||||||
|
pricing.QuotaType = 1
|
||||||
|
} else {
|
||||||
|
pricing.ModelRatio = common.GetModelRatio(model)
|
||||||
|
pricing.CompletionRatio = common.GetCompletionRatio(model)
|
||||||
|
pricing.QuotaType = 0
|
||||||
|
}
|
||||||
|
pricingMap = append(pricingMap, pricing)
|
||||||
|
}
|
||||||
|
lastGetPricingTime = time.Now()
|
||||||
|
}
|
@ -45,6 +45,7 @@ func logQuotaDataCache(userId int, username string, modelName string, quota int,
|
|||||||
if ok {
|
if ok {
|
||||||
quotaData.Count += 1
|
quotaData.Count += 1
|
||||||
quotaData.Quota += quota
|
quotaData.Quota += quota
|
||||||
|
quotaData.TokenUsed += tokenUsed
|
||||||
} else {
|
} else {
|
||||||
quotaData = &QuotaData{
|
quotaData = &QuotaData{
|
||||||
UserID: userId,
|
UserID: userId,
|
||||||
|
@ -6,3 +6,5 @@ var ModelList = []string{
|
|||||||
"embedding_s1_v1",
|
"embedding_s1_v1",
|
||||||
"semantic_similarity_s1_v1",
|
"semantic_similarity_s1_v1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ChannelName = "ai360"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package ollama
|
package ollama
|
||||||
|
|
||||||
var ModelList []string
|
var ModelList = []string{
|
||||||
|
"llama3-7b",
|
||||||
|
}
|
||||||
|
|
||||||
var ChannelName = "ollama"
|
var ChannelName = "ollama"
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
package openai
|
package openai
|
||||||
|
|
||||||
var ModelList = []string{
|
var ModelList = []string{
|
||||||
"gpt-4o", "gpt-4o-2024-05-13",
|
|
||||||
"gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125",
|
"gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125",
|
||||||
"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613",
|
"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613",
|
||||||
"gpt-3.5-turbo-instruct",
|
"gpt-3.5-turbo-instruct",
|
||||||
"gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview",
|
"gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview",
|
||||||
"gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
|
"gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
|
||||||
"gpt-4-turbo",
|
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||||
"gpt-4-turbo-2024-04-09",
|
|
||||||
"gpt-4-turbo-preview",
|
|
||||||
"gpt-4-vision-preview",
|
"gpt-4-vision-preview",
|
||||||
|
"gpt-4o", "gpt-4o-2024-05-13",
|
||||||
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
|
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
|
||||||
"text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003",
|
"text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003",
|
||||||
"text-moderation-latest", "text-moderation-stable",
|
"text-moderation-latest", "text-moderation-stable",
|
||||||
|
@ -38,7 +38,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|||||||
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
apiType := constant.ChannelType2APIType(channelType)
|
apiType, _ := constant.ChannelType2APIType(channelType)
|
||||||
|
|
||||||
info := &RelayInfo{
|
info := &RelayInfo{
|
||||||
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
|
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
|
||||||
|
@ -24,9 +24,11 @@ const (
|
|||||||
APITypeDummy // this one is only for count, do not add any channel after this
|
APITypeDummy // this one is only for count, do not add any channel after this
|
||||||
)
|
)
|
||||||
|
|
||||||
func ChannelType2APIType(channelType int) int {
|
func ChannelType2APIType(channelType int) (int, bool) {
|
||||||
apiType := APITypeOpenAI
|
apiType := -1
|
||||||
switch channelType {
|
switch channelType {
|
||||||
|
case common.ChannelTypeOpenAI:
|
||||||
|
apiType = APITypeOpenAI
|
||||||
case common.ChannelTypeAnthropic:
|
case common.ChannelTypeAnthropic:
|
||||||
apiType = APITypeAnthropic
|
apiType = APITypeAnthropic
|
||||||
case common.ChannelTypeBaidu:
|
case common.ChannelTypeBaidu:
|
||||||
@ -56,5 +58,8 @@ func ChannelType2APIType(channelType int) int {
|
|||||||
case common.ChannelTypeCohere:
|
case common.ChannelTypeCohere:
|
||||||
apiType = APITypeCohere
|
apiType = APITypeCohere
|
||||||
}
|
}
|
||||||
return apiType
|
if apiType == -1 {
|
||||||
|
return APITypeOpenAI, false
|
||||||
|
}
|
||||||
|
return apiType, true
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,10 @@ func AudioHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
|||||||
if quota != 0 {
|
if quota != 0 {
|
||||||
tokenName := c.GetString("token_name")
|
tokenName := c.GetString("token_name")
|
||||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||||
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false)
|
other := make(map[string]interface{})
|
||||||
|
other["model_ratio"] = modelRatio
|
||||||
|
other["group_ratio"] = groupRatio
|
||||||
|
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false, other)
|
||||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
model.UpdateChannelUsedQuota(channelId, quota)
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
|
@ -106,21 +106,26 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
|
|||||||
requestBody = c.Request.Body
|
requestBody = c.Request.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
modelRatio := common.GetModelRatio(imageRequest.Model)
|
modelPrice, success := common.GetModelPrice(imageRequest.Model, true)
|
||||||
|
if !success {
|
||||||
|
modelRatio := common.GetModelRatio(imageRequest.Model)
|
||||||
|
// modelRatio 16 = modelPrice $0.04
|
||||||
|
// per 1 modelRatio = $0.04 / 16
|
||||||
|
modelPrice = 0.0025 * modelRatio
|
||||||
|
}
|
||||||
groupRatio := common.GetGroupRatio(group)
|
groupRatio := common.GetGroupRatio(group)
|
||||||
ratio := modelRatio * groupRatio
|
|
||||||
userQuota, err := model.CacheGetUserQuota(userId)
|
userQuota, err := model.CacheGetUserQuota(userId)
|
||||||
|
|
||||||
sizeRatio := 1.0
|
sizeRatio := 1.0
|
||||||
// Size
|
// Size
|
||||||
if imageRequest.Size == "256x256" {
|
if imageRequest.Size == "256x256" {
|
||||||
sizeRatio = 1
|
sizeRatio = 0.4
|
||||||
} else if imageRequest.Size == "512x512" {
|
} else if imageRequest.Size == "512x512" {
|
||||||
sizeRatio = 1.125
|
sizeRatio = 0.45
|
||||||
} else if imageRequest.Size == "1024x1024" {
|
} else if imageRequest.Size == "1024x1024" {
|
||||||
sizeRatio = 1.25
|
sizeRatio = 1
|
||||||
} else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
|
} else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
|
||||||
sizeRatio = 2.5
|
sizeRatio = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
qualityRatio := 1.0
|
qualityRatio := 1.0
|
||||||
@ -131,7 +136,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
quota := int(ratio*sizeRatio*qualityRatio*1000) * imageRequest.N
|
quota := int(modelPrice*groupRatio*common.QuotaPerUnit*sizeRatio*qualityRatio) * imageRequest.N
|
||||||
|
|
||||||
if userQuota-quota < 0 {
|
if userQuota-quota < 0 {
|
||||||
return service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
return service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
||||||
@ -190,8 +195,11 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
|
|||||||
if imageRequest.Quality == "hd" {
|
if imageRequest.Quality == "hd" {
|
||||||
quality = "hd"
|
quality = "hd"
|
||||||
}
|
}
|
||||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f, 大小 %s, 品质 %s", modelRatio, groupRatio, imageRequest.Size, quality)
|
logContent := fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f, 大小 %s, 品质 %s", modelPrice, groupRatio, imageRequest.Size, quality)
|
||||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false)
|
other := make(map[string]interface{})
|
||||||
|
other["model_price"] = modelPrice
|
||||||
|
other["group_ratio"] = groupRatio
|
||||||
|
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false, other)
|
||||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
model.UpdateChannelUsedQuota(channelId, quota)
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
|
@ -155,9 +155,9 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
|||||||
return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required")
|
return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required")
|
||||||
}
|
}
|
||||||
modelName := service.CoverActionToModelName(constant.MjActionSwapFace)
|
modelName := service.CoverActionToModelName(constant.MjActionSwapFace)
|
||||||
modelPrice := common.GetModelPrice(modelName, true)
|
modelPrice, success := common.GetModelPrice(modelName, true)
|
||||||
// 如果没有配置价格,则使用默认价格
|
// 如果没有配置价格,则使用默认价格
|
||||||
if modelPrice == -1 {
|
if !success {
|
||||||
defaultPrice, ok := common.DefaultModelPrice[modelName]
|
defaultPrice, ok := common.DefaultModelPrice[modelName]
|
||||||
if !ok {
|
if !ok {
|
||||||
modelPrice = 0.1
|
modelPrice = 0.1
|
||||||
@ -202,7 +202,10 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
|||||||
if quota != 0 {
|
if quota != 0 {
|
||||||
tokenName := c.GetString("token_name")
|
tokenName := c.GetString("token_name")
|
||||||
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, constant.MjActionSwapFace)
|
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, constant.MjActionSwapFace)
|
||||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false)
|
other := make(map[string]interface{})
|
||||||
|
other["model_price"] = modelPrice
|
||||||
|
other["group_ratio"] = groupRatio
|
||||||
|
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
|
||||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
model.UpdateChannelUsedQuota(channelId, quota)
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
@ -451,9 +454,9 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
|||||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||||
|
|
||||||
modelName := service.CoverActionToModelName(midjRequest.Action)
|
modelName := service.CoverActionToModelName(midjRequest.Action)
|
||||||
modelPrice := common.GetModelPrice(modelName, true)
|
modelPrice, success := common.GetModelPrice(modelName, true)
|
||||||
// 如果没有配置价格,则使用默认价格
|
// 如果没有配置价格,则使用默认价格
|
||||||
if modelPrice == -1 {
|
if !success {
|
||||||
defaultPrice, ok := common.DefaultModelPrice[modelName]
|
defaultPrice, ok := common.DefaultModelPrice[modelName]
|
||||||
if !ok {
|
if !ok {
|
||||||
modelPrice = 0.1
|
modelPrice = 0.1
|
||||||
@ -498,7 +501,10 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
|||||||
if quota != 0 {
|
if quota != 0 {
|
||||||
tokenName := c.GetString("token_name")
|
tokenName := c.GetString("token_name")
|
||||||
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, midjRequest.Action)
|
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, midjRequest.Action)
|
||||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false)
|
other := make(map[string]interface{})
|
||||||
|
other["model_price"] = modelPrice
|
||||||
|
other["group_ratio"] = groupRatio
|
||||||
|
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
|
||||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
model.UpdateChannelUsedQuota(channelId, quota)
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
|
@ -91,7 +91,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
relayInfo.UpstreamModelName = textRequest.Model
|
relayInfo.UpstreamModelName = textRequest.Model
|
||||||
modelPrice := common.GetModelPrice(textRequest.Model, false)
|
modelPrice, success := common.GetModelPrice(textRequest.Model, false)
|
||||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||||
|
|
||||||
var preConsumedQuota int
|
var preConsumedQuota int
|
||||||
@ -108,7 +108,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if modelPrice == -1 {
|
if !success {
|
||||||
preConsumedTokens := common.PreConsumedQuota
|
preConsumedTokens := common.PreConsumedQuota
|
||||||
if textRequest.MaxTokens != 0 {
|
if textRequest.MaxTokens != 0 {
|
||||||
preConsumedTokens = promptTokens + int(textRequest.MaxTokens)
|
preConsumedTokens = promptTokens + int(textRequest.MaxTokens)
|
||||||
@ -178,7 +178,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
|
|||||||
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
|
||||||
return openaiErr
|
return openaiErr
|
||||||
}
|
}
|
||||||
postConsumeQuota(c, relayInfo, *textRequest, usage, ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice)
|
postConsumeQuota(c, relayInfo, *textRequest, usage, ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, success)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ func returnPreConsumedQuota(c *gin.Context, tokenId int, userQuota int, preConsu
|
|||||||
|
|
||||||
func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest dto.GeneralOpenAIRequest,
|
func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest dto.GeneralOpenAIRequest,
|
||||||
usage *dto.Usage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
|
usage *dto.Usage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
|
||||||
modelPrice float64) {
|
modelPrice float64, usePrice bool) {
|
||||||
|
|
||||||
useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
|
useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
|
||||||
promptTokens := usage.PromptTokens
|
promptTokens := usage.PromptTokens
|
||||||
@ -267,9 +267,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe
|
|||||||
completionRatio := common.GetCompletionRatio(textRequest.Model)
|
completionRatio := common.GetCompletionRatio(textRequest.Model)
|
||||||
|
|
||||||
quota := 0
|
quota := 0
|
||||||
if modelPrice == -1 {
|
if !usePrice {
|
||||||
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio))
|
||||||
quota = int(float64(quota) * ratio)
|
quota = int(math.Round(float64(quota) * ratio))
|
||||||
if ratio != 0 && quota <= 0 {
|
if ratio != 0 && quota <= 0 {
|
||||||
quota = 1
|
quota = 1
|
||||||
}
|
}
|
||||||
@ -318,7 +318,12 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe
|
|||||||
logModel = "g-*"
|
logModel = "g-*"
|
||||||
logContent += fmt.Sprintf(",模型 %s", textRequest.Model)
|
logContent += fmt.Sprintf(",模型 %s", textRequest.Model)
|
||||||
}
|
}
|
||||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream)
|
other := make(map[string]interface{})
|
||||||
|
other["model_ratio"] = modelRatio
|
||||||
|
other["group_ratio"] = groupRatio
|
||||||
|
other["completion_ratio"] = completionRatio
|
||||||
|
other["model_price"] = modelPrice
|
||||||
|
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
|
||||||
|
|
||||||
//if quota != 0 {
|
//if quota != 0 {
|
||||||
//
|
//
|
||||||
|
@ -14,11 +14,13 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
{
|
{
|
||||||
apiRouter.GET("/status", controller.GetStatus)
|
apiRouter.GET("/status", controller.GetStatus)
|
||||||
|
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
||||||
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
||||||
apiRouter.GET("/notice", controller.GetNotice)
|
apiRouter.GET("/notice", controller.GetNotice)
|
||||||
apiRouter.GET("/about", controller.GetAbout)
|
apiRouter.GET("/about", controller.GetAbout)
|
||||||
apiRouter.GET("/midjourney", controller.GetMidjourney)
|
apiRouter.GET("/midjourney", controller.GetMidjourney)
|
||||||
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
|
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
|
||||||
|
apiRouter.GET("/pricing", middleware.CriticalRateLimit(), middleware.TryUserAuth(), controller.GetPricing)
|
||||||
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
|
@ -63,7 +63,7 @@ func ShouldDisableChannel(err *relaymodel.OpenAIError, statusCode int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShouldEnableChannel(err error, openAIErr *relaymodel.OpenAIError) bool {
|
func ShouldEnableChannel(err error, openAIErr *relaymodel.OpenAIError, status int) bool {
|
||||||
if !common.AutomaticEnableChannelEnabled {
|
if !common.AutomaticEnableChannelEnabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -73,5 +73,8 @@ func ShouldEnableChannel(err error, openAIErr *relaymodel.OpenAIError) bool {
|
|||||||
if openAIErr != nil {
|
if openAIErr != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if status != common.ChannelStatusAutoDisabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,12 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-icons": "^2.46.1",
|
"@douyinfe/semi-icons": "^2.46.1",
|
||||||
"@douyinfe/semi-ui": "^2.46.1",
|
"@douyinfe/semi-ui": "^2.55.3",
|
||||||
"@visactor/react-vchart": "~1.8.8",
|
"@visactor/react-vchart": "~1.8.8",
|
||||||
"@visactor/vchart": "~1.8.8",
|
"@visactor/vchart": "~1.8.8",
|
||||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"dayjs": "^1.11.11",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"marked": "^4.1.1",
|
"marked": "^4.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -23,6 +23,7 @@ import Log from './pages/Log';
|
|||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
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 Detail from './pages/Detail';
|
// import Detail from './pages/Detail';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
@ -228,6 +229,14 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/pricing'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<Pricing />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/about'
|
path='/about'
|
||||||
element={
|
element={
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import EditChannel from '../pages/Channel/EditChannel';
|
import EditChannel from '../pages/Channel/EditChannel';
|
||||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
||||||
|
import { loadChannelModels } from './utils.js';
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return <>{timestamp2string(timestamp)}</>;
|
return <>{timestamp2string(timestamp)}</>;
|
||||||
@ -397,6 +398,7 @@ const ChannelsTable = () => {
|
|||||||
showError(reason);
|
showError(reason);
|
||||||
});
|
});
|
||||||
fetchGroups().then();
|
fetchGroups().then();
|
||||||
|
loadChannelModels().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const manageChannel = async (id, action, record, value) => {
|
const manageChannel = async (id, action, record, value) => {
|
||||||
|
@ -20,6 +20,7 @@ import TelegramLoginButton from 'react-telegram-login';
|
|||||||
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||||
import LinuxDoIcon from './LinuxDoIcon';
|
import LinuxDoIcon from './LinuxDoIcon';
|
||||||
import WeChatIcon from './WeChatIcon';
|
import WeChatIcon from './WeChatIcon';
|
||||||
|
import { setUserData } from '../helpers/data.js';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@ -100,7 +101,7 @@ const LoginForm = () => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
userDispatch({ type: 'login', payload: data });
|
userDispatch({ type: 'login', payload: data });
|
||||||
localStorage.setItem('user', JSON.stringify(data));
|
setUserData(data);
|
||||||
showSuccess('登录成功!');
|
showSuccess('登录成功!');
|
||||||
if (username === 'root' && password === '123456') {
|
if (username === 'root' && password === '123456') {
|
||||||
Modal.error({
|
Modal.error({
|
||||||
|
@ -19,9 +19,15 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
|
Tooltip,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderNumber, renderQuota, stringToColor } from '../helpers/render';
|
import {
|
||||||
|
renderModelPrice,
|
||||||
|
renderNumber,
|
||||||
|
renderQuota,
|
||||||
|
stringToColor,
|
||||||
|
} from '../helpers/render';
|
||||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
@ -292,16 +298,42 @@ const LogsTable = () => {
|
|||||||
title: '详情',
|
title: '详情',
|
||||||
dataIndex: 'content',
|
dataIndex: 'content',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
|
if (record.other === '') {
|
||||||
|
return (
|
||||||
|
<Paragraph
|
||||||
|
ellipsis={{
|
||||||
|
rows: 2,
|
||||||
|
showTooltip: {
|
||||||
|
type: 'popover',
|
||||||
|
opts: { style: { width: 240 } },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ maxWidth: 240 }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let other = JSON.parse(record.other);
|
||||||
|
let content = renderModelPrice(
|
||||||
|
record.prompt_tokens,
|
||||||
|
record.completion_tokens,
|
||||||
|
other.model_ratio,
|
||||||
|
other.model_price,
|
||||||
|
other.completion_ratio,
|
||||||
|
other.group_ratio,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Tooltip content={content}>
|
||||||
ellipsis={{
|
<Paragraph
|
||||||
rows: 2,
|
ellipsis={{
|
||||||
showTooltip: { type: 'popover', opts: { style: { width: 240 } } },
|
rows: 2,
|
||||||
}}
|
}}
|
||||||
style={{ maxWidth: 240 }}
|
style={{ maxWidth: 240 }}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
229
web/src/components/ModelPricing.js
Normal file
229
web/src/components/ModelPricing.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { API, copy, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
|
import { Banner, Layout, Modal, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { stringToColor } from '../helpers/render.js';
|
||||||
|
import { UserContext } from '../context/User/index.js';
|
||||||
|
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||||
|
|
||||||
|
function renderQuotaType(type) {
|
||||||
|
// Ensure all cases are string literals by adding quotes.
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Tag color='green' size='large'>
|
||||||
|
按次计费
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Tag color='blue' size='large'>
|
||||||
|
按量计费
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='white' size='large'>
|
||||||
|
未知
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAvailable(available) {
|
||||||
|
return available ? (
|
||||||
|
<Tag color='green' size='large'>
|
||||||
|
可用
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tooltip content='您所在的分组不可用'>
|
||||||
|
<Tag color='red' size='large'>
|
||||||
|
不可用
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelPricing = () => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '可用性',
|
||||||
|
dataIndex: 'available',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return renderAvailable(text);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提供者',
|
||||||
|
dataIndex: 'owner_by',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tag color={stringToColor(text)} size='large'>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模型名称',
|
||||||
|
dataIndex: 'model_name', // 以finish_time作为dataIndex
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tag
|
||||||
|
color={stringToColor(record.owner_by)}
|
||||||
|
size='large'
|
||||||
|
onClick={() => {
|
||||||
|
copyText(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '计费类型',
|
||||||
|
dataIndex: 'quota_type',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return renderQuotaType(parseInt(text));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模型倍率',
|
||||||
|
dataIndex: 'model_ratio',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return <div>{record.quota_type === 0 ? text : 'N/A'}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '补全倍率',
|
||||||
|
dataIndex: 'completion_ratio',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
let ratio = parseFloat(text.toFixed(3));
|
||||||
|
return <div>{record.quota_type === 0 ? ratio : 'N/A'}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模型价格',
|
||||||
|
dataIndex: 'model_price',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
let content = text;
|
||||||
|
if (record.quota_type === 0) {
|
||||||
|
let inputRatioPrice = record.model_ratio * 2.0 * record.group_ratio;
|
||||||
|
let completionRatioPrice =
|
||||||
|
record.model_ratio *
|
||||||
|
record.completion_ratio *
|
||||||
|
2.0 *
|
||||||
|
record.group_ratio;
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
|
||||||
|
<br />
|
||||||
|
<Text>补全 ${completionRatioPrice} / 1M tokens</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let price = parseFloat(text) * record.group_ratio;
|
||||||
|
content = <>模型价格:${price}</>;
|
||||||
|
}
|
||||||
|
return <div>{content}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [models, setModels] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [groupRatio, setGroupRatio] = useState(1);
|
||||||
|
|
||||||
|
const setModelsFormat = (models, groupRatio) => {
|
||||||
|
for (let i = 0; i < models.length; i++) {
|
||||||
|
models[i].key = i;
|
||||||
|
models[i].group_ratio = groupRatio;
|
||||||
|
}
|
||||||
|
// sort by quota_type
|
||||||
|
models.sort((a, b) => {
|
||||||
|
return a.quota_type - b.quota_type;
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort by owner_by, openai is max, other use localeCompare
|
||||||
|
models.sort((a, b) => {
|
||||||
|
if (a.owner_by === 'openai') {
|
||||||
|
return -1;
|
||||||
|
} else if (b.owner_by === 'openai') {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return a.owner_by.localeCompare(b.owner_by);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setModels(models);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPricing = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
let url = '';
|
||||||
|
url = `/api/pricing`;
|
||||||
|
const res = await API.get(url);
|
||||||
|
const { success, message, data, group_ratio } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setGroupRatio(group_ratio);
|
||||||
|
setModelsFormat(data, group_ratio);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
await loadPricing();
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyText = async (text) => {
|
||||||
|
if (await copy(text)) {
|
||||||
|
showSuccess('已复制:' + text);
|
||||||
|
} else {
|
||||||
|
// setSearchKeyword(text);
|
||||||
|
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Layout>
|
||||||
|
{userState.user ? (
|
||||||
|
<Banner
|
||||||
|
type='info'
|
||||||
|
description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Banner
|
||||||
|
type='warning'
|
||||||
|
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Table
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={models}
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
pageSize: models.length,
|
||||||
|
showSizeChanger: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelPricing;
|
@ -1,17 +1,17 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import {
|
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
|
||||||
API,
|
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
|
||||||
showError,
|
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||||
showSuccess,
|
import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
|
||||||
timestamp2string,
|
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||||
verifyJSON,
|
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
|
||||||
} from '../helpers';
|
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||||
|
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
|
||||||
|
|
||||||
import { useTheme } from '../context/Theme';
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
const OperationSetting = () => {
|
const OperationSetting = () => {
|
||||||
let now = new Date();
|
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
QuotaForNewUser: 0,
|
QuotaForNewUser: 0,
|
||||||
QuotaForInviter: 0,
|
QuotaForInviter: 0,
|
||||||
@ -20,45 +20,38 @@ const OperationSetting = () => {
|
|||||||
PreConsumedQuota: 0,
|
PreConsumedQuota: 0,
|
||||||
StreamCacheQueueLength: 0,
|
StreamCacheQueueLength: 0,
|
||||||
ModelRatio: '',
|
ModelRatio: '',
|
||||||
|
CompletionRatio: '',
|
||||||
ModelPrice: '',
|
ModelPrice: '',
|
||||||
GroupRatio: '',
|
GroupRatio: '',
|
||||||
TopUpLink: '',
|
TopUpLink: '',
|
||||||
ChatLink: '',
|
ChatLink: '',
|
||||||
ChatLink2: '', // 添加的新状态变量
|
ChatLink2: '', // 添加的新状态变量
|
||||||
QuotaPerUnit: 0,
|
QuotaPerUnit: 0,
|
||||||
AutomaticDisableChannelEnabled: '',
|
AutomaticDisableChannelEnabled: false,
|
||||||
AutomaticEnableChannelEnabled: '',
|
AutomaticEnableChannelEnabled: false,
|
||||||
ChannelDisableThreshold: 0,
|
ChannelDisableThreshold: 0,
|
||||||
LogConsumeEnabled: '',
|
LogConsumeEnabled: false,
|
||||||
DisplayInCurrencyEnabled: '',
|
DisplayInCurrencyEnabled: false,
|
||||||
DisplayTokenStatEnabled: '',
|
DisplayTokenStatEnabled: false,
|
||||||
CheckSensitiveEnabled: '',
|
CheckSensitiveEnabled: false,
|
||||||
CheckSensitiveOnPromptEnabled: '',
|
CheckSensitiveOnPromptEnabled: false,
|
||||||
CheckSensitiveOnCompletionEnabled: '',
|
CheckSensitiveOnCompletionEnabled: '',
|
||||||
StopOnSensitiveEnabled: '',
|
StopOnSensitiveEnabled: '',
|
||||||
SensitiveWords: '',
|
SensitiveWords: '',
|
||||||
MjNotifyEnabled: '',
|
MjNotifyEnabled: false,
|
||||||
MjAccountFilterEnabled: '',
|
MjAccountFilterEnabled: false,
|
||||||
MjModeClearEnabled: '',
|
MjModeClearEnabled: false,
|
||||||
MjForwardUrlEnabled: '',
|
MjForwardUrlEnabled: false,
|
||||||
DrawingEnabled: '',
|
DrawingEnabled: false,
|
||||||
DataExportEnabled: '',
|
DataExportEnabled: false,
|
||||||
DataExportDefaultTime: 'hour',
|
DataExportDefaultTime: 'hour',
|
||||||
DataExportInterval: 5,
|
DataExportInterval: 5,
|
||||||
DefaultCollapseSidebar: '', // 默认折叠侧边栏
|
DefaultCollapseSidebar: false, // 默认折叠侧边栏
|
||||||
RetryTimes: 0,
|
RetryTimes: 0,
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
let [historyTimestamp, setHistoryTimestamp] = useState(
|
|
||||||
timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600),
|
|
||||||
); // a month ago
|
|
||||||
// 精确时间选项(小时,天,周)
|
|
||||||
const timeOptions = [
|
|
||||||
{ key: 'hour', text: '小时', value: 'hour' },
|
|
||||||
{ key: 'day', text: '天', value: 'day' },
|
|
||||||
{ key: 'week', text: '周', value: 'week' },
|
|
||||||
];
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
@ -68,552 +61,79 @@ const OperationSetting = () => {
|
|||||||
if (
|
if (
|
||||||
item.key === 'ModelRatio' ||
|
item.key === 'ModelRatio' ||
|
||||||
item.key === 'GroupRatio' ||
|
item.key === 'GroupRatio' ||
|
||||||
|
item.key === 'CompletionRatio' ||
|
||||||
item.key === 'ModelPrice'
|
item.key === 'ModelPrice'
|
||||||
) {
|
) {
|
||||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||||
}
|
}
|
||||||
newInputs[item.key] = item.value;
|
if (
|
||||||
|
item.key.endsWith('Enabled') ||
|
||||||
|
['DefaultCollapseSidebar'].includes(item.key)
|
||||||
|
) {
|
||||||
|
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||||
|
} else {
|
||||||
|
newInputs[item.key] = item.value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
setOriginInputs(newInputs);
|
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
async function onRefresh() {
|
||||||
const theme = useTheme();
|
try {
|
||||||
const isDark = theme === 'dark';
|
setLoading(true);
|
||||||
|
await getOptions();
|
||||||
|
showSuccess('刷新成功');
|
||||||
|
} catch (error) {
|
||||||
|
showError('刷新失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getOptions().then();
|
onRefresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateOption = async (key, value) => {
|
|
||||||
setLoading(true);
|
|
||||||
if (key.endsWith('Enabled')) {
|
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
|
||||||
}
|
|
||||||
if (key === 'DefaultCollapseSidebar') {
|
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
|
||||||
}
|
|
||||||
console.log(key, value);
|
|
||||||
const res = await API.put('/api/option/', {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = async (e, { name, value }) => {
|
|
||||||
if (
|
|
||||||
name.endsWith('Enabled') ||
|
|
||||||
name === 'DataExportInterval' ||
|
|
||||||
name === 'DataExportDefaultTime' ||
|
|
||||||
name === 'DefaultCollapseSidebar'
|
|
||||||
) {
|
|
||||||
if (name === 'DataExportDefaultTime') {
|
|
||||||
localStorage.setItem('data_export_default_time', value);
|
|
||||||
} else if (name === 'MjNotifyEnabled') {
|
|
||||||
localStorage.setItem('mj_notify_enabled', value);
|
|
||||||
}
|
|
||||||
await updateOption(name, value);
|
|
||||||
} else {
|
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitConfig = async (group) => {
|
|
||||||
switch (group) {
|
|
||||||
case 'monitor':
|
|
||||||
if (
|
|
||||||
originInputs['ChannelDisableThreshold'] !==
|
|
||||||
inputs.ChannelDisableThreshold
|
|
||||||
) {
|
|
||||||
await updateOption(
|
|
||||||
'ChannelDisableThreshold',
|
|
||||||
inputs.ChannelDisableThreshold,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
|
|
||||||
) {
|
|
||||||
await updateOption(
|
|
||||||
'QuotaRemindThreshold',
|
|
||||||
inputs.QuotaRemindThreshold,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ratio':
|
|
||||||
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
|
||||||
if (!verifyJSON(inputs.ModelRatio)) {
|
|
||||||
showError('模型倍率不是合法的 JSON 字符串');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await updateOption('ModelRatio', inputs.ModelRatio);
|
|
||||||
}
|
|
||||||
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
|
|
||||||
if (!verifyJSON(inputs.GroupRatio)) {
|
|
||||||
showError('分组倍率不是合法的 JSON 字符串');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await updateOption('GroupRatio', inputs.GroupRatio);
|
|
||||||
}
|
|
||||||
if (originInputs['ModelPrice'] !== inputs.ModelPrice) {
|
|
||||||
if (!verifyJSON(inputs.ModelPrice)) {
|
|
||||||
showError('模型固定价格不是合法的 JSON 字符串');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await updateOption('ModelPrice', inputs.ModelPrice);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'words':
|
|
||||||
if (originInputs['SensitiveWords'] !== inputs.SensitiveWords) {
|
|
||||||
await updateOption('SensitiveWords', inputs.SensitiveWords);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'quota':
|
|
||||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
|
||||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
|
||||||
}
|
|
||||||
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
|
|
||||||
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
|
|
||||||
}
|
|
||||||
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
|
|
||||||
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
|
|
||||||
}
|
|
||||||
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
|
|
||||||
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'general':
|
|
||||||
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
|
||||||
await updateOption('TopUpLink', inputs.TopUpLink);
|
|
||||||
}
|
|
||||||
if (originInputs['ChatLink'] !== inputs.ChatLink) {
|
|
||||||
await updateOption('ChatLink', inputs.ChatLink);
|
|
||||||
}
|
|
||||||
if (originInputs['ChatLink2'] !== inputs.ChatLink2) {
|
|
||||||
await updateOption('ChatLink2', inputs.ChatLink2);
|
|
||||||
}
|
|
||||||
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
|
||||||
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
|
||||||
}
|
|
||||||
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
|
|
||||||
await updateOption('RetryTimes', inputs.RetryTimes);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteHistoryLogs = async () => {
|
|
||||||
console.log(inputs);
|
|
||||||
const res = await API.delete(
|
|
||||||
`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`,
|
|
||||||
);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess(`${data} 条日志已清理!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showError('日志清理失败:' + message);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Grid columns={1}>
|
<>
|
||||||
<Grid.Column>
|
<Spin spinning={loading} size='large'>
|
||||||
<Form loading={loading} inverted={isDark}>
|
{/* 通用设置 */}
|
||||||
<Header as='h3' inverted={isDark}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
通用设置
|
<SettingsGeneral options={inputs} refresh={onRefresh} />
|
||||||
</Header>
|
</Card>
|
||||||
<Form.Group widths={4}>
|
{/* 绘图设置 */}
|
||||||
<Form.Input
|
<Card style={{ marginTop: '10px' }}>
|
||||||
label='充值链接'
|
<SettingsDrawing options={inputs} refresh={onRefresh} />
|
||||||
name='TopUpLink'
|
</Card>
|
||||||
onChange={handleInputChange}
|
{/* 屏蔽词过滤设置 */}
|
||||||
autoComplete='new-password'
|
<Card style={{ marginTop: '10px' }}>
|
||||||
value={inputs.TopUpLink}
|
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />
|
||||||
type='link'
|
</Card>
|
||||||
placeholder='例如发卡网站的购买链接'
|
{/* 日志设置 */}
|
||||||
/>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<Form.Input
|
<SettingsLog options={inputs} refresh={onRefresh} />
|
||||||
label='默认聊天页面链接'
|
</Card>
|
||||||
name='ChatLink'
|
{/* 数据看板 */}
|
||||||
onChange={handleInputChange}
|
<Card style={{ marginTop: '10px' }}>
|
||||||
autoComplete='new-password'
|
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
|
||||||
value={inputs.ChatLink}
|
</Card>
|
||||||
type='link'
|
{/* 监控设置 */}
|
||||||
placeholder='例如 ChatGPT Next Web 的部署地址'
|
<Card style={{ marginTop: '10px' }}>
|
||||||
/>
|
<SettingsMonitoring options={inputs} refresh={onRefresh} />
|
||||||
<Form.Input
|
</Card>
|
||||||
label='聊天页面2链接'
|
{/* 额度设置 */}
|
||||||
name='ChatLink2'
|
<Card style={{ marginTop: '10px' }}>
|
||||||
onChange={handleInputChange}
|
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
||||||
autoComplete='new-password'
|
</Card>
|
||||||
value={inputs.ChatLink2}
|
{/* 倍率设置 */}
|
||||||
type='link'
|
<Card style={{ marginTop: '10px' }}>
|
||||||
placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
|
<SettingsMagnification options={inputs} refresh={onRefresh} />
|
||||||
/>
|
</Card>
|
||||||
<Form.Input
|
</Spin>
|
||||||
label='单位美元额度'
|
</>
|
||||||
name='QuotaPerUnit'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.QuotaPerUnit}
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
placeholder='一单位货币能兑换的额度'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='失败重试次数'
|
|
||||||
name='RetryTimes'
|
|
||||||
type={'number'}
|
|
||||||
step='1'
|
|
||||||
min='0'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.RetryTimes}
|
|
||||||
placeholder='失败重试次数'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.DisplayInCurrencyEnabled === 'true'}
|
|
||||||
label='以货币形式显示额度'
|
|
||||||
name='DisplayInCurrencyEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
|
||||||
label='Billing 相关 API 显示令牌额度而非用户额度'
|
|
||||||
name='DisplayTokenStatEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.DefaultCollapseSidebar === 'true'}
|
|
||||||
label='默认折叠侧边栏'
|
|
||||||
name='DefaultCollapseSidebar'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
submitConfig('general').then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存通用设置
|
|
||||||
</Form.Button>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
绘图设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.DrawingEnabled === 'true'}
|
|
||||||
label='启用绘图功能'
|
|
||||||
name='DrawingEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.MjNotifyEnabled === 'true'}
|
|
||||||
label='允许回调(会泄露服务器ip地址)'
|
|
||||||
name='MjNotifyEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.MjAccountFilterEnabled === 'true'}
|
|
||||||
label='允许AccountFilter参数'
|
|
||||||
name='MjAccountFilterEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.MjForwardUrlEnabled === 'true'}
|
|
||||||
label='开启之后将上游地址替换为服务器地址'
|
|
||||||
name='MjForwardUrlEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.MjModeClearEnabled === 'true'}
|
|
||||||
label='开启之后会清除用户提示词中的--fast、--relax以及--turbo参数'
|
|
||||||
name='MjModeClearEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
屏蔽词过滤设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.CheckSensitiveEnabled === 'true'}
|
|
||||||
label='启用屏蔽词过滤功能'
|
|
||||||
name='CheckSensitiveEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
|
|
||||||
label='启用prompt检查'
|
|
||||||
name='CheckSensitiveOnPromptEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
{/*<Form.Checkbox*/}
|
|
||||||
{/* checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
|
|
||||||
{/* label='启用生成内容检查'*/}
|
|
||||||
{/* name='CheckSensitiveOnCompletionEnabled'*/}
|
|
||||||
{/* onChange={handleInputChange}*/}
|
|
||||||
{/*/>*/}
|
|
||||||
</Form.Group>
|
|
||||||
{/*<Form.Group inline>*/}
|
|
||||||
{/* <Form.Checkbox*/}
|
|
||||||
{/* checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
|
|
||||||
{/* label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
|
|
||||||
{/* name='StopOnSensitiveEnabled'*/}
|
|
||||||
{/* onChange={handleInputChange}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/*</Form.Group>*/}
|
|
||||||
{/*<Form.Group>*/}
|
|
||||||
{/* <Form.Input*/}
|
|
||||||
{/* label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
|
|
||||||
{/* name="StreamCacheTextLength"*/}
|
|
||||||
{/* onChange={handleInputChange}*/}
|
|
||||||
{/* value={inputs.StreamCacheQueueLength}*/}
|
|
||||||
{/* type="number"*/}
|
|
||||||
{/* min="0"*/}
|
|
||||||
{/* placeholder="例如:10"*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/*</Form.Group>*/}
|
|
||||||
<Form.Group widths='equal'>
|
|
||||||
<Form.TextArea
|
|
||||||
label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
|
|
||||||
name='SensitiveWords'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
||||||
value={inputs.SensitiveWords}
|
|
||||||
placeholder='一行一个屏蔽词'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
submitConfig('words').then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存屏蔽词设置
|
|
||||||
</Form.Button>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
日志设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.LogConsumeEnabled === 'true'}
|
|
||||||
label='启用额度消费日志记录'
|
|
||||||
name='LogConsumeEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group widths={4}>
|
|
||||||
<Form.Input
|
|
||||||
label='目标时间'
|
|
||||||
value={historyTimestamp}
|
|
||||||
type='datetime-local'
|
|
||||||
name='history_timestamp'
|
|
||||||
onChange={(e, { name, value }) => {
|
|
||||||
setHistoryTimestamp(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
deleteHistoryLogs().then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
清理历史日志
|
|
||||||
</Form.Button>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
数据看板
|
|
||||||
</Header>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.DataExportEnabled === 'true'}
|
|
||||||
label='启用数据看板(实验性)'
|
|
||||||
name='DataExportEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Input
|
|
||||||
label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
|
|
||||||
name='DataExportInterval'
|
|
||||||
type={'number'}
|
|
||||||
step='1'
|
|
||||||
min='1'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.DataExportInterval}
|
|
||||||
placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
|
|
||||||
/>
|
|
||||||
<Form.Select
|
|
||||||
label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
|
|
||||||
options={timeOptions}
|
|
||||||
name='DataExportDefaultTime'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.DataExportDefaultTime}
|
|
||||||
placeholder='数据看板默认时间粒度'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
监控设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths={3}>
|
|
||||||
<Form.Input
|
|
||||||
label='最长响应时间'
|
|
||||||
name='ChannelDisableThreshold'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.ChannelDisableThreshold}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='额度提醒阈值'
|
|
||||||
name='QuotaRemindThreshold'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.QuotaRemindThreshold}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='低于此额度时将发送邮件提醒用户'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group inline>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
|
||||||
label='失败时自动禁用通道'
|
|
||||||
name='AutomaticDisableChannelEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Form.Checkbox
|
|
||||||
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
|
|
||||||
label='成功时自动启用通道'
|
|
||||||
name='AutomaticEnableChannelEnabled'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
submitConfig('monitor').then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存监控设置
|
|
||||||
</Form.Button>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
额度设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths={4}>
|
|
||||||
<Form.Input
|
|
||||||
label='新用户初始额度'
|
|
||||||
name='QuotaForNewUser'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.QuotaForNewUser}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:100'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='请求预扣费额度'
|
|
||||||
name='PreConsumedQuota'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.PreConsumedQuota}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='请求结束后多退少补'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='邀请新用户奖励额度'
|
|
||||||
name='QuotaForInviter'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.QuotaForInviter}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:2000'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='新用户使用邀请码奖励额度'
|
|
||||||
name='QuotaForInvitee'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.QuotaForInvitee}
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:1000'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
submitConfig('quota').then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存额度设置
|
|
||||||
</Form.Button>
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3' inverted={isDark}>
|
|
||||||
倍率设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths='equal'>
|
|
||||||
<Form.TextArea
|
|
||||||
label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
|
|
||||||
name='ModelPrice'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.ModelPrice}
|
|
||||||
placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1(或 "g-*": 0.1),一次消耗0.1刀'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group widths='equal'>
|
|
||||||
<Form.TextArea
|
|
||||||
label='模型倍率'
|
|
||||||
name='ModelRatio'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.ModelRatio}
|
|
||||||
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group widths='equal'>
|
|
||||||
<Form.TextArea
|
|
||||||
label='分组倍率'
|
|
||||||
name='GroupRatio'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={inputs.GroupRatio}
|
|
||||||
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Button
|
|
||||||
onClick={() => {
|
|
||||||
submitConfig('ratio').then();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保存倍率设置
|
|
||||||
</Form.Button>
|
|
||||||
</Form>
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,10 +23,12 @@ import {
|
|||||||
IconImage,
|
IconImage,
|
||||||
IconKey,
|
IconKey,
|
||||||
IconLayers,
|
IconLayers,
|
||||||
|
IconPriceTag,
|
||||||
IconSetting,
|
IconSetting,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Layout, Nav } from '@douyinfe/semi-ui';
|
import { Layout, Nav } from '@douyinfe/semi-ui';
|
||||||
|
import { setStatusData } from '../helpers/data.js';
|
||||||
|
|
||||||
// HeaderBar Buttons
|
// HeaderBar Buttons
|
||||||
|
|
||||||
@ -55,6 +57,7 @@ const SiderBar = () => {
|
|||||||
about: '/about',
|
about: '/about',
|
||||||
chat: '/chat',
|
chat: '/chat',
|
||||||
detail: '/detail',
|
detail: '/detail',
|
||||||
|
pricing: '/pricing',
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerButtons = useMemo(
|
const headerButtons = useMemo(
|
||||||
@ -100,6 +103,12 @@ const SiderBar = () => {
|
|||||||
to: '/topup',
|
to: '/topup',
|
||||||
icon: <IconCreditCard />,
|
icon: <IconCreditCard />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: '模型价格',
|
||||||
|
itemKey: 'pricing',
|
||||||
|
to: '/pricing',
|
||||||
|
icon: <IconPriceTag />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: '用户管理',
|
text: '用户管理',
|
||||||
itemKey: 'user',
|
itemKey: 'user',
|
||||||
@ -161,34 +170,8 @@ const SiderBar = () => {
|
|||||||
}
|
}
|
||||||
const { success, data } = res.data;
|
const { success, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem('status', JSON.stringify(data));
|
|
||||||
statusDispatch({ type: 'set', payload: data });
|
statusDispatch({ type: 'set', payload: data });
|
||||||
localStorage.setItem('system_name', data.system_name);
|
setStatusData(data);
|
||||||
localStorage.setItem('logo', data.logo);
|
|
||||||
localStorage.setItem('footer_html', data.footer_html);
|
|
||||||
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
|
||||||
localStorage.setItem('display_in_currency', data.display_in_currency);
|
|
||||||
localStorage.setItem('enable_drawing', data.enable_drawing);
|
|
||||||
localStorage.setItem('enable_data_export', data.enable_data_export);
|
|
||||||
localStorage.setItem(
|
|
||||||
'data_export_default_time',
|
|
||||||
data.data_export_default_time,
|
|
||||||
);
|
|
||||||
localStorage.setItem(
|
|
||||||
'default_collapse_sidebar',
|
|
||||||
data.default_collapse_sidebar,
|
|
||||||
);
|
|
||||||
localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
|
|
||||||
if (data.chat_link) {
|
|
||||||
localStorage.setItem('chat_link', data.chat_link);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('chat_link');
|
|
||||||
}
|
|
||||||
if (data.chat_link2) {
|
|
||||||
localStorage.setItem('chat_link2', data.chat_link2);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('chat_link2');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showError('无法正常连接至服务器!');
|
showError('无法正常连接至服务器!');
|
||||||
}
|
}
|
||||||
|
@ -22,3 +22,32 @@ export async function onLinuxDoOAuthClicked(linuxdo_client_id) {
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
location.href = `https://connect.linux.do/oauth2/authorize?client_id=${linuxdo_client_id}&response_type=code&state=${state}&scope=user:profile`;
|
location.href = `https://connect.linux.do/oauth2/authorize?client_id=${linuxdo_client_id}&response_type=code&state=${state}&scope=user:profile`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let channelModels = undefined;
|
||||||
|
export async function loadChannelModels() {
|
||||||
|
const res = await API.get('/api/models');
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channelModels = data;
|
||||||
|
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChannelModels(type) {
|
||||||
|
if (channelModels !== undefined && type in channelModels) {
|
||||||
|
if (!channelModels[type]) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return channelModels[type];
|
||||||
|
}
|
||||||
|
let models = localStorage.getItem('channel_models');
|
||||||
|
if (!models) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
channelModels = JSON.parse(models);
|
||||||
|
if (type in channelModels) {
|
||||||
|
return channelModels[type];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
@ -86,13 +86,13 @@ export const CHANNEL_OPTIONS = [
|
|||||||
label: '智谱 ChatGLM',
|
label: '智谱 ChatGLM',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 16,
|
key: 26,
|
||||||
text: '智谱 GLM-4V',
|
text: '智谱 GLM-4V',
|
||||||
value: 26,
|
value: 26,
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
label: '智谱 GLM-4V',
|
label: '智谱 GLM-4V',
|
||||||
},
|
},
|
||||||
{ key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
|
{ key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
|
||||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
|
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
|
||||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
|
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
|
||||||
{ key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' },
|
{ key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' },
|
||||||
|
33
web/src/helpers/data.js
Normal file
33
web/src/helpers/data.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export function setStatusData(data) {
|
||||||
|
localStorage.setItem('status', JSON.stringify(data));
|
||||||
|
localStorage.setItem('system_name', data.system_name);
|
||||||
|
localStorage.setItem('logo', data.logo);
|
||||||
|
localStorage.setItem('footer_html', data.footer_html);
|
||||||
|
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
||||||
|
localStorage.setItem('display_in_currency', data.display_in_currency);
|
||||||
|
localStorage.setItem('enable_drawing', data.enable_drawing);
|
||||||
|
localStorage.setItem('enable_data_export', data.enable_data_export);
|
||||||
|
localStorage.setItem(
|
||||||
|
'data_export_default_time',
|
||||||
|
data.data_export_default_time,
|
||||||
|
);
|
||||||
|
localStorage.setItem(
|
||||||
|
'default_collapse_sidebar',
|
||||||
|
data.default_collapse_sidebar,
|
||||||
|
);
|
||||||
|
localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
|
||||||
|
if (data.chat_link) {
|
||||||
|
localStorage.setItem('chat_link', data.chat_link);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('chat_link');
|
||||||
|
}
|
||||||
|
if (data.chat_link2) {
|
||||||
|
localStorage.setItem('chat_link2', data.chat_link2);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('chat_link2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserData(data) {
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { Label } from 'semantic-ui-react';
|
|
||||||
import { Tag } from '@douyinfe/semi-ui';
|
import { Tag } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
export function renderText(text, limit) {
|
export function renderText(text, limit) {
|
||||||
@ -135,6 +134,43 @@ export function renderQuota(quota, digits = 2) {
|
|||||||
return renderNumber(quota);
|
return renderNumber(quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderModelPrice(
|
||||||
|
inputTokens,
|
||||||
|
completionTokens,
|
||||||
|
modelRatio,
|
||||||
|
modelPrice = -1,
|
||||||
|
completionRatio,
|
||||||
|
groupRatio,
|
||||||
|
) {
|
||||||
|
// 1 ratio = $0.002 / 1K tokens
|
||||||
|
if (modelPrice !== -1) {
|
||||||
|
return '模型价格:$' + modelPrice * groupRatio;
|
||||||
|
} else {
|
||||||
|
if (completionRatio === undefined) {
|
||||||
|
completionRatio = 0;
|
||||||
|
}
|
||||||
|
let inputRatioPrice = modelRatio * 2.0 * groupRatio;
|
||||||
|
let completionRatioPrice = modelRatio * completionRatio * 2.0 * groupRatio;
|
||||||
|
let price =
|
||||||
|
(inputTokens / 1000000) * inputRatioPrice +
|
||||||
|
(completionTokens / 1000000) * completionRatioPrice;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<article>
|
||||||
|
<p>提示 ${inputRatioPrice} / 1M tokens</p>
|
||||||
|
<p>补全 ${completionRatioPrice} / 1M tokens</p>
|
||||||
|
<p></p>
|
||||||
|
<p>
|
||||||
|
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
|
||||||
|
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $
|
||||||
|
{price.toFixed(6)}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function renderQuotaWithPrompt(quota, digits) {
|
export function renderQuotaWithPrompt(quota, digits) {
|
||||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||||
displayInCurrency = displayInCurrency === 'true';
|
displayInCurrency = displayInCurrency === 'true';
|
||||||
|
@ -224,3 +224,28 @@ export function shouldShowPrompt(id) {
|
|||||||
export function setPromptShown(id) {
|
export function setPromptShown(id) {
|
||||||
localStorage.setItem(`prompt-${id}`, 'true');
|
localStorage.setItem(`prompt-${id}`, 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个对象的属性,找出有变化的属性,并返回包含变化属性信息的数组
|
||||||
|
* @param {Object} oldObject - 旧对象
|
||||||
|
* @param {Object} newObject - 新对象
|
||||||
|
* @return {Array} 包含变化属性信息的数组,每个元素是一个对象,包含 key, oldValue 和 newValue
|
||||||
|
*/
|
||||||
|
export function compareObjects(oldObject, newObject) {
|
||||||
|
const changedProperties = [];
|
||||||
|
|
||||||
|
// 比较两个对象的属性
|
||||||
|
for (const key in oldObject) {
|
||||||
|
if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
|
||||||
|
if (oldObject[key] !== newObject[key]) {
|
||||||
|
changedProperties.push({
|
||||||
|
key: key,
|
||||||
|
oldValue: oldObject[key],
|
||||||
|
newValue: newObject[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changedProperties;
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
Banner,
|
Banner,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { Divider } from 'semantic-ui-react';
|
import { Divider } from 'semantic-ui-react';
|
||||||
|
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||||
|
|
||||||
const MODEL_MAPPING_EXAMPLE = {
|
const MODEL_MAPPING_EXAMPLE = {
|
||||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
||||||
@ -87,97 +88,9 @@ const EditChannel = (props) => {
|
|||||||
const [customModel, setCustomModel] = useState('');
|
const [customModel, setCustomModel] = useState('');
|
||||||
const handleInputChange = (name, value) => {
|
const handleInputChange = (name, value) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
if (name === 'type' && inputs.models.length === 0) {
|
if (name === 'type') {
|
||||||
let localModels = [];
|
let localModels = [];
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 33:
|
|
||||||
case 14:
|
|
||||||
localModels = [
|
|
||||||
'claude-instant-1.2',
|
|
||||||
'claude-2',
|
|
||||||
'claude-2.0',
|
|
||||||
'claude-2.1',
|
|
||||||
'claude-3-opus-20240229',
|
|
||||||
'claude-3-sonnet-20240229',
|
|
||||||
'claude-3-haiku-20240307',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
localModels = ['PaLM-2'];
|
|
||||||
break;
|
|
||||||
case 15:
|
|
||||||
localModels = [
|
|
||||||
'ERNIE-Bot',
|
|
||||||
'ERNIE-Bot-turbo',
|
|
||||||
'ERNIE-Bot-4',
|
|
||||||
'Embedding-V1',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 17:
|
|
||||||
localModels = [
|
|
||||||
'qwen-turbo',
|
|
||||||
'qwen-plus',
|
|
||||||
'qwen-max',
|
|
||||||
'qwen-max-longcontext',
|
|
||||||
'text-embedding-v1',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
|
|
||||||
break;
|
|
||||||
case 18:
|
|
||||||
localModels = [
|
|
||||||
'SparkDesk',
|
|
||||||
'SparkDesk-v1.1',
|
|
||||||
'SparkDesk-v2.1',
|
|
||||||
'SparkDesk-v3.1',
|
|
||||||
'SparkDesk-v3.5',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 19:
|
|
||||||
localModels = [
|
|
||||||
'360GPT_S2_V9',
|
|
||||||
'embedding-bert-512-v1',
|
|
||||||
'embedding_s1_v1',
|
|
||||||
'semantic_similarity_s1_v1',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 23:
|
|
||||||
localModels = ['hunyuan'];
|
|
||||||
break;
|
|
||||||
case 24:
|
|
||||||
localModels = [
|
|
||||||
'gemini-1.0-pro-001',
|
|
||||||
'gemini-1.0-pro-vision-001',
|
|
||||||
'gemini-1.5-pro',
|
|
||||||
'gemini-1.5-pro-latest',
|
|
||||||
'gemini-pro',
|
|
||||||
'gemini-pro-vision',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 34:
|
|
||||||
localModels = [
|
|
||||||
'command-r',
|
|
||||||
'command-r-plus',
|
|
||||||
'command-light',
|
|
||||||
'command-light-nightly',
|
|
||||||
'command',
|
|
||||||
'command-nightly',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 25:
|
|
||||||
localModels = [
|
|
||||||
'moonshot-v1-8k',
|
|
||||||
'moonshot-v1-32k',
|
|
||||||
'moonshot-v1-128k',
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
case 26:
|
|
||||||
localModels = ['glm-4', 'glm-4v', 'glm-3-turbo'];
|
|
||||||
break;
|
|
||||||
case 31:
|
|
||||||
localModels = ['yi-34b-chat-0205', 'yi-34b-chat-200k', 'yi-vl-plus'];
|
|
||||||
break;
|
|
||||||
case 2:
|
case 2:
|
||||||
localModels = [
|
localModels = [
|
||||||
'mj_imagine',
|
'mj_imagine',
|
||||||
@ -186,6 +99,7 @@ const EditChannel = (props) => {
|
|||||||
'mj_blend',
|
'mj_blend',
|
||||||
'mj_upscale',
|
'mj_upscale',
|
||||||
'mj_describe',
|
'mj_describe',
|
||||||
|
'mj_uploads',
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
@ -205,10 +119,17 @@ const EditChannel = (props) => {
|
|||||||
'mj_high_variation',
|
'mj_high_variation',
|
||||||
'mj_low_variation',
|
'mj_low_variation',
|
||||||
'mj_pan',
|
'mj_pan',
|
||||||
|
'mj_uploads',
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
localModels = getChannelModels(value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
if (inputs.models.length === 0) {
|
||||||
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
||||||
|
}
|
||||||
|
setBasicModels(localModels);
|
||||||
}
|
}
|
||||||
//setAutoBan
|
//setAutoBan
|
||||||
};
|
};
|
||||||
@ -244,6 +165,7 @@ const EditChannel = (props) => {
|
|||||||
} else {
|
} else {
|
||||||
setAutoBan(true);
|
setAutoBan(true);
|
||||||
}
|
}
|
||||||
|
setBasicModels(getChannelModels(data.type));
|
||||||
// console.log(data);
|
// console.log(data);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
@ -312,6 +234,9 @@ const EditChannel = (props) => {
|
|||||||
loadChannel().then(() => {});
|
loadChannel().then(() => {});
|
||||||
} else {
|
} else {
|
||||||
setInputs(originInputs);
|
setInputs(originInputs);
|
||||||
|
let localModels = getChannelModels(inputs.type);
|
||||||
|
setBasicModels(localModels);
|
||||||
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
||||||
}
|
}
|
||||||
}, [props.editingChannel.id]);
|
}, [props.editingChannel.id]);
|
||||||
|
|
||||||
@ -373,20 +298,35 @@ const EditChannel = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addCustomModel = () => {
|
const addCustomModels = () => {
|
||||||
if (customModel.trim() === '') return;
|
if (customModel.trim() === '') return;
|
||||||
if (inputs.models.includes(customModel)) return showError('该模型已存在!');
|
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
|
||||||
|
const modelArray = customModel.split(',').map((model) => model.trim());
|
||||||
|
|
||||||
let localModels = [...inputs.models];
|
let localModels = [...inputs.models];
|
||||||
localModels.push(customModel);
|
let localModelOptions = [...modelOptions];
|
||||||
let localModelOptions = [];
|
let hasError = false;
|
||||||
localModelOptions.push({
|
|
||||||
key: customModel,
|
modelArray.forEach((model) => {
|
||||||
text: customModel,
|
// 检查模型是否已存在,且模型名称非空
|
||||||
value: customModel,
|
if (model && !localModels.includes(model)) {
|
||||||
});
|
localModels.push(model); // 添加到模型列表
|
||||||
setModelOptions((modelOptions) => {
|
localModelOptions.push({
|
||||||
return [...modelOptions, ...localModelOptions];
|
// 添加到下拉选项
|
||||||
|
key: model,
|
||||||
|
text: model,
|
||||||
|
value: model,
|
||||||
|
});
|
||||||
|
} else if (model) {
|
||||||
|
showError('某些模型已存在!');
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasError) return; // 如果有错误则终止操作
|
||||||
|
|
||||||
|
// 更新状态值
|
||||||
|
setModelOptions(localModelOptions);
|
||||||
setCustomModel('');
|
setCustomModel('');
|
||||||
handleInputChange('models', localModels);
|
handleInputChange('models', localModels);
|
||||||
};
|
};
|
||||||
@ -596,7 +536,7 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('models', basicModels);
|
handleInputChange('models', basicModels);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
填入基础模型
|
填入相关模型
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='secondary'
|
type='secondary'
|
||||||
@ -617,7 +557,7 @@ const EditChannel = (props) => {
|
|||||||
</Space>
|
</Space>
|
||||||
<Input
|
<Input
|
||||||
addonAfter={
|
addonAfter={
|
||||||
<Button type='primary' onClick={addCustomModel}>
|
<Button type='primary' onClick={addCustomModels}>
|
||||||
填入
|
填入
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
10
web/src/pages/Pricing/index.js
Normal file
10
web/src/pages/Pricing/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ModelPricing from '../../components/ModelPricing.js';
|
||||||
|
|
||||||
|
const Pricing = () => (
|
||||||
|
<>
|
||||||
|
<ModelPricing />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Pricing;
|
156
web/src/pages/Setting/Operation/SettingsCreditLimit.js
Normal file
156
web/src/pages/Setting/Operation/SettingsCreditLimit.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsCreditLimit(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
QuotaForNewUser: '',
|
||||||
|
PreConsumedQuota: '',
|
||||||
|
QuotaForInviter: '',
|
||||||
|
QuotaForInvitee: '',
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'额度设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'新用户初始额度'}
|
||||||
|
field={'QuotaForNewUser'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'Token'}
|
||||||
|
placeholder={''}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
QuotaForNewUser: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'请求预扣费额度'}
|
||||||
|
field={'PreConsumedQuota'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'Token'}
|
||||||
|
extraText={'请求结束后多退少补'}
|
||||||
|
placeholder={''}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
PreConsumedQuota: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'邀请新用户奖励额度'}
|
||||||
|
field={'QuotaForInviter'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'Token'}
|
||||||
|
extraText={''}
|
||||||
|
placeholder={'例如:2000'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
QuotaForInviter: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'新用户使用邀请码奖励额度'}
|
||||||
|
field={'QuotaForInvitee'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'Token'}
|
||||||
|
extraText={''}
|
||||||
|
placeholder={'例如:1000'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
QuotaForInvitee: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存额度设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
147
web/src/pages/Setting/Operation/SettingsDataDashboard.js
Normal file
147
web/src/pages/Setting/Operation/SettingsDataDashboard.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function DataDashboard(props) {
|
||||||
|
const optionsDataExportDefaultTime = [
|
||||||
|
{ key: 'hour', label: '小时', value: 'hour' },
|
||||||
|
{ key: 'day', label: '天', value: 'day' },
|
||||||
|
{ key: 'week', label: '周', value: 'week' },
|
||||||
|
];
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
DataExportEnabled: false,
|
||||||
|
DataExportInterval: '',
|
||||||
|
DataExportDefaultTime: '',
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
localStorage.setItem(
|
||||||
|
'data_export_default_time',
|
||||||
|
String(inputs.DataExportDefaultTime),
|
||||||
|
);
|
||||||
|
}, [props.options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'数据看板设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'DataExportEnabled'}
|
||||||
|
label={'启用数据看板(实验性)'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DataExportEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'数据看板更新间隔 '}
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
suffix={'分钟'}
|
||||||
|
extraText={'设置过短会影响数据库性能'}
|
||||||
|
placeholder={'数据看板更新间隔'}
|
||||||
|
field={'DataExportInterval'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DataExportInterval: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Select
|
||||||
|
label='数据看板默认时间粒度'
|
||||||
|
optionList={optionsDataExportDefaultTime}
|
||||||
|
field={'DataExportDefaultTime'}
|
||||||
|
extraText={'仅修改展示粒度,统计精确到小时'}
|
||||||
|
placeholder={'数据看板默认时间粒度'}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DataExportDefaultTime: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存数据看板设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
170
web/src/pages/Setting/Operation/SettingsDrawing.js
Normal file
170
web/src/pages/Setting/Operation/SettingsDrawing.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsDrawing(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
DrawingEnabled: false,
|
||||||
|
MjNotifyEnabled: false,
|
||||||
|
MjAccountFilterEnabled: false,
|
||||||
|
MjForwardUrlEnabled: false,
|
||||||
|
MjModeClearEnabled: false,
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled));
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'绘图设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'DrawingEnabled'}
|
||||||
|
label={'启用绘图功能'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DrawingEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'MjNotifyEnabled'}
|
||||||
|
label={'允许回调(会泄露服务器 IP 地址)'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
MjNotifyEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'MjAccountFilterEnabled'}
|
||||||
|
label={'允许 AccountFilter 参数'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
MjAccountFilterEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'MjForwardUrlEnabled'}
|
||||||
|
label={'开启之后将上游地址替换为服务器地址'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
MjForwardUrlEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'MjModeClearEnabled'}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
开启之后会清除用户提示词中的 <Tag>--fast</Tag> 、
|
||||||
|
<Tag>--relax</Tag> 以及 <Tag>--turbo</Tag> 参数
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
MjModeClearEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存绘图设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
192
web/src/pages/Setting/Operation/SettingsGeneral.js
Normal file
192
web/src/pages/Setting/Operation/SettingsGeneral.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function GeneralSettings(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
TopUpLink: '',
|
||||||
|
ChatLink: '',
|
||||||
|
ChatLink2: '',
|
||||||
|
QuotaPerUnit: '',
|
||||||
|
RetryTimes: '',
|
||||||
|
DisplayInCurrencyEnabled: false,
|
||||||
|
DisplayTokenStatEnabled: false,
|
||||||
|
DefaultCollapseSidebar: false,
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
function onChange(value, e) {
|
||||||
|
const name = e.target.id;
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
}
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'通用设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Input
|
||||||
|
field={'TopUpLink'}
|
||||||
|
label={'充值链接'}
|
||||||
|
initValue={''}
|
||||||
|
placeholder={'例如发卡网站的购买链接'}
|
||||||
|
onChange={onChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Input
|
||||||
|
field={'ChatLink'}
|
||||||
|
label={'默认聊天页面链接'}
|
||||||
|
initValue={''}
|
||||||
|
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||||
|
onChange={onChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Input
|
||||||
|
field={'ChatLink2'}
|
||||||
|
label={'聊天页面 2 链接'}
|
||||||
|
initValue={''}
|
||||||
|
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||||
|
onChange={onChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Input
|
||||||
|
field={'QuotaPerUnit'}
|
||||||
|
label={'单位美元额度'}
|
||||||
|
initValue={''}
|
||||||
|
placeholder='一单位货币能兑换的额度'
|
||||||
|
onChange={onChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Input
|
||||||
|
field={'RetryTimes'}
|
||||||
|
label={'失败重试次数'}
|
||||||
|
initValue={''}
|
||||||
|
placeholder='失败重试次数'
|
||||||
|
onChange={onChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'DisplayInCurrencyEnabled'}
|
||||||
|
label={'以货币形式显示额度'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DisplayInCurrencyEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'DisplayTokenStatEnabled'}
|
||||||
|
label={'Billing 相关 API 显示令牌额度而非用户额度'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DisplayTokenStatEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'DefaultCollapseSidebar'}
|
||||||
|
label={'默认折叠侧边栏'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
DefaultCollapseSidebar: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存通用设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
147
web/src/pages/Setting/Operation/SettingsLog.js
Normal file
147
web/src/pages/Setting/Operation/SettingsLog.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsLog(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
LogConsumeEnabled: false,
|
||||||
|
historyTimestamp: dayjs().subtract(1, 'month').toDate(),
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow).filter(
|
||||||
|
(item) => item.key !== 'historyTimestamp',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function onCleanHistoryLog() {
|
||||||
|
try {
|
||||||
|
setLoadingCleanHistoryLog(true);
|
||||||
|
if (!inputs.historyTimestamp) throw new Error('请选择日志记录时间');
|
||||||
|
const res = await API.delete(
|
||||||
|
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
|
||||||
|
);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(`${data} 条日志已清理!`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error('日志清理失败:' + message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
} finally {
|
||||||
|
setLoadingCleanHistoryLog(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentInputs['historyTimestamp'] = inputs.historyTimestamp;
|
||||||
|
setInputs(Object.assign(inputs, currentInputs));
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'日志设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'LogConsumeEnabled'}
|
||||||
|
label={'启用额度消费日志记录'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
LogConsumeEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Spin spinning={loadingCleanHistoryLog}>
|
||||||
|
<Form.DatePicker
|
||||||
|
label='日志记录时间'
|
||||||
|
field={'historyTimestamp'}
|
||||||
|
type='dateTime'
|
||||||
|
inputReadOnly={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
historyTimestamp: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size='default' onClick={onCleanHistoryLog}>
|
||||||
|
清除历史日志
|
||||||
|
</Button>
|
||||||
|
</Spin>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存日志设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
194
web/src/pages/Setting/Operation/SettingsMagnification.js
Normal file
194
web/src/pages/Setting/Operation/SettingsMagnification.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
verifyJSON,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsMagnification(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
ModelPrice: '',
|
||||||
|
ModelRatio: '',
|
||||||
|
CompletionRatio: '',
|
||||||
|
GroupRatio: '',
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
try {
|
||||||
|
await refForm.current.validate();
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined))
|
||||||
|
return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showError('请检查输入');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'倍率设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.TextArea
|
||||||
|
label={'模型固定价格'}
|
||||||
|
extraText={'一次调用消耗多少刀,优先级大于模型倍率'}
|
||||||
|
placeholder={
|
||||||
|
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
|
||||||
|
}
|
||||||
|
field={'ModelPrice'}
|
||||||
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
|
trigger='blur'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (rule, value) => verifyJSON(value),
|
||||||
|
message: '不是合法的 JSON 字符串',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
ModelPrice: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.TextArea
|
||||||
|
label={'模型倍率'}
|
||||||
|
extraText={''}
|
||||||
|
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
|
||||||
|
field={'ModelRatio'}
|
||||||
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
|
trigger='blur'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (rule, value) => verifyJSON(value),
|
||||||
|
message: '不是合法的 JSON 字符串',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
ModelRatio: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.TextArea
|
||||||
|
label={'模型补全倍率'}
|
||||||
|
extraText={'仅对自定义模型有效'}
|
||||||
|
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
|
||||||
|
field={'CompletionRatio'}
|
||||||
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
|
trigger='blur'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (rule, value) => verifyJSON(value),
|
||||||
|
message: '不是合法的 JSON 字符串',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
CompletionRatio: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.TextArea
|
||||||
|
label={'分组倍率'}
|
||||||
|
extraText={''}
|
||||||
|
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
|
||||||
|
field={'GroupRatio'}
|
||||||
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
|
trigger='blur'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (rule, value) => verifyJSON(value),
|
||||||
|
message: '不是合法的 JSON 字符串',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
GroupRatio: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存倍率设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
154
web/src/pages/Setting/Operation/SettingsMonitoring.js
Normal file
154
web/src/pages/Setting/Operation/SettingsMonitoring.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsMonitoring(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
ChannelDisableThreshold: '',
|
||||||
|
QuotaRemindThreshold: '',
|
||||||
|
AutomaticDisableChannelEnabled: false,
|
||||||
|
AutomaticEnableChannelEnabled: false,
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'监控设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'最长响应时间'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'秒'}
|
||||||
|
extraText={'当运行通道全部测试时,超过此时间将自动禁用通道'}
|
||||||
|
placeholder={''}
|
||||||
|
field={'ChannelDisableThreshold'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
ChannelDisableThreshold: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.InputNumber
|
||||||
|
label={'额度提醒阈值'}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
suffix={'Token'}
|
||||||
|
extraText={'低于此额度时将发送邮件提醒用户'}
|
||||||
|
placeholder={''}
|
||||||
|
field={'QuotaRemindThreshold'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
QuotaRemindThreshold: String(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'AutomaticDisableChannelEnabled'}
|
||||||
|
label={'失败时自动禁用通道'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
AutomaticDisableChannelEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'AutomaticEnableChannelEnabled'}
|
||||||
|
label={'成功时自动启用通道'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
AutomaticEnableChannelEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存监控设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
135
web/src/pages/Setting/Operation/SettingsSensitiveWords.js
Normal file
135
web/src/pages/Setting/Operation/SettingsSensitiveWords.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
|
export default function SettingsSensitiveWords(props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
CheckSensitiveEnabled: false,
|
||||||
|
CheckSensitiveOnPromptEnabled: false,
|
||||||
|
SensitiveWords: '',
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined)) return showError('部分保存失败,请重试');
|
||||||
|
}
|
||||||
|
showSuccess('保存成功');
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError('保存失败,请重试');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={'屏蔽词过滤设置'}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'CheckSensitiveEnabled'}
|
||||||
|
label={'启用屏蔽词过滤功能'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) => {
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
CheckSensitiveEnabled: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'CheckSensitiveOnPromptEnabled'}
|
||||||
|
label={'启用 Prompt 检查'}
|
||||||
|
size='large'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
CheckSensitiveOnPromptEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.TextArea
|
||||||
|
label={'屏蔽词列表'}
|
||||||
|
extraText={'一行一个屏蔽词,不需要符号分割'}
|
||||||
|
placeholder={'一行一个屏蔽词,不需要符号分割'}
|
||||||
|
field={'SensitiveWords'}
|
||||||
|
onChange={(value) =>
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
SensitiveWords: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
autosize={{ minRows: 6, maxRows: 12 }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='large' onClick={onSubmit}>
|
||||||
|
保存屏蔽词过滤设置
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
||||||
import { renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
SideSheet,
|
SideSheet,
|
||||||
Space,
|
Space,
|
||||||
@ -17,6 +18,8 @@ import {
|
|||||||
const EditUser = (props) => {
|
const EditUser = (props) => {
|
||||||
const userId = props.editingUser.id;
|
const userId = props.editingUser.id;
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
username: '',
|
username: '',
|
||||||
display_name: '',
|
display_name: '',
|
||||||
@ -112,6 +115,16 @@ const EditUser = (props) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addLocalQuota = () => {
|
||||||
|
let newQuota = parseInt(quota) + parseInt(addQuotaLocal);
|
||||||
|
setInputs((inputs) => ({ ...inputs, quota: newQuota }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddQuotaModal = () => {
|
||||||
|
setAddQuotaLocal('0');
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SideSheet
|
<SideSheet
|
||||||
@ -197,14 +210,17 @@ const EditUser = (props) => {
|
|||||||
<div style={{ marginTop: 20 }}>
|
<div style={{ marginTop: 20 }}>
|
||||||
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
|
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Space>
|
||||||
name='quota'
|
<Input
|
||||||
placeholder={'请输入新的剩余额度'}
|
name='quota'
|
||||||
onChange={(value) => handleInputChange('quota', value)}
|
placeholder={'请输入新的剩余额度'}
|
||||||
value={quota}
|
onChange={(value) => handleInputChange('quota', value)}
|
||||||
type={'number'}
|
value={quota}
|
||||||
autoComplete='new-password'
|
type={'number'}
|
||||||
/>
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
<Button onClick={openAddQuotaModal}>添加额度</Button>
|
||||||
|
</Space>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
|
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
|
||||||
@ -260,6 +276,30 @@ const EditUser = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
|
<Modal
|
||||||
|
centered={true}
|
||||||
|
visible={addQuotaModalOpen}
|
||||||
|
onOk={() => {
|
||||||
|
addLocalQuota();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
closable={null}
|
||||||
|
>
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<Typography.Text>{`新额度${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal))}`}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
name='addQuotaLocal'
|
||||||
|
placeholder={'需要添加的额度(支持负数)'}
|
||||||
|
onChange={(value) => {
|
||||||
|
setAddQuotaLocal(value);
|
||||||
|
}}
|
||||||
|
value={addQuotaLocal}
|
||||||
|
type={'number'}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -252,33 +252,33 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@douyinfe/semi-animation-react@2.54.1":
|
"@douyinfe/semi-animation-react@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.54.1.tgz#0bd3b3d980eeb5c958d98180a10908c02fe0e1f7"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.58.0.tgz#648acbfac88c6a323d598833bc471ddb483b0a50"
|
||||||
integrity sha512-xLVK8Tu75p90oduD16Nhftk77REgo+Z608irWOumuYdfsAiSNjLq5xn/OqHGfmtn4iFYCkJglQDorH6cyckJBQ==
|
integrity sha512-0dJD5+cRCrwIqxVK4Ywj/Wt1tmkslX6QNd9vODZO8MKQcgQh1MD+dFi14WKZxjzJhR8FZIMY08DmRBDtyXY3jQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@douyinfe/semi-animation" "2.54.1"
|
"@douyinfe/semi-animation" "2.58.0"
|
||||||
"@douyinfe/semi-animation-styled" "2.54.1"
|
"@douyinfe/semi-animation-styled" "2.58.0"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
|
|
||||||
"@douyinfe/semi-animation-styled@2.54.1":
|
"@douyinfe/semi-animation-styled@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.54.1.tgz#b491fa6356e7ee97e875d779bb80ba34a9dcdd2e"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.58.0.tgz#b7d9a7dc77d73e9de1030eb69d845278922d7d87"
|
||||||
integrity sha512-Y/DCtb6UyCenNY6aG8oQGE/Sy2s6+D6ghrjJ0HdcXutoLGuWmk7xMw6RDx+aZUxthQE3dqfc7EbuZ8rYqQRpYg==
|
integrity sha512-PqWUfRn4q0BDkPt5hxYYDEZwokiIpNa4S+7F5mP4nYyVJppkkylnpJcktSDy25YMfSdkXYcF7wrPAIsGfoZQ8g==
|
||||||
|
|
||||||
"@douyinfe/semi-animation@2.54.1":
|
"@douyinfe/semi-animation@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.54.1.tgz#1d5334a5c89291c8dfac9408ca8017eea0f048ca"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.58.0.tgz#bda6376b0b3cb11b076fa9cbd33abd0171e0b71d"
|
||||||
integrity sha512-Bj/RUTIeSXi5IZqK02UC4bTMD1O9JCzaomtQ7v8dyp9SF3MIpeKNppNw7/wa3vHv5iW+QbJdF2wsxTGgJtCnrQ==
|
integrity sha512-ks1v8xqTV/YkXO/9HKr7yooWgtvfKvAWJ0nhsZOUi9xL04PbTjjvbD80FMOvemHFY320PZpurIXwIPrx72xtUQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
bezier-easing "^2.1.0"
|
bezier-easing "^2.1.0"
|
||||||
|
|
||||||
"@douyinfe/semi-foundation@2.54.1":
|
"@douyinfe/semi-foundation@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.54.1.tgz#d46dca2749fe3c6dd506e41ed2c18dbfbb0e0b67"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.58.0.tgz#fdb5ff7f1ee1e8ba2883278486fc4cbb2912daa7"
|
||||||
integrity sha512-RaeTfCb/0AsqJrDWRI6zRsQcie0IogddkVuYFiQfuMhHFrjUG8O5IPNvv6d5+8O0L9zkdIxQ95LE6BcEhZe+dg==
|
integrity sha512-jMmaTuGHG/MUW5d1CvEmWuxMI+vdLUElM2ol1B4pglDx0lOHBCyGRk9j78H1rZg15fxXXxByOD/CZmNNv1l7Cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@douyinfe/semi-animation" "2.54.1"
|
"@douyinfe/semi-animation" "2.58.0"
|
||||||
async-validator "^3.5.0"
|
async-validator "^3.5.0"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
date-fns "^2.29.3"
|
date-fns "^2.29.3"
|
||||||
@ -288,39 +288,46 @@
|
|||||||
memoize-one "^5.2.1"
|
memoize-one "^5.2.1"
|
||||||
scroll-into-view-if-needed "^2.2.24"
|
scroll-into-view-if-needed "^2.2.24"
|
||||||
|
|
||||||
"@douyinfe/semi-icons@2.54.1", "@douyinfe/semi-icons@^2.46.1":
|
"@douyinfe/semi-icons@2.58.0":
|
||||||
|
version "2.58.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.58.0.tgz#b3b63b781d449db367c306fc955e4e3b751c950c"
|
||||||
|
integrity sha512-DV9hhAbvTahwx5XsTiLrW5TBW8c3TDnrHIDr79Pe83am+0RFVm+PkugwqtRrA1ETVcbrSu6VBpNMGaBrz5IVDg==
|
||||||
|
dependencies:
|
||||||
|
classnames "^2.2.6"
|
||||||
|
|
||||||
|
"@douyinfe/semi-icons@^2.46.1":
|
||||||
version "2.54.1"
|
version "2.54.1"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.54.1.tgz#ff3ea6601eabae741dc85de0b8e496b311696352"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.54.1.tgz#ff3ea6601eabae741dc85de0b8e496b311696352"
|
||||||
integrity sha512-cykK857iXU4nkGqk8sm/ZgFD8A48NVk9yr1ThLS0E6PzYwBU/KpavYWGHciNdeFOpFMsiUvMPYVH96jRglpkJw==
|
integrity sha512-cykK857iXU4nkGqk8sm/ZgFD8A48NVk9yr1ThLS0E6PzYwBU/KpavYWGHciNdeFOpFMsiUvMPYVH96jRglpkJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
|
|
||||||
"@douyinfe/semi-illustrations@2.54.1":
|
"@douyinfe/semi-illustrations@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.54.1.tgz#006d79568856942442ad7e8da9d5b841397bcf7b"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.58.0.tgz#39dd5243f9efcdcd588895eb8609e61b9148b5b2"
|
||||||
integrity sha512-8Vg0Ty6OdQRHVpu5OBFpw5b92Z4eRx1ArQtygsOfYnVeKVOc7IytrS5eQZxijMx1ScFshrlk3qISe40D3HVS6g==
|
integrity sha512-WMicSSvcW7VXxMVu+XqEdZ0W3jcWPZPCHslV2yM6PUao7wr6dGvdmwsH8HIBTzvh/iE886hEx60MFW5IUwaN9A==
|
||||||
|
|
||||||
"@douyinfe/semi-theme-default@2.54.1":
|
"@douyinfe/semi-theme-default@2.58.0":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.54.1.tgz#86e403a1ebb4f25ae44242701a0c5616406ea7ca"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.58.0.tgz#598ce75c93e0f32ee4558d6e00787108359dc91f"
|
||||||
integrity sha512-LispAQ9H0MjgO3WcoM2lLFvtMUMAyqontOrdoIq37s2iaC8ZP7nHH50wvlOprzWmz7zH7F7ls6DiWLw0Nd6/UA==
|
integrity sha512-YDVJCa/VupAA2rAwbLKiUeMy46W8W0ZVqWWydoA2Rv8XB+65RkWY/w7D3Yg2FG0+Eg4f9/DwazD9IFUWPeIhCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.6"
|
glob "^7.1.6"
|
||||||
|
|
||||||
"@douyinfe/semi-ui@^2.46.1":
|
"@douyinfe/semi-ui@^2.55.3":
|
||||||
version "2.54.1"
|
version "2.58.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.54.1.tgz#ccba9779daee6584a80382f82aa595b1ceda89c3"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.58.0.tgz#efc9113bde06675b64ae09b111241f02c9352cda"
|
||||||
integrity sha512-rgABgQO038Q/pdsuz1kUTpaliZmhIj64yOmuoNBG3zVEhRJ+WpkWuwn1kGXHR+sCKDZ+4EydEFhwpLaRV96+sQ==
|
integrity sha512-UGwOlVmuZuK8n3N65R3lhhk3qcDGgw3iEyZTBb5C26NC+W56oF6mT7r+DGdLN6ui6cZJbB8dJKMiSAghb4BBXw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dnd-kit/core" "^6.0.8"
|
"@dnd-kit/core" "^6.0.8"
|
||||||
"@dnd-kit/sortable" "^7.0.2"
|
"@dnd-kit/sortable" "^7.0.2"
|
||||||
"@dnd-kit/utilities" "^3.2.1"
|
"@dnd-kit/utilities" "^3.2.1"
|
||||||
"@douyinfe/semi-animation" "2.54.1"
|
"@douyinfe/semi-animation" "2.58.0"
|
||||||
"@douyinfe/semi-animation-react" "2.54.1"
|
"@douyinfe/semi-animation-react" "2.58.0"
|
||||||
"@douyinfe/semi-foundation" "2.54.1"
|
"@douyinfe/semi-foundation" "2.58.0"
|
||||||
"@douyinfe/semi-icons" "2.54.1"
|
"@douyinfe/semi-icons" "2.58.0"
|
||||||
"@douyinfe/semi-illustrations" "2.54.1"
|
"@douyinfe/semi-illustrations" "2.58.0"
|
||||||
"@douyinfe/semi-theme-default" "2.54.1"
|
"@douyinfe/semi-theme-default" "2.58.0"
|
||||||
async-validator "^3.5.0"
|
async-validator "^3.5.0"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
copy-text-to-clipboard "^2.1.1"
|
copy-text-to-clipboard "^2.1.1"
|
||||||
@ -1240,6 +1247,11 @@ date-fns@^2.29.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.21.0"
|
"@babel/runtime" "^7.21.0"
|
||||||
|
|
||||||
|
dayjs@^1.11.11:
|
||||||
|
version "1.11.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
|
||||||
|
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
|
||||||
|
|
||||||
debug@^4.1.0, debug@^4.3.1:
|
debug@^4.1.0, debug@^4.3.1:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
|
Loading…
Reference in New Issue
Block a user