mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-08 09:43:42 +08:00
Compare commits
21 Commits
v0.5.3-alp
...
v0.5.3-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bddc73b96 | ||
|
|
2a527ee436 | ||
|
|
e42119b73d | ||
|
|
821c559e89 | ||
|
|
7e2bca7e9c | ||
|
|
1e16ef3e0d | ||
|
|
476a46ad7e | ||
|
|
c58f710227 | ||
|
|
150d068e9f | ||
|
|
be780462f1 | ||
|
|
f2159e1033 | ||
|
|
466005de07 | ||
|
|
2b088a1678 | ||
|
|
3a18cebe34 | ||
|
|
cc36bf9c13 | ||
|
|
3b36608bbd | ||
|
|
29fa94e7d2 | ||
|
|
9c436921d1 | ||
|
|
463b0b3c51 | ||
|
|
c3d85a28d4 | ||
|
|
7422b0d051 |
@@ -4,7 +4,7 @@ WORKDIR /build
|
|||||||
COPY ./web .
|
COPY ./web .
|
||||||
COPY ./VERSION .
|
COPY ./VERSION .
|
||||||
RUN npm install
|
RUN npm install
|
||||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
|
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
|
||||||
|
|
||||||
FROM golang AS builder2
|
FROM golang AS builder2
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co
|
|||||||
> Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage.
|
> Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage.
|
||||||
|
|
||||||
1. First, fork the code.
|
1. First, fork the code.
|
||||||
2. Go to [Zeabur](https://zeabur.com/), log in, and enter the console.
|
2. Go to [Zeabur](https://zeabur.com?referralCode=songquanpeng), log in, and enter the console.
|
||||||
3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port).
|
3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port).
|
||||||
4. Copy the connection parameters and run ```create database `one-api` ``` to create the database.
|
4. Copy the connection parameters and run ```create database `one-api` ``` to create the database.
|
||||||
5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository.
|
5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository.
|
||||||
@@ -283,7 +283,7 @@ If the channel ID is not provided, load balancing will be used to distribute the
|
|||||||
+ Double-check that your interface address and API Key are correct.
|
+ Double-check that your interface address and API Key are correct.
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
[FastGPT](https://github.com/c121914yu/FastGPT): Build an AI knowledge base in three minutes
|
[FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
|
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -102,16 +102,16 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
||||||
|
|
||||||
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
|
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
||||||
|
|
||||||
如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
|
|
||||||
|
|
||||||
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
|
|
||||||
|
|
||||||
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
|
||||||
|
|
||||||
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
||||||
|
|
||||||
|
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
|
||||||
|
|
||||||
|
如果你的并发量较大,**务必**设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
|
||||||
|
|
||||||
|
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
|
||||||
|
|
||||||
Nginx 的参考配置:
|
Nginx 的参考配置:
|
||||||
```
|
```
|
||||||
server{
|
server{
|
||||||
@@ -213,7 +213,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
|
|
||||||
> Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。
|
> Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。
|
||||||
|
|
||||||
点击以下按钮一键部署:
|
点击以下按钮一键部署(部署后访问出现 404 请等待 3~5 分钟):
|
||||||
|
|
||||||
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
|
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
|
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
|
||||||
|
|
||||||
1. 首先 fork 一份代码。
|
1. 首先 fork 一份代码。
|
||||||
2. 进入 [Zeabur](https://zeabur.com/),登录,进入控制台。
|
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
|
||||||
3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。
|
3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。
|
||||||
4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。
|
4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。
|
||||||
5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。
|
5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。
|
||||||
@@ -281,6 +281,11 @@ graph LR
|
|||||||
+ 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
|
+ 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
|
||||||
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
|
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
|
||||||
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
|
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
|
||||||
|
+ 请根据你的数据库配置修改下列参数(或者保持默认值):
|
||||||
|
+ `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `10`。
|
||||||
|
+ `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `100`。
|
||||||
|
+ 如果报错 `Error 1040: Too many connections`,请适当减小该值。
|
||||||
|
+ `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。
|
||||||
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
|
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
|
||||||
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
|
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
|
||||||
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
|
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
|
||||||
@@ -333,7 +338,8 @@ https://openai.justsong.cn
|
|||||||
+ 上游通道 429 了。
|
+ 上游通道 429 了。
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库
|
* [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统
|
||||||
|
* [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web): 一键拥有你自己的跨平台 ChatGPT 应用
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ var EmailDomainWhitelist = []string{
|
|||||||
"foxmail.com",
|
"foxmail.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var DebugEnabled = os.Getenv("DEBUG") == "true"
|
||||||
|
|
||||||
var LogConsumeEnabled = true
|
var LogConsumeEnabled = true
|
||||||
|
|
||||||
var SMTPServer = ""
|
var SMTPServer = ""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -177,3 +178,15 @@ func Max(a int, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrDefault(env string, defaultValue int) int {
|
||||||
|
if env == "" || os.Getenv(env) == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
num, err := strconv.Atoi(os.Getenv(env))
|
||||||
|
if err != nil {
|
||||||
|
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|||||||
@@ -121,7 +121,10 @@ func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse {
|
|||||||
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
|
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = aliResponse.Output.Text
|
choice.Delta.Content = aliResponse.Output.Text
|
||||||
choice.FinishReason = aliResponse.Output.FinishReason
|
if aliResponse.Output.FinishReason != "null" {
|
||||||
|
finishReason := aliResponse.Output.FinishReason
|
||||||
|
choice.FinishReason = &finishReason
|
||||||
|
}
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Id: aliResponse.RequestId,
|
Id: aliResponse.RequestId,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse {
|
|||||||
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
|
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = baiduResponse.Result
|
choice.Delta.Content = baiduResponse.Result
|
||||||
choice.FinishReason = "stop"
|
if baiduResponse.IsEnd {
|
||||||
|
choice.FinishReason = &stopFinishReason
|
||||||
|
}
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Id: baiduResponse.Id,
|
Id: baiduResponse.Id,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
|
|||||||
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
|
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = claudeResponse.Completion
|
choice.Delta.Content = claudeResponse.Completion
|
||||||
choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
||||||
|
if finishReason != "null" {
|
||||||
|
choice.FinishReason = &finishReason
|
||||||
|
}
|
||||||
var response ChatCompletionsStreamResponse
|
var response ChatCompletionsStreamResponse
|
||||||
response.Object = "chat.completion.chunk"
|
response.Object = "chat.completion.chunk"
|
||||||
response.Model = claudeResponse.Model
|
response.Model = claudeResponse.Model
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsS
|
|||||||
if len(palmResponse.Candidates) > 0 {
|
if len(palmResponse.Candidates) > 0 {
|
||||||
choice.Delta.Content = palmResponse.Candidates[0].Content
|
choice.Delta.Content = palmResponse.Candidates[0].Content
|
||||||
}
|
}
|
||||||
choice.FinishReason = "stop"
|
choice.FinishReason = &stopFinishReason
|
||||||
var response ChatCompletionsStreamResponse
|
var response ChatCompletionsStreamResponse
|
||||||
response.Object = "chat.completion.chunk"
|
response.Object = "chat.completion.chunk"
|
||||||
response.Model = "palm2"
|
response.Model = "palm2"
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -308,6 +307,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
var textResponse TextResponse
|
var textResponse TextResponse
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
c.Writer.Flush()
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
quota := 0
|
quota := 0
|
||||||
completionRatio := 1.0
|
completionRatio := 1.0
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var stopFinishReason = "stop"
|
||||||
|
|
||||||
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
||||||
|
|
||||||
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatComple
|
|||||||
}
|
}
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
|
choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
|
||||||
|
if xunfeiResponse.Payload.Choices.Status == 2 {
|
||||||
|
choice.FinishReason = &stopFinishReason
|
||||||
|
}
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: common.GetTimestamp(),
|
Created: common.GetTimestamp(),
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
|
|||||||
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = zhipuResponse
|
choice.Delta.Content = zhipuResponse
|
||||||
choice.FinishReason = ""
|
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: common.GetTimestamp(),
|
Created: common.GetTimestamp(),
|
||||||
@@ -176,7 +175,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResp
|
|||||||
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = ""
|
choice.Delta.Content = ""
|
||||||
choice.FinishReason = "stop"
|
choice.FinishReason = &stopFinishReason
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Id: zhipuResponse.RequestId,
|
Id: zhipuResponse.RequestId,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ type ChatCompletionsStreamResponseChoice struct {
|
|||||||
Delta struct {
|
Delta struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
} `json:"delta"`
|
} `json:"delta"`
|
||||||
FinishReason string `json:"finish_reason,omitempty"`
|
FinishReason *string `json:"finish_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatCompletionsStreamResponse struct {
|
type ChatCompletionsStreamResponse struct {
|
||||||
@@ -176,7 +176,7 @@ func Relay(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
||||||
} else {
|
} else {
|
||||||
if err.StatusCode == http.StatusTooManyRequests {
|
if err.StatusCode == http.StatusTooManyRequests {
|
||||||
err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试"
|
||||||
}
|
}
|
||||||
c.JSON(err.StatusCode, gin.H{
|
c.JSON(err.StatusCode, gin.H{
|
||||||
"error": err.OpenAIError,
|
"error": err.OpenAIError,
|
||||||
@@ -207,10 +207,10 @@ func RelayNotImplemented(c *gin.Context) {
|
|||||||
|
|
||||||
func RelayNotFound(c *gin.Context) {
|
func RelayNotFound(c *gin.Context) {
|
||||||
err := OpenAIError{
|
err := OpenAIError{
|
||||||
Message: fmt.Sprintf("API not found: %s:%s", c.Request.Method, c.Request.URL.Path),
|
Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path),
|
||||||
Type: "one_api_error",
|
Type: "invalid_request_error",
|
||||||
Param: "",
|
Param: "",
|
||||||
Code: "api_not_found",
|
Code: "",
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ func AddToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(token.Name) == 0 || len(token.Name) > 20 {
|
if len(token.Name) > 30 {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "令牌名称长度必须在1-20之间",
|
"message": "令牌名称过长",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,13 @@ func UpdateToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(token.Name) > 30 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "令牌名称过长",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -14,7 +14,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1
|
github.com/pkoukk/tiktoken-go v0.1.5
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.9.0
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.4.3
|
||||||
gorm.io/driver/sqlite v1.4.3
|
gorm.io/driver/sqlite v1.4.3
|
||||||
@@ -26,7 +26,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
@@ -112,8 +112,8 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
|
|||||||
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
|
github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
|
github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
|||||||
11
i18n/en.json
11
i18n/en.json
@@ -39,8 +39,8 @@
|
|||||||
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
||||||
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
||||||
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
||||||
"当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.",
|
"当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
|
||||||
"令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20",
|
"令牌名称过长": "Token name is too long",
|
||||||
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
||||||
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
||||||
"管理员关闭了密码登录": "The administrator has turned off password login",
|
"管理员关闭了密码登录": "The administrator has turned off password login",
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
"已是最新版本": "Is the latest version",
|
"已是最新版本": "Is the latest version",
|
||||||
"检查更新": "Check for updates",
|
"检查更新": "Check for updates",
|
||||||
"公告": "Announcement",
|
"公告": "Announcement",
|
||||||
"在此输入新的公告内容": "Enter new announcement content here",
|
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
|
||||||
"保存公告": "Save Announcement",
|
"保存公告": "Save Announcement",
|
||||||
"个性化设置": "Personalization Settings",
|
"个性化设置": "Personalization Settings",
|
||||||
"系统名称": "System Name",
|
"系统名称": "System Name",
|
||||||
@@ -516,5 +516,8 @@
|
|||||||
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
||||||
"注意,": "Note that, ",
|
"注意,": "Note that, ",
|
||||||
",图片演示。": "related image demo.",
|
",图片演示。": "related image demo.",
|
||||||
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!"
|
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
|
||||||
|
"代理": "Proxy",
|
||||||
|
"此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
|
||||||
|
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?"
|
||||||
}
|
}
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -26,6 +26,9 @@ func main() {
|
|||||||
if os.Getenv("GIN_MODE") != "debug" {
|
if os.Getenv("GIN_MODE") != "debug" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
if common.DebugEnabled {
|
||||||
|
common.SysLog("running in debug mode")
|
||||||
|
}
|
||||||
// Initialize SQL Database
|
// Initialize SQL Database
|
||||||
err := model.InitDB()
|
err := model.InitDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
@@ -56,11 +57,22 @@ func InitDB() (err error) {
|
|||||||
}
|
}
|
||||||
common.SysLog("database connected")
|
common.SysLog("database connected")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if common.DebugEnabled {
|
||||||
|
db = db.Debug()
|
||||||
|
}
|
||||||
DB = db
|
DB = db
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 10))
|
||||||
|
sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 100))
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetOrDefault("SQL_MAX_LIFETIME", 60)))
|
||||||
|
|
||||||
if !common.IsMasterNode {
|
if !common.IsMasterNode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := db.AutoMigrate(&Channel{})
|
err = db.AutoMigrate(&Channel{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
selfRoute.GET("/self", controller.GetSelf)
|
selfRoute.GET("/self", controller.GetSelf)
|
||||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf)
|
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
selfRoute.POST("/topup", controller.TopUp)
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
|||||||
router.Use(middleware.Cache())
|
router.Use(middleware.Cache())
|
||||||
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
|
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
|
||||||
router.NoRoute(func(c *gin.Context) {
|
router.NoRoute(func(c *gin.Context) {
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/v1") {
|
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") {
|
||||||
controller.RelayNotFound(c)
|
controller.RelayNotFound(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,8 +447,8 @@ const ChannelsTable = () => {
|
|||||||
<Button size='small' loading={loading} onClick={testAllChannels}>
|
<Button size='small' loading={loading} onClick={testAllChannels}>
|
||||||
测试所有已启用通道
|
测试所有已启用通道
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='small' onClick={updateAllChannelsBalance}
|
{/* <Button size='small' onClick={updateAllChannelsBalance}
|
||||||
loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
|
loading={loading || updatingBalance}>更新所有已启用通道余额</Button> */}
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const OtherSetting = () => {
|
|||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='公告'
|
label='公告'
|
||||||
placeholder='在此输入新的公告内容'
|
placeholder='在此输入新的公告内容,支持 Markdown & HTML 代码'
|
||||||
value={inputs.Notice}
|
value={inputs.Notice}
|
||||||
name='Notice'
|
name='Notice'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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, Popup, 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';
|
||||||
|
|
||||||
@@ -240,15 +240,25 @@ const RedemptionsTable = () => {
|
|||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Popup
|
||||||
size={'small'}
|
trigger={
|
||||||
negative
|
<Button size='small' negative>
|
||||||
onClick={() => {
|
删除
|
||||||
manageRedemption(redemption.id, 'delete', idx);
|
</Button>
|
||||||
}}
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageRedemption(redemption.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
disabled={redemption.status === 3} // used
|
disabled={redemption.status === 3} // used
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react';
|
import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
|
||||||
import { API, removeTrailingSlash, showError } from '../helpers';
|
import { API, removeTrailingSlash, showError } from '../helpers';
|
||||||
|
|
||||||
const SystemSetting = () => {
|
const SystemSetting = () => {
|
||||||
@@ -33,6 +33,7 @@ const SystemSetting = () => {
|
|||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||||
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||||
|
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/option/');
|
||||||
@@ -95,6 +96,11 @@ const SystemSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = async (e, { name, value }) => {
|
const handleInputChange = async (e, { name, value }) => {
|
||||||
|
if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
|
||||||
|
// block disabling password login
|
||||||
|
setShowPasswordWarningModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
name === 'Notice' ||
|
name === 'Notice' ||
|
||||||
name.startsWith('SMTP') ||
|
name.startsWith('SMTP') ||
|
||||||
@@ -243,6 +249,32 @@ const SystemSetting = () => {
|
|||||||
name='PasswordLoginEnabled'
|
name='PasswordLoginEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
showPasswordWarningModal &&
|
||||||
|
<Modal
|
||||||
|
open={showPasswordWarningModal}
|
||||||
|
onClose={() => setShowPasswordWarningModal(false)}
|
||||||
|
size={'tiny'}
|
||||||
|
style={{ maxWidth: '450px' }}
|
||||||
|
>
|
||||||
|
<Modal.Header>警告</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
color='yellow'
|
||||||
|
onClick={async () => {
|
||||||
|
setShowPasswordWarningModal(false);
|
||||||
|
await updateOption('PasswordLoginEnabled', 'false');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.PasswordRegisterEnabled === 'true'}
|
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||||
label='允许通过密码进行注册'
|
label='允许通过密码进行注册'
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { toastConstants } from '../constants';
|
import { toastConstants } from '../constants';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HTMLToastContent = ({ htmlContent }) => {
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||||
|
};
|
||||||
|
export default HTMLToastContent;
|
||||||
export function isAdmin() {
|
export function isAdmin() {
|
||||||
let user = localStorage.getItem('user');
|
let user = localStorage.getItem('user');
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
@@ -107,8 +112,12 @@ export function showInfo(message) {
|
|||||||
toast.info(message, showInfoOptions);
|
toast.info(message, showInfoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showNotice(message) {
|
export function showNotice(message, isHTML = false) {
|
||||||
toast.info(message, showNoticeOptions);
|
if (isHTML) {
|
||||||
|
toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
|
||||||
|
} else {
|
||||||
|
toast.info(message, showNoticeOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openPage(url) {
|
export function openPage(url) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } 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';
|
||||||
|
|
||||||
@@ -12,9 +12,14 @@ const MODEL_MAPPING_EXAMPLE = {
|
|||||||
|
|
||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const channelId = params.id;
|
const channelId = params.id;
|
||||||
const isEdit = channelId !== undefined;
|
const isEdit = channelId !== undefined;
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const [loading, setLoading] = useState(isEdit);
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate('/channel');
|
||||||
|
};
|
||||||
|
|
||||||
const originInputs = {
|
const originInputs = {
|
||||||
name: '',
|
name: '',
|
||||||
type: 1,
|
type: 1,
|
||||||
@@ -371,9 +376,9 @@ const EditChannel = () => {
|
|||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
inputs.type !== 3 && inputs.type !== 8 && (
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='镜像'
|
label='代理'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'}
|
placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -381,6 +386,7 @@ const EditChannel = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
<Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
|
<Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ const Home = () => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let oldNotice = localStorage.getItem('notice');
|
let oldNotice = localStorage.getItem('notice');
|
||||||
if (data !== oldNotice && data !== '') {
|
if (data !== oldNotice && data !== '') {
|
||||||
showNotice(data);
|
const htmlNotice = marked(data);
|
||||||
localStorage.setItem('notice', data);
|
showNotice(htmlNotice, true);
|
||||||
}
|
localStorage.setItem('notice', data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,7 @@ const Home = () => {
|
|||||||
<Card.Meta>系统信息总览</Card.Meta>
|
<Card.Meta>系统信息总览</Card.Meta>
|
||||||
<Card.Description>
|
<Card.Description>
|
||||||
<p>名称:{statusState?.status?.system_name}</p>
|
<p>名称:{statusState?.status?.system_name}</p>
|
||||||
<p>版本:{statusState?.status?.version}</p>
|
<p>版本:{statusState?.status?.version ? statusState?.status?.version : "unknown"}</p>
|
||||||
<p>
|
<p>
|
||||||
源码:
|
源码:
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
|
import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
const EditRedemption = () => {
|
const EditRedemption = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const redemptionId = params.id;
|
const redemptionId = params.id;
|
||||||
const isEdit = redemptionId !== undefined;
|
const isEdit = redemptionId !== undefined;
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const [loading, setLoading] = useState(isEdit);
|
||||||
@@ -17,6 +18,10 @@ const EditRedemption = () => {
|
|||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, quota, count } = inputs;
|
const { name, quota, count } = inputs;
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate('/redemption');
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
@@ -113,6 +118,7 @@ const EditRedemption = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button positive onClick={submit}>提交</Button>
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user