mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			v0.6.2-alp
			...
			0.0.3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2c62053933 | ||
|  | 442673a4be | ||
|  | bb17e09818 | ||
|  | 80288492f9 | ||
|  | d89e13dac3 | ||
|  | 0c70181f81 | ||
|  | 3893d4bdf1 | ||
|  | 4aa9f97017 | ||
|  | ab041d94e2 | ||
|  | 18655fed08 | ||
|  | bba49c959e | ||
|  | 22f4419b85 | ||
|  | c7c3b9d326 | ||
|  | 0d6163a9fb | ||
|  | 7419ca511e | ||
|  | ccd7a99b68 | ||
|  | 759423d69e | ||
|  | 04110d4b01 | ||
|  | d6b2131720 | ||
|  | 438daea433 | ||
|  | c6c070b8bd | ||
|  | 13b3bfee2a | ||
|  | 2b42b4f364 | ||
|  | f37e41eb1d | ||
|  | c144c64fff | ||
|  | 8956e2fd60 | ||
|  | 30187cebe8 | ||
|  | 00d3a78bef | ||
|  | a588241515 | ||
|  | 546f9e1db5 | ||
|  | 4908a9eddc | ||
|  | 15cdaee762 | ||
|  | 395ee121ed | ||
|  | 4cea6279ab | ||
|  | f50683e75f | 
							
								
								
									
										2
									
								
								.github/workflows/docker-image-amd64-en.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-amd64-en.yml
									
									
									
									
										vendored
									
									
								
							| @@ -38,7 +38,7 @@ jobs: | |||||||
|         uses: docker/metadata-action@v4 |         uses: docker/metadata-action@v4 | ||||||
|         with: |         with: | ||||||
|           images: | |           images: | | ||||||
|             justsong/one-api-en |             ckt1031/one-api-en | ||||||
|  |  | ||||||
|       - name: Build and push Docker images |       - name: Build and push Docker images | ||||||
|         uses: docker/build-push-action@v3 |         uses: docker/build-push-action@v3 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							| @@ -42,7 +42,7 @@ jobs: | |||||||
|         uses: docker/metadata-action@v4 |         uses: docker/metadata-action@v4 | ||||||
|         with: |         with: | ||||||
|           images: | |           images: | | ||||||
|             justsong/one-api |             ckt1031/one-api | ||||||
|             ghcr.io/${{ github.repository }} |             ghcr.io/${{ github.repository }} | ||||||
|  |  | ||||||
|       - name: Build and push Docker images |       - name: Build and push Docker images | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/docker-image-arm64.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-arm64.yml
									
									
									
									
										vendored
									
									
								
							| @@ -49,7 +49,7 @@ jobs: | |||||||
