mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v0.4.2-alp
			...
			v0.4.3-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7c7eb6b7ec | ||
|  | 8b2ef666ef | ||
|  | 955d5f8707 | ||
|  | 47ca449e32 | ||
|  | 39481eb6c0 | ||
|  | 69153e7231 | ||
|  | cdef10cad8 | ||
|  | 077853416d | ||
|  | 596446dba4 | 
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @@ -66,7 +66,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
| 5. 支持**令牌管理**,设置令牌的过期时间和使用次数。 | 5. 支持**令牌管理**,设置令牌的过期时间和使用次数。 | ||||||
| 6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | 6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | ||||||
| 7. 支持**通道管理**,批量创建通道。 | 7. 支持**通道管理**,批量创建通道。 | ||||||
| 8. 支持**用户分组**以及**渠道分组**。 | 8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 | ||||||
| 9. 支持渠道**设置模型列表**。 | 9. 支持渠道**设置模型列表**。 | ||||||
| 10. 支持**查看额度明细**。 | 10. 支持**查看额度明细**。 | ||||||
| 11. 支持发布公告,设置充值链接,设置新用户初始额度。 | 11. 支持发布公告,设置充值链接,设置新用户初始额度。 | ||||||
| @@ -201,3 +201,11 @@ https://openai.justsong.cn | |||||||
|    + 令牌额度仅供用户设置最大使用量,用户可自由设置。 |    + 令牌额度仅供用户设置最大使用量,用户可自由设置。 | ||||||
| 2. 宝塔部署后访问出现空白页面? | 2. 宝塔部署后访问出现空白页面? | ||||||
|    + 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 |    + 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 | ||||||
|  | 3. 提示无可用渠道? | ||||||
|  |    + 请检查的用户分组和渠道分组设置。 | ||||||
|  |    + 以及渠道的模型设置。 | ||||||
|  |  | ||||||
|  | ## 注意 | ||||||
|  | 本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及法律法规的情况下使用,不得用于非法用途。 | ||||||
|  |  | ||||||
|  | 本项目依据 MIT 协议开源,请以某种方式保留 One API 的版权信息。 | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								common/group-ratio.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								common/group-ratio.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | package common | ||||||
|  |  | ||||||
|  | import "encoding/json" | ||||||
|  |  | ||||||
|  | var GroupRatio = map[string]float64{ | ||||||
|  | 	"default": 1, | ||||||
|  | 	"vip":     1, | ||||||
|  | 	"svip":    1, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GroupRatio2JSONString() string { | ||||||
|  | 	jsonBytes, err := json.Marshal(GroupRatio) | ||||||
|  | 	if err != nil { | ||||||
|  | 		SysError("Error marshalling model ratio: " + err.Error()) | ||||||
|  | 	} | ||||||
|  | 	return string(jsonBytes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func UpdateGroupRatioByJSONString(jsonStr string) error { | ||||||
|  | 	GroupRatio = make(map[string]float64) | ||||||
|  | 	return json.Unmarshal([]byte(jsonStr), &GroupRatio) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetGroupRatio(name string) float64 { | ||||||
|  | 	ratio, ok := GroupRatio[name] | ||||||
|  | 	if !ok { | ||||||
|  | 		SysError("Group ratio not found: " + name) | ||||||
|  | 		return 1 | ||||||
|  | 	} | ||||||
|  | 	return ratio | ||||||
|  | } | ||||||
| @@ -39,6 +39,7 @@ func ModelRatio2JSONString() string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func UpdateModelRatioByJSONString(jsonStr string) error { | func UpdateModelRatioByJSONString(jsonStr string) error { | ||||||
|  | 	ModelRatio = make(map[string]float64) | ||||||
| 	return json.Unmarshal([]byte(jsonStr), &ModelRatio) | 	return json.Unmarshal([]byte(jsonStr), &ModelRatio) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,6 +37,58 @@ type OpenAIUsageResponse struct { | |||||||
| 	TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar | 	TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type OpenAISBUsageResponse struct { | ||||||
|  | 	Msg  string `json:"msg"` | ||||||
|  | 	Data *struct { | ||||||
|  | 		Credit string `json:"credit"` | ||||||
|  | 	} `json:"data"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetResponseBody(method, url string, channel *model.Channel) ([]byte, error) { | ||||||
|  | 	client := &http.Client{} | ||||||
|  | 	req, err := http.NewRequest(method, url, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	auth := fmt.Sprintf("Bearer %s", channel.Key) | ||||||
|  | 	req.Header.Add("Authorization", auth) | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	body, err := io.ReadAll(res.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	err = res.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return body, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { | ||||||
|  | 	url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key) | ||||||
|  | 	body, err := GetResponseBody("GET", url, channel) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	response := OpenAISBUsageResponse{} | ||||||
|  | 	err = json.Unmarshal(body, &response) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	if response.Data == nil { | ||||||
|  | 		return 0, errors.New(response.Msg) | ||||||
|  | 	} | ||||||
|  | 	balance, err := strconv.ParseFloat(response.Data.Credit, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	channel.UpdateBalance(balance) | ||||||
|  | 	return balance, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func updateChannelBalance(channel *model.Channel) (float64, error) { | func updateChannelBalance(channel *model.Channel) (float64, error) { | ||||||
| 	baseURL := common.ChannelBaseURLs[channel.Type] | 	baseURL := common.ChannelBaseURLs[channel.Type] | ||||||
| 	switch channel.Type { | 	switch channel.Type { | ||||||
| @@ -48,27 +100,14 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { | |||||||
| 		return 0, errors.New("尚未实现") | 		return 0, errors.New("尚未实现") | ||||||
| 	case common.ChannelTypeCustom: | 	case common.ChannelTypeCustom: | ||||||
| 		baseURL = channel.BaseURL | 		baseURL = channel.BaseURL | ||||||
|  | 	case common.ChannelTypeOpenAISB: | ||||||
|  | 		return updateChannelOpenAISBBalance(channel) | ||||||
| 	default: | 	default: | ||||||
| 		return 0, errors.New("尚未实现") | 		return 0, errors.New("尚未实现") | ||||||
| 	} | 	} | ||||||
| 	url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) | 	url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) | ||||||
|  |  | ||||||
| 	client := &http.Client{} | 	body, err := GetResponseBody("GET", url, channel) | ||||||
| 	req, err := http.NewRequest("GET", url, nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	auth := fmt.Sprintf("Bearer %s", channel.Key) |  | ||||||
| 	req.Header.Add("Authorization", auth) |  | ||||||
| 	res, err := client.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	body, err := io.ReadAll(res.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	err = res.Body.Close() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| @@ -84,20 +123,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { | |||||||
| 		startDate = now.AddDate(0, 0, -100).Format("2006-01-02") | 		startDate = now.AddDate(0, 0, -100).Format("2006-01-02") | ||||||
| 	} | 	} | ||||||
| 	url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) | 	url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) | ||||||
| 	req, err = http.NewRequest("GET", url, nil) | 	body, err = GetResponseBody("GET", url, channel) | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	req.Header.Add("Authorization", auth) |  | ||||||
| 	res, err = client.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	body, err = io.ReadAll(res.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	err = res.Body.Close() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								controller/group.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								controller/group.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | package controller | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"net/http" | ||||||
|  | 	"one-api/common" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GetGroups(c *gin.Context) { | ||||||
|  | 	groupNames := make([]string, 0) | ||||||
|  | 	for groupName, _ := range common.GroupRatio { | ||||||
|  | 		groupNames = append(groupNames, groupName) | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 		"success": true, | ||||||
|  | 		"message": "", | ||||||
|  | 		"data":    groupNames, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -58,6 +58,20 @@ func countTokenMessages(messages []Message, model string) int { | |||||||
| 	return tokenNum | 	return tokenNum | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func countTokenInput(input any, model string) int { | ||||||
|  | 	switch input.(type) { | ||||||
|  | 	case string: | ||||||
|  | 		return countTokenText(input.(string), model) | ||||||
|  | 	case []string: | ||||||
|  | 		text := "" | ||||||
|  | 		for _, s := range input.([]string) { | ||||||
|  | 			text += s | ||||||
|  | 		} | ||||||
|  | 		return countTokenText(text, model) | ||||||
|  | 	} | ||||||
|  | 	return 0 | ||||||
|  | } | ||||||
|  |  | ||||||
| func countTokenText(text string, model string) int { | func countTokenText(text string, model string) int { | ||||||
| 	tokenEncoder := getTokenEncoder(model) | 	tokenEncoder := getTokenEncoder(model) | ||||||
| 	token := tokenEncoder.Encode(text, nil, nil) | 	token := tokenEncoder.Encode(text, nil, nil) | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ type GeneralOpenAIRequest struct { | |||||||
| 	Temperature float64   `json:"temperature"` | 	Temperature float64   `json:"temperature"` | ||||||
| 	TopP        float64   `json:"top_p"` | 	TopP        float64   `json:"top_p"` | ||||||
| 	N           int       `json:"n"` | 	N           int       `json:"n"` | ||||||
| 	Input       string    `json:"input"` | 	Input       any       `json:"input"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ChatRequest struct { | type ChatRequest struct { | ||||||
| @@ -140,6 +140,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	channelType := c.GetInt("channel") | 	channelType := c.GetInt("channel") | ||||||
| 	tokenId := c.GetInt("token_id") | 	tokenId := c.GetInt("token_id") | ||||||
| 	consumeQuota := c.GetBool("consume_quota") | 	consumeQuota := c.GetBool("consume_quota") | ||||||
|  | 	group := c.GetString("group") | ||||||
| 	var textRequest GeneralOpenAIRequest | 	var textRequest GeneralOpenAIRequest | ||||||
| 	if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM { | 	if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM { | ||||||
| 		err := common.UnmarshalBodyReusable(c, &textRequest) | 		err := common.UnmarshalBodyReusable(c, &textRequest) | ||||||
| @@ -188,13 +189,15 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	case RelayModeCompletions: | 	case RelayModeCompletions: | ||||||
| 		promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) | 		promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) | ||||||
| 	case RelayModeModeration: | 	case RelayModeModeration: | ||||||
| 		promptTokens = countTokenText(textRequest.Input, textRequest.Model) | 		promptTokens = countTokenInput(textRequest.Input, textRequest.Model) | ||||||
| 	} | 	} | ||||||
| 	preConsumedTokens := common.PreConsumedQuota | 	preConsumedTokens := common.PreConsumedQuota | ||||||
| 	if textRequest.MaxTokens != 0 { | 	if textRequest.MaxTokens != 0 { | ||||||
| 		preConsumedTokens = promptTokens + textRequest.MaxTokens | 		preConsumedTokens = promptTokens + textRequest.MaxTokens | ||||||
| 	} | 	} | ||||||
| 	ratio := common.GetModelRatio(textRequest.Model) | 	modelRatio := common.GetModelRatio(textRequest.Model) | ||||||
|  | 	groupRatio := common.GetGroupRatio(group) | ||||||
|  | 	ratio := modelRatio * groupRatio | ||||||
| 	preConsumedQuota := int(float64(preConsumedTokens) * ratio) | 	preConsumedQuota := int(float64(preConsumedTokens) * ratio) | ||||||
| 	if consumeQuota { | 	if consumeQuota { | ||||||
| 		err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota) | 		err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota) | ||||||
| @@ -257,7 +260,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 				common.SysError("Error consuming token remain quota: " + err.Error()) | 				common.SysError("Error consuming token remain quota: " + err.Error()) | ||||||
| 			} | 			} | ||||||
| 			userId := c.GetInt("id") | 			userId := c.GetInt("id") | ||||||
| 			model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度", textRequest.Model, quota)) | 			model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,9 @@ type ModelRequest struct { | |||||||
|  |  | ||||||
| func Distribute() func(c *gin.Context) { | func Distribute() func(c *gin.Context) { | ||||||
| 	return func(c *gin.Context) { | 	return func(c *gin.Context) { | ||||||
|  | 		userId := c.GetInt("id") | ||||||
|  | 		userGroup, _ := model.GetUserGroup(userId) | ||||||
|  | 		c.Set("group", userGroup) | ||||||
| 		var channel *model.Channel | 		var channel *model.Channel | ||||||
| 		channelId, ok := c.Get("channelId") | 		channelId, ok := c.Get("channelId") | ||||||
| 		if ok { | 		if ok { | ||||||
| @@ -70,8 +73,6 @@ func Distribute() func(c *gin.Context) { | |||||||
| 					modelRequest.Model = "text-moderation-stable" | 					modelRequest.Model = "text-moderation-stable" | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			userId := c.GetInt("id") |  | ||||||
| 			userGroup, _ := model.GetUserGroup(userId) |  | ||||||
| 			channel, err = model.GetRandomSatisfiedChannel(userGroup, modelRequest.Model) | 			channel, err = model.GetRandomSatisfiedChannel(userGroup, modelRequest.Model) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				c.JSON(200, gin.H{ | 				c.JSON(200, gin.H{ | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ func InitOptionMap() { | |||||||
| 	common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold) | 	common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold) | ||||||
| 	common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) | 	common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) | ||||||
| 	common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() | 	common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() | ||||||
|  | 	common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString() | ||||||
| 	common.OptionMap["TopUpLink"] = common.TopUpLink | 	common.OptionMap["TopUpLink"] = common.TopUpLink | ||||||
| 	common.OptionMapRWMutex.Unlock() | 	common.OptionMapRWMutex.Unlock() | ||||||
| 	loadOptionsFromDatabase() | 	loadOptionsFromDatabase() | ||||||
| @@ -177,6 +178,8 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 		common.PreConsumedQuota, _ = strconv.Atoi(value) | 		common.PreConsumedQuota, _ = strconv.Atoi(value) | ||||||
| 	case "ModelRatio": | 	case "ModelRatio": | ||||||
| 		err = common.UpdateModelRatioByJSONString(value) | 		err = common.UpdateModelRatioByJSONString(value) | ||||||
|  | 	case "GroupRatio": | ||||||
|  | 		err = common.UpdateGroupRatioByJSONString(value) | ||||||
| 	case "TopUpLink": | 	case "TopUpLink": | ||||||
| 		common.TopUpLink = value | 		common.TopUpLink = value | ||||||
| 	case "ChannelDisableThreshold": | 	case "ChannelDisableThreshold": | ||||||
|   | |||||||
| @@ -98,5 +98,10 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 		logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) | 		logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) | ||||||
| 		logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) | 		logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) | ||||||
| 		logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) | 		logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) | ||||||
|  | 		groupRoute := apiRouter.Group("/group") | ||||||
|  | 		groupRoute.Use(middleware.AdminAuth()) | ||||||
|  | 		{ | ||||||
|  | 			groupRoute.GET("/", controller.GetGroups) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,6 +27,13 @@ function renderType(type) { | |||||||
|   return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>; |   return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function renderBalance(type, balance) { | ||||||
|  |   if (type === 5) { | ||||||
|  |     return <span>¥{(balance / 10000).toFixed(2)}</span> | ||||||
|  |   } | ||||||
|  |   return <span>${balance.toFixed(2)}</span> | ||||||
|  | } | ||||||
|  |  | ||||||
| const ChannelsTable = () => { | const ChannelsTable = () => { | ||||||
|   const [channels, setChannels] = useState([]); |   const [channels, setChannels] = useState([]); | ||||||
|   const [loading, setLoading] = useState(true); |   const [loading, setLoading] = useState(true); | ||||||
| @@ -336,7 +343,7 @@ const ChannelsTable = () => { | |||||||
|                     <Popup |                     <Popup | ||||||
|                       content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'} |                       content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'} | ||||||
|                       key={channel.id} |                       key={channel.id} | ||||||
|                       trigger={<span>${channel.balance.toFixed(2)}</span>} |                       trigger={renderBalance(channel.type, channel.balance)} | ||||||
|                       basic |                       basic | ||||||
|                     /> |                     /> | ||||||
|                   </Table.Cell> |                   </Table.Cell> | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ const SystemSetting = () => { | |||||||
|     QuotaRemindThreshold: 0, |     QuotaRemindThreshold: 0, | ||||||
|     PreConsumedQuota: 0, |     PreConsumedQuota: 0, | ||||||
|     ModelRatio: '', |     ModelRatio: '', | ||||||
|  |     GroupRatio: '', | ||||||
|     TopUpLink: '', |     TopUpLink: '', | ||||||
|     AutomaticDisableChannelEnabled: '', |     AutomaticDisableChannelEnabled: '', | ||||||
|     ChannelDisableThreshold: 0, |     ChannelDisableThreshold: 0, | ||||||
| @@ -101,6 +102,7 @@ const SystemSetting = () => { | |||||||
|       name === 'QuotaRemindThreshold' || |       name === 'QuotaRemindThreshold' || | ||||||
|       name === 'PreConsumedQuota' || |       name === 'PreConsumedQuota' || | ||||||
|       name === 'ModelRatio' || |       name === 'ModelRatio' || | ||||||
|  |       name === 'GroupRatio' || | ||||||
|       name === 'TopUpLink' |       name === 'TopUpLink' | ||||||
|     ) { |     ) { | ||||||
|       setInputs((inputs) => ({ ...inputs, [name]: value })); |       setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
| @@ -131,6 +133,13 @@ const SystemSetting = () => { | |||||||
|       } |       } | ||||||
|       await updateOption('ModelRatio', inputs.ModelRatio); |       await updateOption('ModelRatio', inputs.ModelRatio); | ||||||
|     } |     } | ||||||
|  |     if (originInputs['GroupRatio'] !== inputs.GroupRatio) { | ||||||
|  |       if (!verifyJSON(inputs.GroupRatio)) { | ||||||
|  |         showError('分组倍率不是合法的 JSON 字符串'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       await updateOption('GroupRatio', inputs.GroupRatio); | ||||||
|  |     } | ||||||
|     if (originInputs['TopUpLink'] !== inputs.TopUpLink) { |     if (originInputs['TopUpLink'] !== inputs.TopUpLink) { | ||||||
|       await updateOption('TopUpLink', inputs.TopUpLink); |       await updateOption('TopUpLink', inputs.TopUpLink); | ||||||
|     } |     } | ||||||
| @@ -329,6 +338,17 @@ const SystemSetting = () => { | |||||||
|               placeholder='为一个 JSON 文本,键为模型名称,值为倍率' |               placeholder='为一个 JSON 文本,键为模型名称,值为倍率' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </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={submitOperationConfig}>保存运营设置</Form.Button> |           <Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|   | |||||||
| @@ -10,6 +10,10 @@ export function renderText(text, limit) { | |||||||
| export function renderGroup(group) { | export function renderGroup(group) { | ||||||
|   if (group === "") { |   if (group === "") { | ||||||
|     return <Label>default</Label> |     return <Label>default</Label> | ||||||
|  |   } else if (group === "vip" || group === "pro") { | ||||||
|  |     return <Label color='yellow'>{group}</Label> | ||||||
|  |   } else if (group === "svip" || group === "premium") { | ||||||
|  |     return <Label color='red'>{group}</Label> | ||||||
|   } |   } | ||||||
|   return <Label>{group}</Label> |   return <Label>{group}</Label> | ||||||
| } | } | ||||||
| @@ -21,6 +21,7 @@ const EditChannel = () => { | |||||||
|   const [batch, setBatch] = useState(false); |   const [batch, setBatch] = useState(false); | ||||||
|   const [inputs, setInputs] = useState(originInputs); |   const [inputs, setInputs] = useState(originInputs); | ||||||
|   const [modelOptions, setModelOptions] = useState([]); |   const [modelOptions, setModelOptions] = useState([]); | ||||||
|  |   const [groupOptions, setGroupOptions] = useState([]); | ||||||
|   const [basicModels, setBasicModels] = useState([]); |   const [basicModels, setBasicModels] = useState([]); | ||||||
|   const [fullModels, setFullModels] = useState([]); |   const [fullModels, setFullModels] = useState([]); | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
| @@ -58,11 +59,25 @@ const EditChannel = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const fetchGroups = async () => { | ||||||
|  |     try { | ||||||
|  |       let res = await API.get(`/api/group/`); | ||||||
|  |       setGroupOptions(res.data.data.map((group) => ({ | ||||||
|  |         key: group, | ||||||
|  |         text: group, | ||||||
|  |         value: group, | ||||||
|  |       }))); | ||||||
|  |     } catch (error) { | ||||||
|  |       showError(error.message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (isEdit) { |     if (isEdit) { | ||||||
|       loadChannel().then(); |       loadChannel().then(); | ||||||
|     } |     } | ||||||
|     fetchModels().then(); |     fetchModels().then(); | ||||||
|  |     fetchGroups().then(); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   const submit = async () => { |   const submit = async () => { | ||||||
| @@ -167,13 +182,19 @@ const EditChannel = () => { | |||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Dropdown | ||||||
|               label='分组' |               label='分组' | ||||||
|  |               placeholder={'请选择分组'} | ||||||
|               name='group' |               name='group' | ||||||
|               placeholder={'请输入分组'} |               fluid | ||||||
|  |               search | ||||||
|  |               selection | ||||||
|  |               allowAdditions | ||||||
|  |               additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={inputs.group} |               value={inputs.group} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|  |               options={groupOptions} | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|   | |||||||
| @@ -17,11 +17,24 @@ const EditUser = () => { | |||||||
|     quota: 0, |     quota: 0, | ||||||
|     group: 'default' |     group: 'default' | ||||||
|   }); |   }); | ||||||
|  |   const [groupOptions, setGroupOptions] = useState([]); | ||||||
|   const { username, display_name, password, github_id, wechat_id, email, quota, group } = |   const { username, display_name, password, github_id, wechat_id, email, quota, group } = | ||||||
|     inputs; |     inputs; | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|   }; |   }; | ||||||
|  |   const fetchGroups = async () => { | ||||||
|  |     try { | ||||||
|  |       let res = await API.get(`/api/group/`); | ||||||
|  |       setGroupOptions(res.data.data.map((group) => ({ | ||||||
|  |         key: group, | ||||||
|  |         text: group, | ||||||
|  |         value: group, | ||||||
|  |       }))); | ||||||
|  |     } catch (error) { | ||||||
|  |       showError(error.message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const loadUser = async () => { |   const loadUser = async () => { | ||||||
|     let res = undefined; |     let res = undefined; | ||||||
| @@ -41,6 +54,9 @@ const EditUser = () => { | |||||||
|   }; |   }; | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     loadUser().then(); |     loadUser().then(); | ||||||
|  |     if (userId) { | ||||||
|  |       fetchGroups().then(); | ||||||
|  |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   const submit = async () => { |   const submit = async () => { | ||||||
| @@ -101,13 +117,19 @@ const EditUser = () => { | |||||||
|           { |           { | ||||||
|             userId && <> |             userId && <> | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|                 <Form.Input |                 <Form.Dropdown | ||||||
|                   label='分组' |                   label='分组' | ||||||
|  |                   placeholder={'请选择分组'} | ||||||
|                   name='group' |                   name='group' | ||||||
|                   placeholder={'请输入用户分组'} |                   fluid | ||||||
|  |                   search | ||||||
|  |                   selection | ||||||
|  |                   allowAdditions | ||||||
|  |                   additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} | ||||||
|                   onChange={handleInputChange} |                   onChange={handleInputChange} | ||||||
|                   value={group} |                   value={inputs.group} | ||||||
|                   autoComplete='new-password' |                   autoComplete='new-password' | ||||||
|  |                   options={groupOptions} | ||||||
|                 /> |                 /> | ||||||
|               </Form.Field> |               </Form.Field> | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user