mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			39 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 379d03798c | ||
|  | 520eb34b72 | ||
|  | 855bb82ae7 | ||
|  | 8c91bd9c97 | ||
|  | 7c7a45a4f5 | ||
|  | 0ac0214c41 | ||
|  | b63400ebe2 | ||
|  | b17d9bc649 | ||
|  | 9ef8167e5d | ||
|  | 3baad1d926 | ||
|  | 80d5d6edfb | ||
|  | 12365ccf69 | ||
|  | 4928319494 | ||
|  | 839dcc3ab2 | ||
|  | 270e366cd9 | ||
|  | 67b8e82457 | ||
|  | 4b2cb573b6 | ||
|  | bde43cc358 | ||
|  | 6e06dcfcf8 | ||
|  | b617599211 | ||
|  | 1656b9a1de | ||
|  | eb5f8f2d75 | ||
|  | 2ae5741214 | ||
|  | 28d58849a0 | ||
|  | adc9679d56 | ||
|  | 07589ae305 | ||
|  | 95bc32c555 | ||
|  | d61dc4a9ca | ||
|  | b29acb0c89 | ||
|  | a8e418275d | ||
|  | d7ab9b0935 | ||
|  | ebd62c3bfc | ||
|  | 4e6f9f67b3 | ||
|  | 77d295bbf5 | ||
|  | 7e3e25fbd9 | ||
|  | 9bf98ab53a | ||
|  | 3ed9a219c7 | ||
|  | 37d7afcedc | ||
|  | 2756554f7c | 
							
								
								
									
										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 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,4 +4,5 @@ upload | |||||||
