Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f126d783c9 | ||
|
66e02a4bcf | ||
|
cf564f36fa | ||
|
43f8d5fd92 | ||
|
15d9a0c177 | ||
|
17125448cb | ||
|
a891a3e64f | ||
|
fd72565011 | ||
|
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
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ upload
|
|||||||
*.db
|
*.db
|
||||||
build
|
build
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
.env*
|
||||||
|
24
Dockerfile
24
Dockerfile
@ -1,31 +1,29 @@
|
|||||||
FROM node:16 as builder
|
# Node build stage
|
||||||
|
FROM node:18 as builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
COPY ./web/package*.json ./
|
||||||
|
RUN npm ci
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
COPY ./VERSION .
|
COPY ./VERSION .
|
||||||
RUN npm install
|
|
||||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
|
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
|
||||||
|
|
||||||
|
# Go build stage
|
||||||
FROM golang AS builder2
|
FROM golang AS builder2
|
||||||
|
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=1 \
|
CGO_ENABLED=1 \
|
||||||
GOOS=linux
|
GOOS=linux
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /build/build ./web/build
|
COPY --from=builder /build/build ./web/build
|
||||||
RUN go mod download
|
|
||||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
||||||
|
|
||||||
|
# Final stage
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true
|
||||||
RUN apk update \
|
WORKDIR /data
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add --no-cache ca-certificates tzdata \
|
|
||||||
&& update-ca-certificates 2>/dev/null || true
|
|
||||||
|
|
||||||
COPY --from=builder2 /build/one-api /
|
COPY --from=builder2 /build/one-api /
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
WORKDIR /data
|
|
||||||
ENTRYPOINT ["/one-api"]
|
ENTRYPOINT ["/one-api"]
|
||||||
|
@ -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,12 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var DiscordClientId = ""
|
||||||
|
var DiscordClientSecret = ""
|
||||||
|
var DiscordGuildId = ""
|
||||||
|
var DiscordAllowJoiningGuild = "false"
|
||||||
|
var DiscordBotToken = ""
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
@ -151,6 +158,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 +179,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,159 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_http_headers := channel.CustomHttpHeaders
|
||||||
|
if custom_http_headers != "" {
|
||||||
|
var custom_http_headers_map map[string]string
|
||||||
|
err := json.Unmarshal([]byte(custom_http_headers), &custom_http_headers_map)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range custom_http_headers_map {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
var response TextResponse
|
if resp.StatusCode != http.StatusOK {
|
||||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
// 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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
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))
|
// 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()
|
||||||
|
|
||||||
|
// Check if streaming is complete and streamResponseText is populated
|
||||||
|
if streamResponseText == "" {
|
||||||
|
return errors.New("Streaming not complete")
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
237
controller/discord.go
Normal file
237
controller/discord.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"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, disgoauth.ScopeGuilds, disgoauth.ScopeGuildsJoin},
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add guild member.
|
||||||
|
if common.DiscordGuildId != "" && discordUser.Id != "" && common.DiscordBotToken != "" && common.DiscordAllowJoiningGuild == "true" {
|
||||||
|
url := fmt.Sprintf("https://discord.com/api/guilds/%s/members/%s", common.DiscordGuildId, discordUser.Id)
|
||||||
|
|
||||||
|
// Set JSON
|
||||||
|
map1 := map[string]interface{}{
|
||||||
|
// accessToken remove "Bearer "
|
||||||
|
"access_token": string(accessToken[7:]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to JSON
|
||||||
|
jsonData, _ := json.Marshal(map1)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
|
||||||
|
|
||||||
|
// Set Header
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bot %s", common.DiscordBotToken))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Create a new HTTP Client
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
|
log.Print(resp.StatusCode)
|
||||||
|
|
||||||
|
if err != nil || (resp.StatusCode != 200 && resp.StatusCode != 201 && resp.StatusCode != 204) {
|
||||||
|
// Print content
|
||||||
|
stringBuff := new(bytes.Buffer)
|
||||||
|
stringBuff.ReadFrom(resp.Body)
|
||||||
|
|
||||||
|
// Print error
|
||||||
|
fmt.Println("Error: ", stringBuff.String())
|
||||||
|
|
||||||
|
return nil, errors.New("You must join the discord server first or be verified member to be able to login!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the response body
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,10 @@ func GetStatus(c *gin.Context) {
|
|||||||
"email_verification": common.EmailVerificationEnabled,
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"discord_oauth": common.DiscordOAuthEnabled,
|
||||||
|
"discord_client_id": common.DiscordClientId,
|
||||||
|
"discord_guild_id": common.DiscordGuildId,
|
||||||
|
"discord_allow_joining_guild": common.DiscordAllowJoiningGuild,
|
||||||
"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{
|
||||||
|
@ -109,6 +109,20 @@ func relayImageHelper(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"))
|
||||||
|
|
||||||
|
custom_http_headers := c.GetString("custom_http_headers")
|
||||||
|
if custom_http_headers != "" {
|
||||||
|
var custom_http_headers_map map[string]string
|
||||||
|
err := json.Unmarshal([]byte(custom_http_headers), &custom_http_headers_map)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_custom_http_headers_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range custom_http_headers_map {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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,50 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_http_headers := c.GetString("custom_http_headers")
|
||||||
|
if custom_http_headers != "" {
|
||||||
|
var custom_http_headers_map map[string]string
|
||||||
|
err := json.Unmarshal([]byte(custom_http_headers), &custom_http_headers_map)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_custom_http_headers_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range custom_http_headers_map {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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): %s", baseURL, textRequest.Model, 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 +359,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,15 +409,20 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if isStream {
|
if isStream || channelType == common.ChannelTypeChatGPTWeb || channelType == common.ChannelTypeChatbotUI {
|
||||||
|
dataChan := make(chan string)
|
||||||
|
stopChan := make(chan bool)
|
||||||
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
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) {
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
if atEOF && len(data) == 0 {
|
if atEOF && len(data) == 0 {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if i := strings.Index(string(data), "\n"); i >= 0 {
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
return i + 1, data[0:i], nil
|
return i + 2, data[0:i], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if atEOF {
|
if atEOF {
|
||||||
@ -237,12 +431,95 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
|
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
})
|
})
|
||||||
dataChan := make(chan string)
|
}
|
||||||
stopChan := make(chan bool)
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if channelType == 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
returnObj := map[string]interface{}{
|
||||||
|
"id": chatResponse.ID,
|
||||||
|
"object": chatResponse.Detail.Object,
|
||||||
|
"created": chatResponse.Detail.Created,
|
||||||
|
"model": chatResponse.Detail.Model,
|
||||||
|
"choices": []map[string]interface{}{
|
||||||
|
// set finish_reason to null in json
|
||||||
|
{
|
||||||
|
"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
|
continue
|
||||||
}
|
}
|
||||||
dataChan <- data
|
dataChan <- data
|
||||||
@ -270,10 +547,14 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
streamResponseText += choice.Text
|
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 && 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"]
|
43
go.mod
43
go.mod
@ -9,28 +9,33 @@ 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 (
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||||
|
github.com/knz/go-libedit v1.10.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
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.10.0-rc2 // 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-20230717121745-296ad89f973d // 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 +43,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
|
||||||
)
|
)
|
||||||
|
69
go.sum
69
go.sum
@ -3,11 +3,23 @@ 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/bytedance/sonic v1.10.0-rc h1:3S5HeWxjX08CUqNrXtEittExpJsEKBNzrV5UnrzHxVQ=
|
||||||
|
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
|
github.com/bytedance/sonic v1.10.0-rc2 h1:oDfRZ+4m6AYCOC0GFeOCeYqvBmucy1isvouS2K0cPzo=
|
||||||
|
github.com/bytedance/sonic v1.10.0-rc2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||||
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=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@ -16,6 +28,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 +61,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 +97,18 @@ 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/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
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 +140,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 +171,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 +183,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 +249,17 @@ 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=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
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,8 @@ 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("custom_http_headers", channel.CustomHttpHeaders)
|
||||||
|
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,10 @@ 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"`
|
||||||
|
CustomHttpHeaders string `json:"custom_http_headers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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,11 @@ 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["DiscordGuildId"] = ""
|
||||||
|
common.OptionMap["DiscordBotToken"] = ""
|
||||||
|
common.OptionMap["DiscordAllowJoiningGuild"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
common.OptionMap["WeChatServerToken"] = ""
|
common.OptionMap["WeChatServerToken"] = ""
|
||||||
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
@ -133,6 +139,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 +179,16 @@ 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 "DiscordGuildId":
|
||||||
|
common.DiscordGuildId = value
|
||||||
|
case "DiscordBotToken":
|
||||||
|
common.DiscordBotToken = value
|
||||||
|
case "DiscordAllowJoiningGuild":
|
||||||
|
common.DiscordAllowJoiningGuild = 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())
|
||||||
{
|
{
|
||||||
|
1
web/.gitignore
vendored
1
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
|
17126
web/package-lock.json
generated
Normal file
17126
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,18 @@
|
|||||||
"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.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.1.3",
|
||||||
"react-turnstile": "^1.0.5",
|
"react-turnstile": "^1.1.1",
|
||||||
"semantic-ui-css": "^2.5.0",
|
"semantic-ui-css": "^2.5.0",
|
||||||
"semantic-ui-react": "^2.1.3"
|
"semantic-ui-react": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.7.1"
|
"prettier": "^3.0.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
@ -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={
|
||||||
|
@ -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.js
Normal file
57
web/src/components/DiscordOAuth.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
const DiscordOAuth = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const [processing, setProcessing] = useState(true);
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/discord?code=${code}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/setting');
|
||||||
|
} else {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
navigate('/setting'); // in case this is failed to bind GitHub
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
sendCode(code, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscordOAuth;
|
@ -37,10 +37,7 @@ 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'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||||
</a>
|
</a>
|
||||||
由{' '}
|
由{' '}
|
||||||
|
@ -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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,18 +137,14 @@ const Header = () => {
|
|||||||
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%20guilds%20email%20guilds.join`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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 === '') {
|
||||||
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await API.post(
|
||||||
|
`/api/user/login?turnstile=${turnstileToken}`,
|
||||||
|
{
|
||||||
username,
|
username,
|
||||||
password,
|
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='可选值'
|
/>
|
||||||
|
)}
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label={'令牌名称'}
|
||||||
|
width={isAdminUser ? 2 : 3}
|
||||||
|
value={token_name}
|
||||||
|
placeholder={'可选值'}
|
||||||
|
name='token_name'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='模型名称'
|
||||||
|
width={isAdminUser ? 2 : 3}
|
||||||
|
value={model_name}
|
||||||
|
placeholder='可选值'
|
||||||
name='model_name'
|
name='model_name'
|
||||||
onChange={handleInputChange} />
|
onChange={handleInputChange}
|
||||||
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='起始时间'
|
||||||
|
width={4}
|
||||||
|
value={start_timestamp}
|
||||||
|
type='datetime-local'
|
||||||
name='start_timestamp'
|
name='start_timestamp'
|
||||||
onChange={handleInputChange} />
|
onChange={handleInputChange}
|
||||||
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='结束时间'
|
||||||
|
width={4}
|
||||||
|
value={end_timestamp}
|
||||||
|
type='datetime-local'
|
||||||
name='end_timestamp'
|
name='end_timestamp'
|
||||||
onChange={handleInputChange} />
|
onChange={handleInputChange}
|
||||||
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
|
/>
|
||||||
|
<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
|
||||||
|
onClick={() => {
|
||||||
submitConfig('general').then();
|
submitConfig('general').then();
|
||||||
}}>保存通用设置</Form.Button>
|
}}
|
||||||
|
>
|
||||||
|
保存通用设置
|
||||||
|
</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
|
||||||
|
onClick={() => {
|
||||||
submitConfig('monitor').then();
|
submitConfig('monitor').then();
|
||||||
}}>保存监控设置</Form.Button>
|
}}
|
||||||
|
>
|
||||||
|
保存监控设置
|
||||||
|
</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
|
||||||
|
onClick={() => {
|
||||||
submitConfig('quota').then();
|
submitConfig('quota').then();
|
||||||
}}>保存额度设置</Form.Button>
|
}}
|
||||||
|
>
|
||||||
|
保存额度设置
|
||||||
|
</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
|
||||||
|
onClick={() => {
|
||||||
submitConfig('ratio').then();
|
submitConfig('ratio').then();
|
||||||
}}>保存倍率设置</Form.Button>
|
}}
|
||||||
|
>
|
||||||
|
保存倍率设置
|
||||||
|
</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,13 +91,12 @@ 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 === process.env.REACT_APP_VERSION) {
|
||||||
@ -97,7 +104,7 @@ const OtherSetting = () => {
|
|||||||
} 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}&response_type=code&redirect_uri=${window.location.origin}/oauth/discord&scope=identify%20guilds%20email%20guilds.join`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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,17 +164,25 @@ 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);
|
||||||
@ -134,8 +190,7 @@ const PersonalSetting = () => {
|
|||||||
>
|
>
|
||||||
绑定微信账号
|
绑定微信账号
|
||||||
</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 }} />;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -256,7 +298,7 @@ const RedemptionsTable = () => {
|
|||||||
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,14 @@ const SystemSetting = () => {
|
|||||||
PasswordRegisterEnabled: '',
|
PasswordRegisterEnabled: '',
|
||||||
EmailVerificationEnabled: '',
|
EmailVerificationEnabled: '',
|
||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
|
DiscordOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
DiscordClientId: '',
|
||||||
|
DiscordAllowJoiningGuild: 'false',
|
||||||
|
DiscordGuildId: '',
|
||||||
|
DiscordBotToken: '',
|
||||||
|
DiscordClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@ -56,6 +62,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 +73,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 +89,11 @@ const SystemSetting = () => {
|
|||||||
name === 'Notice' ||
|
name === 'Notice' ||
|
||||||
name.startsWith('SMTP') ||
|
name.startsWith('SMTP') ||
|
||||||
name === 'ServerAddress' ||
|
name === 'ServerAddress' ||
|
||||||
|
name === 'DiscordClientId' ||
|
||||||
|
name === 'DiscordGuildId' ||
|
||||||
|
name === 'DiscordAllowJoiningGuild' ||
|
||||||
|
name === 'DiscordBotToken' ||
|
||||||
|
name === 'DiscordClientSecret' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
@ -129,7 +141,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 +150,7 @@ const SystemSetting = () => {
|
|||||||
) {
|
) {
|
||||||
await updateOption(
|
await updateOption(
|
||||||
'WeChatAccountQRCodeImageURL',
|
'WeChatAccountQRCodeImageURL',
|
||||||
inputs.WeChatAccountQRCodeImageURL
|
inputs.WeChatAccountQRCodeImageURL,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -161,6 +173,36 @@ 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);
|
||||||
|
}
|
||||||
|
if (originInputs['DiscordGuildId'] !== inputs.DiscordGuildId) {
|
||||||
|
await updateOption('DiscordGuildId', inputs.DiscordGuildId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['DiscordBotToken'] !== inputs.DiscordBotToken &&
|
||||||
|
inputs.DiscordBotToken !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('DiscordBotToken', inputs.DiscordBotToken);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['DiscordAllowJoiningGuild'] !==
|
||||||
|
inputs.DiscordAllowJoiningGuild
|
||||||
|
) {
|
||||||
|
await updateOption(
|
||||||
|
'DiscordAllowJoiningGuild',
|
||||||
|
inputs.DiscordAllowJoiningGuild,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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 +219,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 +275,151 @@ 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.Checkbox
|
||||||
|
label='Allow Joining Guild'
|
||||||
|
name='DiscordAllowJoiningGuild'
|
||||||
|
autoComplete='new-password'
|
||||||
|
checked={inputs.DiscordAllowJoiningGuild === 'true'}
|
||||||
|
onChange={(e, { name, checked }) =>
|
||||||
|
handleInputChange(e, { name, value: checked ? 'true' : 'false' })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord Guild ID'
|
||||||
|
name='DiscordGuildId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordGuildId}
|
||||||
|
placeholder='Enter the ID of your Discord server'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord Bot Token'
|
||||||
|
name='DiscordBotToken'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordBotToken}
|
||||||
|
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 +429,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 +438,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 +507,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 +516,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' },
|
||||||
];
|
];
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -15,5 +15,5 @@ export const userConstants = {
|
|||||||
|
|
||||||
DELETE_REQUEST: 'USERS_DELETE_REQUEST',
|
DELETE_REQUEST: 'USERS_DELETE_REQUEST',
|
||||||
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
|
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
|
||||||
DELETE_FAILURE: 'USERS_DELETE_FAILURE'
|
DELETE_FAILURE: 'USERS_DELETE_FAILURE',
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
@ -9,5 +9,5 @@ API.interceptors.response.use(
|
|||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
showError(error);
|
showError(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ export function authHeader() {
|
|||||||
let user = JSON.parse(localStorage.getItem('user'));
|
let user = JSON.parse(localStorage.getItem('user'));
|
||||||
|
|
||||||
if (user && user.token) {
|
if (user && user.token) {
|
||||||
return { 'Authorization': 'Bearer ' + user.token };
|
return { Authorization: 'Bearer ' + user.token };
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@ export function renderGroup(group) {
|
|||||||
}
|
}
|
||||||
let groups = group.split(',');
|
let groups = group.split(',');
|
||||||
groups.sort();
|
groups.sort();
|
||||||
return <>
|
return (
|
||||||
|
<>
|
||||||
{groups.map((group) => {
|
{groups.map((group) => {
|
||||||
if (group === 'vip' || group === 'pro') {
|
if (group === 'vip' || group === 'pro') {
|
||||||
return <Label color='yellow'>{group}</Label>;
|
return <Label color='yellow'>{group}</Label>;
|
||||||
@ -22,7 +23,8 @@ export function renderGroup(group) {
|
|||||||
}
|
}
|
||||||
return <Label>{group}</Label>;
|
return <Label>{group}</Label>;
|
||||||
})}
|
})}
|
||||||
</>;
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderNumber(num) {
|
export function renderNumber(num) {
|
||||||
|
@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ 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',
|
||||||
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@ -13,7 +14,8 @@ body::-webkit-scrollbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -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,30 @@
|
|||||||
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 CUSTOM_HTTP_HEADERS_EXAMPLE = {
|
||||||
|
'X-OpenAI-Organization': 'OpenAI',
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
@ -22,8 +39,10 @@ const EditChannel = () => {
|
|||||||
base_url: '',
|
base_url: '',
|
||||||
other: '',
|
other: '',
|
||||||
model_mapping: '',
|
model_mapping: '',
|
||||||
|
custom_http_headers: '',
|
||||||
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 +52,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 +71,7 @@ const EditChannel = () => {
|
|||||||
localModelOptions.push({
|
localModelOptions.push({
|
||||||
key: model,
|
key: model,
|
||||||
text: model,
|
text: model,
|
||||||
value: model
|
value: model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -64,7 +84,18 @@ 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.custom_http_headers !== '') {
|
||||||
|
data.custom_http_headers = JSON.stringify(
|
||||||
|
JSON.parse(data.custom_http_headers),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
@ -76,13 +107,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 +132,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(
|
||||||
|
res.data.data.map((group) => ({
|
||||||
key: group,
|
key: group,
|
||||||
text: group,
|
text: group,
|
||||||
value: 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 () => {
|
||||||
@ -122,9 +165,19 @@ const EditChannel = () => {
|
|||||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
inputs.custom_http_headers !== '' &&
|
||||||
|
!verifyJSON(inputs.custom_http_headers)
|
||||||
|
) {
|
||||||
|
showInfo('自定义 HTTP 头必须是合法的 JSON 格式!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
let localInputs = inputs;
|
let localInputs = inputs;
|
||||||
if (localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(
|
||||||
|
0,
|
||||||
|
localInputs.base_url.length - 1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
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 +186,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,19 +221,27 @@ const EditChannel = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
{inputs.type === 3 && (
|
||||||
inputs.type === 3 && (
|
|
||||||
<>
|
<>
|
||||||
<Message>
|
<Message>
|
||||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
|
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为
|
||||||
参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
|
One API 会把请求体中的 model
|
||||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
|
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||||
|
<a
|
||||||
|
target='_blank'
|
||||||
|
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
|
||||||
|
>
|
||||||
|
图片演示
|
||||||
|
</a>
|
||||||
|
。
|
||||||
</Message>
|
</Message>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='AZURE_OPENAI_ENDPOINT'
|
label='AZURE_OPENAI_ENDPOINT'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
|
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'
|
||||||
@ -187,43 +251,44 @@ const EditChannel = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='默认 API 版本'
|
label='默认 API 版本'
|
||||||
name='other'
|
name='other'
|
||||||
placeholder={'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'}
|
placeholder={
|
||||||
|
'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||||
|
}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</>
|
</>
|
||||||
)
|
)}
|
||||||
}
|
{inputs.type === 8 && (
|
||||||
{
|
|
||||||
inputs.type === 8 && (
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='Base URL'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
placeholder={
|
||||||
|
'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'
|
||||||
|
}
|
||||||
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 && (
|
||||||
{
|
|
||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='镜像'
|
label='镜像'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'}
|
placeholder={
|
||||||
|
'此项可选,输入镜像站地址,格式为:https://domain.com'
|
||||||
|
}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
@ -268,18 +333,35 @@ const EditChannel = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
handleInputChange(null, { name: 'models', value: basicModels });
|
handleInputChange(null, { name: 'models', value: basicModels });
|
||||||
}}>填入基础模型</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
填入基础模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
handleInputChange(null, { name: 'models', value: fullModels });
|
handleInputChange(null, { name: 'models', value: fullModels });
|
||||||
}}>填入所有模型</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
填入所有模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
handleInputChange(null, { name: 'models', value: [] });
|
handleInputChange(null, { name: 'models', value: [] });
|
||||||
}}>清除所有模型</Button>
|
}}
|
||||||
|
>
|
||||||
|
清除所有模型
|
||||||
|
</Button>
|
||||||
<Input
|
<Input
|
||||||
action={
|
action={
|
||||||
<Button type={'button'} onClick={()=>{
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
let localModels = [...inputs.models];
|
let localModels = [...inputs.models];
|
||||||
localModels.push(customModel);
|
localModels.push(customModel);
|
||||||
let localModelOptions = [...modelOptions];
|
let localModelOptions = [...modelOptions];
|
||||||
@ -289,8 +371,14 @@ const EditChannel = () => {
|
|||||||
value: customModel,
|
value: customModel,
|
||||||
});
|
});
|
||||||
setModelOptions(localModelOptions);
|
setModelOptions(localModelOptions);
|
||||||
handleInputChange(null, { name: 'models', value: localModels });
|
handleInputChange(null, {
|
||||||
}}>填入</Button>
|
name: 'models',
|
||||||
|
value: localModels,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
placeholder='输入自定义模型名称'
|
placeholder='输入自定义模型名称'
|
||||||
value={customModel}
|
value={customModel}
|
||||||
@ -299,10 +387,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 +413,38 @@ const EditChannel = () => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
<Form.Field>
|
||||||
batch ? <Form.Field>
|
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='密钥'
|
label='自定义 HTTP 头'
|
||||||
name='key'
|
placeholder={`此项可选,为一个 JSON 文本,键为 HTTP 头名称,值为 HTTP 头内容,例如:\n${JSON.stringify(
|
||||||
required
|
CUSTOM_HTTP_HEADERS_EXAMPLE,
|
||||||
placeholder={'请输入密钥,一行一个'}
|
null,
|
||||||
|
2,
|
||||||
|
)}`}
|
||||||
|
name='custom_http_headers'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.key}
|
value={inputs.custom_http_headers}
|
||||||
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>
|
||||||
|
{batch ? (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.TextArea
|
||||||
|
label='密钥'
|
||||||
|
name='key'
|
||||||
|
placeholder={'请输入密钥,一行一个'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.key}
|
||||||
|
style={{
|
||||||
|
minHeight: 150,
|
||||||
|
fontFamily: 'JetBrains Mono, Consolas',
|
||||||
|
}}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
) : (
|
||||||
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
@ -333,18 +455,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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,15 +5,13 @@ const NotFound = () => (
|
|||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
block
|
block
|
||||||
as="h4"
|
as='h4'
|
||||||
content="404"
|
content='404'
|
||||||
attached="top"
|
attached='top'
|
||||||
icon="info"
|
icon='info'
|
||||||
className="small-icon"
|
className='small-icon'
|
||||||
/>
|
/>
|
||||||
<Segment attached="bottom">
|
<Segment attached='bottom'>未找到所请求的页面</Segment>
|
||||||
未找到所请求的页面
|
|
||||||
</Segment>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
const EditToken = () => {
|
const EditToken = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -13,8 +13,20 @@ const EditToken = () => {
|
|||||||
name: '',
|
name: '',
|
||||||
remain_quota: isEdit ? 0 : 500000,
|
remain_quota: isEdit ? 0 : 500000,
|
||||||
expired_time: -1,
|
expired_time: -1,
|
||||||
unlimited_quota: false
|
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 [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
||||||
|
|
||||||
@ -41,6 +53,27 @@ const EditToken = () => {
|
|||||||
setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
|
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 () => {
|
const loadToken = async () => {
|
||||||
let res = await API.get(`/api/token/${tokenId}`);
|
let res = await API.get(`/api/token/${tokenId}`);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
@ -48,6 +81,11 @@ const EditToken = () => {
|
|||||||
if (data.expired_time !== -1) {
|
if (data.expired_time !== -1) {
|
||||||
data.expired_time = timestamp2string(data.expired_time);
|
data.expired_time = timestamp2string(data.expired_time);
|
||||||
}
|
}
|
||||||
|
if (data.models === '') {
|
||||||
|
data.models = [];
|
||||||
|
} else {
|
||||||
|
data.models = data.models.split(',');
|
||||||
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
@ -72,9 +110,17 @@ const EditToken = () => {
|
|||||||
}
|
}
|
||||||
localInputs.expired_time = Math.ceil(time / 1000);
|
localInputs.expired_time = Math.ceil(time / 1000);
|
||||||
}
|
}
|
||||||
|
if (inputs.models.length === 0) {
|
||||||
|
showError('请至少选择一个模型!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localInputs.models = localInputs.models.join(',');
|
||||||
let res;
|
let res;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) });
|
res = await API.put(`/api/token/`, {
|
||||||
|
...localInputs,
|
||||||
|
id: parseInt(tokenId),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res = await API.post(`/api/token/`, localInputs);
|
res = await API.post(`/api/token/`, localInputs);
|
||||||
}
|
}
|
||||||
@ -91,6 +137,10 @@ const EditToken = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchModels().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
@ -111,7 +161,9 @@ const EditToken = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='过期时间'
|
label='过期时间'
|
||||||
name='expired_time'
|
name='expired_time'
|
||||||
placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'}
|
placeholder={
|
||||||
|
'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'
|
||||||
|
}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={expired_time}
|
value={expired_time}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@ -119,23 +171,50 @@ const EditToken = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<div style={{ lineHeight: '40px' }}>
|
<div style={{ lineHeight: '40px' }}>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setExpiredTime(0, 0, 0, 0);
|
setExpiredTime(0, 0, 0, 0);
|
||||||
}}>永不过期</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
永不过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setExpiredTime(1, 0, 0, 0);
|
setExpiredTime(1, 0, 0, 0);
|
||||||
}}>一个月后过期</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
一个月后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setExpiredTime(0, 1, 0, 0);
|
setExpiredTime(0, 1, 0, 0);
|
||||||
}}>一天后过期</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
一天后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setExpiredTime(0, 0, 1, 0);
|
setExpiredTime(0, 0, 1, 0);
|
||||||
}}>一小时后过期</Button>
|
}}
|
||||||
<Button type={'button'} onClick={() => {
|
>
|
||||||
|
一小时后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setExpiredTime(0, 0, 0, 1);
|
setExpiredTime(0, 0, 0, 1);
|
||||||
}}>一分钟后过期</Button>
|
}}
|
||||||
|
>
|
||||||
|
一分钟后过期
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
|
<Message>
|
||||||
|
注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
|
||||||
|
</Message>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label={`额度${renderQuotaWithPrompt(remain_quota)}`}
|
label={`额度${renderQuotaWithPrompt(remain_quota)}`}
|
||||||
@ -148,10 +227,58 @@ const EditToken = () => {
|
|||||||
disabled={unlimited_quota}
|
disabled={unlimited_quota}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
setUnlimitedQuota();
|
setUnlimitedQuota();
|
||||||
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
}}
|
||||||
<Button positive onClick={submit}>提交</Button>
|
>
|
||||||
|
{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>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
|
@ -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(
|
||||||
|
res.data.data.map((group) => ({
|
||||||
key: group,
|
key: group,
|
||||||
text: group,
|
text: group,
|
||||||
value: 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user