mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			82 Commits
		
	
	
		
			v0.4.8-alp
			...
			0.4.10
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e2f5c1eb8c | ||
| 
						 | 
					d68aa4c96f | ||
| 
						 | 
					47cb77de53 | ||
| 
						 | 
					61912f5e2c | ||
| 
						 | 
					379d03798c | ||
| 
						 | 
					520eb34b72 | ||
| 
						 | 
					855bb82ae7 | ||
| 
						 | 
					8c91bd9c97 | ||
| 
						 | 
					7c7a45a4f5 | ||
| 
						 | 
					0ac0214c41 | ||
| 
						 | 
					b63400ebe2 | ||
| 
						 | 
					b17d9bc649 | ||
| 
						 | 
					9ef8167e5d | ||
| 
						 | 
					3baad1d926 | ||
| 
						 | 
					80d5d6edfb | ||
| 
						 | 
					12365ccf69 | ||
| 
						 | 
					4928319494 | ||
| 
						 | 
					839dcc3ab2 | ||
| 
						 | 
					270e366cd9 | ||
| 
						 | 
					67b8e82457 | ||
| 
						 | 
					4b2cb573b6 | ||
| 
						 | 
					bde43cc358 | ||
| 
						 | 
					6e06dcfcf8 | ||
| 
						 | 
					b617599211 | ||
| 
						 | 
					1656b9a1de | ||
| 
						 | 
					eb5f8f2d75 | ||
| 
						 | 
					2ae5741214 | ||
| 
						 | 
					28d58849a0 | ||
| 
						 | 
					adc9679d56 | ||
| 
						 | 
					07589ae305 | ||
| 
						 | 
					95bc32c555 | ||
| 
						 | 
					d61dc4a9ca | ||
| 
						 | 
					b29acb0c89 | ||
| 
						 | 
					a8e418275d | ||
| 
						 | 
					d7ab9b0935 | ||
| 
						 | 
					ebd62c3bfc | ||
| 
						 | 
					4e6f9f67b3 | ||
| 
						 | 
					77d295bbf5 | ||
| 
						 | 
					7e3e25fbd9 | ||
| 
						 | 
					9bf98ab53a | ||
| 
						 | 
					3ed9a219c7 | ||
| 
						 | 
					37d7afcedc | ||
| 
						 | 
					2756554f7c | ||
| 
						 | 
					6215d2e71c | ||
| 
						 | 
					d17bdc40a7 | ||
| 
						 | 
					280df27705 | ||
| 
						 | 
					991f5bf4ee | ||
| 
						 | 
					701aaba191 | ||
| 
						 | 
					3bab5b48bf | ||
| 
						 | 
					f3bccee3b5 | ||
| 
						 | 
					d84b0b0f5d | ||
| 
						 | 
					d383302e8a | ||
| 
						 | 
					04f40def2f | ||
| 
						 | 
					c48b7bc0f5 | ||
| 
						 | 
					b09daf5ec1 | ||
| 
						 | 
					c90c0ecef4 | ||
| 
						 | 
					1ab5fb7d2d | ||
| 
						 | 
					f769711c19 | ||
| 
						 | 
					edc5156693 | ||
| 
						 | 
					9ec6506c32 | ||
| 
						 | 
					f387cc5ead | ||
| 
						 | 
					569b68c43b | ||
| 
						 | 
					f0c40a6cd0 | ||
| 
						 | 
					0cea9e6a6f | ||
| 
						 | 
					b1b3651e84 | ||
| 
						 | 
					8f6bd51f58 | ||
| 
						 | 
					bddbf57104 | ||
| 
						 | 
					9a16b0f9e5 | ||
| 
						 | 
					3530309a31 | ||
| 
						 | 
					733ebc067b | ||
| 
						 | 
					6a8567ac14 | ||
| 
						 | 
					aabc546691 | ||
| 
						 | 
					1c82b06f35 | ||
| 
						 | 
					9e4109672a | ||
| 
						 | 
					64c35334e6 | ||
| 
						 | 
					0ce572b405 | ||
| 
						 | 
					a326ac4b28 | ||
| 
						 | 
					05b0e77839 | ||
| 
						 | 
					51f19470bc | ||
| 
						 | 
					737672fb0b | ||
| 
						 | 
					0941e294bf | ||
| 
						 | 
					431d505f79 | 
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -8,11 +8,13 @@ assignees: ''
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**例行检查**
 | 
			
		||||
 | 
			
		||||