| *.exe | *.exe | ||||||
| *.db | *.db | ||||||
| build | build | ||||||
| *.db-journal | *.db-journal | ||||||
|  | .env* | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,31 +1,29 @@ | |||||||
| FROM node:16 as builder | # Node build stage | ||||||
|  | FROM node:18 as builder | ||||||
| WORKDIR /build | WORKDIR /build | ||||||
|  | COPY ./web/package*.json ./ | ||||||
|  | RUN npm ci | ||||||
| COPY ./web . | COPY ./web . | ||||||
| COPY ./VERSION . | COPY ./VERSION . | ||||||
| RUN npm install |  | ||||||
| RUN REACT_APP_VERSION=$(cat VERSION) npm run build | RUN REACT_APP_VERSION=$(cat VERSION) npm run build | ||||||
|  |  | ||||||
|  | # Go build stage | ||||||
| FROM golang AS builder2 | FROM golang AS builder2 | ||||||
|  |  | ||||||
| ENV GO111MODULE=on \ | ENV GO111MODULE=on \ | ||||||
|     CGO_ENABLED=1 \ |     CGO_ENABLED=1 \ | ||||||
|     GOOS=linux |     GOOS=linux | ||||||
|  |  | ||||||
| WORKDIR /build | WORKDIR /build | ||||||
|  | COPY go.mod . | ||||||
|  | COPY go.sum . | ||||||
|  | RUN go mod download | ||||||
| COPY . . | COPY . . | ||||||
| COPY --from=builder /build/build ./web/build | COPY --from=builder /build/build ./web/build | ||||||
| RUN go mod download |  | ||||||
| RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api | RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api | ||||||
|  |  | ||||||
|  | # Final stage | ||||||
| FROM alpine | FROM alpine | ||||||
|  | RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true | ||||||
| RUN apk update \ | WORKDIR /data | ||||||
|     && apk upgrade \ |  | ||||||
|     && apk add --no-cache ca-certificates tzdata \ |  | ||||||
|     && update-ca-certificates 2>/dev/null || true |  | ||||||
|  |  | ||||||
| COPY --from=builder2 /build/one-api / | COPY --from=builder2 /build/one-api / | ||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
| WORKDIR /data |  | ||||||
| ENTRYPOINT ["/one-api"] | ENTRYPOINT ["/one-api"] | ||||||
|   | |||||||
| @@ -57,6 +57,12 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
|  |  | ||||||
| > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 | > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 | ||||||
|  |  | ||||||
|  | ## 👍Forks 特殊功能⚡ | ||||||
|  |  | ||||||
|  | 1. 增强的**频道测试**以提高稳定性。 | ||||||
|  | 2. 支持 **Dall-E 2 模型图像生成** API。 | ||||||
|  | 3. 修复**登录页面**中缺少的 **Turnstile 验证码**。 | ||||||
|  |  | ||||||
| ## 功能 | ## 功能 | ||||||
| 1. 支持多种 API 访问渠道: | 1. 支持多种 API 访问渠道: | ||||||
|    + [x] OpenAI 官方通道(支持配置镜像) |    + [x] OpenAI 官方通道(支持配置镜像) | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ 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 TurnstileCheckEnabled = false | var TurnstileCheckEnabled = false | ||||||
| var RegisterEnabled = true | var RegisterEnabled = true | ||||||
| @@ -53,6 +54,9 @@ 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 = "" | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ var ModelRatio = map[string]float64{ | |||||||
| 	"text-search-ada-doc-001": 10, | 	"text-search-ada-doc-001": 10, | ||||||
| 	"text-moderation-stable":  0.1, | 	"text-moderation-stable":  0.1, | ||||||
| 	"text-moderation-latest":  0.1, | 	"text-moderation-latest":  0.1, | ||||||
|  | 	"dall-e":                  8, | ||||||
| } | } | ||||||
|  |  | ||||||
| func ModelRatio2JSONString() string { | func ModelRatio2JSONString() string { | ||||||
|   | |||||||
| @@ -1,18 +1,23 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetSubscription(c *gin.Context) { | func GetSubscription(c *gin.Context) { | ||||||
| 	var quota int | 	var quota int | ||||||
| 	var err error | 	var err error | ||||||
| 	var token *model.Token | 	var expirationDate int64 | ||||||
|  |  | ||||||
|  | 	tokenId := c.GetInt("token_id") | ||||||
|  | 	token, err := model.GetTokenById(tokenId) | ||||||
|  |  | ||||||
|  | 	expirationDate = token.ExpiredTime | ||||||
|  |  | ||||||
| 	if common.DisplayTokenStatEnabled { | 	if common.DisplayTokenStatEnabled { | ||||||
| 		tokenId := c.GetInt("token_id") |  | ||||||
| 		token, err = model.GetTokenById(tokenId) |  | ||||||
| 		quota = token.RemainQuota | 		quota = token.RemainQuota | ||||||
| 	} else { | 	} else { | ||||||
| 		userId := c.GetInt("id") | 		userId := c.GetInt("id") | ||||||
| @@ -41,6 +46,7 @@ func GetSubscription(c *gin.Context) { | |||||||
| 		SoftLimitUSD:       amount, | 		SoftLimitUSD:       amount, | ||||||
| 		HardLimitUSD:       amount, | 		HardLimitUSD:       amount, | ||||||
| 		SystemHardLimitUSD: amount, | 		SystemHardLimitUSD: amount, | ||||||
|  | 		AccessUntil:        expirationDate, | ||||||
| 	} | 	} | ||||||
| 	c.JSON(200, subscription) | 	c.JSON(200, subscription) | ||||||
| 	return | 	return | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ type OpenAISubscriptionResponse struct { | |||||||
| 	SoftLimitUSD       float64 `json:"soft_limit_usd"` | 	SoftLimitUSD       float64 `json:"soft_limit_usd"` | ||||||
| 	HardLimitUSD       float64 `json:"hard_limit_usd"` | 	HardLimitUSD       float64 `json:"hard_limit_usd"` | ||||||
| 	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` | 	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` | ||||||
|  | 	AccessUntil        int64   `json:"access_until"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type OpenAIUsageDailyCost struct { | type OpenAIUsageDailyCost struct { | ||||||
| @@ -96,6 +97,9 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 	if res.StatusCode != http.StatusOK { | ||||||
|  | 		return nil, fmt.Errorf("status code: %d", res.StatusCode) | ||||||
|  | 	} | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|   | |||||||
| @@ -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 { | func testChannel(channel *model.Channel, request ChatRequest) error { | ||||||
| @@ -50,15 +53,56 @@ func testChannel(channel *model.Channel, request ChatRequest) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		return errors.New("invalid status code: " + strconv.Itoa(resp.StatusCode)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var streamResponseText string | ||||||
|  |  | ||||||
|  | 	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\n"); i >= 0 { | ||||||
|  | 			return i + 2, 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 { // must be something wrong! | ||||||
|  | 			common.SysError("invalid stream response: " + data) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		data = data[6:] | ||||||
|  | 		if !strings.HasPrefix(data, "[DONE]") { | ||||||
|  | 			var streamResponse ChatCompletionsStreamResponse | ||||||
|  | 			err = json.Unmarshal([]byte(data), &streamResponse) | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			for _, choice := range streamResponse.Choices { | ||||||
|  | 				streamResponseText += choice.Delta.Content | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 	var response TextResponse |  | ||||||
| 	err = json.NewDecoder(resp.Body).Decode(&response) | 	// Check if streaming is complete and streamResponseText is populated | ||||||
| 	if err != nil { | 	if streamResponseText == "" { | ||||||
| 		return err | 		return errors.New("Streaming not complete") | ||||||
| 	} |  | ||||||
| 	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)) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -66,6 +110,7 @@ func buildTestRequest() *ChatRequest { | |||||||
| 	testRequest := &ChatRequest{ | 	testRequest := &ChatRequest{ | ||||||
| 		Model:     "", // this will be set later | 		Model:     "", // this will be set later | ||||||
| 		MaxTokens: 1, | 		MaxTokens: 1, | ||||||
|  | 		Stream:    true, | ||||||
| 	} | 	} | ||||||
| 	testMessage := Message{ | 	testMessage := Message{ | ||||||
| 		Role:    "user", | 		Role:    "user", | ||||||
|   | |||||||
							
								
								
									
										195
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | |||||||
|  | package controller | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"one-api/common" | ||||||
|  | 	"one-api/model" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  |  | ||||||
|  | 	disgoauth "github.com/realTristan/disgoauth" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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("Invalid parameter") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Establish a new discord client | ||||||
|  | 	var dc *disgoauth.Client = disgoauth.Init(&disgoauth.Client{ | ||||||
|  | 		ClientID:     common.DiscordClientId, | ||||||
|  | 		ClientSecret: common.DiscordClientSecret, | ||||||
|  | 		RedirectURI:  fmt.Sprintf("https://%s/oauth/discord", host), | ||||||
|  | 		Scopes:       []string{disgoauth.ScopeIdentify, disgoauth.ScopeEmail}, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	accessToken, _ := dc.GetOnlyAccessToken(codeFromURLParamaters) | ||||||
|  |  | ||||||
|  | 	// Get the authorized user's data using the above accessToken | ||||||
|  | 	userData, _ := disgoauth.GetUserData(accessToken) | ||||||
|  |  | ||||||
|  | 	// Create a new DiscordUser | ||||||
|  | 	var discordUser DiscordUser | ||||||
|  |  | ||||||
|  | 	// Decode the userData map[string]interface{} into the discordUser | ||||||
|  | 	// Convert the map to JSON | ||||||
|  | 	jsonData, _ := json.Marshal(userData) | ||||||
|  |  | ||||||
|  | 	// Convert the JSON to a struct | ||||||
|  | 	err := json.Unmarshal(jsonData, &discordUser) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if discordUser.Username == "" { | ||||||
|  | 		return nil, errors.New("Invalid return value, user field is empty, please try again later!") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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") | ||||||
|  | 	host := c.Request.Host | ||||||
|  | 	discordUser, err := getDiscordUserInfoByCode(code, 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 | ||||||
|  | } | ||||||
| @@ -5,13 +5,14 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type GitHubOAuthResponse struct { | type GitHubOAuthResponse struct { | ||||||
|   | |||||||
| @@ -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,8 @@ 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, | ||||||
| 			"system_name":         common.SystemName, | 			"system_name":         common.SystemName, | ||||||
| 			"logo":                common.Logo, | 			"logo":                common.Logo, | ||||||
| 			"footer_html":         common.Footer, | 			"footer_html":         common.Footer, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package controller | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -53,6 +54,15 @@ func init() { | |||||||
| 	}) | 	}) | ||||||
| 	// https://platform.openai.com/docs/models/model-endpoint-compatibility | 	// https://platform.openai.com/docs/models/model-endpoint-compatibility | ||||||
| 	openAIModels = []OpenAIModels{ | 	openAIModels = []OpenAIModels{ | ||||||
|  | 		{ | ||||||
|  | 			Id:         "dall-e", | ||||||
|  | 			Object:     "model", | ||||||
|  | 			Created:    1677649963, | ||||||
|  | 			OwnedBy:    "openai", | ||||||
|  | 			Permission: permission, | ||||||
|  | 			Root:       "dall-e", | ||||||
|  | 			Parent:     nil, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Id:         "gpt-3.5-turbo", | 			Id:         "gpt-3.5-turbo", | ||||||
| 			Object:     "model", | 			Object:     "model", | ||||||
|   | |||||||
| @@ -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) { | ||||||
| @@ -41,6 +42,14 @@ func UpdateOption(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	switch option.Key { | 	switch option.Key { | ||||||
|  | 	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 "GitHubOAuthEnabled": | 	case "GitHubOAuthEnabled": | ||||||
| 		if option.Value == "true" && common.GitHubClientId == "" { | 		if option.Value == "true" && common.GitHubClientId == "" { | ||||||
| 			c.JSON(http.StatusOK, gin.H{ | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -1,34 +1,181 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"one-api/common" | ||||||
|  | 	"one-api/model" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | ||||||
| 	// TODO: this part is not finished | 	imageModel := "dall-e" | ||||||
| 	req, err := http.NewRequest(c.Request.Method, c.Request.RequestURI, c.Request.Body) |  | ||||||
|  | 	tokenId := c.GetInt("token_id") | ||||||
|  | 	channelType := c.GetInt("channel") | ||||||
|  | 	userId := c.GetInt("id") | ||||||
|  | 	consumeQuota := c.GetBool("consume_quota") | ||||||
|  | 	group := c.GetString("group") | ||||||
|  |  | ||||||
|  | 	var textRequest GeneralOpenAIRequest | ||||||
|  | 	if consumeQuota { | ||||||
|  | 		err := common.UnmarshalBodyReusable(c, &textRequest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Prompt validation | ||||||
|  | 	if textRequest.Prompt == "" { | ||||||
|  | 		return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Not "256x256", "512x512", or "1024x1024" | ||||||
|  | 	if textRequest.Size != "" && textRequest.Size != "256x256" && textRequest.Size != "512x512" && textRequest.Size != "1024x1024" { | ||||||
|  | 		return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// N should between 1 to 10 | ||||||
|  | 	if textRequest.N != 0 && (textRequest.N < 1 || textRequest.N > 10) { | ||||||
|  | 		return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// map model name | ||||||
|  | 	modelMapping := c.GetString("model_mapping") | ||||||
|  | 	isModelMapped := false | ||||||
|  | 	if modelMapping != "" { | ||||||
|  | 		modelMap := make(map[string]string) | ||||||
|  | 		err := json.Unmarshal([]byte(modelMapping), &modelMap) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		if modelMap[imageModel] != "" { | ||||||
|  | 			imageModel = modelMap[imageModel] | ||||||
|  | 			isModelMapped = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	baseURL := common.ChannelBaseURLs[channelType] | ||||||
|  | 	requestURL := c.Request.URL.String() | ||||||
|  |  | ||||||
|  | 	if c.GetString("base_url") != "" { | ||||||
|  | 		baseURL = c.GetString("base_url") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) | ||||||
|  |  | ||||||
|  | 	var requestBody io.Reader | ||||||
|  | 	if isModelMapped { | ||||||
|  | 		jsonStr, err := json.Marshal(textRequest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		requestBody = bytes.NewBuffer(jsonStr) | ||||||
|  | 	} else { | ||||||
|  | 		requestBody = c.Request.Body | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	modelRatio := common.GetModelRatio(imageModel) | ||||||
|  | 	groupRatio := common.GetGroupRatio(group) | ||||||
|  | 	ratio := modelRatio * groupRatio | ||||||
|  | 	userQuota, err := model.CacheGetUserQuota(userId) | ||||||
|  |  | ||||||
|  | 	sizeRatio := 1.0 | ||||||
|  | 	// Size | ||||||
|  | 	if textRequest.Size == "256x256" { | ||||||
|  | 		sizeRatio = 1 | ||||||
|  | 	} else if textRequest.Size == "512x512" { | ||||||
|  | 		sizeRatio = 1.125 | ||||||
|  | 	} else if textRequest.Size == "1024x1024" { | ||||||
|  | 		sizeRatio = 1.25 | ||||||
|  | 	} | ||||||
|  | 	quota := int(ratio * sizeRatio * 1000) | ||||||
|  |  | ||||||
|  | 	if consumeQuota && userQuota-quota < 0 { | ||||||
|  | 		return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) | ||||||
|  |  | ||||||
|  | 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||||
|  | 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||||
|  |  | ||||||
| 	client := &http.Client{} | 	client := &http.Client{} | ||||||
| 	resp, err := client.Do(req) | 	resp, err := client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "do_request_failed", http.StatusOK) | 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = req.Body.Close() | 	err = req.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusOK) | 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | 	err = c.Request.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	var textResponse ImageResponse | ||||||
|  |  | ||||||
|  | 	defer func() { | ||||||
|  | 		if consumeQuota { | ||||||
|  | 			err := model.PostConsumeTokenQuota(tokenId, quota) | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error consuming token remain quota: " + err.Error()) | ||||||
|  | 			} | ||||||
|  | 			err = model.CacheUpdateUserQuota(userId) | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error update user quota cache: " + err.Error()) | ||||||
|  | 			} | ||||||
|  | 			if quota != 0 { | ||||||
|  | 				tokenName := c.GetString("token_name") | ||||||
|  | 				logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) | ||||||
|  | 				model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent) | ||||||
|  | 				model.UpdateUserUsedQuotaAndRequestCount(userId, quota) | ||||||
|  | 				channelId := c.GetInt("channel_id") | ||||||
|  | 				model.UpdateChannelUsedQuota(channelId, quota) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	if consumeQuota { | ||||||
|  | 		responseBody, err := io.ReadAll(resp.Body) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		err = resp.Body.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		err = json.Unmarshal(responseBody, &textResponse) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for k, v := range resp.Header { | 	for k, v := range resp.Header { | ||||||
| 		c.Writer.Header().Set(k, v[0]) | 		c.Writer.Header().Set(k, v[0]) | ||||||
| 	} | 	} | ||||||
| 	c.Writer.WriteHeader(resp.StatusCode) | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  |  | ||||||
| 	_, err = io.Copy(c.Writer, resp.Body) | 	_, err = io.Copy(c.Writer, resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "copy_response_body_failed", http.StatusOK) | 		return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
| 	err = resp.Body.Close() | 	err = resp.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "close_response_body_failed", http.StatusOK) | 		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,12 +6,14 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | ||||||
| @@ -136,7 +138,28 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		} | 		} | ||||||
| 		requestBody = bytes.NewBuffer(jsonStr) | 		requestBody = bytes.NewBuffer(jsonStr) | ||||||
| 	} else { | 	} else { | ||||||
| 		requestBody = c.Request.Body | 		bodyBytes, err := io.ReadAll(c.Request.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "read_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		var bodyMap map[string]interface{} | ||||||
|  | 		err = json.Unmarshal(bodyBytes, &bodyMap) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "unmarshal_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Add "stream":true to body map if it doesn't exist | ||||||
|  | 		if _, exists := bodyMap["stream"]; !exists { | ||||||
|  | 			bodyMap["stream"] = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Marshal the body map back into JSON | ||||||
|  | 		bodyBytes, err = json.Marshal(bodyMap) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "marshal_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		requestBody = bytes.NewBuffer(bodyBytes) | ||||||
| 	} | 	} | ||||||
| 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -157,6 +180,19 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		// Print Data if Error | ||||||
|  | 		bodyBytes, err := io.ReadAll(resp.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bodyString := string(bodyBytes) | ||||||
|  |  | ||||||
|  | 		log.Printf("Error: %s", bodyString) | ||||||
|  |  | ||||||
|  | 		return errorWrapper(err, "request_failed", resp.StatusCode) | ||||||
|  | 	} | ||||||
| 	err = req.Body.Close() | 	err = req.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
|   | |||||||
| @@ -2,10 +2,11 @@ package controller | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Message struct { | type Message struct { | ||||||
| @@ -37,12 +38,14 @@ type GeneralOpenAIRequest struct { | |||||||
| 	N           int       `json:"n,omitempty"` | 	N           int       `json:"n,omitempty"` | ||||||
| 	Input       any       `json:"input,omitempty"` | 	Input       any       `json:"input,omitempty"` | ||||||
| 	Instruction string    `json:"instruction,omitempty"` | 	Instruction string    `json:"instruction,omitempty"` | ||||||
|  | 	Size        string    `json:"size,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ChatRequest struct { | 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 { | ||||||
| @@ -76,6 +79,13 @@ type TextResponse struct { | |||||||
| 	Error OpenAIError `json:"error"` | 	Error OpenAIError `json:"error"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ImageResponse struct { | ||||||
|  | 	Created int `json:"created"` | ||||||
|  | 	Data    []struct { | ||||||
|  | 		Url string `json:"url"` | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| type ChatCompletionsStreamResponse struct { | type ChatCompletionsStreamResponse struct { | ||||||
| 	Choices []struct { | 	Choices []struct { | ||||||
| 		Delta struct { | 		Delta struct { | ||||||
|   | |||||||
| @@ -3,12 +3,13 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type LoginRequest struct { | type LoginRequest struct { | ||||||
| @@ -477,6 +478,16 @@ func DeleteUser(c *gin.Context) { | |||||||
|  |  | ||||||
| func DeleteSelf(c *gin.Context) { | func DeleteSelf(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt("id") | ||||||
|  | 	user, _ := model.GetUserById(id, false) | ||||||
|  |  | ||||||
|  | 	if user.Role == common.RoleRootUser { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "无权删除超级管理员", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	err := model.DeleteUserById(id) | 	err := model.DeleteUserById(id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								english.dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								english.dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | # Initial stage | ||||||
|  | FROM python:3.11 as translator | ||||||
|  | WORKDIR /app | ||||||
|  | COPY . . | ||||||
|  | RUN python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json | ||||||
|  |  | ||||||
|  | # Node build stage | ||||||
|  | FROM node:18-alpine as nodeBuilder | ||||||
|  | WORKDIR /build | ||||||
|  | COPY ./web/package*.json ./ | ||||||
|  | RUN npm ci | ||||||
|  | COPY --from=translator /app . | ||||||
|  | RUN cd web && REACT_APP_VERSION=$(cat VERSION) npm run build | ||||||
|  |  | ||||||
|  | # Go build stage | ||||||
|  | FROM golang:1.20.5 AS goBuilder | ||||||
|  | ENV GO111MODULE=on \ | ||||||
|  |     CGO_ENABLED=1 \ | ||||||
|  |     GOOS=linux | ||||||
|  | WORKDIR /build | ||||||
|  | COPY go.mod . | ||||||
|  | COPY go.sum . | ||||||
|  | RUN go mod download | ||||||
|  | COPY --from=translator /app . | ||||||
|  | COPY --from=nodeBuilder /build/web/build ./web/build | ||||||
|  | RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api | ||||||
|  |  | ||||||
|  | # Final stage | ||||||
|  | FROM alpine:latest | ||||||
|  | RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true | ||||||
|  | WORKDIR /data | ||||||
|  | COPY --from=goBuilder /build/one-api / | ||||||
|  | EXPOSE 3000 | ||||||
|  | ENTRYPOINT ["/one-api"] | ||||||
							
								
								
									
										34
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								go.mod
									
									
									
									
									
								
							| @@ -9,28 +9,28 @@ 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/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.4 | ||||||
| 	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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect | 	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect | ||||||
| 	github.com/bytedance/sonic v1.9.1 // indirect | 	github.com/bytedance/sonic v1.9.2 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.1.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/gomodule/redigo v2.0.0+incompatible // indirect | 	github.com/gomodule/redigo v2.0.0+incompatible // indirect | ||||||
| 	github.com/gorilla/context v1.1.1 // indirect | 	github.com/gorilla/context v1.1.1 // indirect | ||||||
| @@ -38,20 +38,22 @@ require ( | |||||||
| 	github.com/gorilla/sessions v1.2.1 // indirect | 	github.com/gorilla/sessions v1.2.1 // indirect | ||||||
| 	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/joho/godotenv v1.5.1 | ||||||
| 	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.8 // indirect | ||||||
|  | 	github.com/realTristan/disgoauth v1.0.2 | ||||||
| 	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 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								go.sum
									
									
									
									
									
								
							| @@ -3,8 +3,12 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P | |||||||
| 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.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | ||||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||||
|  | github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM= | ||||||
|  | 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.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | ||||||
| github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
|  | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= | ||||||
|  | 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= | ||||||
| @@ -16,6 +20,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r | |||||||
| 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.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | ||||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||||
|  | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= | ||||||
|  | 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= | ||||||
| @@ -47,15 +53,23 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO | |||||||
| 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.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= | ||||||
| github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
|  | github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= | ||||||
|  | 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.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||||
|  | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||||
|  | 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= | ||||||
|  | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
| github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | ||||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||||
|  | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= | ||||||
|  | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= | ||||||
| github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= | ||||||
| github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= | ||||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||||
| @@ -75,12 +89,16 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr | |||||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||||
|  | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||||
| 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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||||
|  | 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= | ||||||
| @@ -115,8 +133,14 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc | |||||||
| 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.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= | github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= | ||||||
|  | github.com/pkoukk/tiktoken-go v0.1.4 h1:bniMzWdUvNO6YkRbASo2x5qJf2LAG/TIJojqz+Igm8E= | ||||||
|  | github.com/pkoukk/tiktoken-go v0.1.4/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/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3 h1:x3LgcvujjG+mx8PUMfPmwn3tcu2aA95uCB6ilGGObWk= | ||||||
|  | github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3/go.mod h1:P/mZMYLZ87lqRSECEWsOqywGrO1hlZkk9RTwEw35IP4= | ||||||
|  | github.com/realTristan/disgoauth v1.0.2 h1:dfto2Kf1gFlZsf8XuwRNoemLgk+hGn/TJpSdtMrEh8E= | ||||||
|  | github.com/realTristan/disgoauth v1.0.2/go.mod h1:t72aRaWMq2gknUZcKONReJlEYFod5sHC86WCJ0X9GxA= | ||||||
| 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= | ||||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||||||
| @@ -144,34 +168,56 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ | |||||||
| 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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | ||||||
| golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
|  | golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= | ||||||
|  | golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
|  | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| 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.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= | ||||||
| golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | ||||||
|  | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= | ||||||
|  | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= | ||||||
|  | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | ||||||
| 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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= | ||||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||||
|  | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= | ||||||
|  | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= | ||||||
|  | golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= | ||||||
|  | golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= | ||||||
|  | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| 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.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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= | ||||||
| golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= | ||||||
|  | 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| 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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= | ||||||
| golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||||
|  | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= | ||||||
|  | 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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= | ||||||
|  | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= | ||||||
| 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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||||||
| 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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= | ||||||
| google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
|  | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= | ||||||
|  | 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= | ||||||
| @@ -188,9 +234,16 @@ 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.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= | ||||||
| gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= | gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= | ||||||
|  | gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= | ||||||
|  | gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= | ||||||
| gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= | gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= | ||||||
| gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= | gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= | ||||||
|  | gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= | ||||||
|  | gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= | ||||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | ||||||
| gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= | gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= | ||||||
| gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= | 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= | ||||||
|   | |||||||
							
								
								
									
										67
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								i18n/en.json
									
									
									
									
									
								
							| @@ -107,6 +107,11 @@ | |||||||
|   "已禁用": "Disabled", |   "已禁用": "Disabled", | ||||||
|   "未知状态": "Unknown status", |   "未知状态": "Unknown status", | ||||||
|   " 秒": "s", |   " 秒": "s", | ||||||
|  |   " 分钟 ": " m ", | ||||||
|  |   " 小时 ": " h ", | ||||||
|  |   " 天 ": " d ", | ||||||
|  |   " 个月 ": " M ", | ||||||
|  |   " 年 ": " y ", | ||||||
|   "未测试": "Not tested", |   "未测试": "Not tested", | ||||||
|   "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.", |   "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.", | ||||||
|   "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.", |   "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.", | ||||||
| @@ -438,6 +443,7 @@ | |||||||
|   "显示名称": "Display Name", |   "显示名称": "Display Name", | ||||||
|   "请输入新的显示名称": "Please enter a new display name", |   "请输入新的显示名称": "Please enter a new display name", | ||||||
|   "已绑定的 GitHub 账户": "GitHub Account Bound", |   "已绑定的 GitHub 账户": "GitHub Account Bound", | ||||||
|  |   "已绑定的 Discord 账户": "Discord Account Bound", | ||||||
|   "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly", |   "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly", | ||||||
|   "已绑定的微信账户": "WeChat Account Bound", |   "已绑定的微信账户": "WeChat Account Bound", | ||||||
|   "已绑定的邮箱账户": "Email Account Bound", |   "已绑定的邮箱账户": "Email Account Bound", | ||||||
| @@ -458,5 +464,64 @@ | |||||||
|   "消耗额度": "Used Quota", |   "消耗额度": "Used Quota", | ||||||
|   "可选值": "Optional Values", |   "可选值": "Optional Values", | ||||||
|   "渠道不存在:%d": "Channel does not exist: %d", |   "渠道不存在:%d": "Channel does not exist: %d", | ||||||
|   "数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator" |   "数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator", | ||||||
|  |   "使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load", | ||||||
|  |   "请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!", | ||||||
|  |   "请至少选择一个Model!": "Please select at least one Model!", | ||||||
|  |   "加载首页内容失败": "Failed to load the homepage content", | ||||||
|  |   "加载关于内容失败": "Failed to load the About content", | ||||||
|  |   "兑换码更新成功!": "Redemption code updated successfully!", | ||||||
|  |   "兑换码创建成功!": "Redemption code created successfully!", | ||||||
|  |   "用户账户创建成功!": "User account created successfully!", | ||||||
|  |   "生成数量": "Generate quantity", | ||||||
|  |   "请输入生成数量": "Please enter the quantity to generate", | ||||||
|  |   "创建新用户账户": "Create new user account", | ||||||
|  |   "渠道更新成功!": "Channel updated successfully!", | ||||||
|  |   "渠道创建成功!": "Channel created successfully!", | ||||||
|  |   "请选择分组": "Please select a group", | ||||||
|  |   "更新兑换码信息": "Update redemption code information", | ||||||
|  |   "创建新的兑换码": "Create a new redemption code", | ||||||
|  |   "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group ratio in the system settings page to add a new group:", | ||||||
|  |   "未找到所请求的页面": "The requested page was not found", | ||||||
|  |   "过期时间格式错误!": "Expiration time format error!", | ||||||
|  |   "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit", | ||||||
|  |   "此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:", | ||||||
|  |   "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:", | ||||||
|  |   "模型映射": "Model mapping", | ||||||
|  |   "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", | ||||||
|  |   "默认": "Default", | ||||||
|  |   "图片演示": "Image demo", | ||||||
|  |   "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)", | ||||||
|  |   "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", | ||||||
|  |   "取消无限额度": "Cancel unlimited quota", | ||||||
|  |   "请输入新的剩余额度": "Please enter the new remaining quota", | ||||||
|  |   "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", | ||||||
|  |   "请输入用户名": "Please enter username", | ||||||
|  |   "请输入显示名称": "Please enter display name", | ||||||
|  |   "请输入密码": "Please enter password", | ||||||
|  |   "模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name", | ||||||
|  |   ",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body", | ||||||
|  |   "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", | ||||||
|  |   "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", | ||||||
|  |   "Homepage URL 填": "Fill in the Homepage URL", | ||||||
|  |   "Authorization callback URL 填": "Fill in the Authorization callback URL", | ||||||
|  |   "允许通过 Discord 账户登录和注册": "Allow login and registration via Discord account", | ||||||
|  |   "Discord 身份验证": "Discord Authentication", | ||||||
|  |   "确认文字": "Confirmation Text", | ||||||
|  |   "请输入 \"CONFIRM\" 以删除您的帐户。": "Please enter \"CONFIRM\" to delete your account.", | ||||||
|  |   "请确认您要删除账户!": "Please confirm that you want to delete the account!", | ||||||
|  |   "账户已删除!": "Account deleted!", | ||||||
|  |   "您是否确认删除自己的帐户?": "Are you sure you want to delete your account?", | ||||||
|  |   "配置 Discord OAuth App": "Configure Discord OAuth App", | ||||||
|  |   "管理你的 Discord OAuth App": "Manage your Discord OAuth App", | ||||||
|  |   "输入你注册的 Discord OAuth APP 的 ID": "Enter the ID of your registered Discord OAuth APP", | ||||||
|  |   "保存 Discord OAuth 设置": "Save Discord OAuth Settings", | ||||||
|  |   "删除个人账户": "Delete personal account", | ||||||
|  |   "绑定 Discord 账号": "Bind Discord account", | ||||||
|  |   "无权将其他用户权限等级提升到大于等于自己的权限等级": "You are not allowed to upgrade the permission level of other users to greater than or equal to your own permission level", | ||||||
|  |   "无权删除超级管理员": "You are not allowed to delete super administrators", | ||||||
|  |   "该 Discord 账户已被绑定": "The Discord account has been bound", | ||||||
|  |   "管理员未开启通过 Discord 登录以及注册": "The administrator has not enabled login and registration via Discord", | ||||||
|  |   "无法启用 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!", | ||||||
|  |   "兑换失败,": "Redemption failed, " | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								main.go
									
									
									
									
									
								
							| @@ -2,9 +2,6 @@ package main | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"embed" | 	"embed" | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-contrib/sessions/cookie" |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/controller" | 	"one-api/controller" | ||||||
| 	"one-api/middleware" | 	"one-api/middleware" | ||||||
| @@ -12,6 +9,11 @@ import ( | |||||||
| 	"one-api/router" | 	"one-api/router" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-contrib/sessions/cookie" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/joho/godotenv" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //go:embed web/build | //go:embed web/build | ||||||
| @@ -21,6 +23,8 @@ var buildFS embed.FS | |||||||
| var indexPage []byte | var indexPage []byte | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|  | 	godotenv.Load(".env") | ||||||
|  |  | ||||||
| 	common.SetupGinLog() | 	common.SetupGinLog() | ||||||
| 	common.SysLog("One API " + common.Version + " started") | 	common.SysLog("One API " + common.Version + " started") | ||||||
| 	if os.Getenv("GIN_MODE") != "debug" { | 	if os.Getenv("GIN_MODE") != "debug" { | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ package middleware | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"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" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ModelRequest struct { | type ModelRequest struct { | ||||||
| @@ -73,6 +74,11 @@ func Distribute() func(c *gin.Context) { | |||||||
| 					modelRequest.Model = "text-moderation-stable" | 					modelRequest.Model = "text-moderation-stable" | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 			if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { | ||||||
|  | 				if modelRequest.Model == "" { | ||||||
|  | 					modelRequest.Model = "dall-e" | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) | 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				message := "无可用渠道" | 				message := "无可用渠道" | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ 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["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) | ||||||
| @@ -53,6 +54,8 @@ 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"] = "" | ||||||
| @@ -132,6 +135,8 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 			common.PasswordLoginEnabled = boolValue | 			common.PasswordLoginEnabled = boolValue | ||||||
| 		case "EmailVerificationEnabled": | 		case "EmailVerificationEnabled": | ||||||
| 			common.EmailVerificationEnabled = boolValue | 			common.EmailVerificationEnabled = boolValue | ||||||
|  | 		case "DiscordOAuthEnabled": | ||||||
|  | 			common.DiscordOAuthEnabled = boolValue | ||||||
| 		case "GitHubOAuthEnabled": | 		case "GitHubOAuthEnabled": | ||||||
| 			common.GitHubOAuthEnabled = boolValue | 			common.GitHubOAuthEnabled = boolValue | ||||||
| 		case "WeChatAuthEnabled": | 		case "WeChatAuthEnabled": | ||||||
| @@ -170,6 +175,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": | ||||||
|   | |||||||
| @@ -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,6 +20,7 @@ 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"` | ||||||
| 	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 | ||||||
| @@ -169,6 +171,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 为空!") | ||||||
| @@ -197,6 +207,10 @@ 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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func IsDiscordIdAlreadyTaken(discordId string) bool { | ||||||
|  | 	return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1 | ||||||
|  | } | ||||||
|  |  | ||||||
| func IsUsernameAlreadyTaken(username string) bool { | func IsUsernameAlreadyTaken(username string) bool { | ||||||
| 	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 | 	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| package router | package router | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-contrib/gzip" |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"one-api/controller" | 	"one-api/controller" | ||||||
| 	"one-api/middleware" | 	"one-api/middleware" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/gzip" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func SetApiRouter(router *gin.Engine) { | func SetApiRouter(router *gin.Engine) { | ||||||
| @@ -20,6 +21,7 @@ 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/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) | ||||||
| @@ -27,7 +29,7 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 		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("/") | ||||||
| @@ -35,7 +37,7 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 			{ | 			{ | ||||||
| 				selfRoute.GET("/self", controller.GetSelf) | 				selfRoute.GET("/self", controller.GetSelf) | ||||||
| 				selfRoute.PUT("/self", controller.UpdateSelf) | 				selfRoute.PUT("/self", controller.UpdateSelf) | ||||||
| 				selfRoute.DELETE("/self", controller.DeleteSelf) | 				selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf) | ||||||
| 				selfRoute.GET("/token", controller.GenerateAccessToken) | 				selfRoute.GET("/token", controller.GenerateAccessToken) | ||||||
| 				selfRoute.GET("/aff", controller.GetAffCode) | 				selfRoute.GET("/aff", controller.GetAffCode) | ||||||
| 				selfRoute.POST("/topup", controller.TopUp) | 				selfRoute.POST("/topup", controller.TopUp) | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| package router | package router | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"one-api/controller" | 	"one-api/controller" | ||||||
| 	"one-api/middleware" | 	"one-api/middleware" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func SetRelayRouter(router *gin.Engine) { | func SetRelayRouter(router *gin.Engine) { | ||||||
| @@ -20,7 +21,7 @@ func SetRelayRouter(router *gin.Engine) { | |||||||
| 		relayV1Router.POST("/completions", controller.Relay) | 		relayV1Router.POST("/completions", controller.Relay) | ||||||
| 		relayV1Router.POST("/chat/completions", controller.Relay) | 		relayV1Router.POST("/chat/completions", controller.Relay) | ||||||
| 		relayV1Router.POST("/edits", controller.Relay) | 		relayV1Router.POST("/edits", controller.Relay) | ||||||
| 		relayV1Router.POST("/images/generations", controller.RelayNotImplemented) | 		relayV1Router.POST("/images/generations", controller.Relay) | ||||||
| 		relayV1Router.POST("/images/edits", controller.RelayNotImplemented) | 		relayV1Router.POST("/images/edits", controller.RelayNotImplemented) | ||||||
| 		relayV1Router.POST("/images/variations", controller.RelayNotImplemented) | 		relayV1Router.POST("/images/variations", controller.RelayNotImplemented) | ||||||
| 		relayV1Router.POST("/embeddings", controller.Relay) | 		relayV1Router.POST("/embeddings", controller.Relay) | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -22,5 +22,4 @@ npm-debug.log* | |||||||
| yarn-debug.log* | yarn-debug.log* | ||||||
| yarn-error.log* | yarn-error.log* | ||||||
| .idea | .idea | ||||||
| package-lock.json | yarn.lock | ||||||
| yarn.lock |  | ||||||
|   | |||||||
							
								
								
									
										18300
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										18300
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,18 +3,17 @@ | |||||||
|   "version": "0.1.0", |   "version": "0.1.0", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "axios": "^0.27.2", |     "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.1", | ||||||
|     "react-scripts": "5.0.1", |     "react-toastify": "^9.1.3", | ||||||
|     "react-toastify": "^9.0.8", |     "react-turnstile": "^1.1.1", | ||||||
|     "react-turnstile": "^1.0.5", |  | ||||||
|     "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 +40,9 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "prettier": "^2.7.1" |     "@babel/plugin-proposal-private-property-in-object": "^7.21.11", | ||||||
|  |     "prettier": "^2.7.1", | ||||||
|  |     "react-scripts": "^5.0.1" | ||||||
|   }, |   }, | ||||||
|   "prettier": { |   "prettier": { | ||||||
|     "singleQuote": true, |     "singleQuote": true, | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ 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 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'; | ||||||
| @@ -55,15 +56,6 @@ function App() { | |||||||
|       } else { |       } else { | ||||||
|         localStorage.removeItem('chat_link'); |         localStorage.removeItem('chat_link'); | ||||||
|       } |       } | ||||||
|       if ( |  | ||||||
|         data.version !== process.env.REACT_APP_VERSION && |  | ||||||
|         data.version !== 'v0.0.0' && |  | ||||||
|         process.env.REACT_APP_VERSION !== '' |  | ||||||
|       ) { |  | ||||||
|         showNotice( |  | ||||||
|           `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面` |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } else { |     } else { | ||||||
|       showError('无法正常连接至服务器!'); |       showError('无法正常连接至服务器!'); | ||||||
|     } |     } | ||||||
| @@ -239,6 +231,14 @@ function App() { | |||||||
|           </Suspense> |           </Suspense> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|  |       <Route | ||||||
|  |         path='/oauth/discord' | ||||||
|  |         element={ | ||||||
|  |           <Suspense fallback={<Loading></Loading>}> | ||||||
|  |             <DiscordOAuth /> | ||||||
|  |           </Suspense> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|       <Route |       <Route | ||||||
|         path='/setting' |         path='/setting' | ||||||
|         element={ |         element={ | ||||||
| @@ -252,11 +252,11 @@ function App() { | |||||||
|       <Route |       <Route | ||||||
|         path='/topup' |         path='/topup' | ||||||
|         element={ |         element={ | ||||||
|         <PrivateRoute> |           <PrivateRoute> | ||||||
|           <Suspense fallback={<Loading></Loading>}> |             <Suspense fallback={<Loading></Loading>}> | ||||||
|             <TopUp /> |               <TopUp /> | ||||||
|           </Suspense> |             </Suspense> | ||||||
|         </PrivateRoute> |           </PrivateRoute> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|       <Route |       <Route | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								web/src/components/DiscordOAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/src/components/DiscordOAuth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import React, { useContext, useEffect, useState } from 'react'; | ||||||
|  | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; | ||||||
|  | import { useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
|  | import { API, showError, showSuccess } from '../helpers'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
|  | const DiscordOAuth = () => { | ||||||
|  |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |  | ||||||
|  |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [prompt, setPrompt] = useState('处理中...'); | ||||||
|  |   const [processing, setProcessing] = useState(true); | ||||||
|  |  | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const sendCode = async (code, count) => { | ||||||
|  |     const res = await API.get(`/api/oauth/discord?code=${code}`); | ||||||
|  |     const { success, message, data } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (message === 'bind') { | ||||||
|  |         showSuccess('绑定成功!'); | ||||||
|  |         navigate('/setting'); | ||||||
|  |       } else { | ||||||
|  |         userDispatch({ type: 'login', payload: data }); | ||||||
|  |         localStorage.setItem('user', JSON.stringify(data)); | ||||||
|  |         showSuccess('登录成功!'); | ||||||
|  |         navigate('/'); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |       if (count === 0) { | ||||||
|  |         setPrompt(`操作失败,重定向至登录界面中...`); | ||||||
|  |         navigate('/setting'); // in case this is failed to bind GitHub | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       count++; | ||||||
|  |       setPrompt(`出现错误,第 ${count} 次重试中...`); | ||||||
|  |       await new Promise((resolve) => setTimeout(resolve, count * 2000)); | ||||||
|  |       await sendCode(code, count); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let code = searchParams.get('code'); | ||||||
|  |     sendCode(code, 0).then(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Segment style={{ minHeight: '300px' }}> | ||||||
|  |       <Dimmer active inverted> | ||||||
|  |         <Loader size='large'>{prompt}</Loader> | ||||||
|  |       </Dimmer> | ||||||
|  |     </Segment> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default DiscordOAuth; | ||||||
| @@ -12,7 +12,8 @@ import { | |||||||
| } from 'semantic-ui-react'; | } 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, showSuccess, showInfo } from '../helpers'; | ||||||
|  | import Turnstile from 'react-turnstile'; | ||||||
|  |  | ||||||
| const LoginForm = () => { | const LoginForm = () => { | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
| @@ -24,6 +25,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({}); | ||||||
| @@ -37,6 +41,11 @@ 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); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
| @@ -48,6 +57,12 @@ const LoginForm = () => { | |||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   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); | ||||||
|   }; |   }; | ||||||
| @@ -76,7 +91,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, | ||||||
|       }); |       }); | ||||||
| @@ -119,6 +139,16 @@ const LoginForm = () => { | |||||||
|               value={password} |               value={password} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|  |             {turnstileEnabled ? ( | ||||||
|  |               <Turnstile | ||||||
|  |                 sitekey={turnstileSiteKey} | ||||||
|  |                 onVerify={(token) => { | ||||||
|  |                   setTurnstileToken(token); | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             ) : ( | ||||||
|  |               <></> | ||||||
|  |             )} | ||||||
|             <Button color="" fluid size="large" onClick={handleSubmit}> |             <Button color="" fluid size="large" onClick={handleSubmit}> | ||||||
|               登录 |               登录 | ||||||
|             </Button> |             </Button> | ||||||
| @@ -134,28 +164,32 @@ const LoginForm = () => { | |||||||
|             点击注册 |             点击注册 | ||||||
|           </Link> |           </Link> | ||||||
|         </Message> |         </Message> | ||||||
|         {status.github_oauth || status.wechat_login ? ( |         {status.github_oauth || status.wechat_login || status.discord_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} | ||||||
|               /> |               /> | ||||||
|             ) : ( |  | ||||||
|               <></> |  | ||||||
|             )} |             )} | ||||||
|           </> |           </> | ||||||
|         ) : ( |         ) : ( | ||||||
|   | |||||||
| @@ -1,18 +1,24 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState, useContext } from 'react'; | ||||||
| import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; | import { Button, Divider, Form, Header, Image, Message, Modal, Label } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link, useNavigate } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | ||||||
| import Turnstile from 'react-turnstile'; | import Turnstile from 'react-turnstile'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
| const PersonalSetting = () => { | const PersonalSetting = () => { | ||||||
|  |   const [userDispatch] = useContext(UserContext); | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
|     wechat_verification_code: '', |     wechat_verification_code: '', | ||||||
|     email_verification_code: '', |     email_verification_code: '', | ||||||
|     email: '', |     email: '', | ||||||
|  |     self_account_deletion_confirmation: '', | ||||||
|   }); |   }); | ||||||
|   const [status, setStatus] = useState({}); |   const [status, setStatus] = useState({}); | ||||||
|   const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); |   const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); | ||||||
|   const [showEmailBindModal, setShowEmailBindModal] = useState(false); |   const [showEmailBindModal, setShowEmailBindModal] = useState(false); | ||||||
|  |   const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); | ||||||
|   const [turnstileEnabled, setTurnstileEnabled] = useState(false); |   const [turnstileEnabled, setTurnstileEnabled] = useState(false); | ||||||
|   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); |   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||||
|   const [turnstileToken, setTurnstileToken] = useState(''); |   const [turnstileToken, setTurnstileToken] = useState(''); | ||||||
| @@ -57,6 +63,26 @@ const PersonalSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const deleteAccount = async () => { | ||||||
|  |     if (inputs.self_account_deletion_confirmation !== 'CONFIRM') { | ||||||
|  |       showError('请确认您要删除账户!'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const res = await API.delete('/api/user/self'); | ||||||
|  |     const { success, message } = res.data; | ||||||
|  |  | ||||||
|  |     if (success) { | ||||||
|  |       showSuccess('账户已删除!'); | ||||||
|  |       await API.get('/api/user/logout'); | ||||||
|  |       userDispatch({ type: 'logout' }); | ||||||
|  |       localStorage.removeItem('user'); | ||||||
|  |       navigate('/login'); | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const bindWeChat = async () => { |   const bindWeChat = async () => { | ||||||
|     if (inputs.wechat_verification_code === '') return; |     if (inputs.wechat_verification_code === '') return; | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
| @@ -77,6 +103,12 @@ const PersonalSetting = () => { | |||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const openDiscordOAuth = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const sendVerificationCode = async () => { |   const sendVerificationCode = async () => { | ||||||
|     if (inputs.email === '') return; |     if (inputs.email === '') return; | ||||||
|     if (turnstileEnabled && turnstileToken === '') { |     if (turnstileEnabled && turnstileToken === '') { | ||||||
| @@ -123,6 +155,9 @@ const PersonalSetting = () => { | |||||||
|       </Button> |       </Button> | ||||||
|       <Button onClick={generateAccessToken}>生成系统访问令牌</Button> |       <Button onClick={generateAccessToken}>生成系统访问令牌</Button> | ||||||
|       <Button onClick={getAffLink}>复制邀请链接</Button> |       <Button onClick={getAffLink}>复制邀请链接</Button> | ||||||
|  |       <Button onClick={() => { | ||||||
|  |         setShowAccountDeleteModal(true); | ||||||
|  |       }} color='red'>删除个人账户</Button> | ||||||
|       <Divider /> |       <Divider /> | ||||||
|       <Header as='h3'>账号绑定</Header> |       <Header as='h3'>账号绑定</Header> | ||||||
|       { |       { | ||||||
| @@ -170,6 +205,11 @@ const PersonalSetting = () => { | |||||||
|           <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> |           <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> | ||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
|  |       { | ||||||
|  |         status.discord_oauth && ( | ||||||
|  |           <Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button> | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|       <Button |       <Button | ||||||
|         onClick={() => { |         onClick={() => { | ||||||
|           setShowEmailBindModal(true); |           setShowEmailBindModal(true); | ||||||
| @@ -230,6 +270,48 @@ const PersonalSetting = () => { | |||||||
|           </Modal.Description> |           </Modal.Description> | ||||||
|         </Modal.Content> |         </Modal.Content> | ||||||
|       </Modal> |       </Modal> | ||||||
|  |       <Modal | ||||||
|  |         onClose={() => setShowAccountDeleteModal(false)} | ||||||
|  |         onOpen={() => setShowAccountDeleteModal(true)} | ||||||
|  |         open={showAccountDeleteModal} | ||||||
|  |         size={'tiny'} | ||||||
|  |         style={{ maxWidth: '450px' }} | ||||||
|  |       > | ||||||
|  |         <Modal.Header>您是否确认删除自己的帐户?</Modal.Header> | ||||||
|  |         <Modal.Content> | ||||||
|  |           <Modal.Description> | ||||||
|  |             <Form size='large'> | ||||||
|  |               <Form.Input | ||||||
|  |                 fluid | ||||||
|  |                 label='请输入 "CONFIRM" 以删除您的帐户。' | ||||||
|  |                 placeholder='确认文字' | ||||||
|  |                 name='self_account_deletion_confirmation' | ||||||
|  |                 value={inputs.self_account_deletion_confirmation} | ||||||
|  |                 onChange={handleInputChange} | ||||||
|  |               /> | ||||||
|  |               {turnstileEnabled ? ( | ||||||
|  |                 <Turnstile | ||||||
|  |                   sitekey={turnstileSiteKey} | ||||||
|  |                   onVerify={(token) => { | ||||||
|  |                     setTurnstileToken(token); | ||||||
|  |                   }} | ||||||
|  |                 /> | ||||||
|  |               ) : ( | ||||||
|  |                 <></> | ||||||
|  |               )} | ||||||
|  |               <Button | ||||||
|  |                 color='red' | ||||||
|  |                 fluid | ||||||
|  |                 size='large' | ||||||
|  |                 onClick={deleteAccount} | ||||||
|  |                 loading={loading} | ||||||
|  |               > | ||||||
|  |                 删除 | ||||||
|  |               </Button> | ||||||
|  |             </Form> | ||||||
|  |           </Modal.Description> | ||||||
|  |         </Modal.Content> | ||||||
|  |       </Modal> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -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: '', | ||||||
| @@ -56,6 +59,7 @@ const SystemSetting = () => { | |||||||
|       case 'PasswordRegisterEnabled': |       case 'PasswordRegisterEnabled': | ||||||
|       case 'EmailVerificationEnabled': |       case 'EmailVerificationEnabled': | ||||||
|       case 'GitHubOAuthEnabled': |       case 'GitHubOAuthEnabled': | ||||||
|  |       case 'DiscordOAuthEnabled': | ||||||
|       case 'WeChatAuthEnabled': |       case 'WeChatAuthEnabled': | ||||||
|       case 'TurnstileCheckEnabled': |       case 'TurnstileCheckEnabled': | ||||||
|       case 'RegisterEnabled': |       case 'RegisterEnabled': | ||||||
| @@ -82,6 +86,8 @@ const SystemSetting = () => { | |||||||
|       name === 'Notice' || |       name === 'Notice' || | ||||||
|       name.startsWith('SMTP') || |       name.startsWith('SMTP') || | ||||||
|       name === 'ServerAddress' || |       name === 'ServerAddress' || | ||||||
|  |       name === 'DiscordClientId' || | ||||||
|  |       name === 'DiscordClientSecret' || | ||||||
|       name === 'GitHubClientId' || |       name === 'GitHubClientId' || | ||||||
|       name === 'GitHubClientSecret' || |       name === 'GitHubClientSecret' || | ||||||
|       name === 'WeChatServerAddress' || |       name === 'WeChatServerAddress' || | ||||||
| @@ -161,6 +167,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); | ||||||
| @@ -177,49 +195,55 @@ const SystemSetting = () => { | |||||||
|     <Grid columns={1}> |     <Grid columns={1}> | ||||||
|       <Grid.Column> |       <Grid.Column> | ||||||
|         <Form loading={loading}> |         <Form loading={loading}> | ||||||
|           <Header as='h3'>通用设置</Header> |           <Header as='h3'>General Settings</Header> | ||||||
|           <Form.Group widths='equal'> |           <Form.Group widths='equal'> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='服务器地址' |               label='Server Address' | ||||||
|               placeholder='例如:https://yourdomain.com' |               placeholder='For example:https://yourdomain.com' | ||||||
|               value={inputs.ServerAddress} |               value={inputs.ServerAddress} | ||||||
|               name='ServerAddress' |               name='ServerAddress' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitServerAddress}> |           <Form.Button onClick={submitServerAddress}> | ||||||
|             更新服务器地址 |             Update Server Address | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'>配置登录注册</Header> |           <Header as='h3'>Configure Login/Registration</Header> | ||||||
|           <Form.Group inline> |           <Form.Group inline> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.PasswordLoginEnabled === 'true'} |               checked={inputs.PasswordLoginEnabled === 'true'} | ||||||
|               label='允许通过密码进行登录' |               label='Allow login via password' | ||||||
|               name='PasswordLoginEnabled' |               name='PasswordLoginEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.PasswordRegisterEnabled === 'true'} |               checked={inputs.PasswordRegisterEnabled === 'true'} | ||||||
|               label='允许通过密码进行注册' |               label='Allow registration via password' | ||||||
|               name='PasswordRegisterEnabled' |               name='PasswordRegisterEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.EmailVerificationEnabled === 'true'} |               checked={inputs.EmailVerificationEnabled === 'true'} | ||||||
|               label='通过密码注册时需要进行邮箱验证' |               label='Email verification is required when registering via password' | ||||||
|               name='EmailVerificationEnabled' |               name='EmailVerificationEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.GitHubOAuthEnabled === 'true'} |               checked={inputs.GitHubOAuthEnabled === 'true'} | ||||||
|               label='允许通过 GitHub 账户登录 & 注册' |               label='Allow login & registration via GitHub account' | ||||||
|               name='GitHubOAuthEnabled' |               name='GitHubOAuthEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               checked={inputs.DiscordOAuthEnabled === 'true'} | ||||||
|  |               label='允许通过 Discord 账户登录和注册' | ||||||
|  |               name='DiscordOAuthEnabled' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.WeChatAuthEnabled === 'true'} |               checked={inputs.WeChatAuthEnabled === 'true'} | ||||||
|               label='允许通过微信登录 & 注册' |               label='Allow login & registration via WeChat' | ||||||
|               name='WeChatAuthEnabled' |               name='WeChatAuthEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
| @@ -227,82 +251,120 @@ const SystemSetting = () => { | |||||||
|           <Form.Group inline> |           <Form.Group inline> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.RegisterEnabled === 'true'} |               checked={inputs.RegisterEnabled === 'true'} | ||||||
|               label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)' |               label='Allow new user registration (if this option is off, new users will not be able to register in any way)' | ||||||
|               name='RegisterEnabled' |               name='RegisterEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Checkbox |             <Form.Checkbox | ||||||
|               checked={inputs.TurnstileCheckEnabled === 'true'} |               checked={inputs.TurnstileCheckEnabled === 'true'} | ||||||
|               label='启用 Turnstile 用户校验' |               label='Enable Turnstile user verification' | ||||||
|               name='TurnstileCheckEnabled' |               name='TurnstileCheckEnabled' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 SMTP |             Configure SMTP | ||||||
|             <Header.Subheader>用以支持系统的邮件发送</Header.Subheader> |             <Header.Subheader>To support the system email sending</Header.Subheader> | ||||||
|           </Header> |           </Header> | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='SMTP 服务器地址' |               label='SMTP Server Address' | ||||||
|               name='SMTPServer' |               name='SMTPServer' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPServer} |               value={inputs.SMTPServer} | ||||||
|               placeholder='例如:smtp.qq.com' |               placeholder='For example: smtp.qq.com' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='SMTP 端口' |               label='SMTP Port' | ||||||
|               name='SMTPPort' |               name='SMTPPort' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPPort} |               value={inputs.SMTPPort} | ||||||
|               placeholder='默认: 587' |               placeholder='Default: 587' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='SMTP 账户' |               label='SMTP Account' | ||||||
|               name='SMTPAccount' |               name='SMTPAccount' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPAccount} |               value={inputs.SMTPAccount} | ||||||
|               placeholder='通常是邮箱地址' |               placeholder='Usually an email address' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='SMTP 发送者邮箱' |               label='SMTP Sender email' | ||||||
|               name='SMTPFrom' |               name='SMTPFrom' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPFrom} |               value={inputs.SMTPFrom} | ||||||
|               placeholder='通常和邮箱地址保持一致' |               placeholder='Usually consistent with the email address' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='SMTP 访问凭证' |               label='SMTP Access Credential' | ||||||
|               name='SMTPToken' |               name='SMTPToken' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               type='password' |               type='password' | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPToken} |               value={inputs.SMTPToken} | ||||||
|               placeholder='敏感信息不会发送到前端显示' |               placeholder='Sensitive information will not be displayed in the frontend' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button> |           <Form.Button onClick={submitSMTP}>Save SMTP Settings</Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 GitHub OAuth App |             Configure Discord OAuth App | ||||||
|             <Header.Subheader> |             <Header.Subheader> | ||||||
|               用以支持通过 GitHub 进行登录注册, |               To support login & registration via GitHub, | ||||||
|               <a href='https://github.com/settings/developers' target='_blank'> |               <a href='https://discord.com/developers/applications' target='_blank'> | ||||||
|                 点击此处 |                 Click here | ||||||
|               </a> |               </a> | ||||||
|               管理你的 GitHub OAuth App |               Manage your Discord OAuth App | ||||||
|             </Header.Subheader> |             </Header.Subheader> | ||||||
|           </Header> |           </Header> | ||||||
|           <Message> |           <Message> | ||||||
|             Homepage URL 填 <code>{inputs.ServerAddress}</code> |             Fill in the Homepage URL <code>{inputs.ServerAddress}</code> | ||||||
|             ,Authorization callback URL 填{' '} |             ,Fill in the Authorization callback URL{' '} | ||||||
|  |             <code>{`${inputs.ServerAddress}/oauth/discord`}</code> | ||||||
|  |           </Message> | ||||||
|  |           <Form.Group widths={3}> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Discord Client ID' | ||||||
|  |               name='DiscordClientId' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.DiscordClientId} | ||||||
|  |               placeholder='Enter the ID of your registered Discord OAuth APP' | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               label='Discord Client Secret' | ||||||
|  |               name='DiscordClientSecret' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               type='password' | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.DiscordClientSecret} | ||||||
|  |               placeholder='Sensitive information will not be displayed in the frontend' | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Button onClick={submitDiscordOAuth}> | ||||||
|  |             Save Discord OAuth Settings | ||||||
|  |           </Form.Button> | ||||||
|  |           <Divider /> | ||||||
|  |           <Header as='h3'> | ||||||
|  |             Configure GitHub OAuth App | ||||||
|  |             <Header.Subheader> | ||||||
|  |               To support login & registration via GitHub, | ||||||
|  |               <a href='https://github.com/settings/developers' target='_blank'> | ||||||
|  |                 Click here | ||||||
|  |               </a> | ||||||
|  |               Manage your GitHub OAuth App | ||||||
|  |             </Header.Subheader> | ||||||
|  |           </Header> | ||||||
|  |           <Message> | ||||||
|  |             Fill in the Homepage URL <code>{inputs.ServerAddress}</code> | ||||||
|  |             ,Fill in the Authorization callback URL{' '} | ||||||
|             <code>{`${inputs.ServerAddress}/oauth/github`}</code> |             <code>{`${inputs.ServerAddress}/oauth/github`}</code> | ||||||
|           </Message> |           </Message> | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
| @@ -312,7 +374,7 @@ const SystemSetting = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.GitHubClientId} |               value={inputs.GitHubClientId} | ||||||
|               placeholder='输入你注册的 GitHub OAuth APP 的 ID' |               placeholder='Enter your registered GitHub OAuth APP ID' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='GitHub Client Secret' |               label='GitHub Client Secret' | ||||||
| @@ -321,65 +383,65 @@ const SystemSetting = () => { | |||||||
|               type='password' |               type='password' | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.GitHubClientSecret} |               value={inputs.GitHubClientSecret} | ||||||
|               placeholder='敏感信息不会发送到前端显示' |               placeholder='Sensitive information will not be displayed in the frontend' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitGitHubOAuth}> |           <Form.Button onClick={submitGitHubOAuth}> | ||||||
|             保存 GitHub OAuth 设置 |             Save GitHub OAuth Settings | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 WeChat Server |             Configure WeChat Server | ||||||
|             <Header.Subheader> |             <Header.Subheader> | ||||||
|               用以支持通过微信进行登录注册, |               To support login & registration via WeChat, | ||||||
|               <a |               <a | ||||||
|                 href='https://github.com/songquanpeng/wechat-server' |                 href='https://github.com/songquanpeng/wechat-server' | ||||||
|                 target='_blank' |                 target='_blank' | ||||||
|               > |               > | ||||||
|                 点击此处 |                 Click here | ||||||
|               </a> |               </a> | ||||||
|               了解 WeChat Server |               Learn about WeChat Server | ||||||
|             </Header.Subheader> |             </Header.Subheader> | ||||||
|           </Header> |           </Header> | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='WeChat Server 服务器地址' |               label='WeChat Server Server Address' | ||||||
|               name='WeChatServerAddress' |               name='WeChatServerAddress' | ||||||
|               placeholder='例如:https://yourdomain.com' |               placeholder='For example:https://yourdomain.com' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.WeChatServerAddress} |               value={inputs.WeChatServerAddress} | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='WeChat Server 访问凭证' |               label='WeChat Server Access Credential' | ||||||
|               name='WeChatServerToken' |               name='WeChatServerToken' | ||||||
|               type='password' |               type='password' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.WeChatServerToken} |               value={inputs.WeChatServerToken} | ||||||
|               placeholder='敏感信息不会发送到前端显示' |               placeholder='Sensitive information will not be displayed in the frontend' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='微信公众号二维码图片链接' |               label='WeChat Public Account QR Code Image Link' | ||||||
|               name='WeChatAccountQRCodeImageURL' |               name='WeChatAccountQRCodeImageURL' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.WeChatAccountQRCodeImageURL} |               value={inputs.WeChatAccountQRCodeImageURL} | ||||||
|               placeholder='输入一个图片链接' |               placeholder='Enter an image link' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitWeChat}> |           <Form.Button onClick={submitWeChat}> | ||||||
|             保存 WeChat Server 设置 |             Save WeChat Server Settings | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 Turnstile |             Configure Turnstile | ||||||
|             <Header.Subheader> |             <Header.Subheader> | ||||||
|               用以支持用户校验, |               To support user verification, | ||||||
|               <a href='https://dash.cloudflare.com/' target='_blank'> |               <a href='https://dash.cloudflare.com/' target='_blank'> | ||||||
|                 点击此处 |                 Click here | ||||||
|               </a> |               </a> | ||||||
|               管理你的 Turnstile Sites,推荐选择 Invisible Widget Type |               Manage your Turnstile Sites, recommend selecting Invisible Widget Type | ||||||
|             </Header.Subheader> |             </Header.Subheader> | ||||||
|           </Header> |           </Header> | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
| @@ -389,7 +451,7 @@ const SystemSetting = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.TurnstileSiteKey} |               value={inputs.TurnstileSiteKey} | ||||||
|               placeholder='输入你注册的 Turnstile Site Key' |               placeholder='Enter your registered Turnstile Site Key' | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='Turnstile Secret Key' |               label='Turnstile Secret Key' | ||||||
| @@ -398,11 +460,11 @@ const SystemSetting = () => { | |||||||
|               type='password' |               type='password' | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.TurnstileSecretKey} |               value={inputs.TurnstileSecretKey} | ||||||
|               placeholder='敏感信息不会发送到前端显示' |               placeholder='Sensitive information will not be displayed in the frontend' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitTurnstile}> |           <Form.Button onClick={submitTurnstile}> | ||||||
|             保存 Turnstile 设置 |             Save Turnstile Settings | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Grid.Column> |       </Grid.Column> | ||||||
|   | |||||||
| @@ -97,6 +97,12 @@ const Home = () => { | |||||||
|                           ? '已启用' |                           ? '已启用' | ||||||
|                           : '未启用'} |                           : '未启用'} | ||||||
|                       </p> |                       </p> | ||||||
|  |                       <p> | ||||||
|  |                         Discord 身份验证: | ||||||
|  |                         {statusState?.status?.discord_oauth === true | ||||||
|  |                           ? '已启用' | ||||||
|  |                           : '未启用'} | ||||||
|  |                       </p> | ||||||
|                       <p> |                       <p> | ||||||
|                         微信身份验证: |                         微信身份验证: | ||||||
|                         {statusState?.status?.wechat_login === true |                         {statusState?.status?.wechat_login === true | ||||||
|   | |||||||
| @@ -13,13 +13,14 @@ const EditUser = () => { | |||||||
|     display_name: '', |     display_name: '', | ||||||
|     password: '', |     password: '', | ||||||
|     github_id: '', |     github_id: '', | ||||||
|  |     discord_id: '', | ||||||
|     wechat_id: '', |     wechat_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, discord_id } = | ||||||
|     inputs; |     inputs; | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
| @@ -156,6 +157,16 @@ 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.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='已绑定的微信账户' |               label='已绑定的微信账户' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user