|         uses: docker/metadata-action@v4 |         uses: docker/metadata-action@v4 | ||||||
|         with: |         with: | ||||||
|           images: | |           images: | | ||||||
|             justsong/one-api |             ckt1031/one-api | ||||||
|             ghcr.io/${{ github.repository }} |             ghcr.io/${{ github.repository }} | ||||||
|  |  | ||||||
|       - name: Build and push Docker images |       - name: Build and push Docker images | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								.github/workflows/linux-release-en.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								.github/workflows/linux-release-en.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | name: Linux Release (English) | ||||||
|  | permissions: | ||||||
|  |   contents: write | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     tags: | ||||||
|  |       - "*" | ||||||
|  |       - "!*-alpha*" | ||||||
|  | jobs: | ||||||
|  |   release: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 0 | ||||||
|  |  | ||||||
|  |       - name: Translate | ||||||
|  |         run: | | ||||||
|  |           python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: 16 | ||||||
|  |       - name: Build Frontend | ||||||
|  |         env: | ||||||
|  |           CI: "" | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm install | ||||||
|  |           REACT_APP_VERSION=$(git describe --tags) npm run build | ||||||
|  |           cd .. | ||||||
|  |       - name: Set up Go | ||||||
|  |         uses: actions/setup-go@v3 | ||||||
|  |         with: | ||||||
|  |           go-version: ">=1.18.0" | ||||||
|  |       - name: Build Backend (amd64) | ||||||
|  |         run: | | ||||||
|  |           go mod download | ||||||
|  |           go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-en | ||||||
|  |  | ||||||
|  |       - name: Build Backend (arm64) | ||||||
|  |         run: | | ||||||
|  |           sudo apt-get update | ||||||
|  |           sudo apt-get install gcc-aarch64-linux-gnu | ||||||
|  |           CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-arm64-en | ||||||
|  |  | ||||||
|  |       - name: Release | ||||||
|  |         uses: softprops/action-gh-release@v1 | ||||||
|  |         if: startsWith(github.ref, 'refs/tags/') | ||||||
|  |         with: | ||||||
|  |           files: | | ||||||
|  |             one-api-en | ||||||
|  |             one-api-arm64-en | ||||||
|  |           draft: true | ||||||
|  |           generate_release_notes: true | ||||||
|  |         env: | ||||||
|  |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
| @@ -22,6 +22,7 @@ var DisplayInCurrencyEnabled = true | |||||||
| var DisplayTokenStatEnabled = true | var DisplayTokenStatEnabled = true | ||||||
|  |  | ||||||
| var UsingSQLite = false | var UsingSQLite = false | ||||||
|  | var UsingPostgreSQL = false | ||||||
|  |  | ||||||
| // Any options with "Secret", "Token" in its key won't be return by GetOptions | // Any options with "Secret", "Token" in its key won't be return by GetOptions | ||||||
|  |  | ||||||
| @@ -38,7 +39,9 @@ var PasswordLoginEnabled = true | |||||||
| var PasswordRegisterEnabled = true | var PasswordRegisterEnabled = true | ||||||
| var EmailVerificationEnabled = false | var EmailVerificationEnabled = false | ||||||
| var GitHubOAuthEnabled = false | var GitHubOAuthEnabled = false | ||||||
|  | var DiscordOAuthEnabled = false | ||||||
| var WeChatAuthEnabled = false | var WeChatAuthEnabled = false | ||||||
|  | var GoogleOAuthEnabled = false | ||||||
| var TurnstileCheckEnabled = false | var TurnstileCheckEnabled = false | ||||||
| var RegisterEnabled = true | var RegisterEnabled = true | ||||||
|  |  | ||||||
| @@ -53,10 +56,16 @@ var SMTPToken = "" | |||||||
| var GitHubClientId = "" | var GitHubClientId = "" | ||||||
| var GitHubClientSecret = "" | var GitHubClientSecret = "" | ||||||
|  |  | ||||||
|  | var DiscordClientId = "" | ||||||
|  | var DiscordClientSecret = "" | ||||||
|  |  | ||||||
| var WeChatServerAddress = "" | var WeChatServerAddress = "" | ||||||
| var WeChatServerToken = "" | var WeChatServerToken = "" | ||||||
| var WeChatAccountQRCodeImageURL = "" | var WeChatAccountQRCodeImageURL = "" | ||||||
|  |  | ||||||
|  | var GoogleClientId = "" | ||||||
|  | var GoogleClientSecret = "" | ||||||
|  |  | ||||||
| var TurnstileSiteKey = "" | var TurnstileSiteKey = "" | ||||||
| var TurnstileSecretKey = "" | var TurnstileSecretKey = "" | ||||||
|  |  | ||||||
| @@ -138,6 +147,16 @@ const ( | |||||||
| 	ChannelStatusDisabled = 2 // also don't use 0 | 	ChannelStatusDisabled = 2 // also don't use 0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ChannelAllowNonStreamEnabled  = 1 | ||||||
|  | 	ChannelAllowNonStreamDisabled = 2 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ChannelAllowStreamEnabled  = 1 | ||||||
|  | 	ChannelAllowStreamDisabled = 2 | ||||||
|  | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	ChannelTypeUnknown   = 0 | 	ChannelTypeUnknown   = 0 | ||||||
| 	ChannelTypeOpenAI    = 1 | 	ChannelTypeOpenAI    = 1 | ||||||
|   | |||||||
| @@ -1,17 +1,20 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) { | func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) { | ||||||
| @@ -58,21 +61,86 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr | |||||||
| 		return err, nil | 		return err, nil | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 	var response TextResponse | 	if resp.StatusCode != http.StatusOK { | ||||||
| 	err = json.NewDecoder(resp.Body).Decode(&response) | 		return errors.New(fmt.Sprintf("status code %d", resp.StatusCode)), nil | ||||||
| 	if err != nil { |  | ||||||
| 		return err, nil |  | ||||||
| 	} | 	} | ||||||
| 	if response.Usage.CompletionTokens == 0 { |  | ||||||
| 		return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error | 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") | ||||||
|  |  | ||||||
|  | 	if channel.AllowStreaming == common.ChannelAllowStreamEnabled && isStream { | ||||||
|  | 		responseText := "" | ||||||
|  | 		scanner := bufio.NewScanner(resp.Body) | ||||||
|  | 		scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | ||||||
|  | 			if atEOF && len(data) == 0 { | ||||||
|  | 				return 0, nil, nil | ||||||
|  | 			} | ||||||
|  | 			if i := strings.Index(string(data), "\n"); i >= 0 { | ||||||
|  | 				return i + 1, data[0:i], nil | ||||||
|  | 			} | ||||||
|  | 			if atEOF { | ||||||
|  | 				return len(data), data, nil | ||||||
|  | 			} | ||||||
|  | 			return 0, nil, nil | ||||||
|  | 		}) | ||||||
|  | 		for scanner.Scan() { | ||||||
|  | 			data := scanner.Text() | ||||||
|  | 			if len(data) < 6 { // ignore blank line or wrong format | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// ChatGPT Next Web | ||||||
|  | 			if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") { | ||||||
|  | 				// Remove event: event in the front or back | ||||||
|  | 				data = strings.TrimPrefix(data, "event: event") | ||||||
|  | 				data = strings.TrimSuffix(data, "event: event") | ||||||
|  | 				// Remove everything, only keep `data: {...}` <--- this is the json | ||||||
|  | 				// Find the start and end indices of `data: {...}` substring | ||||||
|  | 				startIndex := strings.Index(data, "data:") | ||||||
|  | 				endIndex := strings.LastIndex(data, "}") | ||||||
|  |  | ||||||
|  | 				// If both indices are found and end index is greater than start index | ||||||
|  | 				if startIndex != -1 && endIndex != -1 && endIndex > startIndex { | ||||||
|  | 					// Extract the `data: {...}` substring | ||||||
|  | 					data = data[startIndex : endIndex+1] | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if !strings.HasPrefix(data, "data:") { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			data = data[6:] | ||||||
|  | 			if !strings.HasPrefix(data, "[DONE]") { | ||||||
|  | 				var streamResponse ChatCompletionsStreamResponse | ||||||
|  | 				err := json.Unmarshal([]byte(data), &streamResponse) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err, nil | ||||||
|  | 				} | ||||||
|  | 				for _, choice := range streamResponse.Choices { | ||||||
|  | 					responseText += choice.Delta.Content | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if responseText == "" { | ||||||
|  | 			return errors.New("Empty response"), nil | ||||||
|  | 		} | ||||||
|  | 	} else if channel.AllowNonStreaming == common.ChannelAllowNonStreamEnabled { | ||||||
|  | 		var response TextResponse | ||||||
|  | 		err = json.NewDecoder(resp.Body).Decode(&response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err, nil | ||||||
|  | 		} | ||||||
|  | 		if response.Usage.CompletionTokens == 0 { | ||||||
|  | 			return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func buildTestRequest() *ChatRequest { | func buildTestRequest(stream bool) *ChatRequest { | ||||||
| 	testRequest := &ChatRequest{ | 	testRequest := &ChatRequest{ | ||||||
| 		Model:     "", // this will be set later | 		Model:     "", // this will be set later | ||||||
| 		MaxTokens: 1, | 		MaxTokens: 1, | ||||||
|  | 		Stream:    stream, | ||||||
| 	} | 	} | ||||||
| 	testMessage := Message{ | 	testMessage := Message{ | ||||||
| 		Role:    "user", | 		Role:    "user", | ||||||
| @@ -99,7 +167,7 @@ func TestChannel(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	testRequest := buildTestRequest() | 	testRequest := buildTestRequest(channel.AllowStreaming == common.ChannelAllowStreamEnabled) | ||||||
| 	tik := time.Now() | 	tik := time.Now() | ||||||
| 	err, _ = testChannel(channel, *testRequest) | 	err, _ = testChannel(channel, *testRequest) | ||||||
| 	tok := time.Now() | 	tok := time.Now() | ||||||
| @@ -154,7 +222,6 @@ func testAllChannels(notify bool) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	testRequest := buildTestRequest() |  | ||||||
| 	var disableThreshold = int64(common.ChannelDisableThreshold * 1000) | 	var disableThreshold = int64(common.ChannelDisableThreshold * 1000) | ||||||
| 	if disableThreshold == 0 { | 	if disableThreshold == 0 { | ||||||
| 		disableThreshold = 10000000 // a impossible value | 		disableThreshold = 10000000 // a impossible value | ||||||
| @@ -165,6 +232,7 @@ func testAllChannels(notify bool) error { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			tik := time.Now() | 			tik := time.Now() | ||||||
|  | 			testRequest := buildTestRequest(channel.AllowStreaming == common.ChannelAllowStreamEnabled) | ||||||
| 			err, openaiErr := testChannel(channel, *testRequest) | 			err, openaiErr := testChannel(channel, *testRequest) | ||||||
| 			tok := time.Now() | 			tok := time.Now() | ||||||
| 			milliseconds := tok.Sub(tik).Milliseconds() | 			milliseconds := tok.Sub(tik).Milliseconds() | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetAllChannels(c *gin.Context) { | func GetAllChannels(c *gin.Context) { | ||||||
|   | |||||||
							
								
								
									
										223
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | package controller | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"one-api/common" | ||||||
|  | 	"one-api/model" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type DiscordOAuthResponse struct { | ||||||
|  | 	AccessToken string `json:"access_token"` | ||||||
|  | 	Scope       string `json:"scope"` | ||||||
|  | 	TokenType   string `json:"token_type"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DiscordUser struct { | ||||||
|  | 	Id       string `json:"id"` | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getDiscordUserInfoByCode(codeFromURLParamaters string, host string) (*DiscordUser, error) { | ||||||
|  | 	if codeFromURLParamaters == "" { | ||||||
|  | 		return nil, errors.New("无效参数") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	RequestClient := &http.Client{} | ||||||
|  |  | ||||||
|  | 	accessTokenBody := bytes.NewBuffer([]byte(fmt.Sprintf( | ||||||
|  | 		"client_id=%s&client_secret=%s&grant_type=authorization_code&redirect_uri=https://%s/oauth/discord&code=%s&scope=identify", | ||||||
|  | 		common.DiscordClientId, common.DiscordClientSecret, common.ServerAddress, codeFromURLParamaters, | ||||||
|  | 	))) | ||||||
|  |  | ||||||
|  | 	req, _ := http.NewRequest("POST", | ||||||
|  | 		"https://discordapp.com/api/oauth2/token", | ||||||
|  | 		accessTokenBody, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	req.Header = http.Header{ | ||||||
|  | 		"Content-Type": []string{"application/x-www-form-urlencoded"}, | ||||||
|  | 		"Accept":       []string{"application/json"}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, err := RequestClient.Do(req) | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != 200 || err != nil { | ||||||
|  | 		return nil, errors.New("访问令牌无效") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var discordOAuthResponse DiscordOAuthResponse | ||||||
|  |  | ||||||
|  | 	json.NewDecoder(resp.Body).Decode(&discordOAuthResponse) | ||||||
|  |  | ||||||
|  | 	accessToken := fmt.Sprintf("Bearer %s", discordOAuthResponse.AccessToken) | ||||||
|  |  | ||||||
|  | 	// Get User Info | ||||||
|  | 	req, _ = http.NewRequest("GET", "https://discord.com/api/users/@me", nil) | ||||||
|  |  | ||||||
|  | 	req.Header = http.Header{ | ||||||
|  | 		"Content-Type":  []string{"application/json"}, | ||||||
|  | 		"Authorization": []string{accessToken}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	resp, err = RequestClient.Do(req) | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != 200 || err != nil { | ||||||
|  | 		return nil, errors.New("Discord 用户信息无效") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var discordUser DiscordUser | ||||||
|  |  | ||||||
|  | 	json.NewDecoder(resp.Body).Decode(&discordUser) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if discordUser.Id == "" { | ||||||
|  | 		return nil, errors.New("返回值无效,用户字段为空,请稍后再试!") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	return &discordUser, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DiscordOAuth(c *gin.Context) { | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	username := session.Get("username") | ||||||
|  | 	if username != nil { | ||||||
|  | 		DiscordBind(c) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !common.DiscordOAuthEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "管理员未开启通过 Discord 登录以及注册", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	code := c.Query("code") | ||||||
|  |  | ||||||
|  | 	discordUser, err := getDiscordUserInfoByCode(code, c.Request.Host) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		DiscordId: discordUser.Id, | ||||||
|  | 	} | ||||||
|  | 	if model.IsDiscordIdAlreadyTaken(user.DiscordId) { | ||||||
|  | 		err := user.FillUserByDiscordId() | ||||||
|  | 		if err != nil { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": err.Error(), | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if common.RegisterEnabled { | ||||||
|  | 			user.Username = "discord_" + strconv.Itoa(model.GetMaxUserId()+1) | ||||||
|  | 			if discordUser.Username != "" { | ||||||
|  | 				user.DisplayName = discordUser.Username | ||||||
|  | 			} else { | ||||||
|  | 				user.DisplayName = "Discord User" | ||||||
|  | 			} | ||||||
|  | 			user.Role = common.RoleCommonUser | ||||||
|  | 			user.Status = common.UserStatusEnabled | ||||||
|  |  | ||||||
|  | 			if err := user.Insert(0); err != nil { | ||||||
|  | 				c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 					"success": false, | ||||||
|  | 					"message": err.Error(), | ||||||
|  | 				}) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "管理员关闭了新用户注册", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if user.Status != common.UserStatusEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"message": "用户已被封禁", | ||||||
|  | 			"success": false, | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	setupLogin(&user, c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DiscordBind(c *gin.Context) { | ||||||
|  | 	if !common.DiscordOAuthEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "管理员未开启通过 Discord 登录以及注册", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	code := c.Query("code") | ||||||
|  |  | ||||||
|  | 	discordUser, err := getDiscordUserInfoByCode(code, c.Request.Host) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		DiscordId: discordUser.Id, | ||||||
|  | 	} | ||||||
|  | 	if model.IsDiscordIdAlreadyTaken(user.DiscordId) { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "该 Discord 账户已被绑定", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	id := session.Get("id") | ||||||
|  | 	// id := c.GetInt("id")  // critical bug! | ||||||
|  | 	user.Id = id.(int) | ||||||
|  | 	err = user.FillUserById() | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user.DiscordId = discordUser.Id | ||||||
|  | 	err = user.Update(false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 		"success": true, | ||||||
|  | 		"message": "bind", | ||||||
|  | 	}) | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										226
									
								
								controller/google.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								controller/google.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | |||||||
|  | package controller | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"one-api/common" | ||||||
|  | 	"one-api/model" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type GoogleAccessTokenResponse struct { | ||||||
|  | 	AccessToken  string `json:"access_token"` | ||||||
|  | 	ExpiresIn    int    `json:"expires_in"` | ||||||
|  | 	TokenType    string `json:"token_type"` | ||||||
|  | 	Scope        string `json:"scope"` | ||||||
|  | 	RefreshToken string `json:"refresh_token"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GoogleUser struct { | ||||||
|  | 	Sub  string `json:"sub"` | ||||||
|  | 	Name string `json:"name"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getGoogleUserInfoByCode(codeFromURLParamaters string, host string) (*GoogleUser, error) { | ||||||
|  | 	if codeFromURLParamaters == "" { | ||||||
|  | 		return nil, errors.New("无效参数") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	RequestClient := &http.Client{} | ||||||
|  |  | ||||||
|  | 	accessTokenBody := bytes.NewBuffer([]byte(fmt.Sprintf( | ||||||
|  | 		"code=%s&client_id=%s&client_secret=%s&redirect_uri=%s/oauth/google&grant_type=authorization_code", | ||||||
|  | 		codeFromURLParamaters, common.GoogleClientId, common.GoogleClientSecret, common.ServerAddress, | ||||||
|  | 	))) | ||||||
|  |  | ||||||
|  | 	req, _ := http.NewRequest("POST", | ||||||
|  | 		"https://oauth2.googleapis.com/token", | ||||||
|  | 		accessTokenBody, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	req.Header = http.Header{ | ||||||
|  | 		"Content-Type": []string{"application/x-www-form-urlencoded"}, | ||||||
|  | 		"Accept":       []string{"application/json"}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, err := RequestClient.Do(req) | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != 200 || err != nil { | ||||||
|  | 		return nil, errors.New("访问令牌无效") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var googleTokenResponse GoogleAccessTokenResponse | ||||||
|  |  | ||||||
|  | 	json.NewDecoder(resp.Body).Decode(&googleTokenResponse) | ||||||
|  |  | ||||||
|  | 	accessToken := "Bearer " + googleTokenResponse.AccessToken | ||||||
|  |  | ||||||
|  | 	// Get User Info | ||||||
|  | 	req, _ = http.NewRequest("GET", "https://www.googleapis.com/oauth2/v3/userinfo", nil) | ||||||
|  |  | ||||||
|  | 	req.Header = http.Header{ | ||||||
|  | 		"Content-Type":  []string{"application/json"}, | ||||||
|  | 		"Authorization": []string{accessToken}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	resp, err = RequestClient.Do(req) | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != 200 || err != nil { | ||||||
|  | 		return nil, errors.New("Google 用户信息无效") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var googleUser GoogleUser | ||||||
|  |  | ||||||
|  | 	// Parse json to googleUser | ||||||
|  | 	err = json.NewDecoder(resp.Body).Decode(&googleUser) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if googleUser.Sub == "" { | ||||||
|  | 		return nil, errors.New("返回值无效,用户字段为空,请稍后再试!") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	return &googleUser, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GoogleOAuth(c *gin.Context) { | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	username := session.Get("username") | ||||||
|  | 	if username != nil { | ||||||
|  | 		GoogleBind(c) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !common.GoogleOAuthEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "管理员未开启通过 Google 登录以及注册", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	code := c.Query("code") | ||||||
|  |  | ||||||
|  | 	googleUser, err := getGoogleUserInfoByCode(code, c.Request.Host) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		GoogleId: googleUser.Sub, | ||||||
|  | 	} | ||||||
|  | 	if model.IsGoogleIdAlreadyTaken(user.GoogleId) { | ||||||
|  | 		err := user.FillUserByGoogleId() | ||||||
|  | 		if err != nil { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": err.Error(), | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if common.RegisterEnabled { | ||||||
|  | 			user.Username = "google_" + strconv.Itoa(model.GetMaxUserId()+1) | ||||||
|  | 			if googleUser.Name != "" { | ||||||
|  | 				user.DisplayName = googleUser.Name | ||||||
|  | 			} else { | ||||||
|  | 				user.DisplayName = "Google User" | ||||||
|  | 			} | ||||||
|  | 			user.Role = common.RoleCommonUser | ||||||
|  | 			user.Status = common.UserStatusEnabled | ||||||
|  |  | ||||||
|  | 			if err := user.Insert(0); err != nil { | ||||||
|  | 				c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 					"success": false, | ||||||
|  | 					"message": err.Error(), | ||||||
|  | 				}) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "管理员关闭了新用户注册", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if user.Status != common.UserStatusEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"message": "用户已被封禁", | ||||||
|  | 			"success": false, | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	setupLogin(&user, c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GoogleBind(c *gin.Context) { | ||||||
|  | 	if !common.GoogleOAuthEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "管理员未开启通过 Google 登录以及注册", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	code := c.Query("code") | ||||||
|  |  | ||||||
|  | 	googleUser, err := getGoogleUserInfoByCode(code, c.Request.Host) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		GoogleId: googleUser.Sub, | ||||||
|  | 	} | ||||||
|  | 	if model.IsGoogleIdAlreadyTaken(user.GoogleId) { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "该 Google 账户已被绑定", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	id := session.Get("id") | ||||||
|  | 	// id := c.GetInt("id")  // critical bug! | ||||||
|  | 	user.Id = id.(int) | ||||||
|  | 	err = user.FillUserById() | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user.GoogleId = googleUser.Sub | ||||||
|  | 	err = user.Update(false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 		"success": true, | ||||||
|  | 		"message": "bind", | ||||||
|  | 	}) | ||||||
|  | 	return | ||||||
|  | } | ||||||
| @@ -3,10 +3,11 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetStatus(c *gin.Context) { | func GetStatus(c *gin.Context) { | ||||||
| @@ -19,6 +20,10 @@ func GetStatus(c *gin.Context) { | |||||||
| 			"email_verification":  common.EmailVerificationEnabled, | 			"email_verification":  common.EmailVerificationEnabled, | ||||||
| 			"github_oauth":        common.GitHubOAuthEnabled, | 			"github_oauth":        common.GitHubOAuthEnabled, | ||||||
| 			"github_client_id":    common.GitHubClientId, | 			"github_client_id":    common.GitHubClientId, | ||||||
|  | 			"discord_oauth":       common.DiscordOAuthEnabled, | ||||||
|  | 			"discord_client_id":   common.DiscordClientId, | ||||||
|  | 			"google_oauth":        common.GoogleOAuthEnabled, | ||||||
|  | 			"google_client_id":    common.GoogleClientId, | ||||||
| 			"system_name":         common.SystemName, | 			"system_name":         common.SystemName, | ||||||
| 			"logo":                common.Logo, | 			"logo":                common.Logo, | ||||||
| 			"footer_html":         common.Footer, | 			"footer_html":         common.Footer, | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ package controller | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetOptions(c *gin.Context) { | func GetOptions(c *gin.Context) { | ||||||
| @@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) { | |||||||
| 			}) | 			}) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	case "DiscordOAuthEnabled": | ||||||
|  | 		if option.Value == "true" && common.DiscordClientId == "" { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	case "WeChatAuthEnabled": | 	case "WeChatAuthEnabled": | ||||||
| 		if option.Value == "true" && common.WeChatServerAddress == "" { | 		if option.Value == "true" && common.WeChatServerAddress == "" { | ||||||
| 			c.JSON(http.StatusOK, gin.H{ | 			c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -57,6 +66,14 @@ func UpdateOption(c *gin.Context) { | |||||||
| 			}) | 			}) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	case "GoogleOAuthEnabled": | ||||||
|  | 		if option.Value == "true" && common.GoogleClientId == "" { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "无法启用 Google OAuth,请先填入 Google Client ID 以及 Google Client Secret!", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	case "TurnstileCheckEnabled": | 	case "TurnstileCheckEnabled": | ||||||
| 		if option.Value == "true" && common.TurnstileSiteKey == "" { | 		if option.Value == "true" && common.TurnstileSiteKey == "" { | ||||||
| 			c.JSON(http.StatusOK, gin.H{ | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -4,11 +4,12 @@ import ( | |||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) { | func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) { | ||||||
| @@ -34,6 +35,25 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O | |||||||
| 			if len(data) < 6 { // ignore blank line or wrong format | 			if len(data) < 6 { // ignore blank line or wrong format | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  | 			// ChatGPT Next Web | ||||||
|  | 			if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") { | ||||||
|  | 				// Remove event: event in the front or back | ||||||
|  | 				data = strings.TrimPrefix(data, "event: event") | ||||||
|  | 				data = strings.TrimSuffix(data, "event: event") | ||||||
|  | 				// Remove everything, only keep `data: {...}` <--- this is the json | ||||||
|  | 				// Find the start and end indices of `data: {...}` substring | ||||||
|  | 				startIndex := strings.Index(data, "data:") | ||||||
|  | 				endIndex := strings.LastIndex(data, "}") | ||||||
|  |  | ||||||
|  | 				// If both indices are found and end index is greater than start index | ||||||
|  | 				if startIndex != -1 && endIndex != -1 && endIndex > startIndex { | ||||||
|  | 					// Extract the `data: {...}` substring | ||||||
|  | 					data = data[startIndex : endIndex+1] | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if !strings.HasPrefix(data, "data:") { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			dataChan <- data | 			dataChan <- data | ||||||
| 			data = data[6:] | 			data = data[6:] | ||||||
| 			if !strings.HasPrefix(data, "[DONE]") { | 			if !strings.HasPrefix(data, "[DONE]") { | ||||||
|   | |||||||
| @@ -266,6 +266,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		return errorWrapper(nil, "bad_status_code", resp.StatusCode) | ||||||
|  | 	} | ||||||
| 	var textResponse TextResponse | 	var textResponse TextResponse | ||||||
| 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") | 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") | ||||||
| 	var streamResponseText string | 	var streamResponseText string | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ type ChatRequest struct { | |||||||
| 	Model     string    `json:"model"` | 	Model     string    `json:"model"` | ||||||
| 	Messages  []Message `json:"messages"` | 	Messages  []Message `json:"messages"` | ||||||
| 	MaxTokens int       `json:"max_tokens"` | 	MaxTokens int       `json:"max_tokens"` | ||||||
|  | 	Stream    bool      `json:"stream"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type TextRequest struct { | type TextRequest struct { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ version: '3.4' | |||||||
|  |  | ||||||
| services: | services: | ||||||
|   one-api: |   one-api: | ||||||
|     image: justsong/one-api:latest |     image: ckt1031/one-api:latest | ||||||
|     container_name: one-api |     container_name: one-api | ||||||
|     restart: always |     restart: always | ||||||
|     command: --log-dir /app/logs |     command: --log-dir /app/logs | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								go.mod
									
									
									
									
									
								
							| @@ -9,28 +9,34 @@ require ( | |||||||
| 	github.com/gin-contrib/sessions v0.0.5 | 	github.com/gin-contrib/sessions v0.0.5 | ||||||
| 	github.com/gin-contrib/static v0.0.1 | 	github.com/gin-contrib/static v0.0.1 | ||||||
| 	github.com/gin-gonic/gin v1.9.1 | 	github.com/gin-gonic/gin v1.9.1 | ||||||
| 	github.com/go-playground/validator/v10 v10.14.0 | 	github.com/go-playground/validator/v10 v10.14.1 | ||||||
| 	github.com/go-redis/redis/v8 v8.11.5 | 	github.com/go-redis/redis/v8 v8.11.5 | ||||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible | 	github.com/golang-jwt/jwt v3.2.2+incompatible | ||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.3.0 | ||||||
| 	github.com/pkoukk/tiktoken-go v0.1.1 | 	github.com/pkoukk/tiktoken-go v0.1.5 | ||||||
| 	golang.org/x/crypto v0.9.0 | 	golang.org/x/crypto v0.11.0 | ||||||
| 	gorm.io/driver/mysql v1.4.3 | 	gorm.io/driver/mysql v1.5.1 | ||||||
| 	gorm.io/driver/sqlite v1.4.3 | 	gorm.io/driver/sqlite v1.5.2 | ||||||
| 	gorm.io/gorm v1.24.0 | 	gorm.io/gorm v1.25.2 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/bytedance/sonic v1.9.1 // indirect | 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | ||||||
|  | 	github.com/jackc/pgx/v5 v5.4.2 // indirect | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | require ( | ||||||
|  | 	github.com/bytedance/sonic v1.9.2 // indirect | ||||||
|  | 	github.com/cespare/xxhash/v2 v2.2.0 // indirect | ||||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | ||||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||||
| 	github.com/dlclark/regexp2 v1.8.1 // indirect | 	github.com/dlclark/regexp2 v1.10.0 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect | 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect | ||||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect | 	github.com/go-playground/locales v0.14.1 // indirect | ||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/go-sql-driver/mysql v1.6.0 // indirect | 	github.com/go-sql-driver/mysql v1.7.1 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.2 // indirect | 	github.com/goccy/go-json v0.10.2 // indirect | ||||||
| 	github.com/gorilla/context v1.1.1 // indirect | 	github.com/gorilla/context v1.1.1 // indirect | ||||||
| 	github.com/gorilla/securecookie v1.1.1 // indirect | 	github.com/gorilla/securecookie v1.1.1 // indirect | ||||||
| @@ -38,19 +44,20 @@ require ( | |||||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
| 	github.com/jinzhu/now v1.1.5 // indirect | 	github.com/jinzhu/now v1.1.5 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect | ||||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | 	github.com/leodido/go-urn v1.2.4 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | 	github.com/mattn/go-isatty v0.0.19 // indirect | ||||||
| 	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect | 	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect | ||||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
| 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | 	github.com/pelletier/go-toml/v2 v2.0.9 // indirect | ||||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | 	github.com/ugorji/go/codec v1.2.11 // indirect | ||||||
| 	golang.org/x/arch v0.3.0 // indirect | 	golang.org/x/arch v0.4.0 // indirect | ||||||
| 	golang.org/x/net v0.10.0 // indirect | 	golang.org/x/net v0.12.0 // indirect | ||||||
| 	golang.org/x/sys v0.8.0 // indirect | 	golang.org/x/sys v0.10.0 // indirect | ||||||
| 	golang.org/x/text v0.9.0 // indirect | 	golang.org/x/text v0.11.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.30.0 // indirect | 	google.golang.org/protobuf v1.31.0 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
|  | 	gorm.io/driver/postgres v1.5.2 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||||
| github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM= | ||||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||||
| github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= | ||||||
| github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||||
| @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= | ||||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||||
| github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | ||||||
| @@ -43,12 +43,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn | |||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= | ||||||
| github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= | ||||||
| github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= | github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= | ||||||
| github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
| github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | ||||||
| github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | ||||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= | ||||||
|  | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||||
| github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||||||
| github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||||
| @@ -67,17 +68,24 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC | |||||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
|  | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | ||||||
|  | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | ||||||
|  | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= | ||||||
|  | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | ||||||
|  | github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= | ||||||
|  | github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= | ||||||
|  | github.com/jackc/pgx/v5 v5.4.2 h1:u1gmGDwbdRUZiwisBm/Ky2M14uQyUP65bG8+20nnyrg= | ||||||
|  | github.com/jackc/pgx/v5 v5.4.2/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= | ||||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |  | ||||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||||
| @@ -94,7 +102,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky | |||||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||||
| github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | ||||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||||
| github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= |  | ||||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= | ||||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| @@ -107,11 +114,11 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | |||||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||||
| github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= | github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= | github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||||
| @@ -128,8 +135,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ | |||||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||||||
| github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | ||||||
| @@ -139,36 +146,36 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 | |||||||
| github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | ||||||
| github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
| golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= | ||||||
| golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
| golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
| golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= | ||||||
| golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= | ||||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
| golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= | ||||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= | ||||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= | ||||||
| golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= | ||||||
| golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||||
| google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
| google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= | ||||||
| google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
| @@ -183,11 +190,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C | |||||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= | gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= | ||||||
| gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= | gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= | ||||||
| gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= | gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= | ||||||
| gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= | gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= | ||||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= | ||||||
| gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= | gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= | ||||||
| gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= | gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||||
|  | gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= | ||||||
|  | gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								i18n/en.json
									
									
									
									
									
								
							| @@ -260,7 +260,7 @@ | |||||||
|   "注意": "Note", |   "注意": "Note", | ||||||
|   "此处生成的令牌用于系统管理": "The token generated here is used for system management", |   "此处生成的令牌用于系统管理": "The token generated here is used for system management", | ||||||
|   "而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services", |   "而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services", | ||||||
|   "请知悉": "Please be aware", |   "请知悉。": "Please be aware.", | ||||||
|   "更新个人信息": "Update Personal Information", |   "更新个人信息": "Update Personal Information", | ||||||
|   "生成系统访问令牌": "Generate System Access Token", |   "生成系统访问令牌": "Generate System Access Token", | ||||||
|   "复制邀请链接": "Copy Invitation Link", |   "复制邀请链接": "Copy Invitation Link", | ||||||
| @@ -284,7 +284,7 @@ | |||||||
|   "兑换时间": "Redemption Time", |   "兑换时间": "Redemption Time", | ||||||
|   "尚未兑换": "Not yet redeemed", |   "尚未兑换": "Not yet redeemed", | ||||||
|   "已复制到剪贴板": "Copied to clipboard", |   "已复制到剪贴板": "Copied to clipboard", | ||||||
|   "无法复制到剪贴板": "Unable to copy to clipboard", |   "无法复制到剪贴板。": "Unable to copy to clipboard.", | ||||||
|   "请手动复制": "Please copy manually", |   "请手动复制": "Please copy manually", | ||||||
|   "已将兑换码填入搜索框": "The voucher code has been filled into the search box", |   "已将兑换码填入搜索框": "The voucher code has been filled into the search box", | ||||||
|   "复制": "Copy", |   "复制": "Copy", | ||||||
| @@ -503,5 +503,73 @@ | |||||||
|   "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", |   "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", | ||||||
|   "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", |   "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", | ||||||
|   "Homepage URL 填": "Fill in the Homepage URL", |   "Homepage URL 填": "Fill in the Homepage URL", | ||||||
|   "Authorization callback URL 填": "Fill in the Authorization callback URL" |   "Authorization callback URL 填": "Fill in the Authorization callback URL", | ||||||
|  |  | ||||||
|  |   "兑换中": "Redeeming", | ||||||
|  |   "请求失败": "Request failed", | ||||||
|  |   "百度文心千帆": "Baidu Wenxin Qianfan", | ||||||
|  |   "智谱": "Zhipuai", | ||||||
|  |   "代理:": "Agent: ", | ||||||
|  |   "请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!", | ||||||
|  |   "账户已删除!": "Account deleted!", | ||||||
|  |   "删除个人账户": "Delete personal account", | ||||||
|  |   "重新发送": "Resend ", | ||||||
|  |   "确认删除自己的帐户": "Confirm deletion of your own account", | ||||||
|  |   "输入你的账户名": "Enter your account name", | ||||||
|  |   "以确认删除": "to confirm deletion", | ||||||
|  |   "重试": "Retry", | ||||||
|  |   "无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。": "Unable to copy to clipboard, please copy manually, the redemption code has been filled in the search box.", | ||||||
|  |   "密码重置完成": "Password reset completed", | ||||||
|  |  | ||||||
|  |   "流式请求和非流式请求不能同时禁用!": "Streaming requests and non-streaming requests cannot be disabled at the same time!", | ||||||
|  |  | ||||||
|  |   "请为渠道命名": "Please name the channel", | ||||||
|  |   "请选择可以使用该渠道的分组": "Please select the group that can use this channel", | ||||||
|  |   "请选择该渠道所支持的模型": "Please select the models supported by this channel", | ||||||
|  |   "填入": "Fill in", | ||||||
|  |   "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:", | ||||||
|  |   "允许流式请求": "Allow streaming requests", | ||||||
|  |   "允许非流式请求": "Allow non-streaming requests", | ||||||
|  |   "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次": "Please enter the access token, the current version does not support automatic refresh, please update it every 30 days", | ||||||
|  |   "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel", | ||||||
|  |   "此项可选,用于通过Mirror站来进行 API 调用,请EnterMirror站地址,格式为:https://domain.com": "This is optional, used to make API calls through the Mirror site, please enter the Mirror site address, the format is: https://domain.com", | ||||||
|  |  | ||||||
|  |   "新密码": "New password", | ||||||
|  |   "新密码已复制到剪贴板:": "New password copied to clipboard: ", | ||||||
|  |   "兑换失败,": "Redemption failed, ", | ||||||
|  |   "当前分组 %s 下对于模型 %s 无可用渠道": "There are no available channels for model %s under the current group %s", | ||||||
|  |   "无权将其他用户权限等级提升到大于等于自己的权限等级": "You are not allowed to raise the permission level of other users to greater than or equal to your own permission level", | ||||||
|  |   "不能删除超级管理员账户": "Cannot delete super administrator account", | ||||||
|  |   "无效参数": "Invalid parameter", | ||||||
|  |   "访问令牌无效": "Access token invalid", | ||||||
|  |   "Google 用户信息无效": "Google user information invalid", | ||||||
|  |   "返回值无效,用户字段为空,请稍后再试!": "The return value is invalid, the user field is empty, please try again later!", | ||||||
|  |   "管理员未开启通过 Google 登录以及注册": "The administrator has not enabled login and registration via Google", | ||||||
|  |   "该 Google 账户已被绑定": "The Google account has been bound", | ||||||
|  |   "点击 <a href='%s'>此处</a> 进行密码重置。": "Click <a href='%s'>here</a> to reset your password.", | ||||||
|  |   "如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s ": "If the link cannot be clicked, please try to click the link below or copy it to the browser to open: <br> %s ", | ||||||
|  |   "该渠道类型当前版本不支持测试,请手动测试": "The current version of the channel type does not support testing, please test manually", | ||||||
|  |   "无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!": "Unable to enable Discord OAuth, please fill in the Discord Client ID and Discord Client Secret first!", | ||||||
|  |   "无法启用 Google OAuth,请先填入 Google Client ID 以及 Google Client Secret!": "Unable to enable Google OAuth, please fill in the Google Client ID and Google Client Secret first!", | ||||||
|  |   "管理员未开启通过 Discord 登录以及注册": "The administrator has not enabled login and registration via Discord", | ||||||
|  |   "该 Discord 账户已被绑定": "The Discord account has been bound", | ||||||
|  |   "Discord 用户信息无效": "Discord user information invalid", | ||||||
|  |   "绑定 Discord 账号": "Bind Discord account", | ||||||
|  |   "绑定 Google 账号": "Bind Google account", | ||||||
|  |   "已绑定的 Discord 账户": "Bound Discord account", | ||||||
|  |   "已绑定的 Google 账户": "Bound Google account", | ||||||
|  |   "身份验证": "Authentication", | ||||||
|  |   "允许通过 Discord 账户登录和注册": "Allow login and registration via Discord account", | ||||||
|  |   "允许通过 Google 账户登录和注册": "Allow login and registration via Google account", | ||||||
|  |   "配置 Discord OAuth 应用程序": "Configure Discord OAuth application", | ||||||
|  |   "配置 Google OAuth 应用程序": "Configure Google OAuth application", | ||||||
|  |   "用以支持通过 Discord 进行登录注册,": "Used to support login and registration via Discord, ", | ||||||
|  |   "用以支持通过 Google 进行登录注册,": "Used to support login and registration via Google, ", | ||||||
|  |   "管理你的": "Manage your ", | ||||||
|  |   "客户 ID": "Client ID", | ||||||
|  |   "客户秘密": "Client Secret", | ||||||
|  |   "失败重试次数": "Retry times", | ||||||
|  |   "保存": "Save", | ||||||
|  |   "输入您注册的 Discord OAuth APP 的 ID": "Enter the ID of your registered Discord OAuth APP", | ||||||
|  |   "输入您注册的 Google OAuth APP 的 ID": "Enter the ID of your registered Google OAuth APP" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package middleware | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| @@ -12,7 +13,8 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type ModelRequest struct { | type ModelRequest struct { | ||||||
| 	Model string `json:"model"` | 	Model  string `json:"model"` | ||||||
|  | 	Stream bool   `json:"stream" default:"true"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func Distribute() func(c *gin.Context) { | func Distribute() func(c *gin.Context) { | ||||||
| @@ -84,7 +86,8 @@ func Distribute() func(c *gin.Context) { | |||||||
| 					modelRequest.Model = "dall-e" | 					modelRequest.Model = "dall-e" | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) | 			log.Print(modelRequest.Stream) | ||||||
|  | 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, modelRequest.Stream) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model) | 				message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model) | ||||||
| 				if channel != nil { | 				if channel != nil { | ||||||
|   | |||||||
| @@ -1,24 +1,47 @@ | |||||||
| package model | package model | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Ability struct { | type Ability struct { | ||||||
| 	Group     string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"` | 	Group             string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"` | ||||||
| 	Model     string `json:"model" gorm:"primaryKey;autoIncrement:false"` | 	Model             string `json:"model" gorm:"primaryKey;autoIncrement:false"` | ||||||
| 	ChannelId int    `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"` | 	ChannelId         int    `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"` | ||||||
| 	Enabled   bool   `json:"enabled"` | 	Enabled           bool   `json:"enabled"` | ||||||
|  | 	AllowStreaming    int    `json:"allow_streaming" gorm:"default:1"` | ||||||
|  | 	AllowNonStreaming int    `json:"allow_non_streaming" gorm:"default:1"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { | func GetRandomSatisfiedChannel(group string, model string, stream bool) (*Channel, error) { | ||||||
| 	ability := Ability{} | 	ability := Ability{} | ||||||
| 	var err error = nil | 	var err error = nil | ||||||
| 	if common.UsingSQLite { |  | ||||||
| 		err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RANDOM()").Limit(1).First(&ability).Error | 	cmd := "`group` = ? and model = ? and enabled = 1" | ||||||
|  |  | ||||||
|  | 	if common.UsingPostgreSQL { | ||||||
|  | 		// Make cmd compatible with PostgreSQL | ||||||
|  | 		cmd = "\"group\" = ? and model = ? and enabled = true" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if stream { | ||||||
|  | 		cmd += fmt.Sprintf(" and allow_streaming = %d", common.ChannelAllowStreamEnabled) | ||||||
| 	} else { | 	} else { | ||||||
| 		err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RAND()").Limit(1).First(&ability).Error | 		cmd += fmt.Sprintf(" and allow_non_streaming = %d", common.ChannelAllowNonStreamEnabled) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if common.UsingSQLite || common.UsingPostgreSQL { | ||||||
|  | 		err = DB.Where(cmd, group, model).Order("RANDOM()").Limit(1).First(&ability).Error | ||||||
|  | 	} else { | ||||||
|  | 		cmd += fmt.Sprintf(" and allow_non_streaming = %d", common.ChannelAllowNonStreamEnabled) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if common.UsingSQLite { | ||||||
|  | 		err = DB.Where(cmd, group, model).Order("RANDOM()").Limit(1).First(&ability).Error | ||||||
|  | 	} else { | ||||||
|  | 		err = DB.Where(cmd, group, model).Order("RAND()").Limit(1).First(&ability).Error | ||||||
| 	} | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -36,10 +59,12 @@ func (channel *Channel) AddAbilities() error { | |||||||
| 	for _, model := range models_ { | 	for _, model := range models_ { | ||||||
| 		for _, group := range groups_ { | 		for _, group := range groups_ { | ||||||
| 			ability := Ability{ | 			ability := Ability{ | ||||||
| 				Group:     group, | 				Group:             group, | ||||||
| 				Model:     model, | 				Model:             model, | ||||||
| 				ChannelId: channel.Id, | 				ChannelId:         channel.Id, | ||||||
| 				Enabled:   channel.Status == common.ChannelStatusEnabled, | 				Enabled:           channel.Status == common.ChannelStatusEnabled, | ||||||
|  | 				AllowStreaming:    channel.AllowStreaming, | ||||||
|  | 				AllowNonStreaming: channel.AllowNonStreaming, | ||||||
| 			} | 			} | ||||||
| 			abilities = append(abilities, ability) | 			abilities = append(abilities, ability) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -21,13 +21,19 @@ var ( | |||||||
|  |  | ||||||
| func CacheGetTokenByKey(key string) (*Token, error) { | func CacheGetTokenByKey(key string) (*Token, error) { | ||||||
| 	var token Token | 	var token Token | ||||||
|  | 	whereItem := "`key` = ?" | ||||||
|  | 	if common.UsingPostgreSQL { | ||||||
|  | 		// Make cmd compatible with PostgreSQL | ||||||
|  | 		whereItem = "\"key\" = ?" | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if !common.RedisEnabled { | 	if !common.RedisEnabled { | ||||||
| 		err := DB.Where("`key` = ?", key).First(&token).Error | 		err := DB.Where(whereItem, key).First(&token).Error | ||||||
| 		return &token, err | 		return &token, err | ||||||
| 	} | 	} | ||||||
| 	tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key)) | 	tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err := DB.Where("`key` = ?", key).First(&token).Error | 		err := DB.Where(whereItem, key).First(&token).Error | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| @@ -160,9 +166,9 @@ func SyncChannelCache(frequency int) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error) { | func CacheGetRandomSatisfiedChannel(group string, model string, stream bool) (*Channel, error) { | ||||||
| 	if !common.RedisEnabled { | 	if !common.RedisEnabled { | ||||||
| 		return GetRandomSatisfiedChannel(group, model) | 		return GetRandomSatisfiedChannel(group, model, stream) | ||||||
| 	} | 	} | ||||||
| 	channelSyncLock.RLock() | 	channelSyncLock.RLock() | ||||||
| 	defer channelSyncLock.RUnlock() | 	defer channelSyncLock.RUnlock() | ||||||
| @@ -170,6 +176,14 @@ func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error | |||||||
| 	if len(channels) == 0 { | 	if len(channels) == 0 { | ||||||
| 		return nil, errors.New("channel not found") | 		return nil, errors.New("channel not found") | ||||||
| 	} | 	} | ||||||
| 	idx := rand.Intn(len(channels)) |  | ||||||
| 	return channels[idx], nil | 	var filteredChannels []*Channel | ||||||
|  | 	for _, channel := range channels { | ||||||
|  | 		if (stream && channel.AllowStreaming == common.ChannelAllowStreamEnabled) || (!stream && channel.AllowNonStreaming == common.ChannelAllowNonStreamEnabled) { | ||||||
|  | 			filteredChannels = append(filteredChannels, channel) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	idx := rand.Intn(len(filteredChannels)) | ||||||
|  | 	return filteredChannels[idx], nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| package model | package model | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Channel struct { | type Channel struct { | ||||||
| @@ -23,6 +24,8 @@ type Channel struct { | |||||||
| 	Group              string  `json:"group" gorm:"type:varchar(32);default:'default'"` | 	Group              string  `json:"group" gorm:"type:varchar(32);default:'default'"` | ||||||
| 	UsedQuota          int64   `json:"used_quota" gorm:"bigint;default:0"` | 	UsedQuota          int64   `json:"used_quota" gorm:"bigint;default:0"` | ||||||
| 	ModelMapping       string  `json:"model_mapping" gorm:"type:varchar(1024);default:''"` | 	ModelMapping       string  `json:"model_mapping" gorm:"type:varchar(1024);default:''"` | ||||||
|  | 	AllowStreaming     int     `json:"allow_streaming" gorm:"default:1"` | ||||||
|  | 	AllowNonStreaming  int     `json:"allow_non_streaming" gorm:"default:1"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | ||||||
| @@ -37,7 +40,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func SearchChannels(keyword string) (channels []*Channel, err error) { | func SearchChannels(keyword string) (channels []*Channel, err error) { | ||||||
| 	err = DB.Omit("key").Where("id = ? or name LIKE ? or `key` = ?", keyword, keyword+"%", keyword).Find(&channels).Error | 	whereItem := "id = ? or name LIKE ? or `key` = ?" | ||||||
|  |  | ||||||
|  | 	if common.UsingPostgreSQL { | ||||||
|  | 		whereItem = "id = ? or name LIKE ? or \"key\" = ?" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = DB.Omit("key").Where(whereItem, keyword, keyword+"%", keyword).Find(&channels).Error | ||||||
| 	return channels, err | 	return channels, err | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -55,7 +64,9 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) { | |||||||
| func GetRandomChannel() (*Channel, error) { | func GetRandomChannel() (*Channel, error) { | ||||||
| 	channel := Channel{} | 	channel := Channel{} | ||||||
| 	var err error = nil | 	var err error = nil | ||||||
| 	if common.UsingSQLite { | 	if common.UsingPostgreSQL { | ||||||
|  | 		err = DB.Where("status = ? and \"group\" = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error | ||||||
|  | 	} else if common.UsingSQLite { | ||||||
| 		err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error | 		err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error | ||||||
| 	} else { | 	} else { | ||||||
| 		err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RAND()").Limit(1).First(&channel).Error | 		err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RAND()").Limit(1).First(&channel).Error | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| package model | package model | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"gorm.io/driver/mysql" |  | ||||||
| 	"gorm.io/driver/sqlite" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
|  | 	"gorm.io/driver/mysql" | ||||||
|  | 	"gorm.io/driver/postgres" | ||||||
|  | 	"gorm.io/driver/sqlite" | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var DB *gorm.DB | var DB *gorm.DB | ||||||
| @@ -40,7 +42,14 @@ func CountTable(tableName string) (num int64) { | |||||||
|  |  | ||||||
| func InitDB() (err error) { | func InitDB() (err error) { | ||||||
| 	var db *gorm.DB | 	var db *gorm.DB | ||||||
| 	if os.Getenv("SQL_DSN") != "" { | 	if os.Getenv("POSTGRES_DSN") != "" { | ||||||
|  | 		// Use PostgreSQL | ||||||
|  | 		common.SysLog("using PostgreSQL as database") | ||||||
|  | 		common.UsingPostgreSQL = true | ||||||
|  | 		db, err = gorm.Open(postgres.Open(os.Getenv("POSTGRES_DSN")), &gorm.Config{ | ||||||
|  | 			PrepareStmt: true, // precompile SQL | ||||||
|  | 		}) | ||||||
|  | 	} else if os.Getenv("SQL_DSN") != "" { | ||||||
| 		// Use MySQL | 		// Use MySQL | ||||||
| 		common.SysLog("using MySQL as database") | 		common.SysLog("using MySQL as database") | ||||||
| 		db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{ | 		db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{ | ||||||
|   | |||||||
| @@ -30,7 +30,9 @@ func InitOptionMap() { | |||||||
| 	common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled) | 	common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled) | ||||||
| 	common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) | 	common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) | ||||||
| 	common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) | 	common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) | ||||||
|  | 	common.OptionMap["DiscordOAuthEnabled"] = strconv.FormatBool(common.DiscordOAuthEnabled) | ||||||
| 	common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) | 	common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) | ||||||
|  | 	common.OptionMap["GoogleOAuthEnabled"] = strconv.FormatBool(common.GoogleOAuthEnabled) | ||||||
| 	common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) | 	common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) | ||||||
| 	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) | 	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) | ||||||
| 	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled) | 	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled) | ||||||
| @@ -53,9 +55,13 @@ func InitOptionMap() { | |||||||
| 	common.OptionMap["ServerAddress"] = "" | 	common.OptionMap["ServerAddress"] = "" | ||||||
| 	common.OptionMap["GitHubClientId"] = "" | 	common.OptionMap["GitHubClientId"] = "" | ||||||
| 	common.OptionMap["GitHubClientSecret"] = "" | 	common.OptionMap["GitHubClientSecret"] = "" | ||||||
|  | 	common.OptionMap["DiscordClientId"] = "" | ||||||
|  | 	common.OptionMap["DiscordClientSecret"] = "" | ||||||
| 	common.OptionMap["WeChatServerAddress"] = "" | 	common.OptionMap["WeChatServerAddress"] = "" | ||||||
| 	common.OptionMap["WeChatServerToken"] = "" | 	common.OptionMap["WeChatServerToken"] = "" | ||||||
| 	common.OptionMap["WeChatAccountQRCodeImageURL"] = "" | 	common.OptionMap["WeChatAccountQRCodeImageURL"] = "" | ||||||
|  | 	common.OptionMap["GoogleClientId"] = "" | ||||||
|  | 	common.OptionMap["GoogleClientSecret"] = "" | ||||||
| 	common.OptionMap["TurnstileSiteKey"] = "" | 	common.OptionMap["TurnstileSiteKey"] = "" | ||||||
| 	common.OptionMap["TurnstileSecretKey"] = "" | 	common.OptionMap["TurnstileSecretKey"] = "" | ||||||
| 	common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) | 	common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) | ||||||
| @@ -135,8 +141,12 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 			common.EmailVerificationEnabled = boolValue | 			common.EmailVerificationEnabled = boolValue | ||||||
| 		case "GitHubOAuthEnabled": | 		case "GitHubOAuthEnabled": | ||||||
| 			common.GitHubOAuthEnabled = boolValue | 			common.GitHubOAuthEnabled = boolValue | ||||||
|  | 		case "DiscordOAuthEnabled": | ||||||
|  | 			common.DiscordOAuthEnabled = boolValue | ||||||
| 		case "WeChatAuthEnabled": | 		case "WeChatAuthEnabled": | ||||||
| 			common.WeChatAuthEnabled = boolValue | 			common.WeChatAuthEnabled = boolValue | ||||||
|  | 		case "GoogleOAuthEnabled": | ||||||
|  | 			common.GoogleOAuthEnabled = boolValue | ||||||
| 		case "TurnstileCheckEnabled": | 		case "TurnstileCheckEnabled": | ||||||
| 			common.TurnstileCheckEnabled = boolValue | 			common.TurnstileCheckEnabled = boolValue | ||||||
| 		case "RegisterEnabled": | 		case "RegisterEnabled": | ||||||
| @@ -171,6 +181,10 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 		common.GitHubClientId = value | 		common.GitHubClientId = value | ||||||
| 	case "GitHubClientSecret": | 	case "GitHubClientSecret": | ||||||
| 		common.GitHubClientSecret = value | 		common.GitHubClientSecret = value | ||||||
|  | 	case "DiscordClientId": | ||||||
|  | 		common.DiscordClientId = value | ||||||
|  | 	case "DiscordClientSecret": | ||||||
|  | 		common.DiscordClientSecret = value | ||||||
| 	case "Footer": | 	case "Footer": | ||||||
| 		common.Footer = value | 		common.Footer = value | ||||||
| 	case "SystemName": | 	case "SystemName": | ||||||
| @@ -183,6 +197,10 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 		common.WeChatServerToken = value | 		common.WeChatServerToken = value | ||||||
| 	case "WeChatAccountQRCodeImageURL": | 	case "WeChatAccountQRCodeImageURL": | ||||||
| 		common.WeChatAccountQRCodeImageURL = value | 		common.WeChatAccountQRCodeImageURL = value | ||||||
|  | 	case "GoogleClientId": | ||||||
|  | 		common.GoogleClientId = value | ||||||
|  | 	case "GoogleClientSecret": | ||||||
|  | 		common.GoogleClientSecret = value | ||||||
| 	case "TurnstileSiteKey": | 	case "TurnstileSiteKey": | ||||||
| 		common.TurnstileSiteKey = value | 		common.TurnstileSiteKey = value | ||||||
| 	case "TurnstileSecretKey": | 	case "TurnstileSecretKey": | ||||||
|   | |||||||
| @@ -3,8 +3,9 @@ package model | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Redemption struct { | type Redemption struct { | ||||||
| @@ -51,7 +52,14 @@ func Redeem(key string, userId int) (quota int, err error) { | |||||||
| 	redemption := &Redemption{} | 	redemption := &Redemption{} | ||||||
|  |  | ||||||
| 	err = DB.Transaction(func(tx *gorm.DB) error { | 	err = DB.Transaction(func(tx *gorm.DB) error { | ||||||
| 		err := tx.Set("gorm:query_option", "FOR UPDATE").Where("`key` = ?", key).First(redemption).Error | 		whereItem := "`key` = ?" | ||||||
|  |  | ||||||
|  | 		if common.UsingPostgreSQL { | ||||||
|  | 			// Make cmd compatible with PostgreSQL | ||||||
|  | 			whereItem = "\"key\" = ?" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err := tx.Set("gorm:query_option", "FOR UPDATE").Where(whereItem, key).First(redemption).Error | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return errors.New("无效的兑换码") | 			return errors.New("无效的兑换码") | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -3,9 +3,10 @@ package model | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // User if you add sensitive fields, don't forget to clean them in setupLogin function. | // User if you add sensitive fields, don't forget to clean them in setupLogin function. | ||||||
| @@ -19,7 +20,9 @@ type User struct { | |||||||
| 	Status           int    `json:"status" gorm:"type:int;default:1"` // enabled, disabled | 	Status           int    `json:"status" gorm:"type:int;default:1"` // enabled, disabled | ||||||
| 	Email            string `json:"email" gorm:"index" validate:"max=50"` | 	Email            string `json:"email" gorm:"index" validate:"max=50"` | ||||||
| 	GitHubId         string `json:"github_id" gorm:"column:github_id;index"` | 	GitHubId         string `json:"github_id" gorm:"column:github_id;index"` | ||||||
|  | 	DiscordId        string `json:"discord_id" gorm:"column:discord_id;index"` | ||||||
| 	WeChatId         string `json:"wechat_id" gorm:"column:wechat_id;index"` | 	WeChatId         string `json:"wechat_id" gorm:"column:wechat_id;index"` | ||||||
|  | 	GoogleId         string `json:"google_id" gorm:"column:google_id;index"` | ||||||
| 	VerificationCode string `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database! | 	VerificationCode string `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database! | ||||||
| 	AccessToken      string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management | 	AccessToken      string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management | ||||||
| 	Quota            int    `json:"quota" gorm:"type:int;default:0"` | 	Quota            int    `json:"quota" gorm:"type:int;default:0"` | ||||||
| @@ -169,6 +172,14 @@ func (user *User) FillUserByGitHubId() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (user *User) FillUserByDiscordId() error { | ||||||
|  | 	if user.DiscordId == "" { | ||||||
|  | 		return errors.New("Discord id 为空!") | ||||||
|  | 	} | ||||||
|  | 	DB.Where(User{DiscordId: user.DiscordId}).First(user) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (user *User) FillUserByWeChatId() error { | func (user *User) FillUserByWeChatId() error { | ||||||
| 	if user.WeChatId == "" { | 	if user.WeChatId == "" { | ||||||
| 		return errors.New("WeChat id 为空!") | 		return errors.New("WeChat id 为空!") | ||||||
| @@ -177,6 +188,14 @@ func (user *User) FillUserByWeChatId() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (user *User) FillUserByGoogleId() error { | ||||||
|  | 	if user.GoogleId == "" { | ||||||
|  | 		return errors.New("Google id 为空!") | ||||||
|  | 	} | ||||||
|  | 	DB.Where(User{GoogleId: user.GoogleId}).First(user) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (user *User) FillUserByUsername() error { | func (user *User) FillUserByUsername() error { | ||||||
| 	if user.Username == "" { | 	if user.Username == "" { | ||||||
| 		return errors.New("username 为空!") | 		return errors.New("username 为空!") | ||||||
| @@ -193,6 +212,14 @@ func IsWeChatIdAlreadyTaken(wechatId string) bool { | |||||||
| 	return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1 | 	return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func IsDiscordIdAlreadyTaken(discordId string) bool { | ||||||
|  | 	return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsGoogleIdAlreadyTaken(googleId string) bool { | ||||||
|  | 	return DB.Where("google_id = ?", googleId).Find(&User{}).RowsAffected == 1 | ||||||
|  | } | ||||||
|  |  | ||||||
| func IsGitHubIdAlreadyTaken(githubId string) bool { | func IsGitHubIdAlreadyTaken(githubId string) bool { | ||||||
| 	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 | 	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 | ||||||
| } | } | ||||||
| @@ -267,7 +294,13 @@ func GetUserEmail(id int) (email string, err error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetUserGroup(id int) (group string, err error) { | func GetUserGroup(id int) (group string, err error) { | ||||||
| 	err = DB.Model(&User{}).Where("id = ?", id).Select("`group`").Find(&group).Error | 	selectItem := "`group`" | ||||||
|  |  | ||||||
|  | 	if common.UsingPostgreSQL { | ||||||
|  | 		selectItem = "\"group\"" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = DB.Model(&User{}).Where("id = ?", id).Select(selectItem).Find(&group).Error | ||||||
| 	return group, err | 	return group, err | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,14 +21,16 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) | 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) | ||||||
| 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) | 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) | ||||||
| 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) | 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) | ||||||
|  | 		apiRouter.GET("/oauth/discord", middleware.CriticalRateLimit(), controller.DiscordOAuth) | ||||||
| 		apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) | 		apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) | ||||||
|  | 		apiRouter.GET("/oauth/google", middleware.CriticalRateLimit(), controller.GoogleOAuth) | ||||||
| 		apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) | 		apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) | ||||||
| 		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) | 		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) | ||||||
|  |  | ||||||
| 		userRoute := apiRouter.Group("/user") | 		userRoute := apiRouter.Group("/user") | ||||||
| 		{ | 		{ | ||||||
| 			userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) | 			userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) | ||||||
| 			userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login) | 			userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login) | ||||||
| 			userRoute.GET("/logout", controller.Logout) | 			userRoute.GET("/logout", controller.Logout) | ||||||
|  |  | ||||||
| 			selfRoute := userRoute.Group("/") | 			selfRoute := userRoute.Group("/") | ||||||
|   | |||||||
| @@ -3,18 +3,19 @@ | |||||||
|   "version": "0.1.0", |   "version": "0.1.0", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "axios": "^0.27.2", |     "@babel/plugin-proposal-private-property-in-object": "^7.21.11", | ||||||
|  |     "axios": "^1.4.0", | ||||||
|     "history": "^5.3.0", |     "history": "^5.3.0", | ||||||
|     "marked": "^4.1.1", |     "marked": "^5.1.1", | ||||||
|     "react": "^18.2.0", |     "react": "^18.2.0", | ||||||
|     "react-dom": "^18.2.0", |     "react-dom": "^18.2.0", | ||||||
|     "react-dropzone": "^14.2.3", |     "react-dropzone": "^14.2.3", | ||||||
|     "react-router-dom": "^6.3.0", |     "react-router-dom": "^6.14.2", | ||||||
|     "react-scripts": "5.0.1", |     "react-scripts": "5.0.1", | ||||||
|     "react-toastify": "^9.0.8", |     "react-toastify": "^9.1.3", | ||||||
|     "react-turnstile": "^1.0.5", |     "react-turnstile": "^1.1.1", | ||||||
|     "semantic-ui-css": "^2.5.0", |     "semantic-ui-css": "^2.5.0", | ||||||
|     "semantic-ui-react": "^2.1.3" |     "semantic-ui-react": "^2.1.4" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "react-scripts start", |     "start": "react-scripts start", | ||||||
| @@ -41,7 +42,7 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "prettier": "^2.7.1" |     "prettier": "^3.0.0" | ||||||
|   }, |   }, | ||||||
|   "prettier": { |   "prettier": { | ||||||
|     "singleQuote": true, |     "singleQuote": true, | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import AddUser from './pages/User/AddUser'; | |||||||
| import { API, getLogo, getSystemName, showError, showNotice } from './helpers'; | import { API, getLogo, getSystemName, showError, showNotice } from './helpers'; | ||||||
| import PasswordResetForm from './components/PasswordResetForm'; | import PasswordResetForm from './components/PasswordResetForm'; | ||||||
| import GitHubOAuth from './components/GitHubOAuth'; | import GitHubOAuth from './components/GitHubOAuth'; | ||||||
|  | import DiscordOAuth from './components/DiscordOAuth'; | ||||||
|  | import GoogleOAuth from './components/GoogleOAuth'; | ||||||
| import PasswordResetConfirm from './components/PasswordResetConfirm'; | import PasswordResetConfirm from './components/PasswordResetConfirm'; | ||||||
| import { UserContext } from './context/User'; | import { UserContext } from './context/User'; | ||||||
| import { StatusContext } from './context/Status'; | import { StatusContext } from './context/Status'; | ||||||
| @@ -239,6 +241,24 @@ function App() { | |||||||
|           </Suspense> |           </Suspense> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|  |       <Route | ||||||
|  |         HEAD | ||||||
|  |         path='/oauth/discord' | ||||||
|  |         element={ | ||||||
|  |           <Suspense fallback={<Loading></Loading>}> | ||||||
|  |             <DiscordOAuth /> | ||||||
|  |           </Suspense> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  |       <Route | ||||||
|  |         path='/oauth/google' | ||||||
|  |         element={ | ||||||
|  |           <Suspense fallback={<Loading></Loading>}> | ||||||
|  |             <GoogleOAuth /> | ||||||
|  |             support-google-oauth | ||||||
|  |           </Suspense> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|       <Route |       <Route | ||||||
|         path='/setting' |         path='/setting' | ||||||
|         element={ |         element={ | ||||||
| @@ -252,11 +272,11 @@ function App() { | |||||||
|       <Route |       <Route | ||||||
|         path='/topup' |         path='/topup' | ||||||
|         element={ |         element={ | ||||||
|         <PrivateRoute> |           <PrivateRoute> | ||||||
|           <Suspense fallback={<Loading></Loading>}> |             <Suspense fallback={<Loading></Loading>}> | ||||||
|             <TopUp /> |               <TopUp /> | ||||||
|           </Suspense> |             </Suspense> | ||||||
|         </PrivateRoute> |           </PrivateRoute> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|       <Route |       <Route | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								web/src/components/DiscordOAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/src/components/DiscordOAuth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import React, { useContext, useEffect, useState } from 'react'; | ||||||
|  | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; | ||||||
|  | import { useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
|  | import { API, showError, showSuccess } from '../helpers'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
|  | const DiscordOAuth = () => { | ||||||
|  |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |  | ||||||
|  |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [prompt, setPrompt] = useState('处理中...'); | ||||||
|  |   const [processing, setProcessing] = useState(true); | ||||||
|  |  | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const sendCode = async (code, count) => { | ||||||
|  |     const res = await API.get(`/api/oauth/discord?code=${code}`); | ||||||
|  |     const { success, message, data } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (message === 'bind') { | ||||||
|  |         showSuccess('绑定成功!'); | ||||||
|  |         navigate('/setting'); | ||||||
|  |       } else { | ||||||
|  |         userDispatch({ type: 'login', payload: data }); | ||||||
|  |         localStorage.setItem('user', JSON.stringify(data)); | ||||||
|  |         showSuccess('登录成功!'); | ||||||
|  |         navigate('/'); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |       if (count === 0) { | ||||||
|  |         setPrompt(`操作失败,重定向至登录界面中...`); | ||||||
|  |         navigate('/setting'); // in case this is failed to bind GitHub | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       count++; | ||||||
|  |       setPrompt(`出现错误,第 ${count} 次重试中...`); | ||||||
|  |       await new Promise((resolve) => setTimeout(resolve, count * 2000)); | ||||||
|  |       await sendCode(code, count); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let code = searchParams.get('code'); | ||||||
|  |     sendCode(code, 0).then(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Segment style={{ minHeight: '300px' }}> | ||||||
|  |       <Dimmer active inverted> | ||||||
|  |         <Loader size='large'>{prompt}</Loader> | ||||||
|  |       </Dimmer> | ||||||
|  |     </Segment> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default DiscordOAuth; | ||||||
							
								
								
									
										57
									
								
								web/src/components/GoogleOAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/src/components/GoogleOAuth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import React, { useContext, useEffect, useState } from 'react'; | ||||||
|  | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; | ||||||
|  | import { useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
|  | import { API, showError, showSuccess } from '../helpers'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
|  | const GoogleOAuth = () => { | ||||||
|  |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |  | ||||||
|  |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [prompt, setPrompt] = useState('处理中...'); | ||||||
|  |   const [processing, setProcessing] = useState(true); | ||||||
|  |  | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const sendCode = async (code, count) => { | ||||||
|  |     const res = await API.get(`/api/oauth/google?code=${code}`); | ||||||
|  |     const { success, message, data } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (message === 'bind') { | ||||||
|  |         showSuccess('绑定成功!'); | ||||||
|  |         navigate('/setting'); | ||||||
|  |       } else { | ||||||
|  |         userDispatch({ type: 'login', payload: data }); | ||||||
|  |         localStorage.setItem('user', JSON.stringify(data)); | ||||||
|  |         showSuccess('登录成功!'); | ||||||
|  |         navigate('/'); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |       if (count === 0) { | ||||||
|  |         setPrompt(`操作失败,重定向至登录界面中...`); | ||||||
|  |         navigate('/setting'); // in case this is failed to bind GitHub | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       count++; | ||||||
|  |       setPrompt(`出现错误,第 ${count} 次重试中...`); | ||||||
|  |       await new Promise((resolve) => setTimeout(resolve, count * 2000)); | ||||||
|  |       await sendCode(code, count); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let code = searchParams.get('code'); | ||||||
|  |     sendCode(code, 0).then(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Segment style={{ minHeight: '300px' }}> | ||||||
|  |       <Dimmer active inverted> | ||||||
|  |         <Loader size='large'>{prompt}</Loader> | ||||||
|  |       </Dimmer> | ||||||
|  |     </Segment> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default GoogleOAuth; | ||||||
| @@ -2,7 +2,8 @@ import React, { useContext, useEffect, useState } from 'react'; | |||||||
| import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react'; | import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react'; | ||||||
| import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
| import { UserContext } from '../context/User'; | import { UserContext } from '../context/User'; | ||||||
| import { API, getLogo, showError, showSuccess } from '../helpers'; | import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; | ||||||
|  | import Turnstile from 'react-turnstile'; | ||||||
|  |  | ||||||
| const LoginForm = () => { | const LoginForm = () => { | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
| @@ -14,6 +15,9 @@ const LoginForm = () => { | |||||||
|   const [submitted, setSubmitted] = useState(false); |   const [submitted, setSubmitted] = useState(false); | ||||||
|   const { username, password } = inputs; |   const { username, password } = inputs; | ||||||
|   const [userState, userDispatch] = useContext(UserContext); |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [turnstileEnabled, setTurnstileEnabled] = useState(false); | ||||||
|  |   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||||
|  |   const [turnstileToken, setTurnstileToken] = useState(''); | ||||||
|   let navigate = useNavigate(); |   let navigate = useNavigate(); | ||||||
|   const [status, setStatus] = useState({}); |   const [status, setStatus] = useState({}); | ||||||
|   const logo = getLogo(); |   const logo = getLogo(); | ||||||
| @@ -26,17 +30,34 @@ const LoginForm = () => { | |||||||
|     if (status) { |     if (status) { | ||||||
|       status = JSON.parse(status); |       status = JSON.parse(status); | ||||||
|       setStatus(status); |       setStatus(status); | ||||||
|  |  | ||||||
|  |       if (status.turnstile_check) { | ||||||
|  |         setTurnstileEnabled(true); | ||||||
|  |         setTurnstileSiteKey(status.turnstile_site_key); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); |   const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); | ||||||
|  |  | ||||||
|  |   const openGoogleOAuth = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://accounts.google.com/o/oauth2/v2/auth?client_id=${status.google_client_id}&redirect_uri=${window.location.origin}/oauth/google&response_type=code&scope=profile` | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const onGitHubOAuthClicked = () => { |   const onGitHubOAuthClicked = () => { | ||||||
|     window.open( |     window.open( | ||||||
|       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` |       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const onDiscordOAuthClicked = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`, | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const onWeChatLoginClicked = () => { |   const onWeChatLoginClicked = () => { | ||||||
|     setShowWeChatLoginModal(true); |     setShowWeChatLoginModal(true); | ||||||
|   }; |   }; | ||||||
| @@ -65,7 +86,12 @@ const LoginForm = () => { | |||||||
|   async function handleSubmit(e) { |   async function handleSubmit(e) { | ||||||
|     setSubmitted(true); |     setSubmitted(true); | ||||||
|     if (username && password) { |     if (username && password) { | ||||||
|       const res = await API.post(`/api/user/login`, { |       if (turnstileEnabled && turnstileToken === '') { | ||||||
|  |         showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, { | ||||||
|         username, |         username, | ||||||
|         password |         password | ||||||
|       }); |       }); | ||||||
| @@ -108,6 +134,16 @@ const LoginForm = () => { | |||||||
|               value={password} |               value={password} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|  |             {turnstileEnabled ? ( | ||||||
|  |               <Turnstile | ||||||
|  |                 sitekey={turnstileSiteKey} | ||||||
|  |                 onVerify={(token) => { | ||||||
|  |                   setTurnstileToken(token); | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             ) : ( | ||||||
|  |               <></> | ||||||
|  |             )} | ||||||
|             <Button color='green' fluid size='large' onClick={handleSubmit}> |             <Button color='green' fluid size='large' onClick={handleSubmit}> | ||||||
|               登录 |               登录 | ||||||
|             </Button> |             </Button> | ||||||
| @@ -123,28 +159,40 @@ const LoginForm = () => { | |||||||
|             点击注册 |             点击注册 | ||||||
|           </Link> |           </Link> | ||||||
|         </Message> |         </Message> | ||||||
|         {status.github_oauth || status.wechat_login ? ( |         {status.github_oauth || status.wechat_login || status.discord_oauth || status.google_oauth ? ( | ||||||
|           <> |           <> | ||||||
|             <Divider horizontal>Or</Divider> |             <Divider horizontal>Or</Divider> | ||||||
|             {status.github_oauth ? ( |             {status.discord_oauth && ( | ||||||
|  |               <Button | ||||||
|  |                 circular | ||||||
|  |                 color='blue' | ||||||
|  |                 icon='discord' | ||||||
|  |                 onClick={onDiscordOAuthClicked} | ||||||
|  |               /> | ||||||
|  |             )} | ||||||
|  |             {status.github_oauth && ( | ||||||
|               <Button |               <Button | ||||||
|                 circular |                 circular | ||||||
|                 color='black' |                 color='black' | ||||||
|                 icon='github' |                 icon='github' | ||||||
|                 onClick={onGitHubOAuthClicked} |                 onClick={onGitHubOAuthClicked} | ||||||
|               /> |               /> | ||||||
|             ) : ( |  | ||||||
|               <></> |  | ||||||
|             )} |             )} | ||||||
|             {status.wechat_login ? ( |             {status.wechat_login && ( | ||||||
|               <Button |               <Button | ||||||
|                 circular |                 circular | ||||||
|                 color='green' |                 color='green' | ||||||
|                 icon='wechat' |                 icon='wechat' | ||||||
|                 onClick={onWeChatLoginClicked} |                 onClick={onWeChatLoginClicked} | ||||||
|               /> |               /> | ||||||
|             ) : ( |             )} | ||||||
|               <></> |             {status.google_oauth && ( | ||||||
|  |               <Button | ||||||
|  |                 circular | ||||||
|  |                 color='red' | ||||||
|  |                 icon='google' | ||||||
|  |                 onClick={openGoogleOAuth} | ||||||
|  |               /> | ||||||
|             )} |             )} | ||||||
|           </> |           </> | ||||||
|         ) : ( |         ) : ( | ||||||
|   | |||||||
| @@ -112,12 +112,24 @@ const PersonalSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const openGoogleOAuth = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://accounts.google.com/o/oauth2/v2/auth?client_id=${status.google_client_id}&redirect_uri=${window.location.origin}/oauth/google&response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile` | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const openGitHubOAuth = () => { |   const openGitHubOAuth = () => { | ||||||
|     window.open( |     window.open( | ||||||
|       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` |       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const openDiscordOAuth = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&response_type=code&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`, | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const sendVerificationCode = async () => { |   const sendVerificationCode = async () => { | ||||||
|     setDisableButton(true); |     setDisableButton(true); | ||||||
|     if (inputs.email === '') return; |     if (inputs.email === '') return; | ||||||
| @@ -215,6 +227,17 @@ const PersonalSetting = () => { | |||||||
|           <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> |           <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> | ||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
|  |       { | ||||||
|  |         status.discord_oauth && ( | ||||||
|  |           <Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button> | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |       { | ||||||
|  |         status.google_oauth && ( | ||||||
|  |           <Button onClick={openGoogleOAuth}>绑定 Google 账号</Button> | ||||||
|  |  | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|       <Button |       <Button | ||||||
|         onClick={() => { |         onClick={() => { | ||||||
|           setShowEmailBindModal(true); |           setShowEmailBindModal(true); | ||||||
|   | |||||||
| @@ -8,8 +8,11 @@ const SystemSetting = () => { | |||||||
|     PasswordRegisterEnabled: '', |     PasswordRegisterEnabled: '', | ||||||
|     EmailVerificationEnabled: '', |     EmailVerificationEnabled: '', | ||||||
|     GitHubOAuthEnabled: '', |     GitHubOAuthEnabled: '', | ||||||
|  |     DiscordOAuthEnabled: '', | ||||||
|     GitHubClientId: '', |     GitHubClientId: '', | ||||||
|     GitHubClientSecret: '', |     GitHubClientSecret: '', | ||||||
|  |     DiscordClientId: '', | ||||||
|  |     DiscordClientSecret: '', | ||||||
|     Notice: '', |     Notice: '', | ||||||
|     SMTPServer: '', |     SMTPServer: '', | ||||||
|     SMTPPort: '', |     SMTPPort: '', | ||||||
| @@ -22,6 +25,9 @@ const SystemSetting = () => { | |||||||
|     WeChatServerAddress: '', |     WeChatServerAddress: '', | ||||||
|     WeChatServerToken: '', |     WeChatServerToken: '', | ||||||
|     WeChatAccountQRCodeImageURL: '', |     WeChatAccountQRCodeImageURL: '', | ||||||
|  |     GoogleOAuthEnabled: '', | ||||||
|  |     GoogleClientId: '', | ||||||
|  |     GoogleClientSecret: '', | ||||||
|     TurnstileCheckEnabled: '', |     TurnstileCheckEnabled: '', | ||||||
|     TurnstileSiteKey: '', |     TurnstileSiteKey: '', | ||||||
|     TurnstileSecretKey: '', |     TurnstileSecretKey: '', | ||||||
| @@ -56,7 +62,9 @@ const SystemSetting = () => { | |||||||
|       case 'PasswordRegisterEnabled': |       case 'PasswordRegisterEnabled': | ||||||
|       case 'EmailVerificationEnabled': |       case 'EmailVerificationEnabled': | ||||||
|       case 'GitHubOAuthEnabled': |       case 'GitHubOAuthEnabled': | ||||||
|  |       case 'DiscordOAuthEnabled': | ||||||
|       case 'WeChatAuthEnabled': |       case 'WeChatAuthEnabled': | ||||||
|  |       case 'GoogleOAuthEnabled': | ||||||
|       case 'TurnstileCheckEnabled': |       case 'TurnstileCheckEnabled': | ||||||
|       case 'RegisterEnabled': |       case 'RegisterEnabled': | ||||||
|         value = inputs[key] === 'true' ? 'false' : 'true'; |         value = inputs[key] === 'true' ? 'false' : 'true'; | ||||||
| @@ -84,9 +92,13 @@ const SystemSetting = () => { | |||||||
|       name === 'ServerAddress' || |       name === 'ServerAddress' || | ||||||
|       name === 'GitHubClientId' || |       name === 'GitHubClientId' || | ||||||
|       name === 'GitHubClientSecret' || |       name === 'GitHubClientSecret' || | ||||||
|  |       name === 'DiscordClientId' || | ||||||
|  |       name === 'DiscordClientSecret' || | ||||||
|       name === 'WeChatServerAddress' || |       name === 'WeChatServerAddress' || | ||||||
|       name === 'WeChatServerToken' || |       name === 'WeChatServerToken' || | ||||||
|       name === 'WeChatAccountQRCodeImageURL' || |       name === 'WeChatAccountQRCodeImageURL' || | ||||||
|  |       name === 'GoogleClientId' || | ||||||
|  |       name === 'GoogleClientSecret' || | ||||||
|       name === 'TurnstileSiteKey' || |       name === 'TurnstileSiteKey' || | ||||||
|       name === 'TurnstileSecretKey' |       name === 'TurnstileSecretKey' | ||||||
|     ) { |     ) { | ||||||
| @@ -149,6 +161,18 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const submitGoogleOAuth = async () => { | ||||||
|  |     if (originInputs['GoogleClientId'] !== inputs.GoogleClientId) { | ||||||
|  |       await updateOption('GoogleClientId', inputs.GoogleClientId); | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       originInputs['GoogleClientSecret'] !== inputs.GoogleClientSecret && | ||||||
|  |       inputs.GoogleClientSecret !== '' | ||||||
|  |     ) { | ||||||
|  |       await updateOption('GoogleClientSecret', inputs.GoogleClientSecret); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const submitGitHubOAuth = async () => { |   const submitGitHubOAuth = async () => { | ||||||
|     if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { |     if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { | ||||||
|       await updateOption('GitHubClientId', inputs.GitHubClientId); |       await updateOption('GitHubClientId', inputs.GitHubClientId); | ||||||
| @@ -161,6 +185,18 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const submitDiscordOAuth = async () => { | ||||||
|  |     if (originInputs['DiscordClientId'] !== inputs.DiscordClientId) { | ||||||
|  |       await updateOption('DiscordClientId', inputs.DiscordClientId); | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       originInputs['DiscordClientSecret'] !== inputs.DiscordClientSecret && | ||||||
|  |       inputs.DiscordClientSecret !== '' | ||||||
|  |     ) { | ||||||
|  |       await updateOption('DiscordClientSecret', inputs.DiscordClientSecret); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const submitTurnstile = async () => { |   const submitTurnstile = async () => { | ||||||
|     if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { |     if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { | ||||||
|       await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); |       await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); | ||||||
| @@ -211,12 +247,24 @@ const SystemSetting = () => { | |||||||
|               name='EmailVerificationEnabled' |               name='EmailVerificationEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               checked={inputs.DiscordOAuthEnabled === 'true'} | ||||||
|  |               label='允许通过 Discord 账户登录和注册' | ||||||
|  |               name='DiscordOAuthEnabled' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.GitHubOAuthEnabled === 'true'} |               checked={inputs.GitHubOAuthEnabled === 'true'} | ||||||
|               label='允许通过 GitHub 账户登录 & 注册' |               label='允许通过 GitHub 账户登录 & 注册' | ||||||
|               name='GitHubOAuthEnabled' |               name='GitHubOAuthEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               checked={inputs.GoogleOAuthEnabled === 'true'} | ||||||
|  |               label='允许通过 Google 账户登录和注册' | ||||||
|  |               name='GoogleOAuthEnabled' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.WeChatAuthEnabled === 'true'} |               checked={inputs.WeChatAuthEnabled === 'true'} | ||||||
|               label='允许通过微信登录 & 注册' |               label='允许通过微信登录 & 注册' | ||||||
| @@ -372,6 +420,82 @@ const SystemSetting = () => { | |||||||
|             保存 WeChat Server 设置 |             保存 WeChat Server 设置 | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|  |           <Header as='h3'> | ||||||
|  |             配置 Discord OAuth 应用程序 | ||||||
|  |             <Header.Subheader> | ||||||
|  |               用以支持通过 Discord 进行登录注册, | ||||||
|  |               <a href='https://discord.com/developers/applications' target='_blank'> | ||||||
|  |                 点击此处 | ||||||
|  |               </a> | ||||||
|  |               管理你的 Discord OAuth App | ||||||
|  |             </Header.Subheader> | ||||||
|  |           </Header> | ||||||
|  |           <Message> | ||||||
|  |             Homepage URL 填 <code>{inputs.ServerAddress}</code> | ||||||
|  |             ,Authorization callback URL 填{' '} | ||||||
|  |             <code>{`${inputs.ServerAddress}/oauth/discord`}</code> | ||||||
|  |           </Message> | ||||||
|  |           <Form.Group widths={3}> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Discord 客户 ID' | ||||||
|  |               name='DiscordClientId' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.DiscordClientId} | ||||||
|  |               placeholder='输入您注册的 Discord OAuth APP 的 ID' | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Discord 客户秘密' | ||||||
|  |               name='DiscordClientSecret' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               type='password' | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.DiscordClientSecret} | ||||||
|  |               placeholder='敏感信息不会发送到前端显示' | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Button onClick={submitDiscordOAuth}> | ||||||
|  |             保存 Discord OAuth 设置 | ||||||
|  |           </Form.Button> | ||||||
|  |           <Divider /> | ||||||
|  |           <Header as='h3'> | ||||||
|  |             配置 Google OAuth 应用程序 | ||||||
|  |             <Header.Subheader> | ||||||
|  |               用以支持通过 Google 进行登录注册, | ||||||
|  |               <a href='https://console.cloud.google.com/' target='_blank'> | ||||||
|  |                 点击此处 | ||||||
|  |               </a> | ||||||
|  |               管理你的 Google OAuth App | ||||||
|  |             </Header.Subheader> | ||||||
|  |           </Header> | ||||||
|  |           <Message> | ||||||
|  |             Homepage URL 填 <code>{inputs.ServerAddress}</code> | ||||||
|  |             ,Authorization callback URL 填{' '} | ||||||
|  |             <code>{`${inputs.ServerAddress}/oauth/google`}</code> | ||||||
|  |           </Message> | ||||||
|  |           <Form.Group widths={3}> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Google 客户 ID' | ||||||
|  |               name='GoogleClientId' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.GoogleClientId} | ||||||
|  |               placeholder='输入您注册的 Google OAuth APP 的 ID' | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Google 客户秘密' | ||||||
|  |               name='GoogleClientSecret' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               type='password' | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.GoogleClientSecret} | ||||||
|  |               placeholder='敏感信息不会发送到前端显示' | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Button onClick={submitGoogleOAuth}> | ||||||
|  |             保存 Google OAuth 设置 | ||||||
|  |           </Form.Button> | ||||||
|  |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 Turnstile |             配置 Turnstile | ||||||
|             <Header.Subheader> |             <Header.Subheader> | ||||||
|   | |||||||
| @@ -22,6 +22,8 @@ const EditChannel = () => { | |||||||
|     base_url: '', |     base_url: '', | ||||||
|     other: '', |     other: '', | ||||||
|     model_mapping: '', |     model_mapping: '', | ||||||
|  |     allow_streaming: 1, | ||||||
|  |     allow_non_streaming: 1, | ||||||
|     models: [], |     models: [], | ||||||
|     groups: ['default'] |     groups: ['default'] | ||||||
|   }; |   }; | ||||||
| @@ -94,6 +96,9 @@ const EditChannel = () => { | |||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let localModelOptions = [...originModelOptions]; |     let localModelOptions = [...originModelOptions]; | ||||||
|  |     if (!Array.isArray(inputs.models)) { | ||||||
|  |       inputs.models = inputs.models.split(','); | ||||||
|  |     } | ||||||
|     inputs.models.forEach((model) => { |     inputs.models.forEach((model) => { | ||||||
|       if (!localModelOptions.find((option) => option.key === model)) { |       if (!localModelOptions.find((option) => option.key === model)) { | ||||||
|         localModelOptions.push({ |         localModelOptions.push({ | ||||||
| @@ -127,6 +132,11 @@ const EditChannel = () => { | |||||||
|       showInfo('模型映射必须是合法的 JSON 格式!'); |       showInfo('模型映射必须是合法的 JSON 格式!'); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     // allow streaming and allow non streaming cannot be both false | ||||||
|  |     if (inputs.allow_streaming === 2 && inputs.allow_non_streaming === 2) { | ||||||
|  |       showInfo('流式请求和非流式请求不能同时禁用!'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     let localInputs = inputs; |     let localInputs = inputs; | ||||||
|     if (localInputs.base_url.endsWith('/')) { |     if (localInputs.base_url.endsWith('/')) { | ||||||
|       localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); |       localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); | ||||||
| @@ -176,7 +186,7 @@ const EditChannel = () => { | |||||||
|                 <Message> |                 <Message> | ||||||
|                   注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model |                   注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model | ||||||
|                   参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank' |                   参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank' | ||||||
|                                                                     href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。 |                     href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。 | ||||||
|                 </Message> |                 </Message> | ||||||
|                 <Form.Field> |                 <Form.Field> | ||||||
|                   <Form.Input |                   <Form.Input | ||||||
| @@ -270,7 +280,7 @@ const EditChannel = () => { | |||||||
|             }}>清除所有模型</Button> |             }}>清除所有模型</Button> | ||||||
|             <Input |             <Input | ||||||
|               action={ |               action={ | ||||||
|                 <Button type={'button'} onClick={()=>{ |                 <Button type={'button'} onClick={() => { | ||||||
|                   if (customModel.trim() === "") return; |                   if (customModel.trim() === "") return; | ||||||
|                   if (inputs.models.includes(customModel)) return; |                   if (inputs.models.includes(customModel)) return; | ||||||
|                   let localModels = [...inputs.models]; |                   let localModels = [...inputs.models]; | ||||||
| @@ -281,7 +291,7 @@ const EditChannel = () => { | |||||||
|                     text: customModel, |                     text: customModel, | ||||||
|                     value: customModel, |                     value: customModel, | ||||||
|                   }); |                   }); | ||||||
|                   setModelOptions(modelOptions=>{ |                   setModelOptions(modelOptions => { | ||||||
|                     return [...modelOptions, ...localModelOptions]; |                     return [...modelOptions, ...localModelOptions]; | ||||||
|                   }); |                   }); | ||||||
|                   setCustomModel(''); |                   setCustomModel(''); | ||||||
| @@ -306,6 +316,26 @@ const EditChannel = () => { | |||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               checked={inputs.allow_streaming === 1} | ||||||
|  |               label='允许流式请求' | ||||||
|  |               name='allow_streaming' | ||||||
|  |               onChange={() => { | ||||||
|  |                 setInputs((inputs) => ({ ...inputs, allow_streaming: inputs.allow_streaming === 1 ? 2 : 1 })); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               checked={inputs.allow_non_streaming === 1} | ||||||
|  |               label='允许非流式请求' | ||||||
|  |               name='allow_non_streaming' | ||||||
|  |               onChange={() => { | ||||||
|  |                 setInputs((inputs) => ({ ...inputs, allow_non_streaming: inputs.allow_non_streaming === 1 ? 2 : 1 })); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|           { |           { | ||||||
|             batch ? <Form.Field> |             batch ? <Form.Field> | ||||||
|               <Form.TextArea |               <Form.TextArea | ||||||
|   | |||||||
| @@ -97,12 +97,24 @@ const Home = () => { | |||||||
|                           ? '已启用' |                           ? '已启用' | ||||||
|                           : '未启用'} |                           : '未启用'} | ||||||
|                       </p> |                       </p> | ||||||
|  |                       <p> | ||||||
|  |                         Discord 身份验证: | ||||||
|  |                         {statusState?.status?.discord_oauth === true | ||||||
|  |                           ? '已启用' | ||||||
|  |                           : '未启用'} | ||||||
|  |                       </p> | ||||||
|                       <p> |                       <p> | ||||||
|                         微信身份验证: |                         微信身份验证: | ||||||
|                         {statusState?.status?.wechat_login === true |                         {statusState?.status?.wechat_login === true | ||||||
|                           ? '已启用' |                           ? '已启用' | ||||||
|                           : '未启用'} |                           : '未启用'} | ||||||
|                       </p> |                       </p> | ||||||
|  |                       <p> | ||||||
|  |                         Google 身份验证: | ||||||
|  |                         {statusState?.status?.google_oauth === true | ||||||
|  |                           ? '已启用' | ||||||
|  |                           : '未启用'} | ||||||
|  |                       </p> | ||||||
|                       <p> |                       <p> | ||||||
|                         Turnstile 用户校验: |                         Turnstile 用户校验: | ||||||
|                         {statusState?.status?.turnstile_check === true |                         {statusState?.status?.turnstile_check === true | ||||||
|   | |||||||
| @@ -13,13 +13,15 @@ const EditUser = () => { | |||||||
|     display_name: '', |     display_name: '', | ||||||
|     password: '', |     password: '', | ||||||
|     github_id: '', |     github_id: '', | ||||||
|  |     discord_id: '', | ||||||
|     wechat_id: '', |     wechat_id: '', | ||||||
|  |     google_id: '', | ||||||
|     email: '', |     email: '', | ||||||
|     quota: 0, |     quota: 0, | ||||||
|     group: 'default' |     group: 'default' | ||||||
|   }); |   }); | ||||||
|   const [groupOptions, setGroupOptions] = useState([]); |   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, google_id, discord_id } = | ||||||
|     inputs; |     inputs; | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
| @@ -166,6 +168,26 @@ const EditUser = () => { | |||||||
|               readOnly |               readOnly | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label='已绑定的 Discord 账户' | ||||||
|  |               name='discord_id' | ||||||
|  |               value={discord_id} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' | ||||||
|  |               readOnly | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label='已绑定的 Google 账户' | ||||||
|  |               name='google_id' | ||||||
|  |               value={google_id} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' | ||||||
|  |               readOnly | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='已绑定的邮箱账户' |               label='已绑定的邮箱账户' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user