mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-24 02:13:42 +08:00
Compare commits
77 Commits
v0.2.2-alp
...
v0.3.2
Author | SHA1 | Date | |
---|---|---|---|
|
3711f4a741 | ||
|
7c6bf3e97b | ||
|
481ba41fbd | ||
|
2779d6629c | ||
|
e509899daf | ||
|
b53cdbaf05 | ||
|
ced89398a5 | ||
|
09c2e3bcec | ||
|
5cba800fa6 | ||
|
2d39a135f2 | ||
|
3c6834a79c | ||
|
6da3410823 | ||
|
ceb289cb4d | ||
|
6f8cc712b0 | ||
|
ad01e1f3b3 | ||
|
cc1ef2ffd5 | ||
|
7201bd1c97 | ||
|
73d5e0f283 | ||
|
efc744ca35 | ||
|
e8da98139f | ||
|
519cb030f7 | ||
|
58fe923c85 | ||
|
c9ac5e391f | ||
|
69cf1de7bd | ||
|
4d6172a242 | ||
|
8afdc56b11 | ||
|
a9ea1d9d10 | ||
|
ea8e7c517b | ||
|
d1e9b86f05 | ||
|
6d1e5cb5dc | ||
|
01abed0a30 | ||
|
7c56a36a1c | ||
|
c48327ff91 | ||
|
a5406c6963 | ||
|
a1f61384c5 | ||
|
44ebae1559 | ||
|
aae92683d7 | ||
|
cc3072c4df | ||
|
bffee4e91d | ||
|
79dc53ff0d | ||
|
68e53d3e10 | ||
|
d267211ee7 | ||
|
570b3bc71c | ||
|
225176aae9 | ||
|
443a22b75d | ||
|
b44f0519a0 | ||
|
4a0e81fe83 | ||
|
976c29ea9f | ||
|
926951ee03 | ||
|
2cdc718fde | ||
|
57cb150177 | ||
|
6167e20b34 | ||
|
8835d8302e | ||
|
224bebe67a | ||
|
cf6883778e | ||
|
246b981e23 | ||
|
2edd52e851 | ||
|
e123c66bc7 | ||
|
9edc82bde0 | ||
|
d84c2f5c70 | ||
|
46e77389a4 | ||
|
f5f4e6fbc6 | ||
|
dc4a6cb711 | ||
|
5798fdac50 | ||
|
3710688efd | ||
|
83e86b9f8a | ||
|
74c1ba7cbc | ||
|
73aa53f536 | ||
|
da9ccb528d | ||
|
44729da277 | ||
|
7a3378b4b7 | ||
|
fd19d7d246 | ||
|
5c694a1503 | ||
|
9edc54ca69 | ||
|
e6af636fa0 | ||
|
6e1ef75009 | ||
|
d9db16e999 |
60
README.md
60
README.md
@@ -38,31 +38,39 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
<a href="https://github.com/songquanpeng/one-api#截图展示">截图展示</a>
|
<a href="https://github.com/songquanpeng/one-api#截图展示">截图展示</a>
|
||||||
·
|
·
|
||||||
<a href="https://openai.justsong.cn/">在线演示</a>
|
<a href="https://openai.justsong.cn/">在线演示</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/songquanpeng/one-api#常见问题">常见问题</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> **Warning**:从 `v0.2` 版本升级到 `v0.3` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.2-v0.3.sql)。
|
||||||
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
||||||
+ [x] One API 服务端中继
|
+ [x] OpenAI 官方通道
|
||||||
|
+ [x] **Azure OpenAI API**
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
+ [ ] Azure OpenAI API
|
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
|
||||||
+ [x] [CloseAI](https://console.openai-asia.com)
|
+ [x] [AI.LS](https://ai.ls)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
|
||||||
+ [x] [OpenAI Max](https://openaimax.com)
|
+ [x] [OpenAI Max](https://openaimax.com)
|
||||||
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
|
+ [x] [CloseAI](https://console.openai-asia.com)
|
||||||
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
||||||
2. 支持通过负载均衡的方式访问多个渠道。
|
2. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
|
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。
|
4. 支持**多机部署**,[详见此处](#多机部署)。
|
||||||
5. 支持设置令牌的过期时间和使用次数。
|
5. 支持**令牌管理**,设置令牌的过期时间和使用次数。
|
||||||
6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
||||||
7. 支持为新用户设置初始配额。
|
7. 支持**通道管理**,批量创建通道。
|
||||||
8. 支持发布公告,在线修改关于页面,设置充值链接,自定义页脚。
|
8. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
9. 支持通过系统访问令牌访问管理 API。
|
9. 支持丰富的**自定义**设置,
|
||||||
10. 多种用户登录注册方式:
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
|
10. 支持通过系统访问令牌访问管理 API。
|
||||||
|
11. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [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))。
|
||||||
11. 支持用户管理。
|
|
||||||
12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
@@ -86,13 +94,10 @@ server{
|
|||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
proxy_set_header Accept-Encoding gzip;
|
proxy_set_header Accept-Encoding gzip;
|
||||||
proxy_buffering off; # 重要:关闭代理缓冲
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,为了 SSE 正常工作,需要关闭 Nginx 的代理缓冲。
|
|
||||||
|
|
||||||
之后使用 Let's Encrypt 的 certbot 配置 HTTPS:
|
之后使用 Let's Encrypt 的 certbot 配置 HTTPS:
|
||||||
```bash
|
```bash
|
||||||
# Ubuntu 安装 certbot:
|
# Ubuntu 安装 certbot:
|
||||||
@@ -129,6 +134,14 @@ sudo service nginx restart
|
|||||||
|
|
||||||
更加详细的部署教程[参见此处](https://iamazing.cn/page/how-to-deploy-a-website)。
|
更加详细的部署教程[参见此处](https://iamazing.cn/page/how-to-deploy-a-website)。
|
||||||
|
|
||||||
|
### 多机部署
|
||||||
|
1. 所有服务器 `SESSION_SECRET` 设置一样的值。
|
||||||
|
2. 必须设置 `SQL_DSN`,使用 MySQL 数据库而非 SQLite,请自行配置主备数据库同步。
|
||||||
|
3. 所有从服务器必须设置 `SYNC_FREQUENCY`,以定期从数据库同步配置。
|
||||||
|
4. 从服务器可以选择设置 `FRONTEND_BASE_URL`,以重定向页面请求到主服务器。
|
||||||
|
|
||||||
|
环境变量的具体使用方法详见[此处](#环境变量)。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
系统本身开箱即用。
|
系统本身开箱即用。
|
||||||
|
|
||||||
@@ -153,6 +166,10 @@ sudo service nginx restart
|
|||||||
+ 例子:`SESSION_SECRET=random_string`
|
+ 例子:`SESSION_SECRET=random_string`
|
||||||
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite。
|
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite。
|
||||||
+ 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/one-api`
|
+ 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/one-api`
|
||||||
|
4. `FRONTEND_BASE_URL`:设置之后将使用指定的前端地址,而非后端地址。
|
||||||
|
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
|
||||||
|
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
|
||||||
|
+ 例子:`SYNC_FREQUENCY=60`
|
||||||
|
|
||||||
### 命令行参数
|
### 命令行参数
|
||||||
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
||||||
@@ -170,3 +187,10 @@ https://openai.justsong.cn
|
|||||||
### 截图展示
|
### 截图展示
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
1. 账户额度足够为什么提示额度不足?
|
||||||
|
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
|
||||||
|
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
|
||||||
|
2. 宝塔部署后访问出现空白页面?
|
||||||
|
+ 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。
|
6
bin/migration_v0.2-v0.3.sql
Normal file
6
bin/migration_v0.2-v0.3.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
UPDATE users
|
||||||
|
SET quota = quota + (
|
||||||
|
SELECT SUM(remain_quota)
|
||||||
|
FROM tokens
|
||||||
|
WHERE tokens.user_id = users.id
|
||||||
|
)
|
@@ -11,6 +11,7 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
|
|||||||
var SystemName = "One API"
|
var SystemName = "One API"
|
||||||
var ServerAddress = "http://localhost:3000"
|
var ServerAddress = "http://localhost:3000"
|
||||||
var Footer = ""
|
var Footer = ""
|
||||||
|
var Logo = ""
|
||||||
var TopUpLink = ""
|
var TopUpLink = ""
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
@@ -34,7 +35,9 @@ var TurnstileCheckEnabled = false
|
|||||||
var RegisterEnabled = true
|
var RegisterEnabled = true
|
||||||
|
|
||||||
var SMTPServer = ""
|
var SMTPServer = ""
|
||||||
|
var SMTPPort = 587
|
||||||
var SMTPAccount = ""
|
var SMTPAccount = ""
|
||||||
|
var SMTPFrom = ""
|
||||||
var SMTPToken = ""
|
var SMTPToken = ""
|
||||||
|
|
||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
@@ -47,12 +50,13 @@ var WeChatAccountQRCodeImageURL = ""
|
|||||||
var TurnstileSiteKey = ""
|
var TurnstileSiteKey = ""
|
||||||
var TurnstileSecretKey = ""
|
var TurnstileSecretKey = ""
|
||||||
|
|
||||||
var QuotaForNewUser = 100
|
var QuotaForNewUser = 0
|
||||||
|
var ChannelDisableThreshold = 5.0
|
||||||
|
var AutomaticDisableChannelEnabled = false
|
||||||
|
var QuotaRemindThreshold = 1000
|
||||||
|
var PreConsumedQuota = 500
|
||||||
|
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
var RootUserEmail = ""
|
||||||
var RatioGPT3dot5 float64 = 2
|
|
||||||
var RatioGPT4 float64 = 30
|
|
||||||
var RatioGPT4_32k float64 = 60
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
@@ -123,16 +127,18 @@ const (
|
|||||||
ChannelTypeOpenAIMax = 6
|
ChannelTypeOpenAIMax = 6
|
||||||
ChannelTypeOhMyGPT = 7
|
ChannelTypeOhMyGPT = 7
|
||||||
ChannelTypeCustom = 8
|
ChannelTypeCustom = 8
|
||||||
|
ChannelTypeAILS = 9
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
"", // 0
|
"", // 0
|
||||||
"https://api.openai.com", // 1
|
"https://api.openai.com", // 1
|
||||||
"https://openai.api2d.net", // 2
|
"https://oa.api2d.net", // 2
|
||||||
"", // 3
|
"", // 3
|
||||||
"https://api.openai-asia.com", // 4
|
"https://api.openai-asia.com", // 4
|
||||||
"https://api.openai-sb.com", // 5
|
"https://api.openai-sb.com", // 5
|
||||||
"https://api.openaimax.com", // 6
|
"https://api.openaimax.com", // 6
|
||||||
"https://api.ohmygpt.com", // 7
|
"https://api.ohmygpt.com", // 7
|
||||||
"", // 8
|
"", // 8
|
||||||
|
"https://api.caipacity.com", // 9
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,67 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import "gopkg.in/gomail.v2"
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func SendEmail(subject string, receiver string, content string) error {
|
func SendEmail(subject string, receiver string, content string) error {
|
||||||
m := gomail.NewMessage()
|
if SMTPFrom == "" { // for compatibility
|
||||||
m.SetHeader("From", SMTPAccount)
|
SMTPFrom = SMTPAccount
|
||||||
m.SetHeader("To", receiver)
|
}
|
||||||
m.SetHeader("Subject", subject)
|
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
||||||
m.SetBody("text/html", content)
|
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||||
d := gomail.NewDialer(SMTPServer, 587, SMTPAccount, SMTPToken)
|
"From: %s<%s>\r\n"+
|
||||||
err := d.DialAndSend(m)
|
"Subject: %s\r\n"+
|
||||||
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||||
|
receiver, SystemName, SMTPFrom, encodedSubject, content))
|
||||||
|
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||||
|
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||||
|
to := strings.Split(receiver, ";")
|
||||||
|
var err error
|
||||||
|
if SMTPPort == 465 {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
ServerName: SMTPServer,
|
||||||
|
}
|
||||||
|
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := smtp.NewClient(conn, SMTPServer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = client.Mail(SMTPFrom); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
receiverEmails := strings.Split(receiver, ";")
|
||||||
|
for _, receiver := range receiverEmails {
|
||||||
|
if err = client.Rcpt(receiver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(mail)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
52
common/model-ratio.go
Normal file
52
common/model-ratio.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
|
// https://openai.com/pricing
|
||||||
|
// TODO: when a new api is enabled, check the pricing here
|
||||||
|
var ModelRatio = map[string]float64{
|
||||||
|
"gpt-4": 15,
|
||||||
|
"gpt-4-0314": 15,
|
||||||
|
"gpt-4-32k": 30,
|
||||||
|
"gpt-4-32k-0314": 30,
|
||||||
|
"gpt-3.5-turbo": 1,
|
||||||
|
"gpt-3.5-turbo-0301": 1,
|
||||||
|
"text-ada-001": 0.2,
|
||||||
|
"text-babbage-001": 0.25,
|
||||||
|
"text-curie-001": 1,
|
||||||
|
"text-davinci-002": 10,
|
||||||
|
"text-davinci-003": 10,
|
||||||
|
"text-davinci-edit-001": 10,
|
||||||
|
"code-davinci-edit-001": 10,
|
||||||
|
"whisper-1": 10,
|
||||||
|
"davinci": 10,
|
||||||
|
"curie": 10,
|
||||||
|
"babbage": 10,
|
||||||
|
"ada": 10,
|
||||||
|
"text-embedding-ada-002": 0.2,
|
||||||
|
"text-search-ada-doc-001": 10,
|
||||||
|
"text-moderation-stable": 10,
|
||||||
|
"text-moderation-latest": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModelRatio2JSONString() string {
|
||||||
|
jsonBytes, err := json.Marshal(ModelRatio)
|
||||||
|
if err != nil {
|
||||||
|
SysError("Error marshalling model ratio: " + err.Error())
|
||||||
|
}
|
||||||
|
return string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateModelRatioByJSONString(jsonStr string) error {
|
||||||
|
return json.Unmarshal([]byte(jsonStr), &ModelRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModelRatio(name string) float64 {
|
||||||
|
ratio, ok := ModelRatio[name]
|
||||||
|
if !ok {
|
||||||
|
SysError("Model ratio not found: " + name)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return ratio
|
||||||
|
}
|
@@ -1,11 +1,18 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
@@ -13,7 +20,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
if p < 0 {
|
if p < 0 {
|
||||||
p = 0
|
p = 0
|
||||||
}
|
}
|
||||||
channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage)
|
channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -83,8 +90,17 @@ func AddChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
channel.CreatedTime = common.GetTimestamp()
|
channel.CreatedTime = common.GetTimestamp()
|
||||||
channel.AccessedTime = common.GetTimestamp()
|
keys := strings.Split(channel.Key, "\n")
|
||||||
err = channel.Insert()
|
channels := make([]model.Channel, 0)
|
||||||
|
for _, key := range keys {
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
localChannel := channel
|
||||||
|
localChannel.Key = key
|
||||||
|
channels = append(channels, localChannel)
|
||||||
|
}
|
||||||
|
err = model.BatchInsertChannels(channels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -142,3 +158,187 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testChannel(channel *model.Channel, request *ChatRequest) error {
|
||||||
|
if request.Model == "" {
|
||||||
|
request.Model = "gpt-3.5-turbo"
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
request.Model = "gpt-35-turbo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestURL := common.ChannelBaseURLs[channel.Type]
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model)
|
||||||
|
} else {
|
||||||
|
if channel.Type == common.ChannelTypeCustom {
|
||||||
|
requestURL = channel.BaseURL
|
||||||
|
}
|
||||||
|
requestURL += "/v1/chat/completions"
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
req.Header.Set("api-key", channel.Key)
|
||||||
|
} else {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var response TextResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.Error.Message != "" {
|
||||||
|
return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTestRequest(c *gin.Context) *ChatRequest {
|
||||||
|
model_ := c.Query("model")
|
||||||
|
testRequest := &ChatRequest{
|
||||||
|
Model: model_,
|
||||||
|
MaxTokens: 1,
|
||||||
|
}
|
||||||
|
testMessage := Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "hi",
|
||||||
|
}
|
||||||
|
testRequest.Messages = append(testRequest.Messages, testMessage)
|
||||||
|
return testRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannel(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel, err := model.GetChannelById(id, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testRequest := buildTestRequest(c)
|
||||||
|
tik := time.Now()
|
||||||
|
err = testChannel(channel, testRequest)
|
||||||
|
tok := time.Now()
|
||||||
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
|
go channel.UpdateResponseTime(milliseconds)
|
||||||
|
consumedTime := float64(milliseconds) / 1000.0
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
"time": consumedTime,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"time": consumedTime,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAllChannelsLock sync.Mutex
|
||||||
|
var testAllChannelsRunning bool = false
|
||||||
|
|
||||||
|
// disable & notify
|
||||||
|
func disableChannel(channelId int, channelName string, reason string) {
|
||||||
|
if common.RootUserEmail == "" {
|
||||||
|
common.RootUserEmail = model.GetRootUserEmail()
|
||||||
|
}
|
||||||
|
model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled)
|
||||||
|
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
|
||||||
|
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
||||||
|
err := common.SendEmail(subject, common.RootUserEmail, content)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllChannels(c *gin.Context) error {
|
||||||
|
testAllChannelsLock.Lock()
|
||||||
|
if testAllChannelsRunning {
|
||||||
|
testAllChannelsLock.Unlock()
|
||||||
|
return errors.New("测试已在运行中")
|
||||||
|
}
|
||||||
|
testAllChannelsRunning = true
|
||||||
|
testAllChannelsLock.Unlock()
|
||||||
|
channels, err := model.GetAllChannels(0, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
testRequest := buildTestRequest(c)
|
||||||
|
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
||||||
|
if disableThreshold == 0 {
|
||||||
|
disableThreshold = 10000000 // a impossible value
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.Status != common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tik := time.Now()
|
||||||
|
err := testChannel(channel, testRequest)
|
||||||
|
tok := time.Now()
|
||||||
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
|
if err != nil || milliseconds > disableThreshold {
|
||||||
|
if milliseconds > disableThreshold {
|
||||||
|
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
||||||
|
}
|
||||||
|
disableChannel(channel.Id, channel.Name, err.Error())
|
||||||
|
}
|
||||||
|
channel.UpdateResponseTime(milliseconds)
|
||||||
|
}
|
||||||
|
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error()))
|
||||||
|
}
|
||||||
|
testAllChannelsLock.Lock()
|
||||||
|
testAllChannelsRunning = false
|
||||||
|
testAllChannelsLock.Unlock()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllChannels(c *gin.Context) {
|
||||||
|
err := testAllChannels(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@@ -20,6 +20,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
@@ -54,6 +55,17 @@ func GetAbout(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetHomePageContent(c *gin.Context) {
|
||||||
|
common.OptionMapRWMutex.RLock()
|
||||||
|
defer common.OptionMapRWMutex.RUnlock()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": common.OptionMap["HomePageContent"],
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func SendEmailVerification(c *gin.Context) {
|
func SendEmailVerification(c *gin.Context) {
|
||||||
email := c.Query("email")
|
email := c.Query("email")
|
||||||
if err := common.Validate.Var(email, "required,email"); err != nil {
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||||
|
153
controller/model.go
Normal file
153
controller/model.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://platform.openai.com/docs/api-reference/models/list
|
||||||
|
|
||||||
|
type OpenAIModelPermission struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
AllowCreateEngine bool `json:"allow_create_engine"`
|
||||||
|
AllowSampling bool `json:"allow_sampling"`
|
||||||
|
AllowLogprobs bool `json:"allow_logprobs"`
|
||||||
|
AllowSearchIndices bool `json:"allow_search_indices"`
|
||||||
|
AllowView bool `json:"allow_view"`
|
||||||
|
AllowFineTuning bool `json:"allow_fine_tuning"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Group *string `json:"group"`
|
||||||
|
IsBlocking bool `json:"is_blocking"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIModels struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
OwnedBy string `json:"owned_by"`
|
||||||
|
Permission OpenAIModelPermission `json:"permission"`
|
||||||
|
Root string `json:"root"`
|
||||||
|
Parent *string `json:"parent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var openAIModels []OpenAIModels
|
||||||
|
var openAIModelsMap map[string]OpenAIModels
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
permission := OpenAIModelPermission{
|
||||||
|
Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
|
||||||
|
Object: "model_permission",
|
||||||
|
Created: 1626777600,
|
||||||
|
AllowCreateEngine: true,
|
||||||
|
AllowSampling: true,
|
||||||
|
AllowLogprobs: true,
|
||||||
|
AllowSearchIndices: false,
|
||||||
|
AllowView: true,
|
||||||
|
AllowFineTuning: false,
|
||||||
|
Organization: "*",
|
||||||
|
Group: nil,
|
||||||
|
IsBlocking: false,
|
||||||
|
}
|
||||||
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
|
openAIModels = []OpenAIModels{
|
||||||
|
{
|
||||||
|
Id: "gpt-3.5-turbo",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-3.5-turbo",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-3.5-turbo-0301",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-3.5-turbo-0301",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-4",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-4",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-4-0314",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-4-0314",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-4-32k",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-4-32k",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-4-32k-0314",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-4-32k-0314",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "gpt-3.5-turbo",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "gpt-3.5-turbo",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "text-embedding-ada-002",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "text-embedding-ada-002",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
openAIModelsMap = make(map[string]OpenAIModels)
|
||||||
|
for _, model := range openAIModels {
|
||||||
|
openAIModelsMap[model.Id] = model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListModels(c *gin.Context) {
|
||||||
|
c.JSON(200, openAIModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveModel(c *gin.Context) {
|
||||||
|
modelId := c.Param("model")
|
||||||
|
if model, ok := openAIModelsMap[modelId]; ok {
|
||||||
|
c.JSON(200, model)
|
||||||
|
} else {
|
||||||
|
openAIError := OpenAIError{
|
||||||
|
Message: fmt.Sprintf("The model '%s' does not exist", modelId),
|
||||||
|
Type: "invalid_request_error",
|
||||||
|
Param: "model",
|
||||||
|
Code: "model_not_found",
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"error": openAIError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
61
controller/relay-utils.go
Normal file
61
controller/relay-utils.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/pkoukk/tiktoken-go"
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
||||||
|
|
||||||
|
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
||||||
|
if tokenEncoder, ok := tokenEncoderMap[model]; ok {
|
||||||
|
return tokenEncoder
|
||||||
|
}
|
||||||
|
tokenEncoder, err := tiktoken.EncodingForModel(model)
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(fmt.Sprintf("failed to get token encoder for model %s: %s", model, err.Error()))
|
||||||
|
}
|
||||||
|
tokenEncoderMap[model] = tokenEncoder
|
||||||
|
return tokenEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func countTokenMessages(messages []Message, model string) int {
|
||||||
|
tokenEncoder := getTokenEncoder(model)
|
||||||
|
// Reference:
|
||||||
|
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||||
|
// https://github.com/pkoukk/tiktoken-go/issues/6
|
||||||
|
//
|
||||||
|
// Every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||||
|
var tokensPerMessage int
|
||||||
|
var tokensPerName int
|
||||||
|
if strings.HasPrefix(model, "gpt-3.5") {
|
||||||
|
tokensPerMessage = 4
|
||||||
|
tokensPerName = -1 // If there's a name, the role is omitted
|
||||||
|
} else if strings.HasPrefix(model, "gpt-4") {
|
||||||
|
tokensPerMessage = 3
|
||||||
|
tokensPerName = 1
|
||||||
|
} else {
|
||||||
|
tokensPerMessage = 3
|
||||||
|
tokensPerName = 1
|
||||||
|
}
|
||||||
|
tokenNum := 0
|
||||||
|
for _, message := range messages {
|
||||||
|
tokenNum += tokensPerMessage
|
||||||
|
tokenNum += len(tokenEncoder.Encode(message.Content, nil, nil))
|
||||||
|
tokenNum += len(tokenEncoder.Encode(message.Role, nil, nil))
|
||||||
|
if message.Name != nil {
|
||||||
|
tokenNum += tokensPerName
|
||||||
|
tokenNum += len(tokenEncoder.Encode(*message.Name, nil, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenNum += 3 // Every reply is primed with <|start|>assistant<|message|>
|
||||||
|
return tokenNum
|
||||||
|
}
|
||||||
|
|
||||||
|
func countTokenText(text string, model string) int {
|
||||||
|
tokenEncoder := getTokenEncoder(model)
|
||||||
|
token := tokenEncoder.Encode(text, nil, nil)
|
||||||
|
return len(token)
|
||||||
|
}
|
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkoukk/tiktoken-go"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
@@ -15,14 +14,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []Message `json:"messages"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextRequest struct {
|
type TextRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
//Stream bool `json:"stream"`
|
//Stream bool `json:"stream"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +39,21 @@ type Usage struct {
|
|||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Param string `json:"param"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIErrorWithStatusCode struct {
|
||||||
|
OpenAIError
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
}
|
||||||
|
|
||||||
type TextResponse struct {
|
type TextResponse struct {
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
|
Error OpenAIError `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamResponse struct {
|
type StreamResponse struct {
|
||||||
@@ -45,99 +65,145 @@ type StreamResponse struct {
|
|||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenEncoder, _ = tiktoken.GetEncoding("cl100k_base")
|
|
||||||
|
|
||||||
func countToken(text string) int {
|
|
||||||
token := tokenEncoder.Encode(text, nil, nil)
|
|
||||||
return len(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Relay(c *gin.Context) {
|
func Relay(c *gin.Context) {
|
||||||
err := relayHelper(c)
|
err := relayHelper(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
if err.StatusCode == http.StatusTooManyRequests {
|
||||||
"error": gin.H{
|
err.OpenAIError.Message = "负载已满,请稍后再试,或升级账户以提升服务质量。"
|
||||||
"message": err.Error(),
|
}
|
||||||
"type": "one_api_error",
|
c.JSON(err.StatusCode, gin.H{
|
||||||
},
|
"error": err.OpenAIError,
|
||||||
})
|
})
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message))
|
||||||
|
if err.Type != "invalid_request_error" && err.StatusCode != http.StatusTooManyRequests &&
|
||||||
|
common.AutomaticDisableChannelEnabled {
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
channelName := c.GetString("channel_name")
|
||||||
|
disableChannel(channelId, channelName, err.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func relayHelper(c *gin.Context) error {
|
func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatusCode {
|
||||||
|
openAIError := OpenAIError{
|
||||||
|
Message: err.Error(),
|
||||||
|
Type: "one_api_error",
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: openAIError,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode {
|
||||||
channelType := c.GetInt("channel")
|
channelType := c.GetInt("channel")
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
consumeQuota := c.GetBool("consume_quota")
|
consumeQuota := c.GetBool("consume_quota")
|
||||||
baseURL := common.ChannelBaseURLs[channelType]
|
|
||||||
if channelType == common.ChannelTypeCustom {
|
|
||||||
baseURL = c.GetString("base_url")
|
|
||||||
}
|
|
||||||
var textRequest TextRequest
|
var textRequest TextRequest
|
||||||
if consumeQuota {
|
if consumeQuota || channelType == common.ChannelTypeAzure {
|
||||||
requestBody, err := io.ReadAll(c.Request.Body)
|
requestBody, err := io.ReadAll(c.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "read_request_body_failed", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
err = c.Request.Body.Close()
|
err = c.Request.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_request_body_failed", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(requestBody, &textRequest)
|
err = json.Unmarshal(requestBody, &textRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "unmarshal_request_body_failed", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
// Reset request body
|
// Reset request body
|
||||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||||
}
|
}
|
||||||
|
baseURL := common.ChannelBaseURLs[channelType]
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
req, err := http.NewRequest(c.Request.Method, fmt.Sprintf("%s%s", baseURL, requestURL), c.Request.Body)
|
if channelType == common.ChannelTypeCustom {
|
||||||
if err != nil {
|
baseURL = c.GetString("base_url")
|
||||||
return err
|
}
|
||||||
|
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||||
|
if channelType == common.ChannelTypeAzure {
|
||||||
|
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
|
||||||
|
query := c.Request.URL.Query()
|
||||||
|
apiVersion := query.Get("api-version")
|
||||||
|
if apiVersion == "" {
|
||||||
|
apiVersion = c.GetString("api_version")
|
||||||
|
}
|
||||||
|
requestURL := strings.Split(requestURL, "?")[0]
|
||||||
|
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
||||||
|
baseURL = c.GetString("base_url")
|
||||||
|
task := strings.TrimPrefix(requestURL, "/v1/")
|
||||||
|
model_ := textRequest.Model
|
||||||
|
model_ = strings.Replace(model_, ".", "", -1)
|
||||||
|
// https://github.com/songquanpeng/one-api/issues/67
|
||||||
|
model_ = strings.TrimSuffix(model_, "-0301")
|
||||||
|
model_ = strings.TrimSuffix(model_, "-0314")
|
||||||
|
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model)
|
||||||
|
preConsumedTokens := common.PreConsumedQuota
|
||||||
|
if textRequest.MaxTokens != 0 {
|
||||||
|
preConsumedTokens = promptTokens + textRequest.MaxTokens
|
||||||
|
}
|
||||||
|
ratio := common.GetModelRatio(textRequest.Model)
|
||||||
|
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
||||||
|
if consumeQuota {
|
||||||
|
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "new_request_failed", http.StatusOK)
|
||||||
|
}
|
||||||
|
if channelType == common.ChannelTypeAzure {
|
||||||
|
key := c.Request.Header.Get("Authorization")
|
||||||
|
key = strings.TrimPrefix(key, "Bearer ")
|
||||||
|
req.Header.Set("api-key", key)
|
||||||
|
} else {
|
||||||
|
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
|
||||||
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"))
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "do_request_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
err = req.Body.Close()
|
err = req.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_request_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
err = c.Request.Body.Close()
|
err = c.Request.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_request_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
var textResponse TextResponse
|
var textResponse TextResponse
|
||||||
isStream := resp.Header.Get("Content-Type") == "text/event-stream"
|
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
var streamResponseText string
|
var streamResponseText string
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
quota := 0
|
quota := 0
|
||||||
if isStream {
|
usingGPT4 := strings.HasPrefix(textRequest.Model, "gpt-4")
|
||||||
var text string
|
completionRatio := 1
|
||||||
for _, message := range textRequest.Messages {
|
if usingGPT4 {
|
||||||
text += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
|
completionRatio = 2
|
||||||
}
|
|
||||||
text += fmt.Sprintf("%s: %s\n", "assistant", streamResponseText)
|
|
||||||
quota = countToken(text) + 3
|
|
||||||
} else {
|
|
||||||
quota = textResponse.Usage.TotalTokens
|
|
||||||
}
|
}
|
||||||
ratio := common.RatioGPT3dot5
|
if isStream {
|
||||||
if strings.HasPrefix(textRequest.Model, "gpt-4-32k") {
|
responseTokens := countTokenText(streamResponseText, textRequest.Model)
|
||||||
ratio = common.RatioGPT4_32k
|
quota = promptTokens + responseTokens*completionRatio
|
||||||
} else if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
|
||||||
ratio = common.RatioGPT4
|
|
||||||
} else {
|
} else {
|
||||||
ratio = common.RatioGPT3dot5
|
quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio
|
||||||
}
|
}
|
||||||
quota = int(float64(quota) * ratio)
|
quota = int(float64(quota) * ratio)
|
||||||
err := model.DecreaseTokenQuota(tokenId, quota)
|
quotaDelta := quota - preConsumedQuota
|
||||||
|
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Error consuming token remain quota: " + err.Error())
|
common.SysError("Error consuming token remain quota: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -166,9 +232,13 @@ func relayHelper(c *gin.Context) error {
|
|||||||
go func() {
|
go func() {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
data := scanner.Text()
|
data := scanner.Text()
|
||||||
|
if len(data) < 6 { // must be something wrong!
|
||||||
|
common.SysError("Invalid stream response: " + data)
|
||||||
|
continue
|
||||||
|
}
|
||||||
dataChan <- data
|
dataChan <- data
|
||||||
data = data[6:]
|
data = data[6:]
|
||||||
if data != "[DONE]" {
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
var streamResponse StreamResponse
|
var streamResponse StreamResponse
|
||||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -186,9 +256,13 @@ func relayHelper(c *gin.Context) error {
|
|||||||
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")
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
|
if strings.HasPrefix(data, "data: [DONE]") {
|
||||||
|
data = data[:12]
|
||||||
|
}
|
||||||
c.Render(-1, common.CustomEvent{Data: data})
|
c.Render(-1, common.CustomEvent{Data: data})
|
||||||
return true
|
return true
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
@@ -197,46 +271,60 @@ func relayHelper(c *gin.Context) error {
|
|||||||
})
|
})
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
for k, v := range resp.Header {
|
|
||||||
c.Writer.Header().Set(k, v[0])
|
|
||||||
}
|
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "read_response_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(responseBody, &textResponse)
|
err = json.Unmarshal(responseBody, &textResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusOK)
|
||||||
|
}
|
||||||
|
if textResponse.Error.Type != "" {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: textResponse.Error,
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Reset response body
|
// Reset response body
|
||||||
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
||||||
}
|
}
|
||||||
|
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
||||||
|
// And then we will have to send an error response, but in this case, the header has already been set.
|
||||||
|
// So the client will be confused by the response.
|
||||||
|
// For example, Postman will report error, and we cannot check the response at all.
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
c.Writer.Header().Set(k, v[0])
|
||||||
|
}
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
_, err = io.Copy(c.Writer, resp.Body)
|
_, err = io.Copy(c.Writer, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "copy_response_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RelayNotImplemented(c *gin.Context) {
|
func RelayNotImplemented(c *gin.Context) {
|
||||||
|
err := OpenAIError{
|
||||||
|
Message: "API not implemented",
|
||||||
|
Type: "one_api_error",
|
||||||
|
Param: "",
|
||||||
|
Code: "api_not_implemented",
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"error": gin.H{
|
"error": err,
|
||||||
"message": "Not Implemented",
|
|
||||||
"type": "one_api_error",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -100,7 +100,6 @@ func GetTokenStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddToken(c *gin.Context) {
|
func AddToken(c *gin.Context) {
|
||||||
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
|
||||||
token := model.Token{}
|
token := model.Token{}
|
||||||
err := c.ShouldBindJSON(&token)
|
err := c.ShouldBindJSON(&token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,29 +117,14 @@ func AddToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cleanToken := model.Token{
|
cleanToken := model.Token{
|
||||||
UserId: c.GetInt("id"),
|
UserId: c.GetInt("id"),
|
||||||
Name: token.Name,
|
Name: token.Name,
|
||||||
Key: common.GetUUID(),
|
Key: common.GetUUID(),
|
||||||
CreatedTime: common.GetTimestamp(),
|
CreatedTime: common.GetTimestamp(),
|
||||||
AccessedTime: common.GetTimestamp(),
|
AccessedTime: common.GetTimestamp(),
|
||||||
ExpiredTime: token.ExpiredTime,
|
ExpiredTime: token.ExpiredTime,
|
||||||
}
|
RemainQuota: token.RemainQuota,
|
||||||
if isAdmin {
|
UnlimitedQuota: token.UnlimitedQuota,
|
||||||
cleanToken.RemainQuota = token.RemainQuota
|
|
||||||
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
|
||||||
} else {
|
|
||||||
userId := c.GetInt("id")
|
|
||||||
quota, err := model.GetUserQuota(userId)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if quota > 0 {
|
|
||||||
cleanToken.RemainQuota = quota
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = cleanToken.Insert()
|
err = cleanToken.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,10 +134,6 @@ func AddToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isAdmin {
|
|
||||||
// update user quota
|
|
||||||
err = model.DecreaseUserQuota(c.GetInt("id"), cleanToken.RemainQuota)
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -180,7 +160,6 @@ func DeleteToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateToken(c *gin.Context) {
|
func UpdateToken(c *gin.Context) {
|
||||||
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
statusOnly := c.Query("status_only")
|
statusOnly := c.Query("status_only")
|
||||||
token := model.Token{}
|
token := model.Token{}
|
||||||
@@ -211,7 +190,7 @@ func UpdateToken(c *gin.Context) {
|
|||||||
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "令牌可用次数已用尽,无法启用,请先修改令牌剩余次数,或者设置为无限次数",
|
"message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -222,10 +201,8 @@ func UpdateToken(c *gin.Context) {
|
|||||||
// If you add more fields, please also update token.Update()
|
// If you add more fields, please also update token.Update()
|
||||||
cleanToken.Name = token.Name
|
cleanToken.Name = token.Name
|
||||||
cleanToken.ExpiredTime = token.ExpiredTime
|
cleanToken.ExpiredTime = token.ExpiredTime
|
||||||
if isAdmin {
|
cleanToken.RemainQuota = token.RemainQuota
|
||||||
cleanToken.RemainQuota = token.RemainQuota
|
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
||||||
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = cleanToken.Update()
|
err = cleanToken.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,34 +219,3 @@ func UpdateToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type topUpRequest struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TopUp(c *gin.Context) {
|
|
||||||
req := topUpRequest{}
|
|
||||||
err := c.ShouldBindJSON(&req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
quota, err := model.Redeem(req.Key, req.Id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"message": "",
|
|
||||||
"data": quota,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
@@ -467,6 +467,13 @@ func CreateUser(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := common.Validate.Struct(&user); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "输入不合法 " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if user.DisplayName == "" {
|
if user.DisplayName == "" {
|
||||||
user.DisplayName = user.Username
|
user.DisplayName = user.Username
|
||||||
}
|
}
|
||||||
@@ -654,3 +661,34 @@ func EmailBind(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type topUpRequest struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TopUp(c *gin.Context) {
|
||||||
|
req := topUpRequest{}
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.GetInt("id")
|
||||||
|
quota, err := model.Redeem(req.Key, id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": quota,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
5
go.mod
5
go.mod
@@ -12,8 +12,8 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.12.0
|
github.com/go-playground/validator/v10 v10.12.0
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/pkoukk/tiktoken-go v0.1.1
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
|
||||||
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
|
||||||
gorm.io/gorm v1.24.0
|
gorm.io/gorm v1.24.0
|
||||||
@@ -45,7 +45,6 @@ require (
|
|||||||
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.7 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
@@ -53,7 +52,5 @@ require (
|
|||||||
golang.org/x/sys v0.7.0 // indirect
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
31
go.sum
31
go.sum
@@ -28,33 +28,27 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
|||||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
|
||||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
|
||||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
|
||||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
@@ -94,19 +88,16 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
|
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
|
||||||
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -116,7 +107,6 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
|||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
|
||||||
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.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||||
@@ -136,7 +126,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
@@ -146,7 +135,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
|
|||||||
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=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
|
||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
@@ -154,26 +142,17 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
|
|||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
|
||||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -181,28 +160,20 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
7
main.go
7
main.go
@@ -47,6 +47,13 @@ func main() {
|
|||||||
|
|
||||||
// Initialize options
|
// Initialize options
|
||||||
model.InitOptionMap()
|
model.InitOptionMap()
|
||||||
|
if os.Getenv("SYNC_FREQUENCY") != "" {
|
||||||
|
frequency, err := strconv.Atoi(os.Getenv("SYNC_FREQUENCY"))
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(err)
|
||||||
|
}
|
||||||
|
go model.SyncOptions(frequency)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.Default()
|
server := gin.Default()
|
||||||
|
@@ -85,6 +85,8 @@ func RootAuth() func(c *gin.Context) {
|
|||||||
func TokenAuth() func(c *gin.Context) {
|
func TokenAuth() func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
key := c.Request.Header.Get("Authorization")
|
key := c.Request.Header.Get("Authorization")
|
||||||
|
key = strings.TrimPrefix(key, "Bearer ")
|
||||||
|
key = strings.TrimPrefix(key, "sk-")
|
||||||
parts := strings.Split(key, "-")
|
parts := strings.Split(key, "-")
|
||||||
key = parts[0]
|
key = parts[0]
|
||||||
token, err := model.ValidateUserToken(key)
|
token, err := model.ValidateUserToken(key)
|
||||||
@@ -111,14 +113,9 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
consumeQuota := false
|
consumeQuota := true
|
||||||
switch requestURL {
|
if strings.HasPrefix(requestURL, "/v1/models") {
|
||||||
case "/v1/chat/completions":
|
consumeQuota = false
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/completions":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/edits":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
}
|
}
|
||||||
c.Set("consume_quota", consumeQuota)
|
c.Set("consume_quota", consumeQuota)
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
|
@@ -62,9 +62,14 @@ func Distribute() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Set("channel", channel.Type)
|
c.Set("channel", channel.Type)
|
||||||
|
c.Set("channel_id", channel.Id)
|
||||||
|
c.Set("channel_name", channel.Name)
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
if channel.Type == common.ChannelTypeCustom {
|
if channel.Type == common.ChannelTypeCustom || channel.Type == common.ChannelTypeAzure {
|
||||||
c.Set("base_url", channel.BaseURL)
|
c.Set("base_url", channel.BaseURL)
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
c.Set("api_version", channel.Other)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
@@ -13,14 +13,20 @@ type Channel struct {
|
|||||||
Name string `json:"name" gorm:"index"`
|
Name string `json:"name" gorm:"index"`
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
|
TestTime int64 `json:"test_time" gorm:"bigint"`
|
||||||
|
ResponseTime int `json:"response_time"` // in milliseconds
|
||||||
BaseURL string `json:"base_url" gorm:"column:base_url"`
|
BaseURL string `json:"base_url" gorm:"column:base_url"`
|
||||||
|
Other string `json:"other"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
var err error
|
var err error
|
||||||
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
if selectAll {
|
||||||
|
err = DB.Order("id desc").Find(&channels).Error
|
||||||
|
} else {
|
||||||
|
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
||||||
|
}
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +58,12 @@ func GetRandomChannel() (*Channel, error) {
|
|||||||
return &channel, err
|
return &channel, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BatchInsertChannels(channels []Channel) error {
|
||||||
|
var err error
|
||||||
|
err = DB.Create(&channels).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) Insert() error {
|
func (channel *Channel) Insert() error {
|
||||||
var err error
|
var err error
|
||||||
err = DB.Create(channel).Error
|
err = DB.Create(channel).Error
|
||||||
@@ -64,8 +76,25 @@ func (channel *Channel) Update() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) UpdateResponseTime(responseTime int64) {
|
||||||
|
err := DB.Model(channel).Select("response_time", "test_time").Updates(Channel{
|
||||||
|
TestTime: common.GetTimestamp(),
|
||||||
|
ResponseTime: int(responseTime),
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update response time: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) Delete() error {
|
func (channel *Channel) Delete() error {
|
||||||
var err error
|
var err error
|
||||||
err = DB.Delete(channel).Error
|
err = DB.Delete(channel).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateChannelStatusById(id int, status int) {
|
||||||
|
err := DB.Model(&Channel{}).Where("id = ?", id).Update("status", status).Error
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update channel status: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
@@ -32,12 +33,19 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
||||||
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
||||||
|
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
||||||
|
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
||||||
common.OptionMap["SMTPServer"] = ""
|
common.OptionMap["SMTPServer"] = ""
|
||||||
|
common.OptionMap["SMTPFrom"] = ""
|
||||||
|
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
||||||
common.OptionMap["SMTPAccount"] = ""
|
common.OptionMap["SMTPAccount"] = ""
|
||||||
common.OptionMap["SMTPToken"] = ""
|
common.OptionMap["SMTPToken"] = ""
|
||||||
common.OptionMap["Notice"] = ""
|
common.OptionMap["Notice"] = ""
|
||||||
common.OptionMap["About"] = ""
|
common.OptionMap["About"] = ""
|
||||||
|
common.OptionMap["HomePageContent"] = ""
|
||||||
common.OptionMap["Footer"] = common.Footer
|
common.OptionMap["Footer"] = common.Footer
|
||||||
|
common.OptionMap["SystemName"] = common.SystemName
|
||||||
|
common.OptionMap["Logo"] = common.Logo
|
||||||
common.OptionMap["ServerAddress"] = ""
|
common.OptionMap["ServerAddress"] = ""
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
@@ -47,14 +55,29 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["TurnstileSiteKey"] = ""
|
common.OptionMap["TurnstileSiteKey"] = ""
|
||||||
common.OptionMap["TurnstileSecretKey"] = ""
|
common.OptionMap["TurnstileSecretKey"] = ""
|
||||||
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
||||||
common.OptionMap["RatioGPT3dot5"] = strconv.FormatFloat(common.RatioGPT3dot5, 'f', -1, 64)
|
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
|
||||||
common.OptionMap["RatioGPT4"] = strconv.FormatFloat(common.RatioGPT4, 'f', -1, 64)
|
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
|
||||||
common.OptionMap["RatioGPT4_32k"] = strconv.FormatFloat(common.RatioGPT4_32k, 'f', -1, 64)
|
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
|
loadOptionsFromDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOptionsFromDatabase() {
|
||||||
options, _ := AllOption()
|
options, _ := AllOption()
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
updateOptionMap(option.Key, option.Value)
|
err := updateOptionMap(option.Key, option.Value)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("Failed to update option map: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncOptions(frequency int) {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(frequency) * time.Second)
|
||||||
|
common.SysLog("Syncing options from database")
|
||||||
|
loadOptionsFromDatabase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,11 +94,10 @@ func UpdateOption(key string, value string) error {
|
|||||||
// otherwise it will execute Update (with all fields).
|
// otherwise it will execute Update (with all fields).
|
||||||
DB.Save(&option)
|
DB.Save(&option)
|
||||||
// Update OptionMap
|
// Update OptionMap
|
||||||
updateOptionMap(key, value)
|
return updateOptionMap(key, value)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOptionMap(key string, value string) {
|
func updateOptionMap(key string, value string) (err error) {
|
||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
defer common.OptionMapRWMutex.Unlock()
|
defer common.OptionMapRWMutex.Unlock()
|
||||||
common.OptionMap[key] = value
|
common.OptionMap[key] = value
|
||||||
@@ -109,13 +131,20 @@ func updateOptionMap(key string, value string) {
|
|||||||
common.TurnstileCheckEnabled = boolValue
|
common.TurnstileCheckEnabled = boolValue
|
||||||
case "RegisterEnabled":
|
case "RegisterEnabled":
|
||||||
common.RegisterEnabled = boolValue
|
common.RegisterEnabled = boolValue
|
||||||
|
case "AutomaticDisableChannelEnabled":
|
||||||
|
common.AutomaticDisableChannelEnabled = boolValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
case "SMTPServer":
|
case "SMTPServer":
|
||||||
common.SMTPServer = value
|
common.SMTPServer = value
|
||||||
|
case "SMTPPort":
|
||||||
|
intValue, _ := strconv.Atoi(value)
|
||||||
|
common.SMTPPort = intValue
|
||||||
case "SMTPAccount":
|
case "SMTPAccount":
|
||||||
common.SMTPAccount = value
|
common.SMTPAccount = value
|
||||||
|
case "SMTPFrom":
|
||||||
|
common.SMTPFrom = value
|
||||||
case "SMTPToken":
|
case "SMTPToken":
|
||||||
common.SMTPToken = value
|
common.SMTPToken = value
|
||||||
case "ServerAddress":
|
case "ServerAddress":
|
||||||
@@ -126,6 +155,10 @@ func updateOptionMap(key string, value string) {
|
|||||||
common.GitHubClientSecret = value
|
common.GitHubClientSecret = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
|
case "SystemName":
|
||||||
|
common.SystemName = value
|
||||||
|
case "Logo":
|
||||||
|
common.Logo = value
|
||||||
case "WeChatServerAddress":
|
case "WeChatServerAddress":
|
||||||
common.WeChatServerAddress = value
|
common.WeChatServerAddress = value
|
||||||
case "WeChatServerToken":
|
case "WeChatServerToken":
|
||||||
@@ -138,13 +171,16 @@ func updateOptionMap(key string, value string) {
|
|||||||
common.TurnstileSecretKey = value
|
common.TurnstileSecretKey = value
|
||||||
case "QuotaForNewUser":
|
case "QuotaForNewUser":
|
||||||
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
||||||
case "RatioGPT3dot5":
|
case "QuotaRemindThreshold":
|
||||||
common.RatioGPT3dot5, _ = strconv.ParseFloat(value, 64)
|
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
|
||||||
case "RatioGPT4":
|
case "PreConsumedQuota":
|
||||||
common.RatioGPT4, _ = strconv.ParseFloat(value, 64)
|
common.PreConsumedQuota, _ = strconv.Atoi(value)
|
||||||
case "RatioGPT4_32k":
|
case "ModelRatio":
|
||||||
common.RatioGPT4_32k, _ = strconv.ParseFloat(value, 64)
|
err = common.UpdateModelRatioByJSONString(value)
|
||||||
case "TopUpLink":
|
case "TopUpLink":
|
||||||
common.TopUpLink = value
|
common.TopUpLink = value
|
||||||
|
case "ChannelDisableThreshold":
|
||||||
|
common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -40,12 +40,12 @@ func GetRedemptionById(id int) (*Redemption, error) {
|
|||||||
return &redemption, err
|
return &redemption, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Redeem(key string, tokenId int) (quota int, err error) {
|
func Redeem(key string, userId int) (quota int, err error) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return 0, errors.New("未提供兑换码")
|
return 0, errors.New("未提供兑换码")
|
||||||
}
|
}
|
||||||
if tokenId == 0 {
|
if userId == 0 {
|
||||||
return 0, errors.New("未提供 token id")
|
return 0, errors.New("无效的 user id")
|
||||||
}
|
}
|
||||||
redemption := &Redemption{}
|
redemption := &Redemption{}
|
||||||
err = DB.Where("`key` = ?", key).First(redemption).Error
|
err = DB.Where("`key` = ?", key).First(redemption).Error
|
||||||
@@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
|
|||||||
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
||||||
return 0, errors.New("该兑换码已被使用")
|
return 0, errors.New("该兑换码已被使用")
|
||||||
}
|
}
|
||||||
err = IncreaseTokenQuota(tokenId, redemption.Quota)
|
err = IncreaseUserQuota(userId, redemption.Quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
103
model/token.go
103
model/token.go
@@ -2,10 +2,10 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
_ "gorm.io/driver/sqlite"
|
_ "gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
@@ -37,7 +37,6 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
return nil, errors.New("未提供 token")
|
return nil, errors.New("未提供 token")
|
||||||
}
|
}
|
||||||
key = strings.Replace(key, "Bearer ", "", 1)
|
|
||||||
token = &Token{}
|
token = &Token{}
|
||||||
err = DB.Where("`key` = ?", key).First(token).Error
|
err = DB.Where("`key` = ?", key).First(token).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -82,6 +81,16 @@ func GetTokenByIds(id int, userId int) (*Token, error) {
|
|||||||
return &token, err
|
return &token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTokenById(id int) (*Token, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("id 为空!")
|
||||||
|
}
|
||||||
|
token := Token{Id: id}
|
||||||
|
var err error = nil
|
||||||
|
err = DB.First(&token, "id = ?", id).Error
|
||||||
|
return &token, err
|
||||||
|
}
|
||||||
|
|
||||||
func (token *Token) Insert() error {
|
func (token *Token) Insert() error {
|
||||||
var err error
|
var err error
|
||||||
err = DB.Create(token).Error
|
err = DB.Create(token).Error
|
||||||
@@ -116,26 +125,94 @@ func DeleteTokenById(id int, userId int) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
quota := token.RemainQuota
|
|
||||||
if quota != 0 {
|
|
||||||
if quota > 0 {
|
|
||||||
err = IncreaseUserQuota(userId, quota)
|
|
||||||
} else {
|
|
||||||
err = DecreaseUserQuota(userId, -quota)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return token.Delete()
|
return token.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
func IncreaseTokenQuota(id int, quota int) (err error) {
|
func IncreaseTokenQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecreaseTokenQuota(id int, quota int) (err error) {
|
func DecreaseTokenQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PreConsumeTokenQuota(tokenId int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
|
token, err := GetTokenById(tokenId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !token.UnlimitedQuota && token.RemainQuota < quota {
|
||||||
|
return errors.New("令牌额度不足")
|
||||||
|
}
|
||||||
|
userQuota, err := GetUserQuota(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if userQuota < quota {
|
||||||
|
return errors.New("用户额度不足")
|
||||||
|
}
|
||||||
|
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-quota < common.QuotaRemindThreshold
|
||||||
|
noMoreQuota := userQuota-quota <= 0
|
||||||
|
if quotaTooLow || noMoreQuota {
|
||||||
|
go func() {
|
||||||
|
email, err := GetUserEmail(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("获取用户邮箱失败:" + err.Error())
|
||||||
|
}
|
||||||
|
prompt := "您的额度即将用尽"
|
||||||
|
if noMoreQuota {
|
||||||
|
prompt = "您的额度已用尽"
|
||||||
|
}
|
||||||
|
if email != "" {
|
||||||
|
topUpLink := fmt.Sprintf("%s/topup", common.ServerAddress)
|
||||||
|
err = common.SendEmail(prompt, email,
|
||||||
|
fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("发送邮件失败:" + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if !token.UnlimitedQuota {
|
||||||
|
err = DecreaseTokenQuota(tokenId, quota)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = DecreaseUserQuota(token.UserId, quota)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostConsumeTokenQuota(tokenId int, quota int) (err error) {
|
||||||
|
token, err := GetTokenById(tokenId)
|
||||||
|
if quota > 0 {
|
||||||
|
err = DecreaseUserQuota(token.UserId, quota)
|
||||||
|
} else {
|
||||||
|
err = IncreaseUserQuota(token.UserId, -quota)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !token.UnlimitedQuota {
|
||||||
|
if quota > 0 {
|
||||||
|
err = DecreaseTokenQuota(tokenId, quota)
|
||||||
|
} else {
|
||||||
|
err = IncreaseTokenQuota(tokenId, -quota)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -225,12 +225,28 @@ func GetUserQuota(id int) (quota int, err error) {
|
|||||||
return quota, err
|
return quota, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserEmail(id int) (email string, err error) {
|
||||||
|
err = DB.Model(&User{}).Where("id = ?", id).Select("email").Find(&email).Error
|
||||||
|
return email, err
|
||||||
|
}
|
||||||
|
|
||||||
func IncreaseUserQuota(id int, quota int) (err error) {
|
func IncreaseUserQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecreaseUserQuota(id int, quota int) (err error) {
|
func DecreaseUserQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRootUserEmail() (email string) {
|
||||||
|
DB.Model(&User{}).Where("role = ?", common.RoleRootUser).Select("email").Find(&email)
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
@@ -15,6 +15,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/status", controller.GetStatus)
|
apiRouter.GET("/status", controller.GetStatus)
|
||||||
apiRouter.GET("/notice", controller.GetNotice)
|
apiRouter.GET("/notice", controller.GetNotice)
|
||||||
apiRouter.GET("/about", controller.GetAbout)
|
apiRouter.GET("/about", controller.GetAbout)
|
||||||
|
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
|
||||||
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
@@ -36,6 +37,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRoute := userRoute.Group("/")
|
adminRoute := userRoute.Group("/")
|
||||||
@@ -62,6 +64,8 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.GET("/", controller.GetAllChannels)
|
channelRoute.GET("/", controller.GetAllChannels)
|
||||||
channelRoute.GET("/search", controller.SearchChannels)
|
channelRoute.GET("/search", controller.SearchChannels)
|
||||||
channelRoute.GET("/:id", controller.GetChannel)
|
channelRoute.GET("/:id", controller.GetChannel)
|
||||||
|
channelRoute.GET("/test", controller.TestAllChannels)
|
||||||
|
channelRoute.GET("/test/:id", controller.TestChannel)
|
||||||
channelRoute.POST("/", controller.AddChannel)
|
channelRoute.POST("/", controller.AddChannel)
|
||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
channelRoute.PUT("/", controller.UpdateChannel)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||||
@@ -71,7 +75,6 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
tokenRoute.GET("/", controller.GetAllTokens)
|
tokenRoute.GET("/", controller.GetAllTokens)
|
||||||
tokenRoute.GET("/search", controller.SearchTokens)
|
tokenRoute.GET("/search", controller.SearchTokens)
|
||||||
tokenRoute.POST("/topup", controller.TopUp)
|
|
||||||
tokenRoute.GET("/:id", controller.GetToken)
|
tokenRoute.GET("/:id", controller.GetToken)
|
||||||
tokenRoute.POST("/", controller.AddToken)
|
tokenRoute.POST("/", controller.AddToken)
|
||||||
tokenRoute.PUT("/", controller.UpdateToken)
|
tokenRoute.PUT("/", controller.UpdateToken)
|
||||||
|
@@ -2,12 +2,24 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||||
SetApiRouter(router)
|
SetApiRouter(router)
|
||||||
SetDashboardRouter(router)
|
SetDashboardRouter(router)
|
||||||
SetRelayRouter(router)
|
SetRelayRouter(router)
|
||||||
setWebRouter(router, buildFS, indexPage)
|
frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
|
||||||
|
if frontendBaseUrl == "" {
|
||||||
|
SetWebRouter(router, buildFS, indexPage)
|
||||||
|
} else {
|
||||||
|
frontendBaseUrl = strings.TrimSuffix(frontendBaseUrl, "/")
|
||||||
|
router.NoRoute(func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusMovedPermanently, fmt.Sprintf("%s%s", frontendBaseUrl, c.Request.RequestURI))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,15 +11,15 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
relayV1Router := router.Group("/v1")
|
relayV1Router := router.Group("/v1")
|
||||||
relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||||
{
|
{
|
||||||
relayV1Router.GET("/models", controller.Relay)
|
relayV1Router.GET("/models", controller.ListModels)
|
||||||
relayV1Router.GET("/models/:model", controller.Relay)
|
relayV1Router.GET("/models/:model", controller.RetrieveModel)
|
||||||
relayV1Router.POST("/completions", controller.RelayNotImplemented)
|
relayV1Router.POST("/completions", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/chat/completions", controller.Relay)
|
relayV1Router.POST("/chat/completions", controller.Relay)
|
||||||
relayV1Router.POST("/edits", controller.RelayNotImplemented)
|
relayV1Router.POST("/edits", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
||||||
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.RelayNotImplemented)
|
relayV1Router.POST("/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,7 +10,7 @@ import (
|
|||||||
"one-api/middleware"
|
"one-api/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||||
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
router.Use(middleware.GlobalWebRateLimit())
|
router.Use(middleware.GlobalWebRateLimit())
|
||||||
router.Use(middleware.Cache())
|
router.Use(middleware.Cache())
|
||||||
|
@@ -9,7 +9,7 @@ import NotFound from './pages/NotFound';
|
|||||||
import Setting from './pages/Setting';
|
import Setting from './pages/Setting';
|
||||||
import EditUser from './pages/User/EditUser';
|
import EditUser from './pages/User/EditUser';
|
||||||
import AddUser from './pages/User/AddUser';
|
import AddUser from './pages/User/AddUser';
|
||||||
import { API, showError, showNotice } from './helpers';
|
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
||||||
import PasswordResetForm from './components/PasswordResetForm';
|
import PasswordResetForm from './components/PasswordResetForm';
|
||||||
import GitHubOAuth from './components/GitHubOAuth';
|
import GitHubOAuth from './components/GitHubOAuth';
|
||||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||||
@@ -19,9 +19,9 @@ import Channel from './pages/Channel';
|
|||||||
import Token from './pages/Token';
|
import Token from './pages/Token';
|
||||||
import EditToken from './pages/Token/EditToken';
|
import EditToken from './pages/Token/EditToken';
|
||||||
import EditChannel from './pages/Channel/EditChannel';
|
import EditChannel from './pages/Channel/EditChannel';
|
||||||
import AddChannel from './pages/Channel/AddChannel';
|
|
||||||
import Redemption from './pages/Redemption';
|
import Redemption from './pages/Redemption';
|
||||||
import EditRedemption from './pages/Redemption/EditRedemption';
|
import EditRedemption from './pages/Redemption/EditRedemption';
|
||||||
|
import TopUp from './pages/TopUp';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
@@ -43,6 +43,8 @@ function App() {
|
|||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem('status', JSON.stringify(data));
|
localStorage.setItem('status', JSON.stringify(data));
|
||||||
statusDispatch({ type: 'set', payload: data });
|
statusDispatch({ type: 'set', payload: data });
|
||||||
|
localStorage.setItem('system_name', data.system_name);
|
||||||
|
localStorage.setItem('logo', data.logo);
|
||||||
localStorage.setItem('footer_html', data.footer_html);
|
localStorage.setItem('footer_html', data.footer_html);
|
||||||
if (
|
if (
|
||||||
data.version !== process.env.REACT_APP_VERSION &&
|
data.version !== process.env.REACT_APP_VERSION &&
|
||||||
@@ -61,6 +63,17 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
loadStatus().then();
|
loadStatus().then();
|
||||||
|
let systemName = getSystemName();
|
||||||
|
if (systemName) {
|
||||||
|
document.title = systemName;
|
||||||
|
}
|
||||||
|
let logo = getLogo();
|
||||||
|
if (logo) {
|
||||||
|
let linkElement = document.querySelector("link[rel~='icon']");
|
||||||
|
if (linkElement) {
|
||||||
|
linkElement.href = logo;
|
||||||
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,7 +106,7 @@ function App() {
|
|||||||
path='/channel/add'
|
path='/channel/add'
|
||||||
element={
|
element={
|
||||||
<Suspense fallback={<Loading></Loading>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<AddChannel />
|
<EditChannel />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -227,6 +240,16 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/topup'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<TopUp />
|
||||||
|
</Suspense>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/about'
|
path='/about'
|
||||||
element={
|
element={
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Pagination, 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, copy, showError, 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';
|
||||||
|
|
||||||
@@ -60,6 +60,11 @@ const ChannelsTable = () => {
|
|||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await loadChannels(0);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadChannels(0)
|
loadChannels(0)
|
||||||
.then()
|
.then()
|
||||||
@@ -120,6 +125,22 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderResponseTime = (responseTime) => {
|
||||||
|
let time = responseTime / 1000;
|
||||||
|
time = time.toFixed(2) + " 秒";
|
||||||
|
if (responseTime === 0) {
|
||||||
|
return <Label basic color='grey'>未测试</Label>;
|
||||||
|
} else if (responseTime <= 1000) {
|
||||||
|
return <Label basic color='green'>{time}</Label>;
|
||||||
|
} else if (responseTime <= 3000) {
|
||||||
|
return <Label basic color='olive'>{time}</Label>;
|
||||||
|
} else if (responseTime <= 5000) {
|
||||||
|
return <Label basic color='yellow'>{time}</Label>;
|
||||||
|
} else {
|
||||||
|
return <Label basic color='red'>{time}</Label>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const searchChannels = async () => {
|
const searchChannels = async () => {
|
||||||
if (searchKeyword === '') {
|
if (searchKeyword === '') {
|
||||||
// if keyword is blank, load files instead.
|
// if keyword is blank, load files instead.
|
||||||
@@ -139,6 +160,31 @@ const ChannelsTable = () => {
|
|||||||
setSearching(false);
|
setSearching(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testChannel = async (id, name, idx) => {
|
||||||
|
const res = await API.get(`/api/channel/test/${id}/`);
|
||||||
|
const { success, message, time } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let newChannels = [...channels];
|
||||||
|
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||||
|
newChannels[realIdx].response_time = time * 1000;
|
||||||
|
newChannels[realIdx].test_time = Date.now() / 1000;
|
||||||
|
setChannels(newChannels);
|
||||||
|
showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAllChannels = async () => {
|
||||||
|
const res = await API.get(`/api/channel/test`);
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showInfo("已成功开始测试所有已启用通道,请刷新页面查看结果。");
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeywordChange = async (e, { value }) => {
|
const handleKeywordChange = async (e, { value }) => {
|
||||||
setSearchKeyword(value.trim());
|
setSearchKeyword(value.trim());
|
||||||
};
|
};
|
||||||
@@ -209,18 +255,18 @@ const ChannelsTable = () => {
|
|||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortChannel('created_time');
|
sortChannel('response_time');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
创建时间
|
响应时间
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortChannel('accessed_time');
|
sortChannel('test_time');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
访问时间
|
测试时间
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
@@ -240,19 +286,38 @@ const ChannelsTable = () => {
|
|||||||
<Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
|
<Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
|
||||||
<Table.Cell>{renderType(channel.type)}</Table.Cell>
|
<Table.Cell>{renderType(channel.type)}</Table.Cell>
|
||||||
<Table.Cell>{renderStatus(channel.status)}</Table.Cell>
|
<Table.Cell>{renderStatus(channel.status)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(channel.created_time)}</Table.Cell>
|
<Table.Cell>{renderResponseTime(channel.response_time)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(channel.accessed_time)}</Table.Cell>
|
<Table.Cell>{channel.test_time ? renderTimestamp(channel.test_time) : "未测试"}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
negative
|
positive
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
manageChannel(channel.id, 'delete', idx);
|
testChannel(channel.id, channel.name, idx);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
删除
|
测试
|
||||||
</Button>
|
</Button>
|
||||||
|
<Popup
|
||||||
|
trigger={
|
||||||
|
<Button size='small' negative>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageChannel(channel.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除渠道 {channel.name}
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -285,6 +350,9 @@ const ChannelsTable = () => {
|
|||||||
<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>
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
@@ -296,6 +364,7 @@ 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>
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Footer>
|
</Table.Footer>
|
||||||
|
@@ -1,40 +1,37 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Container, Segment } from 'semantic-ui-react';
|
import { Container, Segment } from 'semantic-ui-react';
|
||||||
|
import { getFooterHTML, getSystemName } from '../helpers';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const [Footer, setFooter] = useState('');
|
const systemName = getSystemName();
|
||||||
useEffect(() => {
|
const footer = getFooterHTML();
|
||||||
let savedFooter = localStorage.getItem('footer_html');
|
|
||||||
if (!savedFooter) savedFooter = '';
|
|
||||||
setFooter(savedFooter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Segment vertical>
|
<Segment vertical>
|
||||||
<Container textAlign="center">
|
<Container textAlign='center'>
|
||||||
{Footer === '' ? (
|
{footer ? (
|
||||||
<div className="custom-footer">
|
<div
|
||||||
|
className='custom-footer'
|
||||||
|
dangerouslySetInnerHTML={{ __html: footer }}
|
||||||
|
></div>
|
||||||
|
) : (
|
||||||
|
<div className='custom-footer'>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/songquanpeng/one-api"
|
href='https://github.com/songquanpeng/one-api'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
>
|
>
|
||||||
One API {process.env.REACT_APP_VERSION}{' '}
|
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||||
</a>
|
</a>
|
||||||
由{' '}
|
由{' '}
|
||||||
<a href="https://github.com/songquanpeng" target="_blank">
|
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||||
JustSong
|
JustSong
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
构建,源代码遵循{' '}
|
构建,源代码遵循{' '}
|
||||||
<a href="https://opensource.org/licenses/mit-license.php">
|
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||||
MIT 协议
|
MIT 协议
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="custom-footer"
|
|
||||||
dangerouslySetInnerHTML={{ __html: Footer }}
|
|
||||||
></div>
|
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
@@ -3,7 +3,7 @@ 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 { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||||
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
|
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||||
import '../index.css';
|
import '../index.css';
|
||||||
|
|
||||||
// Header Buttons
|
// Header Buttons
|
||||||
@@ -30,6 +30,11 @@ const headerButtons = [
|
|||||||
icon: 'dollar sign',
|
icon: 'dollar sign',
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '充值',
|
||||||
|
to: '/topup',
|
||||||
|
icon: 'cart',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '用户',
|
name: '用户',
|
||||||
to: '/user',
|
to: '/user',
|
||||||
@@ -53,6 +58,8 @@ const Header = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
const systemName = getSystemName();
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
setShowSidebar(false);
|
setShowSidebar(false);
|
||||||
@@ -111,12 +118,12 @@ const Header = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/'>
|
<Menu.Item as={Link} to='/'>
|
||||||
<img
|
<img
|
||||||
src='/logo.png'
|
src={logo}
|
||||||
alt='logo'
|
alt='logo'
|
||||||
style={{ marginRight: '0.75em' }}
|
style={{ marginRight: '0.75em' }}
|
||||||
/>
|
/>
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Menu position='right'>
|
<Menu.Menu position='right'>
|
||||||
@@ -168,9 +175,9 @@ const Header = () => {
|
|||||||
<Menu borderless style={{ borderTop: 'none' }}>
|
<Menu borderless style={{ borderTop: 'none' }}>
|
||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||||
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
|
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{renderButtons(false)}
|
{renderButtons(false)}
|
||||||
|
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, showError, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@@ -27,6 +27,7 @@ const LoginForm = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("expired")) {
|
if (searchParams.get("expired")) {
|
||||||
@@ -95,7 +96,7 @@ const LoginForm = () => {
|
|||||||
<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.png" /> 用户登录
|
<Image src={logo} /> 用户登录
|
||||||
</Header>
|
</Header>
|
||||||
<Form size="large">
|
<Form size="large">
|
||||||
<Segment>
|
<Segment>
|
||||||
|
@@ -8,8 +8,10 @@ const OtherSetting = () => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
About: '',
|
About: '',
|
||||||
|
SystemName: '',
|
||||||
|
Logo: '',
|
||||||
|
HomePageContent: '',
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
|
||||||
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({
|
||||||
@@ -18,7 +20,7 @@ const OtherSetting = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
@@ -28,7 +30,6 @@ const OtherSetting = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
originInputs = newInputs;
|
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ const OtherSetting = () => {
|
|||||||
|
|
||||||
const updateOption = async (key, value) => {
|
const updateOption = async (key, value) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.put('/api/option', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
@@ -65,10 +66,22 @@ const OtherSetting = () => {
|
|||||||
await updateOption('Footer', inputs.Footer);
|
await updateOption('Footer', inputs.Footer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitSystemName = async () => {
|
||||||
|
await updateOption('SystemName', inputs.SystemName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitLogo = async () => {
|
||||||
|
await updateOption('Logo', inputs.Logo);
|
||||||
|
};
|
||||||
|
|
||||||
const submitAbout = async () => {
|
const submitAbout = async () => {
|
||||||
await updateOption('About', inputs.About);
|
await updateOption('About', inputs.About);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitOption = async (key) => {
|
||||||
|
await updateOption(key, inputs[key]);
|
||||||
|
};
|
||||||
|
|
||||||
const openGitHubRelease = () => {
|
const openGitHubRelease = () => {
|
||||||
window.location =
|
window.location =
|
||||||
'https://github.com/songquanpeng/one-api/releases/latest';
|
'https://github.com/songquanpeng/one-api/releases/latest';
|
||||||
@@ -109,10 +122,42 @@ const OtherSetting = () => {
|
|||||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>个性化设置</Header>
|
<Header as='h3'>个性化设置</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='系统名称'
|
||||||
|
placeholder='在此输入系统名称'
|
||||||
|
value={inputs.SystemName}
|
||||||
|
name='SystemName'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='Logo 图片地址'
|
||||||
|
placeholder='在此输入 Logo 图片地址'
|
||||||
|
value={inputs.Logo}
|
||||||
|
name='Logo'
|
||||||
|
type='url'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.TextArea
|
||||||
|
label='首页内容'
|
||||||
|
placeholder='在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
|
||||||
|
value={inputs.HomePageContent}
|
||||||
|
name='HomePageContent'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={()=>submitOption('HomePageContent')}>保存首页内容</Form.Button>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='关于'
|
label='关于'
|
||||||
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码'
|
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
|
||||||
value={inputs.About}
|
value={inputs.About}
|
||||||
name='About'
|
name='About'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
Segment,
|
Segment,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
|
||||||
const RegisterForm = () => {
|
const RegisterForm = () => {
|
||||||
@@ -26,6 +26,7 @@ const RegisterForm = () => {
|
|||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -100,7 +101,7 @@ const RegisterForm = () => {
|
|||||||
<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.png' /> 新用户注册
|
<Image src={logo} /> 新用户注册
|
||||||
</Header>
|
</Header>
|
||||||
<Form size='large'>
|
<Form size='large'>
|
||||||
<Segment>
|
<Segment>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||||
import { API, removeTrailingSlash, showError } from '../helpers';
|
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
||||||
|
|
||||||
const SystemSetting = () => {
|
const SystemSetting = () => {
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
@@ -12,7 +12,9 @@ const SystemSetting = () => {
|
|||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
|
SMTPPort: '',
|
||||||
SMTPAccount: '',
|
SMTPAccount: '',
|
||||||
|
SMTPFrom: '',
|
||||||
SMTPToken: '',
|
SMTPToken: '',
|
||||||
ServerAddress: '',
|
ServerAddress: '',
|
||||||
Footer: '',
|
Footer: '',
|
||||||
@@ -25,16 +27,18 @@ const SystemSetting = () => {
|
|||||||
TurnstileSecretKey: '',
|
TurnstileSecretKey: '',
|
||||||
RegisterEnabled: '',
|
RegisterEnabled: '',
|
||||||
QuotaForNewUser: 0,
|
QuotaForNewUser: 0,
|
||||||
RatioGPT3dot5: 2,
|
QuotaRemindThreshold: 0,
|
||||||
RatioGPT4: 30,
|
PreConsumedQuota: 0,
|
||||||
RatioGPT4_32k: 60,
|
ModelRatio: '',
|
||||||
TopUpLink: ''
|
TopUpLink: '',
|
||||||
|
AutomaticDisableChannelEnabled: '',
|
||||||
|
ChannelDisableThreshold: 0,
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
@@ -42,7 +46,7 @@ const SystemSetting = () => {
|
|||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
originInputs = newInputs;
|
setOriginInputs(newInputs);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -62,12 +66,13 @@ const SystemSetting = () => {
|
|||||||
case 'WeChatAuthEnabled':
|
case 'WeChatAuthEnabled':
|
||||||
case 'TurnstileCheckEnabled':
|
case 'TurnstileCheckEnabled':
|
||||||
case 'RegisterEnabled':
|
case 'RegisterEnabled':
|
||||||
|
case 'AutomaticDisableChannelEnabled':
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const res = await API.put('/api/option', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
@@ -93,7 +98,9 @@ const SystemSetting = () => {
|
|||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey' ||
|
name === 'TurnstileSecretKey' ||
|
||||||
name === 'QuotaForNewUser' ||
|
name === 'QuotaForNewUser' ||
|
||||||
name.startsWith('Ratio') ||
|
name === 'QuotaRemindThreshold' ||
|
||||||
|
name === 'PreConsumedQuota' ||
|
||||||
|
name === 'ModelRatio' ||
|
||||||
name === 'TopUpLink'
|
name === 'TopUpLink'
|
||||||
) {
|
) {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
@@ -111,19 +118,23 @@ const SystemSetting = () => {
|
|||||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||||
}
|
}
|
||||||
if (originInputs['RatioGPT3dot5'] !== inputs.RatioGPT3dot5) {
|
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||||
await updateOption('RatioGPT3dot5', inputs.RatioGPT3dot5);
|
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||||
}
|
}
|
||||||
if (originInputs['RatioGPT4'] !== inputs.RatioGPT4) {
|
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
|
||||||
await updateOption('RatioGPT4', inputs.RatioGPT4);
|
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
|
||||||
}
|
}
|
||||||
if (originInputs['RatioGPT4_32k'] !== inputs.RatioGPT4_32k) {
|
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
||||||
await updateOption('RatioGPT4_32k', inputs.RatioGPT4_32k);
|
if (!verifyJSON(inputs.ModelRatio)) {
|
||||||
|
showError('模型倍率不是合法的 JSON 字符串');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateOption('ModelRatio', inputs.ModelRatio);
|
||||||
}
|
}
|
||||||
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
||||||
await updateOption('TopUpLink', inputs.TopUpLink);
|
await updateOption('TopUpLink', inputs.TopUpLink);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const submitSMTP = async () => {
|
const submitSMTP = async () => {
|
||||||
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
||||||
@@ -132,6 +143,15 @@ const SystemSetting = () => {
|
|||||||
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
||||||
await updateOption('SMTPAccount', inputs.SMTPAccount);
|
await updateOption('SMTPAccount', inputs.SMTPAccount);
|
||||||
}
|
}
|
||||||
|
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
|
||||||
|
await updateOption('SMTPFrom', inputs.SMTPFrom);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['SMTPPort'] !== inputs.SMTPPort &&
|
||||||
|
inputs.SMTPPort !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('SMTPPort', inputs.SMTPPort);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
originInputs['SMTPToken'] !== inputs.SMTPToken &&
|
originInputs['SMTPToken'] !== inputs.SMTPToken &&
|
||||||
inputs.SMTPToken !== ''
|
inputs.SMTPToken !== ''
|
||||||
@@ -257,12 +277,12 @@ const SystemSetting = () => {
|
|||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
运营设置
|
运营设置
|
||||||
</Header>
|
</Header>
|
||||||
<Form.Group widths={3}>
|
<Form.Group widths={4}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='新用户初始配额'
|
label='新用户初始配额'
|
||||||
name='QuotaForNewUser'
|
name='QuotaForNewUser'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.QuotaForNewUser}
|
value={inputs.QuotaForNewUser}
|
||||||
type='number'
|
type='number'
|
||||||
min='0'
|
min='0'
|
||||||
@@ -272,49 +292,69 @@ const SystemSetting = () => {
|
|||||||
label='充值链接'
|
label='充值链接'
|
||||||
name='TopUpLink'
|
name='TopUpLink'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.TopUpLink}
|
value={inputs.TopUpLink}
|
||||||
type='link'
|
type='link'
|
||||||
placeholder='例如发卡网站的购买链接'
|
placeholder='例如发卡网站的购买链接'
|
||||||
/>
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='额度提醒阈值'
|
||||||
|
name='QuotaRemindThreshold'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.QuotaRemindThreshold}
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
placeholder='低于此额度时将发送邮件提醒用户'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='请求预扣费额度'
|
||||||
|
name='PreConsumedQuota'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.PreConsumedQuota}
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
placeholder='请求结束后多退少补'
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group widths={3}>
|
<Form.Group widths='equal'>
|
||||||
<Form.Input
|
<Form.TextArea
|
||||||
label='GPT-3.5 系列模型倍率'
|
label='模型倍率'
|
||||||
name='RatioGPT3dot5'
|
name='ModelRatio'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
value={inputs.RatioGPT3dot5}
|
autoComplete='new-password'
|
||||||
type='number'
|
value={inputs.ModelRatio}
|
||||||
step='0.01'
|
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
||||||
min='0'
|
|
||||||
placeholder='例如:2'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='GPT-4 系列模型倍率'
|
|
||||||
name='RatioGPT4'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='off'
|
|
||||||
value={inputs.RatioGPT4}
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:30'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='GPT-4 32k 系列模型倍率'
|
|
||||||
name='RatioGPT4_32k'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='off'
|
|
||||||
value={inputs.RatioGPT4_32k}
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:60'
|
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
|
<Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
监控设置
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='最长响应时间'
|
||||||
|
name='ChannelDisableThreshold'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.ChannelDisableThreshold}
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group inline>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
||||||
|
label='失败时自动禁用通道'
|
||||||
|
name='AutomaticDisableChannelEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
配置 SMTP
|
配置 SMTP
|
||||||
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||||
@@ -324,24 +364,42 @@ const SystemSetting = () => {
|
|||||||
label='SMTP 服务器地址'
|
label='SMTP 服务器地址'
|
||||||
name='SMTPServer'
|
name='SMTPServer'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.SMTPServer}
|
value={inputs.SMTPServer}
|
||||||
placeholder='例如:smtp.qq.com'
|
placeholder='例如:smtp.qq.com'
|
||||||
/>
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 端口'
|
||||||
|
name='SMTPPort'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPPort}
|
||||||
|
placeholder='默认: 587'
|
||||||
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='SMTP 账户'
|
label='SMTP 账户'
|
||||||
name='SMTPAccount'
|
name='SMTPAccount'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.SMTPAccount}
|
value={inputs.SMTPAccount}
|
||||||
placeholder='通常是邮箱地址'
|
placeholder='通常是邮箱地址'
|
||||||
/>
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='SMTP 发送者邮箱'
|
||||||
|
name='SMTPFrom'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.SMTPFrom}
|
||||||
|
placeholder='通常和邮箱地址保持一致'
|
||||||
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='SMTP 访问凭证'
|
label='SMTP 访问凭证'
|
||||||
name='SMTPToken'
|
name='SMTPToken'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.SMTPToken}
|
value={inputs.SMTPToken}
|
||||||
placeholder='敏感信息不会发送到前端显示'
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
/>
|
/>
|
||||||
@@ -368,7 +426,7 @@ const SystemSetting = () => {
|
|||||||
label='GitHub Client ID'
|
label='GitHub Client ID'
|
||||||
name='GitHubClientId'
|
name='GitHubClientId'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.GitHubClientId}
|
value={inputs.GitHubClientId}
|
||||||
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
||||||
/>
|
/>
|
||||||
@@ -377,7 +435,7 @@ const SystemSetting = () => {
|
|||||||
name='GitHubClientSecret'
|
name='GitHubClientSecret'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.GitHubClientSecret}
|
value={inputs.GitHubClientSecret}
|
||||||
placeholder='敏感信息不会发送到前端显示'
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
/>
|
/>
|
||||||
@@ -405,7 +463,7 @@ const SystemSetting = () => {
|
|||||||
name='WeChatServerAddress'
|
name='WeChatServerAddress'
|
||||||
placeholder='例如:https://yourdomain.com'
|
placeholder='例如:https://yourdomain.com'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.WeChatServerAddress}
|
value={inputs.WeChatServerAddress}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
@@ -413,7 +471,7 @@ const SystemSetting = () => {
|
|||||||
name='WeChatServerToken'
|
name='WeChatServerToken'
|
||||||
type='password'
|
type='password'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.WeChatServerToken}
|
value={inputs.WeChatServerToken}
|
||||||
placeholder='敏感信息不会发送到前端显示'
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
/>
|
/>
|
||||||
@@ -421,7 +479,7 @@ const SystemSetting = () => {
|
|||||||
label='微信公众号二维码图片链接'
|
label='微信公众号二维码图片链接'
|
||||||
name='WeChatAccountQRCodeImageURL'
|
name='WeChatAccountQRCodeImageURL'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.WeChatAccountQRCodeImageURL}
|
value={inputs.WeChatAccountQRCodeImageURL}
|
||||||
placeholder='输入一个图片链接'
|
placeholder='输入一个图片链接'
|
||||||
/>
|
/>
|
||||||
@@ -445,7 +503,7 @@ const SystemSetting = () => {
|
|||||||
label='Turnstile Site Key'
|
label='Turnstile Site Key'
|
||||||
name='TurnstileSiteKey'
|
name='TurnstileSiteKey'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.TurnstileSiteKey}
|
value={inputs.TurnstileSiteKey}
|
||||||
placeholder='输入你注册的 Turnstile Site Key'
|
placeholder='输入你注册的 Turnstile Site Key'
|
||||||
/>
|
/>
|
||||||
@@ -454,7 +512,7 @@ const SystemSetting = () => {
|
|||||||
name='TurnstileSecretKey'
|
name='TurnstileSecretKey'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
value={inputs.TurnstileSecretKey}
|
value={inputs.TurnstileSecretKey}
|
||||||
placeholder='敏感信息不会发送到前端显示'
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
/>
|
/>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Modal, Pagination, 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';
|
||||||
|
|
||||||
@@ -36,8 +36,6 @@ const TokensTable = () => {
|
|||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||||
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
||||||
const [redemptionCode, setRedemptionCode] = useState('');
|
|
||||||
const [topUpLink, setTopUpLink] = useState('');
|
|
||||||
|
|
||||||
const loadTokens = async (startIdx) => {
|
const loadTokens = async (startIdx) => {
|
||||||
const res = await API.get(`/api/token/?p=${startIdx}`);
|
const res = await API.get(`/api/token/?p=${startIdx}`);
|
||||||
@@ -66,19 +64,17 @@ const TokensTable = () => {
|
|||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await loadTokens(0);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTokens(0)
|
loadTokens(0)
|
||||||
.then()
|
.then()
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
showError(reason);
|
showError(reason);
|
||||||
});
|
});
|
||||||
let status = localStorage.getItem('status');
|
|
||||||
if (status) {
|
|
||||||
status = JSON.parse(status);
|
|
||||||
if (status.top_up_link) {
|
|
||||||
setTopUpLink(status.top_up_link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const manageToken = async (id, action, idx) => {
|
const manageToken = async (id, action, idx) => {
|
||||||
@@ -151,28 +147,6 @@ const TokensTable = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const topUp = async () => {
|
|
||||||
if (redemptionCode === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await API.post('/api/token/topup/', {
|
|
||||||
id: tokens[targetTokenIdx].id,
|
|
||||||
key: redemptionCode
|
|
||||||
});
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('充值成功!');
|
|
||||||
let newTokens = [...tokens];
|
|
||||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + targetTokenIdx;
|
|
||||||
newTokens[realIdx].remain_quota += data;
|
|
||||||
setTokens(newTokens);
|
|
||||||
setRedemptionCode('');
|
|
||||||
setShowTopUpModal(false);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form onSubmit={searchTokens}>
|
<Form onSubmit={searchTokens}>
|
||||||
@@ -274,24 +248,25 @@ const TokensTable = () => {
|
|||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Popup
|
||||||
size={'small'}
|
trigger={
|
||||||
color={'yellow'}
|
<Button size='small' negative>
|
||||||
onClick={() => {
|
删除
|
||||||
setTargetTokenIdx(idx);
|
</Button>
|
||||||
setShowTopUpModal(true);
|
}
|
||||||
}}>
|
on='click'
|
||||||
充值
|
flowing
|
||||||
</Button>
|
hoverable
|
||||||
<Button
|
|
||||||
size={'small'}
|
|
||||||
negative
|
|
||||||
onClick={() => {
|
|
||||||
manageToken(token.id, 'delete', idx);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageToken(token.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除令牌 {token.name}
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -324,6 +299,7 @@ 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>
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
@@ -339,39 +315,6 @@ const TokensTable = () => {
|
|||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Footer>
|
</Table.Footer>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Modal
|
|
||||||
onClose={() => setShowTopUpModal(false)}
|
|
||||||
onOpen={() => setShowTopUpModal(true)}
|
|
||||||
open={showTopUpModal}
|
|
||||||
size={'mini'}
|
|
||||||
>
|
|
||||||
<Modal.Header>通过兑换码为令牌「{tokens[targetTokenIdx]?.name}」充值</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Modal.Description>
|
|
||||||
{/*<Image src={status.wechat_qrcode} fluid />*/}
|
|
||||||
{
|
|
||||||
topUpLink && <p>
|
|
||||||
<a target='_blank' href={topUpLink}>点击此处获取兑换码</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<Form size='large'>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder='兑换码'
|
|
||||||
name='redemptionCode'
|
|
||||||
value={redemptionCode}
|
|
||||||
onChange={(e) => {
|
|
||||||
setRedemptionCode(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button color='' fluid size='large' onClick={topUp}>
|
|
||||||
充值
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Modal.Description>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,6 +4,7 @@ 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 { renderText } from '../helpers/render';
|
||||||
|
|
||||||
function renderRole(role) {
|
function renderRole(role) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -64,7 +65,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) {
|
||||||
@@ -158,6 +159,14 @@ const UsersTable = () => {
|
|||||||
<Table basic>
|
<Table basic>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
|
<Table.HeaderCell
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
sortUser('id');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ID
|
||||||
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -166,14 +175,6 @@ const UsersTable = () => {
|
|||||||
>
|
>
|
||||||
用户名
|
用户名
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
sortUser('display_name');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
显示名称
|
|
||||||
</Table.HeaderCell>
|
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -182,6 +183,14 @@ const UsersTable = () => {
|
|||||||
>
|
>
|
||||||
邮箱地址
|
邮箱地址
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
sortUser('quota');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
剩余额度
|
||||||
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -212,9 +221,18 @@ const UsersTable = () => {
|
|||||||
if (user.deleted) return <></>;
|
if (user.deleted) return <></>;
|
||||||
return (
|
return (
|
||||||
<Table.Row key={user.id}>
|
<Table.Row key={user.id}>
|
||||||
<Table.Cell>{user.username}</Table.Cell>
|
<Table.Cell>{user.id}</Table.Cell>
|
||||||
<Table.Cell>{user.display_name}</Table.Cell>
|
<Table.Cell>
|
||||||
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
|
<Popup
|
||||||
|
content={user.email ? user.email : '未绑定邮箱地址'}
|
||||||
|
key={user.display_name}
|
||||||
|
header={user.display_name ? user.display_name : user.username}
|
||||||
|
trigger={<span>{renderText(user.username, 10)}</span>}
|
||||||
|
hoverable
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{user.email ? renderText(user.email, 30) : '无'}</Table.Cell>
|
||||||
|
<Table.Cell>{user.quota}</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>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@@ -225,6 +243,7 @@ const UsersTable = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
manageUser(user.username, 'promote', idx);
|
manageUser(user.username, 'promote', idx);
|
||||||
}}
|
}}
|
||||||
|
disabled={user.role === 100}
|
||||||
>
|
>
|
||||||
提升
|
提升
|
||||||
</Button>
|
</Button>
|
||||||
@@ -234,12 +253,13 @@ const UsersTable = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
manageUser(user.username, 'demote', idx);
|
manageUser(user.username, 'demote', idx);
|
||||||
}}
|
}}
|
||||||
|
disabled={user.role === 100}
|
||||||
>
|
>
|
||||||
降级
|
降级
|
||||||
</Button>
|
</Button>
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<Button size='small' negative>
|
<Button size='small' negative disabled={user.role === 100}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -265,6 +285,7 @@ const UsersTable = () => {
|
|||||||
idx
|
idx
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
disabled={user.role === 100}
|
||||||
>
|
>
|
||||||
{user.status === 1 ? '禁用' : '启用'}
|
{user.status === 1 ? '禁用' : '启用'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -272,6 +293,7 @@ const UsersTable = () => {
|
|||||||
size={'small'}
|
size={'small'}
|
||||||
as={Link}
|
as={Link}
|
||||||
to={'/user/edit/' + user.id}
|
to={'/user/edit/' + user.id}
|
||||||
|
disabled={user.role === 100}
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
@@ -284,7 +306,7 @@ const UsersTable = () => {
|
|||||||
|
|
||||||
<Table.Footer>
|
<Table.Footer>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.HeaderCell colSpan='6'>
|
<Table.HeaderCell colSpan='7'>
|
||||||
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
||||||
添加新的用户
|
添加新的用户
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -6,5 +6,6 @@ export const CHANNEL_OPTIONS = [
|
|||||||
{ key: 5, text: 'OpenAI-SB', value: 5, color: 'brown' },
|
{ key: 5, text: 'OpenAI-SB', value: 5, color: 'brown' },
|
||||||
{ key: 6, text: 'OpenAI Max', value: 6, color: 'violet' },
|
{ key: 6, text: 'OpenAI Max', value: 6, color: 'violet' },
|
||||||
{ key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
|
{ key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
|
||||||
|
{ key: 9, text: 'AI.LS', value: 9, color: 'yellow' },
|
||||||
{ key: 8, text: '自定义', value: 8, color: 'pink' }
|
{ key: 8, text: '自定义', value: 8, color: 'pink' }
|
||||||
];
|
];
|
||||||
|
6
web/src/helpers/render.js
Normal file
6
web/src/helpers/render.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function renderText(text, limit) {
|
||||||
|
if (text.length > limit) {
|
||||||
|
return text.slice(0, limit - 3) + '...';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
@@ -15,6 +15,22 @@ export function isRoot() {
|
|||||||
return user.role >= 100;
|
return user.role >= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSystemName() {
|
||||||
|
let system_name = localStorage.getItem('system_name');
|
||||||
|
if (!system_name) return 'One API';
|
||||||
|
return system_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogo() {
|
||||||
|
let logo = localStorage.getItem('logo');
|
||||||
|
if (!logo) return '/logo.png';
|
||||||
|
return logo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFooterHTML() {
|
||||||
|
return localStorage.getItem('footer_html');
|
||||||
|
}
|
||||||
|
|
||||||
export async function copy(text) {
|
export async function copy(text) {
|
||||||
let okay = true;
|
let okay = true;
|
||||||
try {
|
try {
|
||||||
@@ -152,4 +168,13 @@ export function downloadTextAsFile(text, filename) {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const verifyJSON = (str) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
@@ -5,6 +5,11 @@ body {
|
|||||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
|
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@@ -5,18 +5,24 @@ import { marked } from 'marked';
|
|||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const [about, setAbout] = useState('');
|
const [about, setAbout] = useState('');
|
||||||
|
const [aboutLoaded, setAboutLoaded] = useState(false);
|
||||||
|
|
||||||
const displayAbout = async () => {
|
const displayAbout = async () => {
|
||||||
|
setAbout(localStorage.getItem('about') || '');
|
||||||
const res = await API.get('/api/about');
|
const res = await API.get('/api/about');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let HTMLAbout = marked.parse(data);
|
let aboutContent = data;
|
||||||
localStorage.setItem('about', HTMLAbout);
|
if (!data.startsWith('https://')) {
|
||||||
setAbout(HTMLAbout);
|
aboutContent = marked.parse(data);
|
||||||
|
}
|
||||||
|
setAbout(aboutContent);
|
||||||
|
localStorage.setItem('about', aboutContent);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
setAbout('加载关于内容失败...');
|
setAbout('加载关于内容失败...');
|
||||||
}
|
}
|
||||||
|
setAboutLoaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -25,20 +31,27 @@ const About = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
{
|
||||||
{
|
aboutLoaded && about === '' ? <>
|
||||||
about === '' ? <>
|
<Segment>
|
||||||
<Header as='h3'>关于</Header>
|
<Header as='h3'>关于</Header>
|
||||||
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
||||||
项目仓库地址:
|
项目仓库地址:
|
||||||
<a href="https://github.com/songquanpeng/one-api">
|
<a href='https://github.com/songquanpeng/one-api'>
|
||||||
https://github.com/songquanpeng/one-api
|
https://github.com/songquanpeng/one-api
|
||||||
</a>
|
</a>
|
||||||
</> : <>
|
</Segment>
|
||||||
<div dangerouslySetInnerHTML={{ __html: about}}></div>
|
</> : <>
|
||||||
</>
|
{
|
||||||
}
|
about.startsWith('https://') ? <iframe
|
||||||
</Segment>
|
src={about}
|
||||||
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
|
/> : <Segment>
|
||||||
|
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
||||||
|
</Segment>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,95 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
|
||||||
|
|
||||||
const AddChannel = () => {
|
|
||||||
const originInputs = {
|
|
||||||
name: '',
|
|
||||||
type: 1,
|
|
||||||
key: '',
|
|
||||||
base_url: '',
|
|
||||||
};
|
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
|
||||||
const { name, type, key } = inputs;
|
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (inputs.name === '' || inputs.key === '') return;
|
|
||||||
if (inputs.base_url.endsWith('/')) {
|
|
||||||
inputs.base_url = inputs.base_url.slice(0, inputs.base_url.length - 1);
|
|
||||||
}
|
|
||||||
const res = await API.post(`/api/channel/`, inputs);
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('渠道创建成功!');
|
|
||||||
setInputs(originInputs);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Segment>
|
|
||||||
<Header as='h3'>创建新的渠道</Header>
|
|
||||||
<Form autoComplete='off'>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Select
|
|
||||||
label='类型'
|
|
||||||
name='type'
|
|
||||||
options={CHANNEL_OPTIONS}
|
|
||||||
value={inputs.type}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
{
|
|
||||||
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='off'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='名称'
|
|
||||||
name='name'
|
|
||||||
placeholder={'请输入名称'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={name}
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='密钥'
|
|
||||||
name='key'
|
|
||||||
placeholder={'请输入密钥'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={key}
|
|
||||||
// type='password'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Button type={'submit'} onClick={submit}>
|
|
||||||
提交
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Segment>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddChannel;
|
|
@@ -1,5 +1,5 @@
|
|||||||
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, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../constants';
|
||||||
@@ -7,14 +7,19 @@ import { CHANNEL_OPTIONS } from '../../constants';
|
|||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const channelId = params.id;
|
const channelId = params.id;
|
||||||
const [loading, setLoading] = useState(true);
|
const isEdit = channelId !== undefined;
|
||||||
const [inputs, setInputs] = useState({
|
const [loading, setLoading] = useState(isEdit);
|
||||||
|
const originInputs = {
|
||||||
name: '',
|
name: '',
|
||||||
key: '',
|
|
||||||
type: 1,
|
type: 1,
|
||||||
|
key: '',
|
||||||
base_url: '',
|
base_url: '',
|
||||||
});
|
other: ''
|
||||||
|
};
|
||||||
|
const [batch, setBatch] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
|
console.log(name, value);
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,17 +35,31 @@ const EditChannel = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadChannel().then();
|
if (isEdit) {
|
||||||
|
loadChannel().then();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (inputs.base_url.endsWith('/')) {
|
if (!isEdit && (inputs.name === '' || inputs.key === '')) return;
|
||||||
inputs.base_url = inputs.base_url.slice(0, inputs.base_url.length - 1);
|
let localInputs = inputs;
|
||||||
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
|
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||||
|
}
|
||||||
|
let res;
|
||||||
|
if (isEdit) {
|
||||||
|
res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
|
||||||
|
} else {
|
||||||
|
res = await API.post(`/api/channel/`, localInputs);
|
||||||
}
|
}
|
||||||
let res = await API.put(`/api/channel/`, { ...inputs, id: parseInt(channelId) });
|
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
showSuccess('渠道更新成功!');
|
if (isEdit) {
|
||||||
|
showSuccess('渠道更新成功!');
|
||||||
|
} else {
|
||||||
|
showSuccess('渠道创建成功!');
|
||||||
|
setInputs(originInputs);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -49,8 +68,8 @@ const EditChannel = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
<Header as='h3'>更新渠道信息</Header>
|
<Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
|
||||||
<Form autoComplete='off'>
|
<Form autoComplete='new-password'>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
label='类型'
|
label='类型'
|
||||||
@@ -60,16 +79,45 @@ const EditChannel = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
{
|
||||||
|
inputs.type === 3 && (
|
||||||
|
<>
|
||||||
|
<Message>
|
||||||
|
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)。
|
||||||
|
</Message>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='AZURE_OPENAI_ENDPOINT'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
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 && (
|
inputs.type === 8 && (
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='Base URL'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入新的自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)
|
||||||
@@ -78,23 +126,44 @@ const EditChannel = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder={'请输入新的名称'}
|
placeholder={'请输入名称'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.name}
|
value={inputs.name}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='密钥'
|
|
||||||
name='key'
|
|
||||||
placeholder={'请输入新的密钥'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={inputs.key}
|
|
||||||
// type='password'
|
|
||||||
autoComplete='off'
|
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
{
|
||||||
|
batch ? <Form.Field>
|
||||||
|
<Form.TextArea
|
||||||
|
label='密钥'
|
||||||
|
name='key'
|
||||||
|
placeholder={'请输入密钥,一行一个'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.key}
|
||||||
|
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field> : <Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='密钥'
|
||||||
|
name='key'
|
||||||
|
placeholder={'请输入密钥'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.key}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isEdit && (
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={batch}
|
||||||
|
label='批量创建'
|
||||||
|
name='batch'
|
||||||
|
onChange={() => setBatch(!batch)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button onClick={submit}>提交</Button>
|
<Button onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Card, Grid, Header, Segment } from 'semantic-ui-react';
|
import { Card, Grid, Header, Segment } from 'semantic-ui-react';
|
||||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||||
|
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||||
|
const [homePageContent, setHomePageContent] = useState('');
|
||||||
|
|
||||||
const displayNotice = async () => {
|
const displayNotice = async () => {
|
||||||
const res = await API.get('/api/notice');
|
const res = await API.get('/api/notice');
|
||||||
@@ -20,6 +23,24 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayHomePageContent = async () => {
|
||||||
|
setHomePageContent(localStorage.getItem('home_page_content') || '');
|
||||||
|
const res = await API.get('/api/home_page_content');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let content = data;
|
||||||
|
if (!data.startsWith('https://')) {
|
||||||
|
content = marked.parse(data);
|
||||||
|
}
|
||||||
|
setHomePageContent(content);
|
||||||
|
localStorage.setItem('home_page_content', content);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
setHomePageContent('加载首页内容失败...');
|
||||||
|
}
|
||||||
|
setHomePageContentLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
const getStartTimeString = () => {
|
const getStartTimeString = () => {
|
||||||
const timestamp = statusState?.status?.start_time;
|
const timestamp = statusState?.status?.start_time;
|
||||||
return timestamp2string(timestamp);
|
return timestamp2string(timestamp);
|
||||||
@@ -27,70 +48,83 @@ const Home = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
displayNotice().then();
|
displayNotice().then();
|
||||||
|
displayHomePageContent().then();
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
{
|
||||||
<Header as='h3'>系统状况</Header>
|
homePageContentLoaded && homePageContent === '' ? <>
|
||||||
<Grid columns={2} stackable>
|
<Segment>
|
||||||
<Grid.Column>
|
<Header as='h3'>系统状况</Header>
|
||||||
<Card fluid>
|
<Grid columns={2} stackable>
|
||||||
<Card.Content>
|
<Grid.Column>
|
||||||
<Card.Header>系统信息</Card.Header>
|
<Card fluid>
|
||||||
<Card.Meta>系统信息总览</Card.Meta>
|
<Card.Content>
|
||||||
<Card.Description>
|
<Card.Header>系统信息</Card.Header>
|
||||||
<p>名称:{statusState?.status?.system_name}</p>
|
<Card.Meta>系统信息总览</Card.Meta>
|
||||||
<p>版本:{statusState?.status?.version}</p>
|
<Card.Description>
|
||||||
<p>
|
<p>名称:{statusState?.status?.system_name}</p>
|
||||||
源码:
|
<p>版本:{statusState?.status?.version}</p>
|
||||||
<a
|
<p>
|
||||||
href='https://github.com/songquanpeng/one-api'
|
源码:
|
||||||
target='_blank'
|
<a
|
||||||
>
|
href='https://github.com/songquanpeng/one-api'
|
||||||
https://github.com/songquanpeng/one-api
|
target='_blank'
|
||||||
</a>
|
>
|
||||||
</p>
|
https://github.com/songquanpeng/one-api
|
||||||
<p>启动时间:{getStartTimeString()}</p>
|
</a>
|
||||||
</Card.Description>
|
</p>
|
||||||
</Card.Content>
|
<p>启动时间:{getStartTimeString()}</p>
|
||||||
</Card>
|
</Card.Description>
|
||||||
</Grid.Column>
|
</Card.Content>
|
||||||
<Grid.Column>
|
</Card>
|
||||||
<Card fluid>
|
</Grid.Column>
|
||||||
<Card.Content>
|
<Grid.Column>
|
||||||
<Card.Header>系统配置</Card.Header>
|
<Card fluid>
|
||||||
<Card.Meta>系统配置总览</Card.Meta>
|
<Card.Content>
|
||||||
<Card.Description>
|
<Card.Header>系统配置</Card.Header>
|
||||||
<p>
|
<Card.Meta>系统配置总览</Card.Meta>
|
||||||
邮箱验证:
|
<Card.Description>
|
||||||
{statusState?.status?.email_verification === true
|
<p>
|
||||||
? '已启用'
|
邮箱验证:
|
||||||
: '未启用'}
|
{statusState?.status?.email_verification === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
GitHub 身份验证:
|
</p>
|
||||||
{statusState?.status?.github_oauth === true
|
<p>
|
||||||
? '已启用'
|
GitHub 身份验证:
|
||||||
: '未启用'}
|
{statusState?.status?.github_oauth === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
微信身份验证:
|
</p>
|
||||||
{statusState?.status?.wechat_login === true
|
<p>
|
||||||
? '已启用'
|
微信身份验证:
|
||||||
: '未启用'}
|
{statusState?.status?.wechat_login === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
Turnstile 用户校验:
|
</p>
|
||||||
{statusState?.status?.turnstile_check === true
|
<p>
|
||||||
? '已启用'
|
Turnstile 用户校验:
|
||||||
: '未启用'}
|
{statusState?.status?.turnstile_check === true
|
||||||
</p>
|
? '已启用'
|
||||||
</Card.Description>
|
: '未启用'}
|
||||||
</Card.Content>
|
</p>
|
||||||
</Card>
|
</Card.Description>
|
||||||
</Grid.Column>
|
</Card.Content>
|
||||||
</Grid>
|
</Card>
|
||||||
</Segment>
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
</Segment>
|
||||||
|
</> : <>
|
||||||
|
{
|
||||||
|
homePageContent.startsWith('https://') ? <iframe
|
||||||
|
src={homePageContent}
|
||||||
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
|
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -73,7 +73,7 @@ const EditRedemption = () => {
|
|||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
<Header as='h3'>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Header>
|
<Header as='h3'>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Header>
|
||||||
<Form autoComplete='off'>
|
<Form autoComplete='new-password'>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
@@ -81,7 +81,7 @@ const EditRedemption = () => {
|
|||||||
placeholder={'请输入名称'}
|
placeholder={'请输入名称'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={name}
|
value={name}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
required={!isEdit}
|
required={!isEdit}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
@@ -92,7 +92,7 @@ const EditRedemption = () => {
|
|||||||
placeholder={'请输入单个兑换码中包含的额度'}
|
placeholder={'请输入单个兑换码中包含的额度'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={quota}
|
value={quota}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
type='number'
|
type='number'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
@@ -105,7 +105,7 @@ const EditRedemption = () => {
|
|||||||
placeholder={'请输入生成数量'}
|
placeholder={'请输入生成数量'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={count}
|
value={count}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
type='number'
|
type='number'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
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, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, isAdmin, showError, showSuccess, timestamp2string } from '../../helpers';
|
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||||
|
|
||||||
const EditToken = () => {
|
const EditToken = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -14,7 +14,6 @@ const EditToken = () => {
|
|||||||
expired_time: -1,
|
expired_time: -1,
|
||||||
unlimited_quota: false
|
unlimited_quota: false
|
||||||
};
|
};
|
||||||
const isAdminUser = isAdmin();
|
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ const EditToken = () => {
|
|||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
<Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header>
|
<Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header>
|
||||||
<Form autoComplete='off'>
|
<Form autoComplete='new-password'>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
@@ -103,29 +102,26 @@ const EditToken = () => {
|
|||||||
placeholder={'请输入名称'}
|
placeholder={'请输入名称'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={name}
|
value={name}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
required={!isEdit}
|
required={!isEdit}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
<Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
|
||||||
isAdminUser && <>
|
<Form.Field>
|
||||||
<Form.Field>
|
<Form.Input
|
||||||
<Form.Input
|
label='额度'
|
||||||
label='额度'
|
name='remain_quota'
|
||||||
name='remain_quota'
|
placeholder={'请输入额度'}
|
||||||
placeholder={'请输入额度'}
|
onChange={handleInputChange}
|
||||||
onChange={handleInputChange}
|
value={remain_quota}
|
||||||
value={remain_quota}
|
autoComplete='new-password'
|
||||||
autoComplete='off'
|
type='number'
|
||||||
type='number'
|
disabled={unlimited_quota}
|
||||||
disabled={unlimited_quota}
|
/>
|
||||||
/>
|
</Form.Field>
|
||||||
</Form.Field>
|
<Button type={'button'} style={{ marginBottom: '14px' }} onClick={() => {
|
||||||
<Button type={'button'} style={{marginBottom: '14px'}} onClick={() => {
|
setUnlimitedQuota();
|
||||||
setUnlimitedQuota();
|
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
||||||
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='过期时间'
|
label='过期时间'
|
||||||
@@ -133,7 +129,7 @@ const EditToken = () => {
|
|||||||
placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'}
|
placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={expired_time}
|
value={expired_time}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
type='datetime-local'
|
type='datetime-local'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
94
web/src/pages/TopUp/index.js
Normal file
94
web/src/pages/TopUp/index.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
|
||||||
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
|
|
||||||
|
const TopUp = () => {
|
||||||
|
const [redemptionCode, setRedemptionCode] = useState('');
|
||||||
|
const [topUpLink, setTopUpLink] = useState('');
|
||||||
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
|
|
||||||
|
const topUp = async () => {
|
||||||
|
if (redemptionCode === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await API.post('/api/user/topup', {
|
||||||
|
key: redemptionCode
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('充值成功!');
|
||||||
|
setUserQuota((quota) => {
|
||||||
|
return quota + data;
|
||||||
|
});
|
||||||
|
setRedemptionCode('');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTopUpLink = () => {
|
||||||
|
if (!topUpLink) {
|
||||||
|
showError('超级管理员未设置充值链接!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(topUpLink, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserQuota = async ()=>{
|
||||||
|
let res = await API.get(`/api/user/self`);
|
||||||
|
const {success, message, data} = res.data;
|
||||||
|
if (success) {
|
||||||
|
setUserQuota(data.quota);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
if (status.top_up_link) {
|
||||||
|
setTopUpLink(status.top_up_link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getUserQuota().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment>
|
||||||
|
<Header as='h3'>充值额度</Header>
|
||||||
|
<Grid columns={2} stackable>
|
||||||
|
<Grid.Column>
|
||||||
|
<Form>
|
||||||
|
<Form.Input
|
||||||
|
placeholder='兑换码'
|
||||||
|
name='redemptionCode'
|
||||||
|
value={redemptionCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRedemptionCode(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button color='green' onClick={openTopUpLink}>
|
||||||
|
获取兑换码
|
||||||
|
</Button>
|
||||||
|
<Button color='yellow' onClick={topUp}>
|
||||||
|
充值
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column>
|
||||||
|
<Statistic.Group widths='one'>
|
||||||
|
<Statistic>
|
||||||
|
<Statistic.Value>{userQuota}</Statistic.Value>
|
||||||
|
<Statistic.Label>剩余额度</Statistic.Label>
|
||||||
|
</Statistic>
|
||||||
|
</Statistic.Group>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default TopUp;
|
@@ -60,7 +60,7 @@ const EditUser = () => {
|
|||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
<Header as='h3'>更新用户信息</Header>
|
<Header as='h3'>更新用户信息</Header>
|
||||||
<Form autoComplete='off'>
|
<Form autoComplete='new-password'>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='用户名'
|
label='用户名'
|
||||||
@@ -68,7 +68,7 @@ const EditUser = () => {
|
|||||||
placeholder={'请输入新的用户名'}
|
placeholder={'请输入新的用户名'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={username}
|
value={username}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
@@ -79,7 +79,7 @@ const EditUser = () => {
|
|||||||
placeholder={'请输入新的密码'}
|
placeholder={'请输入新的密码'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={password}
|
value={password}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
@@ -89,7 +89,7 @@ const EditUser = () => {
|
|||||||
placeholder={'请输入新的显示名称'}
|
placeholder={'请输入新的显示名称'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={display_name}
|
value={display_name}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
@@ -97,7 +97,7 @@ const EditUser = () => {
|
|||||||
label='已绑定的 GitHub 账户'
|
label='已绑定的 GitHub 账户'
|
||||||
name='github_id'
|
name='github_id'
|
||||||
value={github_id}
|
value={github_id}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@@ -107,7 +107,7 @@ const EditUser = () => {
|
|||||||
label='已绑定的微信账户'
|
label='已绑定的微信账户'
|
||||||
name='wechat_id'
|
name='wechat_id'
|
||||||
value={wechat_id}
|
value={wechat_id}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@@ -117,7 +117,7 @@ const EditUser = () => {
|
|||||||
label='已绑定的邮箱账户'
|
label='已绑定的邮箱账户'
|
||||||
name='email'
|
name='email'
|
||||||
value={email}
|
value={email}
|
||||||
autoComplete='off'
|
autoComplete='new-password'
|
||||||
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user