mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-29 21:03:41 +08:00 
			
		
		
		
	Compare commits
	
		
			69 Commits
		
	
	
		
			v0.6.6-alp
			...
			0.5.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 4b9756b257 | ||
|  | a6ae20ed54 | ||
|  | 617149d731 | ||
|  | edd2c4f6e9 | ||
|  | 481c4ebf49 | ||
|  | 203471d7a9 | ||
|  | 7ad6f7d99d | ||
|  | 68abcd48ab | ||
|  | 0c175b4e44 | ||
|  | 4e31c3991d | ||
|  | 5b8a826cf9 | ||
|  | f5f21dffd8 | ||
|  | 4e94c85a9a | ||
|  | caabdd1e21 | ||
|  | 0424baef6a | ||
|  | 256d290507 | ||
|  | 8f0799d909 | ||
|  | 349e3a3661 | ||
|  | 8cc7f983e1 | ||
|  | 455643e317 | ||
|  | 1c7bad7b87 | ||
|  | 3141292026 | ||
|  | e4500bf8bf | ||
|  | 4043fccedb | ||
|  | 164df4e708 | ||
|  | d850f465cd | ||
|  | e2f5c1eb8c | ||
|  | d68aa4c96f | ||
|  | 47cb77de53 | ||
|  | 61912f5e2c | ||
|  | 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 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           cd web |           cd web | ||||||
|           npm install |           npm install | ||||||
|           REACT_APP_VERSION=$(git describe --tags) npm run build |           VITE_REACT_APP_VERSION=$(git describe --tags) npm run build | ||||||
|           cd .. |           cd .. | ||||||
|       - name: Set up Go |       - name: Set up Go | ||||||
|         uses: actions/setup-go@v3 |         uses: actions/setup-go@v3 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           cd web |           cd web | ||||||
|           npm install |           npm install | ||||||
|           REACT_APP_VERSION=$(git describe --tags) npm run build |           VITE_REACT_APP_VERSION=$(git describe --tags) npm run build | ||||||
|           cd .. |           cd .. | ||||||
|       - name: Set up Go |       - name: Set up Go | ||||||
|         uses: actions/setup-go@v3 |         uses: actions/setup-go@v3 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           cd web |           cd web | ||||||
|           npm install |           npm install | ||||||
|           REACT_APP_VERSION=$(git describe --tags) npm run build |           VITE_REACT_APP_VERSION=$(git describe --tags) npm run build | ||||||
|           cd .. |           cd .. | ||||||
|       - name: Set up Go |       - name: Set up Go | ||||||
|         uses: actions/setup-go@v3 |         uses: actions/setup-go@v3 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,4 +4,5 @@ upload | |||||||
| *.exe | *.exe | ||||||
| *.db | *.db | ||||||
| build | build | ||||||
| *.db-journal | *.db-journal | ||||||
|  | .env* | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								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 VITE_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"] | ||||||
|   | |||||||
| @@ -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 = "" | ||||||
| @@ -151,6 +155,10 @@ const ( | |||||||
| 	ChannelTypePaLM      = 11 | 	ChannelTypePaLM      = 11 | ||||||
| 	ChannelTypeAPI2GPT   = 12 | 	ChannelTypeAPI2GPT   = 12 | ||||||
| 	ChannelTypeAIGC2D    = 13 | 	ChannelTypeAIGC2D    = 13 | ||||||
|  |  | ||||||
|  | 	// Reserve engineering for public projects | ||||||
|  | 	ChannelTypeChatGPTWeb = 14 // Chanzhaoyu/chatgpt-web | ||||||
|  | 	ChannelTypeChatbotUI  = 15 // mckaywrigley/chatbot-ui | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ChannelBaseURLs = []string{ | var ChannelBaseURLs = []string{ | ||||||
| @@ -168,4 +176,8 @@ var ChannelBaseURLs = []string{ | |||||||
| 	"",                              // 11 | 	"",                              // 11 | ||||||
| 	"https://api.api2gpt.com",       // 12 | 	"https://api.api2gpt.com",       // 12 | ||||||
| 	"https://api.aigc2d.com",        // 13 | 	"https://api.aigc2d.com",        // 13 | ||||||
|  |  | ||||||
|  | 	// Reserve engineering for public projects | ||||||
|  | 	"", // 14 // Chanzhaoyu/chatgpt-web | ||||||
|  | 	"", // 15 // mckaywrigley/chatbot-ui | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								common/ip-gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								common/ip-gen.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | package common | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GenerateIP() string { | ||||||
|  | 	// Generate a random number between 20 and 240 | ||||||
|  | 	segment2 := rand.Intn(221) + 20 | ||||||
|  | 	segment3 := rand.Intn(256) | ||||||
|  | 	segment4 := rand.Intn(256) | ||||||
|  |  | ||||||
|  | 	ipAddress := fmt.Sprintf("104.%d.%d.%d", segment2, segment3, segment4) | ||||||
|  | 	return ipAddress | ||||||
|  | } | ||||||
| @@ -1,16 +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 remainQuota int | 	var remainQuota int | ||||||
| 	var usedQuota int | 	var usedQuota 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") | 		tokenId := c.GetInt("token_id") | ||||||
| 		token, err = model.GetTokenById(tokenId) | 		token, err = model.GetTokenById(tokenId) | ||||||
| @@ -45,6 +52,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,19 +1,30 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"log" | ||||||
| 	"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 formatFloat(input float64) float64 { | ||||||
|  | 	if input == float64(int64(input)) { | ||||||
|  | 		return input | ||||||
|  | 	} | ||||||
|  | 	return float64(int64(input*10)) / 10 | ||||||
|  | } | ||||||
|  |  | ||||||
| func testChannel(channel *model.Channel, request ChatRequest) error { | func testChannel(channel *model.Channel, request ChatRequest) error { | ||||||
| 	switch channel.Type { | 	switch channel.Type { | ||||||
| 	case common.ChannelTypeAzure: | 	case common.ChannelTypeAzure: | ||||||
| @@ -24,6 +35,14 @@ func testChannel(channel *model.Channel, request ChatRequest) error { | |||||||
| 	requestURL := common.ChannelBaseURLs[channel.Type] | 	requestURL := common.ChannelBaseURLs[channel.Type] | ||||||
| 	if channel.Type == common.ChannelTypeAzure { | 	if channel.Type == common.ChannelTypeAzure { | ||||||
| 		requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) | 		requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) | ||||||
|  | 	} else if channel.Type == common.ChannelTypeChatGPTWeb { | ||||||
|  | 		if channel.BaseURL != "" { | ||||||
|  | 			requestURL = channel.BaseURL | ||||||
|  | 		} | ||||||
|  | 	} else if channel.Type == common.ChannelTypeChatbotUI { | ||||||
|  | 		if channel.BaseURL != "" { | ||||||
|  | 			requestURL = channel.BaseURL | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		if channel.BaseURL != "" { | 		if channel.BaseURL != "" { | ||||||
| 			requestURL = channel.BaseURL | 			requestURL = channel.BaseURL | ||||||
| @@ -32,6 +51,74 @@ func testChannel(channel *model.Channel, request ChatRequest) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	jsonData, err := json.Marshal(request) | 	jsonData, err := json.Marshal(request) | ||||||
|  |  | ||||||
|  | 	if channel.Type == common.ChannelTypeChatGPTWeb { | ||||||
|  | 		// Get system message from Message json, Role == "system" | ||||||
|  | 		var systemMessage Message | ||||||
|  |  | ||||||
|  | 		for _, message := range request.Messages { | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				systemMessage = message | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var prompt string | ||||||
|  |  | ||||||
|  | 		// Get all the Message, Roles from request.Messages, and format it into string by | ||||||
|  | 		// ||> role: content | ||||||
|  | 		for _, message := range request.Messages { | ||||||
|  | 			// Exclude system message | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			prompt += "||> " + message.Role + ": " + message.Content + "\n" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Construct json data without adding escape character | ||||||
|  | 		map1 := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 		map1["prompt"] = prompt | ||||||
|  | 		map1["systemMessage"] = systemMessage.Content | ||||||
|  |  | ||||||
|  | 		if request.Temperature != 0 { | ||||||
|  | 			map1["temperature"] = formatFloat(request.Temperature) | ||||||
|  | 		} | ||||||
|  | 		if request.TopP != 0 { | ||||||
|  | 			map1["top_p"] = formatFloat(request.TopP) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert map to json string | ||||||
|  | 		jsonData, err = json.Marshal(map1) | ||||||
|  | 	} else if channel.Type == common.ChannelTypeChatbotUI { | ||||||
|  | 		// Get system message from Message json, Role == "system" | ||||||
|  | 		var systemMessage string | ||||||
|  |  | ||||||
|  | 		for _, message := range request.Messages { | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				systemMessage = message.Content | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Construct json data without adding escape character | ||||||
|  | 		map1 := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 		map1["prompt"] = systemMessage | ||||||
|  | 		map1["temperature"] = formatFloat(request.Temperature) | ||||||
|  | 		map1["key"] = "" | ||||||
|  | 		map1["messages"] = request.Messages | ||||||
|  | 		map1["model"] = map[string]interface{}{ | ||||||
|  | 			"id": request.Model, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert map to json string | ||||||
|  | 		jsonData, err = json.Marshal(map1) | ||||||
|  |  | ||||||
|  | 		//Print jsoinData to console | ||||||
|  | 		log.Println(string(jsonData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -45,31 +132,145 @@ func testChannel(channel *model.Channel, request ChatRequest) error { | |||||||
| 		req.Header.Set("Authorization", "Bearer "+channel.Key) | 		req.Header.Set("Authorization", "Bearer "+channel.Key) | ||||||
| 	} | 	} | ||||||
| 	req.Header.Set("Content-Type", "application/json") | 	req.Header.Set("Content-Type", "application/json") | ||||||
|  |  | ||||||
|  | 	if channel.EnableIpRandomization { | ||||||
|  | 		// Generate random IP | ||||||
|  | 		ip := common.GenerateIP() | ||||||
|  | 		req.Header.Set("X-Forwarded-For", ip) | ||||||
|  | 		req.Header.Set("X-Real-IP", ip) | ||||||
|  | 		req.Header.Set("X-Client-IP", ip) | ||||||
|  | 		req.Header.Set("X-Forwarded-Host", ip) | ||||||
|  | 		req.Header.Set("X-Originating-IP", ip) | ||||||
|  | 		req.RemoteAddr = ip | ||||||
|  | 		req.Header.Set("X-Remote-IP", ip) | ||||||
|  | 		req.Header.Set("X-Remote-Addr", ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	client := &http.Client{} | 	client := &http.Client{} | ||||||
| 	resp, err := client.Do(req) | 	resp, err := client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		// Print the body in string | ||||||
|  | 		if resp.Body != nil { | ||||||
|  | 			buf := new(bytes.Buffer) | ||||||
|  | 			buf.ReadFrom(resp.Body) | ||||||
|  | 			return errors.New("error response: " + strconv.Itoa(resp.StatusCode) + " " + buf.String()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return errors.New("error response: " + strconv.Itoa(resp.StatusCode)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var streamResponseText = "" | ||||||
|  |  | ||||||
|  | 	scanner := bufio.NewScanner(resp.Body) | ||||||
|  |  | ||||||
|  | 	if channel.Type != common.ChannelTypeChatGPTWeb && channel.Type != common.ChannelTypeChatbotUI { | ||||||
|  | 		scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | ||||||
|  | 			if atEOF && len(data) == 0 { | ||||||
|  | 				return 0, nil, nil | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if i := strings.Index(string(data), "\n"); i >= 0 { | ||||||
|  | 				return i + 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! | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if channel.Type == common.ChannelTypeChatGPTWeb { | ||||||
|  | 			var chatResponse ChatGptWebChatResponse | ||||||
|  | 			err = json.Unmarshal([]byte(data), &chatResponse) | ||||||
|  | 			if err != nil { | ||||||
|  | 				// Print the body in string | ||||||
|  | 				buf := new(bytes.Buffer) | ||||||
|  | 				buf.ReadFrom(resp.Body) | ||||||
|  | 				common.SysError("error unmarshalling chat response: " + err.Error() + " " + buf.String()) | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// if response role is assistant and contains delta, append the content to streamResponseText | ||||||
|  | 			if chatResponse.Role == "assistant" && chatResponse.Detail != nil { | ||||||
|  | 				for _, choice := range chatResponse.Detail.Choices { | ||||||
|  | 					streamResponseText += choice.Delta.Content | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} else if channel.Type == common.ChannelTypeChatbotUI { | ||||||
|  | 			streamResponseText += data | ||||||
|  | 		} else if channel.Type != common.ChannelTypeChatGPTWeb { | ||||||
|  | 			// If data has event: event content inside, remove it, it can be prefix or inside the data | ||||||
|  | 			if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") { | ||||||
|  | 				// Remove event: event in the front or back | ||||||
|  | 				data = strings.TrimPrefix(data, "event: event") | ||||||
|  | 				data = strings.TrimSuffix(data, "event: event") | ||||||
|  | 				// Remove everything, only keep `data: {...}` <--- this is the json | ||||||
|  | 				// Find the start and end indices of `data: {...}` substring | ||||||
|  | 				startIndex := strings.Index(data, "data:") | ||||||
|  | 				endIndex := strings.LastIndex(data, "}") | ||||||
|  |  | ||||||
|  | 				// If both indices are found and end index is greater than start index | ||||||
|  | 				if startIndex != -1 && endIndex != -1 && endIndex > startIndex { | ||||||
|  | 					// Extract the `data: {...}` substring | ||||||
|  | 					data = data[startIndex : endIndex+1] | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Trim whitespace and newlines from the modified data string | ||||||
|  | 				data = strings.TrimSpace(data) | ||||||
|  | 			} | ||||||
|  | 			if !strings.HasPrefix(data, "data:") { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			data = data[6:] | ||||||
|  | 			if !strings.HasPrefix(data, "[DONE]") { | ||||||
|  | 				var streamResponse ChatCompletionsStreamResponse | ||||||
|  | 				err = json.Unmarshal([]byte(data), &streamResponse) | ||||||
|  | 				if err != nil { | ||||||
|  | 					// Prinnt the body in string | ||||||
|  | 					buf := new(bytes.Buffer) | ||||||
|  | 					buf.ReadFrom(resp.Body) | ||||||
|  | 					common.SysError("error unmarshalling stream response: " + err.Error() + " " + buf.String()) | ||||||
|  | 					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 | ||||||
| } | } | ||||||
|  |  | ||||||
| func buildTestRequest() *ChatRequest { | func buildTestRequest() *ChatRequest { | ||||||
| 	testRequest := &ChatRequest{ | 	testRequest := &ChatRequest{ | ||||||
| 		Model:     "", // this will be set later | 		Model:  "", // this will be set later | ||||||
| 		MaxTokens: 1, | 		Stream: true, | ||||||
| 	} | 	} | ||||||
| 	testMessage := Message{ | 	testMessage := Message{ | ||||||
| 		Role:    "user", | 		Role:    "user", | ||||||
| 		Content: "hi", | 		Content: "Hello ChatGPT!", | ||||||
| 	} | 	} | ||||||
| 	testRequest.Messages = append(testRequest.Messages, testMessage) | 	testRequest.Messages = append(testRequest.Messages, testMessage) | ||||||
| 	return testRequest | 	return testRequest | ||||||
|   | |||||||
							
								
								
									
										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,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{ | ||||||
|   | |||||||
| @@ -7,10 +7,13 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
| @@ -71,6 +74,27 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			isModelMapped = true | 			isModelMapped = true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Get token info | ||||||
|  | 	tokenInfo, err := model.GetTokenById(tokenId) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "get_token_info_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hasModelAvailable := func() bool { | ||||||
|  | 		for _, token := range strings.Split(tokenInfo.Models, ",") { | ||||||
|  | 			if token == textRequest.Model { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return false | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	if !hasModelAvailable { | ||||||
|  | 		return errorWrapper(errors.New("model not available for use"), "model_not_available_for_use", http.StatusBadRequest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	baseURL := common.ChannelBaseURLs[channelType] | 	baseURL := common.ChannelBaseURLs[channelType] | ||||||
| 	requestURL := c.Request.URL.String() | 	requestURL := c.Request.URL.String() | ||||||
| 	if c.GetString("base_url") != "" { | 	if c.GetString("base_url") != "" { | ||||||
| @@ -95,6 +119,14 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		model_ = strings.TrimSuffix(model_, "-0314") | 		model_ = strings.TrimSuffix(model_, "-0314") | ||||||
| 		model_ = strings.TrimSuffix(model_, "-0613") | 		model_ = strings.TrimSuffix(model_, "-0613") | ||||||
| 		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) | 		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) | ||||||
|  | 	} else if channelType == common.ChannelTypeChatGPTWeb { | ||||||
|  | 		// remove /v1/chat/completions from request url | ||||||
|  | 		requestURL := strings.Split(requestURL, "/v1/chat/completions")[0] | ||||||
|  | 		fullRequestURL = fmt.Sprintf("%s%s", baseURL, requestURL) | ||||||
|  | 	} else if channelType == common.ChannelTypeChatbotUI { | ||||||
|  | 		// remove /v1/chat/completions from request url | ||||||
|  | 		requestURL := strings.Split(requestURL, "/v1/chat/completions")[0] | ||||||
|  | 		fullRequestURL = fmt.Sprintf("%s%s", baseURL, requestURL) | ||||||
| 	} else if channelType == common.ChannelTypePaLM { | 	} else if channelType == common.ChannelTypePaLM { | ||||||
| 		err := relayPaLM(textRequest, c) | 		err := relayPaLM(textRequest, c) | ||||||
| 		return err | 		return err | ||||||
| @@ -140,8 +172,126 @@ 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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if channelType == common.ChannelTypeChatGPTWeb { | ||||||
|  | 		// Get system message from Message json, Role == "system" | ||||||
|  | 		var reqBody ChatRequest | ||||||
|  | 		var systemMessage Message | ||||||
|  |  | ||||||
|  | 		// Parse requestBody into systemMessage | ||||||
|  | 		err := json.NewDecoder(requestBody).Decode(&reqBody) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "decode_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, message := range reqBody.Messages { | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				systemMessage = message | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var prompt string | ||||||
|  |  | ||||||
|  | 		// Get all the Message, Roles from request.Messages, and format it into string by | ||||||
|  | 		// ||> role: content | ||||||
|  | 		for _, message := range reqBody.Messages { | ||||||
|  | 			// Exclude system message | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			prompt += "||> " + message.Role + ": " + message.Content + "\n" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Construct json data without adding escape character | ||||||
|  | 		map1 := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 		map1["prompt"] = prompt + "\nResponse as assistant, but do not include the role in response." | ||||||
|  | 		map1["systemMessage"] = systemMessage.Content | ||||||
|  |  | ||||||
|  | 		if reqBody.Temperature != 0 { | ||||||
|  | 			map1["temperature"] = formatFloat(reqBody.Temperature) | ||||||
|  | 		} | ||||||
|  | 		if reqBody.TopP != 0 { | ||||||
|  | 			map1["top_p"] = formatFloat(reqBody.TopP) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert map to json string | ||||||
|  | 		jsonData, err := json.Marshal(map1) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "marshal_json_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert json string to io.Reader | ||||||
|  | 		requestBody = bytes.NewReader(jsonData) | ||||||
|  | 	} else if channelType == common.ChannelTypeChatbotUI { | ||||||
|  | 		// Get system message from Message json, Role == "system" | ||||||
|  | 		var reqBody ChatRequest | ||||||
|  |  | ||||||
|  | 		// Parse requestBody into systemMessage | ||||||
|  | 		err := json.NewDecoder(requestBody).Decode(&reqBody) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "decode_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Get system message from Message json, Role == "system" | ||||||
|  | 		var systemMessage string | ||||||
|  |  | ||||||
|  | 		for _, message := range reqBody.Messages { | ||||||
|  | 			if message.Role == "system" { | ||||||
|  | 				systemMessage = message.Content | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Construct json data without adding escape character | ||||||
|  | 		map1 := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 		map1["prompt"] = systemMessage | ||||||
|  | 		map1["temperature"] = formatFloat(reqBody.Temperature) | ||||||
|  | 		map1["key"] = "" | ||||||
|  | 		map1["messages"] = reqBody.Messages | ||||||
|  | 		map1["model"] = map[string]interface{}{ | ||||||
|  | 			"id": reqBody.Model, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert map to json string | ||||||
|  | 		jsonData, err := json.Marshal(map1) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "marshal_json_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Convert json string to io.Reader | ||||||
|  | 		requestBody = bytes.NewReader(jsonData) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) | ||||||
| @@ -156,11 +306,36 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||||
| 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||||
| 	//req.Header.Set("Connection", c.Request.Header.Get("Connection")) | 	//req.Header.Set("Connection", c.Request.Header.Get("Connection")) | ||||||
|  |  | ||||||
|  | 	if c.GetBool("enable_ip_randomization") == true { | ||||||
|  | 		// Generate random IP | ||||||
|  | 		ip := common.GenerateIP() | ||||||
|  | 		req.Header.Set("X-Forwarded-For", ip) | ||||||
|  | 		req.Header.Set("X-Real-IP", ip) | ||||||
|  | 		req.Header.Set("X-Client-IP", ip) | ||||||
|  | 		req.Header.Set("X-Forwarded-Host", ip) | ||||||
|  | 		req.Header.Set("X-Originating-IP", ip) | ||||||
|  | 		req.RemoteAddr = ip | ||||||
|  | 		req.Header.Set("X-Remote-IP", ip) | ||||||
|  | 		req.Header.Set("X-Remote-Addr", ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	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.StatusInternalServerError) | 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		// Print the body in string | ||||||
|  | 		if resp.Body != nil { | ||||||
|  | 			buf := new(bytes.Buffer) | ||||||
|  | 			buf.ReadFrom(resp.Body) | ||||||
|  | 			log.Printf("Error Channel (%s): %s", baseURL, buf.String()) | ||||||
|  | 			return errorWrapper(err, "request_failed", resp.StatusCode) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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) | ||||||
| @@ -170,7 +345,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
| 	var textResponse TextResponse | 	var textResponse TextResponse | ||||||
| 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") | 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") || strings.HasPrefix(resp.Header.Get("Content-Type"), "application/octet-stream") | ||||||
| 	var streamResponseText string | 	var streamResponseText string | ||||||
|  |  | ||||||
| 	defer func() { | 	defer func() { | ||||||
| @@ -220,60 +395,152 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| 	if isStream { | 	if isStream || channelType == common.ChannelTypeChatGPTWeb || channelType == common.ChannelTypeChatbotUI { | ||||||
| 		scanner := bufio.NewScanner(resp.Body) |  | ||||||
| 		scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { |  | ||||||
| 			if atEOF && len(data) == 0 { |  | ||||||
| 				return 0, nil, nil |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if i := strings.Index(string(data), "\n"); i >= 0 { |  | ||||||
| 				return i + 1, data[0:i], nil |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if atEOF { |  | ||||||
| 				return len(data), data, nil |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			return 0, nil, nil |  | ||||||
| 		}) |  | ||||||
| 		dataChan := make(chan string) | 		dataChan := make(chan string) | ||||||
| 		stopChan := make(chan bool) | 		stopChan := make(chan bool) | ||||||
|  |  | ||||||
|  | 		scanner := bufio.NewScanner(resp.Body) | ||||||
|  |  | ||||||
|  | 		if channelType != common.ChannelTypeChatGPTWeb && channelType != common.ChannelTypeChatbotUI { | ||||||
|  | 			scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | ||||||
|  | 				if atEOF && len(data) == 0 { | ||||||
|  | 					return 0, nil, nil | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if i := strings.Index(string(data), "\n"); i >= 0 { | ||||||
|  | 					return i + 2, data[0:i], nil | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if atEOF { | ||||||
|  | 					return len(data), data, nil | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				return 0, nil, nil | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		go func() { | 		go func() { | ||||||
| 			for scanner.Scan() { | 			for scanner.Scan() { | ||||||
| 				data := scanner.Text() | 				data := scanner.Text() | ||||||
| 				if len(data) < 6 { // ignore blank line or wrong format | 				if len(data) < 6 { // must be something wrong! | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
| 				dataChan <- data |  | ||||||
| 				data = data[6:] | 				if channelType == common.ChannelTypeChatGPTWeb { | ||||||
| 				if !strings.HasPrefix(data, "[DONE]") { | 					var chatResponse ChatGptWebChatResponse | ||||||
| 					switch relayMode { | 					err = json.Unmarshal([]byte(data), &chatResponse) | ||||||
| 					case RelayModeChatCompletions: | 					if err != nil { | ||||||
| 						var streamResponse ChatCompletionsStreamResponse | 						// Print the body in string | ||||||
| 						err = json.Unmarshal([]byte(data), &streamResponse) | 						buf := new(bytes.Buffer) | ||||||
| 						if err != nil { | 						buf.ReadFrom(resp.Body) | ||||||
| 							common.SysError("error unmarshalling stream response: " + err.Error()) | 						common.SysError("error unmarshalling chat response: " + err.Error() + " " + buf.String()) | ||||||
| 							return | 						return | ||||||
| 						} | 					} | ||||||
| 						for _, choice := range streamResponse.Choices { |  | ||||||
|  | 					// if response role is assistant and contains delta, append the content to streamResponseText | ||||||
|  | 					if chatResponse.Role == "assistant" && chatResponse.Detail != nil { | ||||||
|  | 						for _, choice := range chatResponse.Detail.Choices { | ||||||
| 							streamResponseText += choice.Delta.Content | 							streamResponseText += choice.Delta.Content | ||||||
| 						} |  | ||||||
| 					case RelayModeCompletions: | 							returnObj := map[string]interface{}{ | ||||||
| 						var streamResponse CompletionsStreamResponse | 								"id":      chatResponse.ID, | ||||||
| 						err = json.Unmarshal([]byte(data), &streamResponse) | 								"object":  chatResponse.Detail.Object, | ||||||
| 						if err != nil { | 								"created": chatResponse.Detail.Created, | ||||||
| 							common.SysError("error unmarshalling stream response: " + err.Error()) | 								"model":   chatResponse.Detail.Model, | ||||||
| 							return | 								"choices": []map[string]interface{}{ | ||||||
| 						} | 									// set finish_reason to null in json | ||||||
| 						for _, choice := range streamResponse.Choices { | 									{ | ||||||
| 							streamResponseText += choice.Text | 										"finish_reason": nil, | ||||||
|  | 										"index":         0, | ||||||
|  | 										"delta": map[string]interface{}{ | ||||||
|  | 											"content": choice.Delta.Content, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							} | ||||||
|  |  | ||||||
|  | 							jsonData, _ := json.Marshal(returnObj) | ||||||
|  |  | ||||||
|  | 							dataChan <- "data: " + string(jsonData) | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
|  | 				} else if channelType == common.ChannelTypeChatbotUI { | ||||||
|  | 					returnObj := map[string]interface{}{ | ||||||
|  | 						"id":      "chatcmpl-" + strconv.Itoa(int(time.Now().UnixNano())), | ||||||
|  | 						"object":  "text_completion", | ||||||
|  | 						"created": time.Now().Unix(), | ||||||
|  | 						"model":   textRequest.Model, | ||||||
|  | 						"choices": []map[string]interface{}{ | ||||||
|  | 							// set finish_reason to null in json | ||||||
|  | 							{ | ||||||
|  | 								"finish_reason": nil, | ||||||
|  | 								"index":         0, | ||||||
|  | 								"delta": map[string]interface{}{ | ||||||
|  | 									"content": data, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					jsonData, _ := json.Marshal(returnObj) | ||||||
|  |  | ||||||
|  | 					dataChan <- "data: " + string(jsonData) | ||||||
|  | 				} else { | ||||||
|  | 					// If data has event: event content inside, remove it, it can be prefix or inside the data | ||||||
|  | 					if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") { | ||||||
|  | 						// Remove event: event in the front or back | ||||||
|  | 						data = strings.TrimPrefix(data, "event: event") | ||||||
|  | 						data = strings.TrimSuffix(data, "event: event") | ||||||
|  | 						// Remove everything, only keep `data: {...}` <--- this is the json | ||||||
|  | 						// Find the start and end indices of `data: {...}` substring | ||||||
|  | 						startIndex := strings.Index(data, "data:") | ||||||
|  | 						endIndex := strings.LastIndex(data, "}") | ||||||
|  |  | ||||||
|  | 						// If both indices are found and end index is greater than start index | ||||||
|  | 						if startIndex != -1 && endIndex != -1 && endIndex > startIndex { | ||||||
|  | 							// Extract the `data: {...}` substring | ||||||
|  | 							data = data[startIndex : endIndex+1] | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 						// Trim whitespace and newlines from the modified data string | ||||||
|  | 						data = strings.TrimSpace(data) | ||||||
|  | 					} | ||||||
|  | 					if !strings.HasPrefix(data, "data:") { | ||||||
|  | 						continue | ||||||
|  | 					} | ||||||
|  | 					dataChan <- data | ||||||
|  | 					data = data[6:] | ||||||
|  | 					if !strings.HasPrefix(data, "[DONE]") { | ||||||
|  | 						switch relayMode { | ||||||
|  | 						case RelayModeChatCompletions: | ||||||
|  | 							var streamResponse ChatCompletionsStreamResponse | ||||||
|  | 							err = json.Unmarshal([]byte(data), &streamResponse) | ||||||
|  | 							if err != nil { | ||||||
|  | 								common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 								return | ||||||
|  | 							} | ||||||
|  | 							for _, choice := range streamResponse.Choices { | ||||||
|  | 								streamResponseText += choice.Delta.Content | ||||||
|  | 							} | ||||||
|  | 						case RelayModeCompletions: | ||||||
|  | 							var streamResponse CompletionsStreamResponse | ||||||
|  | 							err = json.Unmarshal([]byte(data), &streamResponse) | ||||||
|  | 							if err != nil { | ||||||
|  | 								common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 								return | ||||||
|  | 							} | ||||||
|  | 							for _, choice := range streamResponse.Choices { | ||||||
|  | 								streamResponseText += choice.Text | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 					} | ||||||
|  |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			stopChan <- true | 			stopChan <- true | ||||||
| 		}() | 		}() | ||||||
|  |  | ||||||
| 		c.Writer.Header().Set("Content-Type", "text/event-stream") | 		c.Writer.Header().Set("Content-Type", "text/event-stream") | ||||||
| 		c.Writer.Header().Set("Cache-Control", "no-cache") | 		c.Writer.Header().Set("Cache-Control", "no-cache") | ||||||
| 		c.Writer.Header().Set("Connection", "keep-alive") | 		c.Writer.Header().Set("Connection", "keep-alive") | ||||||
|   | |||||||
| @@ -45,7 +45,11 @@ type GeneralOpenAIRequest struct { | |||||||
| 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,omitempty"` | ||||||
|  | 	Stream    bool      `json:"stream"` | ||||||
|  | 	// -1.0 to 1.0 | ||||||
|  | 	Temperature float64 `json:"temperature"` | ||||||
|  | 	TopP        float64 `json:"top_p"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type TextRequest struct { | type TextRequest struct { | ||||||
| @@ -108,6 +112,32 @@ type CompletionsStreamResponse struct { | |||||||
| 	} `json:"choices"` | 	} `json:"choices"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ChatGptWebDetail struct { | ||||||
|  | 	ID      string             `json:"id"` | ||||||
|  | 	Object  string             `json:"object"` | ||||||
|  | 	Created int                `json:"created"` | ||||||
|  | 	Model   string             `json:"model"` | ||||||
|  | 	Choices []ChatGptWebChoice `json:"choices"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ChatGptWebChoice struct { | ||||||
|  | 	Delta struct { | ||||||
|  | 		Content string `json:"content"` | ||||||
|  | 		Role    string `json:"role"` | ||||||
|  | 	} `json:"delta"` | ||||||
|  | 	Index         int    `json:"index"` | ||||||
|  | 	Finish_Reason string `json:"finish_reason"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ChatGptWebChatResponse struct { | ||||||
|  | 	Role            string            `json:"role"` | ||||||
|  | 	ID              string            `json:"id"` | ||||||
|  | 	ParentMessageID string            `json:"parentMessageId"` | ||||||
|  | 	Text            string            `json:"text"` | ||||||
|  | 	Delta           string            `json:"delta"` | ||||||
|  | 	Detail          *ChatGptWebDetail `json:"detail"` | ||||||
|  | } | ||||||
|  |  | ||||||
| func Relay(c *gin.Context) { | func Relay(c *gin.Context) { | ||||||
| 	relayMode := RelayModeUnknown | 	relayMode := RelayModeUnknown | ||||||
| 	if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { | 	if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetAllTokens(c *gin.Context) { | func GetAllTokens(c *gin.Context) { | ||||||
| @@ -125,6 +126,7 @@ func AddToken(c *gin.Context) { | |||||||
| 		ExpiredTime:    token.ExpiredTime, | 		ExpiredTime:    token.ExpiredTime, | ||||||
| 		RemainQuota:    token.RemainQuota, | 		RemainQuota:    token.RemainQuota, | ||||||
| 		UnlimitedQuota: token.UnlimitedQuota, | 		UnlimitedQuota: token.UnlimitedQuota, | ||||||
|  | 		Models:         token.Models, | ||||||
| 	} | 	} | ||||||
| 	err = cleanToken.Insert() | 	err = cleanToken.Insert() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -203,6 +205,7 @@ func UpdateToken(c *gin.Context) { | |||||||
| 		cleanToken.ExpiredTime = token.ExpiredTime | 		cleanToken.ExpiredTime = token.ExpiredTime | ||||||
| 		cleanToken.RemainQuota = token.RemainQuota | 		cleanToken.RemainQuota = token.RemainQuota | ||||||
| 		cleanToken.UnlimitedQuota = token.UnlimitedQuota | 		cleanToken.UnlimitedQuota = token.UnlimitedQuota | ||||||
|  | 		cleanToken.Models = token.Models | ||||||
| 	} | 	} | ||||||
| 	err = cleanToken.Update() | 	err = cleanToken.Update() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -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 && VITE_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"] | ||||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								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.5 | ||||||
| 	golang.org/x/crypto v0.9.0 | 	golang.org/x/crypto v0.11.0 | ||||||
| 	gorm.io/driver/mysql v1.4.3 | 	gorm.io/driver/mysql v1.5.1 | ||||||
| 	gorm.io/driver/sqlite v1.4.3 | 	gorm.io/driver/sqlite v1.5.2 | ||||||
| 	gorm.io/gorm v1.24.0 | 	gorm.io/gorm v1.25.2 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/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.9 // 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 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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= | ||||||
| @@ -112,11 +130,21 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | |||||||
| github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= | github.com/pkoukk/tiktoken-go v0.1.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/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= | ||||||
|  | github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
|  | github.com/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= | ||||||
| @@ -133,6 +161,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o | |||||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | ||||||
| github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
|  | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | ||||||
| @@ -144,34 +173,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 +239,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= | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								i18n/en.json
									
									
									
									
									
								
							| @@ -237,7 +237,8 @@ | |||||||
|   "保存首页内容": "Save Home Page Content", |   "保存首页内容": "Save Home Page Content", | ||||||
|   "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.", |   "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.", | ||||||
|   "保存关于": "Save About", |   "保存关于": "Save About", | ||||||
|   "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.", |   "移除 One API": "Removal of One API", | ||||||
|  |   "的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。": " copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.", | ||||||
|   "页脚": "Footer", |   "页脚": "Footer", | ||||||
|   "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.", |   "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.", | ||||||
|   "设置页脚": "Set Footer", |   "设置页脚": "Set Footer", | ||||||
| @@ -443,6 +444,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", | ||||||
| @@ -503,5 +505,37 @@ | |||||||
|   "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", |   "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", | ||||||
|   "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", |   "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", | ||||||
|   "Homepage URL 填": "Fill in the Homepage URL", |   "Homepage URL 填": "Fill in the Homepage URL", | ||||||
|   "Authorization callback URL 填": "Fill in the Authorization callback URL" |   "Authorization callback URL 填": "Fill in the Authorization callback URL", | ||||||
|  |   "允许通过 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, ", | ||||||
|  |   "请选择此密钥支持的模型": "Please select the models supported by this key", | ||||||
|  |   "将IP随机地址传递给HTTP头": "Pass the IP random address to the HTTP header", | ||||||
|  |   "失败重试次数": "Number of failed retries", | ||||||
|  |   "消费": "Consumption", | ||||||
|  |   "管理": "Management", | ||||||
|  |   "系统": "System", | ||||||
|  |   "未知": "Unknown", | ||||||
|  |   "One API 会把请求体中的 model": "One API will take the model in the request body", | ||||||
|  |   ",因为": ", because", | ||||||
|  |   "参数替换为你的部署名称(模型名称中的点会被剔除),": "Replace the parameter with your deployment name (dots in the model name will be removed), ", | ||||||
|  |   "注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI": "Note that the generated token here is used for system management, not for requesting OpenAI", | ||||||
|  |   "相关的服务,请知悉。": "related services, please be aware.", | ||||||
|  |   "填入": "Fill in" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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" { | ||||||
|   | |||||||
| @@ -105,6 +105,7 @@ func Distribute() func(c *gin.Context) { | |||||||
| 		c.Set("channel_id", channel.Id) | 		c.Set("channel_id", channel.Id) | ||||||
| 		c.Set("channel_name", channel.Name) | 		c.Set("channel_name", channel.Name) | ||||||
| 		c.Set("model_mapping", channel.ModelMapping) | 		c.Set("model_mapping", channel.ModelMapping) | ||||||
|  | 		c.Set("enable_ip_randomization", channel.EnableIpRandomization) | ||||||
| 		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) | 		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) | ||||||
| 		c.Set("base_url", channel.BaseURL) | 		c.Set("base_url", channel.BaseURL) | ||||||
| 		if channel.Type == common.ChannelTypeAzure { | 		if channel.Type == common.ChannelTypeAzure { | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| package model | package model | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Channel struct { | type Channel struct { | ||||||
| @@ -23,6 +24,9 @@ type Channel struct { | |||||||
| 	Group              string  `json:"group" gorm:"type:varchar(32);default:'default'"` | 	Group              string  `json:"group" gorm:"type:varchar(32);default:'default'"` | ||||||
| 	UsedQuota          int64   `json:"used_quota" gorm:"bigint;default:0"` | 	UsedQuota          int64   `json:"used_quota" gorm:"bigint;default:0"` | ||||||
| 	ModelMapping       string  `json:"model_mapping" gorm:"type:varchar(1024);default:''"` | 	ModelMapping       string  `json:"model_mapping" gorm:"type:varchar(1024);default:''"` | ||||||
|  |  | ||||||
|  | 	// Additional fields, default value is false | ||||||
|  | 	EnableIpRandomization bool `json:"enable_ip_randomization"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | ||||||
|   | |||||||
| @@ -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"] = "" | ||||||
| @@ -133,6 +136,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": | ||||||
| @@ -171,6 +176,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,8 +3,9 @@ package model | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
|  |  | ||||||
|  | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Token struct { | type Token struct { | ||||||
| @@ -19,6 +20,7 @@ type Token struct { | |||||||
| 	RemainQuota    int    `json:"remain_quota" gorm:"default:0"` | 	RemainQuota    int    `json:"remain_quota" gorm:"default:0"` | ||||||
| 	UnlimitedQuota bool   `json:"unlimited_quota" gorm:"default:false"` | 	UnlimitedQuota bool   `json:"unlimited_quota" gorm:"default:false"` | ||||||
| 	UsedQuota      int    `json:"used_quota" gorm:"default:0"` // used quota | 	UsedQuota      int    `json:"used_quota" gorm:"default:0"` // used quota | ||||||
|  | 	Models         string `json:"models"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { | func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { | ||||||
| @@ -99,7 +101,7 @@ func (token *Token) Insert() error { | |||||||
| // Update Make sure your token's fields is completed, because this will update non-zero values | // Update Make sure your token's fields is completed, because this will update non-zero values | ||||||
| func (token *Token) Update() error { | func (token *Token) Update() error { | ||||||
| 	var err error | 	var err error | ||||||
| 	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota").Updates(token).Error | 	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models").Updates(token).Error | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,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) | ||||||
| @@ -28,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("/") | ||||||
| @@ -36,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) | ||||||
| @@ -61,11 +62,13 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 			optionRoute.PUT("/", controller.UpdateOption) | 			optionRoute.PUT("/", controller.UpdateOption) | ||||||
| 		} | 		} | ||||||
| 		channelRoute := apiRouter.Group("/channel") | 		channelRoute := apiRouter.Group("/channel") | ||||||
|  |  | ||||||
|  | 		channelRoute.Use(middleware.UserAuth()).GET("/models", controller.ListModels) | ||||||
|  |  | ||||||
| 		channelRoute.Use(middleware.AdminAuth()) | 		channelRoute.Use(middleware.AdminAuth()) | ||||||
| 		{ | 		{ | ||||||
| 			channelRoute.GET("/", controller.GetAllChannels) | 			channelRoute.GET("/", controller.GetAllChannels) | ||||||
| 			channelRoute.GET("/search", controller.SearchChannels) | 			channelRoute.GET("/search", controller.SearchChannels) | ||||||
| 			channelRoute.GET("/models", controller.ListModels) |  | ||||||
| 			channelRoute.GET("/:id", controller.GetChannel) | 			channelRoute.GET("/:id", controller.GetChannel) | ||||||
| 			channelRoute.GET("/test", controller.TestAllChannels) | 			channelRoute.GET("/test", controller.TestAllChannels) | ||||||
| 			channelRoute.GET("/test/:id", controller.TestChannel) | 			channelRoute.GET("/test/:id", controller.TestChannel) | ||||||
| @@ -75,6 +78,7 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 			channelRoute.PUT("/", controller.UpdateChannel) | 			channelRoute.PUT("/", controller.UpdateChannel) | ||||||
| 			channelRoute.DELETE("/:id", controller.DeleteChannel) | 			channelRoute.DELETE("/:id", controller.DeleteChannel) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		tokenRoute := apiRouter.Group("/token") | 		tokenRoute := apiRouter.Group("/token") | ||||||
| 		tokenRoute.Use(middleware.UserAuth()) | 		tokenRoute.Use(middleware.UserAuth()) | ||||||
| 		{ | 		{ | ||||||
|   | |||||||
							
								
								
									
										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 |  | ||||||
|   | |||||||
| @@ -10,12 +10,12 @@ npm start | |||||||
| npm run build | npm run build | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build, | If you want to change the default server, please set `VITE_REACT_APP_SERVER` environment variables before build, | ||||||
| for example: `REACT_APP_SERVER=http://your.domain.com`. | for example: `VITE_REACT_APP_SERVER=http://your.domain.com`. | ||||||
|  |  | ||||||
| Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. | Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. | ||||||
|  |  | ||||||
| ## Reference | ## Reference | ||||||
|  |  | ||||||
| 1. https://github.com/OIerDb-ng/OIerDb | 1. https://github.com/OIerDb-ng/OIerDb | ||||||
| 2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example | 2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <!DOCTYPE html> | <!doctype html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="utf-8" /> |     <meta charset="utf-8" /> | ||||||
| @@ -14,5 +14,6 @@ | |||||||
|   <body> |   <body> | ||||||
|     <noscript>You need to enable JavaScript to run this app.</noscript> |     <noscript>You need to enable JavaScript to run this app.</noscript> | ||||||
|     <div id="root"></div> |     <div id="root"></div> | ||||||
|  |     <script type="module" src="./src/index.jsx"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
							
								
								
									
										1799
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1799
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,24 +3,21 @@ | |||||||
|   "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": "vite preview", | ||||||
|     "build": "react-scripts build", |     "build": "vite build" | ||||||
|     "test": "react-scripts test", |  | ||||||
|     "eject": "react-scripts eject" |  | ||||||
|   }, |   }, | ||||||
|   "eslintConfig": { |   "eslintConfig": { | ||||||
|     "extends": [ |     "extends": [ | ||||||
| @@ -41,7 +38,10 @@ | |||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "prettier": "^2.7.1" |     "@vitejs/plugin-react": "^4.0.3", | ||||||
|  |     "prettier": "3.0.0", | ||||||
|  |     "terser": "^5.19.0", | ||||||
|  |     "vite": "^4.4.4" | ||||||
|   }, |   }, | ||||||
|   "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 | ||||||
| @@ -1,30 +1,43 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Label, | ||||||
|  |   Pagination, | ||||||
|  |   Popup, | ||||||
|  |   Table, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; | import { | ||||||
|  |   API, | ||||||
|  |   showError, | ||||||
|  |   showInfo, | ||||||
|  |   showSuccess, | ||||||
|  |   timestamp2string, | ||||||
|  | } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; | import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderGroup, renderNumber } from '../helpers/render'; | import { renderGroup, renderNumber } from '../helpers/render'; | ||||||
| 
 | 
 | ||||||
| function renderTimestamp(timestamp) { | function renderTimestamp(timestamp) { | ||||||
|   return ( |   return <>{timestamp2string(timestamp)}</>; | ||||||
|     <> |  | ||||||
|       {timestamp2string(timestamp)} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let type2label = undefined; | let type2label = undefined; | ||||||
| 
 | 
 | ||||||
| function renderType(type) { | function renderType(type) { | ||||||
|   if (!type2label) { |   if (!type2label) { | ||||||
|     type2label = new Map; |     type2label = new Map(); | ||||||
|     for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { |     for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { | ||||||
|       type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; |       type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; | ||||||
|     } |     } | ||||||
|     type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; |     type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; | ||||||
|   } |   } | ||||||
|   return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>; |   return ( | ||||||
|  |     <Label basic color={type2label[type].color}> | ||||||
|  |       {type2label[type].text} | ||||||
|  |     </Label> | ||||||
|  |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function renderBalance(type, balance) { | function renderBalance(type, balance) { | ||||||
| @@ -132,7 +145,11 @@ const ChannelsTable = () => { | |||||||
|   const renderStatus = (status) => { |   const renderStatus = (status) => { | ||||||
|     switch (status) { |     switch (status) { | ||||||
|       case 1: |       case 1: | ||||||
|         return <Label basic color='green'>已启用</Label>; |         return ( | ||||||
|  |           <Label basic color='green'> | ||||||
|  |             已启用 | ||||||
|  |           </Label> | ||||||
|  |         ); | ||||||
|       case 2: |       case 2: | ||||||
|         return ( |         return ( | ||||||
|           <Label basic color='red'> |           <Label basic color='red'> | ||||||
| @@ -152,15 +169,35 @@ const ChannelsTable = () => { | |||||||
|     let time = responseTime / 1000; |     let time = responseTime / 1000; | ||||||
|     time = time.toFixed(2) + ' 秒'; |     time = time.toFixed(2) + ' 秒'; | ||||||
|     if (responseTime === 0) { |     if (responseTime === 0) { | ||||||
|       return <Label basic color='grey'>未测试</Label>; |       return ( | ||||||
|  |         <Label basic color='grey'> | ||||||
|  |           未测试 | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     } else if (responseTime <= 1000) { |     } else if (responseTime <= 1000) { | ||||||
|       return <Label basic color='green'>{time}</Label>; |       return ( | ||||||
|  |         <Label basic color='green'> | ||||||
|  |           {time} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     } else if (responseTime <= 3000) { |     } else if (responseTime <= 3000) { | ||||||
|       return <Label basic color='olive'>{time}</Label>; |       return ( | ||||||
|  |         <Label basic color='olive'> | ||||||
|  |           {time} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     } else if (responseTime <= 5000) { |     } else if (responseTime <= 5000) { | ||||||
|       return <Label basic color='yellow'>{time}</Label>; |       return ( | ||||||
|  |         <Label basic color='yellow'> | ||||||
|  |           {time} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     } else { |     } else { | ||||||
|       return <Label basic color='red'>{time}</Label>; |       return ( | ||||||
|  |         <Label basic color='red'> | ||||||
|  |           {time} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @@ -342,7 +379,7 @@ const ChannelsTable = () => { | |||||||
|           {channels |           {channels | ||||||
|             .slice( |             .slice( | ||||||
|               (activePage - 1) * ITEMS_PER_PAGE, |               (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|               activePage * ITEMS_PER_PAGE |               activePage * ITEMS_PER_PAGE, | ||||||
|             ) |             ) | ||||||
|             .map((channel, idx) => { |             .map((channel, idx) => { | ||||||
|               if (channel.deleted) return <></>; |               if (channel.deleted) return <></>; | ||||||
| @@ -355,7 +392,11 @@ const ChannelsTable = () => { | |||||||
|                   <Table.Cell>{renderStatus(channel.status)}</Table.Cell> |                   <Table.Cell>{renderStatus(channel.status)}</Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <Popup |                     <Popup | ||||||
|                       content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'} |                       content={ | ||||||
|  |                         channel.test_time | ||||||
|  |                           ? renderTimestamp(channel.test_time) | ||||||
|  |                           : '未测试' | ||||||
|  |                       } | ||||||
|                       key={channel.id} |                       key={channel.id} | ||||||
|                       trigger={renderResponseTime(channel.response_time)} |                       trigger={renderResponseTime(channel.response_time)} | ||||||
|                       basic |                       basic | ||||||
| @@ -363,7 +404,11 @@ const ChannelsTable = () => { | |||||||
|                   </Table.Cell> |                   </Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <Popup |                     <Popup | ||||||
|                       content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'} |                       content={ | ||||||
|  |                         channel.balance_updated_time | ||||||
|  |                           ? renderTimestamp(channel.balance_updated_time) | ||||||
|  |                           : '未更新' | ||||||
|  |                       } | ||||||
|                       key={channel.id} |                       key={channel.id} | ||||||
|                       trigger={renderBalance(channel.type, channel.balance)} |                       trigger={renderBalance(channel.type, channel.balance)} | ||||||
|                       basic |                       basic | ||||||
| @@ -415,7 +460,7 @@ const ChannelsTable = () => { | |||||||
|                           manageChannel( |                           manageChannel( | ||||||
|                             channel.id, |                             channel.id, | ||||||
|                             channel.status === 1 ? 'disable' : 'enable', |                             channel.status === 1 ? 'disable' : 'enable', | ||||||
|                             idx |                             idx, | ||||||
|                           ); |                           ); | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
| @@ -438,14 +483,24 @@ const ChannelsTable = () => { | |||||||
|         <Table.Footer> |         <Table.Footer> | ||||||
|           <Table.Row> |           <Table.Row> | ||||||
|             <Table.HeaderCell colSpan='8'> |             <Table.HeaderCell colSpan='8'> | ||||||
|               <Button size='small' as={Link} to='/channel/add' loading={loading}> |               <Button | ||||||
|  |                 size='small' | ||||||
|  |                 as={Link} | ||||||
|  |                 to='/channel/add' | ||||||
|  |                 loading={loading} | ||||||
|  |               > | ||||||
|                 添加新的渠道 |                 添加新的渠道 | ||||||
|               </Button> |               </Button> | ||||||
|               <Button size='small' loading={loading} onClick={testAllChannels}> |               <Button size='small' loading={loading} onClick={testAllChannels}> | ||||||
|                 测试所有已启用通道 |                 测试所有已启用通道 | ||||||
|               </Button> |               </Button> | ||||||
|               <Button size='small' onClick={updateAllChannelsBalance} |               <Button | ||||||
|                       loading={loading || updatingBalance}>更新所有已启用通道余额</Button> |                 size='small' | ||||||
|  |                 onClick={updateAllChannelsBalance} | ||||||
|  |                 loading={loading || updatingBalance} | ||||||
|  |               > | ||||||
|  |                 更新所有已启用通道余额 | ||||||
|  |               </Button> | ||||||
|               <Pagination |               <Pagination | ||||||
|                 floated='right' |                 floated='right' | ||||||
|                 activePage={activePage} |                 activePage={activePage} | ||||||
| @@ -457,7 +512,9 @@ const ChannelsTable = () => { | |||||||
|                   (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0) |                   (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0) | ||||||
|                 } |                 } | ||||||
|               /> |               /> | ||||||
|               <Button size='small' onClick={refresh} loading={loading}>刷新</Button> |               <Button size='small' onClick={refresh} loading={loading}> | ||||||
|  |                 刷新 | ||||||
|  |               </Button> | ||||||
|             </Table.HeaderCell> |             </Table.HeaderCell> | ||||||
|           </Table.Row> |           </Table.Row> | ||||||
|         </Table.Footer> |         </Table.Footer> | ||||||
							
								
								
									
										57
									
								
								web/src/components/DiscordOAuth.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/src/components/DiscordOAuth.jsx
									
									
									
									
									
										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; | ||||||
| @@ -37,11 +37,8 @@ const Footer = () => { | |||||||
|           ></div> |           ></div> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <div className='custom-footer'> |           <div className='custom-footer'> | ||||||
|             <a |             <a href='https://github.com/songquanpeng/one-api' target='_blank'> | ||||||
|               href='https://github.com/songquanpeng/one-api' |               {systemName} {import.meta.env.VITE_REACT_APP_VERSION}{' '} | ||||||
|               target='_blank' |  | ||||||
|             > |  | ||||||
|               {systemName} {process.env.REACT_APP_VERSION}{' '} |  | ||||||
|             </a> |             </a> | ||||||
|             由{' '} |             由{' '} | ||||||
|             <a href='https://github.com/songquanpeng' target='_blank'> |             <a href='https://github.com/songquanpeng' target='_blank'> | ||||||
| @@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react'; | |||||||
| import { Link, useNavigate } from 'react-router-dom'; | import { Link, useNavigate } from 'react-router-dom'; | ||||||
| import { UserContext } from '../context/User'; | import { UserContext } from '../context/User'; | ||||||
| 
 | 
 | ||||||
| import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react'; | import { | ||||||
| import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers'; |   Button, | ||||||
|  |   Container, | ||||||
|  |   Dropdown, | ||||||
|  |   Icon, | ||||||
|  |   Menu, | ||||||
|  |   Segment, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
|  | import { | ||||||
|  |   API, | ||||||
|  |   getLogo, | ||||||
|  |   getSystemName, | ||||||
|  |   isAdmin, | ||||||
|  |   isMobile, | ||||||
|  |   showSuccess, | ||||||
|  | } from '../helpers'; | ||||||
| import '../index.css'; | import '../index.css'; | ||||||
| 
 | 
 | ||||||
| // Header Buttons | // Header Buttons | ||||||
| @@ -11,58 +25,58 @@ let headerButtons = [ | |||||||
|   { |   { | ||||||
|     name: '首页', |     name: '首页', | ||||||
|     to: '/', |     to: '/', | ||||||
|     icon: 'home' |     icon: 'home', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '渠道', |     name: '渠道', | ||||||
|     to: '/channel', |     to: '/channel', | ||||||
|     icon: 'sitemap', |     icon: 'sitemap', | ||||||
|     admin: true |     admin: true, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '令牌', |     name: '令牌', | ||||||
|     to: '/token', |     to: '/token', | ||||||
|     icon: 'key' |     icon: 'key', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '兑换', |     name: '兑换', | ||||||
|     to: '/redemption', |     to: '/redemption', | ||||||
|     icon: 'dollar sign', |     icon: 'dollar sign', | ||||||
|     admin: true |     admin: true, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '充值', |     name: '充值', | ||||||
|     to: '/topup', |     to: '/topup', | ||||||
|     icon: 'cart' |     icon: 'cart', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '用户', |     name: '用户', | ||||||
|     to: '/user', |     to: '/user', | ||||||
|     icon: 'user', |     icon: 'user', | ||||||
|     admin: true |     admin: true, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '日志', |     name: '日志', | ||||||
|     to: '/log', |     to: '/log', | ||||||
|     icon: 'book' |     icon: 'book', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '设置', |     name: '设置', | ||||||
|     to: '/setting', |     to: '/setting', | ||||||
|     icon: 'setting' |     icon: 'setting', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: '关于', |     name: '关于', | ||||||
|     to: '/about', |     to: '/about', | ||||||
|     icon: 'info circle' |     icon: 'info circle', | ||||||
|   } |   }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| if (localStorage.getItem('chat_link')) { | if (localStorage.getItem('chat_link')) { | ||||||
|   headerButtons.splice(1, 0, { |   headerButtons.splice(1, 0, { | ||||||
|     name: '聊天', |     name: '聊天', | ||||||
|     to: '/chat', |     to: '/chat', | ||||||
|     icon: 'comments' |     icon: 'comments', | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -120,21 +134,17 @@ const Header = () => { | |||||||
|           style={ |           style={ | ||||||
|             showSidebar |             showSidebar | ||||||
|               ? { |               ? { | ||||||
|                 borderBottom: 'none', |                   borderBottom: 'none', | ||||||
|                 marginBottom: '0', |                   marginBottom: '0', | ||||||
|                 borderTop: 'none', |                   borderTop: 'none', | ||||||
|                 height: '51px' |                   height: '51px', | ||||||
|               } |                 } | ||||||
|               : { borderTop: 'none', height: '52px' } |               : { borderTop: 'none', height: '52px' } | ||||||
|           } |           } | ||||||
|         > |         > | ||||||
|           <Container> |           <Container> | ||||||
|             <Menu.Item as={Link} to='/'> |             <Menu.Item as={Link} to='/'> | ||||||
|               <img |               <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} /> | ||||||
|                 src={logo} |  | ||||||
|                 alt='logo' |  | ||||||
|                 style={{ marginRight: '0.75em' }} |  | ||||||
|               /> |  | ||||||
|               <div style={{ fontSize: '20px' }}> |               <div style={{ fontSize: '20px' }}> | ||||||
|                 <b>{systemName}</b> |                 <b>{systemName}</b> | ||||||
|               </div> |               </div> | ||||||
| @@ -13,6 +13,7 @@ import { | |||||||
| 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, showInfo } 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,19 +25,27 @@ const LoginForm = () => { | |||||||
|   const [submitted, setSubmitted] = useState(false); |   const [submitted, setSubmitted] = useState(false); | ||||||
|   const { username, password } = inputs; |   const { username, password } = inputs; | ||||||
|   const [userState, userDispatch] = useContext(UserContext); |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [turnstileEnabled, setTurnstileEnabled] = useState(false); | ||||||
|  |   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||||
|  |   const [turnstileToken, setTurnstileToken] = useState(''); | ||||||
|   let navigate = useNavigate(); |   let navigate = useNavigate(); | ||||||
| 
 | 
 | ||||||
|   const [status, setStatus] = useState({}); |   const [status, setStatus] = useState({}); | ||||||
|   const logo = getLogo(); |   const logo = getLogo(); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (searchParams.get("expired")) { |     if (searchParams.get('expired')) { | ||||||
|       showError('未登录或登录已过期,请重新登录!'); |       showError('未登录或登录已过期,请重新登录!'); | ||||||
|     } |     } | ||||||
|     let status = localStorage.getItem('status'); |     let status = localStorage.getItem('status'); | ||||||
|     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); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
| @@ -44,7 +53,13 @@ const LoginForm = () => { | |||||||
| 
 | 
 | ||||||
|   const onGitHubOAuthClicked = () => { |   const onGitHubOAuthClicked = () => { | ||||||
|     window.open( |     window.open( | ||||||
|       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` |       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`, | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onDiscordOAuthClicked = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`, | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @@ -54,7 +69,7 @@ const LoginForm = () => { | |||||||
| 
 | 
 | ||||||
|   const onSubmitWeChatVerificationCode = async () => { |   const onSubmitWeChatVerificationCode = async () => { | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       `/api/oauth/wechat?code=${inputs.wechat_verification_code}` |       `/api/oauth/wechat?code=${inputs.wechat_verification_code}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -76,10 +91,18 @@ 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 === '') { | ||||||
|         username, |         showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); | ||||||
|         password, |         return; | ||||||
|       }); |       } | ||||||
|  | 
 | ||||||
|  |       const res = await API.post( | ||||||
|  |         `/api/user/login?turnstile=${turnstileToken}`, | ||||||
|  |         { | ||||||
|  |           username, | ||||||
|  |           password, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|       const { success, message, data } = res.data; |       const { success, message, data } = res.data; | ||||||
|       if (success) { |       if (success) { | ||||||
|         userDispatch({ type: 'login', payload: data }); |         userDispatch({ type: 'login', payload: data }); | ||||||
| @@ -93,69 +116,83 @@ const LoginForm = () => { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Grid textAlign="center" style={{ marginTop: '48px' }}> |     <Grid textAlign='center' style={{ marginTop: '48px' }}> | ||||||
|       <Grid.Column style={{ maxWidth: 450 }}> |       <Grid.Column style={{ maxWidth: 450 }}> | ||||||
|         <Header as="h2" color="" textAlign="center"> |         <Header as='h2' color='' textAlign='center'> | ||||||
|           <Image src={logo} /> 用户登录 |           <Image src={logo} /> 用户登录 | ||||||
|         </Header> |         </Header> | ||||||
|         <Form size="large"> |         <Form size='large'> | ||||||
|           <Segment> |           <Segment> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               fluid |               fluid | ||||||
|               icon="user" |               icon='user' | ||||||
|               iconPosition="left" |               iconPosition='left' | ||||||
|               placeholder="用户名" |               placeholder='用户名' | ||||||
|               name="username" |               name='username' | ||||||
|               value={username} |               value={username} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               fluid |               fluid | ||||||
|               icon="lock" |               icon='lock' | ||||||
|               iconPosition="left" |               iconPosition='left' | ||||||
|               placeholder="密码" |               placeholder='密码' | ||||||
|               name="password" |               name='password' | ||||||
|               type="password" |               type='password' | ||||||
|               value={password} |               value={password} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|             <Button color="" fluid size="large" onClick={handleSubmit}> |             {turnstileEnabled ? ( | ||||||
|  |               <Turnstile | ||||||
|  |                 sitekey={turnstileSiteKey} | ||||||
|  |                 onVerify={(token) => { | ||||||
|  |                   setTurnstileToken(token); | ||||||
|  |                 }} | ||||||
|  |               /> | ||||||
|  |             ) : ( | ||||||
|  |               <></> | ||||||
|  |             )} | ||||||
|  |             <Button color='' fluid size='large' onClick={handleSubmit}> | ||||||
|               登录 |               登录 | ||||||
|             </Button> |             </Button> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </Form> |         </Form> | ||||||
|         <Message> |         <Message> | ||||||
|           忘记密码? |           忘记密码? | ||||||
|           <Link to="/reset" className="btn btn-link"> |           <Link to='/reset' className='btn btn-link'> | ||||||
|             点击重置 |             点击重置 | ||||||
|           </Link> |           </Link> | ||||||
|           ; 没有账户? |           ; 没有账户? | ||||||
|           <Link to="/register" className="btn btn-link"> |           <Link to='/register' className='btn btn-link'> | ||||||
|             点击注册 |             点击注册 | ||||||
|           </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 |               <Button | ||||||
|                 circular |                 circular | ||||||
|                 color="black" |                 color='blue' | ||||||
|                 icon="github" |                 icon='discord' | ||||||
|  |                 onClick={onDiscordOAuthClicked} | ||||||
|  |               /> | ||||||
|  |             )} | ||||||
|  |             {status.github_oauth && ( | ||||||
|  |               <Button | ||||||
|  |                 circular | ||||||
|  |                 color='black' | ||||||
|  |                 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} | ||||||
|               /> |               /> | ||||||
|             ) : ( |  | ||||||
|               <></> |  | ||||||
|             )} |             )} | ||||||
|           </> |           </> | ||||||
|         ) : ( |         ) : ( | ||||||
| @@ -175,18 +212,18 @@ const LoginForm = () => { | |||||||
|                   微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) |                   微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) | ||||||
|                 </p> |                 </p> | ||||||
|               </div> |               </div> | ||||||
|               <Form size="large"> |               <Form size='large'> | ||||||
|                 <Form.Input |                 <Form.Input | ||||||
|                   fluid |                   fluid | ||||||
|                   placeholder="验证码" |                   placeholder='验证码' | ||||||
|                   name="wechat_verification_code" |                   name='wechat_verification_code' | ||||||
|                   value={inputs.wechat_verification_code} |                   value={inputs.wechat_verification_code} | ||||||
|                   onChange={handleChange} |                   onChange={handleChange} | ||||||
|                 /> |                 /> | ||||||
|                 <Button |                 <Button | ||||||
|                   color="" |                   color='' | ||||||
|                   fluid |                   fluid | ||||||
|                   size="large" |                   size='large' | ||||||
|                   onClick={onSubmitWeChatVerificationCode} |                   onClick={onSubmitWeChatVerificationCode} | ||||||
|                 > |                 > | ||||||
|                   登录 |                   登录 | ||||||
| @@ -1,21 +1,26 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Header, | ||||||
|  |   Label, | ||||||
|  |   Pagination, | ||||||
|  |   Segment, | ||||||
|  |   Select, | ||||||
|  |   Table, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { API, isAdmin, showError, timestamp2string } from '../helpers'; | import { API, isAdmin, showError, timestamp2string } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| import { ITEMS_PER_PAGE } from '../constants'; | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderQuota } from '../helpers/render'; | import { renderQuota } from '../helpers/render'; | ||||||
| 
 | 
 | ||||||
| function renderTimestamp(timestamp) { | function renderTimestamp(timestamp) { | ||||||
|   return ( |   return <>{timestamp2string(timestamp)}</>; | ||||||
|     <> |  | ||||||
|       {timestamp2string(timestamp)} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const MODE_OPTIONS = [ | const MODE_OPTIONS = [ | ||||||
|   { key: 'all', text: '全部用户', value: 'all' }, |   { key: 'all', text: '全部用户', value: 'all' }, | ||||||
|   { key: 'self', text: '当前用户', value: 'self' } |   { key: 'self', text: '当前用户', value: 'self' }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const LOG_OPTIONS = [ | const LOG_OPTIONS = [ | ||||||
| @@ -23,21 +28,46 @@ const LOG_OPTIONS = [ | |||||||
|   { key: '1', text: '充值', value: 1 }, |   { key: '1', text: '充值', value: 1 }, | ||||||
|   { key: '2', text: '消费', value: 2 }, |   { key: '2', text: '消费', value: 2 }, | ||||||
|   { key: '3', text: '管理', value: 3 }, |   { key: '3', text: '管理', value: 3 }, | ||||||
|   { key: '4', text: '系统', value: 4 } |   { key: '4', text: '系统', value: 4 }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| function renderType(type) { | function renderType(type) { | ||||||
|   switch (type) { |   switch (type) { | ||||||
|     case 1: |     case 1: | ||||||
|       return <Label basic color='green'> 充值 </Label>; |       return ( | ||||||
|  |         <Label basic color='green'> | ||||||
|  |           {' '} | ||||||
|  |           充值{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 2: |     case 2: | ||||||
|       return <Label basic color='olive'> 消费 </Label>; |       return ( | ||||||
|  |         <Label basic color='olive'> | ||||||
|  |           {' '} | ||||||
|  |           消费{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 3: |     case 3: | ||||||
|       return <Label basic color='orange'> 管理 </Label>; |       return ( | ||||||
|  |         <Label basic color='orange'> | ||||||
|  |           {' '} | ||||||
|  |           管理{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 4: |     case 4: | ||||||
|       return <Label basic color='purple'> 系统 </Label>; |       return ( | ||||||
|  |         <Label basic color='purple'> | ||||||
|  |           {' '} | ||||||
|  |           系统{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     default: |     default: | ||||||
|       return <Label basic color='black'> 未知 </Label>; |       return ( | ||||||
|  |         <Label basic color='black'> | ||||||
|  |           {' '} | ||||||
|  |           未知{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -55,13 +85,14 @@ const LogsTable = () => { | |||||||
|     token_name: '', |     token_name: '', | ||||||
|     model_name: '', |     model_name: '', | ||||||
|     start_timestamp: timestamp2string(0), |     start_timestamp: timestamp2string(0), | ||||||
|     end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) |     end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), | ||||||
|   }); |   }); | ||||||
|   const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs; |   const { username, token_name, model_name, start_timestamp, end_timestamp } = | ||||||
|  |     inputs; | ||||||
| 
 | 
 | ||||||
|   const [stat, setStat] = useState({ |   const [stat, setStat] = useState({ | ||||||
|     quota: 0, |     quota: 0, | ||||||
|     token: 0 |     token: 0, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
| @@ -71,7 +102,9 @@ const LogsTable = () => { | |||||||
|   const getLogSelfStat = async () => { |   const getLogSelfStat = async () => { | ||||||
|     let localStartTimestamp = Date.parse(start_timestamp) / 1000; |     let localStartTimestamp = Date.parse(start_timestamp) / 1000; | ||||||
|     let localEndTimestamp = Date.parse(end_timestamp) / 1000; |     let localEndTimestamp = Date.parse(end_timestamp) / 1000; | ||||||
|     let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); |     let res = await API.get( | ||||||
|  |       `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`, | ||||||
|  |     ); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       setStat(data); |       setStat(data); | ||||||
| @@ -83,7 +116,9 @@ const LogsTable = () => { | |||||||
|   const getLogStat = async () => { |   const getLogStat = async () => { | ||||||
|     let localStartTimestamp = Date.parse(start_timestamp) / 1000; |     let localStartTimestamp = Date.parse(start_timestamp) / 1000; | ||||||
|     let localEndTimestamp = Date.parse(end_timestamp) / 1000; |     let localEndTimestamp = Date.parse(end_timestamp) / 1000; | ||||||
|     let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); |     let res = await API.get( | ||||||
|  |       `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`, | ||||||
|  |     ); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       setStat(data); |       setStat(data); | ||||||
| @@ -129,7 +164,7 @@ const LogsTable = () => { | |||||||
| 
 | 
 | ||||||
|   const refresh = async () => { |   const refresh = async () => { | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     setActivePage(1) |     setActivePage(1); | ||||||
|     await loadLogs(0); |     await loadLogs(0); | ||||||
|     if (isAdminUser) { |     if (isAdminUser) { | ||||||
|       getLogStat().then(); |       getLogStat().then(); | ||||||
| @@ -169,7 +204,7 @@ const LogsTable = () => { | |||||||
|     if (logs.length === 0) return; |     if (logs.length === 0) return; | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     let sortedLogs = [...logs]; |     let sortedLogs = [...logs]; | ||||||
|     if (typeof sortedLogs[0][key] === 'string'){ |     if (typeof sortedLogs[0][key] === 'string') { | ||||||
|       sortedLogs.sort((a, b) => { |       sortedLogs.sort((a, b) => { | ||||||
|         return ('' + a[key]).localeCompare(b[key]); |         return ('' + a[key]).localeCompare(b[key]); | ||||||
|       }); |       }); | ||||||
| @@ -190,28 +225,61 @@ const LogsTable = () => { | |||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Segment> |       <Segment> | ||||||
|         <Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header> |         <Header as='h3'> | ||||||
|  |           使用明细(总消耗额度:{renderQuota(stat.quota)}) | ||||||
|  |         </Header> | ||||||
|         <Form> |         <Form> | ||||||
|           <Form.Group> |           <Form.Group> | ||||||
|             { |             {isAdminUser && ( | ||||||
|               isAdminUser && ( |               <Form.Input | ||||||
|                 <Form.Input fluid label={'用户名称'} width={2} value={username} |                 fluid | ||||||
|                             placeholder={'可选值'} name='username' |                 label={'用户名称'} | ||||||
|                             onChange={handleInputChange} /> |                 width={2} | ||||||
|               ) |                 value={username} | ||||||
|             } |                 placeholder={'可选值'} | ||||||
|             <Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name} |                 name='username' | ||||||
|                         placeholder={'可选值'} name='token_name' onChange={handleInputChange} /> |                 onChange={handleInputChange} | ||||||
|             <Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值' |               /> | ||||||
|                         name='model_name' |             )} | ||||||
|                         onChange={handleInputChange} /> |             <Form.Input | ||||||
|             <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local' |               fluid | ||||||
|                         name='start_timestamp' |               label={'令牌名称'} | ||||||
|                         onChange={handleInputChange} /> |               width={isAdminUser ? 2 : 3} | ||||||
|             <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local' |               value={token_name} | ||||||
|                         name='end_timestamp' |               placeholder={'可选值'} | ||||||
|                         onChange={handleInputChange} /> |               name='token_name' | ||||||
|             <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button> |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               fluid | ||||||
|  |               label='模型名称' | ||||||
|  |               width={isAdminUser ? 2 : 3} | ||||||
|  |               value={model_name} | ||||||
|  |               placeholder='可选值' | ||||||
|  |               name='model_name' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               fluid | ||||||
|  |               label='起始时间' | ||||||
|  |               width={4} | ||||||
|  |               value={start_timestamp} | ||||||
|  |               type='datetime-local' | ||||||
|  |               name='start_timestamp' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               fluid | ||||||
|  |               label='结束时间' | ||||||
|  |               width={4} | ||||||
|  |               value={end_timestamp} | ||||||
|  |               type='datetime-local' | ||||||
|  |               name='end_timestamp' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |             /> | ||||||
|  |             <Form.Button fluid label='操作' width={2} onClick={refresh}> | ||||||
|  |               查询 | ||||||
|  |             </Form.Button> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|         </Form> |         </Form> | ||||||
|         <Table basic compact size='small'> |         <Table basic compact size='small'> | ||||||
| @@ -226,8 +294,8 @@ const LogsTable = () => { | |||||||
|               > |               > | ||||||
|                 时间 |                 时间 | ||||||
|               </Table.HeaderCell> |               </Table.HeaderCell> | ||||||
|               { |               {isAdminUser && ( | ||||||
|                 isAdminUser && <Table.HeaderCell |                 <Table.HeaderCell | ||||||
|                   style={{ cursor: 'pointer' }} |                   style={{ cursor: 'pointer' }} | ||||||
|                   onClick={() => { |                   onClick={() => { | ||||||
|                     sortLog('username'); |                     sortLog('username'); | ||||||
| @@ -236,7 +304,7 @@ const LogsTable = () => { | |||||||
|                 > |                 > | ||||||
|                   用户 |                   用户 | ||||||
|                 </Table.HeaderCell> |                 </Table.HeaderCell> | ||||||
|               } |               )} | ||||||
|               <Table.HeaderCell |               <Table.HeaderCell | ||||||
|                 style={{ cursor: 'pointer' }} |                 style={{ cursor: 'pointer' }} | ||||||
|                 onClick={() => { |                 onClick={() => { | ||||||
| @@ -307,24 +375,42 @@ const LogsTable = () => { | |||||||
|             {logs |             {logs | ||||||
|               .slice( |               .slice( | ||||||
|                 (activePage - 1) * ITEMS_PER_PAGE, |                 (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|                 activePage * ITEMS_PER_PAGE |                 activePage * ITEMS_PER_PAGE, | ||||||
|               ) |               ) | ||||||
|               .map((log, idx) => { |               .map((log, idx) => { | ||||||
|                 if (log.deleted) return <></>; |                 if (log.deleted) return <></>; | ||||||
|                 return ( |                 return ( | ||||||
|                   <Table.Row key={log.created_at}> |                   <Table.Row key={log.created_at}> | ||||||
|                     <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> |                     <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> | ||||||
|                     { |                     {isAdminUser && ( | ||||||
|                       isAdminUser && ( |                       <Table.Cell> | ||||||
|                         <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell> |                         {log.username ? <Label>{log.username}</Label> : ''} | ||||||
|                       ) |                       </Table.Cell> | ||||||
|                     } |                     )} | ||||||
|                     <Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell> |                     <Table.Cell> | ||||||
|  |                       {log.token_name ? ( | ||||||
|  |                         <Label basic>{log.token_name}</Label> | ||||||
|  |                       ) : ( | ||||||
|  |                         '' | ||||||
|  |                       )} | ||||||
|  |                     </Table.Cell> | ||||||
|                     <Table.Cell>{renderType(log.type)}</Table.Cell> |                     <Table.Cell>{renderType(log.type)}</Table.Cell> | ||||||
|                     <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell> |                     <Table.Cell> | ||||||
|                     <Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell> |                       {log.model_name ? ( | ||||||
|                     <Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell> |                         <Label basic>{log.model_name}</Label> | ||||||
|                     <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell> |                       ) : ( | ||||||
|  |                         '' | ||||||
|  |                       )} | ||||||
|  |                     </Table.Cell> | ||||||
|  |                     <Table.Cell> | ||||||
|  |                       {log.prompt_tokens ? log.prompt_tokens : ''} | ||||||
|  |                     </Table.Cell> | ||||||
|  |                     <Table.Cell> | ||||||
|  |                       {log.completion_tokens ? log.completion_tokens : ''} | ||||||
|  |                     </Table.Cell> | ||||||
|  |                     <Table.Cell> | ||||||
|  |                       {log.quota ? renderQuota(log.quota, 6) : ''} | ||||||
|  |                     </Table.Cell> | ||||||
|                     <Table.Cell>{log.content}</Table.Cell> |                     <Table.Cell>{log.content}</Table.Cell> | ||||||
|                   </Table.Row> |                   </Table.Row> | ||||||
|                 ); |                 ); | ||||||
| @@ -344,7 +430,9 @@ const LogsTable = () => { | |||||||
|                     setLogType(value); |                     setLogType(value); | ||||||
|                   }} |                   }} | ||||||
|                 /> |                 /> | ||||||
|                 <Button size='small' onClick={refresh} loading={loading}>刷新</Button> |                 <Button size='small' onClick={refresh} loading={loading}> | ||||||
|  |                   刷新 | ||||||
|  |                 </Button> | ||||||
|                 <Pagination |                 <Pagination | ||||||
|                   floated='right' |                   floated='right' | ||||||
|                   activePage={activePage} |                   activePage={activePage} | ||||||
| @@ -54,7 +54,7 @@ const OperationSetting = () => { | |||||||
|     } |     } | ||||||
|     const res = await API.put('/api/option/', { |     const res = await API.put('/api/option/', { | ||||||
|       key, |       key, | ||||||
|       value |       value, | ||||||
|     }); |     }); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -76,11 +76,22 @@ const OperationSetting = () => { | |||||||
|   const submitConfig = async (group) => { |   const submitConfig = async (group) => { | ||||||
|     switch (group) { |     switch (group) { | ||||||
|       case 'monitor': |       case 'monitor': | ||||||
|         if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) { |         if ( | ||||||
|           await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold); |           originInputs['ChannelDisableThreshold'] !== | ||||||
|  |           inputs.ChannelDisableThreshold | ||||||
|  |         ) { | ||||||
|  |           await updateOption( | ||||||
|  |             'ChannelDisableThreshold', | ||||||
|  |             inputs.ChannelDisableThreshold, | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|         if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { |         if ( | ||||||
|           await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); |           originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold | ||||||
|  |         ) { | ||||||
|  |           await updateOption( | ||||||
|  |             'QuotaRemindThreshold', | ||||||
|  |             inputs.QuotaRemindThreshold, | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case 'ratio': |       case 'ratio': | ||||||
| @@ -134,9 +145,7 @@ const OperationSetting = () => { | |||||||
|     <Grid columns={1}> |     <Grid columns={1}> | ||||||
|       <Grid.Column> |       <Grid.Column> | ||||||
|         <Form loading={loading}> |         <Form loading={loading}> | ||||||
|           <Header as='h3'> |           <Header as='h3'>通用设置</Header> | ||||||
|             通用设置 |  | ||||||
|           </Header> |  | ||||||
|           <Form.Group widths={4}> |           <Form.Group widths={4}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='充值链接' |               label='充值链接' | ||||||
| @@ -204,13 +213,15 @@ const OperationSetting = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={() => { |           <Form.Button | ||||||
|             submitConfig('general').then(); |             onClick={() => { | ||||||
|           }}>保存通用设置</Form.Button> |               submitConfig('general').then(); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             保存通用设置 | ||||||
|  |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'>监控设置</Header> | ||||||
|             监控设置 |  | ||||||
|           </Header> |  | ||||||
|           <Form.Group widths={3}> |           <Form.Group widths={3}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='最长响应时间' |               label='最长响应时间' | ||||||
| @@ -241,13 +252,15 @@ const OperationSetting = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={() => { |           <Form.Button | ||||||
|             submitConfig('monitor').then(); |             onClick={() => { | ||||||
|           }}>保存监控设置</Form.Button> |               submitConfig('monitor').then(); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             保存监控设置 | ||||||
|  |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'>额度设置</Header> | ||||||
|             额度设置 |  | ||||||
|           </Header> |  | ||||||
|           <Form.Group widths={4}> |           <Form.Group widths={4}> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='新用户初始额度' |               label='新用户初始额度' | ||||||
| @@ -290,13 +303,15 @@ const OperationSetting = () => { | |||||||
|               placeholder='例如:1000' |               placeholder='例如:1000' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={() => { |           <Form.Button | ||||||
|             submitConfig('quota').then(); |             onClick={() => { | ||||||
|           }}>保存额度设置</Form.Button> |               submitConfig('quota').then(); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             保存额度设置 | ||||||
|  |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'>倍率设置</Header> | ||||||
|             倍率设置 |  | ||||||
|           </Header> |  | ||||||
|           <Form.Group widths='equal'> |           <Form.Group widths='equal'> | ||||||
|             <Form.TextArea |             <Form.TextArea | ||||||
|               label='模型倍率' |               label='模型倍率' | ||||||
| @@ -319,9 +334,13 @@ const OperationSetting = () => { | |||||||
|               placeholder='为一个 JSON 文本,键为分组名称,值为倍率' |               placeholder='为一个 JSON 文本,键为分组名称,值为倍率' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={() => { |           <Form.Button | ||||||
|             submitConfig('ratio').then(); |             onClick={() => { | ||||||
|           }}>保存倍率设置</Form.Button> |               submitConfig('ratio').then(); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             保存倍率设置 | ||||||
|  |           </Form.Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Grid.Column> |       </Grid.Column> | ||||||
|     </Grid> |     </Grid> | ||||||
| @@ -1,5 +1,13 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Divider, | ||||||
|  |   Form, | ||||||
|  |   Grid, | ||||||
|  |   Header, | ||||||
|  |   Message, | ||||||
|  |   Modal, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { API, showError, showSuccess } from '../helpers'; | import { API, showError, showSuccess } from '../helpers'; | ||||||
| import { marked } from 'marked'; | import { marked } from 'marked'; | ||||||
| 
 | 
 | ||||||
| @@ -10,13 +18,13 @@ const OtherSetting = () => { | |||||||
|     About: '', |     About: '', | ||||||
|     SystemName: '', |     SystemName: '', | ||||||
|     Logo: '', |     Logo: '', | ||||||
|     HomePageContent: '' |     HomePageContent: '', | ||||||
|   }); |   }); | ||||||
|   let [loading, setLoading] = useState(false); |   let [loading, setLoading] = useState(false); | ||||||
|   const [showUpdateModal, setShowUpdateModal] = useState(false); |   const [showUpdateModal, setShowUpdateModal] = useState(false); | ||||||
|   const [updateData, setUpdateData] = useState({ |   const [updateData, setUpdateData] = useState({ | ||||||
|     tag_name: '', |     tag_name: '', | ||||||
|     content: '' |     content: '', | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const getOptions = async () => { |   const getOptions = async () => { | ||||||
| @@ -43,7 +51,7 @@ const OtherSetting = () => { | |||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.put('/api/option/', { |     const res = await API.put('/api/option/', { | ||||||
|       key, |       key, | ||||||
|       value |       value, | ||||||
|     }); |     }); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -83,21 +91,20 @@ const OtherSetting = () => { | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const openGitHubRelease = () => { |   const openGitHubRelease = () => { | ||||||
|     window.location = |     window.location = 'https://github.com/songquanpeng/one-api/releases/latest'; | ||||||
|       'https://github.com/songquanpeng/one-api/releases/latest'; |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const checkUpdate = async () => { |   const checkUpdate = async () => { | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       'https://api.github.com/repos/songquanpeng/one-api/releases/latest' |       'https://api.github.com/repos/songquanpeng/one-api/releases/latest', | ||||||
|     ); |     ); | ||||||
|     const { tag_name, body } = res.data; |     const { tag_name, body } = res.data; | ||||||
|     if (tag_name === process.env.REACT_APP_VERSION) { |     if (tag_name === import.meta.env.VITE_REACT_APP_VERSION) { | ||||||
|       showSuccess(`已是最新版本:${tag_name}`); |       showSuccess(`已是最新版本:${tag_name}`); | ||||||
|     } else { |     } else { | ||||||
|       setUpdateData({ |       setUpdateData({ | ||||||
|         tag_name: tag_name, |         tag_name: tag_name, | ||||||
|         content: marked.parse(body) |         content: marked.parse(body), | ||||||
|       }); |       }); | ||||||
|       setShowUpdateModal(true); |       setShowUpdateModal(true); | ||||||
|     } |     } | ||||||
| @@ -153,7 +160,9 @@ const OtherSetting = () => { | |||||||
|               style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} |               style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button> |           <Form.Button onClick={() => submitOption('HomePageContent')}> | ||||||
|  |             保存首页内容 | ||||||
|  |           </Form.Button> | ||||||
|           <Form.Group widths='equal'> |           <Form.Group widths='equal'> | ||||||
|             <Form.TextArea |             <Form.TextArea | ||||||
|               label='关于' |               label='关于' | ||||||
| @@ -165,7 +174,10 @@ const OtherSetting = () => { | |||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Form.Button onClick={submitAbout}>保存关于</Form.Button> |           <Form.Button onClick={submitAbout}>保存关于</Form.Button> | ||||||
|           <Message>移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。</Message> |           <Message> | ||||||
|  |             移除 One API | ||||||
|  |             的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。 | ||||||
|  |           </Message> | ||||||
|           <Form.Group widths='equal'> |           <Form.Group widths='equal'> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='页脚' |               label='页脚' | ||||||
| @@ -1,6 +1,13 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'; | import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'; | ||||||
| import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | import { | ||||||
|  |   API, | ||||||
|  |   copy, | ||||||
|  |   showError, | ||||||
|  |   showInfo, | ||||||
|  |   showNotice, | ||||||
|  |   showSuccess, | ||||||
|  | } from '../helpers'; | ||||||
| import { useSearchParams } from 'react-router-dom'; | import { useSearchParams } from 'react-router-dom'; | ||||||
| 
 | 
 | ||||||
| const PasswordResetConfirm = () => { | const PasswordResetConfirm = () => { | ||||||
| @@ -38,7 +38,7 @@ const PasswordResetForm = () => { | |||||||
|     } |     } | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       `/api/reset_password?email=${email}&turnstile=${turnstileToken}` |       `/api/reset_password?email=${email}&turnstile=${turnstileToken}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -1,18 +1,40 @@ | |||||||
| 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 { | ||||||
| import { Link } from 'react-router-dom'; |   Button, | ||||||
| import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; |   Divider, | ||||||
|  |   Form, | ||||||
|  |   Header, | ||||||
|  |   Image, | ||||||
|  |   Message, | ||||||
|  |   Modal, | ||||||
|  |   Label, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
|  | import { Link, useNavigate } from 'react-router-dom'; | ||||||
|  | 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,10 +79,30 @@ 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( | ||||||
|       `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}` |       `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -73,7 +115,13 @@ const PersonalSetting = () => { | |||||||
| 
 | 
 | ||||||
|   const openGitHubOAuth = () => { |   const openGitHubOAuth = () => { | ||||||
|     window.open( |     window.open( | ||||||
|       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` |       `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`, | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const openDiscordOAuth = () => { | ||||||
|  |     window.open( | ||||||
|  |       `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`, | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @@ -85,7 +133,7 @@ const PersonalSetting = () => { | |||||||
|     } |     } | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` |       `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -100,7 +148,7 @@ const PersonalSetting = () => { | |||||||
|     if (inputs.email_verification_code === '') return; |     if (inputs.email_verification_code === '') return; | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}` |       `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -116,26 +164,33 @@ const PersonalSetting = () => { | |||||||
|     <div style={{ lineHeight: '40px' }}> |     <div style={{ lineHeight: '40px' }}> | ||||||
|       <Header as='h3'>通用设置</Header> |       <Header as='h3'>通用设置</Header> | ||||||
|       <Message> |       <Message> | ||||||
|         注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。 |         注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI | ||||||
|  |         相关的服务,请知悉。 | ||||||
|       </Message> |       </Message> | ||||||
|       <Button as={Link} to={`/user/edit/`}> |       <Button as={Link} to={`/user/edit/`}> | ||||||
|         更新个人信息 |         更新个人信息 | ||||||
|       </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> | ||||||
|       { |       {status.wechat_login && ( | ||||||
|         status.wechat_login && ( |         <Button | ||||||
|           <Button |           onClick={() => { | ||||||
|             onClick={() => { |             setShowWeChatBindModal(true); | ||||||
|               setShowWeChatBindModal(true); |           }} | ||||||
|             }} |         > | ||||||
|           > |           绑定微信账号 | ||||||
|             绑定微信账号 |         </Button> | ||||||
|           </Button> |       )} | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|       <Modal |       <Modal | ||||||
|         onClose={() => setShowWeChatBindModal(false)} |         onClose={() => setShowWeChatBindModal(false)} | ||||||
|         onOpen={() => setShowWeChatBindModal(true)} |         onOpen={() => setShowWeChatBindModal(true)} | ||||||
| @@ -165,11 +220,12 @@ const PersonalSetting = () => { | |||||||
|           </Modal.Description> |           </Modal.Description> | ||||||
|         </Modal.Content> |         </Modal.Content> | ||||||
|       </Modal> |       </Modal> | ||||||
|       { |       {status.github_oauth && ( | ||||||
|         status.github_oauth && ( |         <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 +286,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> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @@ -2,7 +2,6 @@ import { Navigate } from 'react-router-dom'; | |||||||
| 
 | 
 | ||||||
| import { history } from '../helpers'; | import { history } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| function PrivateRoute({ children }) { | function PrivateRoute({ children }) { | ||||||
|   if (!localStorage.getItem('user')) { |   if (!localStorage.getItem('user')) { | ||||||
|     return <Navigate to='/login' state={{ from: history.location }} />; |     return <Navigate to='/login' state={{ from: history.location }} />; | ||||||
| @@ -10,4 +9,4 @@ function PrivateRoute({ children }) { | |||||||
|   return children; |   return children; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export { PrivateRoute }; | export { PrivateRoute }; | ||||||
| @@ -1,29 +1,59 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Label, | ||||||
|  |   Message, | ||||||
|  |   Pagination, | ||||||
|  |   Table, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers'; | import { | ||||||
|  |   API, | ||||||
|  |   copy, | ||||||
|  |   showError, | ||||||
|  |   showInfo, | ||||||
|  |   showSuccess, | ||||||
|  |   showWarning, | ||||||
|  |   timestamp2string, | ||||||
|  | } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| import { ITEMS_PER_PAGE } from '../constants'; | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderQuota } from '../helpers/render'; | import { renderQuota } from '../helpers/render'; | ||||||
| 
 | 
 | ||||||
| function renderTimestamp(timestamp) { | function renderTimestamp(timestamp) { | ||||||
|   return ( |   return <>{timestamp2string(timestamp)}</>; | ||||||
|     <> |  | ||||||
|       {timestamp2string(timestamp)} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function renderStatus(status) { | function renderStatus(status) { | ||||||
|   switch (status) { |   switch (status) { | ||||||
|     case 1: |     case 1: | ||||||
|       return <Label basic color='green'>未使用</Label>; |       return ( | ||||||
|  |         <Label basic color='green'> | ||||||
|  |           未使用 | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 2: |     case 2: | ||||||
|       return <Label basic color='red'> 已禁用 </Label>; |       return ( | ||||||
|  |         <Label basic color='red'> | ||||||
|  |           {' '} | ||||||
|  |           已禁用{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 3: |     case 3: | ||||||
|       return <Label basic color='grey'> 已使用 </Label>; |       return ( | ||||||
|  |         <Label basic color='grey'> | ||||||
|  |           {' '} | ||||||
|  |           已使用{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     default: |     default: | ||||||
|       return <Label basic color='black'> 未知状态 </Label>; |       return ( | ||||||
|  |         <Label basic color='black'> | ||||||
|  |           {' '} | ||||||
|  |           未知状态{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -110,7 +140,9 @@ const RedemptionsTable = () => { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     setSearching(true); |     setSearching(true); | ||||||
|     const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`); |     const res = await API.get( | ||||||
|  |       `/api/redemption/search?keyword=${searchKeyword}`, | ||||||
|  |     ); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       setRedemptions(data); |       setRedemptions(data); | ||||||
| @@ -212,18 +244,26 @@ const RedemptionsTable = () => { | |||||||
|           {redemptions |           {redemptions | ||||||
|             .slice( |             .slice( | ||||||
|               (activePage - 1) * ITEMS_PER_PAGE, |               (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|               activePage * ITEMS_PER_PAGE |               activePage * ITEMS_PER_PAGE, | ||||||
|             ) |             ) | ||||||
|             .map((redemption, idx) => { |             .map((redemption, idx) => { | ||||||
|               if (redemption.deleted) return <></>; |               if (redemption.deleted) return <></>; | ||||||
|               return ( |               return ( | ||||||
|                 <Table.Row key={redemption.id}> |                 <Table.Row key={redemption.id}> | ||||||
|                   <Table.Cell>{redemption.id}</Table.Cell> |                   <Table.Cell>{redemption.id}</Table.Cell> | ||||||
|                   <Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell> |                   <Table.Cell> | ||||||
|  |                     {redemption.name ? redemption.name : '无'} | ||||||
|  |                   </Table.Cell> | ||||||
|                   <Table.Cell>{renderStatus(redemption.status)}</Table.Cell> |                   <Table.Cell>{renderStatus(redemption.status)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell> |                   <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell> |                   <Table.Cell> | ||||||
|                   <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell> |                     {renderTimestamp(redemption.created_time)} | ||||||
|  |                   </Table.Cell> | ||||||
|  |                   <Table.Cell> | ||||||
|  |                     {redemption.redeemed_time | ||||||
|  |                       ? renderTimestamp(redemption.redeemed_time) | ||||||
|  |                       : '尚未兑换'}{' '} | ||||||
|  |                   </Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <div> |                     <div> | ||||||
|                       <Button |                       <Button | ||||||
| @@ -233,7 +273,9 @@ const RedemptionsTable = () => { | |||||||
|                           if (await copy(redemption.key)) { |                           if (await copy(redemption.key)) { | ||||||
|                             showSuccess('已复制到剪贴板!'); |                             showSuccess('已复制到剪贴板!'); | ||||||
|                           } else { |                           } else { | ||||||
|                             showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。') |                             showWarning( | ||||||
|  |                               '无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。', | ||||||
|  |                             ); | ||||||
|                             setSearchKeyword(redemption.key); |                             setSearchKeyword(redemption.key); | ||||||
|                           } |                           } | ||||||
|                         }} |                         }} | ||||||
| @@ -251,12 +293,12 @@ const RedemptionsTable = () => { | |||||||
|                       </Button> |                       </Button> | ||||||
|                       <Button |                       <Button | ||||||
|                         size={'small'} |                         size={'small'} | ||||||
|                         disabled={redemption.status === 3}  // used |                         disabled={redemption.status === 3} // used | ||||||
|                         onClick={() => { |                         onClick={() => { | ||||||
|                           manageRedemption( |                           manageRedemption( | ||||||
|                             redemption.id, |                             redemption.id, | ||||||
|                             redemption.status === 1 ? 'disable' : 'enable', |                             redemption.status === 1 ? 'disable' : 'enable', | ||||||
|                             idx |                             idx, | ||||||
|                           ); |                           ); | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
| @@ -279,7 +321,12 @@ const RedemptionsTable = () => { | |||||||
|         <Table.Footer> |         <Table.Footer> | ||||||
|           <Table.Row> |           <Table.Row> | ||||||
|             <Table.HeaderCell colSpan='8'> |             <Table.HeaderCell colSpan='8'> | ||||||
|               <Button size='small' as={Link} to='/redemption/add' loading={loading}> |               <Button | ||||||
|  |                 size='small' | ||||||
|  |                 as={Link} | ||||||
|  |                 to='/redemption/add' | ||||||
|  |                 loading={loading} | ||||||
|  |               > | ||||||
|                 添加新的兑换码 |                 添加新的兑换码 | ||||||
|               </Button> |               </Button> | ||||||
|               <Pagination |               <Pagination | ||||||
| @@ -73,7 +73,7 @@ const RegisterForm = () => { | |||||||
|       inputs.aff_code = affCode; |       inputs.aff_code = affCode; | ||||||
|       const res = await API.post( |       const res = await API.post( | ||||||
|         `/api/user/register?turnstile=${turnstileToken}`, |         `/api/user/register?turnstile=${turnstileToken}`, | ||||||
|         inputs |         inputs, | ||||||
|       ); |       ); | ||||||
|       const { success, message } = res.data; |       const { success, message } = res.data; | ||||||
|       if (success) { |       if (success) { | ||||||
| @@ -94,7 +94,7 @@ const RegisterForm = () => { | |||||||
|     } |     } | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
|       `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` |       `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, | ||||||
|     ); |     ); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -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': | ||||||
| @@ -66,7 +70,7 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|     const res = await API.put('/api/option/', { |     const res = await API.put('/api/option/', { | ||||||
|       key, |       key, | ||||||
|       value |       value, | ||||||
|     }); |     }); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -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' || | ||||||
| @@ -129,7 +135,7 @@ const SystemSetting = () => { | |||||||
|     if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { |     if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { | ||||||
|       await updateOption( |       await updateOption( | ||||||
|         'WeChatServerAddress', |         'WeChatServerAddress', | ||||||
|         removeTrailingSlash(inputs.WeChatServerAddress) |         removeTrailingSlash(inputs.WeChatServerAddress), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     if ( |     if ( | ||||||
| @@ -138,7 +144,7 @@ const SystemSetting = () => { | |||||||
|     ) { |     ) { | ||||||
|       await updateOption( |       await updateOption( | ||||||
|         'WeChatAccountQRCodeImageURL', |         'WeChatAccountQRCodeImageURL', | ||||||
|         inputs.WeChatAccountQRCodeImageURL |         inputs.WeChatAccountQRCodeImageURL, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     if ( |     if ( | ||||||
| @@ -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,125 @@ 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 +379,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 +388,66 @@ 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 +457,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 +466,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> | ||||||
| @@ -1,31 +1,66 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Label, | ||||||
|  |   Modal, | ||||||
|  |   Pagination, | ||||||
|  |   Popup, | ||||||
|  |   Table, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; | import { | ||||||
|  |   API, | ||||||
|  |   copy, | ||||||
|  |   showError, | ||||||
|  |   showSuccess, | ||||||
|  |   showWarning, | ||||||
|  |   timestamp2string, | ||||||
|  | } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| import { ITEMS_PER_PAGE } from '../constants'; | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderQuota } from '../helpers/render'; | import { renderQuota } from '../helpers/render'; | ||||||
| 
 | 
 | ||||||
| function renderTimestamp(timestamp) { | function renderTimestamp(timestamp) { | ||||||
|   return ( |   return <>{timestamp2string(timestamp)}</>; | ||||||
|     <> |  | ||||||
|       {timestamp2string(timestamp)} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function renderStatus(status) { | function renderStatus(status) { | ||||||
|   switch (status) { |   switch (status) { | ||||||
|     case 1: |     case 1: | ||||||
|       return <Label basic color='green'>已启用</Label>; |       return ( | ||||||
|  |         <Label basic color='green'> | ||||||
|  |           已启用 | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 2: |     case 2: | ||||||
|       return <Label basic color='red'> 已禁用 </Label>; |       return ( | ||||||
|  |         <Label basic color='red'> | ||||||
|  |           {' '} | ||||||
|  |           已禁用{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 3: |     case 3: | ||||||
|       return <Label basic color='yellow'> 已过期 </Label>; |       return ( | ||||||
|  |         <Label basic color='yellow'> | ||||||
|  |           {' '} | ||||||
|  |           已过期{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     case 4: |     case 4: | ||||||
|       return <Label basic color='grey'> 已耗尽 </Label>; |       return ( | ||||||
|  |         <Label basic color='grey'> | ||||||
|  |           {' '} | ||||||
|  |           已耗尽{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|     default: |     default: | ||||||
|       return <Label basic color='black'> 未知状态 </Label>; |       return ( | ||||||
|  |         <Label basic color='black'> | ||||||
|  |           {' '} | ||||||
|  |           未知状态{' '} | ||||||
|  |         </Label> | ||||||
|  |       ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -68,7 +103,7 @@ const TokensTable = () => { | |||||||
|   const refresh = async () => { |   const refresh = async () => { | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     await loadTokens(activePage - 1); |     await loadTokens(activePage - 1); | ||||||
|   } |   }; | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     loadTokens(0) |     loadTokens(0) | ||||||
| @@ -221,7 +256,7 @@ const TokensTable = () => { | |||||||
|           {tokens |           {tokens | ||||||
|             .slice( |             .slice( | ||||||
|               (activePage - 1) * ITEMS_PER_PAGE, |               (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|               activePage * ITEMS_PER_PAGE |               activePage * ITEMS_PER_PAGE, | ||||||
|             ) |             ) | ||||||
|             .map((token, idx) => { |             .map((token, idx) => { | ||||||
|               if (token.deleted) return <></>; |               if (token.deleted) return <></>; | ||||||
| @@ -230,20 +265,30 @@ const TokensTable = () => { | |||||||
|                   <Table.Cell>{token.name ? token.name : '无'}</Table.Cell> |                   <Table.Cell>{token.name ? token.name : '无'}</Table.Cell> | ||||||
|                   <Table.Cell>{renderStatus(token.status)}</Table.Cell> |                   <Table.Cell>{renderStatus(token.status)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell> |                   <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell> | ||||||
|                   <Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell> |                   <Table.Cell> | ||||||
|  |                     {token.unlimited_quota | ||||||
|  |                       ? '无限制' | ||||||
|  |                       : renderQuota(token.remain_quota, 2)} | ||||||
|  |                   </Table.Cell> | ||||||
|                   <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell> |                   <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell> | ||||||
|                   <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> |                   <Table.Cell> | ||||||
|  |                     {token.expired_time === -1 | ||||||
|  |                       ? '永不过期' | ||||||
|  |                       : renderTimestamp(token.expired_time)} | ||||||
|  |                   </Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <div> |                     <div> | ||||||
|                       <Button |                       <Button | ||||||
|                         size={'small'} |                         size={'small'} | ||||||
|                         positive |                         positive | ||||||
|                         onClick={async () => { |                         onClick={async () => { | ||||||
|                           let key = "sk-" + token.key; |                           let key = 'sk-' + token.key; | ||||||
|                           if (await copy(key)) { |                           if (await copy(key)) { | ||||||
|                             showSuccess('已复制到剪贴板!'); |                             showSuccess('已复制到剪贴板!'); | ||||||
|                           } else { |                           } else { | ||||||
|                             showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); |                             showWarning( | ||||||
|  |                               '无法复制到剪贴板,请手动复制,已将令牌填入搜索框。', | ||||||
|  |                             ); | ||||||
|                             setSearchKeyword(key); |                             setSearchKeyword(key); | ||||||
|                           } |                           } | ||||||
|                         }} |                         }} | ||||||
| @@ -275,7 +320,7 @@ const TokensTable = () => { | |||||||
|                           manageToken( |                           manageToken( | ||||||
|                             token.id, |                             token.id, | ||||||
|                             token.status === 1 ? 'disable' : 'enable', |                             token.status === 1 ? 'disable' : 'enable', | ||||||
|                             idx |                             idx, | ||||||
|                           ); |                           ); | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
| @@ -301,7 +346,9 @@ const TokensTable = () => { | |||||||
|               <Button size='small' as={Link} to='/token/add' loading={loading}> |               <Button size='small' as={Link} to='/token/add' loading={loading}> | ||||||
|                 添加新的令牌 |                 添加新的令牌 | ||||||
|               </Button> |               </Button> | ||||||
|               <Button size='small' onClick={refresh} loading={loading}>刷新</Button> |               <Button size='small' onClick={refresh} loading={loading}> | ||||||
|  |                 刷新 | ||||||
|  |               </Button> | ||||||
|               <Pagination |               <Pagination | ||||||
|                 floated='right' |                 floated='right' | ||||||
|                 activePage={activePage} |                 activePage={activePage} | ||||||
| @@ -1,10 +1,22 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Label, | ||||||
|  |   Pagination, | ||||||
|  |   Popup, | ||||||
|  |   Table, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, showError, showSuccess } from '../helpers'; | import { API, showError, showSuccess } from '../helpers'; | ||||||
| 
 | 
 | ||||||
| import { ITEMS_PER_PAGE } from '../constants'; | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render'; | import { | ||||||
|  |   renderGroup, | ||||||
|  |   renderNumber, | ||||||
|  |   renderQuota, | ||||||
|  |   renderText, | ||||||
|  | } from '../helpers/render'; | ||||||
| 
 | 
 | ||||||
| function renderRole(role) { | function renderRole(role) { | ||||||
|   switch (role) { |   switch (role) { | ||||||
| @@ -65,7 +77,7 @@ const UsersTable = () => { | |||||||
|     (async () => { |     (async () => { | ||||||
|       const res = await API.post('/api/user/manage', { |       const res = await API.post('/api/user/manage', { | ||||||
|         username, |         username, | ||||||
|         action |         action, | ||||||
|       }); |       }); | ||||||
|       const { success, message } = res.data; |       const { success, message } = res.data; | ||||||
|       if (success) { |       if (success) { | ||||||
| @@ -215,7 +227,7 @@ const UsersTable = () => { | |||||||
|           {users |           {users | ||||||
|             .slice( |             .slice( | ||||||
|               (activePage - 1) * ITEMS_PER_PAGE, |               (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|               activePage * ITEMS_PER_PAGE |               activePage * ITEMS_PER_PAGE, | ||||||
|             ) |             ) | ||||||
|             .map((user, idx) => { |             .map((user, idx) => { | ||||||
|               if (user.deleted) return <></>; |               if (user.deleted) return <></>; | ||||||
| @@ -226,7 +238,9 @@ const UsersTable = () => { | |||||||
|                     <Popup |                     <Popup | ||||||
|                       content={user.email ? user.email : '未绑定邮箱地址'} |                       content={user.email ? user.email : '未绑定邮箱地址'} | ||||||
|                       key={user.username} |                       key={user.username} | ||||||
|                       header={user.display_name ? user.display_name : user.username} |                       header={ | ||||||
|  |                         user.display_name ? user.display_name : user.username | ||||||
|  |                       } | ||||||
|                       trigger={<span>{renderText(user.username, 10)}</span>} |                       trigger={<span>{renderText(user.username, 10)}</span>} | ||||||
|                       hoverable |                       hoverable | ||||||
|                     /> |                     /> | ||||||
| @@ -236,9 +250,22 @@ const UsersTable = () => { | |||||||
|                   {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/} |                   {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/} | ||||||
|                   {/*</Table.Cell>*/} |                   {/*</Table.Cell>*/} | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} /> |                     <Popup | ||||||
|                     <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} /> |                       content='剩余额度' | ||||||
|                     <Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} /> |                       trigger={<Label basic>{renderQuota(user.quota)}</Label>} | ||||||
|  |                     /> | ||||||
|  |                     <Popup | ||||||
|  |                       content='已用额度' | ||||||
|  |                       trigger={ | ||||||
|  |                         <Label basic>{renderQuota(user.used_quota)}</Label> | ||||||
|  |                       } | ||||||
|  |                     /> | ||||||
|  |                     <Popup | ||||||
|  |                       content='请求次数' | ||||||
|  |                       trigger={ | ||||||
|  |                         <Label basic>{renderNumber(user.request_count)}</Label> | ||||||
|  |                       } | ||||||
|  |                     /> | ||||||
|                   </Table.Cell> |                   </Table.Cell> | ||||||
|                   <Table.Cell>{renderRole(user.role)}</Table.Cell> |                   <Table.Cell>{renderRole(user.role)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderStatus(user.status)}</Table.Cell> |                   <Table.Cell>{renderStatus(user.status)}</Table.Cell> | ||||||
| @@ -266,7 +293,11 @@ const UsersTable = () => { | |||||||
|                       </Button> |                       </Button> | ||||||
|                       <Popup |                       <Popup | ||||||
|                         trigger={ |                         trigger={ | ||||||
|                           <Button size='small' negative disabled={user.role === 100}> |                           <Button | ||||||
|  |                             size='small' | ||||||
|  |                             negative | ||||||
|  |                             disabled={user.role === 100} | ||||||
|  |                           > | ||||||
|                             删除 |                             删除 | ||||||
|                           </Button> |                           </Button> | ||||||
|                         } |                         } | ||||||
| @@ -289,7 +320,7 @@ const UsersTable = () => { | |||||||
|                           manageUser( |                           manageUser( | ||||||
|                             user.username, |                             user.username, | ||||||
|                             user.status === 1 ? 'disable' : 'enable', |                             user.status === 1 ? 'disable' : 'enable', | ||||||
|                             idx |                             idx, | ||||||
|                           ); |                           ); | ||||||
|                         }} |                         }} | ||||||
|                         disabled={user.role === 100} |                         disabled={user.role === 100} | ||||||
| @@ -10,5 +10,9 @@ export const CHANNEL_OPTIONS = [ | |||||||
|   { key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, |   { key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, | ||||||
|   { key: 10, text: 'AI Proxy', value: 10, color: 'purple' }, |   { key: 10, text: 'AI Proxy', value: 10, color: 'purple' }, | ||||||
|   { key: 12, text: 'API2GPT', value: 12, color: 'blue' }, |   { key: 12, text: 'API2GPT', value: 12, color: 'blue' }, | ||||||
|   { key: 13, text: 'AIGC2D', value: 13, color: 'purple' } |   { key: 13, text: 'AIGC2D', value: 13, color: 'purple' }, | ||||||
| ]; | 
 | ||||||
|  |   // | ||||||
|  |   { key: 14, text: 'Chanzhaoyu/chatgpt-web', value: 14, color: 'purple' }, | ||||||
|  |   { key: 14, text: 'mckaywrigley/chatbot-ui', value: 15, color: 'orange' }, | ||||||
|  | ]; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| export * from './toast.constants'; | export * from './toast.constants'; | ||||||
| export * from './user.constants'; | export * from './user.constants'; | ||||||
| export * from './common.constant'; | export * from './common.constant'; | ||||||
| export * from './channel.constants'; | export * from './channel.constants'; | ||||||
| @@ -3,5 +3,5 @@ export const toastConstants = { | |||||||
|   INFO_TIMEOUT: 3000, |   INFO_TIMEOUT: 3000, | ||||||
|   ERROR_TIMEOUT: 5000, |   ERROR_TIMEOUT: 5000, | ||||||
|   WARNING_TIMEOUT: 10000, |   WARNING_TIMEOUT: 10000, | ||||||
|   NOTICE_TIMEOUT: 20000 |   NOTICE_TIMEOUT: 20000, | ||||||
| }; | }; | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| export const userConstants = { |  | ||||||
|     REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', |  | ||||||
|     REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', |  | ||||||
|     REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', |  | ||||||
|  |  | ||||||
|     LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', |  | ||||||
|     LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', |  | ||||||
|     LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', |  | ||||||
|      |  | ||||||
|     LOGOUT: 'USERS_LOGOUT', |  | ||||||
|  |  | ||||||
|     GETALL_REQUEST: 'USERS_GETALL_REQUEST', |  | ||||||
|     GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', |  | ||||||
|     GETALL_FAILURE: 'USERS_GETALL_FAILURE', |  | ||||||
|  |  | ||||||
|     DELETE_REQUEST: 'USERS_DELETE_REQUEST', |  | ||||||
|     DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', |  | ||||||
|     DELETE_FAILURE: 'USERS_DELETE_FAILURE'     |  | ||||||
| }; |  | ||||||
							
								
								
									
										19
									
								
								web/src/constants/user.constants.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								web/src/constants/user.constants.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | export const userConstants = { | ||||||
|  |   REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', | ||||||
|  |   REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', | ||||||
|  |   REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', | ||||||
|  |  | ||||||
|  |   LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', | ||||||
|  |   LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', | ||||||
|  |   LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', | ||||||
|  |  | ||||||
|  |   LOGOUT: 'USERS_LOGOUT', | ||||||
|  |  | ||||||
|  |   GETALL_REQUEST: 'USERS_GETALL_REQUEST', | ||||||
|  |   GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', | ||||||
|  |   GETALL_FAILURE: 'USERS_GETALL_FAILURE', | ||||||
|  |  | ||||||
|  |   DELETE_REQUEST: 'USERS_DELETE_REQUEST', | ||||||
|  |   DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', | ||||||
|  |   DELETE_FAILURE: 'USERS_DELETE_FAILURE', | ||||||
|  | }; | ||||||
| @@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => { | |||||||
|       {children} |       {children} | ||||||
|     </StatusContext.Provider> |     </StatusContext.Provider> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @@ -1,19 +1,19 @@ | |||||||
| // contexts/User/index.jsx | // contexts/User/index.jsx | ||||||
| 
 | 
 | ||||||
| import React from "react" | import React from 'react'; | ||||||
| import { reducer, initialState } from "./reducer" | import { reducer, initialState } from './reducer'; | ||||||
| 
 | 
 | ||||||
| export const UserContext = React.createContext({ | export const UserContext = React.createContext({ | ||||||
|   state: initialState, |   state: initialState, | ||||||
|   dispatch: () => null |   dispatch: () => null, | ||||||
| }) | }); | ||||||
| 
 | 
 | ||||||
| export const UserProvider = ({ children }) => { | export const UserProvider = ({ children }) => { | ||||||
|   const [state, dispatch] = React.useReducer(reducer, initialState) |   const [state, dispatch] = React.useReducer(reducer, initialState); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <UserContext.Provider value={[ state, dispatch ]}> |     <UserContext.Provider value={[state, dispatch]}> | ||||||
|       { children } |       {children} | ||||||
|     </UserContext.Provider> |     </UserContext.Provider> | ||||||
|   ) |   ); | ||||||
| } | }; | ||||||
| @@ -3,12 +3,12 @@ export const reducer = (state, action) => { | |||||||
|     case 'login': |     case 'login': | ||||||
|       return { |       return { | ||||||
|         ...state, |         ...state, | ||||||
|         user: action.payload |         user: action.payload, | ||||||
|       }; |       }; | ||||||
|     case 'logout': |     case 'logout': | ||||||
|       return { |       return { | ||||||
|         ...state, |         ...state, | ||||||
|         user: undefined |         user: undefined, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|     default: |     default: | ||||||
| @@ -17,5 +17,5 @@ export const reducer = (state, action) => { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const initialState = { | export const initialState = { | ||||||
|   user: undefined |   user: undefined, | ||||||
| }; | }; | ||||||
| @@ -2,12 +2,12 @@ import { showError } from './utils'; | |||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| 
 | 
 | ||||||
| export const API = axios.create({ | export const API = axios.create({ | ||||||
|   baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '', |   baseURL: import.meta.env.VITE_REACT_APP_SERVER ? import.meta.env.VITE_REACT_APP_SERVER : '', | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| API.interceptors.response.use( | API.interceptors.response.use( | ||||||
|   (response) => response, |   (response) => response, | ||||||
|   (error) => { |   (error) => { | ||||||
|     showError(error); |     showError(error); | ||||||
|   } |   }, | ||||||
| ); | ); | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| export function authHeader() { |  | ||||||
|     // return authorization header with jwt token |  | ||||||
|     let user = JSON.parse(localStorage.getItem('user')); |  | ||||||
|  |  | ||||||
|     if (user && user.token) { |  | ||||||
|         return { 'Authorization': 'Bearer ' + user.token }; |  | ||||||
|     } else { |  | ||||||
|         return {}; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								web/src/helpers/auth-header.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/src/helpers/auth-header.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | export function authHeader() { | ||||||
|  |   // return authorization header with jwt token | ||||||
|  |   let user = JSON.parse(localStorage.getItem('user')); | ||||||
|  |  | ||||||
|  |   if (user && user.token) { | ||||||
|  |     return { Authorization: 'Bearer ' + user.token }; | ||||||
|  |   } else { | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,3 +1,3 @@ | |||||||
| import { createBrowserHistory } from 'history'; | import { createBrowserHistory } from 'history'; | ||||||
| 
 | 
 | ||||||
| export const history = createBrowserHistory(); | export const history = createBrowserHistory(); | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| export * from './history'; | export * from './history'; | ||||||
| export * from './auth-header'; | export * from './auth-header'; | ||||||
| export * from './utils'; | export * from './utils'; | ||||||
| export * from './api'; | export * from './api'; | ||||||
| @@ -13,16 +13,18 @@ export function renderGroup(group) { | |||||||
|   } |   } | ||||||
|   let groups = group.split(','); |   let groups = group.split(','); | ||||||
|   groups.sort(); |   groups.sort(); | ||||||
|   return <> |   return ( | ||||||
|     {groups.map((group) => { |     <> | ||||||
|       if (group === 'vip' || group === 'pro') { |       {groups.map((group) => { | ||||||
|         return <Label color='yellow'>{group}</Label>; |         if (group === 'vip' || group === 'pro') { | ||||||
|       } else if (group === 'svip' || group === 'premium') { |           return <Label color='yellow'>{group}</Label>; | ||||||
|         return <Label color='red'>{group}</Label>; |         } else if (group === 'svip' || group === 'premium') { | ||||||
|       } |           return <Label color='red'>{group}</Label>; | ||||||
|       return <Label>{group}</Label>; |         } | ||||||
|     })} |         return <Label>{group}</Label>; | ||||||
|   </>; |       })} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function renderNumber(num) { | export function renderNumber(num) { | ||||||
| @@ -55,4 +57,4 @@ export function renderQuotaWithPrompt(quota, digits) { | |||||||
|     return `(等价金额:${renderQuota(quota, digits)})`; |     return `(等价金额:${renderQuota(quota, digits)})`; | ||||||
|   } |   } | ||||||
|   return ''; |   return ''; | ||||||
| } | } | ||||||
| @@ -24,7 +24,7 @@ export function getSystemName() { | |||||||
| export function getLogo() { | export function getLogo() { | ||||||
|   let logo = localStorage.getItem('logo'); |   let logo = localStorage.getItem('logo'); | ||||||
|   if (!logo) return '/logo.png'; |   if (!logo) return '/logo.png'; | ||||||
|   return logo |   return logo; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getFooterHTML() { | export function getFooterHTML() { | ||||||
| @@ -147,17 +147,7 @@ export function timestamp2string(timestamp) { | |||||||
|     second = '0' + second; |     second = '0' + second; | ||||||
|   } |   } | ||||||
|   return ( |   return ( | ||||||
|     year + |     year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second | ||||||
|     '-' + |  | ||||||
|     month + |  | ||||||
|     '-' + |  | ||||||
|     day + |  | ||||||
|     ' ' + |  | ||||||
|     hour + |  | ||||||
|     ':' + |  | ||||||
|     minute + |  | ||||||
|     ':' + |  | ||||||
|     second |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -177,4 +167,4 @@ export const verifyJSON = (str) => { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   return true; |   return true; | ||||||
| }; | }; | ||||||
| @@ -1,35 +1,37 @@ | |||||||
| body { | body { | ||||||
|     margin: 0; |   margin: 0; | ||||||
|     padding-top: 55px; |   padding-top: 55px; | ||||||
|     overflow-y: scroll; |   overflow-y: scroll; | ||||||
|     font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; |   font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', | ||||||
|     -webkit-font-smoothing: antialiased; |     sans-serif; | ||||||
|     -moz-osx-font-smoothing: grayscale; |   -webkit-font-smoothing: antialiased; | ||||||
|     scrollbar-width: none; |   -moz-osx-font-smoothing: grayscale; | ||||||
|  |   scrollbar-width: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| body::-webkit-scrollbar { | body::-webkit-scrollbar { | ||||||
|     display: none; |   display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| code { | code { | ||||||
|     font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; |   font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', | ||||||
|  |     monospace; | ||||||
| } | } | ||||||
|  |  | ||||||
| .main-content { | .main-content { | ||||||
|     padding: 4px; |   padding: 4px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .small-icon .icon { | .small-icon .icon { | ||||||
|     font-size: 1em !important; |   font-size: 1em !important; | ||||||
| } | } | ||||||
|  |  | ||||||
| .custom-footer { | .custom-footer { | ||||||
|     font-size: 1.1em; |   font-size: 1.1em; | ||||||
| } | } | ||||||
|  |  | ||||||
| @media only screen and (max-width: 600px) { | @media only screen and (max-width: 600px) { | ||||||
|     .hide-on-mobile { |   .hide-on-mobile { | ||||||
|         display: none !important; |     display: none !important; | ||||||
|     } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,5 +27,5 @@ root.render( | |||||||
|         </BrowserRouter> |         </BrowserRouter> | ||||||
|       </UserProvider> |       </UserProvider> | ||||||
|     </StatusProvider> |     </StatusProvider> | ||||||
|   </React.StrictMode> |   </React.StrictMode>, | ||||||
| ); | ); | ||||||
| @@ -31,8 +31,8 @@ const About = () => { | |||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       { |       {aboutLoaded && about === '' ? ( | ||||||
|         aboutLoaded && about === '' ? <> |         <> | ||||||
|           <Segment> |           <Segment> | ||||||
|             <Header as='h3'>关于</Header> |             <Header as='h3'>关于</Header> | ||||||
|             <p>可在设置页面设置关于内容,支持 HTML & Markdown</p> |             <p>可在设置页面设置关于内容,支持 HTML & Markdown</p> | ||||||
| @@ -41,20 +41,26 @@ const About = () => { | |||||||
|               https://github.com/songquanpeng/one-api |               https://github.com/songquanpeng/one-api | ||||||
|             </a> |             </a> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </> : <> |         </> | ||||||
|           { |       ) : ( | ||||||
|             about.startsWith('https://') ? <iframe |         <> | ||||||
|  |           {about.startsWith('https://') ? ( | ||||||
|  |             <iframe | ||||||
|               src={about} |               src={about} | ||||||
|               style={{ width: '100%', height: '100vh', border: 'none' }} |               style={{ width: '100%', height: '100vh', border: 'none' }} | ||||||
|             /> : <Segment> |             /> | ||||||
|               <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> |           ) : ( | ||||||
|  |             <Segment> | ||||||
|  |               <div | ||||||
|  |                 style={{ fontSize: 'larger' }} | ||||||
|  |                 dangerouslySetInnerHTML={{ __html: about }} | ||||||
|  |               ></div> | ||||||
|             </Segment> |             </Segment> | ||||||
|           } |           )} | ||||||
|         </> |         </> | ||||||
|       } |       )} | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export default About; | export default About; | ||||||
| @@ -1,13 +1,26 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Header, | ||||||
|  |   Input, | ||||||
|  |   Message, | ||||||
|  |   Segment, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { useParams } from 'react-router-dom'; | import { useParams } from 'react-router-dom'; | ||||||
| import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; | import { | ||||||
|  |   API, | ||||||
|  |   showError, | ||||||
|  |   showInfo, | ||||||
|  |   showSuccess, | ||||||
|  |   verifyJSON, | ||||||
|  | } from '../../helpers'; | ||||||
| import { CHANNEL_OPTIONS } from '../../constants'; | import { CHANNEL_OPTIONS } from '../../constants'; | ||||||
| 
 | 
 | ||||||
| const MODEL_MAPPING_EXAMPLE = { | const MODEL_MAPPING_EXAMPLE = { | ||||||
|   'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', |   'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', | ||||||
|   'gpt-4-0314': 'gpt-4', |   'gpt-4-0314': 'gpt-4', | ||||||
|   'gpt-4-32k-0314': 'gpt-4-32k' |   'gpt-4-32k-0314': 'gpt-4-32k', | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const EditChannel = () => { | const EditChannel = () => { | ||||||
| @@ -23,7 +36,8 @@ const EditChannel = () => { | |||||||
|     other: '', |     other: '', | ||||||
|     model_mapping: '', |     model_mapping: '', | ||||||
|     models: [], |     models: [], | ||||||
|     groups: ['default'] |     groups: ['default'], | ||||||
|  |     enable_ip_randomization: false, | ||||||
|   }; |   }; | ||||||
|   const [batch, setBatch] = useState(false); |   const [batch, setBatch] = useState(false); | ||||||
|   const [inputs, setInputs] = useState(originInputs); |   const [inputs, setInputs] = useState(originInputs); | ||||||
| @@ -33,10 +47,11 @@ const EditChannel = () => { | |||||||
|   const [fullModels, setFullModels] = useState([]); |   const [fullModels, setFullModels] = useState([]); | ||||||
|   const [customModel, setCustomModel] = useState(''); |   const [customModel, setCustomModel] = useState(''); | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|  |     console.log(name, value); | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const loadChannel = async () => { |   const loadChannel = async (modelOptions) => { | ||||||
|     let res = await API.get(`/api/channel/${channelId}`); |     let res = await API.get(`/api/channel/${channelId}`); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -51,7 +66,7 @@ const EditChannel = () => { | |||||||
|               localModelOptions.push({ |               localModelOptions.push({ | ||||||
|                 key: model, |                 key: model, | ||||||
|                 text: model, |                 text: model, | ||||||
|                 value: model |                 value: model, | ||||||
|               }); |               }); | ||||||
|             } |             } | ||||||
|           }); |           }); | ||||||
| @@ -64,7 +79,11 @@ const EditChannel = () => { | |||||||
|         data.groups = data.group.split(','); |         data.groups = data.group.split(','); | ||||||
|       } |       } | ||||||
|       if (data.model_mapping !== '') { |       if (data.model_mapping !== '') { | ||||||
|         data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); |         data.model_mapping = JSON.stringify( | ||||||
|  |           JSON.parse(data.model_mapping), | ||||||
|  |           null, | ||||||
|  |           2, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|       setInputs(data); |       setInputs(data); | ||||||
|     } else { |     } else { | ||||||
| @@ -76,13 +95,23 @@ const EditChannel = () => { | |||||||
|   const fetchModels = async () => { |   const fetchModels = async () => { | ||||||
|     try { |     try { | ||||||
|       let res = await API.get(`/api/channel/models`); |       let res = await API.get(`/api/channel/models`); | ||||||
|       setModelOptions(res.data.data.map((model) => ({ | 
 | ||||||
|  |       setFullModels(res.data.data.map((model) => model.id)); | ||||||
|  |       setBasicModels( | ||||||
|  |         res.data.data | ||||||
|  |           .filter((model) => !model.id.startsWith('gpt-4')) | ||||||
|  |           .map((model) => model.id), | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       const allModels = res.data.data.map((model) => ({ | ||||||
|         key: model.id, |         key: model.id, | ||||||
|         text: model.id, |         text: model.id, | ||||||
|         value: model.id |         value: model.id, | ||||||
|       }))); |       })); | ||||||
|       setFullModels(res.data.data.map((model) => model.id)); | 
 | ||||||
|       setBasicModels(res.data.data.filter((model) => !model.id.startsWith('gpt-4')).map((model) => model.id)); |       setModelOptions(allModels); | ||||||
|  | 
 | ||||||
|  |       return allModels; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       showError(error.message); |       showError(error.message); | ||||||
|     } |     } | ||||||
| @@ -91,22 +120,24 @@ const EditChannel = () => { | |||||||
|   const fetchGroups = async () => { |   const fetchGroups = async () => { | ||||||
|     try { |     try { | ||||||
|       let res = await API.get(`/api/group/`); |       let res = await API.get(`/api/group/`); | ||||||
|       setGroupOptions(res.data.data.map((group) => ({ |       setGroupOptions( | ||||||
|         key: group, |         res.data.data.map((group) => ({ | ||||||
|         text: group, |           key: group, | ||||||
|         value: group |           text: group, | ||||||
|       }))); |           value: group, | ||||||
|  |         })), | ||||||
|  |       ); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       showError(error.message); |       showError(error.message); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(async () => { | ||||||
|  |     const models = await fetchModels(); | ||||||
|  |     await fetchGroups(); | ||||||
|     if (isEdit) { |     if (isEdit) { | ||||||
|       loadChannel().then(); |       await loadChannel(models); | ||||||
|     } |     } | ||||||
|     fetchModels().then(); |  | ||||||
|     fetchGroups().then(); |  | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const submit = async () => { |   const submit = async () => { | ||||||
| @@ -124,7 +155,10 @@ const EditChannel = () => { | |||||||
|     } |     } | ||||||
|     let localInputs = inputs; |     let localInputs = inputs; | ||||||
|     if (localInputs.base_url.endsWith('/')) { |     if (localInputs.base_url.endsWith('/')) { | ||||||
|       localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); |       localInputs.base_url = localInputs.base_url.slice( | ||||||
|  |         0, | ||||||
|  |         localInputs.base_url.length - 1, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|     if (localInputs.type === 3 && localInputs.other === '') { |     if (localInputs.type === 3 && localInputs.other === '') { | ||||||
|       localInputs.other = '2023-03-15-preview'; |       localInputs.other = '2023-03-15-preview'; | ||||||
| @@ -133,7 +167,10 @@ const EditChannel = () => { | |||||||
|     localInputs.models = localInputs.models.join(','); |     localInputs.models = localInputs.models.join(','); | ||||||
|     localInputs.group = localInputs.groups.join(','); |     localInputs.group = localInputs.groups.join(','); | ||||||
|     if (isEdit) { |     if (isEdit) { | ||||||
|       res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); |       res = await API.put(`/api/channel/`, { | ||||||
|  |         ...localInputs, | ||||||
|  |         id: parseInt(channelId), | ||||||
|  |       }); | ||||||
|     } else { |     } else { | ||||||
|       res = await API.post(`/api/channel/`, localInputs); |       res = await API.post(`/api/channel/`, localInputs); | ||||||
|     } |     } | ||||||
| @@ -165,65 +202,74 @@ const EditChannel = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           { |           {inputs.type === 3 && ( | ||||||
|             inputs.type === 3 && ( |             <> | ||||||
|               <> |               <Message> | ||||||
|                 <Message> |                 注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 | ||||||
|                   注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model |                 One API 会把请求体中的 model | ||||||
|                   参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank' |                 参数替换为你的部署名称(模型名称中的点会被剔除), | ||||||
|                                                                     href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。 |                 <a | ||||||
|                 </Message> |                   target='_blank' | ||||||
|                 <Form.Field> |                   href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271' | ||||||
|                   <Form.Input |                 > | ||||||
|                     label='AZURE_OPENAI_ENDPOINT' |                   图片演示 | ||||||
|                     name='base_url' |                 </a> | ||||||
|                     placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'} |                 。 | ||||||
|                     onChange={handleInputChange} |               </Message> | ||||||
|                     value={inputs.base_url} |  | ||||||
|                     autoComplete='new-password' |  | ||||||
|                   /> |  | ||||||
|                 </Form.Field> |  | ||||||
|                 <Form.Field> |  | ||||||
|                   <Form.Input |  | ||||||
|                     label='默认 API 版本' |  | ||||||
|                     name='other' |  | ||||||
|                     placeholder={'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'} |  | ||||||
|                     onChange={handleInputChange} |  | ||||||
|                     value={inputs.other} |  | ||||||
|                     autoComplete='new-password' |  | ||||||
|                   /> |  | ||||||
|                 </Form.Field> |  | ||||||
|               </> |  | ||||||
|             ) |  | ||||||
|           } |  | ||||||
|           { |  | ||||||
|             inputs.type === 8 && ( |  | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|                 <Form.Input |                 <Form.Input | ||||||
|                   label='Base URL' |                   label='AZURE_OPENAI_ENDPOINT' | ||||||
|                   name='base_url' |                   name='base_url' | ||||||
|                   placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'} |                   placeholder={ | ||||||
|  |                     '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com' | ||||||
|  |                   } | ||||||
|                   onChange={handleInputChange} |                   onChange={handleInputChange} | ||||||
|                   value={inputs.base_url} |                   value={inputs.base_url} | ||||||
|                   autoComplete='new-password' |                   autoComplete='new-password' | ||||||
|                 /> |                 /> | ||||||
|               </Form.Field> |               </Form.Field> | ||||||
|             ) |  | ||||||
|           } |  | ||||||
|           { |  | ||||||
|             inputs.type !== 3 && inputs.type !== 8 && ( |  | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|                 <Form.Input |                 <Form.Input | ||||||
|                   label='镜像' |                   label='默认 API 版本' | ||||||
|                   name='base_url' |                   name='other' | ||||||
|                   placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'} |                   placeholder={ | ||||||
|  |                     '请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖' | ||||||
|  |                   } | ||||||
|                   onChange={handleInputChange} |                   onChange={handleInputChange} | ||||||
|                   value={inputs.base_url} |                   value={inputs.other} | ||||||
|                   autoComplete='new-password' |                   autoComplete='new-password' | ||||||
|                 /> |                 /> | ||||||
|               </Form.Field> |               </Form.Field> | ||||||
|             ) |             </> | ||||||
|           } |           )} | ||||||
|  |           {inputs.type === 8 && ( | ||||||
|  |             <Form.Field> | ||||||
|  |               <Form.Input | ||||||
|  |                 label='Base URL' | ||||||
|  |                 name='base_url' | ||||||
|  |                 placeholder={ | ||||||
|  |                   '请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn' | ||||||
|  |                 } | ||||||
|  |                 onChange={handleInputChange} | ||||||
|  |                 value={inputs.base_url} | ||||||
|  |                 autoComplete='new-password' | ||||||
|  |               /> | ||||||
|  |             </Form.Field> | ||||||
|  |           )} | ||||||
|  |           {inputs.type !== 3 && inputs.type !== 8 && ( | ||||||
|  |             <Form.Field> | ||||||
|  |               <Form.Input | ||||||
|  |                 label='镜像' | ||||||
|  |                 name='base_url' | ||||||
|  |                 placeholder={ | ||||||
|  |                   '此项可选,输入镜像站地址,格式为:https://domain.com' | ||||||
|  |                 } | ||||||
|  |                 onChange={handleInputChange} | ||||||
|  |                 value={inputs.base_url} | ||||||
|  |                 autoComplete='new-password' | ||||||
|  |               /> | ||||||
|  |             </Form.Field> | ||||||
|  |           )} | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='名称' |               label='名称' | ||||||
| @@ -268,29 +314,52 @@ const EditChannel = () => { | |||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <div style={{ lineHeight: '40px', marginBottom: '12px' }}> |           <div style={{ lineHeight: '40px', marginBottom: '12px' }}> | ||||||
|             <Button type={'button'} onClick={() => { |             <Button | ||||||
|               handleInputChange(null, { name: 'models', value: basicModels }); |               type={'button'} | ||||||
|             }}>填入基础模型</Button> |               onClick={() => { | ||||||
|             <Button type={'button'} onClick={() => { |                 handleInputChange(null, { name: 'models', value: basicModels }); | ||||||
|               handleInputChange(null, { name: 'models', value: fullModels }); |               }} | ||||||
|             }}>填入所有模型</Button> |             > | ||||||
|             <Button type={'button'} onClick={() => { |               填入基础模型 | ||||||
|               handleInputChange(null, { name: 'models', value: [] }); |             </Button> | ||||||
|             }}>清除所有模型</Button> |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 handleInputChange(null, { name: 'models', value: fullModels }); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               填入所有模型 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 handleInputChange(null, { name: 'models', value: [] }); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               清除所有模型 | ||||||
|  |             </Button> | ||||||
|             <Input |             <Input | ||||||
|               action={ |               action={ | ||||||
|                 <Button type={'button'} onClick={()=>{ |                 <Button | ||||||
|                   let localModels = [...inputs.models]; |                   type={'button'} | ||||||
|                   localModels.push(customModel); |                   onClick={() => { | ||||||
|                   let localModelOptions = [...modelOptions]; |                     let localModels = [...inputs.models]; | ||||||
|                   localModelOptions.push({ |                     localModels.push(customModel); | ||||||
|                     key: customModel, |                     let localModelOptions = [...modelOptions]; | ||||||
|                     text: customModel, |                     localModelOptions.push({ | ||||||
|                     value: customModel, |                       key: customModel, | ||||||
|                   }); |                       text: customModel, | ||||||
|                   setModelOptions(localModelOptions); |                       value: customModel, | ||||||
|                   handleInputChange(null, { name: 'models', value: localModels }); |                     }); | ||||||
|                 }}>填入</Button> |                     setModelOptions(localModelOptions); | ||||||
|  |                     handleInputChange(null, { | ||||||
|  |                       name: 'models', | ||||||
|  |                       value: localModels, | ||||||
|  |                     }); | ||||||
|  |                   }} | ||||||
|  |                 > | ||||||
|  |                   填入 | ||||||
|  |                 </Button> | ||||||
|               } |               } | ||||||
|               placeholder='输入自定义模型名称' |               placeholder='输入自定义模型名称' | ||||||
|               value={customModel} |               value={customModel} | ||||||
| @@ -299,10 +368,25 @@ const EditChannel = () => { | |||||||
|               }} |               }} | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               name='enable_ip_randomization' | ||||||
|  |               label='将IP随机地址传递给HTTP头' | ||||||
|  |               onChange={(e, { name, checked }) => { | ||||||
|  |                 handleInputChange(e, { name, value: checked }); | ||||||
|  |               }} | ||||||
|  |               checked={inputs.enable_ip_randomization} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.TextArea |             <Form.TextArea | ||||||
|               label='模型映射' |               label='模型映射' | ||||||
|               placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} |               placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify( | ||||||
|  |                 MODEL_MAPPING_EXAMPLE, | ||||||
|  |                 null, | ||||||
|  |                 2, | ||||||
|  |               )}`} | ||||||
|               name='model_mapping' |               name='model_mapping' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={inputs.model_mapping} |               value={inputs.model_mapping} | ||||||
| @@ -310,19 +394,23 @@ const EditChannel = () => { | |||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           { |           {batch ? ( | ||||||
|             batch ? <Form.Field> |             <Form.Field> | ||||||
|               <Form.TextArea |               <Form.TextArea | ||||||
|                 label='密钥' |                 label='密钥' | ||||||
|                 name='key' |                 name='key' | ||||||
|                 required |  | ||||||
|                 placeholder={'请输入密钥,一行一个'} |                 placeholder={'请输入密钥,一行一个'} | ||||||
|                 onChange={handleInputChange} |                 onChange={handleInputChange} | ||||||
|                 value={inputs.key} |                 value={inputs.key} | ||||||
|                 style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} |                 style={{ | ||||||
|  |                   minHeight: 150, | ||||||
|  |                   fontFamily: 'JetBrains Mono, Consolas', | ||||||
|  |                 }} | ||||||
|                 autoComplete='new-password' |                 autoComplete='new-password' | ||||||
|               /> |               /> | ||||||
|             </Form.Field> : <Form.Field> |             </Form.Field> | ||||||
|  |           ) : ( | ||||||
|  |             <Form.Field> | ||||||
|               <Form.Input |               <Form.Input | ||||||
|                 label='密钥' |                 label='密钥' | ||||||
|                 name='key' |                 name='key' | ||||||
| @@ -333,18 +421,18 @@ const EditChannel = () => { | |||||||
|                 autoComplete='new-password' |                 autoComplete='new-password' | ||||||
|               /> |               /> | ||||||
|             </Form.Field> |             </Form.Field> | ||||||
|           } |           )} | ||||||
|           { |           {!isEdit && ( | ||||||
|             !isEdit && ( |             <Form.Checkbox | ||||||
|               <Form.Checkbox |               checked={batch} | ||||||
|                 checked={batch} |               label='批量创建' | ||||||
|                 label='批量创建' |               name='batch' | ||||||
|                 name='batch' |               onChange={() => setBatch(!batch)} | ||||||
|                 onChange={() => setBatch(!batch)} |             /> | ||||||
|               /> |           )} | ||||||
|             ) |           <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}> | ||||||
|           } |             提交 | ||||||
|           <Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button> |           </Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Segment> |       </Segment> | ||||||
|     </> |     </> | ||||||
| @@ -11,5 +11,4 @@ const Chat = () => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export default Chat; | export default Chat; | ||||||
| @@ -52,8 +52,8 @@ const Home = () => { | |||||||
|   }, []); |   }, []); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       { |       {homePageContentLoaded && homePageContent === '' ? ( | ||||||
|         homePageContentLoaded && homePageContent === '' ? <> |         <> | ||||||
|           <Segment> |           <Segment> | ||||||
|             <Header as='h3'>系统状况</Header> |             <Header as='h3'>系统状况</Header> | ||||||
|             <Grid columns={2} stackable> |             <Grid columns={2} stackable> | ||||||
| @@ -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 | ||||||
| @@ -115,16 +121,22 @@ const Home = () => { | |||||||
|               </Grid.Column> |               </Grid.Column> | ||||||
|             </Grid> |             </Grid> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </> : <> |         </> | ||||||
|           { |       ) : ( | ||||||
|             homePageContent.startsWith('https://') ? <iframe |         <> | ||||||
|  |           {homePageContent.startsWith('https://') ? ( | ||||||
|  |             <iframe | ||||||
|               src={homePageContent} |               src={homePageContent} | ||||||
|               style={{ width: '100%', height: '100vh', border: 'none' }} |               style={{ width: '100%', height: '100vh', border: 'none' }} | ||||||
|             /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div> |             /> | ||||||
|           } |           ) : ( | ||||||
|  |             <div | ||||||
|  |               style={{ fontSize: 'larger' }} | ||||||
|  |               dangerouslySetInnerHTML={{ __html: homePageContent }} | ||||||
|  |             ></div> | ||||||
|  |           )} | ||||||
|         </> |         </> | ||||||
|       } |       )} | ||||||
| 
 |  | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| import React from 'react'; |  | ||||||
| import { Segment, Header } from 'semantic-ui-react'; |  | ||||||
|  |  | ||||||
| const NotFound = () => ( |  | ||||||
|   <> |  | ||||||
|     <Header |  | ||||||
|       block |  | ||||||
|       as="h4" |  | ||||||
|       content="404" |  | ||||||
|       attached="top" |  | ||||||
|       icon="info" |  | ||||||
|       className="small-icon" |  | ||||||
|     /> |  | ||||||
|     <Segment attached="bottom"> |  | ||||||
|       未找到所请求的页面 |  | ||||||
|     </Segment> |  | ||||||
|   </> |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| export default NotFound; |  | ||||||
							
								
								
									
										18
									
								
								web/src/pages/NotFound/index.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/pages/NotFound/index.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { Segment, Header } from 'semantic-ui-react'; | ||||||
|  |  | ||||||
|  | const NotFound = () => ( | ||||||
|  |   <> | ||||||
|  |     <Header | ||||||
|  |       block | ||||||
|  |       as='h4' | ||||||
|  |       content='404' | ||||||
|  |       attached='top' | ||||||
|  |       icon='info' | ||||||
|  |       className='small-icon' | ||||||
|  |     /> | ||||||
|  |     <Segment attached='bottom'>未找到所请求的页面</Segment> | ||||||
|  |   </> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export default NotFound; | ||||||
| @@ -12,7 +12,7 @@ const EditRedemption = () => { | |||||||
|   const originInputs = { |   const originInputs = { | ||||||
|     name: '', |     name: '', | ||||||
|     quota: 100000, |     quota: 100000, | ||||||
|     count: 1 |     count: 1, | ||||||
|   }; |   }; | ||||||
|   const [inputs, setInputs] = useState(originInputs); |   const [inputs, setInputs] = useState(originInputs); | ||||||
|   const { name, quota, count } = inputs; |   const { name, quota, count } = inputs; | ||||||
| @@ -44,10 +44,13 @@ const EditRedemption = () => { | |||||||
|     localInputs.quota = parseInt(localInputs.quota); |     localInputs.quota = parseInt(localInputs.quota); | ||||||
|     let res; |     let res; | ||||||
|     if (isEdit) { |     if (isEdit) { | ||||||
|       res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(redemptionId) }); |       res = await API.put(`/api/redemption/`, { | ||||||
|  |         ...localInputs, | ||||||
|  |         id: parseInt(redemptionId), | ||||||
|  |       }); | ||||||
|     } else { |     } else { | ||||||
|       res = await API.post(`/api/redemption/`, { |       res = await API.post(`/api/redemption/`, { | ||||||
|         ...localInputs |         ...localInputs, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
| @@ -62,9 +65,9 @@ const EditRedemption = () => { | |||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
|     if (!isEdit && data) { |     if (!isEdit && data) { | ||||||
|       let text = ""; |       let text = ''; | ||||||
|       for (let i = 0; i < data.length; i++) { |       for (let i = 0; i < data.length; i++) { | ||||||
|         text += data[i] + "\n"; |         text += data[i] + '\n'; | ||||||
|       } |       } | ||||||
|       downloadTextAsFile(text, `${inputs.name}.txt`); |       downloadTextAsFile(text, `${inputs.name}.txt`); | ||||||
|     } |     } | ||||||
| @@ -97,8 +100,8 @@ const EditRedemption = () => { | |||||||
|               type='number' |               type='number' | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           { |           {!isEdit && ( | ||||||
|             !isEdit && <> |             <> | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|                 <Form.Input |                 <Form.Input | ||||||
|                   label='生成数量' |                   label='生成数量' | ||||||
| @@ -111,8 +114,10 @@ const EditRedemption = () => { | |||||||
|                 /> |                 /> | ||||||
|               </Form.Field> |               </Form.Field> | ||||||
|             </> |             </> | ||||||
|           } |           )} | ||||||
|           <Button positive onClick={submit}>提交</Button> |           <Button positive onClick={submit}> | ||||||
|  |             提交 | ||||||
|  |           </Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Segment> |       </Segment> | ||||||
|     </> |     </> | ||||||
| @@ -6,7 +6,7 @@ const Redemption = () => ( | |||||||
|   <> |   <> | ||||||
|     <Segment> |     <Segment> | ||||||
|       <Header as='h3'>管理兑换码</Header> |       <Header as='h3'>管理兑换码</Header> | ||||||
|       <RedemptionsTable/> |       <RedemptionsTable /> | ||||||
|     </Segment> |     </Segment> | ||||||
|   </> |   </> | ||||||
| ); | ); | ||||||
| @@ -14,8 +14,8 @@ const Setting = () => { | |||||||
|         <Tab.Pane attached={false}> |         <Tab.Pane attached={false}> | ||||||
|           <PersonalSetting /> |           <PersonalSetting /> | ||||||
|         </Tab.Pane> |         </Tab.Pane> | ||||||
|       ) |       ), | ||||||
|     } |     }, | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   if (isRoot()) { |   if (isRoot()) { | ||||||
| @@ -25,7 +25,7 @@ const Setting = () => { | |||||||
|         <Tab.Pane attached={false}> |         <Tab.Pane attached={false}> | ||||||
|           <OperationSetting /> |           <OperationSetting /> | ||||||
|         </Tab.Pane> |         </Tab.Pane> | ||||||
|       ) |       ), | ||||||
|     }); |     }); | ||||||
|     panes.push({ |     panes.push({ | ||||||
|       menuItem: '系统设置', |       menuItem: '系统设置', | ||||||
| @@ -33,7 +33,7 @@ const Setting = () => { | |||||||
|         <Tab.Pane attached={false}> |         <Tab.Pane attached={false}> | ||||||
|           <SystemSetting /> |           <SystemSetting /> | ||||||
|         </Tab.Pane> |         </Tab.Pane> | ||||||
|       ) |       ), | ||||||
|     }); |     }); | ||||||
|     panes.push({ |     panes.push({ | ||||||
|       menuItem: '其他设置', |       menuItem: '其他设置', | ||||||
| @@ -41,7 +41,7 @@ const Setting = () => { | |||||||
|         <Tab.Pane attached={false}> |         <Tab.Pane attached={false}> | ||||||
|           <OtherSetting /> |           <OtherSetting /> | ||||||
|         </Tab.Pane> |         </Tab.Pane> | ||||||
|       ) |       ), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -1,161 +0,0 @@ | |||||||
| import React, { useEffect, useState } from 'react'; |  | ||||||
| import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; |  | ||||||
| import { useParams } from 'react-router-dom'; |  | ||||||
| import { API, showError, showSuccess, timestamp2string } from '../../helpers'; |  | ||||||
| import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; |  | ||||||
|  |  | ||||||
| const EditToken = () => { |  | ||||||
|   const params = useParams(); |  | ||||||
|   const tokenId = params.id; |  | ||||||
|   const isEdit = tokenId !== undefined; |  | ||||||
|   const [loading, setLoading] = useState(isEdit); |  | ||||||
|   const originInputs = { |  | ||||||
|     name: '', |  | ||||||
|     remain_quota: isEdit ? 0 : 500000, |  | ||||||
|     expired_time: -1, |  | ||||||
|     unlimited_quota: false |  | ||||||
|   }; |  | ||||||
|   const [inputs, setInputs] = useState(originInputs); |  | ||||||
|   const { name, remain_quota, expired_time, unlimited_quota } = inputs; |  | ||||||
|  |  | ||||||
|   const handleInputChange = (e, { name, value }) => { |  | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const setExpiredTime = (month, day, hour, minute) => { |  | ||||||
|     let now = new Date(); |  | ||||||
|     let timestamp = now.getTime() / 1000; |  | ||||||
|     let seconds = month * 30 * 24 * 60 * 60; |  | ||||||
|     seconds += day * 24 * 60 * 60; |  | ||||||
|     seconds += hour * 60 * 60; |  | ||||||
|     seconds += minute * 60; |  | ||||||
|     if (seconds !== 0) { |  | ||||||
|       timestamp += seconds; |  | ||||||
|       setInputs({ ...inputs, expired_time: timestamp2string(timestamp) }); |  | ||||||
|     } else { |  | ||||||
|       setInputs({ ...inputs, expired_time: -1 }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const setUnlimitedQuota = () => { |  | ||||||
|     setInputs({ ...inputs, unlimited_quota: !unlimited_quota }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const loadToken = async () => { |  | ||||||
|     let res = await API.get(`/api/token/${tokenId}`); |  | ||||||
|     const { success, message, data } = res.data; |  | ||||||
|     if (success) { |  | ||||||
|       if (data.expired_time !== -1) { |  | ||||||
|         data.expired_time = timestamp2string(data.expired_time); |  | ||||||
|       } |  | ||||||
|       setInputs(data); |  | ||||||
|     } else { |  | ||||||
|       showError(message); |  | ||||||
|     } |  | ||||||
|     setLoading(false); |  | ||||||
|   }; |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (isEdit) { |  | ||||||
|       loadToken().then(); |  | ||||||
|     } |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   const submit = async () => { |  | ||||||
|     if (!isEdit && inputs.name === '') return; |  | ||||||
|     let localInputs = inputs; |  | ||||||
|     localInputs.remain_quota = parseInt(localInputs.remain_quota); |  | ||||||
|     if (localInputs.expired_time !== -1) { |  | ||||||
|       let time = Date.parse(localInputs.expired_time); |  | ||||||
|       if (isNaN(time)) { |  | ||||||
|         showError('过期时间格式错误!'); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       localInputs.expired_time = Math.ceil(time / 1000); |  | ||||||
|     } |  | ||||||
|     let res; |  | ||||||
|     if (isEdit) { |  | ||||||
|       res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) }); |  | ||||||
|     } else { |  | ||||||
|       res = await API.post(`/api/token/`, localInputs); |  | ||||||
|     } |  | ||||||
|     const { success, message } = res.data; |  | ||||||
|     if (success) { |  | ||||||
|       if (isEdit) { |  | ||||||
|         showSuccess('令牌更新成功!'); |  | ||||||
|       } else { |  | ||||||
|         showSuccess('令牌创建成功!'); |  | ||||||
|         setInputs(originInputs); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       showError(message); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <Segment loading={loading}> |  | ||||||
|         <Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header> |  | ||||||
|         <Form autoComplete='new-password'> |  | ||||||
|           <Form.Field> |  | ||||||
|             <Form.Input |  | ||||||
|               label='名称' |  | ||||||
|               name='name' |  | ||||||
|               placeholder={'请输入名称'} |  | ||||||
|               onChange={handleInputChange} |  | ||||||
|               value={name} |  | ||||||
|               autoComplete='new-password' |  | ||||||
|               required={!isEdit} |  | ||||||
|             /> |  | ||||||
|           </Form.Field> |  | ||||||
|           <Form.Field> |  | ||||||
|             <Form.Input |  | ||||||
|               label='过期时间' |  | ||||||
|               name='expired_time' |  | ||||||
|               placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'} |  | ||||||
|               onChange={handleInputChange} |  | ||||||
|               value={expired_time} |  | ||||||
|               autoComplete='new-password' |  | ||||||
|               type='datetime-local' |  | ||||||
|             /> |  | ||||||
|           </Form.Field> |  | ||||||
|           <div style={{ lineHeight: '40px' }}> |  | ||||||
|             <Button type={'button'} onClick={() => { |  | ||||||
|               setExpiredTime(0, 0, 0, 0); |  | ||||||
|             }}>永不过期</Button> |  | ||||||
|             <Button type={'button'} onClick={() => { |  | ||||||
|               setExpiredTime(1, 0, 0, 0); |  | ||||||
|             }}>一个月后过期</Button> |  | ||||||
|             <Button type={'button'} onClick={() => { |  | ||||||
|               setExpiredTime(0, 1, 0, 0); |  | ||||||
|             }}>一天后过期</Button> |  | ||||||
|             <Button type={'button'} onClick={() => { |  | ||||||
|               setExpiredTime(0, 0, 1, 0); |  | ||||||
|             }}>一小时后过期</Button> |  | ||||||
|             <Button type={'button'} onClick={() => { |  | ||||||
|               setExpiredTime(0, 0, 0, 1); |  | ||||||
|             }}>一分钟后过期</Button> |  | ||||||
|           </div> |  | ||||||
|           <Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message> |  | ||||||
|           <Form.Field> |  | ||||||
|             <Form.Input |  | ||||||
|               label={`额度${renderQuotaWithPrompt(remain_quota)}`} |  | ||||||
|               name='remain_quota' |  | ||||||
|               placeholder={'请输入额度'} |  | ||||||
|               onChange={handleInputChange} |  | ||||||
|               value={remain_quota} |  | ||||||
|               autoComplete='new-password' |  | ||||||
|               type='number' |  | ||||||
|               disabled={unlimited_quota} |  | ||||||
|             /> |  | ||||||
|           </Form.Field> |  | ||||||
|           <Button type={'button'} onClick={() => { |  | ||||||
|             setUnlimitedQuota(); |  | ||||||
|           }}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button> |  | ||||||
|           <Button positive onClick={submit}>提交</Button> |  | ||||||
|         </Form> |  | ||||||
|       </Segment> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default EditToken; |  | ||||||
							
								
								
									
										288
									
								
								web/src/pages/Token/EditToken.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								web/src/pages/Token/EditToken.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | |||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; | ||||||
|  | import { useParams } from 'react-router-dom'; | ||||||
|  | import { API, showError, showSuccess, timestamp2string } from '../../helpers'; | ||||||
|  | import { renderQuotaWithPrompt } from '../../helpers/render'; | ||||||
|  |  | ||||||
|  | const EditToken = () => { | ||||||
|  |   const params = useParams(); | ||||||
|  |   const tokenId = params.id; | ||||||
|  |   const isEdit = tokenId !== undefined; | ||||||
|  |   const [loading, setLoading] = useState(isEdit); | ||||||
|  |   const originInputs = { | ||||||
|  |     name: '', | ||||||
|  |     remain_quota: isEdit ? 0 : 500000, | ||||||
|  |     expired_time: -1, | ||||||
|  |     unlimited_quota: false, | ||||||
|  |     models: isEdit | ||||||
|  |       ? [] | ||||||
|  |       : [ | ||||||
|  |           'gpt-3.5-turbo', | ||||||
|  |           'gpt-3.5-turbo-0301', | ||||||
|  |           'gpt-3.5-turbo-0613', | ||||||
|  |           'gpt-3.5-turbo-16k', | ||||||
|  |           'gpt-3.5-turbo-16k-0613', | ||||||
|  |         ], | ||||||
|  |   }; | ||||||
|  |   const [modelOptions, setModelOptions] = useState([]); | ||||||
|  |   const [basicModels, setBasicModels] = useState([]); | ||||||
|  |   const [fullModels, setFullModels] = useState([]); | ||||||
|  |   const [inputs, setInputs] = useState(originInputs); | ||||||
|  |   const { name, remain_quota, expired_time, unlimited_quota } = inputs; | ||||||
|  |  | ||||||
|  |   const handleInputChange = (e, { name, value }) => { | ||||||
|  |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const setExpiredTime = (month, day, hour, minute) => { | ||||||
|  |     let now = new Date(); | ||||||
|  |     let timestamp = now.getTime() / 1000; | ||||||
|  |     let seconds = month * 30 * 24 * 60 * 60; | ||||||
|  |     seconds += day * 24 * 60 * 60; | ||||||
|  |     seconds += hour * 60 * 60; | ||||||
|  |     seconds += minute * 60; | ||||||
|  |     if (seconds !== 0) { | ||||||
|  |       timestamp += seconds; | ||||||
|  |       setInputs({ ...inputs, expired_time: timestamp2string(timestamp) }); | ||||||
|  |     } else { | ||||||
|  |       setInputs({ ...inputs, expired_time: -1 }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const setUnlimitedQuota = () => { | ||||||
|  |     setInputs({ ...inputs, unlimited_quota: !unlimited_quota }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const fetchModels = async () => { | ||||||
|  |     try { | ||||||
|  |       let res = await API.get(`/api/channel/models`); | ||||||
|  |       setModelOptions( | ||||||
|  |         res.data.data.map((model) => ({ | ||||||
|  |           key: model.id, | ||||||
|  |           text: model.id, | ||||||
|  |           value: model.id, | ||||||
|  |         })), | ||||||
|  |       ); | ||||||
|  |       setFullModels(res.data.data.map((model) => model.id)); | ||||||
|  |       setBasicModels( | ||||||
|  |         res.data.data | ||||||
|  |           .filter((model) => !model.id.startsWith('gpt-4')) | ||||||
|  |           .map((model) => model.id), | ||||||
|  |       ); | ||||||
|  |     } catch (error) { | ||||||
|  |       showError(error.message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const loadToken = async () => { | ||||||
|  |     let res = await API.get(`/api/token/${tokenId}`); | ||||||
|  |     const { success, message, data } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (data.expired_time !== -1) { | ||||||
|  |         data.expired_time = timestamp2string(data.expired_time); | ||||||
|  |       } | ||||||
|  |       if (data.models === '') { | ||||||
|  |         data.models = []; | ||||||
|  |       } else { | ||||||
|  |         data.models = data.models.split(','); | ||||||
|  |       } | ||||||
|  |       setInputs(data); | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |     } | ||||||
|  |     setLoading(false); | ||||||
|  |   }; | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isEdit) { | ||||||
|  |       loadToken().then(); | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const submit = async () => { | ||||||
|  |     if (!isEdit && inputs.name === '') return; | ||||||
|  |     let localInputs = inputs; | ||||||
|  |     localInputs.remain_quota = parseInt(localInputs.remain_quota); | ||||||
|  |     if (localInputs.expired_time !== -1) { | ||||||
|  |       let time = Date.parse(localInputs.expired_time); | ||||||
|  |       if (isNaN(time)) { | ||||||
|  |         showError('过期时间格式错误!'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       localInputs.expired_time = Math.ceil(time / 1000); | ||||||
|  |     } | ||||||
|  |     if (inputs.models.length === 0) { | ||||||
|  |       showError('请至少选择一个模型!'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     localInputs.models = localInputs.models.join(','); | ||||||
|  |     let res; | ||||||
|  |     if (isEdit) { | ||||||
|  |       res = await API.put(`/api/token/`, { | ||||||
|  |         ...localInputs, | ||||||
|  |         id: parseInt(tokenId), | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       res = await API.post(`/api/token/`, localInputs); | ||||||
|  |     } | ||||||
|  |     const { success, message } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (isEdit) { | ||||||
|  |         showSuccess('令牌更新成功!'); | ||||||
|  |       } else { | ||||||
|  |         showSuccess('令牌创建成功!'); | ||||||
|  |         setInputs(originInputs); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchModels().then(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Segment loading={loading}> | ||||||
|  |         <Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header> | ||||||
|  |         <Form autoComplete='new-password'> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label='名称' | ||||||
|  |               name='name' | ||||||
|  |               placeholder={'请输入名称'} | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={name} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               required={!isEdit} | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label='过期时间' | ||||||
|  |               name='expired_time' | ||||||
|  |               placeholder={ | ||||||
|  |                 '请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制' | ||||||
|  |               } | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={expired_time} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               type='datetime-local' | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <div style={{ lineHeight: '40px' }}> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setExpiredTime(0, 0, 0, 0); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               永不过期 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setExpiredTime(1, 0, 0, 0); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               一个月后过期 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setExpiredTime(0, 1, 0, 0); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               一天后过期 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setExpiredTime(0, 0, 1, 0); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               一小时后过期 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setExpiredTime(0, 0, 0, 1); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               一分钟后过期 | ||||||
|  |             </Button> | ||||||
|  |           </div> | ||||||
|  |           <Message> | ||||||
|  |             注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 | ||||||
|  |           </Message> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label={`额度${renderQuotaWithPrompt(remain_quota)}`} | ||||||
|  |               name='remain_quota' | ||||||
|  |               placeholder={'请输入额度'} | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={remain_quota} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               type='number' | ||||||
|  |               disabled={unlimited_quota} | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <Button | ||||||
|  |             type={'button'} | ||||||
|  |             onClick={() => { | ||||||
|  |               setUnlimitedQuota(); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             {unlimited_quota ? '取消无限额度' : '设置为无限额度'} | ||||||
|  |           </Button> | ||||||
|  |           <Form.Field style={{ marginTop: '12px' }}> | ||||||
|  |             <Form.Dropdown | ||||||
|  |               label='模型' | ||||||
|  |               placeholder={'请选择此密钥支持的模型'} | ||||||
|  |               name='models' | ||||||
|  |               required | ||||||
|  |               fluid | ||||||
|  |               multiple | ||||||
|  |               selection | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={inputs.models} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               options={modelOptions} | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|  |           <div style={{ lineHeight: '40px', marginBottom: '12px' }}> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 handleInputChange(null, { name: 'models', value: basicModels }); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               填入基础模型 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 handleInputChange(null, { name: 'models', value: fullModels }); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               填入所有模型 | ||||||
|  |             </Button> | ||||||
|  |             <Button | ||||||
|  |               type={'button'} | ||||||
|  |               onClick={() => { | ||||||
|  |                 handleInputChange(null, { name: 'models', value: [] }); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               清除所有模型 | ||||||
|  |             </Button> | ||||||
|  |           </div> | ||||||
|  |           <Button positive onClick={submit}> | ||||||
|  |             提交 | ||||||
|  |           </Button> | ||||||
|  |         </Form> | ||||||
|  |       </Segment> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default EditToken; | ||||||
| @@ -6,7 +6,7 @@ const Token = () => ( | |||||||
|   <> |   <> | ||||||
|     <Segment> |     <Segment> | ||||||
|       <Header as='h3'>我的令牌</Header> |       <Header as='h3'>我的令牌</Header> | ||||||
|       <TokensTable/> |       <TokensTable /> | ||||||
|     </Segment> |     </Segment> | ||||||
|   </> |   </> | ||||||
| ); | ); | ||||||
| @@ -1,5 +1,12 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react'; | import { | ||||||
|  |   Button, | ||||||
|  |   Form, | ||||||
|  |   Grid, | ||||||
|  |   Header, | ||||||
|  |   Segment, | ||||||
|  |   Statistic, | ||||||
|  | } from 'semantic-ui-react'; | ||||||
| import { API, showError, showInfo, showSuccess } from '../../helpers'; | import { API, showError, showInfo, showSuccess } from '../../helpers'; | ||||||
| import { renderQuota } from '../../helpers/render'; | import { renderQuota } from '../../helpers/render'; | ||||||
| 
 | 
 | ||||||
| @@ -10,11 +17,11 @@ const TopUp = () => { | |||||||
| 
 | 
 | ||||||
|   const topUp = async () => { |   const topUp = async () => { | ||||||
|     if (redemptionCode === '') { |     if (redemptionCode === '') { | ||||||
|       showInfo('请输入充值码!') |       showInfo('请输入充值码!'); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const res = await API.post('/api/user/topup', { |     const res = await API.post('/api/user/topup', { | ||||||
|       key: redemptionCode |       key: redemptionCode, | ||||||
|     }); |     }); | ||||||
|     const { success, message, data } = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -36,15 +43,15 @@ const TopUp = () => { | |||||||
|     window.open(topUpLink, '_blank'); |     window.open(topUpLink, '_blank'); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const getUserQuota = async ()=>{ |   const getUserQuota = async () => { | ||||||
|     let res  = await API.get(`/api/user/self`); |     let res = await API.get(`/api/user/self`); | ||||||
|     const {success, message, data} = res.data; |     const { success, message, data } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       setUserQuota(data.quota); |       setUserQuota(data.quota); | ||||||
|     } else { |     } else { | ||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
|   } |   }; | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let status = localStorage.getItem('status'); |     let status = localStorage.getItem('status'); | ||||||
| @@ -92,5 +99,4 @@ const TopUp = () => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export default TopUp; | export default TopUp; | ||||||
| @@ -30,38 +30,38 @@ const AddUser = () => { | |||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Segment> |       <Segment> | ||||||
|         <Header as="h3">创建新用户账户</Header> |         <Header as='h3'>创建新用户账户</Header> | ||||||
|         <Form autoComplete="off"> |         <Form autoComplete='off'> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label="用户名" |               label='用户名' | ||||||
|               name="username" |               name='username' | ||||||
|               placeholder={'请输入用户名'} |               placeholder={'请输入用户名'} | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={username} |               value={username} | ||||||
|               autoComplete="off" |               autoComplete='off' | ||||||
|               required |               required | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label="显示名称" |               label='显示名称' | ||||||
|               name="display_name" |               name='display_name' | ||||||
|               placeholder={'请输入显示名称'} |               placeholder={'请输入显示名称'} | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={display_name} |               value={display_name} | ||||||
|               autoComplete="off" |               autoComplete='off' | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label="密码" |               label='密码' | ||||||
|               name="password" |               name='password' | ||||||
|               type={'password'} |               type={'password'} | ||||||
|               placeholder={'请输入密码'} |               placeholder={'请输入密码'} | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={password} |               value={password} | ||||||
|               autoComplete="off" |               autoComplete='off' | ||||||
|               required |               required | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
| @@ -13,25 +13,36 @@ 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 { | ||||||
|     inputs; |     username, | ||||||
|  |     display_name, | ||||||
|  |     password, | ||||||
|  |     github_id, | ||||||
|  |     wechat_id, | ||||||
|  |     email, | ||||||
|  |     quota, | ||||||
|  |     discord_id, | ||||||
|  |   } = inputs; | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|   }; |   }; | ||||||
|   const fetchGroups = async () => { |   const fetchGroups = async () => { | ||||||
|     try { |     try { | ||||||
|       let res = await API.get(`/api/group/`); |       let res = await API.get(`/api/group/`); | ||||||
|       setGroupOptions(res.data.data.map((group) => ({ |       setGroupOptions( | ||||||
|         key: group, |         res.data.data.map((group) => ({ | ||||||
|         text: group, |           key: group, | ||||||
|         value: group, |           text: group, | ||||||
|       }))); |           value: group, | ||||||
|  |         })), | ||||||
|  |       ); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       showError(error.message); |       showError(error.message); | ||||||
|     } |     } | ||||||
| @@ -115,8 +126,8 @@ const EditUser = () => { | |||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           { |           {userId && ( | ||||||
|             userId && <> |             <> | ||||||
|               <Form.Field> |               <Form.Field> | ||||||
|                 <Form.Dropdown |                 <Form.Dropdown | ||||||
|                   label='分组' |                   label='分组' | ||||||
| @@ -145,7 +156,7 @@ const EditUser = () => { | |||||||
|                 /> |                 /> | ||||||
|               </Form.Field> |               </Form.Field> | ||||||
|             </> |             </> | ||||||
|           } |           )} | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='已绑定的 GitHub 账户' |               label='已绑定的 GitHub 账户' | ||||||
| @@ -156,6 +167,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='已绑定的微信账户' | ||||||
| @@ -176,7 +197,9 @@ const EditUser = () => { | |||||||
|               readOnly |               readOnly | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|           <Button positive onClick={submit}>提交</Button> |           <Button positive onClick={submit}> | ||||||
|  |             提交 | ||||||
|  |           </Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Segment> |       </Segment> | ||||||
|     </> |     </> | ||||||
| @@ -6,7 +6,7 @@ const User = () => ( | |||||||
|   <> |   <> | ||||||
|     <Segment> |     <Segment> | ||||||
|       <Header as='h3'>管理用户</Header> |       <Header as='h3'>管理用户</Header> | ||||||
|       <UsersTable/> |       <UsersTable /> | ||||||
|     </Segment> |     </Segment> | ||||||
|   </> |   </> | ||||||
| ); | ); | ||||||
							
								
								
									
										11
									
								
								web/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/vite.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import { defineConfig } from 'vite' | ||||||
|  | import react from '@vitejs/plugin-react' | ||||||
|  |  | ||||||
|  | // https://vitejs.dev/config/ | ||||||
|  | export default defineConfig({ | ||||||
|  |   plugins: [react()], | ||||||
|  |   build: { | ||||||
|  |     outDir: 'build', | ||||||
|  |     minify: 'terser', | ||||||
|  |   }, | ||||||
|  | }) | ||||||
		Reference in New Issue
	
	Block a user