[//]: # (方框内删除已有的空格,填 x 号)
 | 
			
		||||
+ [ ] 我已确认目前没有类似 issue
 | 
			
		||||
+ [ ] 我已确认我已升级到最新版本
 | 
			
		||||
+ [ ] 我已完整查看过项目 README,尤其是常见问题部分
 | 
			
		||||
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 
 | 
			
		||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭
 | 
			
		||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
 | 
			
		||||
 | 
			
		||||
**问题描述**
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							@@ -6,6 +6,3 @@ contact_links:
 | 
			
		||||
  - name: 赞赏支持
 | 
			
		||||
    url: https://iamazing.cn/page/reward
 | 
			
		||||
    about: 请作者喝杯咖啡,以激励作者持续开发
 | 
			
		||||
  - name: 付费部署或定制功能
 | 
			
		||||
    url: https://openai.justsong.cn/
 | 
			
		||||
    about: 加群后联系群主
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							@@ -8,10 +8,13 @@ assignees: ''
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**例行检查**
 | 
			
		||||
 | 
			
		||||
[//]: # (方框内删除已有的空格,填 x 号)
 | 
			
		||||
+ [ ] 我已确认目前没有类似 issue
 | 
			
		||||
+ [ ] 我已确认我已升级到最新版本
 | 
			
		||||
+ [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求
 | 
			
		||||
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
 | 
			
		||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭
 | 
			
		||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
 | 
			
		||||
 | 
			
		||||
**功能描述**
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api-en
 | 
			
		||||
            ckt1031/one-api-en
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
        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
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api
 | 
			
		||||
            ckt1031/one-api
 | 
			
		||||
            ghcr.io/${{ github.repository }}
 | 
			
		||||
 | 
			
		||||
      - 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
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api
 | 
			
		||||
            ckt1031/one-api
 | 
			
		||||
            ghcr.io/${{ github.repository }}
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -4,4 +4,5 @@ upload
 | 
			
		||||
*.exe
 | 
			
		||||
*.db
 | 
			
		||||
build
 | 
			
		||||
*.db-journal
 | 
			
		||||
*.db-journal
 | 
			
		||||
.env*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,31 +1,29 @@
 | 
			
		||||
FROM node:16 as builder
 | 
			
		||||
 | 
			
		||||
# Node build stage
 | 
			
		||||
FROM node:18 as builder
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY ./web/package*.json ./
 | 
			
		||||
RUN npm ci
 | 
			
		||||
COPY ./web .
 | 
			
		||||
COPY ./VERSION .
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
 | 
			
		||||
# Go build stage
 | 
			
		||||
FROM golang AS builder2
 | 
			
		||||
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
    GOOS=linux
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY go.mod .
 | 
			
		||||
COPY go.sum .
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=builder /build/build ./web/build
 | 
			
		||||
RUN go mod download
 | 
			
		||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
 | 
			
		||||
# Final stage
 | 
			
		||||
FROM alpine
 | 
			
		||||
 | 
			
		||||
RUN apk update \
 | 
			
		||||
    && apk upgrade \
 | 
			
		||||
    && apk add --no-cache ca-certificates tzdata \
 | 
			
		||||
    && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
 | 
			
		||||
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
COPY --from=builder2 /build/one-api /
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
ENTRYPOINT ["/one-api"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								README.en.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								README.en.md
									
									
									
									
									
								
							@@ -57,17 +57,14 @@ _✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
 | 
			
		||||
> **Note**: The latest image pulled from Docker may be an `alpha` release. Specify the version manually if you require stability.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
1. Supports multiple API access channels. Welcome PRs or issue submissions for additional channels:
 | 
			
		||||
1. Supports multiple API access channels:
 | 
			
		||||
    + [x] Official OpenAI channel (support proxy configuration)
 | 
			
		||||
    + [x] **Azure OpenAI API**
 | 
			
		||||
    + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
 | 
			
		||||
    + [x] [OpenAI-SB](https://openai-sb.com)
 | 
			
		||||
    + [x] [API2D](https://api2d.com/r/197971)
 | 
			
		||||
    + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
 | 
			
		||||
    + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (invitation code: `OneAPI`)
 | 
			
		||||
    + [x] [API2GPT](http://console.api2gpt.com/m/00002S)
 | 
			
		||||
    + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
 | 
			
		||||
    + [x] [AI.LS](https://ai.ls)
 | 
			
		||||
    + [x] [OpenAI Max](https://openaimax.com)
 | 
			
		||||
    + [x] Custom channel: Various third-party proxy services not included in the list
 | 
			
		||||
2. Supports access to multiple channels through **load balancing**.
 | 
			
		||||
3. Supports **stream mode** that enables typewriter-like effect through stream transmission.
 | 
			
		||||
@@ -174,6 +171,15 @@ Refer to [#175](https://github.com/songquanpeng/one-api/issues/175) for detailed
 | 
			
		||||
If you encounter a blank page after deployment, refer to [#97](https://github.com/songquanpeng/one-api/issues/97) for possible solutions.
 | 
			
		||||
 | 
			
		||||
### Deployment on Third-Party Platforms
 | 
			
		||||
<details>
 | 
			
		||||
<summary><strong>Deploy on Sealos</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md).
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary><strong>Deployment on Zeabur</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
@@ -240,7 +246,7 @@ If the channel ID is not provided, load balancing will be used to distribute the
 | 
			
		||||
    + Example: `CHANNEL_UPDATE_FREQUENCY=1440`
 | 
			
		||||
8. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen.
 | 
			
		||||
    + Example: `CHANNEL_TEST_FREQUENCY=1440`
 | 
			
		||||
9. `REQUEST_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval.
 | 
			
		||||
9. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval.
 | 
			
		||||
    + Example: `POLLING_INTERVAL=5`
 | 
			
		||||
 | 
			
		||||
### Command Line Parameters
 | 
			
		||||
@@ -279,6 +285,10 @@ If the channel ID is not provided, load balancing will be used to distribute the
 | 
			
		||||
## Note
 | 
			
		||||
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
 | 
			
		||||
 | 
			
		||||
This project is open-sourced under the MIT license. One must somehow retain the copyright information of One API.
 | 
			
		||||
This project is released under the MIT license. Based on this, attribution and a link to this project must be included at the bottom of the page.
 | 
			
		||||
 | 
			
		||||
The same applies to derivative projects based on this project.
 | 
			
		||||
 | 
			
		||||
If you do not wish to include attribution, prior authorization must be obtained.
 | 
			
		||||
 | 
			
		||||
According to the MIT license, users should bear the risk and responsibility of using this project, and the developer of this open-source project is not responsible for this.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								README.md
									
									
									
									
									
								
							@@ -51,27 +51,33 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
			
		||||
  <a href="https://iamazing.cn/page/reward">赞赏支持</a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
> **Note**:本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
 | 
			
		||||
 | 
			
		||||
> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
 | 
			
		||||
 | 
			
		||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
 | 
			
		||||
 | 
			
		||||
## 👍Forks 特殊功能⚡
 | 
			
		||||
 | 
			
		||||
1. 增强的**频道测试**以提高稳定性。
 | 
			
		||||
2. 支持 **Dall-E 2 模型图像生成** API。
 | 
			
		||||
3. 修复**登录页面**中缺少的 **Turnstile 验证码**。
 | 
			
		||||
 | 
			
		||||
## 功能
 | 
			
		||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
 | 
			
		||||
   + [x] OpenAI 官方通道(支持配置代理)
 | 
			
		||||
1. 支持多种 API 访问渠道:
 | 
			
		||||
   + [x] OpenAI 官方通道(支持配置镜像)
 | 
			
		||||
   + [x] **Azure OpenAI API**
 | 
			
		||||
   + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
 | 
			
		||||
   + [x] [OpenAI-SB](https://openai-sb.com)
 | 
			
		||||
   + [x] [API2D](https://api2d.com/r/197971)
 | 
			
		||||
   + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
 | 
			
		||||
   + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
 | 
			
		||||
   + [x] [API2GPT](http://console.api2gpt.com/m/00002S)
 | 
			
		||||
   + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
 | 
			
		||||
   + [x] [AI.LS](https://ai.ls)
 | 
			
		||||
   + [x] [OpenAI Max](https://openaimax.com)
 | 
			
		||||
   + [x] 自定义渠道:例如各种未收录的第三方代理服务
 | 
			
		||||
2. 支持通过**负载均衡**的方式访问多个渠道。
 | 
			
		||||
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
 | 
			
		||||
4. 支持**多机部署**,[详见此处](#多机部署)。
 | 
			
		||||
5. 支持**令牌管理**,设置令牌的过期时间和使用次数。
 | 
			
		||||
5. 支持**令牌管理**,设置令牌的过期时间和额度。
 | 
			
		||||
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
 | 
			
		||||
7. 支持**通道管理**,批量创建通道。
 | 
			
		||||
8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
 | 
			
		||||
@@ -80,21 +86,26 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
			
		||||
11. 支持**用户邀请奖励**。
 | 
			
		||||
12. 支持以美元为单位显示额度。
 | 
			
		||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
			
		||||
14. 支持丰富的**自定义**设置,
 | 
			
		||||
14. 支持模型映射,重定向用户的请求模型。
 | 
			
		||||
15. 支持丰富的**自定义**设置,
 | 
			
		||||
    1. 支持自定义系统名称,logo 以及页脚。
 | 
			
		||||
    2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
 | 
			
		||||
15. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
16. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
17. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
16. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
17. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
18. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
    + 邮箱登录注册以及通过邮箱进行密码重置。
 | 
			
		||||
    + [GitHub 开放授权](https://github.com/settings/applications/new)。
 | 
			
		||||
    + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
 | 
			
		||||
18. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
			
		||||
19. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
			
		||||
 | 
			
		||||
## 部署
 | 
			
		||||
### 基于 Docker 进行部署
 | 
			
		||||
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
 | 
			
		||||
 | 
			
		||||
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
 | 
			
		||||
 | 
			
		||||
如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
 | 
			
		||||
 | 
			
		||||
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
 | 
			
		||||
 | 
			
		||||
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
 | 
			
		||||
@@ -114,6 +125,7 @@ server{
 | 
			
		||||
          proxy_set_header X-Forwarded-For $remote_addr;
 | 
			
		||||
          proxy_cache_bypass $http_upgrade;
 | 
			
		||||
          proxy_set_header Accept-Encoding gzip;
 | 
			
		||||
          proxy_read_timeout 300s;  # GPT-4 需要较长的超时时间,请自行调整
 | 
			
		||||
   }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
@@ -159,8 +171,8 @@ sudo service nginx restart
 | 
			
		||||
### 多机部署
 | 
			
		||||
1. 所有服务器 `SESSION_SECRET` 设置一样的值。
 | 
			
		||||
2. 必须设置 `SQL_DSN`,使用 MySQL 数据库而非 SQLite,所有服务器连接同一个数据库。
 | 
			
		||||
3. 所有从服务器必须设置 `NODE_TYPE` 为 `slave`。
 | 
			
		||||
4. 设置 `SYNC_FREQUENCY` 后服务器将定期从数据库同步配置。
 | 
			
		||||
3. 所有从服务器必须设置 `NODE_TYPE` 为 `slave`,不设置则默认为主服务器。
 | 
			
		||||
4. 设置 `SYNC_FREQUENCY` 后服务器将定期从数据库同步配置,在使用远程数据库的情况下,推荐设置该项并启用 Redis,无论主从。
 | 
			
		||||
5. 从服务器可以选择设置 `FRONTEND_BASE_URL`,以重定向页面请求到主服务器。
 | 
			
		||||
6. 从服务器上**分别**装好 Redis,设置好 `REDIS_CONN_STRING`,这样可以做到在缓存未过期的情况下数据库零访问,可以减少延迟。
 | 
			
		||||
7. 如果主服务器访问数据库延迟也比较高,则也需要启用 Redis,并设置 `SYNC_FREQUENCY`,以定期从数据库同步配置。
 | 
			
		||||
@@ -183,7 +195,7 @@ sudo service nginx restart
 | 
			
		||||
docker run --name chat-next-web -d -p 3001:3000 yidadaa/chatgpt-next-web
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
注意修改端口号和 `BASE_URL`。
 | 
			
		||||
注意修改端口号,之后在页面上设置接口地址(例如:https://openai.justsong.cn/ )和 API Key 即可。
 | 
			
		||||
 | 
			
		||||
#### ChatGPT Web
 | 
			
		||||
项目主页:https://github.com/Chanzhaoyu/chatgpt-web
 | 
			
		||||
@@ -195,6 +207,17 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 | 
			
		||||
注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。
 | 
			
		||||
 | 
			
		||||
### 部署到第三方平台
 | 
			
		||||
<details>
 | 
			
		||||
<summary><strong>部署到 Sealos </strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
> Sealos 可视化部署,仅需 1 分钟。
 | 
			
		||||
 | 
			
		||||
参考这个[教程](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md)中 1~5 步。
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary><strong>部署到 Zeabur</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
@@ -221,6 +244,8 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 | 
			
		||||
 | 
			
		||||
等到系统启动后,使用 `root` 用户登录系统并做进一步的配置。
 | 
			
		||||
 | 
			
		||||
**Note**:如果你不知道某个配置项的含义,可以临时删掉值以看到进一步的提示文字。
 | 
			
		||||
 | 
			
		||||
## 使用方法
 | 
			
		||||
在`渠道`页面中添加你的 API Key,之后在`令牌`页面中新增访问令牌。
 | 
			
		||||
 | 
			
		||||
@@ -251,7 +276,10 @@ graph LR
 | 
			
		||||
   + 例子:`SESSION_SECRET=random_string`
 | 
			
		||||
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 8.0 版本。
 | 
			
		||||
   + 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
 | 
			
		||||
4. `FRONTEND_BASE_URL`:设置之后将使用指定的前端地址,而非后端地址。
 | 
			
		||||
   + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
 | 
			
		||||
   + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
 | 
			
		||||
   + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
 | 
			
		||||
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
 | 
			
		||||
   + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
 | 
			
		||||
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
 | 
			
		||||
   + 例子:`SYNC_FREQUENCY=60`
 | 
			
		||||
@@ -261,7 +289,7 @@ graph LR
 | 
			
		||||
   + 例子:`CHANNEL_UPDATE_FREQUENCY=1440`
 | 
			
		||||
8. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。
 | 
			
		||||
   + 例子:`CHANNEL_TEST_FREQUENCY=1440`
 | 
			
		||||
9. `REQUEST_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
 | 
			
		||||
9. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
 | 
			
		||||
   + 例子:`POLLING_INTERVAL=5`
 | 
			
		||||
 | 
			
		||||
### 命令行参数
 | 
			
		||||
@@ -298,13 +326,16 @@ https://openai.justsong.cn
 | 
			
		||||
5. ChatGPT Next Web 报错:`Failed to fetch`
 | 
			
		||||
   + 部署的时候不要设置 `BASE_URL`。
 | 
			
		||||
   + 检查你的接口地址和 API Key 有没有填对。
 | 
			
		||||
6. 报错:`当前分组负载已饱和,请稍后再试`
 | 
			
		||||
   + 上游通道 429 了。
 | 
			
		||||
 | 
			
		||||
## 相关项目
 | 
			
		||||
[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库
 | 
			
		||||
 | 
			
		||||
## 注意
 | 
			
		||||
本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
 | 
			
		||||
 | 
			
		||||
本项目使用 MIT 协议进行开源,请以某种方式保留 One API 的版权信息。
 | 
			
		||||
本项目使用 MIT 协议进行开源,**在此基础上**,必须在页面底部保留署名以及指向本项目的链接。如果不想保留署名,必须首先获得授权。
 | 
			
		||||
 | 
			
		||||
同样适用于基于本项目的二开项目。
 | 
			
		||||
 | 
			
		||||
依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
if [ $# -ne 3 ]; then
 | 
			
		||||
  echo "Usage: time_test.sh <domain> <key> <count>"
 | 
			
		||||
if [ $# -lt 3 ]; then
 | 
			
		||||
  echo "Usage: time_test.sh <domain> <key> <count> [<model>]"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
domain=$1
 | 
			
		||||
key=$2
 | 
			
		||||
count=$3
 | 
			
		||||
model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo
 | 
			
		||||
 | 
			
		||||
total_time=0
 | 
			
		||||
times=()
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +18,7 @@ for ((i=1; i<=count; i++)); do
 | 
			
		||||
           https://"$domain"/v1/chat/completions \
 | 
			
		||||
           -H "Content-Type: application/json" \
 | 
			
		||||
           -H "Authorization: Bearer $key" \
 | 
			
		||||
           -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}')
 | 
			
		||||
           -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
 | 
			
		||||
  http_code=$(echo "$result" | awk '{print $1}')
 | 
			
		||||
  time=$(echo "$result" | awk '{print $2}')
 | 
			
		||||
  echo "HTTP status code: $http_code, Time taken: $time"
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ var PasswordLoginEnabled = true
 | 
			
		||||
var PasswordRegisterEnabled = true
 | 
			
		||||
var EmailVerificationEnabled = false
 | 
			
		||||
var GitHubOAuthEnabled = false
 | 
			
		||||
var DiscordOAuthEnabled = false
 | 
			
		||||
var WeChatAuthEnabled = false
 | 
			
		||||
var TurnstileCheckEnabled = false
 | 
			
		||||
var RegisterEnabled = true
 | 
			
		||||
@@ -53,6 +54,9 @@ var SMTPToken = ""
 | 
			
		||||
var GitHubClientId = ""
 | 
			
		||||
var GitHubClientSecret = ""
 | 
			
		||||
 | 
			
		||||
var DiscordClientId = ""
 | 
			
		||||
var DiscordClientSecret = ""
 | 
			
		||||
 | 
			
		||||
var WeChatServerAddress = ""
 | 
			
		||||
var WeChatServerToken = ""
 | 
			
		||||
var WeChatAccountQRCodeImageURL = ""
 | 
			
		||||
@@ -67,12 +71,13 @@ var ChannelDisableThreshold = 5.0
 | 
			
		||||
var AutomaticDisableChannelEnabled = false
 | 
			
		||||
var QuotaRemindThreshold = 1000
 | 
			
		||||
var PreConsumedQuota = 500
 | 
			
		||||
var ApproximateTokenEnabled = false
 | 
			
		||||
 | 
			
		||||
var RootUserEmail = ""
 | 
			
		||||
 | 
			
		||||
var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
 | 
			
		||||
 | 
			
		||||
var requestInterval, _ = strconv.Atoi(os.Getenv("REQUEST_INTERVAL"))
 | 
			
		||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
 | 
			
		||||
var RequestInterval = time.Duration(requestInterval) * time.Second
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -148,20 +153,22 @@ const (
 | 
			
		||||
	ChannelTypeAIProxy   = 10
 | 
			
		||||
	ChannelTypePaLM      = 11
 | 
			
		||||
	ChannelTypeAPI2GPT   = 12
 | 
			
		||||
	ChannelTypeAIGC2D    = 13
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChannelBaseURLs = []string{
 | 
			
		||||
	"",                             // 0
 | 
			
		||||
	"https://api.openai.com",       // 1
 | 
			
		||||
	"https://oa.api2d.net",         // 2
 | 
			
		||||
	"",                             // 3
 | 
			
		||||
	"https://api.openai-proxy.org", // 4
 | 
			
		||||
	"https://api.openai-sb.com",    // 5
 | 
			
		||||
	"https://api.openaimax.com",    // 6
 | 
			
		||||
	"https://api.ohmygpt.com",      // 7
 | 
			
		||||
	"",                             // 8
 | 
			
		||||
	"https://api.caipacity.com",    // 9
 | 
			
		||||
	"https://api.aiproxy.io",       // 10
 | 
			
		||||
	"",                             // 11
 | 
			
		||||
	"https://api.api2gpt.com",      // 12
 | 
			
		||||
	"",                              // 0
 | 
			
		||||
	"https://api.openai.com",        // 1
 | 
			
		||||
	"https://oa.api2d.net",          // 2
 | 
			
		||||
	"",                              // 3
 | 
			
		||||
	"https://api.closeai-proxy.xyz", // 4
 | 
			
		||||
	"https://api.openai-sb.com",     // 5
 | 
			
		||||
	"https://api.openaimax.com",     // 6
 | 
			
		||||
	"https://api.ohmygpt.com",       // 7
 | 
			
		||||
	"",                              // 8
 | 
			
		||||
	"https://api.caipacity.com",     // 9
 | 
			
		||||
	"https://api.aiproxy.io",        // 10
 | 
			
		||||
	"",                              // 11
 | 
			
		||||
	"https://api.api2gpt.com",       // 12
 | 
			
		||||
	"https://api.aigc2d.com",        // 13
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"text-search-ada-doc-001": 10,
 | 
			
		||||
	"text-moderation-stable":  0.1,
 | 
			
		||||
	"text-moderation-latest":  0.1,
 | 
			
		||||
	"dall-e":                  8,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ModelRatio2JSONString() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,23 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetSubscription(c *gin.Context) {
 | 
			
		||||
	var quota int
 | 
			
		||||
	var err error
 | 
			
		||||
	var token *model.Token
 | 
			
		||||
	var expirationDate int64
 | 
			
		||||
 | 
			
		||||
	tokenId := c.GetInt("token_id")
 | 
			
		||||
	token, err := model.GetTokenById(tokenId)
 | 
			
		||||
 | 
			
		||||
	expirationDate = token.ExpiredTime
 | 
			
		||||
 | 
			
		||||
	if common.DisplayTokenStatEnabled {
 | 
			
		||||
		tokenId := c.GetInt("token_id")
 | 
			
		||||
		token, err = model.GetTokenById(tokenId)
 | 
			
		||||
		quota = token.RemainQuota
 | 
			
		||||
	} else {
 | 
			
		||||
		userId := c.GetInt("id")
 | 
			
		||||
@@ -41,6 +46,7 @@ func GetSubscription(c *gin.Context) {
 | 
			
		||||
		SoftLimitUSD:       amount,
 | 
			
		||||
		HardLimitUSD:       amount,
 | 
			
		||||
		SystemHardLimitUSD: amount,
 | 
			
		||||
		AccessUntil:        expirationDate,
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(200, subscription)
 | 
			
		||||
	return
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ type OpenAISubscriptionResponse struct {
 | 
			
		||||
	SoftLimitUSD       float64 `json:"soft_limit_usd"`
 | 
			
		||||
	HardLimitUSD       float64 `json:"hard_limit_usd"`
 | 
			
		||||
	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
 | 
			
		||||
	AccessUntil        int64   `json:"access_until"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAIUsageDailyCost struct {
 | 
			
		||||
@@ -32,6 +33,13 @@ type OpenAIUsageDailyCost struct {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAICreditGrants struct {
 | 
			
		||||
	Object         string  `json:"object"`
 | 
			
		||||
	TotalGranted   float64 `json:"total_granted"`
 | 
			
		||||
	TotalUsed      float64 `json:"total_used"`
 | 
			
		||||
	TotalAvailable float64 `json:"total_available"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAIUsageResponse struct {
 | 
			
		||||
	Object string `json:"object"`
 | 
			
		||||
	//DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"`
 | 
			
		||||
@@ -61,6 +69,14 @@ type API2GPTUsageResponse struct {
 | 
			
		||||
	TotalRemaining float64 `json:"total_remaining"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type APGC2DGPTUsageResponse struct {
 | 
			
		||||
	//Grants         interface{} `json:"grants"`
 | 
			
		||||
	Object         string  `json:"object"`
 | 
			
		||||
	TotalAvailable float64 `json:"total_available"`
 | 
			
		||||
	TotalGranted   float64 `json:"total_granted"`
 | 
			
		||||
	TotalUsed      float64 `json:"total_used"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAuthHeader get auth header
 | 
			
		||||
func GetAuthHeader(token string) http.Header {
 | 
			
		||||
	h := http.Header{}
 | 
			
		||||
@@ -81,6 +97,9 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if res.StatusCode != http.StatusOK {
 | 
			
		||||
		return nil, fmt.Errorf("status code: %d", res.StatusCode)
 | 
			
		||||
	}
 | 
			
		||||
	body, err := io.ReadAll(res.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -92,6 +111,22 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
 | 
			
		||||
	return body, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateChannelCloseAIBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
	url := fmt.Sprintf("%s/dashboard/billing/credit_grants", channel.BaseURL)
 | 
			
		||||
	body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	response := OpenAICreditGrants{}
 | 
			
		||||
	err = json.Unmarshal(body, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	channel.UpdateBalance(response.TotalAvailable)
 | 
			
		||||
	return response.TotalAvailable, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
	url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key)
 | 
			
		||||
	body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
 | 
			
		||||
@@ -150,8 +185,26 @@ func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
	return response.TotalRemaining, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
	url := "https://api.aigc2d.com/dashboard/billing/credit_grants"
 | 
			
		||||
	body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	response := APGC2DGPTUsageResponse{}
 | 
			
		||||
	err = json.Unmarshal(body, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	channel.UpdateBalance(response.TotalAvailable)
 | 
			
		||||
	return response.TotalAvailable, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateChannelBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channel.Type]
 | 
			
		||||
	if channel.BaseURL == "" {
 | 
			
		||||
		channel.BaseURL = baseURL
 | 
			
		||||
	}
 | 
			
		||||
	switch channel.Type {
 | 
			
		||||
	case common.ChannelTypeOpenAI:
 | 
			
		||||
		if channel.BaseURL != "" {
 | 
			
		||||
@@ -161,12 +214,16 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
 | 
			
		||||
		return 0, errors.New("尚未实现")
 | 
			
		||||
	case common.ChannelTypeCustom:
 | 
			
		||||
		baseURL = channel.BaseURL
 | 
			
		||||
	case common.ChannelTypeCloseAI:
 | 
			
		||||
		return updateChannelCloseAIBalance(channel)
 | 
			
		||||
	case common.ChannelTypeOpenAISB:
 | 
			
		||||
		return updateChannelOpenAISBBalance(channel)
 | 
			
		||||
	case common.ChannelTypeAIProxy:
 | 
			
		||||
		return updateChannelAIProxyBalance(channel)
 | 
			
		||||
	case common.ChannelTypeAPI2GPT:
 | 
			
		||||
		return updateChannelAPI2GPTBalance(channel)
 | 
			
		||||
	case common.ChannelTypeAIGC2D:
 | 
			
		||||
		return updateChannelAIGC2DBalance(channel)
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, errors.New("尚未实现")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,20 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func testChannel(channel *model.Channel, request ChatRequest) error {
 | 
			
		||||
@@ -50,26 +53,99 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		// Prinnt the body in string
 | 
			
		||||
		buf := new(bytes.Buffer)
 | 
			
		||||
		buf.ReadFrom(resp.Body)
 | 
			
		||||
		return errors.New("error response: " + strconv.Itoa(resp.StatusCode) + " " + buf.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var done = false
 | 
			
		||||
	var streamResponseText = ""
 | 
			
		||||
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if i := strings.Index(string(data), "\n\n"); i >= 0 {
 | 
			
		||||
			return i + 2, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	for scanner.Scan() {
 | 
			
		||||
		data := scanner.Text()
 | 
			
		||||
		if len(data) < 6 { // must be something wrong!
 | 
			
		||||
			common.SysError("invalid stream response: " + data)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// If data has event: event content inside, remove it, it can be prefix or inside the data
 | 
			
		||||
		if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
 | 
			
		||||
			// Remove event: event in the front or back
 | 
			
		||||
			data = strings.TrimPrefix(data, "event: event")
 | 
			
		||||
			data = strings.TrimSuffix(data, "event: event")
 | 
			
		||||
			// Remove everything, only keep `data: {...}` <--- this is the json
 | 
			
		||||
			// Find the start and end indices of `data: {...}` substring
 | 
			
		||||
			startIndex := strings.Index(data, "data:")
 | 
			
		||||
			endIndex := strings.LastIndex(data, "}")
 | 
			
		||||
 | 
			
		||||
			// If both indices are found and end index is greater than start index
 | 
			
		||||
			if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
 | 
			
		||||
				// Extract the `data: {...}` substring
 | 
			
		||||
				data = data[startIndex : endIndex+1]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Trim whitespace and newlines from the modified data string
 | 
			
		||||
			data = strings.TrimSpace(data)
 | 
			
		||||
		}
 | 
			
		||||
		if !strings.HasPrefix(data, "data:") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		data = data[6:]
 | 
			
		||||
		if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
			var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
			err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// Prinnt the body in string
 | 
			
		||||
				buf := new(bytes.Buffer)
 | 
			
		||||
				buf.ReadFrom(resp.Body)
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error() + " " + buf.String())
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			for _, choice := range streamResponse.Choices {
 | 
			
		||||
				streamResponseText += choice.Delta.Content
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			done = true
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	var response TextResponse
 | 
			
		||||
	err = json.NewDecoder(resp.Body).Decode(&response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if response.Usage.CompletionTokens == 0 {
 | 
			
		||||
		return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
 | 
			
		||||
 | 
			
		||||
	// Check if streaming is complete and streamResponseText is populated
 | 
			
		||||
	if streamResponseText == "" || !done {
 | 
			
		||||
		return errors.New("Streaming not complete")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildTestRequest() *ChatRequest {
 | 
			
		||||
	testRequest := &ChatRequest{
 | 
			
		||||
		Model:     "", // this will be set later
 | 
			
		||||
		MaxTokens: 1,
 | 
			
		||||
		Model:  "", // this will be set later
 | 
			
		||||
		Stream: true,
 | 
			
		||||
	}
 | 
			
		||||
	testMessage := Message{
 | 
			
		||||
		Role:    "user",
 | 
			
		||||
		Content: "hi",
 | 
			
		||||
		Content: "say hi word only",
 | 
			
		||||
	}
 | 
			
		||||
	testRequest.Messages = append(testRequest.Messages, testMessage)
 | 
			
		||||
	return testRequest
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										195
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								controller/discord.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,195 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	disgoauth "github.com/realTristan/disgoauth"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DiscordOAuthResponse struct {
 | 
			
		||||
	AccessToken string `json:"access_token"`
 | 
			
		||||
	Scope       string `json:"scope"`
 | 
			
		||||
	TokenType   string `json:"token_type"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DiscordUser struct {
 | 
			
		||||
	Id       string `json:"id"`
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDiscordUserInfoByCode(codeFromURLParamaters string, host string) (*DiscordUser, error) {
 | 
			
		||||
	if codeFromURLParamaters == "" {
 | 
			
		||||
		return nil, errors.New("Invalid parameter")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Establish a new discord client
 | 
			
		||||
	var dc *disgoauth.Client = disgoauth.Init(&disgoauth.Client{
 | 
			
		||||
		ClientID:     common.DiscordClientId,
 | 
			
		||||
		ClientSecret: common.DiscordClientSecret,
 | 
			
		||||
		RedirectURI:  fmt.Sprintf("https://%s/oauth/discord", host),
 | 
			
		||||
		Scopes:       []string{disgoauth.ScopeIdentify, disgoauth.ScopeEmail},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	accessToken, _ := dc.GetOnlyAccessToken(codeFromURLParamaters)
 | 
			
		||||
 | 
			
		||||
	// Get the authorized user's data using the above accessToken
 | 
			
		||||
	userData, _ := disgoauth.GetUserData(accessToken)
 | 
			
		||||
 | 
			
		||||
	// Create a new DiscordUser
 | 
			
		||||
	var discordUser DiscordUser
 | 
			
		||||
 | 
			
		||||
	// Decode the userData map[string]interface{} into the discordUser
 | 
			
		||||
	// Convert the map to JSON
 | 
			
		||||
	jsonData, _ := json.Marshal(userData)
 | 
			
		||||
 | 
			
		||||
	// Convert the JSON to a struct
 | 
			
		||||
	err := json.Unmarshal(jsonData, &discordUser)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if discordUser.Username == "" {
 | 
			
		||||
		return nil, errors.New("Invalid return value, user field is empty, please try again later!")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &discordUser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DiscordOAuth(c *gin.Context) {
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	username := session.Get("username")
 | 
			
		||||
	if username != nil {
 | 
			
		||||
		DiscordBind(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !common.DiscordOAuthEnabled {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "管理员未开启通过 Discord 登录以及注册",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	code := c.Query("code")
 | 
			
		||||
	host := c.Request.Host
 | 
			
		||||
	discordUser, err := getDiscordUserInfoByCode(code, host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		DiscordId: discordUser.Id,
 | 
			
		||||
	}
 | 
			
		||||
	if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
 | 
			
		||||
		err := user.FillUserByDiscordId()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": err.Error(),
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if common.RegisterEnabled {
 | 
			
		||||
			user.Username = "discord_" + strconv.Itoa(model.GetMaxUserId()+1)
 | 
			
		||||
			if discordUser.Username != "" {
 | 
			
		||||
				user.DisplayName = discordUser.Username
 | 
			
		||||
			} else {
 | 
			
		||||
				user.DisplayName = "Discord User"
 | 
			
		||||
			}
 | 
			
		||||
			user.Role = common.RoleCommonUser
 | 
			
		||||
			user.Status = common.UserStatusEnabled
 | 
			
		||||
 | 
			
		||||
			if err := user.Insert(0); err != nil {
 | 
			
		||||
				c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
					"success": false,
 | 
			
		||||
					"message": err.Error(),
 | 
			
		||||
				})
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "管理员关闭了新用户注册",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Status != common.UserStatusEnabled {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"message": "用户已被封禁",
 | 
			
		||||
			"success": false,
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	setupLogin(&user, c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DiscordBind(c *gin.Context) {
 | 
			
		||||
	if !common.DiscordOAuthEnabled {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "管理员未开启通过 Discord 登录以及注册",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	code := c.Query("code")
 | 
			
		||||
	discordUser, err := getDiscordUserInfoByCode(code, c.Request.Host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		DiscordId: discordUser.Id,
 | 
			
		||||
	}
 | 
			
		||||
	if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "该 Discord 账户已被绑定",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	id := session.Get("id")
 | 
			
		||||
	// id := c.GetInt("id")  // critical bug!
 | 
			
		||||
	user.Id = id.(int)
 | 
			
		||||
	err = user.FillUserById()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user.DiscordId = discordUser.Id
 | 
			
		||||
	err = user.Update(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "bind",
 | 
			
		||||
	})
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@@ -5,13 +5,14 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type GitHubOAuthResponse struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,11 @@ package controller
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetStatus(c *gin.Context) {
 | 
			
		||||
@@ -19,6 +20,8 @@ func GetStatus(c *gin.Context) {
 | 
			
		||||
			"email_verification":  common.EmailVerificationEnabled,
 | 
			
		||||
			"github_oauth":        common.GitHubOAuthEnabled,
 | 
			
		||||
			"github_client_id":    common.GitHubClientId,
 | 
			
		||||
			"discord_oauth":       common.DiscordOAuthEnabled,
 | 
			
		||||
			"discord_client_id":   common.DiscordClientId,
 | 
			
		||||
			"system_name":         common.SystemName,
 | 
			
		||||
			"logo":                common.Logo,
 | 
			
		||||
			"footer_html":         common.Footer,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -53,6 +54,15 @@ func init() {
 | 
			
		||||
	})
 | 
			
		||||
	// https://platform.openai.com/docs/models/model-endpoint-compatibility
 | 
			
		||||
	openAIModels = []OpenAIModels{
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "dall-e",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "openai",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "dall-e",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "gpt-3.5-turbo",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,12 @@ package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetOptions(c *gin.Context) {
 | 
			
		||||
@@ -41,6 +42,14 @@ func UpdateOption(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	switch option.Key {
 | 
			
		||||
	case "DiscordOAuthEnabled":
 | 
			
		||||
		if option.Value == "true" && common.DiscordClientId == "" {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	case "GitHubOAuthEnabled":
 | 
			
		||||
		if option.Value == "true" && common.GitHubClientId == "" {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,181 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	// TODO: this part is not finished
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, c.Request.RequestURI, c.Request.Body)
 | 
			
		||||
	imageModel := "dall-e"
 | 
			
		||||
 | 
			
		||||
	tokenId := c.GetInt("token_id")
 | 
			
		||||
	channelType := c.GetInt("channel")
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	consumeQuota := c.GetBool("consume_quota")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
 | 
			
		||||
	var textRequest GeneralOpenAIRequest
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
		err := common.UnmarshalBodyReusable(c, &textRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt validation
 | 
			
		||||
	if textRequest.Prompt == "" {
 | 
			
		||||
		return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Not "256x256", "512x512", or "1024x1024"
 | 
			
		||||
	if textRequest.Size != "" && textRequest.Size != "256x256" && textRequest.Size != "512x512" && textRequest.Size != "1024x1024" {
 | 
			
		||||
		return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// N should between 1 to 10
 | 
			
		||||
	if textRequest.N != 0 && (textRequest.N < 1 || textRequest.N > 10) {
 | 
			
		||||
		return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// map model name
 | 
			
		||||
	modelMapping := c.GetString("model_mapping")
 | 
			
		||||
	isModelMapped := false
 | 
			
		||||
	if modelMapping != "" {
 | 
			
		||||
		modelMap := make(map[string]string)
 | 
			
		||||
		err := json.Unmarshal([]byte(modelMapping), &modelMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		if modelMap[imageModel] != "" {
 | 
			
		||||
			imageModel = modelMap[imageModel]
 | 
			
		||||
			isModelMapped = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
			
		||||
	requestURL := c.Request.URL.String()
 | 
			
		||||
 | 
			
		||||
	if c.GetString("base_url") != "" {
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
			
		||||
 | 
			
		||||
	var requestBody io.Reader
 | 
			
		||||
	if isModelMapped {
 | 
			
		||||
		jsonStr, err := json.Marshal(textRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		requestBody = c.Request.Body
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	modelRatio := common.GetModelRatio(imageModel)
 | 
			
		||||
	groupRatio := common.GetGroupRatio(group)
 | 
			
		||||
	ratio := modelRatio * groupRatio
 | 
			
		||||
	userQuota, err := model.CacheGetUserQuota(userId)
 | 
			
		||||
 | 
			
		||||
	sizeRatio := 1.0
 | 
			
		||||
	// Size
 | 
			
		||||
	if textRequest.Size == "256x256" {
 | 
			
		||||
		sizeRatio = 1
 | 
			
		||||
	} else if textRequest.Size == "512x512" {
 | 
			
		||||
		sizeRatio = 1.125
 | 
			
		||||
	} else if textRequest.Size == "1024x1024" {
 | 
			
		||||
		sizeRatio = 1.25
 | 
			
		||||
	}
 | 
			
		||||
	quota := int(ratio * sizeRatio * 1000)
 | 
			
		||||
 | 
			
		||||
	if consumeQuota && userQuota-quota < 0 {
 | 
			
		||||
		return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
			
		||||
	req.Header.Set("Accept", c.Request.Header.Get("Accept"))
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "do_request_failed", http.StatusOK)
 | 
			
		||||
		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = req.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_request_body_failed", http.StatusOK)
 | 
			
		||||
		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	err = c.Request.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	var textResponse ImageResponse
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if consumeQuota {
 | 
			
		||||
			err := model.PostConsumeTokenQuota(tokenId, quota)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			err = model.CacheUpdateUserQuota(userId)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error update user quota cache: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			if quota != 0 {
 | 
			
		||||
				tokenName := c.GetString("token_name")
 | 
			
		||||
				logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
 | 
			
		||||
				model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent)
 | 
			
		||||
				model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 | 
			
		||||
				channelId := c.GetInt("channel_id")
 | 
			
		||||
				model.UpdateChannelUsedQuota(channelId, quota)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
		responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		err = json.Unmarshal(responseBody, &textResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range resp.Header {
 | 
			
		||||
		c.Writer.Header().Set(k, v[0])
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(c.Writer, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "copy_response_body_failed", http.StatusOK)
 | 
			
		||||
		return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusOK)
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,14 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
@@ -53,6 +55,20 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			return errorWrapper(errors.New("field instruction is required"), "required_field_missing", http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// map model name
 | 
			
		||||
	modelMapping := c.GetString("model_mapping")
 | 
			
		||||
	isModelMapped := false
 | 
			
		||||
	if modelMapping != "" {
 | 
			
		||||
		modelMap := make(map[string]string)
 | 
			
		||||
		err := json.Unmarshal([]byte(modelMapping), &modelMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		if modelMap[textRequest.Model] != "" {
 | 
			
		||||
			textRequest.Model = modelMap[textRequest.Model]
 | 
			
		||||
			isModelMapped = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
			
		||||
	requestURL := c.Request.URL.String()
 | 
			
		||||
	if c.GetString("base_url") != "" {
 | 
			
		||||
@@ -114,7 +130,38 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body)
 | 
			
		||||
	var requestBody io.Reader
 | 
			
		||||
	if isModelMapped {
 | 
			
		||||
		jsonStr, err := json.Marshal(textRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		bodyBytes, err := io.ReadAll(c.Request.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "read_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		var bodyMap map[string]interface{}
 | 
			
		||||
		err = json.Unmarshal(bodyBytes, &bodyMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Add "stream":true to body map if it doesn't exist
 | 
			
		||||
		if _, exists := bodyMap["stream"]; !exists {
 | 
			
		||||
			bodyMap["stream"] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Marshal the body map back into JSON
 | 
			
		||||
		bodyBytes, err = json.Marshal(bodyMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		requestBody = bytes.NewBuffer(bodyBytes)
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
@@ -127,12 +174,25 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
			
		||||
	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 := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		// Print Data if Error
 | 
			
		||||
		bodyBytes, err := io.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		bodyString := string(bodyBytes)
 | 
			
		||||
 | 
			
		||||
		log.Printf("Error: %s", bodyString)
 | 
			
		||||
 | 
			
		||||
		return errorWrapper(err, "request_failed", resp.StatusCode)
 | 
			
		||||
	}
 | 
			
		||||
	err = req.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
@@ -177,6 +237,10 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			err = model.CacheUpdateUserQuota(userId)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error update user quota cache: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			if quota != 0 {
 | 
			
		||||
				tokenName := c.GetString("token_name")
 | 
			
		||||
				logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
 | 
			
		||||
@@ -214,6 +278,28 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
					common.SysError("invalid stream response: " + data)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				// If data has event: event content inside, remove it, it can be prefix or inside the data
 | 
			
		||||
				if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
 | 
			
		||||
					// Remove event: event in the front or back
 | 
			
		||||
					data = strings.TrimPrefix(data, "event: event")
 | 
			
		||||
					data = strings.TrimSuffix(data, "event: event")
 | 
			
		||||
					// Remove everything, only keep `data: {...}` <--- this is the json
 | 
			
		||||
					// Find the start and end indices of `data: {...}` substring
 | 
			
		||||
					startIndex := strings.Index(data, "data:")
 | 
			
		||||
					endIndex := strings.LastIndex(data, "}")
 | 
			
		||||
 | 
			
		||||
					// If both indices are found and end index is greater than start index
 | 
			
		||||
					if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
 | 
			
		||||
						// Extract the `data: {...}` substring
 | 
			
		||||
						data = data[startIndex : endIndex+1]
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// Trim whitespace and newlines from the modified data string
 | 
			
		||||
					data = strings.TrimSpace(data)
 | 
			
		||||
				}
 | 
			
		||||
				if !strings.HasPrefix(data, "data:") {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				dataChan <- data
 | 
			
		||||
				data = data[6:]
 | 
			
		||||
				if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/pkoukk/tiktoken-go"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
 | 
			
		||||
@@ -25,6 +24,13 @@ func getTokenEncoder(model string) *tiktoken.Tiktoken {
 | 
			
		||||
	return tokenEncoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int {
 | 
			
		||||
	if common.ApproximateTokenEnabled {
 | 
			
		||||
		return int(float64(len(text)) * 0.38)
 | 
			
		||||
	}
 | 
			
		||||
	return len(tokenEncoder.Encode(text, nil, nil))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func countTokenMessages(messages []Message, model string) int {
 | 
			
		||||
	tokenEncoder := getTokenEncoder(model)
 | 
			
		||||
	// Reference:
 | 
			
		||||
@@ -34,12 +40,9 @@ func countTokenMessages(messages []Message, model string) int {
 | 
			
		||||
	// Every message follows <|start|>{role/name}\n{content}<|end|>\n
 | 
			
		||||
	var tokensPerMessage int
 | 
			
		||||
	var tokensPerName int
 | 
			
		||||
	if strings.HasPrefix(model, "gpt-3.5") {
 | 
			
		||||
	if model == "gpt-3.5-turbo-0301" {
 | 
			
		||||
		tokensPerMessage = 4
 | 
			
		||||
		tokensPerName = -1 // If there's a name, the role is omitted
 | 
			
		||||
	} else if strings.HasPrefix(model, "gpt-4") {
 | 
			
		||||
		tokensPerMessage = 3
 | 
			
		||||
		tokensPerName = 1
 | 
			
		||||
	} else {
 | 
			
		||||
		tokensPerMessage = 3
 | 
			
		||||
		tokensPerName = 1
 | 
			
		||||
@@ -47,11 +50,11 @@ func countTokenMessages(messages []Message, model string) int {
 | 
			
		||||
	tokenNum := 0
 | 
			
		||||
	for _, message := range messages {
 | 
			
		||||
		tokenNum += tokensPerMessage
 | 
			
		||||
		tokenNum += len(tokenEncoder.Encode(message.Content, nil, nil))
 | 
			
		||||
		tokenNum += len(tokenEncoder.Encode(message.Role, nil, nil))
 | 
			
		||||
		tokenNum += getTokenNum(tokenEncoder, message.Content)
 | 
			
		||||
		tokenNum += getTokenNum(tokenEncoder, message.Role)
 | 
			
		||||
		if message.Name != nil {
 | 
			
		||||
			tokenNum += tokensPerName
 | 
			
		||||
			tokenNum += len(tokenEncoder.Encode(*message.Name, nil, nil))
 | 
			
		||||
			tokenNum += getTokenNum(tokenEncoder, *message.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	tokenNum += 3 // Every reply is primed with <|start|>assistant<|message|>
 | 
			
		||||
@@ -74,8 +77,7 @@ func countTokenInput(input any, model string) int {
 | 
			
		||||
 | 
			
		||||
func countTokenText(text string, model string) int {
 | 
			
		||||
	tokenEncoder := getTokenEncoder(model)
 | 
			
		||||
	token := tokenEncoder.Encode(text, nil, nil)
 | 
			
		||||
	return len(token)
 | 
			
		||||
	return getTokenNum(tokenEncoder, text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,11 @@ package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
@@ -27,22 +28,24 @@ const (
 | 
			
		||||
// https://platform.openai.com/docs/api-reference/chat
 | 
			
		||||
 | 
			
		||||
type GeneralOpenAIRequest struct {
 | 
			
		||||
	Model       string    `json:"model"`
 | 
			
		||||
	Messages    []Message `json:"messages"`
 | 
			
		||||
	Prompt      any       `json:"prompt"`
 | 
			
		||||
	Stream      bool      `json:"stream"`
 | 
			
		||||
	MaxTokens   int       `json:"max_tokens"`
 | 
			
		||||
	Temperature float64   `json:"temperature"`
 | 
			
		||||
	TopP        float64   `json:"top_p"`
 | 
			
		||||
	N           int       `json:"n"`
 | 
			
		||||
	Input       any       `json:"input"`
 | 
			
		||||
	Instruction string    `json:"instruction"`
 | 
			
		||||
	Model       string    `json:"model,omitempty"`
 | 
			
		||||
	Messages    []Message `json:"messages,omitempty"`
 | 
			
		||||
	Prompt      any       `json:"prompt,omitempty"`
 | 
			
		||||
	Stream      bool      `json:"stream,omitempty"`
 | 
			
		||||
	MaxTokens   int       `json:"max_tokens,omitempty"`
 | 
			
		||||
	Temperature float64   `json:"temperature,omitempty"`
 | 
			
		||||
	TopP        float64   `json:"top_p,omitempty"`
 | 
			
		||||
	N           int       `json:"n,omitempty"`
 | 
			
		||||
	Input       any       `json:"input,omitempty"`
 | 
			
		||||
	Instruction string    `json:"instruction,omitempty"`
 | 
			
		||||
	Size        string    `json:"size,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatRequest struct {
 | 
			
		||||
	Model     string    `json:"model"`
 | 
			
		||||
	Messages  []Message `json:"messages"`
 | 
			
		||||
	MaxTokens int       `json:"max_tokens"`
 | 
			
		||||
	MaxTokens *int      `json:"max_tokens,omitempty"`
 | 
			
		||||
	Stream    bool      `json:"stream"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TextRequest struct {
 | 
			
		||||
@@ -76,6 +79,13 @@ type TextResponse struct {
 | 
			
		||||
	Error OpenAIError `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageResponse struct {
 | 
			
		||||
	Created int `json:"created"`
 | 
			
		||||
	Data    []struct {
 | 
			
		||||
		Url string `json:"url"`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatCompletionsStreamResponse struct {
 | 
			
		||||
	Choices []struct {
 | 
			
		||||
		Delta struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -180,10 +180,10 @@ func UpdateToken(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if token.Status == common.TokenStatusEnabled {
 | 
			
		||||
		if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() {
 | 
			
		||||
		if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "令牌已过期,无法启用,请先修改令牌过期时间",
 | 
			
		||||
				"message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,13 @@ package controller
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LoginRequest struct {
 | 
			
		||||
@@ -477,6 +478,16 @@ func DeleteUser(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
func DeleteSelf(c *gin.Context) {
 | 
			
		||||
	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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								english.dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								english.dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
# Initial stage
 | 
			
		||||
FROM python:3.11 as translator
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
 | 
			
		||||
 | 
			
		||||
# Node build stage
 | 
			
		||||
FROM node:18-alpine as nodeBuilder
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY ./web/package*.json ./
 | 
			
		||||
RUN npm ci
 | 
			
		||||
COPY --from=translator /app .
 | 
			
		||||
RUN cd web && REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
 | 
			
		||||
# Go build stage
 | 
			
		||||
FROM golang:1.20.5 AS goBuilder
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
    GOOS=linux
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY go.mod .
 | 
			
		||||
COPY go.sum .
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY --from=translator /app .
 | 
			
		||||
COPY --from=nodeBuilder /build/web/build ./web/build
 | 
			
		||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
 | 
			
		||||
# Final stage
 | 
			
		||||
FROM alpine:latest
 | 
			
		||||
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
COPY --from=goBuilder /build/one-api /
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
ENTRYPOINT ["/one-api"]
 | 
			
		||||
							
								
								
									
										34
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								go.mod
									
									
									
									
									
								
							@@ -9,28 +9,28 @@ require (
 | 
			
		||||
	github.com/gin-contrib/sessions v0.0.5
 | 
			
		||||
	github.com/gin-contrib/static v0.0.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/google/uuid v1.3.0
 | 
			
		||||
	github.com/pkoukk/tiktoken-go v0.1.1
 | 
			
		||||
	golang.org/x/crypto v0.9.0
 | 
			
		||||
	gorm.io/driver/mysql v1.4.3
 | 
			
		||||
	gorm.io/driver/sqlite v1.4.3
 | 
			
		||||
	gorm.io/gorm v1.24.0
 | 
			
		||||
	github.com/pkoukk/tiktoken-go v0.1.4
 | 
			
		||||
	golang.org/x/crypto v0.11.0
 | 
			
		||||
	gorm.io/driver/mysql v1.5.1
 | 
			
		||||
	gorm.io/driver/sqlite v1.5.2
 | 
			
		||||
	gorm.io/gorm v1.25.2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
 | 
			
		||||
	github.com/bytedance/sonic v1.9.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 | 
			
		||||
	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/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/gin-contrib/sse v0.1.0 // indirect
 | 
			
		||||
	github.com/go-playground/locales v0.14.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/gomodule/redigo v2.0.0+incompatible // indirect
 | 
			
		||||
	github.com/gorilla/context v1.1.1 // indirect
 | 
			
		||||
@@ -38,20 +38,22 @@ require (
 | 
			
		||||
	github.com/gorilla/sessions v1.2.1 // indirect
 | 
			
		||||
	github.com/jinzhu/inflection v1.0.0 // indirect
 | 
			
		||||
	github.com/jinzhu/now v1.1.5 // indirect
 | 
			
		||||
	github.com/joho/godotenv v1.5.1
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/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/mattn/go-isatty v0.0.19 // 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/reflect2 v1.0.2 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 | 
			
		||||
	github.com/realTristan/disgoauth v1.0.2
 | 
			
		||||
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.11 // indirect
 | 
			
		||||
	golang.org/x/arch v0.3.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.8.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.9.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.30.0 // indirect
 | 
			
		||||
	golang.org/x/arch v0.4.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.11.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.31.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								go.sum
									
									
									
									
									
								
							@@ -3,8 +3,12 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
 | 
			
		||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 | 
			
		||||
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
 | 
			
		||||
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 | 
			
		||||
@@ -16,6 +20,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
 | 
			
		||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 | 
			
		||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
 | 
			
		||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 | 
			
		||||
@@ -47,15 +53,23 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
 | 
			
		||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
			
		||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 | 
			
		||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
			
		||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
			
		||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
 | 
			
		||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 | 
			
		||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 | 
			
		||||
@@ -75,12 +89,16 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
 | 
			
		||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 | 
			
		||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
			
		||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 | 
			
		||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
			
		||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
			
		||||
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.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
 | 
			
		||||
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 | 
			
		||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 | 
			
		||||
@@ -115,8 +133,14 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc
 | 
			
		||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.4 h1:bniMzWdUvNO6YkRbASo2x5qJf2LAG/TIJojqz+Igm8E=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.4/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3 h1:x3LgcvujjG+mx8PUMfPmwn3tcu2aA95uCB6ilGGObWk=
 | 
			
		||||
github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3/go.mod h1:P/mZMYLZ87lqRSECEWsOqywGrO1hlZkk9RTwEw35IP4=
 | 
			
		||||
github.com/realTristan/disgoauth v1.0.2 h1:dfto2Kf1gFlZsf8XuwRNoemLgk+hGn/TJpSdtMrEh8E=
 | 
			
		||||
github.com/realTristan/disgoauth v1.0.2/go.mod h1:t72aRaWMq2gknUZcKONReJlEYFod5sHC86WCJ0X9GxA=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 | 
			
		||||
@@ -144,34 +168,56 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
 | 
			
		||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
 | 
			
		||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
 | 
			
		||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
 | 
			
		||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 | 
			
		||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
 | 
			
		||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 | 
			
		||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 | 
			
		||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 | 
			
		||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
 | 
			
		||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 | 
			
		||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
 | 
			
		||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-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-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-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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 | 
			
		||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 | 
			
		||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 | 
			
		||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 | 
			
		||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
 | 
			
		||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 | 
			
		||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
			
		||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
			
		||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
			
		||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 | 
			
		||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
 | 
			
		||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
			
		||||
@@ -188,9 +234,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
 | 
			
		||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
 | 
			
		||||
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
 | 
			
		||||
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
 | 
			
		||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
 | 
			
		||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
 | 
			
		||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 | 
			
		||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
 | 
			
		||||
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=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								i18n/en.json
									
									
									
									
									
								
							@@ -36,7 +36,7 @@
 | 
			
		||||
  "通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
 | 
			
		||||
  "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.",
 | 
			
		||||
  "令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20",
 | 
			
		||||
  "令牌已过期,无法启用,请先修改令牌过期时间": "The token has expired and cannot be enabled. Please modify the token expiration time first",
 | 
			
		||||
  "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
 | 
			
		||||
  "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
 | 
			
		||||
  "管理员关闭了密码登录": "The administrator has turned off password login",
 | 
			
		||||
  "无法保存会话信息,请重试": "Unable to save session information, please try again",
 | 
			
		||||
@@ -107,6 +107,11 @@
 | 
			
		||||
  "已禁用": "Disabled",
 | 
			
		||||
  "未知状态": "Unknown status",
 | 
			
		||||
  " 秒": "s",
 | 
			
		||||
  " 分钟 ": " m ",
 | 
			
		||||
  " 小时 ": " h ",
 | 
			
		||||
  " 天 ": " d ",
 | 
			
		||||
  " 个月 ": " M ",
 | 
			
		||||
  " 年 ": " y ",
 | 
			
		||||
  "未测试": "Not tested",
 | 
			
		||||
  "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
 | 
			
		||||
  "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.",
 | 
			
		||||
@@ -438,6 +443,7 @@
 | 
			
		||||
  "显示名称": "Display Name",
 | 
			
		||||
  "请输入新的显示名称": "Please enter a new display name",
 | 
			
		||||
  "已绑定的 GitHub 账户": "GitHub Account Bound",
 | 
			
		||||
  "已绑定的 Discord 账户": "Discord Account Bound",
 | 
			
		||||
  "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
 | 
			
		||||
  "已绑定的微信账户": "WeChat Account Bound",
 | 
			
		||||
  "已绑定的邮箱账户": "Email Account Bound",
 | 
			
		||||
@@ -456,5 +462,66 @@
 | 
			
		||||
  "提示": "Prompt",
 | 
			
		||||
  "补全": "Completion",
 | 
			
		||||
  "消耗额度": "Used Quota",
 | 
			
		||||
  "可选值": "Optional Values"
 | 
			
		||||
  "可选值": "Optional Values",
 | 
			
		||||
  "渠道不存在:%d": "Channel does not exist: %d",
 | 
			
		||||
  "数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator",
 | 
			
		||||
  "使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load",
 | 
			
		||||
  "请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!",
 | 
			
		||||
  "请至少选择一个Model!": "Please select at least one Model!",
 | 
			
		||||
  "加载首页内容失败": "Failed to load the homepage content",
 | 
			
		||||
  "加载关于内容失败": "Failed to load the About content",
 | 
			
		||||
  "兑换码更新成功!": "Redemption code updated successfully!",
 | 
			
		||||
  "兑换码创建成功!": "Redemption code created successfully!",
 | 
			
		||||
  "用户账户创建成功!": "User account created successfully!",
 | 
			
		||||
  "生成数量": "Generate quantity",
 | 
			
		||||
  "请输入生成数量": "Please enter the quantity to generate",
 | 
			
		||||
  "创建新用户账户": "Create new user account",
 | 
			
		||||
  "渠道更新成功!": "Channel updated successfully!",
 | 
			
		||||
  "渠道创建成功!": "Channel created successfully!",
 | 
			
		||||
  "请选择分组": "Please select a group",
 | 
			
		||||
  "更新兑换码信息": "Update redemption code information",
 | 
			
		||||
  "创建新的兑换码": "Create a new redemption code",
 | 
			
		||||
  "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group ratio in the system settings page to add a new group:",
 | 
			
		||||
  "未找到所请求的页面": "The requested page was not found",
 | 
			
		||||
  "过期时间格式错误!": "Expiration time format error!",
 | 
			
		||||
  "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
 | 
			
		||||
  "此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
 | 
			
		||||
  "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
 | 
			
		||||
  "模型映射": "Model mapping",
 | 
			
		||||
  "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
 | 
			
		||||
  "默认": "Default",
 | 
			
		||||
  "图片演示": "Image demo",
 | 
			
		||||
  "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
 | 
			
		||||
  "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
 | 
			
		||||
  "取消无限额度": "Cancel unlimited quota",
 | 
			
		||||
  "请输入新的剩余额度": "Please enter the new remaining quota",
 | 
			
		||||
  "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
 | 
			
		||||
  "请输入用户名": "Please enter username",
 | 
			
		||||
  "请输入显示名称": "Please enter display name",
 | 
			
		||||
  "请输入密码": "Please enter password",
 | 
			
		||||
  "模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name",
 | 
			
		||||
  ",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body",
 | 
			
		||||
  "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
 | 
			
		||||
  "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
 | 
			
		||||
  "Homepage URL 填": "Fill in the Homepage URL",
 | 
			
		||||
  "Authorization callback URL 填": "Fill in the Authorization callback URL",
 | 
			
		||||
  "允许通过 Discord 账户登录和注册": "Allow login and registration via Discord account",
 | 
			
		||||
  "Discord 身份验证": "Discord Authentication",
 | 
			
		||||
  "确认文字": "Confirmation Text",
 | 
			
		||||
  "请输入 \"CONFIRM\" 以删除您的帐户。": "Please enter \"CONFIRM\" to delete your account.",
 | 
			
		||||
  "请确认您要删除账户!": "Please confirm that you want to delete the account!",
 | 
			
		||||
  "账户已删除!": "Account deleted!",
 | 
			
		||||
  "您是否确认删除自己的帐户?": "Are you sure you want to delete your account?",
 | 
			
		||||
  "配置 Discord OAuth App": "Configure Discord OAuth App",
 | 
			
		||||
  "管理你的 Discord OAuth App": "Manage your Discord OAuth App",
 | 
			
		||||
  "输入你注册的 Discord OAuth APP 的 ID": "Enter the ID of your registered Discord OAuth APP",
 | 
			
		||||
  "保存 Discord OAuth 设置": "Save Discord OAuth Settings",
 | 
			
		||||
  "删除个人账户": "Delete personal account",
 | 
			
		||||
  "绑定 Discord 账号": "Bind Discord account",
 | 
			
		||||
  "无权将其他用户权限等级提升到大于等于自己的权限等级": "You are not allowed to upgrade the permission level of other users to greater than or equal to your own permission level",
 | 
			
		||||
  "无权删除超级管理员": "You are not allowed to delete super administrators",
 | 
			
		||||
  "该 Discord 账户已被绑定": "The Discord account has been bound",
 | 
			
		||||
  "管理员未开启通过 Discord 登录以及注册": "The administrator has not enabled login and registration via Discord",
 | 
			
		||||
  "无法启用 Discord OAuth,请先填入 Discord Client ID 以及 Discord Client Secret!": "Unable to enable Discord OAuth, please fill in the Discord Client ID and Discord Client Secret first!",
 | 
			
		||||
  "兑换失败,": "Redemption failed, "
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								main.go
									
									
									
									
									
								
							@@ -2,10 +2,6 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-contrib/sessions/cookie"
 | 
			
		||||
	"github.com/gin-contrib/sessions/redis"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/controller"
 | 
			
		||||
	"one-api/middleware"
 | 
			
		||||
@@ -13,6 +9,11 @@ import (
 | 
			
		||||
	"one-api/router"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-contrib/sessions/cookie"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed web/build
 | 
			
		||||
@@ -22,6 +23,8 @@ var buildFS embed.FS
 | 
			
		||||
var indexPage []byte
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	godotenv.Load(".env")
 | 
			
		||||
 | 
			
		||||
	common.SetupGinLog()
 | 
			
		||||
	common.SysLog("One API " + common.Version + " started")
 | 
			
		||||
	if os.Getenv("GIN_MODE") != "debug" {
 | 
			
		||||
@@ -82,14 +85,8 @@ func main() {
 | 
			
		||||
	server.Use(middleware.CORS())
 | 
			
		||||
 | 
			
		||||
	// Initialize session store
 | 
			
		||||
	if common.RedisEnabled {
 | 
			
		||||
		opt := common.ParseRedisOption()
 | 
			
		||||
		store, _ := redis.NewStore(opt.MinIdleConns, opt.Network, opt.Addr, opt.Password, []byte(common.SessionSecret))
 | 
			
		||||
		server.Use(sessions.Sessions("session", store))
 | 
			
		||||
	} else {
 | 
			
		||||
		store := cookie.NewStore([]byte(common.SessionSecret))
 | 
			
		||||
		server.Use(sessions.Sessions("session", store))
 | 
			
		||||
	}
 | 
			
		||||
	store := cookie.NewStore([]byte(common.SessionSecret))
 | 
			
		||||
	server.Use(sessions.Sessions("session", store))
 | 
			
		||||
 | 
			
		||||
	router.SetRouter(server, buildFS, indexPage)
 | 
			
		||||
	var port = os.Getenv("PORT")
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,13 @@ package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ModelRequest struct {
 | 
			
		||||
@@ -73,11 +74,21 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
					modelRequest.Model = "text-moderation-stable"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
 | 
			
		||||
				if modelRequest.Model == "" {
 | 
			
		||||
					modelRequest.Model = "dall-e"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				message := "无可用渠道"
 | 
			
		||||
				if channel != nil {
 | 
			
		||||
					common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
 | 
			
		||||
					message = "数据库一致性已被破坏,请联系管理员"
 | 
			
		||||
				}
 | 
			
		||||
				c.JSON(http.StatusServiceUnavailable, gin.H{
 | 
			
		||||
					"error": gin.H{
 | 
			
		||||
						"message": "无可用渠道",
 | 
			
		||||
						"message": message,
 | 
			
		||||
						"type":    "one_api_error",
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
@@ -88,6 +99,7 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
		c.Set("channel", channel.Type)
 | 
			
		||||
		c.Set("channel_id", channel.Id)
 | 
			
		||||
		c.Set("channel_name", channel.Name)
 | 
			
		||||
		c.Set("model_mapping", channel.ModelMapping)
 | 
			
		||||
		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
 | 
			
		||||
		c.Set("base_url", channel.BaseURL)
 | 
			
		||||
		if channel.Type == common.ChannelTypeAzure {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	channel := Channel{}
 | 
			
		||||
	channel.Id = ability.ChannelId
 | 
			
		||||
	err = DB.First(&channel, "id = ?", ability.ChannelId).Error
 | 
			
		||||
	return &channel, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,18 @@ func CacheGetUserQuota(id int) (quota int, err error) {
 | 
			
		||||
	return quota, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CacheUpdateUserQuota(id int) error {
 | 
			
		||||
	if !common.RedisEnabled {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	quota, err := GetUserQuota(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CacheIsUserEnabled(userId int) bool {
 | 
			
		||||
	if !common.RedisEnabled {
 | 
			
		||||
		return IsUserEnabled(userId)
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ type Channel struct {
 | 
			
		||||
	Models             string  `json:"models"`
 | 
			
		||||
	Group              string  `json:"group" gorm:"type:varchar(32);default:'default'"`
 | 
			
		||||
	UsedQuota          int64   `json:"used_quota" gorm:"bigint;default:0"`
 | 
			
		||||
	ModelMapping       string  `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
 | 
			
		||||
@@ -36,7 +37,7 @@ func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, 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
 | 
			
		||||
	err = DB.Omit("key").Where("id = ? or name LIKE ? or `key` = ?", keyword, keyword+"%", keyword).Find(&channels).Error
 | 
			
		||||
	return channels, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,10 +30,12 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
 | 
			
		||||
	common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
 | 
			
		||||
	common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
 | 
			
		||||
	common.OptionMap["DiscordOAuthEnabled"] = strconv.FormatBool(common.DiscordOAuthEnabled)
 | 
			
		||||
	common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
 | 
			
		||||
	common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
 | 
			
		||||
	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
 | 
			
		||||
	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
 | 
			
		||||
	common.OptionMap["ApproximateTokenEnabled"] = strconv.FormatBool(common.ApproximateTokenEnabled)
 | 
			
		||||
	common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
 | 
			
		||||
	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
 | 
			
		||||
	common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
 | 
			
		||||
@@ -52,6 +54,8 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["ServerAddress"] = ""
 | 
			
		||||
	common.OptionMap["GitHubClientId"] = ""
 | 
			
		||||
	common.OptionMap["GitHubClientSecret"] = ""
 | 
			
		||||
	common.OptionMap["DiscordClientId"] = ""
 | 
			
		||||
	common.OptionMap["DiscordClientSecret"] = ""
 | 
			
		||||
	common.OptionMap["WeChatServerAddress"] = ""
 | 
			
		||||
	common.OptionMap["WeChatServerToken"] = ""
 | 
			
		||||
	common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
 | 
			
		||||
@@ -131,6 +135,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
			common.PasswordLoginEnabled = boolValue
 | 
			
		||||
		case "EmailVerificationEnabled":
 | 
			
		||||
			common.EmailVerificationEnabled = boolValue
 | 
			
		||||
		case "DiscordOAuthEnabled":
 | 
			
		||||
			common.DiscordOAuthEnabled = boolValue
 | 
			
		||||
		case "GitHubOAuthEnabled":
 | 
			
		||||
			common.GitHubOAuthEnabled = boolValue
 | 
			
		||||
		case "WeChatAuthEnabled":
 | 
			
		||||
@@ -141,6 +147,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
			common.RegisterEnabled = boolValue
 | 
			
		||||
		case "AutomaticDisableChannelEnabled":
 | 
			
		||||
			common.AutomaticDisableChannelEnabled = boolValue
 | 
			
		||||
		case "ApproximateTokenEnabled":
 | 
			
		||||
			common.ApproximateTokenEnabled = boolValue
 | 
			
		||||
		case "LogConsumeEnabled":
 | 
			
		||||
			common.LogConsumeEnabled = boolValue
 | 
			
		||||
		case "DisplayInCurrencyEnabled":
 | 
			
		||||
@@ -167,6 +175,10 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		common.GitHubClientId = value
 | 
			
		||||
	case "GitHubClientSecret":
 | 
			
		||||
		common.GitHubClientSecret = value
 | 
			
		||||
	case "DiscordClientId":
 | 
			
		||||
		common.DiscordClientId = value
 | 
			
		||||
	case "DiscordClientSecret":
 | 
			
		||||
		common.DiscordClientSecret = value
 | 
			
		||||
	case "Footer":
 | 
			
		||||
		common.Footer = value
 | 
			
		||||
	case "SystemName":
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package model
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -48,26 +49,27 @@ func Redeem(key string, userId int) (quota int, err error) {
 | 
			
		||||
		return 0, errors.New("无效的 user id")
 | 
			
		||||
	}
 | 
			
		||||
	redemption := &Redemption{}
 | 
			
		||||
	err = DB.Where("`key` = ?", key).First(redemption).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, errors.New("无效的兑换码")
 | 
			
		||||
	}
 | 
			
		||||
	if redemption.Status != common.RedemptionCodeStatusEnabled {
 | 
			
		||||
		return 0, errors.New("该兑换码已被使用")
 | 
			
		||||
	}
 | 
			
		||||
	err = IncreaseUserQuota(userId, redemption.Quota)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
 | 
			
		||||
	err = DB.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		err := DB.Where("`key` = ?", key).First(redemption).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("无效的兑换码")
 | 
			
		||||
		}
 | 
			
		||||
		if redemption.Status != common.RedemptionCodeStatusEnabled {
 | 
			
		||||
			return errors.New("该兑换码已被使用")
 | 
			
		||||
		}
 | 
			
		||||
		err = DB.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		redemption.RedeemedTime = common.GetTimestamp()
 | 
			
		||||
		redemption.Status = common.RedemptionCodeStatusUsed
 | 
			
		||||
		err := redemption.SelectUpdate()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("failed to update redemption status: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
 | 
			
		||||
	}()
 | 
			
		||||
		return redemption.SelectUpdate()
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, errors.New("兑换失败," + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
 | 
			
		||||
	return redemption.Quota, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,10 @@ package model
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
 | 
			
		||||
@@ -19,6 +20,7 @@ type User struct {
 | 
			
		||||
	Status           int    `json:"status" gorm:"type:int;default:1"` // enabled, disabled
 | 
			
		||||
	Email            string `json:"email" gorm:"index" validate:"max=50"`
 | 
			
		||||
	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"`
 | 
			
		||||
	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
 | 
			
		||||
@@ -169,6 +171,14 @@ func (user *User) FillUserByGitHubId() error {
 | 
			
		||||
	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 {
 | 
			
		||||
	if user.WeChatId == "" {
 | 
			
		||||
		return errors.New("WeChat id 为空!")
 | 
			
		||||
@@ -197,6 +207,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
 | 
			
		||||
	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsDiscordIdAlreadyTaken(discordId string) bool {
 | 
			
		||||
	return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsUsernameAlreadyTaken(username string) bool {
 | 
			
		||||
	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-contrib/gzip"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/controller"
 | 
			
		||||
	"one-api/middleware"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/gzip"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
@@ -20,6 +21,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
 | 
			
		||||
		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
 | 
			
		||||
		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/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
 | 
			
		||||
		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
 | 
			
		||||
@@ -27,7 +29,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
		userRoute := apiRouter.Group("/user")
 | 
			
		||||
		{
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
			selfRoute := userRoute.Group("/")
 | 
			
		||||
@@ -35,7 +37,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
			{
 | 
			
		||||
				selfRoute.GET("/self", controller.GetSelf)
 | 
			
		||||
				selfRoute.PUT("/self", controller.UpdateSelf)
 | 
			
		||||
				selfRoute.DELETE("/self", controller.DeleteSelf)
 | 
			
		||||
				selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf)
 | 
			
		||||
				selfRoute.GET("/token", controller.GenerateAccessToken)
 | 
			
		||||
				selfRoute.GET("/aff", controller.GetAffCode)
 | 
			
		||||
				selfRoute.POST("/topup", controller.TopUp)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,6 +15,10 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
 | 
			
		||||
	SetDashboardRouter(router)
 | 
			
		||||
	SetRelayRouter(router)
 | 
			
		||||
	frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
 | 
			
		||||
	if common.IsMasterNode && frontendBaseUrl != "" {
 | 
			
		||||
		frontendBaseUrl = ""
 | 
			
		||||
		common.SysLog("FRONTEND_BASE_URL is ignored on master node")
 | 
			
		||||
	}
 | 
			
		||||
	if frontendBaseUrl == "" {
 | 
			
		||||
		SetWebRouter(router, buildFS, indexPage)
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/controller"
 | 
			
		||||
	"one-api/middleware"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SetRelayRouter(router *gin.Engine) {
 | 
			
		||||
@@ -20,7 +21,7 @@ func SetRelayRouter(router *gin.Engine) {
 | 
			
		||||
		relayV1Router.POST("/completions", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/chat/completions", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/edits", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/images/generations", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/embeddings", controller.Relay)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -22,5 +22,4 @@ npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
.idea
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
yarn.lock
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18300
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										18300
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,18 +3,17 @@
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "axios": "^1.4.0",
 | 
			
		||||
    "history": "^5.3.0",
 | 
			
		||||
    "marked": "^4.1.1",
 | 
			
		||||
    "marked": "^5.1.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-dropzone": "^14.2.3",
 | 
			
		||||
    "react-router-dom": "^6.3.0",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
    "react-turnstile": "^1.0.5",
 | 
			
		||||
    "react-router-dom": "^6.14.1",
 | 
			
		||||
    "react-toastify": "^9.1.3",
 | 
			
		||||
    "react-turnstile": "^1.1.1",
 | 
			
		||||
    "semantic-ui-css": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3"
 | 
			
		||||
    "semantic-ui-react": "^2.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-scripts start",
 | 
			
		||||
@@ -41,7 +40,9 @@
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "prettier": "^2.7.1"
 | 
			
		||||
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "react-scripts": "^5.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import AddUser from './pages/User/AddUser';
 | 
			
		||||
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
 | 
			
		||||
import PasswordResetForm from './components/PasswordResetForm';
 | 
			
		||||
import GitHubOAuth from './components/GitHubOAuth';
 | 
			
		||||
import DiscordOAuth from './components/DiscordOAuth';
 | 
			
		||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
 | 
			
		||||
import { UserContext } from './context/User';
 | 
			
		||||
import { StatusContext } from './context/Status';
 | 
			
		||||
@@ -55,15 +56,6 @@ function App() {
 | 
			
		||||
      } else {
 | 
			
		||||
        localStorage.removeItem('chat_link');
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        data.version !== process.env.REACT_APP_VERSION &&
 | 
			
		||||
        data.version !== 'v0.0.0' &&
 | 
			
		||||
        process.env.REACT_APP_VERSION !== ''
 | 
			
		||||
      ) {
 | 
			
		||||
        showNotice(
 | 
			
		||||
          `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError('无法正常连接至服务器!');
 | 
			
		||||
    }
 | 
			
		||||
@@ -239,6 +231,14 @@ function App() {
 | 
			
		||||
          </Suspense>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route
 | 
			
		||||
        path='/oauth/discord'
 | 
			
		||||
        element={
 | 
			
		||||
          <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
            <DiscordOAuth />
 | 
			
		||||
          </Suspense>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route
 | 
			
		||||
        path='/setting'
 | 
			
		||||
        element={
 | 
			
		||||
@@ -252,11 +252,11 @@ function App() {
 | 
			
		||||
      <Route
 | 
			
		||||
        path='/topup'
 | 
			
		||||
        element={
 | 
			
		||||
        <PrivateRoute>
 | 
			
		||||
          <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
            <TopUp />
 | 
			
		||||
          </Suspense>
 | 
			
		||||
        </PrivateRoute>
 | 
			
		||||
          <PrivateRoute>
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <TopUp />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          </PrivateRoute>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,9 @@ function renderType(type) {
 | 
			
		||||
function renderBalance(type, balance) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 1: // OpenAI
 | 
			
		||||
      return <span>${balance.toFixed(2)}</span>;
 | 
			
		||||
    case 4: // CloseAI
 | 
			
		||||
      return <span>¥{balance.toFixed(2)}</span>;
 | 
			
		||||
    case 8: // 自定义
 | 
			
		||||
      return <span>${balance.toFixed(2)}</span>;
 | 
			
		||||
    case 5: // OpenAI-SB
 | 
			
		||||
@@ -38,6 +41,8 @@ function renderBalance(type, balance) {
 | 
			
		||||
      return <span>{renderNumber(balance)}</span>;
 | 
			
		||||
    case 12: // API2GPT
 | 
			
		||||
      return <span>¥{balance.toFixed(2)}</span>;
 | 
			
		||||
    case 13: // AIGC2D
 | 
			
		||||
      return <span>{renderNumber(balance)}</span>;
 | 
			
		||||
    default:
 | 
			
		||||
      return <span>不支持</span>;
 | 
			
		||||
  }
 | 
			
		||||
@@ -58,8 +63,8 @@ const ChannelsTable = () => {
 | 
			
		||||
      if (startIdx === 0) {
 | 
			
		||||
        setChannels(data);
 | 
			
		||||
      } else {
 | 
			
		||||
        let newChannels = channels;
 | 
			
		||||
        newChannels.push(...data);
 | 
			
		||||
        let newChannels = [...channels];
 | 
			
		||||
        newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
 | 
			
		||||
        setChannels(newChannels);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -80,7 +85,7 @@ const ChannelsTable = () => {
 | 
			
		||||
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await loadChannels(0);
 | 
			
		||||
    await loadChannels(activePage - 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -238,7 +243,7 @@ const ChannelsTable = () => {
 | 
			
		||||
    if (channels.length === 0) return;
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    let sortedChannels = [...channels];
 | 
			
		||||
    if (typeof sortedChannels[0][key] === 'string'){
 | 
			
		||||
    if (typeof sortedChannels[0][key] === 'string') {
 | 
			
		||||
      sortedChannels.sort((a, b) => {
 | 
			
		||||
        return ('' + a[key]).localeCompare(b[key]);
 | 
			
		||||
      });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
@@ -12,7 +12,8 @@ import {
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { API, getLogo, showError, showSuccess } from '../helpers';
 | 
			
		||||
import { API, getLogo, showError, showSuccess, showInfo } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
 | 
			
		||||
const LoginForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
@@ -24,6 +25,9 @@ const LoginForm = () => {
 | 
			
		||||
  const [submitted, setSubmitted] = useState(false);
 | 
			
		||||
  const { username, password } = inputs;
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  const [turnstileEnabled, setTurnstileEnabled] = useState(false);
 | 
			
		||||
  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
 | 
			
		||||
  const [turnstileToken, setTurnstileToken] = useState('');
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [status, setStatus] = useState({});
 | 
			
		||||
@@ -37,6 +41,11 @@ const LoginForm = () => {
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      setStatus(status);
 | 
			
		||||
 | 
			
		||||
      if (status.turnstile_check) {
 | 
			
		||||
        setTurnstileEnabled(true);
 | 
			
		||||
        setTurnstileSiteKey(status.turnstile_site_key);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +57,12 @@ const LoginForm = () => {
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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 = () => {
 | 
			
		||||
    setShowWeChatLoginModal(true);
 | 
			
		||||
  };
 | 
			
		||||
@@ -76,7 +91,12 @@ const LoginForm = () => {
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
    setSubmitted(true);
 | 
			
		||||
    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,
 | 
			
		||||
        password,
 | 
			
		||||
      });
 | 
			
		||||
@@ -119,6 +139,16 @@ const LoginForm = () => {
 | 
			
		||||
              value={password}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            {turnstileEnabled ? (
 | 
			
		||||
              <Turnstile
 | 
			
		||||
                sitekey={turnstileSiteKey}
 | 
			
		||||
                onVerify={(token) => {
 | 
			
		||||
                  setTurnstileToken(token);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button color="" fluid size="large" onClick={handleSubmit}>
 | 
			
		||||
              登录
 | 
			
		||||
            </Button>
 | 
			
		||||
@@ -134,28 +164,32 @@ const LoginForm = () => {
 | 
			
		||||
            点击注册
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Message>
 | 
			
		||||
        {status.github_oauth || status.wechat_login ? (
 | 
			
		||||
        {status.github_oauth || status.wechat_login || status.discord_oauth ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <Divider horizontal>Or</Divider>
 | 
			
		||||
            {status.github_oauth ? (
 | 
			
		||||
            {status.discord_oauth && (
 | 
			
		||||
              <Button
 | 
			
		||||
                circular
 | 
			
		||||
                color="blue"
 | 
			
		||||
                icon="discord"
 | 
			
		||||
                onClick={onDiscordOAuthClicked}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {status.github_oauth && (
 | 
			
		||||
              <Button
 | 
			
		||||
                circular
 | 
			
		||||
                color="black"
 | 
			
		||||
                icon="github"
 | 
			
		||||
                onClick={onGitHubOAuthClicked}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            {status.wechat_login ? (
 | 
			
		||||
            {status.wechat_login && (
 | 
			
		||||
              <Button
 | 
			
		||||
                circular
 | 
			
		||||
                color="green"
 | 
			
		||||
                icon="wechat"
 | 
			
		||||
                onClick={onWeChatLoginClicked}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
 
 | 
			
		||||
@@ -108,7 +108,7 @@ const LogsTable = () => {
 | 
			
		||||
        setLogs(data);
 | 
			
		||||
      } else {
 | 
			
		||||
        let newLogs = [...logs];
 | 
			
		||||
        newLogs.push(...data);
 | 
			
		||||
        newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
 | 
			
		||||
        setLogs(newLogs);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,8 @@ const OperationSetting = () => {
 | 
			
		||||
    ChannelDisableThreshold: 0,
 | 
			
		||||
    LogConsumeEnabled: '',
 | 
			
		||||
    DisplayInCurrencyEnabled: '',
 | 
			
		||||
    DisplayTokenStatEnabled: ''
 | 
			
		||||
    DisplayTokenStatEnabled: '',
 | 
			
		||||
    ApproximateTokenEnabled: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [originInputs, setOriginInputs] = useState({});
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
@@ -74,9 +75,6 @@ const OperationSetting = () => {
 | 
			
		||||
  const submitConfig = async (group) => {
 | 
			
		||||
    switch (group) {
 | 
			
		||||
      case 'monitor':
 | 
			
		||||
        if (originInputs['AutomaticDisableChannelEnabled'] !== inputs.AutomaticDisableChannelEnabled) {
 | 
			
		||||
          await updateOption('AutomaticDisableChannelEnabled', inputs.AutomaticDisableChannelEnabled);
 | 
			
		||||
        }
 | 
			
		||||
        if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
 | 
			
		||||
          await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
 | 
			
		||||
        }
 | 
			
		||||
@@ -184,6 +182,12 @@ const OperationSetting = () => {
 | 
			
		||||
              name='DisplayTokenStatEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.ApproximateTokenEnabled === 'true'}
 | 
			
		||||
              label='使用近似的方式估算 token 数以减少计算量'
 | 
			
		||||
              name='ApproximateTokenEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('general').then();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,24 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import React, { useEffect, useState, useContext } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Header, Image, Message, Modal, Label } from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
 | 
			
		||||
const PersonalSetting = () => {
 | 
			
		||||
  const [userDispatch] = useContext(UserContext);
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    wechat_verification_code: '',
 | 
			
		||||
    email_verification_code: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    self_account_deletion_confirmation: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [status, setStatus] = useState({});
 | 
			
		||||
  const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
 | 
			
		||||
  const [showEmailBindModal, setShowEmailBindModal] = useState(false);
 | 
			
		||||
  const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
 | 
			
		||||
  const [turnstileEnabled, setTurnstileEnabled] = useState(false);
 | 
			
		||||
  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
 | 
			
		||||
  const [turnstileToken, setTurnstileToken] = useState('');
 | 
			
		||||
@@ -57,6 +63,26 @@ const PersonalSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteAccount = async () => {
 | 
			
		||||
    if (inputs.self_account_deletion_confirmation !== 'CONFIRM') {
 | 
			
		||||
      showError('请确认您要删除账户!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const res = await API.delete('/api/user/self');
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess('账户已删除!');
 | 
			
		||||
      await API.get('/api/user/logout');
 | 
			
		||||
      userDispatch({ type: 'logout' });
 | 
			
		||||
      localStorage.removeItem('user');
 | 
			
		||||
      navigate('/login');
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const bindWeChat = async () => {
 | 
			
		||||
    if (inputs.wechat_verification_code === '') return;
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
@@ -77,6 +103,12 @@ const PersonalSetting = () => {
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openDiscordOAuth = () => {
 | 
			
		||||
    window.open(
 | 
			
		||||
      `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sendVerificationCode = async () => {
 | 
			
		||||
    if (inputs.email === '') return;
 | 
			
		||||
    if (turnstileEnabled && turnstileToken === '') {
 | 
			
		||||
@@ -123,6 +155,9 @@ const PersonalSetting = () => {
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
 | 
			
		||||
      <Button onClick={getAffLink}>复制邀请链接</Button>
 | 
			
		||||
      <Button onClick={() => {
 | 
			
		||||
        setShowAccountDeleteModal(true);
 | 
			
		||||
      }} color='red'>删除个人账户</Button>
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <Header as='h3'>账号绑定</Header>
 | 
			
		||||
      {
 | 
			
		||||
@@ -170,6 +205,11 @@ const PersonalSetting = () => {
 | 
			
		||||
          <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      {
 | 
			
		||||
        status.discord_oauth && (
 | 
			
		||||
          <Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setShowEmailBindModal(true);
 | 
			
		||||
@@ -230,6 +270,48 @@ const PersonalSetting = () => {
 | 
			
		||||
          </Modal.Description>
 | 
			
		||||
        </Modal.Content>
 | 
			
		||||
      </Modal>
 | 
			
		||||
      <Modal
 | 
			
		||||
        onClose={() => setShowAccountDeleteModal(false)}
 | 
			
		||||
        onOpen={() => setShowAccountDeleteModal(true)}
 | 
			
		||||
        open={showAccountDeleteModal}
 | 
			
		||||
        size={'tiny'}
 | 
			
		||||
        style={{ maxWidth: '450px' }}
 | 
			
		||||
      >
 | 
			
		||||
        <Modal.Header>您是否确认删除自己的帐户?</Modal.Header>
 | 
			
		||||
        <Modal.Content>
 | 
			
		||||
          <Modal.Description>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                label='请输入 "CONFIRM" 以删除您的帐户。'
 | 
			
		||||
                placeholder='确认文字'
 | 
			
		||||
                name='self_account_deletion_confirmation'
 | 
			
		||||
                value={inputs.self_account_deletion_confirmation}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              />
 | 
			
		||||
              {turnstileEnabled ? (
 | 
			
		||||
                <Turnstile
 | 
			
		||||
                  sitekey={turnstileSiteKey}
 | 
			
		||||
                  onVerify={(token) => {
 | 
			
		||||
                    setTurnstileToken(token);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                color='red'
 | 
			
		||||
                fluid
 | 
			
		||||
                size='large'
 | 
			
		||||
                onClick={deleteAccount}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                删除
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form>
 | 
			
		||||
          </Modal.Description>
 | 
			
		||||
        </Modal.Content>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,11 @@ const SystemSetting = () => {
 | 
			
		||||
    PasswordRegisterEnabled: '',
 | 
			
		||||
    EmailVerificationEnabled: '',
 | 
			
		||||
    GitHubOAuthEnabled: '',
 | 
			
		||||
    DiscordOAuthEnabled: '',
 | 
			
		||||
    GitHubClientId: '',
 | 
			
		||||
    GitHubClientSecret: '',
 | 
			
		||||
    DiscordClientId: '',
 | 
			
		||||
    DiscordClientSecret: '',
 | 
			
		||||
    Notice: '',
 | 
			
		||||
    SMTPServer: '',
 | 
			
		||||
    SMTPPort: '',
 | 
			
		||||
@@ -56,6 +59,7 @@ const SystemSetting = () => {
 | 
			
		||||
      case 'PasswordRegisterEnabled':
 | 
			
		||||
      case 'EmailVerificationEnabled':
 | 
			
		||||
      case 'GitHubOAuthEnabled':
 | 
			
		||||
      case 'DiscordOAuthEnabled':
 | 
			
		||||
      case 'WeChatAuthEnabled':
 | 
			
		||||
      case 'TurnstileCheckEnabled':
 | 
			
		||||
      case 'RegisterEnabled':
 | 
			
		||||
@@ -82,6 +86,8 @@ const SystemSetting = () => {
 | 
			
		||||
      name === 'Notice' ||
 | 
			
		||||
      name.startsWith('SMTP') ||
 | 
			
		||||
      name === 'ServerAddress' ||
 | 
			
		||||
      name === 'DiscordClientId' ||
 | 
			
		||||
      name === 'DiscordClientSecret' ||
 | 
			
		||||
      name === 'GitHubClientId' ||
 | 
			
		||||
      name === 'GitHubClientSecret' ||
 | 
			
		||||
      name === 'WeChatServerAddress' ||
 | 
			
		||||
@@ -161,6 +167,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 () => {
 | 
			
		||||
    if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
 | 
			
		||||
      await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
 | 
			
		||||
@@ -177,49 +195,55 @@ const SystemSetting = () => {
 | 
			
		||||
    <Grid columns={1}>
 | 
			
		||||
      <Grid.Column>
 | 
			
		||||
        <Form loading={loading}>
 | 
			
		||||
          <Header as='h3'>通用设置</Header>
 | 
			
		||||
          <Header as='h3'>General Settings</Header>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='服务器地址'
 | 
			
		||||
              placeholder='例如:https://yourdomain.com'
 | 
			
		||||
              label='Server Address'
 | 
			
		||||
              placeholder='For example:https://yourdomain.com'
 | 
			
		||||
              value={inputs.ServerAddress}
 | 
			
		||||
              name='ServerAddress'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitServerAddress}>
 | 
			
		||||
            更新服务器地址
 | 
			
		||||
            Update Server Address
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>配置登录注册</Header>
 | 
			
		||||
          <Header as='h3'>Configure Login/Registration</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.PasswordLoginEnabled === 'true'}
 | 
			
		||||
              label='允许通过密码进行登录'
 | 
			
		||||
              label='Allow login via password'
 | 
			
		||||
              name='PasswordLoginEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.PasswordRegisterEnabled === 'true'}
 | 
			
		||||
              label='允许通过密码进行注册'
 | 
			
		||||
              label='Allow registration via password'
 | 
			
		||||
              name='PasswordRegisterEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.EmailVerificationEnabled === 'true'}
 | 
			
		||||
              label='通过密码注册时需要进行邮箱验证'
 | 
			
		||||
              label='Email verification is required when registering via password'
 | 
			
		||||
              name='EmailVerificationEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.GitHubOAuthEnabled === 'true'}
 | 
			
		||||
              label='允许通过 GitHub 账户登录 & 注册'
 | 
			
		||||
              label='Allow login & registration via GitHub account'
 | 
			
		||||
              name='GitHubOAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.DiscordOAuthEnabled === 'true'}
 | 
			
		||||
              label='允许通过 Discord 账户登录和注册'
 | 
			
		||||
              name='DiscordOAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.WeChatAuthEnabled === 'true'}
 | 
			
		||||
              label='允许通过微信登录 & 注册'
 | 
			
		||||
              label='Allow login & registration via WeChat'
 | 
			
		||||
              name='WeChatAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
@@ -227,82 +251,120 @@ const SystemSetting = () => {
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.RegisterEnabled === 'true'}
 | 
			
		||||
              label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
 | 
			
		||||
              label='Allow new user registration (if this option is off, new users will not be able to register in any way)'
 | 
			
		||||
              name='RegisterEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.TurnstileCheckEnabled === 'true'}
 | 
			
		||||
              label='启用 Turnstile 用户校验'
 | 
			
		||||
              label='Enable Turnstile user verification'
 | 
			
		||||
              name='TurnstileCheckEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 SMTP
 | 
			
		||||
            <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
 | 
			
		||||
            Configure SMTP
 | 
			
		||||
            <Header.Subheader>To support the system email sending</Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='SMTP 服务器地址'
 | 
			
		||||
              label='SMTP Server Address'
 | 
			
		||||
              name='SMTPServer'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPServer}
 | 
			
		||||
              placeholder='例如:smtp.qq.com'
 | 
			
		||||
              placeholder='For example: smtp.qq.com'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='SMTP 端口'
 | 
			
		||||
              label='SMTP Port'
 | 
			
		||||
              name='SMTPPort'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPPort}
 | 
			
		||||
              placeholder='默认: 587'
 | 
			
		||||
              placeholder='Default: 587'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='SMTP 账户'
 | 
			
		||||
              label='SMTP Account'
 | 
			
		||||
              name='SMTPAccount'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPAccount}
 | 
			
		||||
              placeholder='通常是邮箱地址'
 | 
			
		||||
              placeholder='Usually an email address'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='SMTP 发送者邮箱'
 | 
			
		||||
              label='SMTP Sender email'
 | 
			
		||||
              name='SMTPFrom'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPFrom}
 | 
			
		||||
              placeholder='通常和邮箱地址保持一致'
 | 
			
		||||
              placeholder='Usually consistent with the email address'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='SMTP 访问凭证'
 | 
			
		||||
              label='SMTP Access Credential'
 | 
			
		||||
              name='SMTPToken'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPToken}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
              placeholder='Sensitive information will not be displayed in the frontend'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
 | 
			
		||||
          <Form.Button onClick={submitSMTP}>Save SMTP Settings</Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 GitHub OAuth App
 | 
			
		||||
            Configure Discord OAuth App
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持通过 GitHub 进行登录注册,
 | 
			
		||||
              <a href='https://github.com/settings/developers' target='_blank'>
 | 
			
		||||
                点击此处
 | 
			
		||||
              To support login & registration via GitHub,
 | 
			
		||||
              <a href='https://discord.com/developers/applications' target='_blank'>
 | 
			
		||||
                Click here
 | 
			
		||||
              </a>
 | 
			
		||||
              管理你的 GitHub OAuth App
 | 
			
		||||
              Manage your Discord OAuth App
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Message>
 | 
			
		||||
            Homepage URL 填 <code>{inputs.ServerAddress}</code>
 | 
			
		||||
            ,Authorization callback URL 填{' '}
 | 
			
		||||
            Fill in the Homepage URL <code>{inputs.ServerAddress}</code>
 | 
			
		||||
            ,Fill in the Authorization callback URL{' '}
 | 
			
		||||
            <code>{`${inputs.ServerAddress}/oauth/discord`}</code>
 | 
			
		||||
          </Message>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='Discord Client ID'
 | 
			
		||||
              name='DiscordClientId'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.DiscordClientId}
 | 
			
		||||
              placeholder='Enter the ID of your registered Discord OAuth APP'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='Discord Client Secret'
 | 
			
		||||
              name='DiscordClientSecret'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.DiscordClientSecret}
 | 
			
		||||
              placeholder='Sensitive information will not be displayed in the frontend'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitDiscordOAuth}>
 | 
			
		||||
            Save Discord OAuth Settings
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            Configure GitHub OAuth App
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              To support login & registration via GitHub,
 | 
			
		||||
              <a href='https://github.com/settings/developers' target='_blank'>
 | 
			
		||||
                Click here
 | 
			
		||||
              </a>
 | 
			
		||||
              Manage your GitHub OAuth App
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Message>
 | 
			
		||||
            Fill in the Homepage URL <code>{inputs.ServerAddress}</code>
 | 
			
		||||
            ,Fill in the Authorization callback URL{' '}
 | 
			
		||||
            <code>{`${inputs.ServerAddress}/oauth/github`}</code>
 | 
			
		||||
          </Message>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
@@ -312,7 +374,7 @@ const SystemSetting = () => {
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.GitHubClientId}
 | 
			
		||||
              placeholder='输入你注册的 GitHub OAuth APP 的 ID'
 | 
			
		||||
              placeholder='Enter your registered GitHub OAuth APP ID'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='GitHub Client Secret'
 | 
			
		||||
@@ -321,65 +383,65 @@ const SystemSetting = () => {
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.GitHubClientSecret}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
              placeholder='Sensitive information will not be displayed in the frontend'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitGitHubOAuth}>
 | 
			
		||||
            保存 GitHub OAuth 设置
 | 
			
		||||
            Save GitHub OAuth Settings
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 WeChat Server
 | 
			
		||||
            Configure WeChat Server
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持通过微信进行登录注册,
 | 
			
		||||
              To support login & registration via WeChat,
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://github.com/songquanpeng/wechat-server'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
                Click here
 | 
			
		||||
              </a>
 | 
			
		||||
              了解 WeChat Server
 | 
			
		||||
              Learn about WeChat Server
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='WeChat Server 服务器地址'
 | 
			
		||||
              label='WeChat Server Server Address'
 | 
			
		||||
              name='WeChatServerAddress'
 | 
			
		||||
              placeholder='例如:https://yourdomain.com'
 | 
			
		||||
              placeholder='For example:https://yourdomain.com'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatServerAddress}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='WeChat Server 访问凭证'
 | 
			
		||||
              label='WeChat Server Access Credential'
 | 
			
		||||
              name='WeChatServerToken'
 | 
			
		||||
              type='password'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatServerToken}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
              placeholder='Sensitive information will not be displayed in the frontend'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='微信公众号二维码图片链接'
 | 
			
		||||
              label='WeChat Public Account QR Code Image Link'
 | 
			
		||||
              name='WeChatAccountQRCodeImageURL'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatAccountQRCodeImageURL}
 | 
			
		||||
              placeholder='输入一个图片链接'
 | 
			
		||||
              placeholder='Enter an image link'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitWeChat}>
 | 
			
		||||
            保存 WeChat Server 设置
 | 
			
		||||
            Save WeChat Server Settings
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 Turnstile
 | 
			
		||||
            Configure Turnstile
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持用户校验,
 | 
			
		||||
              To support user verification,
 | 
			
		||||
              <a href='https://dash.cloudflare.com/' target='_blank'>
 | 
			
		||||
                点击此处
 | 
			
		||||
                Click here
 | 
			
		||||
              </a>
 | 
			
		||||
              管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
 | 
			
		||||
              Manage your Turnstile Sites, recommend selecting Invisible Widget Type
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
@@ -389,7 +451,7 @@ const SystemSetting = () => {
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TurnstileSiteKey}
 | 
			
		||||
              placeholder='输入你注册的 Turnstile Site Key'
 | 
			
		||||
              placeholder='Enter your registered Turnstile Site Key'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='Turnstile Secret Key'
 | 
			
		||||
@@ -398,11 +460,11 @@ const SystemSetting = () => {
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TurnstileSecretKey}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
              placeholder='Sensitive information will not be displayed in the frontend'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitTurnstile}>
 | 
			
		||||
            保存 Turnstile 设置
 | 
			
		||||
            Save Turnstile Settings
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,8 @@ const TokensTable = () => {
 | 
			
		||||
      if (startIdx === 0) {
 | 
			
		||||
        setTokens(data);
 | 
			
		||||
      } else {
 | 
			
		||||
        let newTokens = tokens;
 | 
			
		||||
        newTokens.push(...data);
 | 
			
		||||
        let newTokens = [...tokens];
 | 
			
		||||
        newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
 | 
			
		||||
        setTokens(newTokens);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -67,7 +67,7 @@ const TokensTable = () => {
 | 
			
		||||
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await loadTokens(0);
 | 
			
		||||
    await loadTokens(activePage - 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -183,14 +183,6 @@ const UsersTable = () => {
 | 
			
		||||
            >
 | 
			
		||||
              分组
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                sortUser('email');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              邮箱地址
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
@@ -233,20 +225,19 @@ const UsersTable = () => {
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={user.email ? user.email : '未绑定邮箱地址'}
 | 
			
		||||
                      key={user.display_name}
 | 
			
		||||
                      header={user.display_name ? user.display_name : user.username}
 | 
			
		||||
                      key={user.username}
 | 
			
		||||
                      trigger={<span>{renderText(user.username, 10)}</span>}
 | 
			
		||||
                      hoverable
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderGroup(user.group)}</Table.Cell>
 | 
			
		||||
                  {/*<Table.Cell>*/}
 | 
			
		||||
                  {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
 | 
			
		||||
                  {/*</Table.Cell>*/}
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup content='剩余额度' trigger={<Label>{renderQuota(user.quota)}</Label>} />
 | 
			
		||||
                    <Popup content='已用额度' trigger={<Label>{renderQuota(user.used_quota)}</Label>} />
 | 
			
		||||
                    <Popup content='请求次数' trigger={<Label>{renderNumber(user.request_count)}</Label>} />
 | 
			
		||||
                    <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
 | 
			
		||||
                    <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
 | 
			
		||||
                    <Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderRole(user.role)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(user.status)}</Table.Cell>
 | 
			
		||||
@@ -320,7 +311,7 @@ const UsersTable = () => {
 | 
			
		||||
 | 
			
		||||
        <Table.Footer>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell colSpan='8'>
 | 
			
		||||
            <Table.HeaderCell colSpan='7'>
 | 
			
		||||
              <Button size='small' as={Link} to='/user/add' loading={loading}>
 | 
			
		||||
                添加新的用户
 | 
			
		||||
              </Button>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,5 +9,6 @@ export const CHANNEL_OPTIONS = [
 | 
			
		||||
  { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
 | 
			
		||||
  { key: 9, text: 'AI.LS', value: 9, color: 'yellow' },
 | 
			
		||||
  { key: 10, text: 'AI Proxy', value: 10, color: 'purple' },
 | 
			
		||||
  { key: 12, text: 'API2GPT', value: 12, color: 'blue' }
 | 
			
		||||
];
 | 
			
		||||
  { key: 12, text: 'API2GPT', value: 12, color: 'blue' },
 | 
			
		||||
  { key: 13, text: 'AIGC2D', value: 13, color: 'purple' }
 | 
			
		||||
];
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
 | 
			
		||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
 | 
			
		||||
import { CHANNEL_OPTIONS } from '../../constants';
 | 
			
		||||
 | 
			
		||||
const MODEL_MAPPING_EXAMPLE = {
 | 
			
		||||
  'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
 | 
			
		||||
  'gpt-4-0314': 'gpt-4',
 | 
			
		||||
  'gpt-4-32k-0314': 'gpt-4-32k'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const EditChannel = () => {
 | 
			
		||||
  const params = useParams();
 | 
			
		||||
  const channelId = params.id;
 | 
			
		||||
@@ -15,6 +21,7 @@ const EditChannel = () => {
 | 
			
		||||
    key: '',
 | 
			
		||||
    base_url: '',
 | 
			
		||||
    other: '',
 | 
			
		||||
    model_mapping: '',
 | 
			
		||||
    models: [],
 | 
			
		||||
    groups: ['default']
 | 
			
		||||
  };
 | 
			
		||||
@@ -42,6 +49,9 @@ const EditChannel = () => {
 | 
			
		||||
      } else {
 | 
			
		||||
        data.groups = data.group.split(',');
 | 
			
		||||
      }
 | 
			
		||||
      if (data.model_mapping !== '') {
 | 
			
		||||
        data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
 | 
			
		||||
      }
 | 
			
		||||
      setInputs(data);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -94,6 +104,10 @@ const EditChannel = () => {
 | 
			
		||||
      showInfo('请至少选择一个模型!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
 | 
			
		||||
      showInfo('模型映射必须是合法的 JSON 格式!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let localInputs = inputs;
 | 
			
		||||
    if (localInputs.base_url.endsWith('/')) {
 | 
			
		||||
      localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
 | 
			
		||||
@@ -131,6 +145,7 @@ const EditChannel = () => {
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              label='类型'
 | 
			
		||||
              name='type'
 | 
			
		||||
              required
 | 
			
		||||
              options={CHANNEL_OPTIONS}
 | 
			
		||||
              value={inputs.type}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
@@ -187,7 +202,7 @@ const EditChannel = () => {
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='镜像'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值'}
 | 
			
		||||
                  placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
@@ -198,6 +213,7 @@ const EditChannel = () => {
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              required
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请输入名称'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
@@ -210,6 +226,7 @@ const EditChannel = () => {
 | 
			
		||||
              label='分组'
 | 
			
		||||
              placeholder={'请选择分组'}
 | 
			
		||||
              name='groups'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
@@ -226,6 +243,7 @@ const EditChannel = () => {
 | 
			
		||||
              label='模型'
 | 
			
		||||
              placeholder={'请选择该通道所支持的模型'}
 | 
			
		||||
              name='models'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
@@ -246,11 +264,23 @@ const EditChannel = () => {
 | 
			
		||||
              handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
            }}>清除所有模型</Button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label='模型映射'
 | 
			
		||||
              placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
 | 
			
		||||
              name='model_mapping'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.model_mapping}
 | 
			
		||||
              style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            batch ? <Form.Field>
 | 
			
		||||
              <Form.TextArea
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={'请输入密钥,一行一个'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
@@ -261,6 +291,7 @@ const EditChannel = () => {
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={'请输入密钥'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,12 @@ const Home = () => {
 | 
			
		||||
                          ? '已启用'
 | 
			
		||||
                          : '未启用'}
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p>
 | 
			
		||||
                        Discord 身份验证:
 | 
			
		||||
                        {statusState?.status?.discord_oauth === true
 | 
			
		||||
                          ? '已启用'
 | 
			
		||||
                          : '未启用'}
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p>
 | 
			
		||||
                        微信身份验证:
 | 
			
		||||
                        {statusState?.status?.wechat_login === true
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ const EditToken = () => {
 | 
			
		||||
  const [loading, setLoading] = useState(isEdit);
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    remain_quota: 0,
 | 
			
		||||
    remain_quota: isEdit ? 0 : 500000,
 | 
			
		||||
    expired_time: -1,
 | 
			
		||||
    unlimited_quota: false
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,14 @@ const EditUser = () => {
 | 
			
		||||
    display_name: '',
 | 
			
		||||
    password: '',
 | 
			
		||||
    github_id: '',
 | 
			
		||||
    discord_id: '',
 | 
			
		||||
    wechat_id: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    group: 'default'
 | 
			
		||||
  });
 | 
			
		||||
  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, discord_id } =
 | 
			
		||||
    inputs;
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
@@ -156,6 +157,16 @@ const EditUser = () => {
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
          </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='已绑定的微信账户'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user