mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-24 02:13:42 +08:00
Compare commits
59 Commits
v0.4.10-al
...
0.0.4
Author | SHA1 | Date | |
---|---|---|---|
|
101c3d83cd | ||
|
6a4bfe2ee4 | ||
|
2c62053933 | ||
|
442673a4be | ||
|
bb17e09818 | ||
|
80288492f9 | ||
|
d89e13dac3 | ||
|
0c70181f81 | ||
|
3893d4bdf1 | ||
|
d1b6f492b6 | ||
|
b9f6461dd4 | ||
|
0a39521a3d | ||
|
4aa9f97017 | ||
|
ab041d94e2 | ||
|
18655fed08 | ||
|
bba49c959e | ||
|
22f4419b85 | ||
|
c7c3b9d326 | ||
|
0d6163a9fb | ||
|
7419ca511e | ||
|
ccd7a99b68 | ||
|
759423d69e | ||
|
04110d4b01 | ||
|
d6b2131720 | ||
|
438daea433 | ||
|
c134604cee | ||
|
929e43ef81 | ||
|
dce8bbe1ca | ||
|
c6c070b8bd | ||
|
13b3bfee2a | ||
|
2b42b4f364 | ||
|
f37e41eb1d | ||
|
c144c64fff | ||
|
8956e2fd60 | ||
|
30187cebe8 | ||
|
00d3a78bef | ||
|
a588241515 | ||
|
546f9e1db5 | ||
|
4908a9eddc | ||
|
15cdaee762 | ||
|
395ee121ed | ||
|
4cea6279ab | ||
|
f50683e75f | ||
|
bc2f48b1f2 | ||
|
889af8b2db | ||
|
4eea096654 | ||
|
4ab3211c0e | ||
|
3da119efba | ||
|
dccd66b852 | ||
|
2fcd6852e0 | ||
|
9b4d1964d4 | ||
|
806bf8241c | ||
|
ce93c9b6b2 | ||
|
4ec4289565 | ||
|
3dc5a0f91d | ||
|
80a846673a | ||
|
26c6719ea3 | ||
|
c87e05bfc2 | ||
|
e6938bd236 |
2
.github/workflows/docker-image-amd64-en.yml
vendored
2
.github/workflows/docker-image-amd64-en.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
justsong/one-api-en
|
ckt1031/one-api-en
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
|
2
.github/workflows/docker-image-amd64.yml
vendored
2
.github/workflows/docker-image-amd64.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
justsong/one-api
|
ckt1031/one-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
|
2
.github/workflows/docker-image-arm64.yml
vendored
2
.github/workflows/docker-image-arm64.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
justsong/one-api
|
ckt1031/one-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
|
59
.github/workflows/linux-release-en.yml
vendored
Normal file
59
.github/workflows/linux-release-en.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Linux Release (English)
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
- "!*-alpha*"
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Translate
|
||||||
|
run: |
|
||||||
|
python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Build Frontend
|
||||||
|
env:
|
||||||
|
CI: ""
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
|
cd ..
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ">=1.18.0"
|
||||||
|
- name: Build Backend (amd64)
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-en
|
||||||
|
|
||||||
|
- name: Build Backend (arm64)
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gcc-aarch64-linux-gnu
|
||||||
|
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-arm64-en
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
one-api-en
|
||||||
|
one-api-arm64-en
|
||||||
|
draft: true
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# One API
|
# One API
|
||||||
|
|
||||||
_✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
|
_✨ Access all LLM through the standard OpenAI API format, easy to deploy & use ✨_
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
49
README.md
49
README.md
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
# One API
|
# One API
|
||||||
|
|
||||||
_✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用✨_
|
_✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ✨_
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -58,12 +58,13 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道:
|
1. 支持多种大模型:
|
||||||
+ [x] OpenAI 官方通道(支持配置镜像)
|
+ [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
|
||||||
+ [x] **Azure OpenAI API**
|
|
||||||
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
||||||
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
||||||
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||||
|
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
|
||||||
|
2. 支持配置镜像以及众多第三方代理服务:
|
||||||
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
@@ -71,32 +72,30 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
||||||
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
||||||
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
||||||
2. 支持通过**负载均衡**的方式访问多个渠道。
|
3. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
4. 支持**多机部署**,[详见此处](#多机部署)。
|
5. 支持**多机部署**,[详见此处](#多机部署)。
|
||||||
5. 支持**令牌管理**,设置令牌的过期时间和额度。
|
6. 支持**令牌管理**,设置令牌的过期时间和额度。
|
||||||
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
|
7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
|
||||||
7. 支持**通道管理**,批量创建通道。
|
8. 支持**通道管理**,批量创建通道。
|
||||||
8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
|
9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
|
||||||
9. 支持渠道**设置模型列表**。
|
10. 支持渠道**设置模型列表**。
|
||||||
10. 支持**查看额度明细**。
|
11. 支持**查看额度明细**。
|
||||||
11. 支持**用户邀请奖励**。
|
12. 支持**用户邀请奖励**。
|
||||||
12. 支持以美元为单位显示额度。
|
13. 支持以美元为单位显示额度。
|
||||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
|
14. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
14. 支持模型映射,重定向用户的请求模型。
|
15. 支持模型映射,重定向用户的请求模型。
|
||||||
15. 支持失败自动重试。
|
16. 支持失败自动重试。
|
||||||
16. 支持绘图接口。
|
17. 支持绘图接口。
|
||||||
17. 支持丰富的**自定义**设置,
|
18. 支持丰富的**自定义**设置,
|
||||||
1. 支持自定义系统名称,logo 以及页脚。
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
18. 支持通过系统访问令牌访问管理 API。
|
19. 支持通过系统访问令牌访问管理 API。
|
||||||
19. 支持 Cloudflare Turnstile 用户校验。
|
20. 支持 Cloudflare Turnstile 用户校验。
|
||||||
20. 支持用户管理,支持**多种用户登录注册方式**:
|
21. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [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))。
|
||||||
21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。
|
|
||||||
22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
|
@@ -22,6 +22,7 @@ var DisplayInCurrencyEnabled = true
|
|||||||
var DisplayTokenStatEnabled = true
|
var DisplayTokenStatEnabled = true
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
|
var UsingPostgreSQL = false
|
||||||
|
|
||||||
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ var PasswordLoginEnabled = true
|
|||||||
var PasswordRegisterEnabled = true
|
var PasswordRegisterEnabled = true
|
||||||
var EmailVerificationEnabled = false
|
var EmailVerificationEnabled = false
|
||||||
var GitHubOAuthEnabled = false
|
var GitHubOAuthEnabled = false
|
||||||
|
var DiscordOAuthEnabled = false
|
||||||
var WeChatAuthEnabled = false
|
var WeChatAuthEnabled = false
|
||||||
|
var GoogleOAuthEnabled = false
|
||||||
var TurnstileCheckEnabled = false
|
var TurnstileCheckEnabled = false
|
||||||
var RegisterEnabled = true
|
var RegisterEnabled = true
|
||||||
|
|
||||||
@@ -53,10 +56,16 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var DiscordClientId = ""
|
||||||
|
var DiscordClientSecret = ""
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
|
||||||
|
var GoogleClientId = ""
|
||||||
|
var GoogleClientSecret = ""
|
||||||
|
|
||||||
var TurnstileSiteKey = ""
|
var TurnstileSiteKey = ""
|
||||||
var TurnstileSecretKey = ""
|
var TurnstileSecretKey = ""
|
||||||
|
|
||||||
@@ -77,6 +86,8 @@ var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
|||||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||||
var RequestInterval = time.Duration(requestInterval) * time.Second
|
var RequestInterval = time.Duration(requestInterval) * time.Second
|
||||||
|
|
||||||
|
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
@@ -136,6 +147,16 @@ const (
|
|||||||
ChannelStatusDisabled = 2 // also don't use 0
|
ChannelStatusDisabled = 2 // also don't use 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChannelAllowNonStreamEnabled = 1
|
||||||
|
ChannelAllowNonStreamDisabled = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChannelAllowStreamEnabled = 1
|
||||||
|
ChannelAllowStreamDisabled = 2
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelTypeUnknown = 0
|
ChannelTypeUnknown = 0
|
||||||
ChannelTypeOpenAI = 1
|
ChannelTypeOpenAI = 1
|
||||||
@@ -153,6 +174,7 @@ const (
|
|||||||
ChannelTypeAIGC2D = 13
|
ChannelTypeAIGC2D = 13
|
||||||
ChannelTypeAnthropic = 14
|
ChannelTypeAnthropic = 14
|
||||||
ChannelTypeBaidu = 15
|
ChannelTypeBaidu = 15
|
||||||
|
ChannelTypeZhipu = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
@@ -172,4 +194,5 @@ var ChannelBaseURLs = []string{
|
|||||||
"https://api.aigc2d.com", // 13
|
"https://api.aigc2d.com", // 13
|
||||||
"https://api.anthropic.com", // 14
|
"https://api.anthropic.com", // 14
|
||||||
"https://aip.baidubce.com", // 15
|
"https://aip.baidubce.com", // 15
|
||||||
|
"https://open.bigmodel.cn", // 16
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import "encoding/json"
|
|||||||
// https://openai.com/pricing
|
// https://openai.com/pricing
|
||||||
// TODO: when a new api is enabled, check the pricing here
|
// TODO: when a new api is enabled, check the pricing here
|
||||||
// 1 === $0.002 / 1K tokens
|
// 1 === $0.002 / 1K tokens
|
||||||
|
// 1 === ¥0.014 / 1k tokens
|
||||||
var ModelRatio = map[string]float64{
|
var ModelRatio = map[string]float64{
|
||||||
"gpt-4": 15,
|
"gpt-4": 15,
|
||||||
"gpt-4-0314": 15,
|
"gpt-4-0314": 15,
|
||||||
@@ -39,9 +40,12 @@ var ModelRatio = map[string]float64{
|
|||||||
"dall-e": 8,
|
"dall-e": 8,
|
||||||
"claude-instant-1": 0.75,
|
"claude-instant-1": 0.75,
|
||||||
"claude-2": 30,
|
"claude-2": 30,
|
||||||
"ERNIE-Bot": 1, // 0.012元/千tokens
|
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
|
||||||
"ERNIE-Bot-turbo": 0.67, // 0.008元/千tokens
|
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
|
||||||
"PaLM-2": 1,
|
"PaLM-2": 1,
|
||||||
|
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||||
|
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
||||||
|
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModelRatio2JSONString() string {
|
func ModelRatio2JSONString() string {
|
||||||
|
@@ -11,9 +11,11 @@ func GetSubscription(c *gin.Context) {
|
|||||||
var usedQuota int
|
var usedQuota int
|
||||||
var err error
|
var err error
|
||||||
var token *model.Token
|
var token *model.Token
|
||||||
|
var expiredTime int64
|
||||||
if common.DisplayTokenStatEnabled {
|
if common.DisplayTokenStatEnabled {
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
token, err = model.GetTokenById(tokenId)
|
token, err = model.GetTokenById(tokenId)
|
||||||
|
expiredTime = token.ExpiredTime
|
||||||
remainQuota = token.RemainQuota
|
remainQuota = token.RemainQuota
|
||||||
usedQuota = token.UsedQuota
|
usedQuota = token.UsedQuota
|
||||||
} else {
|
} else {
|
||||||
@@ -21,6 +23,9 @@ func GetSubscription(c *gin.Context) {
|
|||||||
remainQuota, err = model.GetUserQuota(userId)
|
remainQuota, err = model.GetUserQuota(userId)
|
||||||
usedQuota, err = model.GetUserUsedQuota(userId)
|
usedQuota, err = model.GetUserUsedQuota(userId)
|
||||||
}
|
}
|
||||||
|
if expiredTime <= 0 {
|
||||||
|
expiredTime = 0
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
openAIError := OpenAIError{
|
openAIError := OpenAIError{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -45,6 +50,7 @@ func GetSubscription(c *gin.Context) {
|
|||||||
SoftLimitUSD: amount,
|
SoftLimitUSD: amount,
|
||||||
HardLimitUSD: amount,
|
HardLimitUSD: amount,
|
||||||
SystemHardLimitUSD: amount,
|
SystemHardLimitUSD: amount,
|
||||||
|
AccessUntil: expiredTime,
|
||||||
}
|
}
|
||||||
c.JSON(200, subscription)
|
c.JSON(200, subscription)
|
||||||
return
|
return
|
||||||
|
@@ -22,6 +22,7 @@ type OpenAISubscriptionResponse struct {
|
|||||||
SoftLimitUSD float64 `json:"soft_limit_usd"`
|
SoftLimitUSD float64 `json:"soft_limit_usd"`
|
||||||
HardLimitUSD float64 `json:"hard_limit_usd"`
|
HardLimitUSD float64 `json:"hard_limit_usd"`
|
||||||
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
|
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
|
||||||
|
AccessUntil int64 `json:"access_until"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIUsageDailyCost struct {
|
type OpenAIUsageDailyCost struct {
|
||||||
@@ -84,7 +85,6 @@ func GetAuthHeader(token string) http.Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -92,10 +92,13 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
|
|||||||
for k := range headers {
|
for k := range headers {
|
||||||
req.Header.Add(k, headers.Get(k))
|
req.Header.Add(k, headers.Get(k))
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||||
|
}
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -1,21 +1,32 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) {
|
func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) {
|
||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
|
case common.ChannelTypePaLM:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeAnthropic:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeBaidu:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeZhipu:
|
||||||
|
return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
|
||||||
case common.ChannelTypeAzure:
|
case common.ChannelTypeAzure:
|
||||||
request.Model = "gpt-35-turbo"
|
request.Model = "gpt-35-turbo"
|
||||||
default:
|
default:
|
||||||
@@ -45,27 +56,91 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
|
|||||||
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
var response TextResponse
|
if resp.StatusCode != http.StatusOK {
|
||||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
return errors.New(fmt.Sprintf("status code %d", resp.StatusCode)), nil
|
||||||
if err != nil {
|
|
||||||
return err, nil
|
|
||||||
}
|
}
|
||||||
if response.Usage.CompletionTokens == 0 {
|
|
||||||
return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
|
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
|
|
||||||
|
if channel.AllowStreaming == common.ChannelAllowStreamEnabled && isStream {
|
||||||
|
responseText := ""
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Text()
|
||||||
|
if len(data) < 6 { // ignore blank line or wrong format
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// ChatGPT Next Web
|
||||||
|
if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
|
||||||
|
// Remove event: event in the front or back
|
||||||
|
data = strings.TrimPrefix(data, "event: event")
|
||||||
|
data = strings.TrimSuffix(data, "event: event")
|
||||||
|
// Remove everything, only keep `data: {...}` <--- this is the json
|
||||||
|
// Find the start and end indices of `data: {...}` substring
|
||||||
|
startIndex := strings.Index(data, "data:")
|
||||||
|
endIndex := strings.LastIndex(data, "}")
|
||||||
|
|
||||||
|
// If both indices are found and end index is greater than start index
|
||||||
|
if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
|
||||||
|
// Extract the `data: {...}` substring
|
||||||
|
data = data[startIndex : endIndex+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(data, "data:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data = data[6:]
|
||||||
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
|
var streamResponse ChatCompletionsStreamResponse
|
||||||
|
err := json.Unmarshal([]byte(data), &streamResponse)
|
||||||
|
if err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
for _, choice := range streamResponse.Choices {
|
||||||
|
responseText += choice.Delta.Content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseText == "" {
|
||||||
|
return errors.New("Empty response"), nil
|
||||||
|
}
|
||||||
|
} else if channel.AllowNonStreaming == common.ChannelAllowNonStreamEnabled {
|
||||||
|
var response TextResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
if response.Usage.CompletionTokens == 0 {
|
||||||
|
return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTestRequest() *ChatRequest {
|
func buildTestRequest(stream bool) *ChatRequest {
|
||||||
testRequest := &ChatRequest{
|
testRequest := &ChatRequest{
|
||||||
Model: "", // this will be set later
|
Model: "", // this will be set later
|
||||||
MaxTokens: 1,
|
MaxTokens: 1,
|
||||||
|
Stream: stream,
|
||||||
}
|
}
|
||||||
testMessage := Message{
|
testMessage := Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
@@ -92,7 +167,7 @@ func TestChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
testRequest := buildTestRequest()
|
testRequest := buildTestRequest(channel.AllowStreaming == common.ChannelAllowStreamEnabled)
|
||||||
tik := time.Now()
|
tik := time.Now()
|
||||||
err, _ = testChannel(channel, *testRequest)
|
err, _ = testChannel(channel, *testRequest)
|
||||||
tok := time.Now()
|
tok := time.Now()
|
||||||
@@ -147,7 +222,6 @@ func testAllChannels(notify bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
testRequest := buildTestRequest()
|
|
||||||
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
||||||
if disableThreshold == 0 {
|
if disableThreshold == 0 {
|
||||||
disableThreshold = 10000000 // a impossible value
|
disableThreshold = 10000000 // a impossible value
|
||||||
@@ -158,6 +232,7 @@ func testAllChannels(notify bool) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tik := time.Now()
|
tik := time.Now()
|
||||||
|
testRequest := buildTestRequest(channel.AllowStreaming == common.ChannelAllowStreamEnabled)
|
||||||
err, openaiErr := testChannel(channel, *testRequest)
|
err, openaiErr := testChannel(channel, *testRequest)
|
||||||
tok := time.Now()
|
tok := time.Now()
|
||||||
milliseconds := tok.Sub(tik).Milliseconds()
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
|
223
controller/discord.go
Normal file
223
controller/discord.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscordOAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordUser struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDiscordUserInfoByCode(codeFromURLParamaters string, host string) (*DiscordUser, error) {
|
||||||
|
if codeFromURLParamaters == "" {
|
||||||
|
return nil, errors.New("无效参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestClient := &http.Client{}
|
||||||
|
|
||||||
|
accessTokenBody := bytes.NewBuffer([]byte(fmt.Sprintf(
|
||||||
|
"client_id=%s&client_secret=%s&grant_type=authorization_code&redirect_uri=%s/oauth/discord&code=%s&scope=identify",
|
||||||
|
common.DiscordClientId, common.DiscordClientSecret, common.ServerAddress, codeFromURLParamaters,
|
||||||
|
)))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST",
|
||||||
|
"https://discordapp.com/api/oauth2/token",
|
||||||
|
accessTokenBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/x-www-form-urlencoded"},
|
||||||
|
"Accept": []string{"application/json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("访问令牌无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordOAuthResponse DiscordOAuthResponse
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&discordOAuthResponse)
|
||||||
|
|
||||||
|
accessToken := fmt.Sprintf("Bearer %s", discordOAuthResponse.AccessToken)
|
||||||
|
|
||||||
|
// Get User Info
|
||||||
|
req, _ = http.NewRequest("GET", "https://discord.com/api/users/@me", nil)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
"Authorization": []string{accessToken},
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
resp, err = RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("Discord 用户信息无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordUser DiscordUser
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&discordUser)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if discordUser.Id == "" {
|
||||||
|
return nil, errors.New("返回值无效,用户字段为空,请稍后再试!")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return &discordUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscordOAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
DiscordBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.DiscordOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 Discord 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code, c.Request.Host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.Id,
|
||||||
|
}
|
||||||
|
if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
|
||||||
|
err := user.FillUserByDiscordId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if common.RegisterEnabled {
|
||||||
|
user.Username = "discord_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
if discordUser.Username != "" {
|
||||||
|
user.DisplayName = discordUser.Username
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "Discord User"
|
||||||
|
}
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
|
if err := user.Insert(0); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscordBind(c *gin.Context) {
|
||||||
|
if !common.DiscordOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 Discord 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code, c.Request.Host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.Id,
|
||||||
|
}
|
||||||
|
if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 Discord 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
// id := c.GetInt("id") // critical bug!
|
||||||
|
user.Id = id.(int)
|
||||||
|
err = user.FillUserById()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.DiscordId = discordUser.Id
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
226
controller/google.go
Normal file
226
controller/google.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoogleAccessTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoogleUser struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGoogleUserInfoByCode(codeFromURLParamaters string, host string) (*GoogleUser, error) {
|
||||||
|
if codeFromURLParamaters == "" {
|
||||||
|
return nil, errors.New("无效参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestClient := &http.Client{}
|
||||||
|
|
||||||
|
accessTokenBody := bytes.NewBuffer([]byte(fmt.Sprintf(
|
||||||
|
"code=%s&client_id=%s&client_secret=%s&redirect_uri=%s/oauth/google&grant_type=authorization_code",
|
||||||
|
codeFromURLParamaters, common.GoogleClientId, common.GoogleClientSecret, common.ServerAddress,
|
||||||
|
)))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST",
|
||||||
|
"https://oauth2.googleapis.com/token",
|
||||||
|
accessTokenBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/x-www-form-urlencoded"},
|
||||||
|
"Accept": []string{"application/json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("访问令牌无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var googleTokenResponse GoogleAccessTokenResponse
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&googleTokenResponse)
|
||||||
|
|
||||||
|
accessToken := "Bearer " + googleTokenResponse.AccessToken
|
||||||
|
|
||||||
|
// Get User Info
|
||||||
|
req, _ = http.NewRequest("GET", "https://www.googleapis.com/oauth2/v3/userinfo", nil)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
"Authorization": []string{accessToken},
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
resp, err = RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("Google 用户信息无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var googleUser GoogleUser
|
||||||
|
|
||||||
|
// Parse json to googleUser
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&googleUser)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if googleUser.Sub == "" {
|
||||||
|
return nil, errors.New("返回值无效,用户字段为空,请稍后再试!")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return &googleUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoogleOAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
GoogleBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.GoogleOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 Google 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
|
googleUser, err := getGoogleUserInfoByCode(code, c.Request.Host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
GoogleId: googleUser.Sub,
|
||||||
|
}
|
||||||
|
if model.IsGoogleIdAlreadyTaken(user.GoogleId) {
|
||||||
|
err := user.FillUserByGoogleId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if common.RegisterEnabled {
|
||||||
|
user.Username = "google_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
if googleUser.Name != "" {
|
||||||
|
user.DisplayName = googleUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "Google User"
|
||||||
|
}
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
|
if err := user.Insert(0); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoogleBind(c *gin.Context) {
|
||||||
|
if !common.GoogleOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 Google 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
|
||||||
|
googleUser, err := getGoogleUserInfoByCode(code, c.Request.Host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
GoogleId: googleUser.Sub,
|
||||||
|
}
|
||||||
|
if model.IsGoogleIdAlreadyTaken(user.GoogleId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 Google 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
// id := c.GetInt("id") // critical bug!
|
||||||
|
user.Id = id.(int)
|
||||||
|
err = user.FillUserById()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.GoogleId = googleUser.Sub
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
@@ -3,10 +3,11 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetStatus(c *gin.Context) {
|
func GetStatus(c *gin.Context) {
|
||||||
@@ -19,6 +20,10 @@ func GetStatus(c *gin.Context) {
|
|||||||
"email_verification": common.EmailVerificationEnabled,
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"discord_oauth": common.DiscordOAuthEnabled,
|
||||||
|
"discord_client_id": common.DiscordClientId,
|
||||||
|
"google_oauth": common.GoogleOAuthEnabled,
|
||||||
|
"google_client_id": common.GoogleClientId,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
"logo": common.Logo,
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
@@ -127,8 +132,9 @@ func SendPasswordResetEmail(c *gin.Context) {
|
|||||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
||||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
|
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||||
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := common.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
@@ -252,24 +252,6 @@ func init() {
|
|||||||
Root: "code-davinci-edit-001",
|
Root: "code-davinci-edit-001",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Id: "ChatGLM",
|
|
||||||
Object: "model",
|
|
||||||
Created: 1677649963,
|
|
||||||
OwnedBy: "thudm",
|
|
||||||
Permission: permission,
|
|
||||||
Root: "ChatGLM",
|
|
||||||
Parent: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: "ChatGLM2",
|
|
||||||
Object: "model",
|
|
||||||
Created: 1677649963,
|
|
||||||
OwnedBy: "thudm",
|
|
||||||
Permission: permission,
|
|
||||||
Root: "ChatGLM2",
|
|
||||||
Parent: nil,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Id: "claude-instant-1",
|
Id: "claude-instant-1",
|
||||||
Object: "model",
|
Object: "model",
|
||||||
@@ -315,6 +297,33 @@ func init() {
|
|||||||
Root: "PaLM-2",
|
Root: "PaLM-2",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "chatglm_pro",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "zhipu",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "chatglm_pro",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "chatglm_std",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "zhipu",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "chatglm_std",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "chatglm_lite",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "zhipu",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "chatglm_lite",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]OpenAIModels)
|
openAIModelsMap = make(map[string]OpenAIModels)
|
||||||
for _, model := range openAIModels {
|
for _, model := range openAIModels {
|
||||||
|
@@ -2,11 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOptions(c *gin.Context) {
|
func GetOptions(c *gin.Context) {
|
||||||
@@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "DiscordOAuthEnabled":
|
||||||
|
if option.Value == "true" && common.DiscordClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
if option.Value == "true" && common.WeChatServerAddress == "" {
|
if option.Value == "true" && common.WeChatServerAddress == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -57,6 +66,14 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "GoogleOAuthEnabled":
|
||||||
|
if option.Value == "true" && common.GoogleClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 Google OAuth,请先填入 Google Client ID 以及 Google Client Secret!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
case "TurnstileCheckEnabled":
|
case "TurnstileCheckEnabled":
|
||||||
if option.Value == "true" && common.TurnstileSiteKey == "" {
|
if option.Value == "true" && common.TurnstileSiteKey == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
@@ -57,10 +57,21 @@ type BaiduChatStreamResponse struct {
|
|||||||
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
||||||
messages := make([]BaiduMessage, 0, len(request.Messages))
|
messages := make([]BaiduMessage, 0, len(request.Messages))
|
||||||
for _, message := range request.Messages {
|
for _, message := range request.Messages {
|
||||||
messages = append(messages, BaiduMessage{
|
if message.Role == "system" {
|
||||||
Role: message.Role,
|
messages = append(messages, BaiduMessage{
|
||||||
Content: message.Content,
|
Role: "user",
|
||||||
})
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
messages = append(messages, BaiduMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Okay",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messages = append(messages, BaiduMessage{
|
||||||
|
Role: message.Role,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &BaiduChatRequest{
|
return &BaiduChatRequest{
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
|
@@ -69,11 +69,11 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
|
|||||||
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
|
||||||
} else if message.Role == "assistant" {
|
} else if message.Role == "assistant" {
|
||||||
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
||||||
} else {
|
} else if message.Role == "system" {
|
||||||
// ignore other roles
|
prompt += fmt.Sprintf("\n\nSystem: %s", message.Content)
|
||||||
}
|
}
|
||||||
prompt += "\n\nAssistant:"
|
|
||||||
}
|
}
|
||||||
|
prompt += "\n\nAssistant:"
|
||||||
claudeRequest.Prompt = prompt
|
claudeRequest.Prompt = prompt
|
||||||
return &claudeRequest
|
return &claudeRequest
|
||||||
}
|
}
|
||||||
|
@@ -109,8 +109,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
|
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,12 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) {
|
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) {
|
||||||
@@ -34,6 +35,25 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
if len(data) < 6 { // ignore blank line or wrong format
|
if len(data) < 6 { // ignore blank line or wrong format
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// ChatGPT Next Web
|
||||||
|
if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
|
||||||
|
// Remove event: event in the front or back
|
||||||
|
data = strings.TrimPrefix(data, "event: event")
|
||||||
|
data = strings.TrimSuffix(data, "event: event")
|
||||||
|
// Remove everything, only keep `data: {...}` <--- this is the json
|
||||||
|
// Find the start and end indices of `data: {...}` substring
|
||||||
|
startIndex := strings.Index(data, "data:")
|
||||||
|
endIndex := strings.LastIndex(data, "}")
|
||||||
|
|
||||||
|
// If both indices are found and end index is greater than start index
|
||||||
|
if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
|
||||||
|
// Extract the `data: {...}` substring
|
||||||
|
data = data[startIndex : endIndex+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(data, "data:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dataChan <- data
|
dataChan <- data
|
||||||
data = data[6:]
|
data = data[6:]
|
||||||
if !strings.HasPrefix(data, "[DONE]") {
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
@@ -115,7 +135,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
|
|||||||
}
|
}
|
||||||
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
// 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.
|
// 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.
|
// So the httpClient will be confused by the response.
|
||||||
// For example, Postman will report error, and we cannot check the response at all.
|
// For example, Postman will report error, and we cannot check the response at all.
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
c.Writer.Header().Set(k, v[0])
|
c.Writer.Header().Set(k, v[0])
|
||||||
|
@@ -131,6 +131,9 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSta
|
|||||||
fullTextResponse := streamResponsePaLM2OpenAI(&palmResponse)
|
fullTextResponse := streamResponsePaLM2OpenAI(&palmResponse)
|
||||||
fullTextResponse.Id = responseId
|
fullTextResponse.Id = responseId
|
||||||
fullTextResponse.Created = createdTime
|
fullTextResponse.Created = createdTime
|
||||||
|
if len(palmResponse.Candidates) > 0 {
|
||||||
|
responseText = palmResponse.Candidates[0].Content
|
||||||
|
}
|
||||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error marshalling stream response: " + err.Error())
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
@@ -19,8 +19,15 @@ const (
|
|||||||
APITypeClaude
|
APITypeClaude
|
||||||
APITypePaLM
|
APITypePaLM
|
||||||
APITypeBaidu
|
APITypeBaidu
|
||||||
|
APITypeZhipu
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
httpClient = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||||
channelType := c.GetInt("channel")
|
channelType := c.GetInt("channel")
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
@@ -78,12 +85,15 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiType := APITypeOpenAI
|
apiType := APITypeOpenAI
|
||||||
if strings.HasPrefix(textRequest.Model, "claude") {
|
switch channelType {
|
||||||
|
case common.ChannelTypeAnthropic:
|
||||||
apiType = APITypeClaude
|
apiType = APITypeClaude
|
||||||
} else if strings.HasPrefix(textRequest.Model, "ERNIE") {
|
case common.ChannelTypeBaidu:
|
||||||
apiType = APITypeBaidu
|
apiType = APITypeBaidu
|
||||||
} else if strings.HasPrefix(textRequest.Model, "PaLM") {
|
case common.ChannelTypePaLM:
|
||||||
apiType = APITypePaLM
|
apiType = APITypePaLM
|
||||||
|
case common.ChannelTypeZhipu:
|
||||||
|
apiType = APITypeZhipu
|
||||||
}
|
}
|
||||||
baseURL := common.ChannelBaseURLs[channelType]
|
baseURL := common.ChannelBaseURLs[channelType]
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
@@ -131,9 +141,18 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
fullRequestURL += "?access_token=" + apiKey // TODO: access token expire in 30 days
|
fullRequestURL += "?access_token=" + apiKey // TODO: access token expire in 30 days
|
||||||
case APITypePaLM:
|
case APITypePaLM:
|
||||||
fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
|
fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
|
||||||
|
if baseURL != "" {
|
||||||
|
fullRequestURL = fmt.Sprintf("%s/v1beta2/models/chat-bison-001:generateMessage", baseURL)
|
||||||
|
}
|
||||||
apiKey := c.Request.Header.Get("Authorization")
|
apiKey := c.Request.Header.Get("Authorization")
|
||||||
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
||||||
fullRequestURL += "?key=" + apiKey
|
fullRequestURL += "?key=" + apiKey
|
||||||
|
case APITypeZhipu:
|
||||||
|
method := "invoke"
|
||||||
|
if textRequest.Stream {
|
||||||
|
method = "sse-invoke"
|
||||||
|
}
|
||||||
|
fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method)
|
||||||
}
|
}
|
||||||
var promptTokens int
|
var promptTokens int
|
||||||
var completionTokens int
|
var completionTokens int
|
||||||
@@ -200,6 +219,13 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
requestBody = bytes.NewBuffer(jsonStr)
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
|
case APITypeZhipu:
|
||||||
|
zhipuRequest := requestOpenAI2Zhipu(textRequest)
|
||||||
|
jsonStr, err := json.Marshal(zhipuRequest)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,12 +247,14 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
anthropicVersion = "2023-06-01"
|
anthropicVersion = "2023-06-01"
|
||||||
}
|
}
|
||||||
req.Header.Set("anthropic-version", anthropicVersion)
|
req.Header.Set("anthropic-version", anthropicVersion)
|
||||||
|
case APITypeZhipu:
|
||||||
|
token := getZhipuToken(apiKey)
|
||||||
|
req.Header.Set("Authorization", token)
|
||||||
}
|
}
|
||||||
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{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@@ -238,6 +266,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errorWrapper(nil, "bad_status_code", resp.StatusCode)
|
||||||
|
}
|
||||||
var textResponse TextResponse
|
var textResponse TextResponse
|
||||||
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
var streamResponseText string
|
var streamResponseText string
|
||||||
@@ -252,11 +283,15 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
||||||
completionRatio = 2
|
completionRatio = 2
|
||||||
}
|
}
|
||||||
if isStream && apiType != APITypeBaidu {
|
if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu {
|
||||||
completionTokens = countTokenText(streamResponseText, textRequest.Model)
|
completionTokens = countTokenText(streamResponseText, textRequest.Model)
|
||||||
} else {
|
} else {
|
||||||
promptTokens = textResponse.Usage.PromptTokens
|
promptTokens = textResponse.Usage.PromptTokens
|
||||||
completionTokens = textResponse.Usage.CompletionTokens
|
completionTokens = textResponse.Usage.CompletionTokens
|
||||||
|
if apiType == APITypeZhipu {
|
||||||
|
// zhipu's API does not return prompt tokens & completion tokens
|
||||||
|
promptTokens = textResponse.Usage.TotalTokens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
||||||
quota = int(float64(quota) * ratio)
|
quota = int(float64(quota) * ratio)
|
||||||
@@ -302,7 +337,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
textResponse.Usage = *usage
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case APITypeClaude:
|
case APITypeClaude:
|
||||||
@@ -318,7 +355,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
textResponse.Usage = *usage
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case APITypeBaidu:
|
case APITypeBaidu:
|
||||||
@@ -327,14 +366,18 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
textResponse.Usage = *usage
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := baiduHandler(c, resp)
|
err, usage := baiduHandler(c, resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
textResponse.Usage = *usage
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case APITypePaLM:
|
case APITypePaLM:
|
||||||
@@ -350,7 +393,29 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
textResponse.Usage = *usage
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case APITypeZhipu:
|
||||||
|
if isStream {
|
||||||
|
err, usage := zhipuStreamHandler(c, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
err, usage := zhipuHandler(c, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
301
controller/relay-zhipu.go
Normal file
301
controller/relay-zhipu.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://open.bigmodel.cn/doc/api#chatglm_std
|
||||||
|
// chatglm_std, chatglm_lite
|
||||||
|
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke
|
||||||
|
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke
|
||||||
|
|
||||||
|
type ZhipuMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZhipuRequest struct {
|
||||||
|
Prompt []ZhipuMessage `json:"prompt"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
|
RequestId string `json:"request_id,omitempty"`
|
||||||
|
Incremental bool `json:"incremental,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZhipuResponseData struct {
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
TaskStatus string `json:"task_status"`
|
||||||
|
Choices []ZhipuMessage `json:"choices"`
|
||||||
|
Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZhipuResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data ZhipuResponseData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZhipuStreamMetaResponse struct {
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
TaskStatus string `json:"task_status"`
|
||||||
|
Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type zhipuTokenData struct {
|
||||||
|
Token string
|
||||||
|
ExpiryTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var zhipuTokens sync.Map
|
||||||
|
var expSeconds int64 = 24 * 3600
|
||||||
|
|
||||||
|
func getZhipuToken(apikey string) string {
|
||||||
|
data, ok := zhipuTokens.Load(apikey)
|
||||||
|
if ok {
|
||||||
|
tokenData := data.(zhipuTokenData)
|
||||||
|
if time.Now().Before(tokenData.ExpiryTime) {
|
||||||
|
return tokenData.Token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(apikey, ".")
|
||||||
|
if len(split) != 2 {
|
||||||
|
common.SysError("invalid zhipu key: " + apikey)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := split[0]
|
||||||
|
secret := split[1]
|
||||||
|
|
||||||
|
expMillis := time.Now().Add(time.Duration(expSeconds)*time.Second).UnixNano() / 1e6
|
||||||
|
expiryTime := time.Now().Add(time.Duration(expSeconds) * time.Second)
|
||||||
|
|
||||||
|
timestamp := time.Now().UnixNano() / 1e6
|
||||||
|
|
||||||
|
payload := jwt.MapClaims{
|
||||||
|
"api_key": id,
|
||||||
|
"exp": expMillis,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||||
|
|
||||||
|
token.Header["alg"] = "HS256"
|
||||||
|
token.Header["sign_type"] = "SIGN"
|
||||||
|
|
||||||
|
tokenString, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
zhipuTokens.Store(apikey, zhipuTokenData{
|
||||||
|
Token: tokenString,
|
||||||
|
ExpiryTime: expiryTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
return tokenString
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
||||||
|
messages := make([]ZhipuMessage, 0, len(request.Messages))
|
||||||
|
for _, message := range request.Messages {
|
||||||
|
if message.Role == "system" {
|
||||||
|
messages = append(messages, ZhipuMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
messages = append(messages, ZhipuMessage{
|
||||||
|
Role: "user",
|
||||||
|
Content: "Okay",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messages = append(messages, ZhipuMessage{
|
||||||
|
Role: message.Role,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ZhipuRequest{
|
||||||
|
Prompt: messages,
|
||||||
|
Temperature: request.Temperature,
|
||||||
|
TopP: request.TopP,
|
||||||
|
Incremental: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
|
||||||
|
fullTextResponse := OpenAITextResponse{
|
||||||
|
Id: response.Data.TaskId,
|
||||||
|
Object: "chat.completion",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Choices: make([]OpenAITextResponseChoice, 0, len(response.Data.Choices)),
|
||||||
|
Usage: response.Data.Usage,
|
||||||
|
}
|
||||||
|
for i, choice := range response.Data.Choices {
|
||||||
|
openaiChoice := OpenAITextResponseChoice{
|
||||||
|
Index: i,
|
||||||
|
Message: Message{
|
||||||
|
Role: choice.Role,
|
||||||
|
Content: strings.Trim(choice.Content, "\""),
|
||||||
|
},
|
||||||
|
FinishReason: "",
|
||||||
|
}
|
||||||
|
if i == len(response.Data.Choices)-1 {
|
||||||
|
openaiChoice.FinishReason = "stop"
|
||||||
|
}
|
||||||
|
fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice)
|
||||||
|
}
|
||||||
|
return &fullTextResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = zhipuResponse
|
||||||
|
choice.FinishReason = ""
|
||||||
|
response := ChatCompletionsStreamResponse{
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: "chatglm",
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = ""
|
||||||
|
choice.FinishReason = "stop"
|
||||||
|
response := ChatCompletionsStreamResponse{
|
||||||
|
Id: zhipuResponse.RequestId,
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: "chatglm",
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
return &response, &zhipuResponse.Usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var usage *Usage
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
dataChan := make(chan string)
|
||||||
|
metaChan := make(chan string)
|
||||||
|
stopChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Text()
|
||||||
|
data = strings.Trim(data, "\"")
|
||||||
|
if len(data) < 5 { // ignore blank line or wrong format
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data[:5] == "data:" {
|
||||||
|
dataChan <- data[5:]
|
||||||
|
} else if data[:5] == "meta:" {
|
||||||
|
metaChan <- data[5:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopChan <- true
|
||||||
|
}()
|
||||||
|
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||||
|
c.Writer.Header().Set("Connection", "keep-alive")
|
||||||
|
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case data := <-dataChan:
|
||||||
|
response := streamResponseZhipu2OpenAI(data)
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
return true
|
||||||
|
case data := <-metaChan:
|
||||||
|
var zhipuResponse ZhipuStreamMetaResponse
|
||||||
|
err := json.Unmarshal([]byte(data), &zhipuResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
response, zhipuUsage := streamMetaResponseZhipu2OpenAI(&zhipuResponse)
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
usage = zhipuUsage
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
return true
|
||||||
|
case <-stopChan:
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
err := resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
return nil, usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func zhipuHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var zhipuResponse ZhipuResponse
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &zhipuResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
if !zhipuResponse.Success {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: zhipuResponse.Msg,
|
||||||
|
Type: "zhipu_error",
|
||||||
|
Param: "",
|
||||||
|
Code: zhipuResponse.Code,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fullTextResponse := responseZhipu2OpenAI(&zhipuResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
@@ -46,6 +46,7 @@ type ChatRequest struct {
|
|||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
Stream bool `json:"stream"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextRequest struct {
|
type TextRequest struct {
|
||||||
|
@@ -3,12 +3,13 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -477,6 +478,16 @@ func DeleteUser(c *gin.Context) {
|
|||||||
|
|
||||||
func DeleteSelf(c *gin.Context) {
|
func DeleteSelf(c *gin.Context) {
|
||||||
id := c.GetInt("id")
|
id := c.GetInt("id")
|
||||||
|
user, _ := model.GetUserById(id, false)
|
||||||
|
|
||||||
|
if user.Role == common.RoleRootUser {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "不能删除超级管理员账户",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := model.DeleteUserById(id)
|
err := model.DeleteUserById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
@@ -2,7 +2,7 @@ version: '3.4'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
one-api:
|
one-api:
|
||||||
image: justsong/one-api:latest
|
image: ckt1031/one-api:latest
|
||||||
container_name: one-api
|
container_name: one-api
|
||||||
restart: always
|
restart: always
|
||||||
command: --log-dir /app/logs
|
command: --log-dir /app/logs
|
||||||
|
44
go.mod
44
go.mod
@@ -9,49 +9,55 @@ require (
|
|||||||
github.com/gin-contrib/sessions v0.0.5
|
github.com/gin-contrib/sessions v0.0.5
|
||||||
github.com/gin-contrib/static v0.0.1
|
github.com/gin-contrib/static v0.0.1
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/go-playground/validator/v10 v10.14.0
|
github.com/go-playground/validator/v10 v10.14.1
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1
|
github.com/pkoukk/tiktoken-go v0.1.5
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.11.0
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.5.1
|
||||||
gorm.io/driver/sqlite v1.4.3
|
gorm.io/driver/sqlite v1.5.2
|
||||||
gorm.io/gorm v1.24.0
|
gorm.io/gorm v1.25.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/jackc/pgx/v5 v5.4.2 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.9.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.4.0 // indirect
|
||||||
golang.org/x/net v0.10.0 // indirect
|
golang.org/x/net v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.11.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/postgres v1.5.2
|
||||||
)
|
)
|
||||||
|
96
go.sum
96
go.sum
@@ -1,10 +1,8 @@
|
|||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
|
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
@@ -14,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
@@ -45,19 +43,20 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -67,20 +66,26 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
|
|||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
||||||
|
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.2 h1:u1gmGDwbdRUZiwisBm/Ky2M14uQyUP65bG8+20nnyrg=
|
||||||
|
github.com/jackc/pgx/v5 v5.4.2/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
@@ -97,7 +102,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
|||||||
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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/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 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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -110,11 +114,11 @@ 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/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
|
github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
|
github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
@@ -131,8 +135,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
@@ -142,36 +146,36 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95
|
|||||||
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=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.4.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.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/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/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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -186,11 +190,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
|
||||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
|
||||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
||||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
||||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
|
||||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
||||||
|
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
74
i18n/en.json
74
i18n/en.json
@@ -260,7 +260,7 @@
|
|||||||
"注意": "Note",
|
"注意": "Note",
|
||||||
"此处生成的令牌用于系统管理": "The token generated here is used for system management",
|
"此处生成的令牌用于系统管理": "The token generated here is used for system management",
|
||||||
"而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services",
|
"而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services",
|
||||||
"请知悉": "Please be aware",
|
"请知悉。": "Please be aware.",
|
||||||
"更新个人信息": "Update Personal Information",
|
"更新个人信息": "Update Personal Information",
|
||||||
"生成系统访问令牌": "Generate System Access Token",
|
"生成系统访问令牌": "Generate System Access Token",
|
||||||
"复制邀请链接": "Copy Invitation Link",
|
"复制邀请链接": "Copy Invitation Link",
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
"兑换时间": "Redemption Time",
|
"兑换时间": "Redemption Time",
|
||||||
"尚未兑换": "Not yet redeemed",
|
"尚未兑换": "Not yet redeemed",
|
||||||
"已复制到剪贴板": "Copied to clipboard",
|
"已复制到剪贴板": "Copied to clipboard",
|
||||||
"无法复制到剪贴板": "Unable to copy to clipboard",
|
"无法复制到剪贴板。": "Unable to copy to clipboard.",
|
||||||
"请手动复制": "Please copy manually",
|
"请手动复制": "Please copy manually",
|
||||||
"已将兑换码填入搜索框": "The voucher code has been filled into the search box",
|
"已将兑换码填入搜索框": "The voucher code has been filled into the search box",
|
||||||
"复制": "Copy",
|
"复制": "Copy",
|
||||||
@@ -503,5 +503,73 @@
|
|||||||
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
||||||
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
||||||
"Homepage URL 填": "Fill in the Homepage URL",
|
"Homepage URL 填": "Fill in the Homepage URL",
|
||||||
"Authorization callback URL 填": "Fill in the Authorization callback URL"
|
"Authorization callback URL 填": "Fill in the Authorization callback URL",
|
||||||
|
|
||||||
|
"兑换中": "Redeeming",
|
||||||
|
"请求失败": "Request failed",
|
||||||
|
"百度文心千帆": "Baidu Wenxin Qianfan",
|
||||||
|
"智谱": "Zhipuai",
|
||||||
|
"代理:": "Agent: ",
|
||||||
|
"请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!",
|
||||||
|
"账户已删除!": "Account deleted!",
|
||||||
|
"删除个人账户": "Delete personal account",
|
||||||
|
"重新发送": "Resend ",
|
||||||
|
"确认删除自己的帐户": "Confirm deletion of your own account",
|
||||||
|
"输入你的账户名": "Enter your account name",
|
||||||
|
"以确认删除": "to confirm deletion",
|
||||||
|
"重试": "Retry",
|
||||||
|
"无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。": "Unable to copy to clipboard, please copy manually, the redemption code has been filled in the search box.",
|
||||||
|
"密码重置完成": "Password reset completed",
|
||||||
|
|
||||||
|
"流式请求和非流式请求不能同时禁用!": "Streaming requests and non-streaming requests cannot be disabled at the same time!",
|
||||||
|
|
||||||
|
"请为渠道命名": "Please name the channel",
|
||||||
|
"请选择可以使用该渠道的分组": "Please select the group that can use this channel",
|
||||||
|
"请选择该渠道所支持的模型": "Please select the models supported by this channel",
|
||||||
|
"填入": "Fill in",
|
||||||
|
"此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
|
||||||
|
"允许流式请求": "Allow streaming requests",
|
||||||
|
"允许非流式请求": "Allow non-streaming requests",
|
||||||
|
"请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次": "Please enter the access token, the current version does not support automatic refresh, please update it every 30 days",
|
||||||
|
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
||||||
|
"此项可选,用于通过Mirror站来进行 API 调用,请EnterMirror站地址,格式为:https://domain.com": "This is optional, used to make API calls through the Mirror site, please enter the Mirror site address, the format is: https://domain.com",
|
||||||
|
|
||||||
|
"新密码": "New password",
|
||||||
|
"新密码已复制到剪贴板:": "New password copied to clipboard: ",
|
||||||
|
"兑换失败,": "Redemption failed, ",
|
||||||
|
"当前分组 %s 下对于模型 %s 无可用渠道": "There are no available channels for model %s under the current group %s",
|
||||||
|
"无权将其他用户权限等级提升到大于等于自己的权限等级": "You are not allowed to raise the permission level of other users to greater than or equal to your own permission level",
|
||||||
|
"不能删除超级管理员账户": "Cannot delete super administrator account",
|
||||||
|
"无效参数": "Invalid parameter",
|
||||||
|
"访问令牌无效": "Access token invalid",
|
||||||
|
"Google 用户信息无效": "Google user information invalid",
|
||||||
|
"返回值无效,用户字段为空,请稍后再试!": "The return value is invalid, the user field is empty, please try again later!",
|
||||||
|
"管理员未开启通过 Google 登录以及注册": "The administrator has not enabled login and registration via Google",
|
||||||
|
"该 Google 账户已被绑定": "The Google account has been bound",
|
||||||
|
"点击 <a href='%s'>此处</a> 进行密码重置。": "Click <a href='%s'>here</a> to reset your password.",
|
||||||
|
"如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s ": "If the link cannot be clicked, please try to click the link below or copy it to the browser to open: <br> %s ",
|
||||||
|
"该渠道类型当前版本不支持测试,请手动测试": "The current version of the channel type does not support testing, please test manually",
|
||||||
|
"无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!": "Unable to enable Discord OAuth, please fill in the Discord Client ID and Discord Client Secret first!",
|
||||||
|
"无法启用 Google OAuth,请先填入 Google Client ID 以及 Google Client Secret!": "Unable to enable Google OAuth, please fill in the Google Client ID and Google Client Secret first!",
|
||||||
|
"管理员未开启通过 Discord 登录以及注册": "The administrator has not enabled login and registration via Discord",
|
||||||
|
"该 Discord 账户已被绑定": "The Discord account has been bound",
|
||||||
|
"Discord 用户信息无效": "Discord user information invalid",
|
||||||
|
"绑定 Discord 账号": "Bind Discord account",
|
||||||
|
"绑定 Google 账号": "Bind Google account",
|
||||||
|
"已绑定的 Discord 账户": "Bound Discord account",
|
||||||
|
"已绑定的 Google 账户": "Bound Google account",
|
||||||
|
"身份验证": "Authentication",
|
||||||
|
"允许通过 Discord 账户登录和注册": "Allow login and registration via Discord account",
|
||||||
|
"允许通过 Google 账户登录和注册": "Allow login and registration via Google account",
|
||||||
|
"配置 Discord OAuth 应用程序": "Configure Discord OAuth application",
|
||||||
|
"配置 Google OAuth 应用程序": "Configure Google OAuth application",
|
||||||
|
"用以支持通过 Discord 进行登录注册,": "Used to support login and registration via Discord, ",
|
||||||
|
"用以支持通过 Google 进行登录注册,": "Used to support login and registration via Google, ",
|
||||||
|
"管理你的": "Manage your ",
|
||||||
|
"客户 ID": "Client ID",
|
||||||
|
"客户秘密": "Client Secret",
|
||||||
|
"失败重试次数": "Retry times",
|
||||||
|
"保存": "Save",
|
||||||
|
"输入您注册的 Discord OAuth APP 的 ID": "Enter the ID of your registered Discord OAuth APP",
|
||||||
|
"输入您注册的 Google OAuth APP 的 ID": "Enter the ID of your registered Google OAuth APP"
|
||||||
}
|
}
|
||||||
|
1
main.go
1
main.go
@@ -54,6 +54,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
||||||
}
|
}
|
||||||
|
common.SyncFrequency = frequency
|
||||||
go model.SyncOptions(frequency)
|
go model.SyncOptions(frequency)
|
||||||
if common.RedisEnabled {
|
if common.RedisEnabled {
|
||||||
go model.SyncChannelCache(frequency)
|
go model.SyncChannelCache(frequency)
|
||||||
|
@@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
@@ -12,7 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ModelRequest struct {
|
type ModelRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
|
Stream bool `json:"stream" default:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Distribute() func(c *gin.Context) {
|
func Distribute() func(c *gin.Context) {
|
||||||
@@ -84,9 +86,10 @@ func Distribute() func(c *gin.Context) {
|
|||||||
modelRequest.Model = "dall-e"
|
modelRequest.Model = "dall-e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
log.Print(modelRequest.Stream)
|
||||||
|
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, modelRequest.Stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message := "无可用渠道"
|
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
||||||
message = "数据库一致性已被破坏,请联系管理员"
|
message = "数据库一致性已被破坏,请联系管理员"
|
||||||
|
@@ -1,24 +1,47 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ability struct {
|
type Ability struct {
|
||||||
Group string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"`
|
Group string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"`
|
||||||
Model string `json:"model" gorm:"primaryKey;autoIncrement:false"`
|
Model string `json:"model" gorm:"primaryKey;autoIncrement:false"`
|
||||||
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
|
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
AllowStreaming int `json:"allow_streaming" gorm:"default:1"`
|
||||||
|
AllowNonStreaming int `json:"allow_non_streaming" gorm:"default:1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
|
func GetRandomSatisfiedChannel(group string, model string, stream bool) (*Channel, error) {
|
||||||
ability := Ability{}
|
ability := Ability{}
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if common.UsingSQLite {
|
|
||||||
err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RANDOM()").Limit(1).First(&ability).Error
|
cmd := "`group` = ? and model = ? and enabled = 1"
|
||||||
|
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
// Make cmd compatible with PostgreSQL
|
||||||
|
cmd = "\"group\" = ? and model = ? and enabled = true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if stream {
|
||||||
|
cmd += fmt.Sprintf(" and allow_streaming = %d", common.ChannelAllowStreamEnabled)
|
||||||
} else {
|
} else {
|
||||||
err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RAND()").Limit(1).First(&ability).Error
|
cmd += fmt.Sprintf(" and allow_non_streaming = %d", common.ChannelAllowNonStreamEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.UsingSQLite || common.UsingPostgreSQL {
|
||||||
|
err = DB.Where(cmd, group, model).Order("RANDOM()").Limit(1).First(&ability).Error
|
||||||
|
} else {
|
||||||
|
cmd += fmt.Sprintf(" and allow_non_streaming = %d", common.ChannelAllowNonStreamEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.UsingSQLite {
|
||||||
|
err = DB.Where(cmd, group, model).Order("RANDOM()").Limit(1).First(&ability).Error
|
||||||
|
} else {
|
||||||
|
err = DB.Where(cmd, group, model).Order("RAND()").Limit(1).First(&ability).Error
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -36,10 +59,12 @@ func (channel *Channel) AddAbilities() error {
|
|||||||
for _, model := range models_ {
|
for _, model := range models_ {
|
||||||
for _, group := range groups_ {
|
for _, group := range groups_ {
|
||||||
ability := Ability{
|
ability := Ability{
|
||||||
Group: group,
|
Group: group,
|
||||||
Model: model,
|
Model: model,
|
||||||
ChannelId: channel.Id,
|
ChannelId: channel.Id,
|
||||||
Enabled: channel.Status == common.ChannelStatusEnabled,
|
Enabled: channel.Status == common.ChannelStatusEnabled,
|
||||||
|
AllowStreaming: channel.AllowStreaming,
|
||||||
|
AllowNonStreaming: channel.AllowNonStreaming,
|
||||||
}
|
}
|
||||||
abilities = append(abilities, ability)
|
abilities = append(abilities, ability)
|
||||||
}
|
}
|
||||||
|
@@ -12,22 +12,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
TokenCacheSeconds = 60 * 60
|
TokenCacheSeconds = common.SyncFrequency
|
||||||
UserId2GroupCacheSeconds = 60 * 60
|
UserId2GroupCacheSeconds = common.SyncFrequency
|
||||||
UserId2QuotaCacheSeconds = 10 * 60
|
UserId2QuotaCacheSeconds = common.SyncFrequency
|
||||||
UserId2StatusCacheSeconds = 60 * 60
|
UserId2StatusCacheSeconds = common.SyncFrequency
|
||||||
)
|
)
|
||||||
|
|
||||||
func CacheGetTokenByKey(key string) (*Token, error) {
|
func CacheGetTokenByKey(key string) (*Token, error) {
|
||||||
var token Token
|
var token Token
|
||||||
|
whereItem := "`key` = ?"
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
// Make cmd compatible with PostgreSQL
|
||||||
|
whereItem = "\"key\" = ?"
|
||||||
|
}
|
||||||
|
|
||||||
if !common.RedisEnabled {
|
if !common.RedisEnabled {
|
||||||
err := DB.Where("`key` = ?", key).First(&token).Error
|
err := DB.Where(whereItem, key).First(&token).Error
|
||||||
return &token, err
|
return &token, err
|
||||||
}
|
}
|
||||||
tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key))
|
tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := DB.Where("`key` = ?", key).First(&token).Error
|
err := DB.Where(whereItem, key).First(&token).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -35,7 +41,7 @@ func CacheGetTokenByKey(key string) (*Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), TokenCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set token error: " + err.Error())
|
common.SysError("Redis set token error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -55,7 +61,7 @@ func CacheGetUserGroup(id int) (group string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, UserId2GroupCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, time.Duration(UserId2GroupCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user group error: " + err.Error())
|
common.SysError("Redis set user group error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -73,7 +79,7 @@ func CacheGetUserQuota(id int) (quota int, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user quota error: " + err.Error())
|
common.SysError("Redis set user quota error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -91,7 +97,7 @@ func CacheUpdateUserQuota(id int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +112,7 @@ func CacheIsUserEnabled(userId int) bool {
|
|||||||
status = common.UserStatusEnabled
|
status = common.UserStatusEnabled
|
||||||
}
|
}
|
||||||
enabled = fmt.Sprintf("%d", status)
|
enabled = fmt.Sprintf("%d", status)
|
||||||
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, UserId2StatusCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, time.Duration(UserId2StatusCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user enabled error: " + err.Error())
|
common.SysError("Redis set user enabled error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -160,9 +166,9 @@ func SyncChannelCache(frequency int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
|
func CacheGetRandomSatisfiedChannel(group string, model string, stream bool) (*Channel, error) {
|
||||||
if !common.RedisEnabled {
|
if !common.RedisEnabled {
|
||||||
return GetRandomSatisfiedChannel(group, model)
|
return GetRandomSatisfiedChannel(group, model, stream)
|
||||||
}
|
}
|
||||||
channelSyncLock.RLock()
|
channelSyncLock.RLock()
|
||||||
defer channelSyncLock.RUnlock()
|
defer channelSyncLock.RUnlock()
|
||||||
@@ -170,6 +176,14 @@ func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error
|
|||||||
if len(channels) == 0 {
|
if len(channels) == 0 {
|
||||||
return nil, errors.New("channel not found")
|
return nil, errors.New("channel not found")
|
||||||
}
|
}
|
||||||
idx := rand.Intn(len(channels))
|
|
||||||
return channels[idx], nil
|
var filteredChannels []*Channel
|
||||||
|
for _, channel := range channels {
|
||||||
|
if (stream && channel.AllowStreaming == common.ChannelAllowStreamEnabled) || (!stream && channel.AllowNonStreaming == common.ChannelAllowNonStreamEnabled) {
|
||||||
|
filteredChannels = append(filteredChannels, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := rand.Intn(len(filteredChannels))
|
||||||
|
return filteredChannels[idx], nil
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -23,6 +24,8 @@ type Channel struct {
|
|||||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||||
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
||||||
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
||||||
|
AllowStreaming int `json:"allow_streaming" gorm:"default:1"`
|
||||||
|
AllowNonStreaming int `json:"allow_non_streaming" gorm:"default:1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||||
@@ -37,7 +40,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
||||||
err = DB.Omit("key").Where("id = ? or name LIKE ? or `key` = ?", keyword, keyword+"%", keyword).Find(&channels).Error
|
whereItem := "id = ? or name LIKE ? or `key` = ?"
|
||||||
|
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
whereItem = "id = ? or name LIKE ? or \"key\" = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DB.Omit("key").Where(whereItem, keyword, keyword+"%", keyword).Find(&channels).Error
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +64,9 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) {
|
|||||||
func GetRandomChannel() (*Channel, error) {
|
func GetRandomChannel() (*Channel, error) {
|
||||||
channel := Channel{}
|
channel := Channel{}
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if common.UsingSQLite {
|
if common.UsingPostgreSQL {
|
||||||
|
err = DB.Where("status = ? and \"group\" = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error
|
||||||
|
} else if common.UsingSQLite {
|
||||||
err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error
|
err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error
|
||||||
} else {
|
} else {
|
||||||
err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RAND()").Limit(1).First(&channel).Error
|
err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RAND()").Limit(1).First(&channel).Error
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
@@ -40,7 +42,14 @@ func CountTable(tableName string) (num int64) {
|
|||||||
|
|
||||||
func InitDB() (err error) {
|
func InitDB() (err error) {
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
if os.Getenv("SQL_DSN") != "" {
|
if os.Getenv("POSTGRES_DSN") != "" {
|
||||||
|
// Use PostgreSQL
|
||||||
|
common.SysLog("using PostgreSQL as database")
|
||||||
|
common.UsingPostgreSQL = true
|
||||||
|
db, err = gorm.Open(postgres.Open(os.Getenv("POSTGRES_DSN")), &gorm.Config{
|
||||||
|
PrepareStmt: true, // precompile SQL
|
||||||
|
})
|
||||||
|
} else if os.Getenv("SQL_DSN") != "" {
|
||||||
// Use MySQL
|
// Use MySQL
|
||||||
common.SysLog("using MySQL as database")
|
common.SysLog("using MySQL as database")
|
||||||
db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{
|
db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{
|
||||||
|
@@ -30,7 +30,9 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
||||||
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
||||||
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
||||||
|
common.OptionMap["DiscordOAuthEnabled"] = strconv.FormatBool(common.DiscordOAuthEnabled)
|
||||||
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
|
common.OptionMap["GoogleOAuthEnabled"] = strconv.FormatBool(common.GoogleOAuthEnabled)
|
||||||
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["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
||||||
@@ -53,9 +55,13 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["ServerAddress"] = ""
|
common.OptionMap["ServerAddress"] = ""
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["DiscordClientId"] = ""
|
||||||
|
common.OptionMap["DiscordClientSecret"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
common.OptionMap["WeChatServerToken"] = ""
|
common.OptionMap["WeChatServerToken"] = ""
|
||||||
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
|
common.OptionMap["GoogleClientId"] = ""
|
||||||
|
common.OptionMap["GoogleClientSecret"] = ""
|
||||||
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)
|
||||||
@@ -135,8 +141,12 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.EmailVerificationEnabled = boolValue
|
common.EmailVerificationEnabled = boolValue
|
||||||
case "GitHubOAuthEnabled":
|
case "GitHubOAuthEnabled":
|
||||||
common.GitHubOAuthEnabled = boolValue
|
common.GitHubOAuthEnabled = boolValue
|
||||||
|
case "DiscordOAuthEnabled":
|
||||||
|
common.DiscordOAuthEnabled = boolValue
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
common.WeChatAuthEnabled = boolValue
|
common.WeChatAuthEnabled = boolValue
|
||||||
|
case "GoogleOAuthEnabled":
|
||||||
|
common.GoogleOAuthEnabled = boolValue
|
||||||
case "TurnstileCheckEnabled":
|
case "TurnstileCheckEnabled":
|
||||||
common.TurnstileCheckEnabled = boolValue
|
common.TurnstileCheckEnabled = boolValue
|
||||||
case "RegisterEnabled":
|
case "RegisterEnabled":
|
||||||
@@ -171,6 +181,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.GitHubClientId = value
|
common.GitHubClientId = value
|
||||||
case "GitHubClientSecret":
|
case "GitHubClientSecret":
|
||||||
common.GitHubClientSecret = value
|
common.GitHubClientSecret = value
|
||||||
|
case "DiscordClientId":
|
||||||
|
common.DiscordClientId = value
|
||||||
|
case "DiscordClientSecret":
|
||||||
|
common.DiscordClientSecret = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
case "SystemName":
|
case "SystemName":
|
||||||
@@ -183,6 +197,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.WeChatServerToken = value
|
common.WeChatServerToken = value
|
||||||
case "WeChatAccountQRCodeImageURL":
|
case "WeChatAccountQRCodeImageURL":
|
||||||
common.WeChatAccountQRCodeImageURL = value
|
common.WeChatAccountQRCodeImageURL = value
|
||||||
|
case "GoogleClientId":
|
||||||
|
common.GoogleClientId = value
|
||||||
|
case "GoogleClientSecret":
|
||||||
|
common.GoogleClientSecret = value
|
||||||
case "TurnstileSiteKey":
|
case "TurnstileSiteKey":
|
||||||
common.TurnstileSiteKey = value
|
common.TurnstileSiteKey = value
|
||||||
case "TurnstileSecretKey":
|
case "TurnstileSecretKey":
|
||||||
|
@@ -3,8 +3,9 @@ package model
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Redemption struct {
|
type Redemption struct {
|
||||||
@@ -51,20 +52,28 @@ func Redeem(key string, userId int) (quota int, err error) {
|
|||||||
redemption := &Redemption{}
|
redemption := &Redemption{}
|
||||||
|
|
||||||
err = DB.Transaction(func(tx *gorm.DB) error {
|
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||||
err := DB.Where("`key` = ?", key).First(redemption).Error
|
whereItem := "`key` = ?"
|
||||||
|
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
// Make cmd compatible with PostgreSQL
|
||||||
|
whereItem = "\"key\" = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tx.Set("gorm:query_option", "FOR UPDATE").Where(whereItem, key).First(redemption).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("无效的兑换码")
|
return errors.New("无效的兑换码")
|
||||||
}
|
}
|
||||||
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
||||||
return errors.New("该兑换码已被使用")
|
return errors.New("该兑换码已被使用")
|
||||||
}
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
|
err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
redemption.RedeemedTime = common.GetTimestamp()
|
redemption.RedeemedTime = common.GetTimestamp()
|
||||||
redemption.Status = common.RedemptionCodeStatusUsed
|
redemption.Status = common.RedemptionCodeStatusUsed
|
||||||
return redemption.SelectUpdate()
|
err = tx.Save(redemption).Error
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.New("兑换失败," + err.Error())
|
return 0, errors.New("兑换失败," + err.Error())
|
||||||
|
@@ -3,9 +3,10 @@ package model
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
|
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
|
||||||
@@ -19,7 +20,9 @@ type User struct {
|
|||||||
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
||||||
Email string `json:"email" gorm:"index" validate:"max=50"`
|
Email string `json:"email" gorm:"index" validate:"max=50"`
|
||||||
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
||||||
|
DiscordId string `json:"discord_id" gorm:"column:discord_id;index"`
|
||||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
|
GoogleId string `json:"google_id" gorm:"column:google_id;index"`
|
||||||
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
||||||
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||||
Quota int `json:"quota" gorm:"type:int;default:0"`
|
Quota int `json:"quota" gorm:"type:int;default:0"`
|
||||||
@@ -169,6 +172,14 @@ func (user *User) FillUserByGitHubId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByDiscordId() error {
|
||||||
|
if user.DiscordId == "" {
|
||||||
|
return errors.New("Discord id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{DiscordId: user.DiscordId}).First(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) FillUserByWeChatId() error {
|
func (user *User) FillUserByWeChatId() error {
|
||||||
if user.WeChatId == "" {
|
if user.WeChatId == "" {
|
||||||
return errors.New("WeChat id 为空!")
|
return errors.New("WeChat id 为空!")
|
||||||
@@ -177,6 +188,14 @@ func (user *User) FillUserByWeChatId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByGoogleId() error {
|
||||||
|
if user.GoogleId == "" {
|
||||||
|
return errors.New("Google id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{GoogleId: user.GoogleId}).First(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) FillUserByUsername() error {
|
func (user *User) FillUserByUsername() error {
|
||||||
if user.Username == "" {
|
if user.Username == "" {
|
||||||
return errors.New("username 为空!")
|
return errors.New("username 为空!")
|
||||||
@@ -193,6 +212,14 @@ func IsWeChatIdAlreadyTaken(wechatId string) bool {
|
|||||||
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDiscordIdAlreadyTaken(discordId string) bool {
|
||||||
|
return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsGoogleIdAlreadyTaken(googleId string) bool {
|
||||||
|
return DB.Where("google_id = ?", googleId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
||||||
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
@@ -267,7 +294,13 @@ func GetUserEmail(id int) (email string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUserGroup(id int) (group string, err error) {
|
func GetUserGroup(id int) (group string, err error) {
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Select("`group`").Find(&group).Error
|
selectItem := "`group`"
|
||||||
|
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
selectItem = "\"group\""
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DB.Model(&User{}).Where("id = ?", id).Select(selectItem).Find(&group).Error
|
||||||
return group, err
|
return group, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,14 +21,16 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
||||||
|
apiRouter.GET("/oauth/discord", middleware.CriticalRateLimit(), controller.DiscordOAuth)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
|
apiRouter.GET("/oauth/google", middleware.CriticalRateLimit(), controller.GoogleOAuth)
|
||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
|
|
||||||
userRoute := apiRouter.Group("/user")
|
userRoute := apiRouter.Group("/user")
|
||||||
{
|
{
|
||||||
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
||||||
userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login)
|
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
|
||||||
userRoute.GET("/logout", controller.Logout)
|
userRoute.GET("/logout", controller.Logout)
|
||||||
|
|
||||||
selfRoute := userRoute.Group("/")
|
selfRoute := userRoute.Group("/")
|
||||||
@@ -36,7 +38,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
selfRoute.GET("/self", controller.GetSelf)
|
selfRoute.GET("/self", controller.GetSelf)
|
||||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
selfRoute.POST("/topup", controller.TopUp)
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
|
@@ -12,7 +12,7 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
modelsRouter := router.Group("/v1/models")
|
modelsRouter := router.Group("/v1/models")
|
||||||
modelsRouter.Use(middleware.TokenAuth())
|
modelsRouter.Use(middleware.TokenAuth())
|
||||||
{
|
{
|
||||||
modelsRouter.GET("/", controller.ListModels)
|
modelsRouter.GET("", controller.ListModels)
|
||||||
modelsRouter.GET("/:model", controller.RetrieveModel)
|
modelsRouter.GET("/:model", controller.RetrieveModel)
|
||||||
}
|
}
|
||||||
relayV1Router := router.Group("/v1")
|
relayV1Router := router.Group("/v1")
|
||||||
|
@@ -3,18 +3,19 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"marked": "^4.1.1",
|
"marked": "^5.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.1.3",
|
||||||
"react-turnstile": "^1.0.5",
|
"react-turnstile": "^1.1.1",
|
||||||
"semantic-ui-css": "^2.5.0",
|
"semantic-ui-css": "^2.5.0",
|
||||||
"semantic-ui-react": "^2.1.3"
|
"semantic-ui-react": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.7.1"
|
"prettier": "^3.0.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
@@ -12,6 +12,8 @@ import AddUser from './pages/User/AddUser';
|
|||||||
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
||||||
import PasswordResetForm from './components/PasswordResetForm';
|
import PasswordResetForm from './components/PasswordResetForm';
|
||||||
import GitHubOAuth from './components/GitHubOAuth';
|
import GitHubOAuth from './components/GitHubOAuth';
|
||||||
|
import DiscordOAuth from './components/DiscordOAuth';
|
||||||
|
import GoogleOAuth from './components/GoogleOAuth';
|
||||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||||
import { UserContext } from './context/User';
|
import { UserContext } from './context/User';
|
||||||
import { StatusContext } from './context/Status';
|
import { StatusContext } from './context/Status';
|
||||||
@@ -239,6 +241,24 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
HEAD
|
||||||
|
path='/oauth/discord'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<DiscordOAuth />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/oauth/google'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<GoogleOAuth />
|
||||||
|
support-google-oauth
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/setting'
|
path='/setting'
|
||||||
element={
|
element={
|
||||||
@@ -252,11 +272,11 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path='/topup'
|
path='/topup'
|
||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Loading></Loading>}>
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
<TopUp />
|
<TopUp />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
|
57
web/src/components/DiscordOAuth.js
Normal file
57
web/src/components/DiscordOAuth.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
const DiscordOAuth = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const [processing, setProcessing] = useState(true);
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/discord?code=${code}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/setting');
|
||||||
|
} else {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
navigate('/setting'); // in case this is failed to bind GitHub
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
sendCode(code, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscordOAuth;
|
57
web/src/components/GoogleOAuth.js
Normal file
57
web/src/components/GoogleOAuth.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
const GoogleOAuth = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const [processing, setProcessing] = useState(true);
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/google?code=${code}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/setting');
|
||||||
|
} else {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
navigate('/setting'); // in case this is failed to bind GitHub
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
sendCode(code, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GoogleOAuth;
|
@@ -1,53 +1,63 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react';
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Form,
|
|
||||||
Grid,
|
|
||||||
Header,
|
|
||||||
Image,
|
|
||||||
Message,
|
|
||||||
Modal,
|
|
||||||
Segment,
|
|
||||||
} 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, getLogo, showError, showSuccess, showInfo } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
|
import Turnstile from 'react-turnstile';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
wechat_verification_code: '',
|
wechat_verification_code: ''
|
||||||
});
|
});
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const { username, password } = inputs;
|
const { username, password } = inputs;
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||||
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("expired")) {
|
if (searchParams.get('expired')) {
|
||||||
showError('未登录或登录已过期,请重新登录!');
|
showError('未登录或登录已过期,请重新登录!');
|
||||||
}
|
}
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
if (status) {
|
if (status) {
|
||||||
status = JSON.parse(status);
|
status = JSON.parse(status);
|
||||||
setStatus(status);
|
setStatus(status);
|
||||||
|
|
||||||
|
if (status.turnstile_check) {
|
||||||
|
setTurnstileEnabled(true);
|
||||||
|
setTurnstileSiteKey(status.turnstile_site_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
||||||
|
|
||||||
|
const openGoogleOAuth = () => {
|
||||||
|
window.open(
|
||||||
|
`https://accounts.google.com/o/oauth2/v2/auth?client_id=${status.google_client_id}&redirect_uri=${window.location.origin}/oauth/google&response_type=code&scope=profile`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const onGitHubOAuthClicked = () => {
|
const onGitHubOAuthClicked = () => {
|
||||||
window.open(
|
window.open(
|
||||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDiscordOAuthClicked = () => {
|
||||||
|
window.open(
|
||||||
|
`https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const onWeChatLoginClicked = () => {
|
const onWeChatLoginClicked = () => {
|
||||||
setShowWeChatLoginModal(true);
|
setShowWeChatLoginModal(true);
|
||||||
};
|
};
|
||||||
@@ -76,9 +86,14 @@ const LoginForm = () => {
|
|||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
const res = await API.post(`/api/user/login`, {
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, {
|
||||||
username,
|
username,
|
||||||
password,
|
password
|
||||||
});
|
});
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -93,69 +108,91 @@ const LoginForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as="h2" color="" textAlign="center">
|
<Header as='h2' color='' textAlign='center'>
|
||||||
<Image src={logo} /> 用户登录
|
<Image src={logo} /> 用户登录
|
||||||
</Header>
|
</Header>
|
||||||
<Form size="large">
|
<Form size='large'>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
icon="user"
|
icon='user'
|
||||||
iconPosition="left"
|
iconPosition='left'
|
||||||
placeholder="用户名"
|
placeholder='用户名'
|
||||||
name="username"
|
name='username'
|
||||||
value={username}
|
value={username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
icon="lock"
|
icon='lock'
|
||||||
iconPosition="left"
|
iconPosition='left'
|
||||||
placeholder="密码"
|
placeholder='密码'
|
||||||
name="password"
|
name='password'
|
||||||
type="password"
|
type='password'
|
||||||
value={password}
|
value={password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button color="" fluid size="large" onClick={handleSubmit}>
|
{turnstileEnabled ? (
|
||||||
|
<Turnstile
|
||||||
|
sitekey={turnstileSiteKey}
|
||||||
|
onVerify={(token) => {
|
||||||
|
setTurnstileToken(token);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<Button color='green' fluid size='large' onClick={handleSubmit}>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
<Message>
|
<Message>
|
||||||
忘记密码?
|
忘记密码?
|
||||||
<Link to="/reset" className="btn btn-link">
|
<Link to='/reset' className='btn btn-link'>
|
||||||
点击重置
|
点击重置
|
||||||
</Link>
|
</Link>
|
||||||
; 没有账户?
|
; 没有账户?
|
||||||
<Link to="/register" className="btn btn-link">
|
<Link to='/register' className='btn btn-link'>
|
||||||
点击注册
|
点击注册
|
||||||
</Link>
|
</Link>
|
||||||
</Message>
|
</Message>
|
||||||
{status.github_oauth || status.wechat_login ? (
|
{status.github_oauth || status.wechat_login || status.discord_oauth || status.google_oauth ? (
|
||||||
<>
|
<>
|
||||||
<Divider horizontal>Or</Divider>
|
<Divider horizontal>Or</Divider>
|
||||||
{status.github_oauth ? (
|
{status.discord_oauth && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="black"
|
color='blue'
|
||||||
icon="github"
|
icon='discord'
|
||||||
|
onClick={onDiscordOAuthClicked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{status.github_oauth && (
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
color='black'
|
||||||
|
icon='github'
|
||||||
onClick={onGitHubOAuthClicked}
|
onClick={onGitHubOAuthClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
{status.wechat_login ? (
|
{status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="green"
|
color='green'
|
||||||
icon="wechat"
|
icon='wechat'
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
<></>
|
{status.google_oauth && (
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
color='red'
|
||||||
|
icon='google'
|
||||||
|
onClick={openGoogleOAuth}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,18 +212,18 @@ const LoginForm = () => {
|
|||||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Form size="large">
|
<Form size='large'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
placeholder="验证码"
|
placeholder='验证码'
|
||||||
name="wechat_verification_code"
|
name='wechat_verification_code'
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color=""
|
color=''
|
||||||
fluid
|
fluid
|
||||||
size="large"
|
size='large'
|
||||||
onClick={onSubmitWeChatVerificationCode}
|
onClick={onSubmitWeChatVerificationCode}
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
|
@@ -12,6 +12,11 @@ const PasswordResetConfirm = () => {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let token = searchParams.get('token');
|
let token = searchParams.get('token');
|
||||||
@@ -22,7 +27,21 @@ const PasswordResetConfirm = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval = null;
|
||||||
|
if (disableButton && countdown > 0) {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setCountdown(countdown - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else if (countdown === 0) {
|
||||||
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
|
}
|
||||||
|
return () => clearInterval(countdownInterval);
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
|
setDisableButton(true);
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.post(`/api/user/reset`, {
|
const res = await API.post(`/api/user/reset`, {
|
||||||
@@ -32,14 +51,15 @@ const PasswordResetConfirm = () => {
|
|||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let password = res.data.data;
|
let password = res.data.data;
|
||||||
|
setNewPassword(password);
|
||||||
await copy(password);
|
await copy(password);
|
||||||
showNotice(`密码已重置并已复制到剪贴板:${password}`);
|
showNotice(`新密码已复制到剪贴板:${password}`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
@@ -57,20 +77,37 @@ const PasswordResetConfirm = () => {
|
|||||||
value={email}
|
value={email}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
{newPassword && (
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
icon='lock'
|
||||||
|
iconPosition='left'
|
||||||
|
placeholder='新密码'
|
||||||
|
name='newPassword'
|
||||||
|
value={newPassword}
|
||||||
|
readOnly
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.select();
|
||||||
|
navigator.clipboard.writeText(newPassword);
|
||||||
|
showNotice(`密码已复制到剪贴板:${newPassword}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
提交
|
{disableButton ? `密码重置完成` : '提交'}
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordResetConfirm;
|
export default PasswordResetConfirm;
|
||||||
|
@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
|
|||||||
|
|
||||||
const PasswordResetForm = () => {
|
const PasswordResetForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
email: '',
|
email: ''
|
||||||
});
|
});
|
||||||
const { email } = inputs;
|
const { email } = inputs;
|
||||||
|
|
||||||
@@ -13,24 +13,29 @@ const PasswordResetForm = () => {
|
|||||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let countdownInterval = null;
|
||||||
if (status) {
|
if (disableButton && countdown > 0) {
|
||||||
status = JSON.parse(status);
|
countdownInterval = setInterval(() => {
|
||||||
if (status.turnstile_check) {
|
setCountdown(countdown - 1);
|
||||||
setTurnstileEnabled(true);
|
}, 1000);
|
||||||
setTurnstileSiteKey(status.turnstile_site_key);
|
} else if (countdown === 0) {
|
||||||
}
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
}
|
}
|
||||||
}, []);
|
return () => clearInterval(countdownInterval);
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
function handleChange(e) {
|
function handleChange(e) {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs(inputs => ({ ...inputs, [name]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
|
setDisableButton(true);
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
if (turnstileEnabled && turnstileToken === '') {
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
@@ -78,13 +83,14 @@ const PasswordResetForm = () => {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
提交
|
{disableButton ? `重试 (${countdown})` : '提交'}
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
|
@@ -1,22 +1,30 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
const PersonalSetting = () => {
|
const PersonalSetting = () => {
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
wechat_verification_code: '',
|
wechat_verification_code: '',
|
||||||
email_verification_code: '',
|
email_verification_code: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
self_account_deletion_confirmation: ''
|
||||||
});
|
});
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||||||
|
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
||||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -30,6 +38,19 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval = null;
|
||||||
|
if (disableButton && countdown > 0) {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setCountdown(countdown - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else if (countdown === 0) {
|
||||||
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
|
}
|
||||||
|
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
@@ -57,6 +78,26 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||||
|
showError('请输入你的账户名以确认删除!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await API.delete('/api/user/self');
|
||||||
|
const { success, message } = res.data;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccess('账户已删除!');
|
||||||
|
await API.get('/api/user/logout');
|
||||||
|
userDispatch({ type: 'logout' });
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const bindWeChat = async () => {
|
const bindWeChat = async () => {
|
||||||
if (inputs.wechat_verification_code === '') return;
|
if (inputs.wechat_verification_code === '') return;
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
@@ -71,13 +112,26 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGoogleOAuth = () => {
|
||||||
|
window.open(
|
||||||
|
`https://accounts.google.com/o/oauth2/v2/auth?client_id=${status.google_client_id}&redirect_uri=${window.location.origin}/oauth/google&response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const openGitHubOAuth = () => {
|
const openGitHubOAuth = () => {
|
||||||
window.open(
|
window.open(
|
||||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDiscordOAuth = () => {
|
||||||
|
window.open(
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&response_type=code&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
const sendVerificationCode = async () => {
|
||||||
|
setDisableButton(true);
|
||||||
if (inputs.email === '') return;
|
if (inputs.email === '') return;
|
||||||
if (turnstileEnabled && turnstileToken === '') {
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
@@ -123,6 +177,9 @@ const PersonalSetting = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setShowAccountDeleteModal(true);
|
||||||
|
}}>删除个人账户</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>账号绑定</Header>
|
<Header as='h3'>账号绑定</Header>
|
||||||
{
|
{
|
||||||
@@ -170,6 +227,17 @@ const PersonalSetting = () => {
|
|||||||
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
status.discord_oauth && (
|
||||||
|
<Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
status.google_oauth && (
|
||||||
|
<Button onClick={openGoogleOAuth}>绑定 Google 账号</Button>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEmailBindModal(true);
|
setShowEmailBindModal(true);
|
||||||
@@ -195,8 +263,8 @@ const PersonalSetting = () => {
|
|||||||
name='email'
|
name='email'
|
||||||
type='email'
|
type='email'
|
||||||
action={
|
action={
|
||||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||||
获取验证码
|
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -230,6 +298,47 @@ const PersonalSetting = () => {
|
|||||||
</Modal.Description>
|
</Modal.Description>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
onClose={() => setShowAccountDeleteModal(false)}
|
||||||
|
onOpen={() => setShowAccountDeleteModal(true)}
|
||||||
|
open={showAccountDeleteModal}
|
||||||
|
size={'tiny'}
|
||||||
|
style={{ maxWidth: '450px' }}
|
||||||
|
>
|
||||||
|
<Modal.Header>确认删除自己的帐户</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Description>
|
||||||
|
<Form size='large'>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||||
|
name='self_account_deletion_confirmation'
|
||||||
|
value={inputs.self_account_deletion_confirmation}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
{turnstileEnabled ? (
|
||||||
|
<Turnstile
|
||||||
|
sitekey={turnstileSiteKey}
|
||||||
|
onVerify={(token) => {
|
||||||
|
setTurnstileToken(token);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
fluid
|
||||||
|
size='large'
|
||||||
|
onClick={deleteAccount}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal.Description>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,13 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Grid,
|
|
||||||
Header,
|
|
||||||
Image,
|
|
||||||
Message,
|
|
||||||
Segment,
|
|
||||||
} from 'semantic-ui-react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
@@ -18,7 +10,7 @@ const RegisterForm = () => {
|
|||||||
password: '',
|
password: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
email: '',
|
email: '',
|
||||||
verification_code: '',
|
verification_code: ''
|
||||||
});
|
});
|
||||||
const { username, password, password2 } = inputs;
|
const { username, password, password2 } = inputs;
|
||||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
||||||
@@ -178,7 +170,7 @@ const RegisterForm = () => {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
@@ -8,8 +8,11 @@ const SystemSetting = () => {
|
|||||||
PasswordRegisterEnabled: '',
|
PasswordRegisterEnabled: '',
|
||||||
EmailVerificationEnabled: '',
|
EmailVerificationEnabled: '',
|
||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
|
DiscordOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
DiscordClientId: '',
|
||||||
|
DiscordClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@@ -22,6 +25,9 @@ const SystemSetting = () => {
|
|||||||
WeChatServerAddress: '',
|
WeChatServerAddress: '',
|
||||||
WeChatServerToken: '',
|
WeChatServerToken: '',
|
||||||
WeChatAccountQRCodeImageURL: '',
|
WeChatAccountQRCodeImageURL: '',
|
||||||
|
GoogleOAuthEnabled: '',
|
||||||
|
GoogleClientId: '',
|
||||||
|
GoogleClientSecret: '',
|
||||||
TurnstileCheckEnabled: '',
|
TurnstileCheckEnabled: '',
|
||||||
TurnstileSiteKey: '',
|
TurnstileSiteKey: '',
|
||||||
TurnstileSecretKey: '',
|
TurnstileSecretKey: '',
|
||||||
@@ -56,7 +62,9 @@ const SystemSetting = () => {
|
|||||||
case 'PasswordRegisterEnabled':
|
case 'PasswordRegisterEnabled':
|
||||||
case 'EmailVerificationEnabled':
|
case 'EmailVerificationEnabled':
|
||||||
case 'GitHubOAuthEnabled':
|
case 'GitHubOAuthEnabled':
|
||||||
|
case 'DiscordOAuthEnabled':
|
||||||
case 'WeChatAuthEnabled':
|
case 'WeChatAuthEnabled':
|
||||||
|
case 'GoogleOAuthEnabled':
|
||||||
case 'TurnstileCheckEnabled':
|
case 'TurnstileCheckEnabled':
|
||||||
case 'RegisterEnabled':
|
case 'RegisterEnabled':
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||||
@@ -84,9 +92,13 @@ const SystemSetting = () => {
|
|||||||
name === 'ServerAddress' ||
|
name === 'ServerAddress' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'DiscordClientId' ||
|
||||||
|
name === 'DiscordClientSecret' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
|
name === 'GoogleClientId' ||
|
||||||
|
name === 'GoogleClientSecret' ||
|
||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey'
|
name === 'TurnstileSecretKey'
|
||||||
) {
|
) {
|
||||||
@@ -149,6 +161,18 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitGoogleOAuth = async () => {
|
||||||
|
if (originInputs['GoogleClientId'] !== inputs.GoogleClientId) {
|
||||||
|
await updateOption('GoogleClientId', inputs.GoogleClientId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['GoogleClientSecret'] !== inputs.GoogleClientSecret &&
|
||||||
|
inputs.GoogleClientSecret !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('GoogleClientSecret', inputs.GoogleClientSecret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitGitHubOAuth = async () => {
|
const submitGitHubOAuth = async () => {
|
||||||
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
||||||
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
||||||
@@ -161,6 +185,18 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitDiscordOAuth = async () => {
|
||||||
|
if (originInputs['DiscordClientId'] !== inputs.DiscordClientId) {
|
||||||
|
await updateOption('DiscordClientId', inputs.DiscordClientId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['DiscordClientSecret'] !== inputs.DiscordClientSecret &&
|
||||||
|
inputs.DiscordClientSecret !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('DiscordClientSecret', inputs.DiscordClientSecret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitTurnstile = async () => {
|
const submitTurnstile = async () => {
|
||||||
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
||||||
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
||||||
@@ -211,12 +247,24 @@ const SystemSetting = () => {
|
|||||||
name='EmailVerificationEnabled'
|
name='EmailVerificationEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.DiscordOAuthEnabled === 'true'}
|
||||||
|
label='允许通过 Discord 账户登录和注册'
|
||||||
|
name='DiscordOAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.GitHubOAuthEnabled === 'true'}
|
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||||
label='允许通过 GitHub 账户登录 & 注册'
|
label='允许通过 GitHub 账户登录 & 注册'
|
||||||
name='GitHubOAuthEnabled'
|
name='GitHubOAuthEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.GoogleOAuthEnabled === 'true'}
|
||||||
|
label='允许通过 Google 账户登录和注册'
|
||||||
|
name='GoogleOAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.WeChatAuthEnabled === 'true'}
|
checked={inputs.WeChatAuthEnabled === 'true'}
|
||||||
label='允许通过微信登录 & 注册'
|
label='允许通过微信登录 & 注册'
|
||||||
@@ -372,6 +420,82 @@ const SystemSetting = () => {
|
|||||||
保存 WeChat Server 设置
|
保存 WeChat Server 设置
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
配置 Discord OAuth 应用程序
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过 Discord 进行登录注册,
|
||||||
|
<a href='https://discord.com/developers/applications' target='_blank'>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的 Discord OAuth App
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,Authorization callback URL 填{' '}
|
||||||
|
<code>{`${inputs.ServerAddress}/oauth/discord`}</code>
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord 客户 ID'
|
||||||
|
name='DiscordClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordClientId}
|
||||||
|
placeholder='输入您注册的 Discord OAuth APP 的 ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord 客户秘密'
|
||||||
|
name='DiscordClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitDiscordOAuth}>
|
||||||
|
保存 Discord OAuth 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
配置 Google OAuth 应用程序
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过 Google 进行登录注册,
|
||||||
|
<a href='https://console.cloud.google.com/' target='_blank'>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的 Google OAuth App
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,Authorization callback URL 填{' '}
|
||||||
|
<code>{`${inputs.ServerAddress}/oauth/google`}</code>
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='Google 客户 ID'
|
||||||
|
name='GoogleClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.GoogleClientId}
|
||||||
|
placeholder='输入您注册的 Google OAuth APP 的 ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Google 客户秘密'
|
||||||
|
name='GoogleClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.GoogleClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitGoogleOAuth}>
|
||||||
|
保存 Google OAuth 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
配置 Turnstile
|
配置 Turnstile
|
||||||
<Header.Subheader>
|
<Header.Subheader>
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
export const CHANNEL_OPTIONS = [
|
export const CHANNEL_OPTIONS = [
|
||||||
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
|
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
|
||||||
{ key: 14, text: 'Anthropic', value: 14, color: 'black' },
|
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
|
||||||
{ key: 8, text: '自定义', value: 8, color: 'pink' },
|
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
|
||||||
{ key: 3, text: 'Azure', value: 3, color: 'olive' },
|
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
|
||||||
{ key: 11, text: 'PaLM', value: 11, color: 'orange' },
|
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
||||||
{ key: 15, text: 'Baidu', value: 15, color: 'blue' },
|
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
||||||
{ key: 2, text: 'API2D', value: 2, color: 'blue' },
|
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||||
{ key: 4, text: 'CloseAI', value: 4, color: 'teal' },
|
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||||
{ 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: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||||
{ key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
|
{ key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
|
||||||
{ key: 9, text: 'AI.LS', value: 9, color: 'yellow' },
|
{ key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
|
||||||
{ key: 10, text: 'AI Proxy', value: 10, color: 'purple' },
|
{ key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
|
||||||
{ key: 12, text: 'API2GPT', value: 12, color: 'blue' },
|
{ key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
|
||||||
{ key: 13, text: 'AIGC2D', value: 13, color: 'purple' }
|
{ key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
|
||||||
|
{ key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
|
||||||
];
|
];
|
@@ -46,9 +46,7 @@ const About = () => {
|
|||||||
about.startsWith('https://') ? <iframe
|
about.startsWith('https://') ? <iframe
|
||||||
src={about}
|
src={about}
|
||||||
style={{ width: '100%', height: '100vh', border: 'none' }}
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
/> : <Segment>
|
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
||||||
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
|
||||||
</Segment>
|
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,8 @@ const EditChannel = () => {
|
|||||||
base_url: '',
|
base_url: '',
|
||||||
other: '',
|
other: '',
|
||||||
model_mapping: '',
|
model_mapping: '',
|
||||||
|
allow_streaming: 1,
|
||||||
|
allow_non_streaming: 1,
|
||||||
models: [],
|
models: [],
|
||||||
groups: ['default']
|
groups: ['default']
|
||||||
};
|
};
|
||||||
@@ -94,6 +96,9 @@ const EditChannel = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let localModelOptions = [...originModelOptions];
|
let localModelOptions = [...originModelOptions];
|
||||||
|
if (!Array.isArray(inputs.models)) {
|
||||||
|
inputs.models = inputs.models.split(',');
|
||||||
|
}
|
||||||
inputs.models.forEach((model) => {
|
inputs.models.forEach((model) => {
|
||||||
if (!localModelOptions.find((option) => option.key === model)) {
|
if (!localModelOptions.find((option) => option.key === model)) {
|
||||||
localModelOptions.push({
|
localModelOptions.push({
|
||||||
@@ -127,6 +132,11 @@ const EditChannel = () => {
|
|||||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// allow streaming and allow non streaming cannot be both false
|
||||||
|
if (inputs.allow_streaming === 2 && inputs.allow_non_streaming === 2) {
|
||||||
|
showInfo('流式请求和非流式请求不能同时禁用!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
let localInputs = inputs;
|
let localInputs = inputs;
|
||||||
if (localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||||
@@ -176,7 +186,7 @@ const EditChannel = () => {
|
|||||||
<Message>
|
<Message>
|
||||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
|
注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
|
||||||
参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
|
参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
|
||||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
|
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
|
||||||
</Message>
|
</Message>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
@@ -215,26 +225,12 @@ const EditChannel = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='镜像'
|
|
||||||
name='base_url'
|
|
||||||
placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
required
|
required
|
||||||
name='name'
|
name='name'
|
||||||
placeholder={'请输入名称'}
|
placeholder={'请为渠道命名'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.name}
|
value={inputs.name}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -243,7 +239,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Dropdown
|
<Form.Dropdown
|
||||||
label='分组'
|
label='分组'
|
||||||
placeholder={'请选择分组'}
|
placeholder={'请选择可以使用该渠道的分组'}
|
||||||
name='groups'
|
name='groups'
|
||||||
required
|
required
|
||||||
fluid
|
fluid
|
||||||
@@ -260,7 +256,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Dropdown
|
<Form.Dropdown
|
||||||
label='模型'
|
label='模型'
|
||||||
placeholder={'请选择该通道所支持的模型'}
|
placeholder={'请选择该渠道所支持的模型'}
|
||||||
name='models'
|
name='models'
|
||||||
required
|
required
|
||||||
fluid
|
fluid
|
||||||
@@ -284,7 +280,7 @@ const EditChannel = () => {
|
|||||||
}}>清除所有模型</Button>
|
}}>清除所有模型</Button>
|
||||||
<Input
|
<Input
|
||||||
action={
|
action={
|
||||||
<Button type={'button'} onClick={()=>{
|
<Button type={'button'} onClick={() => {
|
||||||
if (customModel.trim() === "") return;
|
if (customModel.trim() === "") return;
|
||||||
if (inputs.models.includes(customModel)) return;
|
if (inputs.models.includes(customModel)) return;
|
||||||
let localModels = [...inputs.models];
|
let localModels = [...inputs.models];
|
||||||
@@ -295,7 +291,7 @@ const EditChannel = () => {
|
|||||||
text: customModel,
|
text: customModel,
|
||||||
value: customModel,
|
value: customModel,
|
||||||
});
|
});
|
||||||
setModelOptions(modelOptions=>{
|
setModelOptions(modelOptions => {
|
||||||
return [...modelOptions, ...localModelOptions];
|
return [...modelOptions, ...localModelOptions];
|
||||||
});
|
});
|
||||||
setCustomModel('');
|
setCustomModel('');
|
||||||
@@ -312,7 +308,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='模型映射'
|
label='模型映射'
|
||||||
placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||||
name='model_mapping'
|
name='model_mapping'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.model_mapping}
|
value={inputs.model_mapping}
|
||||||
@@ -320,6 +316,26 @@ const EditChannel = () => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.allow_streaming === 1}
|
||||||
|
label='允许流式请求'
|
||||||
|
name='allow_streaming'
|
||||||
|
onChange={() => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, allow_streaming: inputs.allow_streaming === 1 ? 2 : 1 }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.allow_non_streaming === 1}
|
||||||
|
label='允许非流式请求'
|
||||||
|
name='allow_non_streaming'
|
||||||
|
onChange={() => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, allow_non_streaming: inputs.allow_non_streaming === 1 ? 2 : 1 }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
{
|
{
|
||||||
batch ? <Form.Field>
|
batch ? <Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
@@ -337,7 +353,7 @@ const EditChannel = () => {
|
|||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
required
|
required
|
||||||
placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入密钥'}
|
placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入渠道对应的鉴权密钥'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -354,6 +370,20 @@ const EditChannel = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
inputs.type !== 3 && inputs.type !== 8 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='镜像'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
|
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
@@ -97,12 +97,24 @@ const Home = () => {
|
|||||||
? '已启用'
|
? '已启用'
|
||||||
: '未启用'}
|
: '未启用'}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Discord 身份验证:
|
||||||
|
{statusState?.status?.discord_oauth === true
|
||||||
|
? '已启用'
|
||||||
|
: '未启用'}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
微信身份验证:
|
微信身份验证:
|
||||||
{statusState?.status?.wechat_login === true
|
{statusState?.status?.wechat_login === true
|
||||||
? '已启用'
|
? '已启用'
|
||||||
: '未启用'}
|
: '未启用'}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Google 身份验证:
|
||||||
|
{statusState?.status?.google_oauth === true
|
||||||
|
? '已启用'
|
||||||
|
: '未启用'}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Turnstile 用户校验:
|
Turnstile 用户校验:
|
||||||
{statusState?.status?.turnstile_check === true
|
{statusState?.status?.turnstile_check === true
|
||||||
|
@@ -7,24 +7,32 @@ const TopUp = () => {
|
|||||||
const [redemptionCode, setRedemptionCode] = useState('');
|
const [redemptionCode, setRedemptionCode] = useState('');
|
||||||
const [topUpLink, setTopUpLink] = useState('');
|
const [topUpLink, setTopUpLink] = useState('');
|
||||||
const [userQuota, setUserQuota] = useState(0);
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const topUp = async () => {
|
const topUp = async () => {
|
||||||
if (redemptionCode === '') {
|
if (redemptionCode === '') {
|
||||||
showInfo('请输入充值码!')
|
showInfo('请输入充值码!')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await API.post('/api/user/topup', {
|
setIsSubmitting(true);
|
||||||
key: redemptionCode
|
try {
|
||||||
});
|
const res = await API.post('/api/user/topup', {
|
||||||
const { success, message, data } = res.data;
|
key: redemptionCode
|
||||||
if (success) {
|
|
||||||
showSuccess('充值成功!');
|
|
||||||
setUserQuota((quota) => {
|
|
||||||
return quota + data;
|
|
||||||
});
|
});
|
||||||
setRedemptionCode('');
|
const { success, message, data } = res.data;
|
||||||
} else {
|
if (success) {
|
||||||
showError(message);
|
showSuccess('充值成功!');
|
||||||
|
setUserQuota((quota) => {
|
||||||
|
return quota + data;
|
||||||
|
});
|
||||||
|
setRedemptionCode('');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError('请求失败');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,8 +82,8 @@ const TopUp = () => {
|
|||||||
<Button color='green' onClick={openTopUpLink}>
|
<Button color='green' onClick={openTopUpLink}>
|
||||||
获取兑换码
|
获取兑换码
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='yellow' onClick={topUp}>
|
<Button color='yellow' onClick={topUp} disabled={isSubmitting}>
|
||||||
充值
|
{isSubmitting ? '兑换中...' : '兑换'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
@@ -92,5 +100,4 @@ const TopUp = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default TopUp;
|
||||||
export default TopUp;
|
|
@@ -13,13 +13,15 @@ const EditUser = () => {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
password: '',
|
password: '',
|
||||||
github_id: '',
|
github_id: '',
|
||||||
|
discord_id: '',
|
||||||
wechat_id: '',
|
wechat_id: '',
|
||||||
|
google_id: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: 0,
|
quota: 0,
|
||||||
group: 'default'
|
group: 'default'
|
||||||
});
|
});
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
const { username, display_name, password, github_id, wechat_id, email, quota, group } =
|
const { username, display_name, password, github_id, wechat_id, email, quota, google_id, discord_id } =
|
||||||
inputs;
|
inputs;
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
@@ -166,6 +168,26 @@ const EditUser = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='已绑定的 Discord 账户'
|
||||||
|
name='discord_id'
|
||||||
|
value={discord_id}
|
||||||
|
autoComplete='new-password'
|
||||||
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='已绑定的 Google 账户'
|
||||||
|
name='google_id'
|
||||||
|
value={google_id}
|
||||||
|
autoComplete='new-password'
|
||||||
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='已绑定的邮箱账户'
|
label='已绑定的邮箱账户'
|
||||||
|
Reference in New Issue
Block a user