mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			v0.3.3-alp
			...
			v0.3.4-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 54215dc303 | ||
|  | f9f42997b2 | ||
|  | 25eab0b224 | ||
|  | 34bce5b464 | ||
|  | d4794fc051 | ||
|  | 8b43e0dd3f | ||
|  | 92c88fa273 | ||
|  | 38191d55be | ||
|  | d9e39f5906 | ||
|  | 17b7646c12 | 
							
								
								
									
										41
									
								
								controller/billing.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								controller/billing.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package controller | ||||
|  | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"one-api/model" | ||||
| ) | ||||
|  | ||||
| func GetSubscription(c *gin.Context) { | ||||
| 	userId := c.GetInt("id") | ||||
| 	quota, err := model.GetUserQuota(userId) | ||||
| 	if err != nil { | ||||
| 		openAIError := OpenAIError{ | ||||
| 			Message: err.Error(), | ||||
| 			Type:    "one_api_error", | ||||
| 		} | ||||
| 		c.JSON(200, gin.H{ | ||||
| 			"error": openAIError, | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	subscription := OpenAISubscriptionResponse{ | ||||
| 		Object:             "billing_subscription", | ||||
| 		HasPaymentMethod:   true, | ||||
| 		SoftLimitUSD:       float64(quota), | ||||
| 		HardLimitUSD:       float64(quota), | ||||
| 		SystemHardLimitUSD: float64(quota), | ||||
| 	} | ||||
| 	c.JSON(200, subscription) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func GetUsage(c *gin.Context) { | ||||
| 	//userId := c.GetInt("id") | ||||
| 	// TODO: get usage from database | ||||
| 	usage := OpenAIUsageResponse{ | ||||
| 		Object:     "list", | ||||
| 		TotalUsage: 0, | ||||
| 	} | ||||
| 	c.JSON(200, usage) | ||||
| 	return | ||||
| } | ||||
| @@ -13,12 +13,27 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // https://github.com/songquanpeng/one-api/issues/79 | ||||
|  | ||||
| type OpenAISubscriptionResponse struct { | ||||
| 	HasPaymentMethod bool    `json:"has_payment_method"` | ||||
| 	HardLimitUSD     float64 `json:"hard_limit_usd"` | ||||
| 	Object             string  `json:"object"` | ||||
| 	HasPaymentMethod   bool    `json:"has_payment_method"` | ||||
| 	SoftLimitUSD       float64 `json:"soft_limit_usd"` | ||||
| 	HardLimitUSD       float64 `json:"hard_limit_usd"` | ||||
| 	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` | ||||
| } | ||||
|  | ||||
| type OpenAIUsageDailyCost struct { | ||||
| 	Timestamp float64 `json:"timestamp"` | ||||
| 	LineItems []struct { | ||||
| 		Name string  `json:"name"` | ||||
| 		Cost float64 `json:"cost"` | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type OpenAIUsageResponse struct { | ||||
| 	Object string `json:"object"` | ||||
| 	//DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"` | ||||
| 	TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar | ||||
| } | ||||
|  | ||||
| @@ -27,6 +42,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { | ||||
| 	switch channel.Type { | ||||
| 	case common.ChannelTypeAzure: | ||||
| 		return 0, errors.New("尚未实现") | ||||
| 	case common.ChannelTypeCustom: | ||||
| 		baseURL = channel.BaseURL | ||||
| 	} | ||||
| 	url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) | ||||
|  | ||||
| @@ -127,6 +144,10 @@ func updateAllChannelsBalance() error { | ||||
| 		if channel.Status != common.ChannelStatusEnabled { | ||||
| 			continue | ||||
| 		} | ||||
| 		// TODO: support Azure | ||||
| 		if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom { | ||||
| 			continue | ||||
| 		} | ||||
| 		balance, err := updateChannelBalance(channel) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
|   | ||||
							
								
								
									
										199
									
								
								controller/channel-test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								controller/channel-test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| package controller | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"net/http" | ||||
| 	"one-api/common" | ||||
| 	"one-api/model" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func testChannel(channel *model.Channel, request *ChatRequest) error { | ||||
| 	if request.Model == "" { | ||||
| 		request.Model = "gpt-3.5-turbo" | ||||
| 		if channel.Type == common.ChannelTypeAzure { | ||||
| 			request.Model = "gpt-35-turbo" | ||||
| 		} | ||||
| 	} | ||||
| 	requestURL := common.ChannelBaseURLs[channel.Type] | ||||
| 	if channel.Type == common.ChannelTypeAzure { | ||||
| 		requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) | ||||
| 	} else { | ||||
| 		if channel.Type == common.ChannelTypeCustom { | ||||
| 			requestURL = channel.BaseURL | ||||
| 		} | ||||
| 		requestURL += "/v1/chat/completions" | ||||
| 	} | ||||
|  | ||||
| 	jsonData, err := json.Marshal(request) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if channel.Type == common.ChannelTypeAzure { | ||||
| 		req.Header.Set("api-key", channel.Key) | ||||
| 	} else { | ||||
| 		req.Header.Set("Authorization", "Bearer "+channel.Key) | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 	client := &http.Client{} | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	var response TextResponse | ||||
| 	err = json.NewDecoder(resp.Body).Decode(&response) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if response.Error.Message != "" || response.Error.Code != "" { | ||||
| 		return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func buildTestRequest(c *gin.Context) *ChatRequest { | ||||
| 	model_ := c.Query("model") | ||||
| 	testRequest := &ChatRequest{ | ||||
| 		Model:     model_, | ||||
| 		MaxTokens: 1, | ||||
| 	} | ||||
| 	testMessage := Message{ | ||||
| 		Role:    "user", | ||||
| 		Content: "hi", | ||||
| 	} | ||||
| 	testRequest.Messages = append(testRequest.Messages, testMessage) | ||||
| 	return testRequest | ||||
| } | ||||
|  | ||||
| func TestChannel(c *gin.Context) { | ||||
| 	id, err := strconv.Atoi(c.Param("id")) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	channel, err := model.GetChannelById(id, true) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	testRequest := buildTestRequest(c) | ||||
| 	tik := time.Now() | ||||
| 	err = testChannel(channel, testRequest) | ||||
| 	tok := time.Now() | ||||
| 	milliseconds := tok.Sub(tik).Milliseconds() | ||||
| 	go channel.UpdateResponseTime(milliseconds) | ||||
| 	consumedTime := float64(milliseconds) / 1000.0 | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 			"time":    consumedTime, | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, gin.H{ | ||||
| 		"success": true, | ||||
| 		"message": "", | ||||
| 		"time":    consumedTime, | ||||
| 	}) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| var testAllChannelsLock sync.Mutex | ||||
| var testAllChannelsRunning bool = false | ||||
|  | ||||
| // disable & notify | ||||
| func disableChannel(channelId int, channelName string, reason string) { | ||||
| 	if common.RootUserEmail == "" { | ||||
| 		common.RootUserEmail = model.GetRootUserEmail() | ||||
| 	} | ||||
| 	model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled) | ||||
| 	subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId) | ||||
| 	content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) | ||||
| 	err := common.SendEmail(subject, common.RootUserEmail, content) | ||||
| 	if err != nil { | ||||
| 		common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testAllChannels(c *gin.Context) error { | ||||
| 	testAllChannelsLock.Lock() | ||||
| 	if testAllChannelsRunning { | ||||
| 		testAllChannelsLock.Unlock() | ||||
| 		return errors.New("测试已在运行中") | ||||
| 	} | ||||
| 	testAllChannelsRunning = true | ||||
| 	testAllChannelsLock.Unlock() | ||||
| 	channels, err := model.GetAllChannels(0, 0, true) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return err | ||||
| 	} | ||||
| 	testRequest := buildTestRequest(c) | ||||
| 	var disableThreshold = int64(common.ChannelDisableThreshold * 1000) | ||||
| 	if disableThreshold == 0 { | ||||
| 		disableThreshold = 10000000 // a impossible value | ||||
| 	} | ||||
| 	go func() { | ||||
| 		for _, channel := range channels { | ||||
| 			if channel.Status != common.ChannelStatusEnabled { | ||||
| 				continue | ||||
| 			} | ||||
| 			tik := time.Now() | ||||
| 			err := testChannel(channel, testRequest) | ||||
| 			tok := time.Now() | ||||
| 			milliseconds := tok.Sub(tik).Milliseconds() | ||||
| 			if err != nil || milliseconds > disableThreshold { | ||||
| 				if milliseconds > disableThreshold { | ||||
| 					err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) | ||||
| 				} | ||||
| 				disableChannel(channel.Id, channel.Name, err.Error()) | ||||
| 			} | ||||
| 			channel.UpdateResponseTime(milliseconds) | ||||
| 		} | ||||
| 		err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") | ||||
| 		if err != nil { | ||||
| 			common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||
| 		} | ||||
| 		testAllChannelsLock.Lock() | ||||
| 		testAllChannelsRunning = false | ||||
| 		testAllChannelsLock.Unlock() | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestAllChannels(c *gin.Context) { | ||||
| 	err := testAllChannels(c) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, gin.H{ | ||||
| 		"success": true, | ||||
| 		"message": "", | ||||
| 	}) | ||||
| 	return | ||||
| } | ||||
| @@ -1,18 +1,12 @@ | ||||
| package controller | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"net/http" | ||||
| 	"one-api/common" | ||||
| 	"one-api/model" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func GetAllChannels(c *gin.Context) { | ||||
| @@ -158,187 +152,3 @@ func UpdateChannel(c *gin.Context) { | ||||
| 	}) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func testChannel(channel *model.Channel, request *ChatRequest) error { | ||||
| 	if request.Model == "" { | ||||
| 		request.Model = "gpt-3.5-turbo" | ||||
| 		if channel.Type == common.ChannelTypeAzure { | ||||
| 			request.Model = "gpt-35-turbo" | ||||
| 		} | ||||
| 	} | ||||
| 	requestURL := common.ChannelBaseURLs[channel.Type] | ||||
| 	if channel.Type == common.ChannelTypeAzure { | ||||
| 		requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) | ||||
| 	} else { | ||||
| 		if channel.Type == common.ChannelTypeCustom { | ||||
| 			requestURL = channel.BaseURL | ||||
| 		} | ||||
| 		requestURL += "/v1/chat/completions" | ||||
| 	} | ||||
|  | ||||
| 	jsonData, err := json.Marshal(request) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if channel.Type == common.ChannelTypeAzure { | ||||
| 		req.Header.Set("api-key", channel.Key) | ||||
| 	} else { | ||||
| 		req.Header.Set("Authorization", "Bearer "+channel.Key) | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 	client := &http.Client{} | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	var response TextResponse | ||||
| 	err = json.NewDecoder(resp.Body).Decode(&response) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if response.Error.Message != "" { | ||||
| 		return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func buildTestRequest(c *gin.Context) *ChatRequest { | ||||
| 	model_ := c.Query("model") | ||||
| 	testRequest := &ChatRequest{ | ||||
| 		Model:     model_, | ||||
| 		MaxTokens: 1, | ||||
| 	} | ||||
| 	testMessage := Message{ | ||||
| 		Role:    "user", | ||||
| 		Content: "hi", | ||||
| 	} | ||||
| 	testRequest.Messages = append(testRequest.Messages, testMessage) | ||||
| 	return testRequest | ||||
| } | ||||
|  | ||||
| func TestChannel(c *gin.Context) { | ||||
| 	id, err := strconv.Atoi(c.Param("id")) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	channel, err := model.GetChannelById(id, true) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	testRequest := buildTestRequest(c) | ||||
| 	tik := time.Now() | ||||
| 	err = testChannel(channel, testRequest) | ||||
| 	tok := time.Now() | ||||
| 	milliseconds := tok.Sub(tik).Milliseconds() | ||||
| 	go channel.UpdateResponseTime(milliseconds) | ||||
| 	consumedTime := float64(milliseconds) / 1000.0 | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 			"time":    consumedTime, | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, gin.H{ | ||||
| 		"success": true, | ||||
| 		"message": "", | ||||
| 		"time":    consumedTime, | ||||
| 	}) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| var testAllChannelsLock sync.Mutex | ||||
| var testAllChannelsRunning bool = false | ||||
|  | ||||
| // disable & notify | ||||
| func disableChannel(channelId int, channelName string, reason string) { | ||||
| 	if common.RootUserEmail == "" { | ||||
| 		common.RootUserEmail = model.GetRootUserEmail() | ||||
| 	} | ||||
| 	model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled) | ||||
| 	subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId) | ||||
| 	content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) | ||||
| 	err := common.SendEmail(subject, common.RootUserEmail, content) | ||||
| 	if err != nil { | ||||
| 		common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testAllChannels(c *gin.Context) error { | ||||
| 	testAllChannelsLock.Lock() | ||||
| 	if testAllChannelsRunning { | ||||
| 		testAllChannelsLock.Unlock() | ||||
| 		return errors.New("测试已在运行中") | ||||
| 	} | ||||
| 	testAllChannelsRunning = true | ||||
| 	testAllChannelsLock.Unlock() | ||||
| 	channels, err := model.GetAllChannels(0, 0, true) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return err | ||||
| 	} | ||||
| 	testRequest := buildTestRequest(c) | ||||
| 	var disableThreshold = int64(common.ChannelDisableThreshold * 1000) | ||||
| 	if disableThreshold == 0 { | ||||
| 		disableThreshold = 10000000 // a impossible value | ||||
| 	} | ||||
| 	go func() { | ||||
| 		for _, channel := range channels { | ||||
| 			if channel.Status != common.ChannelStatusEnabled { | ||||
| 				continue | ||||
| 			} | ||||
| 			tik := time.Now() | ||||
| 			err := testChannel(channel, testRequest) | ||||
| 			tok := time.Now() | ||||
| 			milliseconds := tok.Sub(tik).Milliseconds() | ||||
| 			if err != nil || milliseconds > disableThreshold { | ||||
| 				if milliseconds > disableThreshold { | ||||
| 					err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) | ||||
| 				} | ||||
| 				disableChannel(channel.Id, channel.Name, err.Error()) | ||||
| 			} | ||||
| 			channel.UpdateResponseTime(milliseconds) | ||||
| 		} | ||||
| 		err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") | ||||
| 		if err != nil { | ||||
| 			common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||
| 		} | ||||
| 		testAllChannelsLock.Lock() | ||||
| 		testAllChannelsRunning = false | ||||
| 		testAllChannelsLock.Unlock() | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestAllChannels(c *gin.Context) { | ||||
| 	err := testAllChannels(c) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, gin.H{ | ||||
| 		"success": true, | ||||
| 		"message": "", | ||||
| 	}) | ||||
| 	return | ||||
| } | ||||
|   | ||||
| @@ -89,8 +89,8 @@ func Relay(c *gin.Context) { | ||||
| 		}) | ||||
| 		channelId := c.GetInt("channel_id") | ||||
| 		common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message)) | ||||
| 		if err.Type != "invalid_request_error" && err.StatusCode != http.StatusTooManyRequests && | ||||
| 			common.AutomaticDisableChannelEnabled { | ||||
| 		// https://platform.openai.com/docs/guides/error-codes/api-errors | ||||
| 		if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") { | ||||
| 			channelId := c.GetInt("channel_id") | ||||
| 			channelName := c.GetString("channel_name") | ||||
| 			disableChannel(channelId, channelName, err.Message) | ||||
|   | ||||
| @@ -6,7 +6,11 @@ import ( | ||||
|  | ||||
| func Cache() func(c *gin.Context) { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Header("Cache-Control", "max-age=604800") // one week | ||||
| 		if c.Request.RequestURI == "/" { | ||||
| 			c.Header("Cache-Control", "no-cache") | ||||
| 		} else { | ||||
| 			c.Header("Cache-Control", "max-age=604800") // one week | ||||
| 		} | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -8,11 +8,14 @@ import ( | ||||
| ) | ||||
|  | ||||
| func SetDashboardRouter(router *gin.Engine) { | ||||
| 	apiRouter := router.Group("/dashboard") | ||||
| 	apiRouter := router.Group("/") | ||||
| 	apiRouter.Use(gzip.Gzip(gzip.DefaultCompression)) | ||||
| 	apiRouter.Use(middleware.GlobalAPIRateLimit()) | ||||
| 	apiRouter.Use(middleware.TokenAuth()) | ||||
| 	{ | ||||
| 		apiRouter.GET("/billing/credit_grants", controller.GetTokenStatus) | ||||
| 		apiRouter.GET("/dashboard/billing/subscription", controller.GetSubscription) | ||||
| 		apiRouter.GET("/v1/dashboard/billing/subscription", controller.GetSubscription) | ||||
| 		apiRouter.GET("/dashboard/billing/usage", controller.GetUsage) | ||||
| 		apiRouter.GET("/v1/dashboard/billing/usage", controller.GetUsage) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { | ||||
| 	router.Use(middleware.Cache()) | ||||
| 	router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) | ||||
| 	router.NoRoute(func(c *gin.Context) { | ||||
| 		c.Header("Cache-Control", "no-cache") | ||||
| 		c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -405,7 +405,7 @@ const ChannelsTable = () => { | ||||
|               <Button size='small' loading={loading} onClick={testAllChannels}> | ||||
|                 测试所有已启用通道 | ||||
|               </Button> | ||||
|               <Button size='small' onClick={updateAllChannelsBalance} loading={updatingBalance}>更新所有已启用通道余额</Button> | ||||
|               <Button size='small' onClick={updateAllChannelsBalance} loading={loading || updatingBalance}>更新所有已启用通道余额</Button> | ||||
|               <Pagination | ||||
|                 floated='right' | ||||
|                 activePage={activePage} | ||||
|   | ||||
| @@ -167,7 +167,7 @@ const EditChannel = () => { | ||||
|               /> | ||||
|             ) | ||||
|           } | ||||
|           <Button onClick={submit}>提交</Button> | ||||
|           <Button positive onClick={submit}>提交</Button> | ||||
|         </Form> | ||||
|       </Segment> | ||||
|     </> | ||||
|   | ||||
| @@ -111,7 +111,7 @@ const EditRedemption = () => { | ||||
|               </Form.Field> | ||||
|             </> | ||||
|           } | ||||
|           <Button onClick={submit}>提交</Button> | ||||
|           <Button positive onClick={submit}>提交</Button> | ||||
|         </Form> | ||||
|       </Segment> | ||||
|     </> | ||||
|   | ||||
| @@ -106,6 +106,34 @@ const EditToken = () => { | ||||
|               required={!isEdit} | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Form.Field> | ||||
|             <Form.Input | ||||
|               label='过期时间' | ||||
|               name='expired_time' | ||||
|               placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'} | ||||
|               onChange={handleInputChange} | ||||
|               value={expired_time} | ||||
|               autoComplete='new-password' | ||||
|               type='datetime-local' | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <div style={{ lineHeight: '40px' }}> | ||||
|             <Button type={'button'} onClick={() => { | ||||
|               setExpiredTime(0, 0, 0, 0); | ||||
|             }}>永不过期</Button> | ||||
|             <Button type={'button'} onClick={() => { | ||||
|               setExpiredTime(1, 0, 0, 0); | ||||
|             }}>一个月后过期</Button> | ||||
|             <Button type={'button'} onClick={() => { | ||||
|               setExpiredTime(0, 1, 0, 0); | ||||
|             }}>一天后过期</Button> | ||||
|             <Button type={'button'} onClick={() => { | ||||
|               setExpiredTime(0, 0, 1, 0); | ||||
|             }}>一小时后过期</Button> | ||||
|             <Button type={'button'} onClick={() => { | ||||
|               setExpiredTime(0, 0, 0, 1); | ||||
|             }}>一分钟后过期</Button> | ||||
|           </div> | ||||
|           <Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message> | ||||
|           <Form.Field> | ||||
|             <Form.Input | ||||
| @@ -119,36 +147,10 @@ const EditToken = () => { | ||||
|               disabled={unlimited_quota} | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Button type={'button'} style={{ marginBottom: '14px' }} onClick={() => { | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setUnlimitedQuota(); | ||||
|           }}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button> | ||||
|           <Form.Field> | ||||
|             <Form.Input | ||||
|               label='过期时间' | ||||
|               name='expired_time' | ||||
|               placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'} | ||||
|               onChange={handleInputChange} | ||||
|               value={expired_time} | ||||
|               autoComplete='new-password' | ||||
|               type='datetime-local' | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setExpiredTime(0, 0, 0, 0); | ||||
|           }}>永不过期</Button> | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setExpiredTime(1, 0, 0, 0); | ||||
|           }}>一个月后过期</Button> | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setExpiredTime(0, 1, 0, 0); | ||||
|           }}>一天后过期</Button> | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setExpiredTime(0, 0, 1, 0); | ||||
|           }}>一小时后过期</Button> | ||||
|           <Button type={'button'} onClick={() => { | ||||
|             setExpiredTime(0, 0, 0, 1); | ||||
|           }}>一分钟后过期</Button> | ||||
|           <Button onClick={submit}>提交</Button> | ||||
|           <Button positive onClick={submit}>提交</Button> | ||||
|         </Form> | ||||
|       </Segment> | ||||
|     </> | ||||
|   | ||||
| @@ -65,7 +65,7 @@ const AddUser = () => { | ||||
|               required | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Button type={'submit'} onClick={submit}> | ||||
|           <Button positive type={'submit'} onClick={submit}> | ||||
|             提交 | ||||
|           </Button> | ||||
|         </Form> | ||||
|   | ||||
| @@ -142,7 +142,7 @@ const EditUser = () => { | ||||
|               readOnly | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Button onClick={submit}>提交</Button> | ||||
|           <Button positive onClick={submit}>提交</Button> | ||||
|         </Form> | ||||
|       </Segment> | ||||
|     </> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user