Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad6f7d99d | ||
|
|
68abcd48ab | ||
|
|
0c175b4e44 | ||
|
|
4e31c3991d | ||
|
|
5b8a826cf9 | ||
|
|
f5f21dffd8 | ||
|
|
4e94c85a9a | ||
|
|
caabdd1e21 | ||
|
|
0424baef6a | ||
|
|
256d290507 | ||
|
|
8f0799d909 | ||
|
|
349e3a3661 | ||
|
|
8cc7f983e1 | ||
|
|
455643e317 | ||
|
|
1c7bad7b87 | ||
|
|
3141292026 | ||
|
|
e4500bf8bf | ||
|
|
4043fccedb | ||
|
|
164df4e708 | ||
|
|
d850f465cd | ||
|
|
e2f5c1eb8c | ||
|
|
d68aa4c96f | ||
|
|
47cb77de53 | ||
|
|
61912f5e2c | ||
|
|
2b17bb8dd7 | ||
|
|
ea73201b6f |
2
.github/workflows/linux-release.yml
vendored
2
.github/workflows/linux-release.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
npm install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
2
.github/workflows/macos-release.yml
vendored
2
.github/workflows/macos-release.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
npm install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
2
.github/workflows/windows-release.yml
vendored
2
.github/workflows/windows-release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
npm install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ COPY ./web/package*.json ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
COPY ./VERSION .
|
COPY ./VERSION .
|
||||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
|
RUN VITE_REACT_APP_VERSION=$(cat VERSION) npm run build
|
||||||
|
|
||||||
# Go build stage
|
# Go build stage
|
||||||
FROM golang AS builder2
|
FROM golang AS builder2
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -57,12 +57,6 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
|
|
||||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
||||||
|
|
||||||
## 👍Forks 特殊功能⚡
|
|
||||||
|
|
||||||
1. 增强的**频道测试**以提高稳定性。
|
|
||||||
2. 支持 **Dall-E 2 模型图像生成** API。
|
|
||||||
3. 修复**登录页面**中缺少的 **Turnstile 验证码**。
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道:
|
1. 支持多种 API 访问渠道:
|
||||||
+ [x] OpenAI 官方通道(支持配置镜像)
|
+ [x] OpenAI 官方通道(支持配置镜像)
|
||||||
@@ -87,16 +81,19 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
12. 支持以美元为单位显示额度。
|
12. 支持以美元为单位显示额度。
|
||||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
|
13. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
14. 支持模型映射,重定向用户的请求模型。
|
14. 支持模型映射,重定向用户的请求模型。
|
||||||
15. 支持丰富的**自定义**设置,
|
15. 支持失败自动重试。
|
||||||
|
16. 支持绘图接口。
|
||||||
|
17. 支持丰富的**自定义**设置,
|
||||||
1. 支持自定义系统名称,logo 以及页脚。
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
16. 支持通过系统访问令牌访问管理 API。
|
18. 支持通过系统访问令牌访问管理 API。
|
||||||
17. 支持 Cloudflare Turnstile 用户校验。
|
19. 支持 Cloudflare Turnstile 用户校验。
|
||||||
18. 支持用户管理,支持**多种用户登录注册方式**:
|
20. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
19. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。
|
||||||
|
22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ var AutomaticDisableChannelEnabled = false
|
|||||||
var QuotaRemindThreshold = 1000
|
var QuotaRemindThreshold = 1000
|
||||||
var PreConsumedQuota = 500
|
var PreConsumedQuota = 500
|
||||||
var ApproximateTokenEnabled = false
|
var ApproximateTokenEnabled = false
|
||||||
|
var RetryTimes = 0
|
||||||
|
|
||||||
var RootUserEmail = ""
|
var RootUserEmail = ""
|
||||||
|
|
||||||
@@ -154,6 +155,9 @@ const (
|
|||||||
ChannelTypePaLM = 11
|
ChannelTypePaLM = 11
|
||||||
ChannelTypeAPI2GPT = 12
|
ChannelTypeAPI2GPT = 12
|
||||||
ChannelTypeAIGC2D = 13
|
ChannelTypeAIGC2D = 13
|
||||||
|
|
||||||
|
// Reserve engineering for public projects
|
||||||
|
ChannelTypeChatGPTWeb = 14 // Chanzhaoyu/chatgpt-web
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
@@ -171,4 +175,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
@@ -27,6 +28,10 @@ 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 {
|
} else {
|
||||||
if channel.BaseURL != "" {
|
if channel.BaseURL != "" {
|
||||||
requestURL = channel.BaseURL
|
requestURL = channel.BaseURL
|
||||||
@@ -35,6 +40,41 @@ 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 := map[string]string{
|
||||||
|
"prompt": prompt,
|
||||||
|
"systemMessage": systemMessage.Content,
|
||||||
|
"temperature": strconv.FormatFloat(request.Temperature, 'f', 2, 64),
|
||||||
|
"top_p": strconv.FormatFloat(request.TopP, 'f', 2, 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to json string
|
||||||
|
jsonData, err = json.Marshal(map1)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -48,6 +88,20 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
|
|||||||
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if channel.EnableIpRandomization {
|
||||||
|
// Generate random IP
|
||||||
|
ip := common.GenerateIP()
|
||||||
|
req.Header.Set("X-Forwarded-For", ip)
|
||||||
|
req.Header.Set("X-Real-IP", ip)
|
||||||
|
req.Header.Set("X-Client-IP", ip)
|
||||||
|
req.Header.Set("X-Forwarded-Host", ip)
|
||||||
|
req.Header.Set("X-Originating-IP", ip)
|
||||||
|
req.RemoteAddr = ip
|
||||||
|
req.Header.Set("X-Remote-IP", ip)
|
||||||
|
req.Header.Set("X-Remote-Addr", ip)
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,51 +109,110 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New("invalid status code: " + strconv.Itoa(resp.StatusCode))
|
// 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 string
|
var done = false
|
||||||
|
var streamResponseText = ""
|
||||||
|
|
||||||
|
if channel.Type != common.ChannelTypeChatGPTWeb {
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 2, data[0:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
|
||||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
||||||
if atEOF && len(data) == 0 {
|
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
})
|
||||||
|
for scanner.Scan() {
|
||||||
if i := strings.Index(string(data), "\n\n"); i >= 0 {
|
data := scanner.Text()
|
||||||
return i + 2, data[0:i], nil
|
if len(data) < 6 { // must be something wrong!
|
||||||
}
|
continue
|
||||||
|
|
||||||
if atEOF {
|
|
||||||
return len(data), data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil, nil
|
|
||||||
})
|
|
||||||
for scanner.Scan() {
|
|
||||||
data := scanner.Text()
|
|
||||||
if len(data) < 6 { // must be something wrong!
|
|
||||||
common.SysError("invalid stream response: " + data)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data = data[6:]
|
|
||||||
if !strings.HasPrefix(data, "[DONE]") {
|
|
||||||
var streamResponse ChatCompletionsStreamResponse
|
|
||||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
|
||||||
if err != nil {
|
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
for _, choice := range streamResponse.Choices {
|
|
||||||
streamResponseText += choice.Delta.Content
|
// 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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
done = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if channel.Type == common.ChannelTypeChatGPTWeb {
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for scanner.Scan() {
|
||||||
|
var chatResponse ChatGptWebChatResponse
|
||||||
|
err = json.Unmarshal(scanner.Bytes(), &chatResponse)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error unmarshal chat response: " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check if streaming is complete and streamResponseText is populated
|
// Check if streaming is complete and streamResponseText is populated
|
||||||
if streamResponseText == "" {
|
if streamResponseText == "" || !done && channel.Type != common.ChannelTypeChatGPTWeb {
|
||||||
return errors.New("Streaming not complete")
|
return errors.New("Streaming not complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +221,12 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
|
|||||||
|
|
||||||
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,
|
||||||
Stream: true,
|
|
||||||
}
|
}
|
||||||
testMessage := Message{
|
testMessage := Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "hi",
|
Content: "say hi word only",
|
||||||
}
|
}
|
||||||
testRequest.Messages = append(testRequest.Messages, testMessage)
|
testRequest.Messages = append(testRequest.Messages, testMessage)
|
||||||
return testRequest
|
return testRequest
|
||||||
|
|||||||
@@ -252,6 +252,24 @@ func init() {
|
|||||||
Root: "code-davinci-edit-001",
|
Root: "code-davinci-edit-001",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "ChatGLM",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "thudm",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "ChatGLM",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "ChatGLM2",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "thudm",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "ChatGLM2",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]OpenAIModels)
|
openAIModelsMap = make(map[string]OpenAIModels)
|
||||||
for _, model := range openAIModels {
|
for _, model := range openAIModels {
|
||||||
|
|||||||
@@ -22,26 +22,26 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
consumeQuota := c.GetBool("consume_quota")
|
consumeQuota := c.GetBool("consume_quota")
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
|
|
||||||
var textRequest GeneralOpenAIRequest
|
var imageRequest ImageRequest
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
err := common.UnmarshalBodyReusable(c, &textRequest)
|
err := common.UnmarshalBodyReusable(c, &imageRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt validation
|
// Prompt validation
|
||||||
if textRequest.Prompt == "" {
|
if imageRequest.Prompt == "" {
|
||||||
return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
|
return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not "256x256", "512x512", or "1024x1024"
|
// Not "256x256", "512x512", or "1024x1024"
|
||||||
if textRequest.Size != "" && textRequest.Size != "256x256" && textRequest.Size != "512x512" && textRequest.Size != "1024x1024" {
|
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
|
||||||
return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest)
|
return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// N should between 1 to 10
|
// N should between 1 and 10
|
||||||
if textRequest.N != 0 && (textRequest.N < 1 || textRequest.N > 10) {
|
if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
|
||||||
return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
|
return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
|
|
||||||
var requestBody io.Reader
|
var requestBody io.Reader
|
||||||
if isModelMapped {
|
if isModelMapped {
|
||||||
jsonStr, err := json.Marshal(textRequest)
|
jsonStr, err := json.Marshal(imageRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@@ -87,14 +87,14 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
|
|
||||||
sizeRatio := 1.0
|
sizeRatio := 1.0
|
||||||
// Size
|
// Size
|
||||||
if textRequest.Size == "256x256" {
|
if imageRequest.Size == "256x256" {
|
||||||
sizeRatio = 1
|
sizeRatio = 1
|
||||||
} else if textRequest.Size == "512x512" {
|
} else if imageRequest.Size == "512x512" {
|
||||||
sizeRatio = 1.125
|
sizeRatio = 1.125
|
||||||
} else if textRequest.Size == "1024x1024" {
|
} else if imageRequest.Size == "1024x1024" {
|
||||||
sizeRatio = 1.25
|
sizeRatio = 1.25
|
||||||
}
|
}
|
||||||
quota := int(ratio * sizeRatio * 1000)
|
quota := int(ratio*sizeRatio*1000) * imageRequest.N
|
||||||
|
|
||||||
if consumeQuota && userQuota-quota < 0 {
|
if consumeQuota && userQuota-quota < 0 {
|
||||||
return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
|
return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -32,6 +33,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if relayMode == RelayModeModerations && textRequest.Model == "" {
|
if relayMode == RelayModeModerations && textRequest.Model == "" {
|
||||||
textRequest.Model = "text-moderation-latest"
|
textRequest.Model = "text-moderation-latest"
|
||||||
}
|
}
|
||||||
|
if relayMode == RelayModeEmbeddings && textRequest.Model == "" {
|
||||||
|
textRequest.Model = c.Param("model")
|
||||||
|
}
|
||||||
// request validation
|
// request validation
|
||||||
if textRequest.Model == "" {
|
if textRequest.Model == "" {
|
||||||
return errorWrapper(errors.New("model is required"), "required_field_missing", http.StatusBadRequest)
|
return errorWrapper(errors.New("model is required"), "required_field_missing", http.StatusBadRequest)
|
||||||
@@ -69,6 +73,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") != "" {
|
||||||
@@ -93,6 +118,10 @@ 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.ChannelTypePaLM {
|
} else if channelType == common.ChannelTypePaLM {
|
||||||
err := relayPaLM(textRequest, c)
|
err := relayPaLM(textRequest, c)
|
||||||
return err
|
return err
|
||||||
@@ -161,6 +190,57 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
|
|
||||||
requestBody = bytes.NewBuffer(bodyBytes)
|
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 := map[string]string{
|
||||||
|
"prompt": prompt,
|
||||||
|
"systemMessage": systemMessage.Content,
|
||||||
|
"temperature": strconv.FormatFloat(reqBody.Temperature, 'f', 2, 64),
|
||||||
|
"top_p": strconv.FormatFloat(reqBody.TopP, 'f', 2, 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
@@ -175,22 +255,34 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
||||||
|
|
||||||
|
if c.GetBool("enable_ip_randomization") == true {
|
||||||
|
// Generate random IP
|
||||||
|
ip := common.GenerateIP()
|
||||||
|
req.Header.Set("X-Forwarded-For", ip)
|
||||||
|
req.Header.Set("X-Real-IP", ip)
|
||||||
|
req.Header.Set("X-Client-IP", ip)
|
||||||
|
req.Header.Set("X-Forwarded-Host", ip)
|
||||||
|
req.Header.Set("X-Originating-IP", ip)
|
||||||
|
req.RemoteAddr = ip
|
||||||
|
req.Header.Set("X-Remote-IP", ip)
|
||||||
|
req.Header.Set("X-Remote-Addr", ip)
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
// Print Data if Error
|
// Print the body in string
|
||||||
bodyBytes, err := io.ReadAll(resp.Body)
|
if resp.Body != nil {
|
||||||
if err != nil {
|
buf := new(bytes.Buffer)
|
||||||
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
buf.ReadFrom(resp.Body)
|
||||||
|
log.Printf("Error Channel (%s): %s", baseURL, buf.String())
|
||||||
|
return errorWrapper(err, "request_failed", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyString := string(bodyBytes)
|
|
||||||
|
|
||||||
log.Printf("Error: %s", bodyString)
|
|
||||||
|
|
||||||
return errorWrapper(err, "request_failed", resp.StatusCode)
|
return errorWrapper(err, "request_failed", resp.StatusCode)
|
||||||
}
|
}
|
||||||
err = req.Body.Close()
|
err = req.Body.Close()
|
||||||
@@ -202,7 +294,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() {
|
||||||
@@ -253,60 +345,129 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if isStream {
|
if isStream {
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
|
||||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
||||||
if atEOF && len(data) == 0 {
|
|
||||||
return 0, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if i := strings.Index(string(data), "\n\n"); i >= 0 {
|
|
||||||
return i + 2, data[0:i], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if atEOF {
|
|
||||||
return len(data), data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil, nil
|
|
||||||
})
|
|
||||||
dataChan := make(chan string)
|
dataChan := make(chan string)
|
||||||
stopChan := make(chan bool)
|
stopChan := make(chan bool)
|
||||||
go func() {
|
|
||||||
for scanner.Scan() {
|
if channelType == common.ChannelTypeChatGPTWeb {
|
||||||
data := scanner.Text()
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
if len(data) < 6 { // must be something wrong!
|
go func() {
|
||||||
common.SysError("invalid stream response: " + data)
|
for scanner.Scan() {
|
||||||
continue
|
var chatResponse ChatGptWebChatResponse
|
||||||
}
|
err = json.Unmarshal(scanner.Bytes(), &chatResponse)
|
||||||
dataChan <- data
|
|
||||||
data = data[6:]
|
if err != nil {
|
||||||
if !strings.HasPrefix(data, "[DONE]") {
|
log.Println("error unmarshal chat response: " + err.Error())
|
||||||
switch relayMode {
|
continue
|
||||||
case RelayModeChatCompletions:
|
}
|
||||||
var streamResponse ChatCompletionsStreamResponse
|
|
||||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
// if response role is assistant and contains delta, append the content to streamResponseText
|
||||||
if err != nil {
|
if chatResponse.Role == "assistant" && chatResponse.Detail != nil {
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
for _, choice := range chatResponse.Detail.Choices {
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, choice := range streamResponse.Choices {
|
|
||||||
streamResponseText += choice.Delta.Content
|
streamResponseText += choice.Delta.Content
|
||||||
}
|
|
||||||
case RelayModeCompletions:
|
returnObj := map[string]interface{}{
|
||||||
var streamResponse CompletionsStreamResponse
|
"id": chatResponse.ID,
|
||||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
"object": chatResponse.Detail.Object,
|
||||||
if err != nil {
|
"created": chatResponse.Detail.Created,
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
"model": chatResponse.Detail.Model,
|
||||||
return
|
"choices": []map[string]interface{}{
|
||||||
}
|
// set finish_reason to null in json
|
||||||
for _, choice := range streamResponse.Choices {
|
{
|
||||||
streamResponseText += choice.Text
|
"finish_reason": nil,
|
||||||
|
"index": 0,
|
||||||
|
"delta": map[string]interface{}{
|
||||||
|
"content": choice.Delta.Content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.Marshal(returnObj)
|
||||||
|
|
||||||
|
dataChan <- "data: " + string(jsonData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
stopChan <- true
|
||||||
stopChan <- true
|
}()
|
||||||
}()
|
} else {
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Text()
|
||||||
|
if len(data) < 6 { // must be something wrong!
|
||||||
|
// common.SysError("invalid stream response: " + data)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If data has event: event content inside, remove it, it can be prefix or inside the data
|
||||||
|
if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
|
||||||
|
// Remove event: event in the front or back
|
||||||
|
data = strings.TrimPrefix(data, "event: event")
|
||||||
|
data = strings.TrimSuffix(data, "event: event")
|
||||||
|
// Remove everything, only keep `data: {...}` <--- this is the json
|
||||||
|
// Find the start and end indices of `data: {...}` substring
|
||||||
|
startIndex := strings.Index(data, "data:")
|
||||||
|
endIndex := strings.LastIndex(data, "}")
|
||||||
|
|
||||||
|
// If both indices are found and end index is greater than start index
|
||||||
|
if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
|
||||||
|
// Extract the `data: {...}` substring
|
||||||
|
data = data[startIndex : endIndex+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace and newlines from the modified data string
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(data, "data:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dataChan <- data
|
||||||
|
data = data[6:]
|
||||||
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
|
switch relayMode {
|
||||||
|
case RelayModeChatCompletions:
|
||||||
|
var streamResponse ChatCompletionsStreamResponse
|
||||||
|
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, choice := range streamResponse.Choices {
|
||||||
|
streamResponseText += choice.Delta.Content
|
||||||
|
}
|
||||||
|
case RelayModeCompletions:
|
||||||
|
var streamResponse CompletionsStreamResponse
|
||||||
|
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, choice := range streamResponse.Choices {
|
||||||
|
streamResponseText += choice.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopChan <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
@@ -318,6 +479,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if strings.HasPrefix(data, "data: [DONE]") {
|
if strings.HasPrefix(data, "data: [DONE]") {
|
||||||
data = data[:12]
|
data = data[:12]
|
||||||
}
|
}
|
||||||
|
// some implementations may add \r at the end of data
|
||||||
|
data = strings.TrimSuffix(data, "\r")
|
||||||
c.Render(-1, common.CustomEvent{Data: data})
|
c.Render(-1, common.CustomEvent{Data: data})
|
||||||
return true
|
return true
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -44,8 +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"`
|
Stream bool `json:"stream"`
|
||||||
|
// -1.0 to 1.0
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
TopP float64 `json:"top_p"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextRequest struct {
|
type TextRequest struct {
|
||||||
@@ -56,6 +60,12 @@ type TextRequest struct {
|
|||||||
//Stream bool `json:"stream"`
|
//Stream bool `json:"stream"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageRequest struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
N int `json:"n"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
@@ -102,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") {
|
||||||
@@ -110,6 +146,8 @@ func Relay(c *gin.Context) {
|
|||||||
relayMode = RelayModeCompletions
|
relayMode = RelayModeCompletions
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
|
||||||
relayMode = RelayModeEmbeddings
|
relayMode = RelayModeEmbeddings
|
||||||
|
} else if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
|
||||||
|
relayMode = RelayModeEmbeddings
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
||||||
relayMode = RelayModeModerations
|
relayMode = RelayModeModerations
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||||
@@ -125,16 +163,25 @@ func Relay(c *gin.Context) {
|
|||||||
err = relayTextHelper(c, relayMode)
|
err = relayTextHelper(c, relayMode)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.StatusCode == http.StatusTooManyRequests {
|
retryTimesStr := c.Query("retry")
|
||||||
err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
retryTimes, _ := strconv.Atoi(retryTimesStr)
|
||||||
|
if retryTimesStr == "" {
|
||||||
|
retryTimes = common.RetryTimes
|
||||||
|
}
|
||||||
|
if retryTimes > 0 {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
||||||
|
} else {
|
||||||
|
if err.StatusCode == http.StatusTooManyRequests {
|
||||||
|
err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
||||||
|
}
|
||||||
|
c.JSON(err.StatusCode, gin.H{
|
||||||
|
"error": err.OpenAIError,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
c.JSON(err.StatusCode, gin.H{
|
|
||||||
"error": err.OpenAIError,
|
|
||||||
})
|
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
||||||
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
||||||
if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") {
|
if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated") {
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
channelName := c.GetString("channel_name")
|
channelName := c.GetString("channel_name")
|
||||||
disableChannel(channelId, channelName, err.Message)
|
disableChannel(channelId, channelName, err.Message)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ WORKDIR /build
|
|||||||
COPY ./web/package*.json ./
|
COPY ./web/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY --from=translator /app .
|
COPY --from=translator /app .
|
||||||
RUN cd web && REACT_APP_VERSION=$(cat VERSION) npm run build
|
RUN cd web && VITE_REACT_APP_VERSION=$(cat VERSION) npm run build
|
||||||
|
|
||||||
# Go build stage
|
# Go build stage
|
||||||
FROM golang:1.20.5 AS goBuilder
|
FROM golang:1.20.5 AS goBuilder
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.14.1
|
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.4
|
github.com/pkoukk/tiktoken-go v0.1.5
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.11.0
|
||||||
gorm.io/driver/mysql v1.5.1
|
gorm.io/driver/mysql v1.5.1
|
||||||
gorm.io/driver/sqlite v1.5.2
|
gorm.io/driver/sqlite v1.5.2
|
||||||
@@ -46,7 +46,7 @@ require (
|
|||||||
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/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
|
||||||
|
|||||||
5
go.sum
5
go.sum
@@ -130,11 +130,15 @@ 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 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.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 h1:x3LgcvujjG+mx8PUMfPmwn3tcu2aA95uCB6ilGGObWk=
|
||||||
@@ -157,6 +161,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
|||||||
18
i18n/en.json
18
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",
|
||||||
@@ -523,5 +524,18 @@
|
|||||||
"该 Discord 账户已被绑定": "The Discord account has been bound",
|
"该 Discord 账户已被绑定": "The Discord account has been bound",
|
||||||
"管理员未开启通过 Discord 登录以及注册": "The administrator has not enabled login and registration via Discord",
|
"管理员未开启通过 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!",
|
"无法启用 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, "
|
"兑换失败,": "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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ func Distribute() func(c *gin.Context) {
|
|||||||
modelRequest.Model = "text-moderation-stable"
|
modelRequest.Model = "text-moderation-stable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
|
||||||
|
if modelRequest.Model == "" {
|
||||||
|
modelRequest.Model = c.Param("model")
|
||||||
|
}
|
||||||
|
}
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||||
if modelRequest.Model == "" {
|
if modelRequest.Model == "" {
|
||||||
modelRequest.Model = "dall-e"
|
modelRequest.Model = "dall-e"
|
||||||
@@ -100,6 +105,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
c.Set("channel_id", channel.Id)
|
c.Set("channel_id", channel.Id)
|
||||||
c.Set("channel_name", channel.Name)
|
c.Set("channel_name", channel.Name)
|
||||||
c.Set("model_mapping", channel.ModelMapping)
|
c.Set("model_mapping", channel.ModelMapping)
|
||||||
|
c.Set("enable_ip_randomization", channel.EnableIpRandomization)
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
c.Set("base_url", channel.BaseURL)
|
c.Set("base_url", channel.BaseURL)
|
||||||
if channel.Type == common.ChannelTypeAzure {
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -23,6 +24,9 @@ type Channel struct {
|
|||||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||||
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
||||||
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
||||||
|
|
||||||
|
// Additional fields, default value is false
|
||||||
|
EnableIpRandomization bool `json:"enable_ip_randomization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMap["ChatLink"] = common.ChatLink
|
common.OptionMap["ChatLink"] = common.ChatLink
|
||||||
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
|
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
|
||||||
|
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
loadOptionsFromDatabase()
|
loadOptionsFromDatabase()
|
||||||
}
|
}
|
||||||
@@ -205,6 +206,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
|
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
|
||||||
case "PreConsumedQuota":
|
case "PreConsumedQuota":
|
||||||
common.PreConsumedQuota, _ = strconv.Atoi(value)
|
common.PreConsumedQuota, _ = strconv.Atoi(value)
|
||||||
|
case "RetryTimes":
|
||||||
|
common.RetryTimes, _ = strconv.Atoi(value)
|
||||||
case "ModelRatio":
|
case "ModelRatio":
|
||||||
err = common.UpdateModelRatioByJSONString(value)
|
err = common.UpdateModelRatioByJSONString(value)
|
||||||
case "GroupRatio":
|
case "GroupRatio":
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,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)
|
||||||
@@ -76,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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/embeddings", controller.Relay)
|
relayV1Router.POST("/embeddings", controller.Relay)
|
||||||
|
relayV1Router.POST("/engines/:model/embeddings", controller.Relay)
|
||||||
relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
|
||||||
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ npm start
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
|
If you want to change the default server, please set `VITE_REACT_APP_SERVER` environment variables before build,
|
||||||
for example: `REACT_APP_SERVER=http://your.domain.com`.
|
for example: `VITE_REACT_APP_SERVER=http://your.domain.com`.
|
||||||
|
|
||||||
Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
|
Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
1. https://github.com/OIerDb-ng/OIerDb
|
1. https://github.com/OIerDb-ng/OIerDb
|
||||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
|
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -14,5 +14,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
17281
web/package-lock.json
generated
17281
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,10 +16,8 @@
|
|||||||
"semantic-ui-react": "^2.1.4"
|
"semantic-ui-react": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "vite preview",
|
||||||
"build": "react-scripts build",
|
"build": "vite build"
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -40,9 +38,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "3.0.0",
|
||||||
"react-scripts": "^5.0.1"
|
"terser": "^5.19.0",
|
||||||
|
"vite": "^4.4.4"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -37,11 +37,8 @@ const Footer = () => {
|
|||||||
></div>
|
></div>
|
||||||
) : (
|
) : (
|
||||||
<div className='custom-footer'>
|
<div className='custom-footer'>
|
||||||
<a
|
<a href='https://github.com/songquanpeng/one-api' target='_blank'>
|
||||||
href='https://github.com/songquanpeng/one-api'
|
{systemName} {import.meta.env.VITE_REACT_APP_VERSION}{' '}
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
|
||||||
</a>
|
</a>
|
||||||
由{' '}
|
由{' '}
|
||||||
<a href='https://github.com/songquanpeng' target='_blank'>
|
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||||
@@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
import {
|
||||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
Button,
|
||||||
|
Container,
|
||||||
|
Dropdown,
|
||||||
|
Icon,
|
||||||
|
Menu,
|
||||||
|
Segment,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import {
|
||||||
|
API,
|
||||||
|
getLogo,
|
||||||
|
getSystemName,
|
||||||
|
isAdmin,
|
||||||
|
isMobile,
|
||||||
|
showSuccess,
|
||||||
|
} from '../helpers';
|
||||||
import '../index.css';
|
import '../index.css';
|
||||||
|
|
||||||
// Header Buttons
|
// Header Buttons
|
||||||
@@ -11,58 +25,58 @@ let headerButtons = [
|
|||||||
{
|
{
|
||||||
name: '首页',
|
name: '首页',
|
||||||
to: '/',
|
to: '/',
|
||||||
icon: 'home'
|
icon: 'home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '渠道',
|
name: '渠道',
|
||||||
to: '/channel',
|
to: '/channel',
|
||||||
icon: 'sitemap',
|
icon: 'sitemap',
|
||||||
admin: true
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '令牌',
|
name: '令牌',
|
||||||
to: '/token',
|
to: '/token',
|
||||||
icon: 'key'
|
icon: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '兑换',
|
name: '兑换',
|
||||||
to: '/redemption',
|
to: '/redemption',
|
||||||
icon: 'dollar sign',
|
icon: 'dollar sign',
|
||||||
admin: true
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '充值',
|
name: '充值',
|
||||||
to: '/topup',
|
to: '/topup',
|
||||||
icon: 'cart'
|
icon: 'cart',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '用户',
|
name: '用户',
|
||||||
to: '/user',
|
to: '/user',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
admin: true
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '日志',
|
name: '日志',
|
||||||
to: '/log',
|
to: '/log',
|
||||||
icon: 'book'
|
icon: 'book',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设置',
|
name: '设置',
|
||||||
to: '/setting',
|
to: '/setting',
|
||||||
icon: 'setting'
|
icon: 'setting',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '关于',
|
name: '关于',
|
||||||
to: '/about',
|
to: '/about',
|
||||||
icon: 'info circle'
|
icon: 'info circle',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (localStorage.getItem('chat_link')) {
|
if (localStorage.getItem('chat_link')) {
|
||||||
headerButtons.splice(1, 0, {
|
headerButtons.splice(1, 0, {
|
||||||
name: '聊天',
|
name: '聊天',
|
||||||
to: '/chat',
|
to: '/chat',
|
||||||
icon: 'comments'
|
icon: 'comments',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,21 +134,17 @@ const Header = () => {
|
|||||||
style={
|
style={
|
||||||
showSidebar
|
showSidebar
|
||||||
? {
|
? {
|
||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
marginBottom: '0',
|
marginBottom: '0',
|
||||||
borderTop: 'none',
|
borderTop: 'none',
|
||||||
height: '51px'
|
height: '51px',
|
||||||
}
|
}
|
||||||
: { borderTop: 'none', height: '52px' }
|
: { borderTop: 'none', height: '52px' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/'>
|
<Menu.Item as={Link} to='/'>
|
||||||
<img
|
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
src={logo}
|
|
||||||
alt='logo'
|
|
||||||
style={{ marginRight: '0.75em' }}
|
|
||||||
/>
|
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>{systemName}</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@ const LoginForm = () => {
|
|||||||
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');
|
||||||
@@ -53,13 +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 = () => {
|
const onDiscordOAuthClicked = () => {
|
||||||
window.open(
|
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`
|
`https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,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) {
|
||||||
@@ -96,10 +96,13 @@ const LoginForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, {
|
const res = await API.post(
|
||||||
username,
|
`/api/user/login?turnstile=${turnstileToken}`,
|
||||||
password,
|
{
|
||||||
});
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
userDispatch({ type: 'login', payload: data });
|
userDispatch({ type: 'login', payload: data });
|
||||||
@@ -113,29 +116,29 @@ 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}
|
||||||
/>
|
/>
|
||||||
@@ -149,18 +152,18 @@ const LoginForm = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button color="" fluid size="large" onClick={handleSubmit}>
|
<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>
|
||||||
@@ -170,24 +173,24 @@ const LoginForm = () => {
|
|||||||
{status.discord_oauth && (
|
{status.discord_oauth && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="blue"
|
color='blue'
|
||||||
icon="discord"
|
icon='discord'
|
||||||
onClick={onDiscordOAuthClicked}
|
onClick={onDiscordOAuthClicked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{status.github_oauth && (
|
{status.github_oauth && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="black"
|
color='black'
|
||||||
icon="github"
|
icon='github'
|
||||||
onClick={onGitHubOAuthClicked}
|
onClick={onGitHubOAuthClicked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{status.wechat_login && (
|
{status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="green"
|
color='green'
|
||||||
icon="wechat"
|
icon='wechat'
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -209,18 +212,18 @@ const LoginForm = () => {
|
|||||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Form size="large">
|
<Form size='large'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
placeholder="验证码"
|
placeholder='验证码'
|
||||||
name="wechat_verification_code"
|
name='wechat_verification_code'
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color=""
|
color=''
|
||||||
fluid
|
fluid
|
||||||
size="large"
|
size='large'
|
||||||
onClick={onSubmitWeChatVerificationCode}
|
onClick={onSubmitWeChatVerificationCode}
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Header,
|
||||||
|
Label,
|
||||||
|
Pagination,
|
||||||
|
Segment,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { API, isAdmin, showError, timestamp2string } from '../helpers';
|
import { API, isAdmin, showError, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
import { renderQuota } from '../helpers/render';
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return (
|
return <>{timestamp2string(timestamp)}</>;
|
||||||
<>
|
|
||||||
{timestamp2string(timestamp)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODE_OPTIONS = [
|
const MODE_OPTIONS = [
|
||||||
{ key: 'all', text: '全部用户', value: 'all' },
|
{ key: 'all', text: '全部用户', value: 'all' },
|
||||||
{ key: 'self', text: '当前用户', value: 'self' }
|
{ key: 'self', text: '当前用户', value: 'self' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LOG_OPTIONS = [
|
const LOG_OPTIONS = [
|
||||||
@@ -23,21 +28,46 @@ const LOG_OPTIONS = [
|
|||||||
{ key: '1', text: '充值', value: 1 },
|
{ key: '1', text: '充值', value: 1 },
|
||||||
{ key: '2', text: '消费', value: 2 },
|
{ key: '2', text: '消费', value: 2 },
|
||||||
{ key: '3', text: '管理', value: 3 },
|
{ key: '3', text: '管理', value: 3 },
|
||||||
{ key: '4', text: '系统', value: 4 }
|
{ key: '4', text: '系统', value: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderType(type) {
|
function renderType(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 1:
|
case 1:
|
||||||
return <Label basic color='green'> 充值 </Label>;
|
return (
|
||||||
|
<Label basic color='green'>
|
||||||
|
{' '}
|
||||||
|
充值{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return <Label basic color='olive'> 消费 </Label>;
|
return (
|
||||||
|
<Label basic color='olive'>
|
||||||
|
{' '}
|
||||||
|
消费{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return <Label basic color='orange'> 管理 </Label>;
|
return (
|
||||||
|
<Label basic color='orange'>
|
||||||
|
{' '}
|
||||||
|
管理{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return <Label basic color='purple'> 系统 </Label>;
|
return (
|
||||||
|
<Label basic color='purple'>
|
||||||
|
{' '}
|
||||||
|
系统{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <Label basic color='black'> 未知 </Label>;
|
return (
|
||||||
|
<Label basic color='black'>
|
||||||
|
{' '}
|
||||||
|
未知{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,13 +85,14 @@ const LogsTable = () => {
|
|||||||
token_name: '',
|
token_name: '',
|
||||||
model_name: '',
|
model_name: '',
|
||||||
start_timestamp: timestamp2string(0),
|
start_timestamp: timestamp2string(0),
|
||||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
|
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||||
});
|
});
|
||||||
const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs;
|
const { username, token_name, model_name, start_timestamp, end_timestamp } =
|
||||||
|
inputs;
|
||||||
|
|
||||||
const [stat, setStat] = useState({
|
const [stat, setStat] = useState({
|
||||||
quota: 0,
|
quota: 0,
|
||||||
token: 0
|
token: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
@@ -71,7 +102,9 @@ const LogsTable = () => {
|
|||||||
const getLogSelfStat = async () => {
|
const getLogSelfStat = async () => {
|
||||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||||
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
let res = await API.get(
|
||||||
|
`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
|
||||||
|
);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setStat(data);
|
setStat(data);
|
||||||
@@ -83,7 +116,9 @@ const LogsTable = () => {
|
|||||||
const getLogStat = async () => {
|
const getLogStat = async () => {
|
||||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||||
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
let res = await API.get(
|
||||||
|
`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
|
||||||
|
);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setStat(data);
|
setStat(data);
|
||||||
@@ -129,7 +164,7 @@ const LogsTable = () => {
|
|||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setActivePage(1)
|
setActivePage(1);
|
||||||
await loadLogs(0);
|
await loadLogs(0);
|
||||||
if (isAdminUser) {
|
if (isAdminUser) {
|
||||||
getLogStat().then();
|
getLogStat().then();
|
||||||
@@ -169,7 +204,7 @@ const LogsTable = () => {
|
|||||||
if (logs.length === 0) return;
|
if (logs.length === 0) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let sortedLogs = [...logs];
|
let sortedLogs = [...logs];
|
||||||
if (typeof sortedLogs[0][key] === 'string'){
|
if (typeof sortedLogs[0][key] === 'string') {
|
||||||
sortedLogs.sort((a, b) => {
|
sortedLogs.sort((a, b) => {
|
||||||
return ('' + a[key]).localeCompare(b[key]);
|
return ('' + a[key]).localeCompare(b[key]);
|
||||||
});
|
});
|
||||||
@@ -190,28 +225,61 @@ const LogsTable = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
|
<Header as='h3'>
|
||||||
|
使用明细(总消耗额度:{renderQuota(stat.quota)})
|
||||||
|
</Header>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
{
|
{isAdminUser && (
|
||||||
isAdminUser && (
|
<Form.Input
|
||||||
<Form.Input fluid label={'用户名称'} width={2} value={username}
|
fluid
|
||||||
placeholder={'可选值'} name='username'
|
label={'用户名称'}
|
||||||
onChange={handleInputChange} />
|
width={2}
|
||||||
)
|
value={username}
|
||||||
}
|
placeholder={'可选值'}
|
||||||
<Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name}
|
name='username'
|
||||||
placeholder={'可选值'} name='token_name' onChange={handleInputChange} />
|
onChange={handleInputChange}
|
||||||
<Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值'
|
/>
|
||||||
name='model_name'
|
)}
|
||||||
onChange={handleInputChange} />
|
<Form.Input
|
||||||
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
|
fluid
|
||||||
name='start_timestamp'
|
label={'令牌名称'}
|
||||||
onChange={handleInputChange} />
|
width={isAdminUser ? 2 : 3}
|
||||||
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
|
value={token_name}
|
||||||
name='end_timestamp'
|
placeholder={'可选值'}
|
||||||
onChange={handleInputChange} />
|
name='token_name'
|
||||||
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='模型名称'
|
||||||
|
width={isAdminUser ? 2 : 3}
|
||||||
|
value={model_name}
|
||||||
|
placeholder='可选值'
|
||||||
|
name='model_name'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='起始时间'
|
||||||
|
width={4}
|
||||||
|
value={start_timestamp}
|
||||||
|
type='datetime-local'
|
||||||
|
name='start_timestamp'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
label='结束时间'
|
||||||
|
width={4}
|
||||||
|
value={end_timestamp}
|
||||||
|
type='datetime-local'
|
||||||
|
name='end_timestamp'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Form.Button fluid label='操作' width={2} onClick={refresh}>
|
||||||
|
查询
|
||||||
|
</Form.Button>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
<Table basic compact size='small'>
|
<Table basic compact size='small'>
|
||||||
@@ -226,8 +294,8 @@ const LogsTable = () => {
|
|||||||
>
|
>
|
||||||
时间
|
时间
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
{
|
{isAdminUser && (
|
||||||
isAdminUser && <Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortLog('username');
|
sortLog('username');
|
||||||
@@ -236,7 +304,7 @@ const LogsTable = () => {
|
|||||||
>
|
>
|
||||||
用户
|
用户
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
}
|
)}
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -307,24 +375,42 @@ const LogsTable = () => {
|
|||||||
{logs
|
{logs
|
||||||
.slice(
|
.slice(
|
||||||
(activePage - 1) * ITEMS_PER_PAGE,
|
(activePage - 1) * ITEMS_PER_PAGE,
|
||||||
activePage * ITEMS_PER_PAGE
|
activePage * ITEMS_PER_PAGE,
|
||||||
)
|
)
|
||||||
.map((log, idx) => {
|
.map((log, idx) => {
|
||||||
if (log.deleted) return <></>;
|
if (log.deleted) return <></>;
|
||||||
return (
|
return (
|
||||||
<Table.Row key={log.created_at}>
|
<Table.Row key={log.created_at}>
|
||||||
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
||||||
{
|
{isAdminUser && (
|
||||||
isAdminUser && (
|
<Table.Cell>
|
||||||
<Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
|
{log.username ? <Label>{log.username}</Label> : ''}
|
||||||
)
|
</Table.Cell>
|
||||||
}
|
)}
|
||||||
<Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
|
<Table.Cell>
|
||||||
|
{log.token_name ? (
|
||||||
|
<Label basic>{log.token_name}</Label>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{renderType(log.type)}</Table.Cell>
|
<Table.Cell>{renderType(log.type)}</Table.Cell>
|
||||||
<Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
|
<Table.Cell>
|
||||||
<Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
|
{log.model_name ? (
|
||||||
<Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
|
<Label basic>{log.model_name}</Label>
|
||||||
<Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{log.prompt_tokens ? log.prompt_tokens : ''}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{log.completion_tokens ? log.completion_tokens : ''}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{log.quota ? renderQuota(log.quota, 6) : ''}
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{log.content}</Table.Cell>
|
<Table.Cell>{log.content}</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
);
|
);
|
||||||
@@ -344,7 +430,9 @@ const LogsTable = () => {
|
|||||||
setLogType(value);
|
setLogType(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
<Button size='small' onClick={refresh} loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
@@ -20,6 +20,7 @@ const OperationSetting = () => {
|
|||||||
DisplayInCurrencyEnabled: '',
|
DisplayInCurrencyEnabled: '',
|
||||||
DisplayTokenStatEnabled: '',
|
DisplayTokenStatEnabled: '',
|
||||||
ApproximateTokenEnabled: '',
|
ApproximateTokenEnabled: '',
|
||||||
|
RetryTimes: 0,
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -53,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) {
|
||||||
@@ -75,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':
|
||||||
@@ -122,6 +134,9 @@ const OperationSetting = () => {
|
|||||||
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
||||||
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
||||||
}
|
}
|
||||||
|
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
|
||||||
|
await updateOption('RetryTimes', inputs.RetryTimes);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -130,10 +145,8 @@ 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>
|
||||||
通用设置
|
<Form.Group widths={4}>
|
||||||
</Header>
|
|
||||||
<Form.Group widths={3}>
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='充值链接'
|
label='充值链接'
|
||||||
name='TopUpLink'
|
name='TopUpLink'
|
||||||
@@ -162,6 +175,17 @@ const OperationSetting = () => {
|
|||||||
step='0.01'
|
step='0.01'
|
||||||
placeholder='一单位货币能兑换的额度'
|
placeholder='一单位货币能兑换的额度'
|
||||||
/>
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='失败重试次数'
|
||||||
|
name='RetryTimes'
|
||||||
|
type={'number'}
|
||||||
|
step='1'
|
||||||
|
min='0'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.RetryTimes}
|
||||||
|
placeholder='失败重试次数'
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group inline>
|
<Form.Group inline>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
@@ -189,13 +213,15 @@ const OperationSetting = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => {
|
<Form.Button
|
||||||
submitConfig('general').then();
|
onClick={() => {
|
||||||
}}>保存通用设置</Form.Button>
|
submitConfig('general').then();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存通用设置
|
||||||
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>监控设置</Header>
|
||||||
监控设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths={3}>
|
<Form.Group widths={3}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='最长响应时间'
|
label='最长响应时间'
|
||||||
@@ -226,13 +252,15 @@ const OperationSetting = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => {
|
<Form.Button
|
||||||
submitConfig('monitor').then();
|
onClick={() => {
|
||||||
}}>保存监控设置</Form.Button>
|
submitConfig('monitor').then();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存监控设置
|
||||||
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>额度设置</Header>
|
||||||
额度设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths={4}>
|
<Form.Group widths={4}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='新用户初始额度'
|
label='新用户初始额度'
|
||||||
@@ -275,13 +303,15 @@ const OperationSetting = () => {
|
|||||||
placeholder='例如:1000'
|
placeholder='例如:1000'
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => {
|
<Form.Button
|
||||||
submitConfig('quota').then();
|
onClick={() => {
|
||||||
}}>保存额度设置</Form.Button>
|
submitConfig('quota').then();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存额度设置
|
||||||
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>倍率设置</Header>
|
||||||
倍率设置
|
|
||||||
</Header>
|
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='模型倍率'
|
label='模型倍率'
|
||||||
@@ -304,9 +334,13 @@ const OperationSetting = () => {
|
|||||||
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => {
|
<Form.Button
|
||||||
submitConfig('ratio').then();
|
onClick={() => {
|
||||||
}}>保存倍率设置</Form.Button>
|
submitConfig('ratio').then();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存倍率设置
|
||||||
|
</Form.Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { API, showError, showSuccess } from '../helpers';
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
@@ -10,13 +18,13 @@ const OtherSetting = () => {
|
|||||||
About: '',
|
About: '',
|
||||||
SystemName: '',
|
SystemName: '',
|
||||||
Logo: '',
|
Logo: '',
|
||||||
HomePageContent: ''
|
HomePageContent: '',
|
||||||
});
|
});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||||
const [updateData, setUpdateData] = useState({
|
const [updateData, setUpdateData] = useState({
|
||||||
tag_name: '',
|
tag_name: '',
|
||||||
content: ''
|
content: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
@@ -43,7 +51,7 @@ const OtherSetting = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.put('/api/option/', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value
|
value,
|
||||||
});
|
});
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -83,21 +91,20 @@ const OtherSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openGitHubRelease = () => {
|
const openGitHubRelease = () => {
|
||||||
window.location =
|
window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
|
||||||
'https://github.com/songquanpeng/one-api/releases/latest';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkUpdate = async () => {
|
const checkUpdate = async () => {
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
|
'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
|
||||||
);
|
);
|
||||||
const { tag_name, body } = res.data;
|
const { tag_name, body } = res.data;
|
||||||
if (tag_name === process.env.REACT_APP_VERSION) {
|
if (tag_name === import.meta.env.VITE_REACT_APP_VERSION) {
|
||||||
showSuccess(`已是最新版本:${tag_name}`);
|
showSuccess(`已是最新版本:${tag_name}`);
|
||||||
} else {
|
} else {
|
||||||
setUpdateData({
|
setUpdateData({
|
||||||
tag_name: tag_name,
|
tag_name: tag_name,
|
||||||
content: marked.parse(body)
|
content: marked.parse(body),
|
||||||
});
|
});
|
||||||
setShowUpdateModal(true);
|
setShowUpdateModal(true);
|
||||||
}
|
}
|
||||||
@@ -153,7 +160,9 @@ const OtherSetting = () => {
|
|||||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button>
|
<Form.Button onClick={() => submitOption('HomePageContent')}>
|
||||||
|
保存首页内容
|
||||||
|
</Form.Button>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='关于'
|
label='关于'
|
||||||
@@ -165,7 +174,10 @@ const OtherSetting = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
|
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
|
||||||
<Message>移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。</Message>
|
<Message>
|
||||||
|
移除 One API
|
||||||
|
的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。
|
||||||
|
</Message>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='页脚'
|
label='页脚'
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
|
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
|
||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import {
|
||||||
|
API,
|
||||||
|
copy,
|
||||||
|
showError,
|
||||||
|
showInfo,
|
||||||
|
showNotice,
|
||||||
|
showSuccess,
|
||||||
|
} from '../helpers';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const PasswordResetConfirm = () => {
|
const PasswordResetConfirm = () => {
|
||||||
@@ -38,7 +38,7 @@ const PasswordResetForm = () => {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
`/api/reset_password?email=${email}&turnstile=${turnstileToken}`
|
`/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
|
||||||
);
|
);
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import { Button, Divider, Form, Header, Image, Message, Modal, Label } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Header,
|
||||||
|
Image,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
Label,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import {
|
||||||
|
API,
|
||||||
|
copy,
|
||||||
|
showError,
|
||||||
|
showInfo,
|
||||||
|
showNotice,
|
||||||
|
showSuccess,
|
||||||
|
} from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
@@ -81,12 +97,12 @@ const PersonalSetting = () => {
|
|||||||
} else {
|
} else {
|
||||||
showError(message);
|
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) {
|
||||||
@@ -99,15 +115,15 @@ 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 = () => {
|
const openDiscordOAuth = () => {
|
||||||
window.open(
|
window.open(
|
||||||
`https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`
|
`https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
const sendVerificationCode = async () => {
|
||||||
if (inputs.email === '') return;
|
if (inputs.email === '') return;
|
||||||
@@ -117,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) {
|
||||||
@@ -132,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) {
|
||||||
@@ -148,29 +164,33 @@ const PersonalSetting = () => {
|
|||||||
<div style={{ lineHeight: '40px' }}>
|
<div style={{ lineHeight: '40px' }}>
|
||||||
<Header as='h3'>通用设置</Header>
|
<Header as='h3'>通用设置</Header>
|
||||||
<Message>
|
<Message>
|
||||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI
|
||||||
|
相关的服务,请知悉。
|
||||||
</Message>
|
</Message>
|
||||||
<Button as={Link} to={`/user/edit/`}>
|
<Button as={Link} to={`/user/edit/`}>
|
||||||
更新个人信息
|
更新个人信息
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||||
<Button onClick={() => {
|
<Button
|
||||||
setShowAccountDeleteModal(true);
|
onClick={() => {
|
||||||
}} color='red'>删除个人账户</Button>
|
setShowAccountDeleteModal(true);
|
||||||
|
}}
|
||||||
|
color='red'
|
||||||
|
>
|
||||||
|
删除个人账户
|
||||||
|
</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>账号绑定</Header>
|
<Header as='h3'>账号绑定</Header>
|
||||||
{
|
{status.wechat_login && (
|
||||||
status.wechat_login && (
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => {
|
setShowWeChatBindModal(true);
|
||||||
setShowWeChatBindModal(true);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
绑定微信账号
|
||||||
绑定微信账号
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => setShowWeChatBindModal(false)}
|
onClose={() => setShowWeChatBindModal(false)}
|
||||||
onOpen={() => setShowWeChatBindModal(true)}
|
onOpen={() => setShowWeChatBindModal(true)}
|
||||||
@@ -200,16 +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>
|
||||||
{
|
)}
|
||||||
status.discord_oauth && (
|
|
||||||
<Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEmailBindModal(true);
|
setShowEmailBindModal(true);
|
||||||
@@ -2,7 +2,6 @@ import { Navigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { history } from '../helpers';
|
import { history } from '../helpers';
|
||||||
|
|
||||||
|
|
||||||
function PrivateRoute({ children }) {
|
function PrivateRoute({ children }) {
|
||||||
if (!localStorage.getItem('user')) {
|
if (!localStorage.getItem('user')) {
|
||||||
return <Navigate to='/login' state={{ from: history.location }} />;
|
return <Navigate to='/login' state={{ from: history.location }} />;
|
||||||
@@ -10,4 +9,4 @@ function PrivateRoute({ children }) {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PrivateRoute };
|
export { PrivateRoute };
|
||||||
@@ -1,29 +1,59 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
Message,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
|
import {
|
||||||
|
API,
|
||||||
|
copy,
|
||||||
|
showError,
|
||||||
|
showInfo,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
timestamp2string,
|
||||||
|
} from '../helpers';
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
import { renderQuota } from '../helpers/render';
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return (
|
return <>{timestamp2string(timestamp)}</>;
|
||||||
<>
|
|
||||||
{timestamp2string(timestamp)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatus(status) {
|
function renderStatus(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return <Label basic color='green'>未使用</Label>;
|
return (
|
||||||
|
<Label basic color='green'>
|
||||||
|
未使用
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return <Label basic color='red'> 已禁用 </Label>;
|
return (
|
||||||
|
<Label basic color='red'>
|
||||||
|
{' '}
|
||||||
|
已禁用{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return <Label basic color='grey'> 已使用 </Label>;
|
return (
|
||||||
|
<Label basic color='grey'>
|
||||||
|
{' '}
|
||||||
|
已使用{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <Label basic color='black'> 未知状态 </Label>;
|
return (
|
||||||
|
<Label basic color='black'>
|
||||||
|
{' '}
|
||||||
|
未知状态{' '}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +140,9 @@ const RedemptionsTable = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearching(true);
|
setSearching(true);
|
||||||
const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
|
const res = await API.get(
|
||||||
|
`/api/redemption/search?keyword=${searchKeyword}`,
|
||||||
|
);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setRedemptions(data);
|
setRedemptions(data);
|
||||||
@@ -212,18 +244,26 @@ const RedemptionsTable = () => {
|
|||||||
{redemptions
|
{redemptions
|
||||||
.slice(
|
.slice(
|
||||||
(activePage - 1) * ITEMS_PER_PAGE,
|
(activePage - 1) * ITEMS_PER_PAGE,
|
||||||
activePage * ITEMS_PER_PAGE
|
activePage * ITEMS_PER_PAGE,
|
||||||
)
|
)
|
||||||
.map((redemption, idx) => {
|
.map((redemption, idx) => {
|
||||||
if (redemption.deleted) return <></>;
|
if (redemption.deleted) return <></>;
|
||||||
return (
|
return (
|
||||||
<Table.Row key={redemption.id}>
|
<Table.Row key={redemption.id}>
|
||||||
<Table.Cell>{redemption.id}</Table.Cell>
|
<Table.Cell>{redemption.id}</Table.Cell>
|
||||||
<Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
|
<Table.Cell>
|
||||||
|
{redemption.name ? redemption.name : '无'}
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
|
<Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
|
||||||
<Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
|
<Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
|
<Table.Cell>
|
||||||
<Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
|
{renderTimestamp(redemption.created_time)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{redemption.redeemed_time
|
||||||
|
? renderTimestamp(redemption.redeemed_time)
|
||||||
|
: '尚未兑换'}{' '}
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@@ -233,7 +273,9 @@ const RedemptionsTable = () => {
|
|||||||
if (await copy(redemption.key)) {
|
if (await copy(redemption.key)) {
|
||||||
showSuccess('已复制到剪贴板!');
|
showSuccess('已复制到剪贴板!');
|
||||||
} else {
|
} else {
|
||||||
showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。')
|
showWarning(
|
||||||
|
'无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。',
|
||||||
|
);
|
||||||
setSearchKeyword(redemption.key);
|
setSearchKeyword(redemption.key);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -251,12 +293,12 @@ const RedemptionsTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
disabled={redemption.status === 3} // used
|
disabled={redemption.status === 3} // used
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
manageRedemption(
|
manageRedemption(
|
||||||
redemption.id,
|
redemption.id,
|
||||||
redemption.status === 1 ? 'disable' : 'enable',
|
redemption.status === 1 ? 'disable' : 'enable',
|
||||||
idx
|
idx,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -279,7 +321,12 @@ const RedemptionsTable = () => {
|
|||||||
<Table.Footer>
|
<Table.Footer>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.HeaderCell colSpan='8'>
|
<Table.HeaderCell colSpan='8'>
|
||||||
<Button size='small' as={Link} to='/redemption/add' loading={loading}>
|
<Button
|
||||||
|
size='small'
|
||||||
|
as={Link}
|
||||||
|
to='/redemption/add'
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
添加新的兑换码
|
添加新的兑换码
|
||||||
</Button>
|
</Button>
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -73,7 +73,7 @@ const RegisterForm = () => {
|
|||||||
inputs.aff_code = affCode;
|
inputs.aff_code = affCode;
|
||||||
const res = await API.post(
|
const res = await API.post(
|
||||||
`/api/user/register?turnstile=${turnstileToken}`,
|
`/api/user/register?turnstile=${turnstileToken}`,
|
||||||
inputs
|
inputs,
|
||||||
);
|
);
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -94,7 +94,7 @@ const RegisterForm = () => {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
|
||||||
);
|
);
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -70,7 +70,7 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
const res = await API.put('/api/option/', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value
|
value,
|
||||||
});
|
});
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -135,7 +135,7 @@ const SystemSetting = () => {
|
|||||||
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
||||||
await updateOption(
|
await updateOption(
|
||||||
'WeChatServerAddress',
|
'WeChatServerAddress',
|
||||||
removeTrailingSlash(inputs.WeChatServerAddress)
|
removeTrailingSlash(inputs.WeChatServerAddress),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -144,7 +144,7 @@ const SystemSetting = () => {
|
|||||||
) {
|
) {
|
||||||
await updateOption(
|
await updateOption(
|
||||||
'WeChatAccountQRCodeImageURL',
|
'WeChatAccountQRCodeImageURL',
|
||||||
inputs.WeChatAccountQRCodeImageURL
|
inputs.WeChatAccountQRCodeImageURL,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -265,7 +265,9 @@ const SystemSetting = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
Configure SMTP
|
Configure SMTP
|
||||||
<Header.Subheader>To support the system email sending</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
|
||||||
@@ -318,7 +320,10 @@ const SystemSetting = () => {
|
|||||||
Configure Discord OAuth App
|
Configure Discord OAuth App
|
||||||
<Header.Subheader>
|
<Header.Subheader>
|
||||||
To support login & registration via GitHub,
|
To support login & registration via GitHub,
|
||||||
<a href='https://discord.com/developers/applications' target='_blank'>
|
<a
|
||||||
|
href='https://discord.com/developers/applications'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
Click here
|
Click here
|
||||||
</a>
|
</a>
|
||||||
Manage your Discord OAuth App
|
Manage your Discord OAuth App
|
||||||
@@ -441,7 +446,8 @@ const SystemSetting = () => {
|
|||||||
<a href='https://dash.cloudflare.com/' target='_blank'>
|
<a href='https://dash.cloudflare.com/' target='_blank'>
|
||||||
Click here
|
Click here
|
||||||
</a>
|
</a>
|
||||||
Manage your Turnstile Sites, recommend selecting 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}>
|
||||||
@@ -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,6 +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
|
||||||
|
}
|
||||||
trigger={<span>{renderText(user.username, 10)}</span>}
|
trigger={<span>{renderText(user.username, 10)}</span>}
|
||||||
hoverable
|
hoverable
|
||||||
/>
|
/>
|
||||||
@@ -235,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>
|
||||||
@@ -265,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>
|
||||||
}
|
}
|
||||||
@@ -288,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,8 @@ 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' },
|
||||||
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './toast.constants';
|
export * from './toast.constants';
|
||||||
export * from './user.constants';
|
export * from './user.constants';
|
||||||
export * from './common.constant';
|
export * from './common.constant';
|
||||||
export * from './channel.constants';
|
export * from './channel.constants';
|
||||||
@@ -3,5 +3,5 @@ export const toastConstants = {
|
|||||||
INFO_TIMEOUT: 3000,
|
INFO_TIMEOUT: 3000,
|
||||||
ERROR_TIMEOUT: 5000,
|
ERROR_TIMEOUT: 5000,
|
||||||
WARNING_TIMEOUT: 10000,
|
WARNING_TIMEOUT: 10000,
|
||||||
NOTICE_TIMEOUT: 20000
|
NOTICE_TIMEOUT: 20000,
|
||||||
};
|
};
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export const userConstants = {
|
|
||||||
REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
|
|
||||||
REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
|
|
||||||
REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
|
|
||||||
|
|
||||||
LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
|
|
||||||
LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
|
|
||||||
LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
|
|
||||||
|
|
||||||
LOGOUT: 'USERS_LOGOUT',
|
|
||||||
|
|
||||||
GETALL_REQUEST: 'USERS_GETALL_REQUEST',
|
|
||||||
GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
|
|
||||||
GETALL_FAILURE: 'USERS_GETALL_FAILURE',
|
|
||||||
|
|
||||||
DELETE_REQUEST: 'USERS_DELETE_REQUEST',
|
|
||||||
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
|
|
||||||
DELETE_FAILURE: 'USERS_DELETE_FAILURE'
|
|
||||||
};
|
|
||||||
19
web/src/constants/user.constants.jsx
Normal file
19
web/src/constants/user.constants.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const userConstants = {
|
||||||
|
REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
|
||||||
|
REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
|
||||||
|
REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
|
||||||
|
|
||||||
|
LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
|
||||||
|
LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
|
||||||
|
LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
|
||||||
|
|
||||||
|
LOGOUT: 'USERS_LOGOUT',
|
||||||
|
|
||||||
|
GETALL_REQUEST: 'USERS_GETALL_REQUEST',
|
||||||
|
GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
|
||||||
|
GETALL_FAILURE: 'USERS_GETALL_FAILURE',
|
||||||
|
|
||||||
|
DELETE_REQUEST: 'USERS_DELETE_REQUEST',
|
||||||
|
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
|
||||||
|
DELETE_FAILURE: 'USERS_DELETE_FAILURE',
|
||||||
|
};
|
||||||
@@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => {
|
|||||||
{children}
|
{children}
|
||||||
</StatusContext.Provider>
|
</StatusContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
// contexts/User/index.jsx
|
// contexts/User/index.jsx
|
||||||
|
|
||||||
import React from "react"
|
import React from 'react';
|
||||||
import { reducer, initialState } from "./reducer"
|
import { reducer, initialState } from './reducer';
|
||||||
|
|
||||||
export const UserContext = React.createContext({
|
export const UserContext = React.createContext({
|
||||||
state: initialState,
|
state: initialState,
|
||||||
dispatch: () => null
|
dispatch: () => null,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const UserProvider = ({ children }) => {
|
export const UserProvider = ({ children }) => {
|
||||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
const [state, dispatch] = React.useReducer(reducer, initialState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={[ state, dispatch ]}>
|
<UserContext.Provider value={[state, dispatch]}>
|
||||||
{ children }
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
|
|||||||
case 'login':
|
case 'login':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: action.payload
|
user: action.payload,
|
||||||
};
|
};
|
||||||
case 'logout':
|
case 'logout':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: undefined
|
user: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
user: undefined
|
user: undefined,
|
||||||
};
|
};
|
||||||
@@ -2,12 +2,12 @@ import { showError } from './utils';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export const API = axios.create({
|
export const API = axios.create({
|
||||||
baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '',
|
baseURL: import.meta.env.VITE_REACT_APP_SERVER ? import.meta.env.VITE_REACT_APP_SERVER : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
API.interceptors.response.use(
|
API.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
showError(error);
|
showError(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export function authHeader() {
|
|
||||||
// return authorization header with jwt token
|
|
||||||
let user = JSON.parse(localStorage.getItem('user'));
|
|
||||||
|
|
||||||
if (user && user.token) {
|
|
||||||
return { 'Authorization': 'Bearer ' + user.token };
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
web/src/helpers/auth-header.jsx
Normal file
10
web/src/helpers/auth-header.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function authHeader() {
|
||||||
|
// return authorization header with jwt token
|
||||||
|
let user = JSON.parse(localStorage.getItem('user'));
|
||||||
|
|
||||||
|
if (user && user.token) {
|
||||||
|
return { Authorization: 'Bearer ' + user.token };
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
export const history = createBrowserHistory();
|
export const history = createBrowserHistory();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './auth-header';
|
export * from './auth-header';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './api';
|
export * from './api';
|
||||||
@@ -13,16 +13,18 @@ export function renderGroup(group) {
|
|||||||
}
|
}
|
||||||
let groups = group.split(',');
|
let groups = group.split(',');
|
||||||
groups.sort();
|
groups.sort();
|
||||||
return <>
|
return (
|
||||||
{groups.map((group) => {
|
<>
|
||||||
if (group === 'vip' || group === 'pro') {
|
{groups.map((group) => {
|
||||||
return <Label color='yellow'>{group}</Label>;
|
if (group === 'vip' || group === 'pro') {
|
||||||
} else if (group === 'svip' || group === 'premium') {
|
return <Label color='yellow'>{group}</Label>;
|
||||||
return <Label color='red'>{group}</Label>;
|
} else if (group === 'svip' || group === 'premium') {
|
||||||
}
|
return <Label color='red'>{group}</Label>;
|
||||||
return <Label>{group}</Label>;
|
}
|
||||||
})}
|
return <Label>{group}</Label>;
|
||||||
</>;
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderNumber(num) {
|
export function renderNumber(num) {
|
||||||
@@ -55,4 +57,4 @@ export function renderQuotaWithPrompt(quota, digits) {
|
|||||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export function getSystemName() {
|
|||||||
export function getLogo() {
|
export function getLogo() {
|
||||||
let logo = localStorage.getItem('logo');
|
let logo = localStorage.getItem('logo');
|
||||||
if (!logo) return '/logo.png';
|
if (!logo) return '/logo.png';
|
||||||
return logo
|
return logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFooterHTML() {
|
export function getFooterHTML() {
|
||||||
@@ -147,17 +147,7 @@ export function timestamp2string(timestamp) {
|
|||||||
second = '0' + second;
|
second = '0' + second;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
year +
|
year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
|
||||||
'-' +
|
|
||||||
month +
|
|
||||||
'-' +
|
|
||||||
day +
|
|
||||||
' ' +
|
|
||||||
hour +
|
|
||||||
':' +
|
|
||||||
minute +
|
|
||||||
':' +
|
|
||||||
second
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,4 +167,4 @@ export const verifyJSON = (str) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 55px;
|
padding-top: 55px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
|
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
|
||||||
-webkit-font-smoothing: antialiased;
|
sans-serif;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-webkit-font-smoothing: antialiased;
|
||||||
scrollbar-width: none;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar {
|
body::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-icon .icon {
|
.small-icon .icon {
|
||||||
font-size: 1em !important;
|
font-size: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-footer {
|
.custom-footer {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
.hide-on-mobile {
|
.hide-on-mobile {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,5 @@ root.render(
|
|||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</StatusProvider>
|
</StatusProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
@@ -31,8 +31,8 @@ const About = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{aboutLoaded && about === '' ? (
|
||||||
aboutLoaded && about === '' ? <>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>关于</Header>
|
<Header as='h3'>关于</Header>
|
||||||
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
||||||
@@ -41,20 +41,26 @@ const About = () => {
|
|||||||
https://github.com/songquanpeng/one-api
|
https://github.com/songquanpeng/one-api
|
||||||
</a>
|
</a>
|
||||||
</Segment>
|
</Segment>
|
||||||
</> : <>
|
</>
|
||||||
{
|
) : (
|
||||||
about.startsWith('https://') ? <iframe
|
<>
|
||||||
|
{about.startsWith('https://') ? (
|
||||||
|
<iframe
|
||||||
src={about}
|
src={about}
|
||||||
style={{ width: '100%', height: '100vh', border: 'none' }}
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
/> : <Segment>
|
/>
|
||||||
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
) : (
|
||||||
|
<Segment>
|
||||||
|
<div
|
||||||
|
style={{ fontSize: 'larger' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: about }}
|
||||||
|
></div>
|
||||||
</Segment>
|
</Segment>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Header,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Segment,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
|
import {
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showInfo,
|
||||||
|
showSuccess,
|
||||||
|
verifyJSON,
|
||||||
|
} from '../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../constants';
|
||||||
|
|
||||||
const MODEL_MAPPING_EXAMPLE = {
|
const MODEL_MAPPING_EXAMPLE = {
|
||||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
||||||
'gpt-4-0314': 'gpt-4',
|
'gpt-4-0314': 'gpt-4',
|
||||||
'gpt-4-32k-0314': 'gpt-4-32k'
|
'gpt-4-32k-0314': 'gpt-4-32k',
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
@@ -23,7 +36,8 @@ const EditChannel = () => {
|
|||||||
other: '',
|
other: '',
|
||||||
model_mapping: '',
|
model_mapping: '',
|
||||||
models: [],
|
models: [],
|
||||||
groups: ['default']
|
groups: ['default'],
|
||||||
|
enable_ip_randomization: false,
|
||||||
};
|
};
|
||||||
const [batch, setBatch] = useState(false);
|
const [batch, setBatch] = useState(false);
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
@@ -31,7 +45,9 @@ const EditChannel = () => {
|
|||||||
const [groupOptions, setGroupOptions] = useState([]);
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
const [basicModels, setBasicModels] = useState([]);
|
const [basicModels, setBasicModels] = useState([]);
|
||||||
const [fullModels, setFullModels] = useState([]);
|
const [fullModels, setFullModels] = 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 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +59,19 @@ const EditChannel = () => {
|
|||||||
data.models = [];
|
data.models = [];
|
||||||
} else {
|
} else {
|
||||||
data.models = data.models.split(',');
|
data.models = data.models.split(',');
|
||||||
|
// setTimeout(() => {
|
||||||
|
// let localModelOptions = [...modelOptions];
|
||||||
|
// data.models.forEach((model) => {
|
||||||
|
// if (!localModelOptions.find((option) => option.key === model)) {
|
||||||
|
// localModelOptions.push({
|
||||||
|
// key: model,
|
||||||
|
// text: model,
|
||||||
|
// value: model,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// setModelOptions(localModelOptions);
|
||||||
|
// }, 1000);
|
||||||
}
|
}
|
||||||
if (data.group === '') {
|
if (data.group === '') {
|
||||||
data.groups = [];
|
data.groups = [];
|
||||||
@@ -50,7 +79,11 @@ const EditChannel = () => {
|
|||||||
data.groups = data.group.split(',');
|
data.groups = data.group.split(',');
|
||||||
}
|
}
|
||||||
if (data.model_mapping !== '') {
|
if (data.model_mapping !== '') {
|
||||||
data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
|
data.model_mapping = JSON.stringify(
|
||||||
|
JSON.parse(data.model_mapping),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -62,13 +95,19 @@ 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) => ({
|
setModelOptions(
|
||||||
key: model.id,
|
res.data.data.map((model) => ({
|
||||||
text: model.id,
|
key: model.id,
|
||||||
value: model.id
|
text: model.id,
|
||||||
})));
|
value: model.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
setFullModels(res.data.data.map((model) => 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));
|
setBasicModels(
|
||||||
|
res.data.data
|
||||||
|
.filter((model) => !model.id.startsWith('gpt-4'))
|
||||||
|
.map((model) => model.id),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
@@ -77,11 +116,13 @@ const EditChannel = () => {
|
|||||||
const fetchGroups = async () => {
|
const fetchGroups = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await API.get(`/api/group/`);
|
let res = await API.get(`/api/group/`);
|
||||||
setGroupOptions(res.data.data.map((group) => ({
|
setGroupOptions(
|
||||||
key: group,
|
res.data.data.map((group) => ({
|
||||||
text: group,
|
key: group,
|
||||||
value: group
|
text: group,
|
||||||
})));
|
value: group,
|
||||||
|
})),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
@@ -110,7 +151,10 @@ const EditChannel = () => {
|
|||||||
}
|
}
|
||||||
let localInputs = inputs;
|
let localInputs = inputs;
|
||||||
if (localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(
|
||||||
|
0,
|
||||||
|
localInputs.base_url.length - 1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (localInputs.type === 3 && localInputs.other === '') {
|
if (localInputs.type === 3 && localInputs.other === '') {
|
||||||
localInputs.other = '2023-03-15-preview';
|
localInputs.other = '2023-03-15-preview';
|
||||||
@@ -119,7 +163,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);
|
||||||
}
|
}
|
||||||
@@ -151,65 +198,74 @@ const EditChannel = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
{inputs.type === 3 && (
|
||||||
inputs.type === 3 && (
|
<>
|
||||||
<>
|
<Message>
|
||||||
<Message>
|
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为
|
||||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
|
One API 会把请求体中的 model
|
||||||
参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
|
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
|
<a
|
||||||
</Message>
|
target='_blank'
|
||||||
<Form.Field>
|
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
|
||||||
<Form.Input
|
>
|
||||||
label='AZURE_OPENAI_ENDPOINT'
|
图片演示
|
||||||
name='base_url'
|
</a>
|
||||||
placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
|
。
|
||||||
onChange={handleInputChange}
|
</Message>
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='默认 API 版本'
|
|
||||||
name='other'
|
|
||||||
placeholder={'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={inputs.other}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
inputs.type === 8 && (
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='AZURE_OPENAI_ENDPOINT'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
placeholder={
|
||||||
|
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
||||||
|
}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='镜像'
|
label='默认 API 版本'
|
||||||
name='base_url'
|
name='other'
|
||||||
placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'}
|
placeholder={
|
||||||
|
'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||||
|
}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
</>
|
||||||
}
|
)}
|
||||||
|
{inputs.type === 8 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='Base URL'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={
|
||||||
|
'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'
|
||||||
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)}
|
||||||
|
{inputs.type !== 3 && inputs.type !== 8 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='镜像'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={
|
||||||
|
'此项可选,输入镜像站地址,格式为:https://domain.com'
|
||||||
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)}
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
@@ -254,20 +310,79 @@ const EditChannel = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button
|
||||||
handleInputChange(null, { name: 'models', value: basicModels });
|
type={'button'}
|
||||||
}}>填入基础模型</Button>
|
onClick={() => {
|
||||||
<Button type={'button'} onClick={() => {
|
handleInputChange(null, { name: 'models', value: basicModels });
|
||||||
handleInputChange(null, { name: 'models', value: fullModels });
|
}}
|
||||||
}}>填入所有模型</Button>
|
>
|
||||||
<Button type={'button'} onClick={() => {
|
填入基础模型
|
||||||
handleInputChange(null, { name: 'models', value: [] });
|
</Button>
|
||||||
}}>清除所有模型</Button>
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: fullModels });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入所有模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: [] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清除所有模型
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
let localModels = [...inputs.models];
|
||||||
|
localModels.push(customModel);
|
||||||
|
let localModelOptions = [...modelOptions];
|
||||||
|
localModelOptions.push({
|
||||||
|
key: customModel,
|
||||||
|
text: customModel,
|
||||||
|
value: customModel,
|
||||||
|
});
|
||||||
|
setModelOptions(localModelOptions);
|
||||||
|
handleInputChange(null, {
|
||||||
|
name: 'models',
|
||||||
|
value: localModels,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
placeholder='输入自定义模型名称'
|
||||||
|
value={customModel}
|
||||||
|
onChange={(e, { value }) => {
|
||||||
|
setCustomModel(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</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}
|
||||||
@@ -275,19 +390,23 @@ const EditChannel = () => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
{batch ? (
|
||||||
batch ? <Form.Field>
|
<Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
required
|
|
||||||
placeholder={'请输入密钥,一行一个'}
|
placeholder={'请输入密钥,一行一个'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
style={{
|
||||||
|
minHeight: 150,
|
||||||
|
fontFamily: 'JetBrains Mono, Consolas',
|
||||||
|
}}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field> : <Form.Field>
|
</Form.Field>
|
||||||
|
) : (
|
||||||
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
@@ -298,18 +417,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 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>
|
||||||
@@ -121,16 +121,22 @@ const Home = () => {
|
|||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Segment>
|
</Segment>
|
||||||
</> : <>
|
</>
|
||||||
{
|
) : (
|
||||||
homePageContent.startsWith('https://') ? <iframe
|
<>
|
||||||
|
{homePageContent.startsWith('https://') ? (
|
||||||
|
<iframe
|
||||||
src={homePageContent}
|
src={homePageContent}
|
||||||
style={{ width: '100%', height: '100vh', border: 'none' }}
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
|
/>
|
||||||
}
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ fontSize: 'larger' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: homePageContent }}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Segment, Header } from 'semantic-ui-react';
|
|
||||||
|
|
||||||
const NotFound = () => (
|
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
block
|
|
||||||
as="h4"
|
|
||||||
content="404"
|
|
||||||
attached="top"
|
|
||||||
icon="info"
|
|
||||||
className="small-icon"
|
|
||||||
/>
|
|
||||||
<Segment attached="bottom">
|
|
||||||
未找到所请求的页面
|
|
||||||
</Segment>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default NotFound;
|
|
||||||
18
web/src/pages/NotFound/index.jsx
Normal file
18
web/src/pages/NotFound/index.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Segment, Header } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
const NotFound = () => (
|
||||||
|
<>
|
||||||
|
<Header
|
||||||
|
block
|
||||||
|
as='h4'
|
||||||
|
content='404'
|
||||||
|
attached='top'
|
||||||
|
icon='info'
|
||||||
|
className='small-icon'
|
||||||
|
/>
|
||||||
|
<Segment attached='bottom'>未找到所请求的页面</Segment>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@@ -12,7 +12,7 @@ const EditRedemption = () => {
|
|||||||
const originInputs = {
|
const originInputs = {
|
||||||
name: '',
|
name: '',
|
||||||
quota: 100000,
|
quota: 100000,
|
||||||
count: 1
|
count: 1,
|
||||||
};
|
};
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, quota, count } = inputs;
|
const { name, quota, count } = inputs;
|
||||||
@@ -44,10 +44,13 @@ const EditRedemption = () => {
|
|||||||
localInputs.quota = parseInt(localInputs.quota);
|
localInputs.quota = parseInt(localInputs.quota);
|
||||||
let res;
|
let res;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(redemptionId) });
|
res = await API.put(`/api/redemption/`, {
|
||||||
|
...localInputs,
|
||||||
|
id: parseInt(redemptionId),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res = await API.post(`/api/redemption/`, {
|
res = await API.post(`/api/redemption/`, {
|
||||||
...localInputs
|
...localInputs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
@@ -62,9 +65,9 @@ const EditRedemption = () => {
|
|||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
if (!isEdit && data) {
|
if (!isEdit && data) {
|
||||||
let text = "";
|
let text = '';
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
text += data[i] + "\n";
|
text += data[i] + '\n';
|
||||||
}
|
}
|
||||||
downloadTextAsFile(text, `${inputs.name}.txt`);
|
downloadTextAsFile(text, `${inputs.name}.txt`);
|
||||||
}
|
}
|
||||||
@@ -97,8 +100,8 @@ const EditRedemption = () => {
|
|||||||
type='number'
|
type='number'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
{!isEdit && (
|
||||||
!isEdit && <>
|
<>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='生成数量'
|
label='生成数量'
|
||||||
@@ -111,8 +114,10 @@ const EditRedemption = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button positive onClick={submit}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
@@ -6,7 +6,7 @@ const Redemption = () => (
|
|||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>管理兑换码</Header>
|
<Header as='h3'>管理兑换码</Header>
|
||||||
<RedemptionsTable/>
|
<RedemptionsTable />
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -14,8 +14,8 @@ const Setting = () => {
|
|||||||
<Tab.Pane attached={false}>
|
<Tab.Pane attached={false}>
|
||||||
<PersonalSetting />
|
<PersonalSetting />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isRoot()) {
|
if (isRoot()) {
|
||||||
@@ -25,7 +25,7 @@ const Setting = () => {
|
|||||||
<Tab.Pane attached={false}>
|
<Tab.Pane attached={false}>
|
||||||
<OperationSetting />
|
<OperationSetting />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
menuItem: '系统设置',
|
menuItem: '系统设置',
|
||||||
@@ -33,7 +33,7 @@ const Setting = () => {
|
|||||||
<Tab.Pane attached={false}>
|
<Tab.Pane attached={false}>
|
||||||
<SystemSetting />
|
<SystemSetting />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
menuItem: '其他设置',
|
menuItem: '其他设置',
|
||||||
@@ -41,7 +41,7 @@ const Setting = () => {
|
|||||||
<Tab.Pane attached={false}>
|
<Tab.Pane attached={false}>
|
||||||
<OtherSetting />
|
<OtherSetting />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
|
||||||
|
|
||||||
const EditToken = () => {
|
|
||||||
const params = useParams();
|
|
||||||
const tokenId = params.id;
|
|
||||||
const isEdit = tokenId !== undefined;
|
|
||||||
const [loading, setLoading] = useState(isEdit);
|
|
||||||
const originInputs = {
|
|
||||||
name: '',
|
|
||||||
remain_quota: isEdit ? 0 : 500000,
|
|
||||||
expired_time: -1,
|
|
||||||
unlimited_quota: false
|
|
||||||
};
|
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
|
||||||
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setExpiredTime = (month, day, hour, minute) => {
|
|
||||||
let now = new Date();
|
|
||||||
let timestamp = now.getTime() / 1000;
|
|
||||||
let seconds = month * 30 * 24 * 60 * 60;
|
|
||||||
seconds += day * 24 * 60 * 60;
|
|
||||||
seconds += hour * 60 * 60;
|
|
||||||
seconds += minute * 60;
|
|
||||||
if (seconds !== 0) {
|
|
||||||
timestamp += seconds;
|
|
||||||
setInputs({ ...inputs, expired_time: timestamp2string(timestamp) });
|
|
||||||
} else {
|
|
||||||
setInputs({ ...inputs, expired_time: -1 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUnlimitedQuota = () => {
|
|
||||||
setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadToken = async () => {
|
|
||||||
let res = await API.get(`/api/token/${tokenId}`);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
if (data.expired_time !== -1) {
|
|
||||||
data.expired_time = timestamp2string(data.expired_time);
|
|
||||||
}
|
|
||||||
setInputs(data);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit) {
|
|
||||||
loadToken().then();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!isEdit && inputs.name === '') return;
|
|
||||||
let localInputs = inputs;
|
|
||||||
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
|
||||||
if (localInputs.expired_time !== -1) {
|
|
||||||
let time = Date.parse(localInputs.expired_time);
|
|
||||||
if (isNaN(time)) {
|
|
||||||
showError('过期时间格式错误!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localInputs.expired_time = Math.ceil(time / 1000);
|
|
||||||
}
|
|
||||||
let res;
|
|
||||||
if (isEdit) {
|
|
||||||
res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) });
|
|
||||||
} else {
|
|
||||||
res = await API.post(`/api/token/`, localInputs);
|
|
||||||
}
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
if (isEdit) {
|
|
||||||
showSuccess('令牌更新成功!');
|
|
||||||
} else {
|
|
||||||
showSuccess('令牌创建成功!');
|
|
||||||
setInputs(originInputs);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Segment loading={loading}>
|
|
||||||
<Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header>
|
|
||||||
<Form autoComplete='new-password'>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='名称'
|
|
||||||
name='name'
|
|
||||||
placeholder={'请输入名称'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={name}
|
|
||||||
autoComplete='new-password'
|
|
||||||
required={!isEdit}
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='过期时间'
|
|
||||||
name='expired_time'
|
|
||||||
placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={expired_time}
|
|
||||||
autoComplete='new-password'
|
|
||||||
type='datetime-local'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<div style={{ lineHeight: '40px' }}>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setExpiredTime(0, 0, 0, 0);
|
|
||||||
}}>永不过期</Button>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setExpiredTime(1, 0, 0, 0);
|
|
||||||
}}>一个月后过期</Button>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setExpiredTime(0, 1, 0, 0);
|
|
||||||
}}>一天后过期</Button>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setExpiredTime(0, 0, 1, 0);
|
|
||||||
}}>一小时后过期</Button>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setExpiredTime(0, 0, 0, 1);
|
|
||||||
}}>一分钟后过期</Button>
|
|
||||||
</div>
|
|
||||||
<Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label={`额度${renderQuotaWithPrompt(remain_quota)}`}
|
|
||||||
name='remain_quota'
|
|
||||||
placeholder={'请输入额度'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={remain_quota}
|
|
||||||
autoComplete='new-password'
|
|
||||||
type='number'
|
|
||||||
disabled={unlimited_quota}
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Button type={'button'} onClick={() => {
|
|
||||||
setUnlimitedQuota();
|
|
||||||
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
|
||||||
<Button positive onClick={submit}>提交</Button>
|
|
||||||
</Form>
|
|
||||||
</Segment>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditToken;
|
|
||||||
288
web/src/pages/Token/EditToken.jsx
Normal file
288
web/src/pages/Token/EditToken.jsx
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||||
|
import { renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
|
const EditToken = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const tokenId = params.id;
|
||||||
|
const isEdit = tokenId !== undefined;
|
||||||
|
const [loading, setLoading] = useState(isEdit);
|
||||||
|
const originInputs = {
|
||||||
|
name: '',
|
||||||
|
remain_quota: isEdit ? 0 : 500000,
|
||||||
|
expired_time: -1,
|
||||||
|
unlimited_quota: false,
|
||||||
|
models: isEdit
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
'gpt-3.5-turbo',
|
||||||
|
'gpt-3.5-turbo-0301',
|
||||||
|
'gpt-3.5-turbo-0613',
|
||||||
|
'gpt-3.5-turbo-16k',
|
||||||
|
'gpt-3.5-turbo-16k-0613',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const [modelOptions, setModelOptions] = useState([]);
|
||||||
|
const [basicModels, setBasicModels] = useState([]);
|
||||||
|
const [fullModels, setFullModels] = useState([]);
|
||||||
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
|
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
||||||
|
|
||||||
|
const handleInputChange = (e, { name, value }) => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setExpiredTime = (month, day, hour, minute) => {
|
||||||
|
let now = new Date();
|
||||||
|
let timestamp = now.getTime() / 1000;
|
||||||
|
let seconds = month * 30 * 24 * 60 * 60;
|
||||||
|
seconds += day * 24 * 60 * 60;
|
||||||
|
seconds += hour * 60 * 60;
|
||||||
|
seconds += minute * 60;
|
||||||
|
if (seconds !== 0) {
|
||||||
|
timestamp += seconds;
|
||||||
|
setInputs({ ...inputs, expired_time: timestamp2string(timestamp) });
|
||||||
|
} else {
|
||||||
|
setInputs({ ...inputs, expired_time: -1 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUnlimitedQuota = () => {
|
||||||
|
setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchModels = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/channel/models`);
|
||||||
|
setModelOptions(
|
||||||
|
res.data.data.map((model) => ({
|
||||||
|
key: model.id,
|
||||||
|
text: model.id,
|
||||||
|
value: model.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setFullModels(res.data.data.map((model) => model.id));
|
||||||
|
setBasicModels(
|
||||||
|
res.data.data
|
||||||
|
.filter((model) => !model.id.startsWith('gpt-4'))
|
||||||
|
.map((model) => model.id),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadToken = async () => {
|
||||||
|
let res = await API.get(`/api/token/${tokenId}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (data.expired_time !== -1) {
|
||||||
|
data.expired_time = timestamp2string(data.expired_time);
|
||||||
|
}
|
||||||
|
if (data.models === '') {
|
||||||
|
data.models = [];
|
||||||
|
} else {
|
||||||
|
data.models = data.models.split(',');
|
||||||
|
}
|
||||||
|
setInputs(data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEdit) {
|
||||||
|
loadToken().then();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!isEdit && inputs.name === '') return;
|
||||||
|
let localInputs = inputs;
|
||||||
|
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
||||||
|
if (localInputs.expired_time !== -1) {
|
||||||
|
let time = Date.parse(localInputs.expired_time);
|
||||||
|
if (isNaN(time)) {
|
||||||
|
showError('过期时间格式错误!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localInputs.expired_time = Math.ceil(time / 1000);
|
||||||
|
}
|
||||||
|
if (inputs.models.length === 0) {
|
||||||
|
showError('请至少选择一个模型!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localInputs.models = localInputs.models.join(',');
|
||||||
|
let res;
|
||||||
|
if (isEdit) {
|
||||||
|
res = await API.put(`/api/token/`, {
|
||||||
|
...localInputs,
|
||||||
|
id: parseInt(tokenId),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await API.post(`/api/token/`, localInputs);
|
||||||
|
}
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (isEdit) {
|
||||||
|
showSuccess('令牌更新成功!');
|
||||||
|
} else {
|
||||||
|
showSuccess('令牌创建成功!');
|
||||||
|
setInputs(originInputs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchModels().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Segment loading={loading}>
|
||||||
|
<Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header>
|
||||||
|
<Form autoComplete='new-password'>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='名称'
|
||||||
|
name='name'
|
||||||
|
placeholder={'请输入名称'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={name}
|
||||||
|
autoComplete='new-password'
|
||||||
|
required={!isEdit}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='过期时间'
|
||||||
|
name='expired_time'
|
||||||
|
placeholder={
|
||||||
|
'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'
|
||||||
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={expired_time}
|
||||||
|
autoComplete='new-password'
|
||||||
|
type='datetime-local'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<div style={{ lineHeight: '40px' }}>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setExpiredTime(0, 0, 0, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
永不过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setExpiredTime(1, 0, 0, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一个月后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setExpiredTime(0, 1, 0, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一天后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setExpiredTime(0, 0, 1, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一小时后过期
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setExpiredTime(0, 0, 0, 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一分钟后过期
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Message>
|
||||||
|
注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
|
||||||
|
</Message>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label={`额度${renderQuotaWithPrompt(remain_quota)}`}
|
||||||
|
name='remain_quota'
|
||||||
|
placeholder={'请输入额度'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={remain_quota}
|
||||||
|
autoComplete='new-password'
|
||||||
|
type='number'
|
||||||
|
disabled={unlimited_quota}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
setUnlimitedQuota();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{unlimited_quota ? '取消无限额度' : '设置为无限额度'}
|
||||||
|
</Button>
|
||||||
|
<Form.Field style={{ marginTop: '12px' }}>
|
||||||
|
<Form.Dropdown
|
||||||
|
label='模型'
|
||||||
|
placeholder={'请选择此密钥支持的模型'}
|
||||||
|
name='models'
|
||||||
|
required
|
||||||
|
fluid
|
||||||
|
multiple
|
||||||
|
selection
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.models}
|
||||||
|
autoComplete='new-password'
|
||||||
|
options={modelOptions}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: basicModels });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入基础模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: fullModels });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入所有模型
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: [] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清除所有模型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button positive onClick={submit}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Segment>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditToken;
|
||||||
@@ -6,7 +6,7 @@ const Token = () => (
|
|||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>我的令牌</Header>
|
<Header as='h3'>我的令牌</Header>
|
||||||
<TokensTable/>
|
<TokensTable />
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Segment,
|
||||||
|
Statistic,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
||||||
import { renderQuota } from '../../helpers/render';
|
import { renderQuota } from '../../helpers/render';
|
||||||
|
|
||||||
@@ -10,11 +17,11 @@ const TopUp = () => {
|
|||||||
|
|
||||||
const topUp = async () => {
|
const topUp = async () => {
|
||||||
if (redemptionCode === '') {
|
if (redemptionCode === '') {
|
||||||
showInfo('请输入充值码!')
|
showInfo('请输入充值码!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await API.post('/api/user/topup', {
|
const res = await API.post('/api/user/topup', {
|
||||||
key: redemptionCode
|
key: redemptionCode,
|
||||||
});
|
});
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -36,15 +43,15 @@ const TopUp = () => {
|
|||||||
window.open(topUpLink, '_blank');
|
window.open(topUpLink, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserQuota = async ()=>{
|
const getUserQuota = async () => {
|
||||||
let res = await API.get(`/api/user/self`);
|
let res = await API.get(`/api/user/self`);
|
||||||
const {success, message, data} = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setUserQuota(data.quota);
|
setUserQuota(data.quota);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -92,5 +99,4 @@ const TopUp = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default TopUp;
|
export default TopUp;
|
||||||
@@ -30,38 +30,38 @@ const AddUser = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as="h3">创建新用户账户</Header>
|
<Header as='h3'>创建新用户账户</Header>
|
||||||
<Form autoComplete="off">
|
<Form autoComplete='off'>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="用户名"
|
label='用户名'
|
||||||
name="username"
|
name='username'
|
||||||
placeholder={'请输入用户名'}
|
placeholder={'请输入用户名'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={username}
|
value={username}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="显示名称"
|
label='显示名称'
|
||||||
name="display_name"
|
name='display_name'
|
||||||
placeholder={'请输入显示名称'}
|
placeholder={'请输入显示名称'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={display_name}
|
value={display_name}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label="密码"
|
label='密码'
|
||||||
name="password"
|
name='password'
|
||||||
type={'password'}
|
type={'password'}
|
||||||
placeholder={'请输入密码'}
|
placeholder={'请输入密码'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={password}
|
value={password}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
@@ -17,22 +17,32 @@ const EditUser = () => {
|
|||||||
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, discord_id } =
|
const {
|
||||||
inputs;
|
username,
|
||||||
|
display_name,
|
||||||
|
password,
|
||||||
|
github_id,
|
||||||
|
wechat_id,
|
||||||
|
email,
|
||||||
|
quota,
|
||||||
|
discord_id,
|
||||||
|
} = inputs;
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
const fetchGroups = async () => {
|
const fetchGroups = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await API.get(`/api/group/`);
|
let res = await API.get(`/api/group/`);
|
||||||
setGroupOptions(res.data.data.map((group) => ({
|
setGroupOptions(
|
||||||
key: group,
|
res.data.data.map((group) => ({
|
||||||
text: group,
|
key: group,
|
||||||
value: group,
|
text: group,
|
||||||
})));
|
value: group,
|
||||||
|
})),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
@@ -116,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='分组'
|
||||||
@@ -146,7 +156,7 @@ const EditUser = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='已绑定的 GitHub 账户'
|
label='已绑定的 GitHub 账户'
|
||||||
@@ -187,7 +197,9 @@ const EditUser = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button positive onClick={submit}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
@@ -6,7 +6,7 @@ const User = () => (
|
|||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>管理用户</Header>
|
<Header as='h3'>管理用户</Header>
|
||||||
<UsersTable/>
|
<UsersTable />
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
11
web/vite.config.js
Normal file
11
web/vite.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
minify: 'terser',
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user