mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 07:43:41 +08:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v0.3.3-alp
			...
			v0.3.3-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					92c88fa273 | ||
| 
						 | 
					38191d55be | ||
| 
						 | 
					d9e39f5906 | ||
| 
						 | 
					17b7646c12 | ||
| 
						 | 
					171b818504 | ||
| 
						 | 
					bcca0cc0bc | ||
| 
						 | 
					b92ec5e54c | ||
| 
						 | 
					fa79e8b7a3 | ||
| 
						 | 
					1cc7c20183 | 
@@ -129,6 +129,7 @@ const (
 | 
				
			|||||||
	ChannelTypeCustom    = 8
 | 
						ChannelTypeCustom    = 8
 | 
				
			||||||
	ChannelTypeAILS      = 9
 | 
						ChannelTypeAILS      = 9
 | 
				
			||||||
	ChannelTypeAIProxy   = 10
 | 
						ChannelTypeAIProxy   = 10
 | 
				
			||||||
 | 
						ChannelTypePaLM      = 11
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var ChannelBaseURLs = []string{
 | 
					var ChannelBaseURLs = []string{
 | 
				
			||||||
@@ -143,4 +144,5 @@ var ChannelBaseURLs = []string{
 | 
				
			|||||||
	"",                            // 8
 | 
						"",                            // 8
 | 
				
			||||||
	"https://api.caipacity.com",   // 9
 | 
						"https://api.caipacity.com",   // 9
 | 
				
			||||||
	"https://api.aiproxy.io",      // 10
 | 
						"https://api.aiproxy.io",      // 10
 | 
				
			||||||
 | 
						"",                            // 11
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										160
									
								
								controller/channel-billing.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								controller/channel-billing.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					package controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"one-api/common"
 | 
				
			||||||
 | 
						"one-api/model"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OpenAISubscriptionResponse struct {
 | 
				
			||||||
 | 
						HasPaymentMethod bool    `json:"has_payment_method"`
 | 
				
			||||||
 | 
						HardLimitUSD     float64 `json:"hard_limit_usd"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OpenAIUsageResponse struct {
 | 
				
			||||||
 | 
						TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func updateChannelBalance(channel *model.Channel) (float64, error) {
 | 
				
			||||||
 | 
						baseURL := common.ChannelBaseURLs[channel.Type]
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client := &http.Client{}
 | 
				
			||||||
 | 
						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 {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						subscription := OpenAISubscriptionResponse{}
 | 
				
			||||||
 | 
						err = json.Unmarshal(body, &subscription)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
						startDate := fmt.Sprintf("%s-01", now.Format("2006-01"))
 | 
				
			||||||
 | 
						//endDate := now.Format("2006-01-02")
 | 
				
			||||||
 | 
						url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, "2023-06-01")
 | 
				
			||||||
 | 
						req, err = http.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
						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 {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						usage := OpenAIUsageResponse{}
 | 
				
			||||||
 | 
						err = json.Unmarshal(body, &usage)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						balance := subscription.HardLimitUSD - usage.TotalUsage/100
 | 
				
			||||||
 | 
						channel.UpdateBalance(balance)
 | 
				
			||||||
 | 
						return balance, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UpdateChannelBalance(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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						balance, err := updateChannelBalance(channel)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
								"success": false,
 | 
				
			||||||
 | 
								"message": err.Error(),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, gin.H{
 | 
				
			||||||
 | 
							"success": true,
 | 
				
			||||||
 | 
							"message": "",
 | 
				
			||||||
 | 
							"balance": balance,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func updateAllChannelsBalance() error {
 | 
				
			||||||
 | 
						channels, err := model.GetAllChannels(0, 0, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, channel := range channels {
 | 
				
			||||||
 | 
							if channel.Status != common.ChannelStatusEnabled {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							balance, err := updateChannelBalance(channel)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// err is nil & balance <= 0 means quota is used up
 | 
				
			||||||
 | 
								if balance <= 0 {
 | 
				
			||||||
 | 
									disableChannel(channel.Id, channel.Name, "余额不足")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UpdateAllChannelsBalance(c *gin.Context) {
 | 
				
			||||||
 | 
						// TODO: make it async
 | 
				
			||||||
 | 
						err := updateAllChannelsBalance()
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -201,7 +201,7 @@ func testChannel(channel *model.Channel, request *ChatRequest) error {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if response.Error.Message != "" {
 | 
						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 errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										59
									
								
								controller/relay-palm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								controller/relay-palm.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					package controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PaLMChatMessage struct {
 | 
				
			||||||
 | 
						Author  string `json:"author"`
 | 
				
			||||||
 | 
						Content string `json:"content"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PaLMFilter struct {
 | 
				
			||||||
 | 
						Reason  string `json:"reason"`
 | 
				
			||||||
 | 
						Message string `json:"message"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body
 | 
				
			||||||
 | 
					type PaLMChatRequest struct {
 | 
				
			||||||
 | 
						Prompt         []Message `json:"prompt"`
 | 
				
			||||||
 | 
						Temperature    float64   `json:"temperature"`
 | 
				
			||||||
 | 
						CandidateCount int       `json:"candidateCount"`
 | 
				
			||||||
 | 
						TopP           float64   `json:"topP"`
 | 
				
			||||||
 | 
						TopK           int       `json:"topK"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#response-body
 | 
				
			||||||
 | 
					type PaLMChatResponse struct {
 | 
				
			||||||
 | 
						Candidates []Message    `json:"candidates"`
 | 
				
			||||||
 | 
						Messages   []Message    `json:"messages"`
 | 
				
			||||||
 | 
						Filters    []PaLMFilter `json:"filters"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func relayPaLM(openAIRequest GeneralOpenAIRequest, c *gin.Context) *OpenAIErrorWithStatusCode {
 | 
				
			||||||
 | 
						// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage
 | 
				
			||||||
 | 
						messages := make([]PaLMChatMessage, 0, len(openAIRequest.Messages))
 | 
				
			||||||
 | 
						for _, message := range openAIRequest.Messages {
 | 
				
			||||||
 | 
							var author string
 | 
				
			||||||
 | 
							if message.Role == "user" {
 | 
				
			||||||
 | 
								author = "0"
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								author = "1"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							messages = append(messages, PaLMChatMessage{
 | 
				
			||||||
 | 
								Author:  author,
 | 
				
			||||||
 | 
								Content: message.Content,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						request := PaLMChatRequest{
 | 
				
			||||||
 | 
							Prompt:         nil,
 | 
				
			||||||
 | 
							Temperature:    openAIRequest.Temperature,
 | 
				
			||||||
 | 
							CandidateCount: openAIRequest.N,
 | 
				
			||||||
 | 
							TopP:           openAIRequest.TopP,
 | 
				
			||||||
 | 
							TopK:           openAIRequest.MaxTokens,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// TODO: forward request to PaLM & convert response
 | 
				
			||||||
 | 
						fmt.Print(request)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,7 +15,11 @@ func getTokenEncoder(model string) *tiktoken.Tiktoken {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	tokenEncoder, err := tiktoken.EncodingForModel(model)
 | 
						tokenEncoder, err := tiktoken.EncodingForModel(model)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		common.FatalLog(fmt.Sprintf("failed to get token encoder for model %s: %s", model, err.Error()))
 | 
							common.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error()))
 | 
				
			||||||
 | 
							tokenEncoder, err = tiktoken.EncodingForModel("gpt-3.5-turbo")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								common.FatalLog(fmt.Sprintf("failed to get token encoder for model gpt-3.5-turbo: %s", err.Error()))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	tokenEncoderMap[model] = tokenEncoder
 | 
						tokenEncoderMap[model] = tokenEncoder
 | 
				
			||||||
	return tokenEncoder
 | 
						return tokenEncoder
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,19 @@ type Message struct {
 | 
				
			|||||||
	Name    *string `json:"name,omitempty"`
 | 
						Name    *string `json:"name,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://platform.openai.com/docs/api-reference/chat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type GeneralOpenAIRequest struct {
 | 
				
			||||||
 | 
						Model       string    `json:"model"`
 | 
				
			||||||
 | 
						Messages    []Message `json:"messages"`
 | 
				
			||||||
 | 
						Prompt      string    `json:"prompt"`
 | 
				
			||||||
 | 
						Stream      bool      `json:"stream"`
 | 
				
			||||||
 | 
						MaxTokens   int       `json:"max_tokens"`
 | 
				
			||||||
 | 
						Temperature float64   `json:"temperature"`
 | 
				
			||||||
 | 
						TopP        float64   `json:"top_p"`
 | 
				
			||||||
 | 
						N           int       `json:"n"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChatRequest struct {
 | 
					type ChatRequest struct {
 | 
				
			||||||
	Model     string    `json:"model"`
 | 
						Model     string    `json:"model"`
 | 
				
			||||||
	Messages  []Message `json:"messages"`
 | 
						Messages  []Message `json:"messages"`
 | 
				
			||||||
@@ -76,8 +89,8 @@ func Relay(c *gin.Context) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		channelId := c.GetInt("channel_id")
 | 
							channelId := c.GetInt("channel_id")
 | 
				
			||||||
		common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message))
 | 
							common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message))
 | 
				
			||||||
		if err.Type != "invalid_request_error" && err.StatusCode != http.StatusTooManyRequests &&
 | 
							// https://platform.openai.com/docs/guides/error-codes/api-errors
 | 
				
			||||||
			common.AutomaticDisableChannelEnabled {
 | 
							if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") {
 | 
				
			||||||
			channelId := c.GetInt("channel_id")
 | 
								channelId := c.GetInt("channel_id")
 | 
				
			||||||
			channelName := c.GetString("channel_name")
 | 
								channelName := c.GetString("channel_name")
 | 
				
			||||||
			disableChannel(channelId, channelName, err.Message)
 | 
								disableChannel(channelId, channelName, err.Message)
 | 
				
			||||||
@@ -101,8 +114,8 @@ func relayHelper(c *gin.Context) *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")
 | 
				
			||||||
	var textRequest TextRequest
 | 
						var textRequest GeneralOpenAIRequest
 | 
				
			||||||
	if consumeQuota || channelType == common.ChannelTypeAzure {
 | 
						if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM {
 | 
				
			||||||
		requestBody, err := io.ReadAll(c.Request.Body)
 | 
							requestBody, err := io.ReadAll(c.Request.Body)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return errorWrapper(err, "read_request_body_failed", http.StatusBadRequest)
 | 
								return errorWrapper(err, "read_request_body_failed", http.StatusBadRequest)
 | 
				
			||||||
@@ -141,6 +154,9 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
		model_ = strings.TrimSuffix(model_, "-0301")
 | 
							model_ = strings.TrimSuffix(model_, "-0301")
 | 
				
			||||||
		model_ = strings.TrimSuffix(model_, "-0314")
 | 
							model_ = strings.TrimSuffix(model_, "-0314")
 | 
				
			||||||
		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
							fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
				
			||||||
 | 
						} else if channelType == common.ChannelTypePaLM {
 | 
				
			||||||
 | 
							err := relayPaLM(textRequest, c)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model)
 | 
						promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,8 @@ type Channel struct {
 | 
				
			|||||||
	ResponseTime       int     `json:"response_time"` // in milliseconds
 | 
						ResponseTime       int     `json:"response_time"` // in milliseconds
 | 
				
			||||||
	BaseURL            string  `json:"base_url" gorm:"column:base_url"`
 | 
						BaseURL            string  `json:"base_url" gorm:"column:base_url"`
 | 
				
			||||||
	Other              string  `json:"other"`
 | 
						Other              string  `json:"other"`
 | 
				
			||||||
 | 
						Balance            float64 `json:"balance"` // in USD
 | 
				
			||||||
 | 
						BalanceUpdatedTime int64   `json:"balance_updated_time" gorm:"bigint"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
 | 
					func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
 | 
				
			||||||
@@ -86,6 +88,16 @@ func (channel *Channel) UpdateResponseTime(responseTime int64) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (channel *Channel) UpdateBalance(balance float64) {
 | 
				
			||||||
 | 
						err := DB.Model(channel).Select("balance_updated_time", "balance").Updates(Channel{
 | 
				
			||||||
 | 
							BalanceUpdatedTime: common.GetTimestamp(),
 | 
				
			||||||
 | 
							Balance:            balance,
 | 
				
			||||||
 | 
						}).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							common.SysError("failed to update balance: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (channel *Channel) Delete() error {
 | 
					func (channel *Channel) Delete() error {
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	err = DB.Delete(channel).Error
 | 
						err = DB.Delete(channel).Error
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,6 +66,8 @@ func SetApiRouter(router *gin.Engine) {
 | 
				
			|||||||
			channelRoute.GET("/:id", controller.GetChannel)
 | 
								channelRoute.GET("/:id", controller.GetChannel)
 | 
				
			||||||
			channelRoute.GET("/test", controller.TestAllChannels)
 | 
								channelRoute.GET("/test", controller.TestAllChannels)
 | 
				
			||||||
			channelRoute.GET("/test/:id", controller.TestChannel)
 | 
								channelRoute.GET("/test/:id", controller.TestChannel)
 | 
				
			||||||
 | 
								channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
 | 
				
			||||||
 | 
								channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
 | 
				
			||||||
			channelRoute.POST("/", controller.AddChannel)
 | 
								channelRoute.POST("/", controller.AddChannel)
 | 
				
			||||||
			channelRoute.PUT("/", controller.UpdateChannel)
 | 
								channelRoute.PUT("/", controller.UpdateChannel)
 | 
				
			||||||
			channelRoute.DELETE("/:id", controller.DeleteChannel)
 | 
								channelRoute.DELETE("/:id", controller.DeleteChannel)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
 | 
				
			|||||||
	router.Use(middleware.Cache())
 | 
						router.Use(middleware.Cache())
 | 
				
			||||||
	router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
 | 
						router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
 | 
				
			||||||
	router.NoRoute(func(c *gin.Context) {
 | 
						router.NoRoute(func(c *gin.Context) {
 | 
				
			||||||
 | 
							c.Header("Cache-Control", "no-cache")
 | 
				
			||||||
		c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)
 | 
							c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,7 @@ const ChannelsTable = () => {
 | 
				
			|||||||
  const [activePage, setActivePage] = useState(1);
 | 
					  const [activePage, setActivePage] = useState(1);
 | 
				
			||||||
  const [searchKeyword, setSearchKeyword] = useState('');
 | 
					  const [searchKeyword, setSearchKeyword] = useState('');
 | 
				
			||||||
  const [searching, setSearching] = useState(false);
 | 
					  const [searching, setSearching] = useState(false);
 | 
				
			||||||
 | 
					  const [updatingBalance, setUpdatingBalance] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loadChannels = async (startIdx) => {
 | 
					  const loadChannels = async (startIdx) => {
 | 
				
			||||||
    const res = await API.get(`/api/channel/?p=${startIdx}`);
 | 
					    const res = await API.get(`/api/channel/?p=${startIdx}`);
 | 
				
			||||||
@@ -63,7 +64,7 @@ const ChannelsTable = () => {
 | 
				
			|||||||
  const refresh = async () => {
 | 
					  const refresh = async () => {
 | 
				
			||||||
    setLoading(true);
 | 
					    setLoading(true);
 | 
				
			||||||
    await loadChannels(0);
 | 
					    await loadChannels(0);
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    loadChannels(0)
 | 
					    loadChannels(0)
 | 
				
			||||||
@@ -127,7 +128,7 @@ const ChannelsTable = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const renderResponseTime = (responseTime) => {
 | 
					  const renderResponseTime = (responseTime) => {
 | 
				
			||||||
    let time = responseTime / 1000;
 | 
					    let time = responseTime / 1000;
 | 
				
			||||||
    time = time.toFixed(2) + " 秒";
 | 
					    time = time.toFixed(2) + ' 秒';
 | 
				
			||||||
    if (responseTime === 0) {
 | 
					    if (responseTime === 0) {
 | 
				
			||||||
      return <Label basic color='grey'>未测试</Label>;
 | 
					      return <Label basic color='grey'>未测试</Label>;
 | 
				
			||||||
    } else if (responseTime <= 1000) {
 | 
					    } else if (responseTime <= 1000) {
 | 
				
			||||||
@@ -179,11 +180,38 @@ const ChannelsTable = () => {
 | 
				
			|||||||
    const res = await API.get(`/api/channel/test`);
 | 
					    const res = await API.get(`/api/channel/test`);
 | 
				
			||||||
    const { success, message } = res.data;
 | 
					    const { success, message } = res.data;
 | 
				
			||||||
    if (success) {
 | 
					    if (success) {
 | 
				
			||||||
      showInfo("已成功开始测试所有已启用通道,请刷新页面查看结果。");
 | 
					      showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      showError(message);
 | 
					      showError(message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateChannelBalance = async (id, name, idx) => {
 | 
				
			||||||
 | 
					    const res = await API.get(`/api/channel/update_balance/${id}/`);
 | 
				
			||||||
 | 
					    const { success, message, balance } = res.data;
 | 
				
			||||||
 | 
					    if (success) {
 | 
				
			||||||
 | 
					      let newChannels = [...channels];
 | 
				
			||||||
 | 
					      let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
 | 
				
			||||||
 | 
					      newChannels[realIdx].balance = balance;
 | 
				
			||||||
 | 
					      newChannels[realIdx].balance_updated_time = Date.now() / 1000;
 | 
				
			||||||
 | 
					      setChannels(newChannels);
 | 
				
			||||||
 | 
					      showInfo(`通道 ${name} 余额更新成功!`);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      showError(message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateAllChannelsBalance = async () => {
 | 
				
			||||||
 | 
					    setUpdatingBalance(true);
 | 
				
			||||||
 | 
					    const res = await API.get(`/api/channel/update_balance`);
 | 
				
			||||||
 | 
					    const { success, message } = res.data;
 | 
				
			||||||
 | 
					    if (success) {
 | 
				
			||||||
 | 
					      showInfo('已更新完毕所有已启用通道余额!');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      showError(message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setUpdatingBalance(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleKeywordChange = async (e, { value }) => {
 | 
					  const handleKeywordChange = async (e, { value }) => {
 | 
				
			||||||
    setSearchKeyword(value.trim());
 | 
					    setSearchKeyword(value.trim());
 | 
				
			||||||
@@ -263,10 +291,10 @@ const ChannelsTable = () => {
 | 
				
			|||||||
            <Table.HeaderCell
 | 
					            <Table.HeaderCell
 | 
				
			||||||
              style={{ cursor: 'pointer' }}
 | 
					              style={{ cursor: 'pointer' }}
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                sortChannel('test_time');
 | 
					                sortChannel('balance');
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              测试时间
 | 
					              余额
 | 
				
			||||||
            </Table.HeaderCell>
 | 
					            </Table.HeaderCell>
 | 
				
			||||||
            <Table.HeaderCell>操作</Table.HeaderCell>
 | 
					            <Table.HeaderCell>操作</Table.HeaderCell>
 | 
				
			||||||
          </Table.Row>
 | 
					          </Table.Row>
 | 
				
			||||||
@@ -286,8 +314,22 @@ const ChannelsTable = () => {
 | 
				
			|||||||
                  <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
 | 
					                  <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
 | 
				
			||||||
                  <Table.Cell>{renderType(channel.type)}</Table.Cell>
 | 
					                  <Table.Cell>{renderType(channel.type)}</Table.Cell>
 | 
				
			||||||
                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
 | 
					                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
 | 
				
			||||||
                  <Table.Cell>{renderResponseTime(channel.response_time)}</Table.Cell>
 | 
					                  <Table.Cell>
 | 
				
			||||||
                  <Table.Cell>{channel.test_time ? renderTimestamp(channel.test_time) : "未测试"}</Table.Cell>
 | 
					                    <Popup
 | 
				
			||||||
 | 
					                      content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
 | 
				
			||||||
 | 
					                      key={channel.id}
 | 
				
			||||||
 | 
					                      trigger={renderResponseTime(channel.response_time)}
 | 
				
			||||||
 | 
					                      basic
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </Table.Cell>
 | 
				
			||||||
 | 
					                  <Table.Cell>
 | 
				
			||||||
 | 
					                    <Popup
 | 
				
			||||||
 | 
					                      content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'}
 | 
				
			||||||
 | 
					                      key={channel.id}
 | 
				
			||||||
 | 
					                      trigger={<span>${channel.balance.toFixed(2)}</span>}
 | 
				
			||||||
 | 
					                      basic
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </Table.Cell>
 | 
				
			||||||
                  <Table.Cell>
 | 
					                  <Table.Cell>
 | 
				
			||||||
                    <div>
 | 
					                    <div>
 | 
				
			||||||
                      <Button
 | 
					                      <Button
 | 
				
			||||||
@@ -299,6 +341,16 @@ const ChannelsTable = () => {
 | 
				
			|||||||
                      >
 | 
					                      >
 | 
				
			||||||
                        测试
 | 
					                        测试
 | 
				
			||||||
                      </Button>
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                      <Button
 | 
				
			||||||
 | 
					                        size={'small'}
 | 
				
			||||||
 | 
					                        positive
 | 
				
			||||||
 | 
					                        loading={updatingBalance}
 | 
				
			||||||
 | 
					                        onClick={() => {
 | 
				
			||||||
 | 
					                          updateChannelBalance(channel.id, channel.name, idx);
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        更新余额
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
                      <Popup
 | 
					                      <Popup
 | 
				
			||||||
                        trigger={
 | 
					                        trigger={
 | 
				
			||||||
                          <Button size='small' negative>
 | 
					                          <Button size='small' negative>
 | 
				
			||||||
@@ -353,6 +405,7 @@ const ChannelsTable = () => {
 | 
				
			|||||||
              <Button size='small' loading={loading} onClick={testAllChannels}>
 | 
					              <Button size='small' loading={loading} onClick={testAllChannels}>
 | 
				
			||||||
                测试所有已启用通道
 | 
					                测试所有已启用通道
 | 
				
			||||||
              </Button>
 | 
					              </Button>
 | 
				
			||||||
 | 
					              <Button size='small' onClick={updateAllChannelsBalance} loading={updatingBalance}>更新所有已启用通道余额</Button>
 | 
				
			||||||
              <Pagination
 | 
					              <Pagination
 | 
				
			||||||
                floated='right'
 | 
					                floated='right'
 | 
				
			||||||
                activePage={activePage}
 | 
					                activePage={activePage}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,6 +112,8 @@ const PersonalSetting = () => {
 | 
				
			|||||||
      <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
 | 
					      <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
 | 
				
			||||||
      <Divider />
 | 
					      <Divider />
 | 
				
			||||||
      <Header as='h3'>账号绑定</Header>
 | 
					      <Header as='h3'>账号绑定</Header>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status.wechat_login && (
 | 
				
			||||||
          <Button
 | 
					          <Button
 | 
				
			||||||
            onClick={() => {
 | 
					            onClick={() => {
 | 
				
			||||||
              setShowWeChatBindModal(true);
 | 
					              setShowWeChatBindModal(true);
 | 
				
			||||||
@@ -119,6 +121,8 @@ const PersonalSetting = () => {
 | 
				
			|||||||
          >
 | 
					          >
 | 
				
			||||||
            绑定微信账号
 | 
					            绑定微信账号
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      <Modal
 | 
					      <Modal
 | 
				
			||||||
        onClose={() => setShowWeChatBindModal(false)}
 | 
					        onClose={() => setShowWeChatBindModal(false)}
 | 
				
			||||||
        onOpen={() => setShowWeChatBindModal(true)}
 | 
					        onOpen={() => setShowWeChatBindModal(true)}
 | 
				
			||||||
@@ -148,7 +152,11 @@ const PersonalSetting = () => {
 | 
				
			|||||||
          </Modal.Description>
 | 
					          </Modal.Description>
 | 
				
			||||||
        </Modal.Content>
 | 
					        </Modal.Content>
 | 
				
			||||||
      </Modal>
 | 
					      </Modal>
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status.github_oauth && (
 | 
				
			||||||
          <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
 | 
					          <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      <Button
 | 
					      <Button
 | 
				
			||||||
        onClick={() => {
 | 
					        onClick={() => {
 | 
				
			||||||
          setShowEmailBindModal(true);
 | 
					          setShowEmailBindModal(true);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import React, { useEffect, useState } from 'react';
 | 
					import React, { useEffect, useState } from 'react';
 | 
				
			||||||
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
 | 
					import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
 | 
				
			||||||
import { API, showError, showSuccess } from '../../helpers';
 | 
					import { API, showError, showInfo, showSuccess } from '../../helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TopUp = () => {
 | 
					const TopUp = () => {
 | 
				
			||||||
  const [redemptionCode, setRedemptionCode] = useState('');
 | 
					  const [redemptionCode, setRedemptionCode] = useState('');
 | 
				
			||||||
@@ -9,6 +9,7 @@ const TopUp = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const topUp = async () => {
 | 
					  const topUp = async () => {
 | 
				
			||||||
    if (redemptionCode === '') {
 | 
					    if (redemptionCode === '') {
 | 
				
			||||||
 | 
					      showInfo('请输入充值码!')
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const res = await API.post('/api/user/topup', {
 | 
					    const res = await API.post('/api/user/topup', {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user