mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			v0.6.11-al
			...
			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,6 +61,68 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
 | 
				
			|||||||
		return err, nil
 | 
							return err, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer resp.Body.Close()
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return errors.New(fmt.Sprintf("status code %d", resp.StatusCode)), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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
 | 
							var response TextResponse
 | 
				
			||||||
		err = json.NewDecoder(resp.Body).Decode(&response)
 | 
							err = json.NewDecoder(resp.Body).Decode(&response)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -66,13 +131,16 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
 | 
				
			|||||||
		if response.Usage.CompletionTokens == 0 {
 | 
							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 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"
 | 
				
			||||||
@@ -13,6 +14,7 @@ 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,6 +1,7 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"one-api/common"
 | 
						"one-api/common"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -10,15 +11,37 @@ type Ability struct {
 | 
				
			|||||||
	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
 | 
				
			||||||
@@ -40,6 +63,8 @@ func (channel *Channel) AddAbilities() error {
 | 
				
			|||||||
				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={
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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);
 | 
				
			||||||
@@ -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