mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
			v0.5.2-alp
			...
			0.5.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f5f21dffd8 | ||
| 
						 | 
					4e94c85a9a | ||
| 
						 | 
					caabdd1e21 | ||
| 
						 | 
					0424baef6a | ||
| 
						 | 
					256d290507 | ||
| 
						 | 
					8f0799d909 | ||
| 
						 | 
					349e3a3661 | ||
| 
						 | 
					8cc7f983e1 | ||
| 
						 | 
					455643e317 | ||
| 
						 | 
					1c7bad7b87 | ||
| 
						 | 
					3141292026 | ||
| 
						 | 
					e4500bf8bf | ||
| 
						 | 
					4043fccedb | ||
| 
						 | 
					164df4e708 | ||
| 
						 | 
					d850f465cd | ||
| 
						 | 
					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 | 
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm install
 | 
			
		||||
          REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          cd ..
 | 
			
		||||
      - name: Set up Go
 | 
			
		||||
        uses: actions/setup-go@v3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm install
 | 
			
		||||
          REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          cd ..
 | 
			
		||||
      - name: Set up Go
 | 
			
		||||
        uses: actions/setup-go@v3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,7 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm install
 | 
			
		||||
          REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          VITE_REACT_APP_VERSION=$(git describe --tags) npm run build
 | 
			
		||||
          cd ..
 | 
			
		||||
      - name: Set up Go
 | 
			
		||||
        uses: actions/setup-go@v3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -4,4 +4,5 @@ upload
 | 
			
		||||
*.exe
 | 
			
		||||
*.db
 | 
			
		||||
build
 | 
			
		||||
*.db-journal
 | 
			
		||||
*.db-journal
 | 
			
		||||
.env*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								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
 | 
			
		||||
RUN VITE_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"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								README.en.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								README.en.md
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
# One API
 | 
			
		||||
 | 
			
		||||
_✨ Access all LLM through the standard OpenAI API format, easy to deploy & use ✨_
 | 
			
		||||
_✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@@ -57,13 +57,15 @@ _✨ Access all LLM through the standard OpenAI API format, 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. Support for multiple large models:
 | 
			
		||||
   + [x] [OpenAI ChatGPT Series Models](https://platform.openai.com/docs/guides/gpt/chat-completions-api) (Supports [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
 | 
			
		||||
   + [x] [Anthropic Claude Series Models](https://anthropic.com)
 | 
			
		||||
   + [x] [Google PaLM2 Series Models](https://developers.generativeai.google)
 | 
			
		||||
   + [x] [Baidu Wenxin Yiyuan Series Models](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
 | 
			
		||||
   + [x] [Alibaba Tongyi Qianwen Series Models](https://help.aliyun.com/document_detail/2400395.html)
 | 
			
		||||
   + [x] [Zhipu ChatGLM Series Models](https://bigmodel.cn)
 | 
			
		||||
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] 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.
 | 
			
		||||
4. Supports **multi-machine deployment**. [See here](#multi-machine-deployment) for more details.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								README.md
									
									
									
									
									
								
							@@ -11,7 +11,7 @@
 | 
			
		||||
 | 
			
		||||
# One API
 | 
			
		||||
 | 
			
		||||
_✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ✨_
 | 
			
		||||
_✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用✨_
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@@ -58,45 +58,42 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
 | 
			
		||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
 | 
			
		||||
 | 
			
		||||
## 功能
 | 
			
		||||
1. 支持多种大模型:
 | 
			
		||||
   + [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
 | 
			
		||||
   + [x] [Anthropic Claude 系列模型](https://anthropic.com)
 | 
			
		||||
   + [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
 | 
			
		||||
   + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
 | 
			
		||||
   + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html)
 | 
			
		||||
   + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html)
 | 
			
		||||
   + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
 | 
			
		||||
2. 支持配置镜像以及众多第三方代理服务:
 | 
			
		||||
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] [CloseAI](https://console.closeai-asia.com/r/2412)
 | 
			
		||||
   + [x] 自定义渠道:例如各种未收录的第三方代理服务
 | 
			
		||||
3. 支持通过**负载均衡**的方式访问多个渠道。
 | 
			
		||||
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
 | 
			
		||||
5. 支持**多机部署**,[详见此处](#多机部署)。
 | 
			
		||||
6. 支持**令牌管理**,设置令牌的过期时间和额度。
 | 
			
		||||
7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
 | 
			
		||||
8. 支持**通道管理**,批量创建通道。
 | 
			
		||||
9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
 | 
			
		||||
10. 支持渠道**设置模型列表**。
 | 
			
		||||
11. 支持**查看额度明细**。
 | 
			
		||||
12. 支持**用户邀请奖励**。
 | 
			
		||||
13. 支持以美元为单位显示额度。
 | 
			
		||||
14. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
			
		||||
15. 支持模型映射,重定向用户的请求模型。
 | 
			
		||||
16. 支持失败自动重试。
 | 
			
		||||
17. 支持绘图接口。
 | 
			
		||||
18. 支持丰富的**自定义**设置,
 | 
			
		||||
2. 支持通过**负载均衡**的方式访问多个渠道。
 | 
			
		||||
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
 | 
			
		||||
4. 支持**多机部署**,[详见此处](#多机部署)。
 | 
			
		||||
5. 支持**令牌管理**,设置令牌的过期时间和额度。
 | 
			
		||||
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
 | 
			
		||||
7. 支持**通道管理**,批量创建通道。
 | 
			
		||||
8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
 | 
			
		||||
9. 支持渠道**设置模型列表**。
 | 
			
		||||
10. 支持**查看额度明细**。
 | 
			
		||||
11. 支持**用户邀请奖励**。
 | 
			
		||||
12. 支持以美元为单位显示额度。
 | 
			
		||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
			
		||||
14. 支持模型映射,重定向用户的请求模型。
 | 
			
		||||
15. 支持失败自动重试。
 | 
			
		||||
16. 支持绘图接口。
 | 
			
		||||
17. 支持丰富的**自定义**设置,
 | 
			
		||||
    1. 支持自定义系统名称,logo 以及页脚。
 | 
			
		||||
    2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
 | 
			
		||||
19. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
20. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
21. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
    + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
 | 
			
		||||
18. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
19. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
20. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
    + 邮箱登录注册以及通过邮箱进行密码重置。
 | 
			
		||||
    + [GitHub 开放授权](https://github.com/settings/applications/new)。
 | 
			
		||||
    + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
 | 
			
		||||
21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。
 | 
			
		||||
22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
			
		||||
 | 
			
		||||
## 部署
 | 
			
		||||
### 基于 Docker 进行部署
 | 
			
		||||
 
 | 
			
		||||
@@ -38,23 +38,11 @@ var PasswordLoginEnabled = true
 | 
			
		||||
var PasswordRegisterEnabled = true
 | 
			
		||||
var EmailVerificationEnabled = false
 | 
			
		||||
var GitHubOAuthEnabled = false
 | 
			
		||||
var DiscordOAuthEnabled = false
 | 
			
		||||
var WeChatAuthEnabled = false
 | 
			
		||||
var TurnstileCheckEnabled = false
 | 
			
		||||
var RegisterEnabled = true
 | 
			
		||||
 | 
			
		||||
var EmailDomainRestrictionEnabled = false
 | 
			
		||||
var EmailDomainWhitelist = []string{
 | 
			
		||||
	"gmail.com",
 | 
			
		||||
	"163.com",
 | 
			
		||||
	"126.com",
 | 
			
		||||
	"qq.com",
 | 
			
		||||
	"outlook.com",
 | 
			
		||||
	"hotmail.com",
 | 
			
		||||
	"icloud.com",
 | 
			
		||||
	"yahoo.com",
 | 
			
		||||
	"foxmail.com",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var LogConsumeEnabled = true
 | 
			
		||||
 | 
			
		||||
var SMTPServer = ""
 | 
			
		||||
@@ -66,6 +54,9 @@ var SMTPToken = ""
 | 
			
		||||
var GitHubClientId = ""
 | 
			
		||||
var GitHubClientSecret = ""
 | 
			
		||||
 | 
			
		||||
var DiscordClientId = ""
 | 
			
		||||
var DiscordClientSecret = ""
 | 
			
		||||
 | 
			
		||||
var WeChatServerAddress = ""
 | 
			
		||||
var WeChatServerToken = ""
 | 
			
		||||
var WeChatAccountQRCodeImageURL = ""
 | 
			
		||||
@@ -90,8 +81,6 @@ var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
 | 
			
		||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
 | 
			
		||||
var RequestInterval = time.Duration(requestInterval) * time.Second
 | 
			
		||||
 | 
			
		||||
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	RoleGuestUser  = 0
 | 
			
		||||
	RoleCommonUser = 1
 | 
			
		||||
@@ -166,31 +155,27 @@ const (
 | 
			
		||||
	ChannelTypePaLM      = 11
 | 
			
		||||
	ChannelTypeAPI2GPT   = 12
 | 
			
		||||
	ChannelTypeAIGC2D    = 13
 | 
			
		||||
	ChannelTypeAnthropic = 14
 | 
			
		||||
	ChannelTypeBaidu     = 15
 | 
			
		||||
	ChannelTypeZhipu     = 16
 | 
			
		||||
	ChannelTypeAli       = 17
 | 
			
		||||
	ChannelTypeXunfei    = 18
 | 
			
		||||
 | 
			
		||||
	// Reserve engineering for public projects
 | 
			
		||||
	ChannelTypeChatGPTWeb = 14 // Chanzhaoyu/chatgpt-web
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChannelBaseURLs = []string{
 | 
			
		||||
	"",                               // 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
 | 
			
		||||
	"https://api.anthropic.com",      // 14
 | 
			
		||||
	"https://aip.baidubce.com",       // 15
 | 
			
		||||
	"https://open.bigmodel.cn",       // 16
 | 
			
		||||
	"https://dashscope.aliyuncs.com", // 17
 | 
			
		||||
	"",                               // 18
 | 
			
		||||
	"",                              // 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
 | 
			
		||||
 | 
			
		||||
	// Reserve engineering for public projects
 | 
			
		||||
	"", // 14 // Chanzhaoyu/chatgpt-web
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								common/ip-gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								common/ip-gen.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GenerateIP() string {
 | 
			
		||||
	// Generate a random number between 20 and 240
 | 
			
		||||
	segment2 := rand.Intn(221) + 20
 | 
			
		||||
	segment3 := rand.Intn(256)
 | 
			
		||||
	segment4 := rand.Intn(256)
 | 
			
		||||
 | 
			
		||||
	ipAddress := fmt.Sprintf("104.%d.%d.%d", segment2, segment3, segment4)
 | 
			
		||||
	return ipAddress
 | 
			
		||||
}
 | 
			
		||||
@@ -4,11 +4,9 @@ import "encoding/json"
 | 
			
		||||
 | 
			
		||||
// ModelRatio
 | 
			
		||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
 | 
			
		||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
 | 
			
		||||
// https://openai.com/pricing
 | 
			
		||||
// TODO: when a new api is enabled, check the pricing here
 | 
			
		||||
// 1 === $0.002 / 1K tokens
 | 
			
		||||
// 1 === ¥0.014 / 1k tokens
 | 
			
		||||
var ModelRatio = map[string]float64{
 | 
			
		||||
	"gpt-4":                   15,
 | 
			
		||||
	"gpt-4-0314":              15,
 | 
			
		||||
@@ -38,18 +36,6 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"text-moderation-stable":  0.1,
 | 
			
		||||
	"text-moderation-latest":  0.1,
 | 
			
		||||
	"dall-e":                  8,
 | 
			
		||||
	"claude-instant-1":        0.75,
 | 
			
		||||
	"claude-2":                30,
 | 
			
		||||
	"ERNIE-Bot":               0.8572, // ¥0.012 / 1k tokens
 | 
			
		||||
	"ERNIE-Bot-turbo":         0.5715, // ¥0.008 / 1k tokens
 | 
			
		||||
	"Embedding-V1":            0.1429, // ¥0.002 / 1k tokens
 | 
			
		||||
	"PaLM-2":                  1,
 | 
			
		||||
	"chatglm_pro":             0.7143, // ¥0.01 / 1k tokens
 | 
			
		||||
	"chatglm_std":             0.3572, // ¥0.005 / 1k tokens
 | 
			
		||||
	"chatglm_lite":            0.1429, // ¥0.002 / 1k tokens
 | 
			
		||||
	"qwen-v1":                 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag
 | 
			
		||||
	"qwen-plus-v1":            0.5715, // Same as above
 | 
			
		||||
	"SparkDesk":               0.8572, // TBD
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ModelRatio2JSONString() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,27 @@
 | 
			
		||||
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 remainQuota int
 | 
			
		||||
	var usedQuota int
 | 
			
		||||
	var quota int
 | 
			
		||||
	var err error
 | 
			
		||||
	var token *model.Token
 | 
			
		||||
	var expiredTime int64
 | 
			
		||||
	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)
 | 
			
		||||
		expiredTime = token.ExpiredTime
 | 
			
		||||
		remainQuota = token.RemainQuota
 | 
			
		||||
		usedQuota = token.UsedQuota
 | 
			
		||||
		quota = token.RemainQuota
 | 
			
		||||
	} else {
 | 
			
		||||
		userId := c.GetInt("id")
 | 
			
		||||
		remainQuota, err = model.GetUserQuota(userId)
 | 
			
		||||
		usedQuota, err = model.GetUserUsedQuota(userId)
 | 
			
		||||
	}
 | 
			
		||||
	if expiredTime <= 0 {
 | 
			
		||||
		expiredTime = 0
 | 
			
		||||
		quota, err = model.GetUserQuota(userId)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		openAIError := OpenAIError{
 | 
			
		||||
@@ -36,7 +33,6 @@ func GetSubscription(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	quota := remainQuota + usedQuota
 | 
			
		||||
	amount := float64(quota)
 | 
			
		||||
	if common.DisplayInCurrencyEnabled {
 | 
			
		||||
		amount /= common.QuotaPerUnit
 | 
			
		||||
@@ -50,7 +46,7 @@ func GetSubscription(c *gin.Context) {
 | 
			
		||||
		SoftLimitUSD:       amount,
 | 
			
		||||
		HardLimitUSD:       amount,
 | 
			
		||||
		SystemHardLimitUSD: amount,
 | 
			
		||||
		AccessUntil:        expiredTime,
 | 
			
		||||
		AccessUntil:        expirationDate,
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(200, subscription)
 | 
			
		||||
	return
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,7 @@ func GetAuthHeader(token string) http.Header {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	req, err := http.NewRequest(method, url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -92,7 +93,7 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
 | 
			
		||||
	for k := range headers {
 | 
			
		||||
		req.Header.Add(k, headers.Get(k))
 | 
			
		||||
	}
 | 
			
		||||
	res, err := httpClient.Do(req)
 | 
			
		||||
	res, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,25 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"log"
 | 
			
		||||
	"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, *OpenAIError) {
 | 
			
		||||
func testChannel(channel *model.Channel, request ChatRequest) error {
 | 
			
		||||
	switch channel.Type {
 | 
			
		||||
	case common.ChannelTypePaLM:
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case common.ChannelTypeAnthropic:
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case common.ChannelTypeBaidu:
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case common.ChannelTypeZhipu:
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case common.ChannelTypeXunfei:
 | 
			
		||||
		return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
 | 
			
		||||
	case common.ChannelTypeAzure:
 | 
			
		||||
		request.Model = "gpt-35-turbo"
 | 
			
		||||
	default:
 | 
			
		||||
@@ -34,6 +28,11 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
 | 
			
		||||
	requestURL := common.ChannelBaseURLs[channel.Type]
 | 
			
		||||
	if channel.Type == common.ChannelTypeAzure {
 | 
			
		||||
		requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model)
 | 
			
		||||
	} else if channel.Type == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
		if channel.BaseURL != "" {
 | 
			
		||||
			requestURL = channel.BaseURL
 | 
			
		||||
		}
 | 
			
		||||
		requestURL += "/api/chat-process"
 | 
			
		||||
	} else {
 | 
			
		||||
		if channel.BaseURL != "" {
 | 
			
		||||
			requestURL = channel.BaseURL
 | 
			
		||||
@@ -42,12 +41,47 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	jsonData, err := json.Marshal(request)
 | 
			
		||||
 | 
			
		||||
	if channel.Type == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
		// Get system message from Message json, Role == "system"
 | 
			
		||||
		var systemMessage Message
 | 
			
		||||
 | 
			
		||||
		for _, message := range request.Messages {
 | 
			
		||||
			if message.Role == "system" {
 | 
			
		||||
				systemMessage = message
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var prompt string
 | 
			
		||||
 | 
			
		||||
		// Get all the Message, Roles from request.Messages, and format it into string by
 | 
			
		||||
		// ||> role: content
 | 
			
		||||
		for _, message := range request.Messages {
 | 
			
		||||
			// Exclude system message
 | 
			
		||||
			if message.Role == "system" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			prompt += "||> " + message.Role + ": " + message.Content + "\n"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Construct json data without adding escape character
 | 
			
		||||
		map1 := map[string]string{
 | 
			
		||||
			"prompt":        prompt,
 | 
			
		||||
			"systemMessage": systemMessage.Content,
 | 
			
		||||
			"temperature":   strconv.FormatFloat(request.Temperature, 'f', 2, 64),
 | 
			
		||||
			"top_p":         strconv.FormatFloat(request.TopP, 'f', 2, 64),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Convert map to json string
 | 
			
		||||
		jsonData, err = json.Marshal(map1)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if channel.Type == common.ChannelTypeAzure {
 | 
			
		||||
		req.Header.Set("api-key", channel.Key)
 | 
			
		||||
@@ -55,30 +89,153 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
 | 
			
		||||
		req.Header.Set("Authorization", "Bearer "+channel.Key)
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	resp, err := httpClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
 | 
			
		||||
	if channel.EnableIpRandomization {
 | 
			
		||||
		// Generate random IP
 | 
			
		||||
		ip := common.GenerateIP()
 | 
			
		||||
		req.Header.Set("X-Forwarded-For", ip)
 | 
			
		||||
		req.Header.Set("X-Real-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Client-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Forwarded-Host", ip)
 | 
			
		||||
		req.Header.Set("X-Originating-IP", ip)
 | 
			
		||||
		req.RemoteAddr = ip
 | 
			
		||||
		req.Header.Set("X-Remote-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Remote-Addr", ip)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		// Print the body in string
 | 
			
		||||
		if resp.Body != nil {
 | 
			
		||||
			buf := new(bytes.Buffer)
 | 
			
		||||
			buf.ReadFrom(resp.Body)
 | 
			
		||||
			return errors.New("error response: " + strconv.Itoa(resp.StatusCode) + " " + buf.String())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return errors.New("error response: " + strconv.Itoa(resp.StatusCode))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var 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 channel.Type != common.ChannelTypeChatGPTWeb {
 | 
			
		||||
			// If data has event: event content inside, remove it, it can be prefix or inside the data
 | 
			
		||||
			if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
 | 
			
		||||
				// Remove event: event in the front or back
 | 
			
		||||
				data = strings.TrimPrefix(data, "event: event")
 | 
			
		||||
				data = strings.TrimSuffix(data, "event: event")
 | 
			
		||||
				// Remove everything, only keep `data: {...}` <--- this is the json
 | 
			
		||||
				// Find the start and end indices of `data: {...}` substring
 | 
			
		||||
				startIndex := strings.Index(data, "data:")
 | 
			
		||||
				endIndex := strings.LastIndex(data, "}")
 | 
			
		||||
 | 
			
		||||
				// If both indices are found and end index is greater than start index
 | 
			
		||||
				if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
 | 
			
		||||
					// Extract the `data: {...}` substring
 | 
			
		||||
					data = data[startIndex : endIndex+1]
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Trim whitespace and newlines from the modified data string
 | 
			
		||||
				data = strings.TrimSpace(data)
 | 
			
		||||
			}
 | 
			
		||||
			if !strings.HasPrefix(data, "data:") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = data[6:]
 | 
			
		||||
			if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
				var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
				err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					// Prinnt the body in string
 | 
			
		||||
					buf := new(bytes.Buffer)
 | 
			
		||||
					buf.ReadFrom(resp.Body)
 | 
			
		||||
					common.SysError("error unmarshalling stream response: " + err.Error() + " " + buf.String())
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				for _, choice := range streamResponse.Choices {
 | 
			
		||||
					streamResponseText += choice.Delta.Content
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				done = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		} else if channel.Type == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
			// data may contain multiple json objects, so we need to split them
 | 
			
		||||
			// they are "{....}{....}{....}" or "{....}\n{....}\n{....}" or "{....}"
 | 
			
		||||
 | 
			
		||||
			// remove all spaces and newlines outside of json objects
 | 
			
		||||
			jsonObjs := strings.Split(data, "\n") // Split the data into multiple JSON objects
 | 
			
		||||
			for _, jsonObj := range jsonObjs {
 | 
			
		||||
				if jsonObj == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var chatResponse ChatGptWebChatResponse
 | 
			
		||||
				err = json.Unmarshal([]byte(jsonObj), &chatResponse)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					// Print the body in string
 | 
			
		||||
					buf := new(bytes.Buffer)
 | 
			
		||||
					buf.ReadFrom(resp.Body)
 | 
			
		||||
					common.SysError("error unmarshalling chat response: " + err.Error() + " " + buf.String())
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// if response role is assistant and contains delta, append the content to streamResponseText
 | 
			
		||||
				if chatResponse.Role == "assistant" && chatResponse.Detail != nil {
 | 
			
		||||
					for _, choice := range chatResponse.Detail.Choices {
 | 
			
		||||
						log.Print(choice.Delta.Content)
 | 
			
		||||
						streamResponseText += choice.Delta.Content
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	var response TextResponse
 | 
			
		||||
	err = json.NewDecoder(resp.Body).Decode(&response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
 | 
			
		||||
	// Check if streaming is complete and streamResponseText is populated
 | 
			
		||||
	if streamResponseText == "" || !done && channel.Type != common.ChannelTypeChatGPTWeb {
 | 
			
		||||
		return errors.New("Streaming not complete")
 | 
			
		||||
	}
 | 
			
		||||
	if response.Usage.CompletionTokens == 0 {
 | 
			
		||||
		return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil, nil
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -103,7 +260,7 @@ func TestChannel(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	testRequest := buildTestRequest()
 | 
			
		||||
	tik := time.Now()
 | 
			
		||||
	err, _ = testChannel(channel, *testRequest)
 | 
			
		||||
	err = testChannel(channel, *testRequest)
 | 
			
		||||
	tok := time.Now()
 | 
			
		||||
	milliseconds := tok.Sub(tik).Milliseconds()
 | 
			
		||||
	go channel.UpdateResponseTime(milliseconds)
 | 
			
		||||
@@ -167,14 +324,13 @@ func testAllChannels(notify bool) error {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			tik := time.Now()
 | 
			
		||||
			err, openaiErr := testChannel(channel, *testRequest)
 | 
			
		||||
			err := testChannel(channel, *testRequest)
 | 
			
		||||
			tok := time.Now()
 | 
			
		||||
			milliseconds := tok.Sub(tik).Milliseconds()
 | 
			
		||||
			if milliseconds > disableThreshold {
 | 
			
		||||
				err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
 | 
			
		||||
				disableChannel(channel.Id, channel.Name, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			if shouldDisableChannel(openaiErr) {
 | 
			
		||||
			if err != nil || milliseconds > disableThreshold {
 | 
			
		||||
				if milliseconds > disableThreshold {
 | 
			
		||||
					err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
 | 
			
		||||
				}
 | 
			
		||||
				disableChannel(channel.Id, channel.Name, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			channel.UpdateResponseTime(milliseconds)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
@@ -21,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,
 | 
			
		||||
@@ -80,22 +81,6 @@ func SendEmailVerification(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if common.EmailDomainRestrictionEnabled {
 | 
			
		||||
		allowed := false
 | 
			
		||||
		for _, domain := range common.EmailDomainWhitelist {
 | 
			
		||||
			if strings.HasSuffix(email, "@"+domain) {
 | 
			
		||||
				allowed = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !allowed {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if model.IsEmailAlreadyTaken(email) {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
@@ -145,9 +130,8 @@ func SendPasswordResetEmail(c *gin.Context) {
 | 
			
		||||
	link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
 | 
			
		||||
	subject := fmt.Sprintf("%s密码重置", common.SystemName)
 | 
			
		||||
	content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
 | 
			
		||||
		"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
 | 
			
		||||
		"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
 | 
			
		||||
		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
 | 
			
		||||
		"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
 | 
			
		||||
		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
 | 
			
		||||
	err := common.SendEmail(subject, email, content)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
 
 | 
			
		||||
@@ -253,111 +253,21 @@ func init() {
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "claude-instant-1",
 | 
			
		||||
			Id:         "ChatGLM",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "anturopic",
 | 
			
		||||
			OwnedBy:    "thudm",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "claude-instant-1",
 | 
			
		||||
			Root:       "ChatGLM",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "claude-2",
 | 
			
		||||
			Id:         "ChatGLM2",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "anturopic",
 | 
			
		||||
			OwnedBy:    "thudm",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "claude-2",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "ERNIE-Bot",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "baidu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "ERNIE-Bot",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "ERNIE-Bot-turbo",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "baidu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "ERNIE-Bot-turbo",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "Embedding-V1",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "baidu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "Embedding-V1",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "PaLM-2",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "google",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "PaLM-2",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "chatglm_pro",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "zhipu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "chatglm_pro",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "chatglm_std",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "zhipu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "chatglm_std",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "chatglm_lite",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "zhipu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "chatglm_lite",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "qwen-v1",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "ali",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "qwen-v1",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "qwen-plus-v1",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "ali",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "qwen-plus-v1",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "SparkDesk",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "xunfei",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "SparkDesk",
 | 
			
		||||
			Root:       "ChatGLM2",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,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{
 | 
			
		||||
@@ -50,14 +58,6 @@ func UpdateOption(c *gin.Context) {
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	case "EmailDomainRestrictionEnabled":
 | 
			
		||||
		if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	case "WeChatAuthEnabled":
 | 
			
		||||
		if option.Value == "true" && common.WeChatServerAddress == "" {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
 
 | 
			
		||||
@@ -1,240 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r
 | 
			
		||||
 | 
			
		||||
type AliMessage struct {
 | 
			
		||||
	User string `json:"user"`
 | 
			
		||||
	Bot  string `json:"bot"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliInput struct {
 | 
			
		||||
	Prompt  string       `json:"prompt"`
 | 
			
		||||
	History []AliMessage `json:"history"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliParameters struct {
 | 
			
		||||
	TopP         float64 `json:"top_p,omitempty"`
 | 
			
		||||
	TopK         int     `json:"top_k,omitempty"`
 | 
			
		||||
	Seed         uint64  `json:"seed,omitempty"`
 | 
			
		||||
	EnableSearch bool    `json:"enable_search,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliChatRequest struct {
 | 
			
		||||
	Model      string        `json:"model"`
 | 
			
		||||
	Input      AliInput      `json:"input"`
 | 
			
		||||
	Parameters AliParameters `json:"parameters,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliError struct {
 | 
			
		||||
	Code      string `json:"code"`
 | 
			
		||||
	Message   string `json:"message"`
 | 
			
		||||
	RequestId string `json:"request_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliUsage struct {
 | 
			
		||||
	InputTokens  int `json:"input_tokens"`
 | 
			
		||||
	OutputTokens int `json:"output_tokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliOutput struct {
 | 
			
		||||
	Text         string `json:"text"`
 | 
			
		||||
	FinishReason string `json:"finish_reason"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliChatResponse struct {
 | 
			
		||||
	Output AliOutput `json:"output"`
 | 
			
		||||
	Usage  AliUsage  `json:"usage"`
 | 
			
		||||
	AliError
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest {
 | 
			
		||||
	messages := make([]AliMessage, 0, len(request.Messages))
 | 
			
		||||
	prompt := ""
 | 
			
		||||
	for i := 0; i < len(request.Messages); i++ {
 | 
			
		||||
		message := request.Messages[i]
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
			messages = append(messages, AliMessage{
 | 
			
		||||
				User: message.Content,
 | 
			
		||||
				Bot:  "Okay",
 | 
			
		||||
			})
 | 
			
		||||
			continue
 | 
			
		||||
		} else {
 | 
			
		||||
			if i == len(request.Messages)-1 {
 | 
			
		||||
				prompt = message.Content
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			messages = append(messages, AliMessage{
 | 
			
		||||
				User: message.Content,
 | 
			
		||||
				Bot:  request.Messages[i+1].Content,
 | 
			
		||||
			})
 | 
			
		||||
			i++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &AliChatRequest{
 | 
			
		||||
		Model: request.Model,
 | 
			
		||||
		Input: AliInput{
 | 
			
		||||
			Prompt:  prompt,
 | 
			
		||||
			History: messages,
 | 
			
		||||
		},
 | 
			
		||||
		//Parameters: AliParameters{  // ChatGPT's parameters are not compatible with Ali's
 | 
			
		||||
		//	TopP: request.TopP,
 | 
			
		||||
		//	TopK: 50,
 | 
			
		||||
		//	//Seed:         0,
 | 
			
		||||
		//	//EnableSearch: false,
 | 
			
		||||
		//},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse {
 | 
			
		||||
	choice := OpenAITextResponseChoice{
 | 
			
		||||
		Index: 0,
 | 
			
		||||
		Message: Message{
 | 
			
		||||
			Role:    "assistant",
 | 
			
		||||
			Content: response.Output.Text,
 | 
			
		||||
		},
 | 
			
		||||
		FinishReason: response.Output.FinishReason,
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Id:      response.RequestId,
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Choices: []OpenAITextResponseChoice{choice},
 | 
			
		||||
		Usage: Usage{
 | 
			
		||||
			PromptTokens:     response.Usage.InputTokens,
 | 
			
		||||
			CompletionTokens: response.Usage.OutputTokens,
 | 
			
		||||
			TotalTokens:      response.Usage.InputTokens + response.Usage.OutputTokens,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = aliResponse.Output.Text
 | 
			
		||||
	choice.FinishReason = aliResponse.Output.FinishReason
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Id:      aliResponse.RequestId,
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Model:   "ernie-bot",
 | 
			
		||||
		Choices: []ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var usage Usage
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if len(data) < 5 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if data[:5] != "data:" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = data[5:]
 | 
			
		||||
			dataChan <- data
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	lastResponseText := ""
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			var aliResponse AliChatResponse
 | 
			
		||||
			err := json.Unmarshal([]byte(data), &aliResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			usage.PromptTokens += aliResponse.Usage.InputTokens
 | 
			
		||||
			usage.CompletionTokens += aliResponse.Usage.OutputTokens
 | 
			
		||||
			usage.TotalTokens += aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
 | 
			
		||||
			response := streamResponseAli2OpenAI(&aliResponse)
 | 
			
		||||
			response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText)
 | 
			
		||||
			lastResponseText = aliResponse.Output.Text
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, &usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func aliHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var aliResponse AliChatResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &aliResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if aliResponse.Code != "" {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: aliResponse.Message,
 | 
			
		||||
				Type:    aliResponse.Code,
 | 
			
		||||
				Param:   aliResponse.RequestId,
 | 
			
		||||
				Code:    aliResponse.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseAli2OpenAI(&aliResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
@@ -1,299 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
 | 
			
		||||
 | 
			
		||||
type BaiduTokenResponse struct {
 | 
			
		||||
	RefreshToken  string `json:"refresh_token"`
 | 
			
		||||
	ExpiresIn     int    `json:"expires_in"`
 | 
			
		||||
	SessionKey    string `json:"session_key"`
 | 
			
		||||
	AccessToken   string `json:"access_token"`
 | 
			
		||||
	Scope         string `json:"scope"`
 | 
			
		||||
	SessionSecret string `json:"session_secret"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduMessage struct {
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduChatRequest struct {
 | 
			
		||||
	Messages []BaiduMessage `json:"messages"`
 | 
			
		||||
	Stream   bool           `json:"stream"`
 | 
			
		||||
	UserId   string         `json:"user_id,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduError struct {
 | 
			
		||||
	ErrorCode int    `json:"error_code"`
 | 
			
		||||
	ErrorMsg  string `json:"error_msg"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduChatResponse struct {
 | 
			
		||||
	Id               string `json:"id"`
 | 
			
		||||
	Object           string `json:"object"`
 | 
			
		||||
	Created          int64  `json:"created"`
 | 
			
		||||
	Result           string `json:"result"`
 | 
			
		||||
	IsTruncated      bool   `json:"is_truncated"`
 | 
			
		||||
	NeedClearHistory bool   `json:"need_clear_history"`
 | 
			
		||||
	Usage            Usage  `json:"usage"`
 | 
			
		||||
	BaiduError
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduChatStreamResponse struct {
 | 
			
		||||
	BaiduChatResponse
 | 
			
		||||
	SentenceId int  `json:"sentence_id"`
 | 
			
		||||
	IsEnd      bool `json:"is_end"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduEmbeddingRequest struct {
 | 
			
		||||
	Input []string `json:"input"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduEmbeddingData struct {
 | 
			
		||||
	Object    string    `json:"object"`
 | 
			
		||||
	Embedding []float64 `json:"embedding"`
 | 
			
		||||
	Index     int       `json:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BaiduEmbeddingResponse struct {
 | 
			
		||||
	Id      string               `json:"id"`
 | 
			
		||||
	Object  string               `json:"object"`
 | 
			
		||||
	Created int64                `json:"created"`
 | 
			
		||||
	Data    []BaiduEmbeddingData `json:"data"`
 | 
			
		||||
	Usage   Usage                `json:"usage"`
 | 
			
		||||
	BaiduError
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
 | 
			
		||||
	messages := make([]BaiduMessage, 0, len(request.Messages))
 | 
			
		||||
	for _, message := range request.Messages {
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
			messages = append(messages, BaiduMessage{
 | 
			
		||||
				Role:    "user",
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
			messages = append(messages, BaiduMessage{
 | 
			
		||||
				Role:    "assistant",
 | 
			
		||||
				Content: "Okay",
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			messages = append(messages, BaiduMessage{
 | 
			
		||||
				Role:    message.Role,
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &BaiduChatRequest{
 | 
			
		||||
		Messages: messages,
 | 
			
		||||
		Stream:   request.Stream,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse {
 | 
			
		||||
	choice := OpenAITextResponseChoice{
 | 
			
		||||
		Index: 0,
 | 
			
		||||
		Message: Message{
 | 
			
		||||
			Role:    "assistant",
 | 
			
		||||
			Content: response.Result,
 | 
			
		||||
		},
 | 
			
		||||
		FinishReason: "stop",
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Id:      response.Id,
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: response.Created,
 | 
			
		||||
		Choices: []OpenAITextResponseChoice{choice},
 | 
			
		||||
		Usage:   response.Usage,
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = baiduResponse.Result
 | 
			
		||||
	choice.FinishReason = "stop"
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Id:      baiduResponse.Id,
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: baiduResponse.Created,
 | 
			
		||||
		Model:   "ernie-bot",
 | 
			
		||||
		Choices: []ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingRequest {
 | 
			
		||||
	baiduEmbeddingRequest := BaiduEmbeddingRequest{
 | 
			
		||||
		Input: nil,
 | 
			
		||||
	}
 | 
			
		||||
	switch request.Input.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		baiduEmbeddingRequest.Input = []string{request.Input.(string)}
 | 
			
		||||
	case []string:
 | 
			
		||||
		baiduEmbeddingRequest.Input = request.Input.([]string)
 | 
			
		||||
	}
 | 
			
		||||
	return &baiduEmbeddingRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func embeddingResponseBaidu2OpenAI(response *BaiduEmbeddingResponse) *OpenAIEmbeddingResponse {
 | 
			
		||||
	openAIEmbeddingResponse := OpenAIEmbeddingResponse{
 | 
			
		||||
		Object: "list",
 | 
			
		||||
		Data:   make([]OpenAIEmbeddingResponseItem, 0, len(response.Data)),
 | 
			
		||||
		Model:  "baidu-embedding",
 | 
			
		||||
		Usage:  response.Usage,
 | 
			
		||||
	}
 | 
			
		||||
	for _, item := range response.Data {
 | 
			
		||||
		openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, OpenAIEmbeddingResponseItem{
 | 
			
		||||
			Object:    item.Object,
 | 
			
		||||
			Index:     item.Index,
 | 
			
		||||
			Embedding: item.Embedding,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	return &openAIEmbeddingResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var usage Usage
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if len(data) < 6 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = data[6:]
 | 
			
		||||
			dataChan <- data
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			var baiduResponse BaiduChatStreamResponse
 | 
			
		||||
			err := json.Unmarshal([]byte(data), &baiduResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			usage.PromptTokens += baiduResponse.Usage.PromptTokens
 | 
			
		||||
			usage.CompletionTokens += baiduResponse.Usage.CompletionTokens
 | 
			
		||||
			usage.TotalTokens += baiduResponse.Usage.TotalTokens
 | 
			
		||||
			response := streamResponseBaidu2OpenAI(&baiduResponse)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, &usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func baiduHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var baiduResponse BaiduChatResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &baiduResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if baiduResponse.ErrorMsg != "" {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: baiduResponse.ErrorMsg,
 | 
			
		||||
				Type:    "baidu_error",
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    baiduResponse.ErrorCode,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseBaidu2OpenAI(&baiduResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var baiduResponse BaiduEmbeddingResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &baiduResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if baiduResponse.ErrorMsg != "" {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: baiduResponse.ErrorMsg,
 | 
			
		||||
				Type:    "baidu_error",
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    baiduResponse.ErrorCode,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
@@ -1,221 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ClaudeMetadata struct {
 | 
			
		||||
	UserId string `json:"user_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClaudeRequest struct {
 | 
			
		||||
	Model             string   `json:"model"`
 | 
			
		||||
	Prompt            string   `json:"prompt"`
 | 
			
		||||
	MaxTokensToSample int      `json:"max_tokens_to_sample"`
 | 
			
		||||
	StopSequences     []string `json:"stop_sequences,omitempty"`
 | 
			
		||||
	Temperature       float64  `json:"temperature,omitempty"`
 | 
			
		||||
	TopP              float64  `json:"top_p,omitempty"`
 | 
			
		||||
	TopK              int      `json:"top_k,omitempty"`
 | 
			
		||||
	//ClaudeMetadata    `json:"metadata,omitempty"`
 | 
			
		||||
	Stream bool `json:"stream,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClaudeError struct {
 | 
			
		||||
	Type    string `json:"type"`
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClaudeResponse struct {
 | 
			
		||||
	Completion string      `json:"completion"`
 | 
			
		||||
	StopReason string      `json:"stop_reason"`
 | 
			
		||||
	Model      string      `json:"model"`
 | 
			
		||||
	Error      ClaudeError `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func stopReasonClaude2OpenAI(reason string) string {
 | 
			
		||||
	switch reason {
 | 
			
		||||
	case "stop_sequence":
 | 
			
		||||
		return "stop"
 | 
			
		||||
	case "max_tokens":
 | 
			
		||||
		return "length"
 | 
			
		||||
	default:
 | 
			
		||||
		return reason
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
 | 
			
		||||
	claudeRequest := ClaudeRequest{
 | 
			
		||||
		Model:             textRequest.Model,
 | 
			
		||||
		Prompt:            "",
 | 
			
		||||
		MaxTokensToSample: textRequest.MaxTokens,
 | 
			
		||||
		StopSequences:     nil,
 | 
			
		||||
		Temperature:       textRequest.Temperature,
 | 
			
		||||
		TopP:              textRequest.TopP,
 | 
			
		||||
		Stream:            textRequest.Stream,
 | 
			
		||||
	}
 | 
			
		||||
	if claudeRequest.MaxTokensToSample == 0 {
 | 
			
		||||
		claudeRequest.MaxTokensToSample = 1000000
 | 
			
		||||
	}
 | 
			
		||||
	prompt := ""
 | 
			
		||||
	for _, message := range textRequest.Messages {
 | 
			
		||||
		if message.Role == "user" {
 | 
			
		||||
			prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
 | 
			
		||||
		} else if message.Role == "assistant" {
 | 
			
		||||
			prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
 | 
			
		||||
		} else if message.Role == "system" {
 | 
			
		||||
			prompt += fmt.Sprintf("\n\nSystem: %s", message.Content)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	prompt += "\n\nAssistant:"
 | 
			
		||||
	claudeRequest.Prompt = prompt
 | 
			
		||||
	return &claudeRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = claudeResponse.Completion
 | 
			
		||||
	choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason)
 | 
			
		||||
	var response ChatCompletionsStreamResponse
 | 
			
		||||
	response.Object = "chat.completion.chunk"
 | 
			
		||||
	response.Model = claudeResponse.Model
 | 
			
		||||
	response.Choices = []ChatCompletionsStreamResponseChoice{choice}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseClaude2OpenAI(claudeResponse *ClaudeResponse) *OpenAITextResponse {
 | 
			
		||||
	choice := OpenAITextResponseChoice{
 | 
			
		||||
		Index: 0,
 | 
			
		||||
		Message: Message{
 | 
			
		||||
			Role:    "assistant",
 | 
			
		||||
			Content: strings.TrimPrefix(claudeResponse.Completion, " "),
 | 
			
		||||
			Name:    nil,
 | 
			
		||||
		},
 | 
			
		||||
		FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Id:      fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Choices: []OpenAITextResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func claudeStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
 | 
			
		||||
	responseText := ""
 | 
			
		||||
	responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
 | 
			
		||||
	createdTime := common.GetTimestamp()
 | 
			
		||||
	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), "\r\n\r\n"); i >= 0 {
 | 
			
		||||
			return i + 4, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if !strings.HasPrefix(data, "event: completion") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = strings.TrimPrefix(data, "event: completion\r\ndata: ")
 | 
			
		||||
			dataChan <- data
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			// some implementations may add \r at the end of data
 | 
			
		||||
			data = strings.TrimSuffix(data, "\r")
 | 
			
		||||
			var claudeResponse ClaudeResponse
 | 
			
		||||
			err := json.Unmarshal([]byte(data), &claudeResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			responseText += claudeResponse.Completion
 | 
			
		||||
			response := streamResponseClaude2OpenAI(&claudeResponse)
 | 
			
		||||
			response.Id = responseId
 | 
			
		||||
			response.Created = createdTime
 | 
			
		||||
			jsonStr, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
 | 
			
		||||
	}
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func claudeHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	var claudeResponse ClaudeResponse
 | 
			
		||||
	err = json.Unmarshal(responseBody, &claudeResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if claudeResponse.Error.Type != "" {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: claudeResponse.Error.Message,
 | 
			
		||||
				Type:    claudeResponse.Error.Type,
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    claudeResponse.Error.Type,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseClaude2OpenAI(&claudeResponse)
 | 
			
		||||
	completionTokens := countTokenText(claudeResponse.Completion, model)
 | 
			
		||||
	usage := Usage{
 | 
			
		||||
		PromptTokens:     promptTokens,
 | 
			
		||||
		CompletionTokens: completionTokens,
 | 
			
		||||
		TotalTokens:      promptTokens + completionTokens,
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse.Usage = usage
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &usage
 | 
			
		||||
}
 | 
			
		||||
@@ -109,7 +109,8 @@ func relayImageHelper(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"))
 | 
			
		||||
 | 
			
		||||
	resp, err := httpClient.Do(req)
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,136 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) {
 | 
			
		||||
	responseText := ""
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if len(data) < 6 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if data[:6] != "data: " && data[:6] != "[DONE]" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			dataChan <- data
 | 
			
		||||
			data = data[6:]
 | 
			
		||||
			if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
				switch relayMode {
 | 
			
		||||
				case RelayModeChatCompletions:
 | 
			
		||||
					var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
					err := json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					for _, choice := range streamResponse.Choices {
 | 
			
		||||
						responseText += choice.Delta.Content
 | 
			
		||||
					}
 | 
			
		||||
				case RelayModeCompletions:
 | 
			
		||||
					var streamResponse CompletionsStreamResponse
 | 
			
		||||
					err := json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					for _, choice := range streamResponse.Choices {
 | 
			
		||||
						responseText += choice.Text
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			if strings.HasPrefix(data, "data: [DONE]") {
 | 
			
		||||
				data = data[:12]
 | 
			
		||||
			}
 | 
			
		||||
			// some implementations may add \r at the end of data
 | 
			
		||||
			data = strings.TrimSuffix(data, "\r")
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: data})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
 | 
			
		||||
	}
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var textResponse TextResponse
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
		responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
		}
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
		}
 | 
			
		||||
		err = json.Unmarshal(responseBody, &textResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
		}
 | 
			
		||||
		if textResponse.Error.Type != "" {
 | 
			
		||||
			return &OpenAIErrorWithStatusCode{
 | 
			
		||||
				OpenAIError: textResponse.Error,
 | 
			
		||||
				StatusCode:  resp.StatusCode,
 | 
			
		||||
			}, nil
 | 
			
		||||
		}
 | 
			
		||||
		// Reset response body
 | 
			
		||||
		resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
	}
 | 
			
		||||
	// We shouldn't set the header before we parse the response body, because the parse part may fail.
 | 
			
		||||
	// And then we will have to send an error response, but in this case, the header has already been set.
 | 
			
		||||
	// So the httpClient will be confused by the response.
 | 
			
		||||
	// For example, Postman will report error, and we cannot check the response at all.
 | 
			
		||||
	for k, v := range resp.Header {
 | 
			
		||||
		c.Writer.Header().Set(k, v[0])
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err := io.Copy(c.Writer, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, &textResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,10 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body
 | 
			
		||||
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#response-body
 | 
			
		||||
 | 
			
		||||
type PaLMChatMessage struct {
 | 
			
		||||
	Author  string `json:"author"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
@@ -22,188 +15,45 @@ type PaLMFilter struct {
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PaLMPrompt struct {
 | 
			
		||||
	Messages []PaLMChatMessage `json:"messages"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#request-body
 | 
			
		||||
type PaLMChatRequest struct {
 | 
			
		||||
	Prompt         PaLMPrompt `json:"prompt"`
 | 
			
		||||
	Temperature    float64    `json:"temperature,omitempty"`
 | 
			
		||||
	CandidateCount int        `json:"candidateCount,omitempty"`
 | 
			
		||||
	TopP           float64    `json:"topP,omitempty"`
 | 
			
		||||
	TopK           int        `json:"topK,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PaLMError struct {
 | 
			
		||||
	Code    int    `json:"code"`
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
	Status  string `json:"status"`
 | 
			
		||||
	Prompt         []Message `json:"prompt"`
 | 
			
		||||
	Temperature    float64   `json:"temperature"`
 | 
			
		||||
	CandidateCount int       `json:"candidateCount"`
 | 
			
		||||
	TopP           float64   `json:"topP"`
 | 
			
		||||
	TopK           int       `json:"topK"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage#response-body
 | 
			
		||||
type PaLMChatResponse struct {
 | 
			
		||||
	Candidates []PaLMChatMessage `json:"candidates"`
 | 
			
		||||
	Messages   []Message         `json:"messages"`
 | 
			
		||||
	Filters    []PaLMFilter      `json:"filters"`
 | 
			
		||||
	Error      PaLMError         `json:"error"`
 | 
			
		||||
	Candidates []Message    `json:"candidates"`
 | 
			
		||||
	Messages   []Message    `json:"messages"`
 | 
			
		||||
	Filters    []PaLMFilter `json:"filters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2PaLM(textRequest GeneralOpenAIRequest) *PaLMChatRequest {
 | 
			
		||||
	palmRequest := PaLMChatRequest{
 | 
			
		||||
		Prompt: PaLMPrompt{
 | 
			
		||||
			Messages: make([]PaLMChatMessage, 0, len(textRequest.Messages)),
 | 
			
		||||
		},
 | 
			
		||||
		Temperature:    textRequest.Temperature,
 | 
			
		||||
		CandidateCount: textRequest.N,
 | 
			
		||||
		TopP:           textRequest.TopP,
 | 
			
		||||
		TopK:           textRequest.MaxTokens,
 | 
			
		||||
	}
 | 
			
		||||
	for _, message := range textRequest.Messages {
 | 
			
		||||
		palmMessage := PaLMChatMessage{
 | 
			
		||||
			Content: message.Content,
 | 
			
		||||
		}
 | 
			
		||||
func relayPaLM(openAIRequest GeneralOpenAIRequest, c *gin.Context) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	// https://developers.generativeai.google/api/rest/generativelanguage/models/generateMessage
 | 
			
		||||
	messages := make([]PaLMChatMessage, 0, len(openAIRequest.Messages))
 | 
			
		||||
	for _, message := range openAIRequest.Messages {
 | 
			
		||||
		var author string
 | 
			
		||||
		if message.Role == "user" {
 | 
			
		||||
			palmMessage.Author = "0"
 | 
			
		||||
			author = "0"
 | 
			
		||||
		} else {
 | 
			
		||||
			palmMessage.Author = "1"
 | 
			
		||||
			author = "1"
 | 
			
		||||
		}
 | 
			
		||||
		palmRequest.Prompt.Messages = append(palmRequest.Prompt.Messages, palmMessage)
 | 
			
		||||
		messages = append(messages, PaLMChatMessage{
 | 
			
		||||
			Author:  author,
 | 
			
		||||
			Content: message.Content,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	return &palmRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responsePaLM2OpenAI(response *PaLMChatResponse) *OpenAITextResponse {
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Choices: make([]OpenAITextResponseChoice, 0, len(response.Candidates)),
 | 
			
		||||
	}
 | 
			
		||||
	for i, candidate := range response.Candidates {
 | 
			
		||||
		choice := OpenAITextResponseChoice{
 | 
			
		||||
			Index: i,
 | 
			
		||||
			Message: Message{
 | 
			
		||||
				Role:    "assistant",
 | 
			
		||||
				Content: candidate.Content,
 | 
			
		||||
			},
 | 
			
		||||
			FinishReason: "stop",
 | 
			
		||||
		}
 | 
			
		||||
		fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	if len(palmResponse.Candidates) > 0 {
 | 
			
		||||
		choice.Delta.Content = palmResponse.Candidates[0].Content
 | 
			
		||||
	}
 | 
			
		||||
	choice.FinishReason = "stop"
 | 
			
		||||
	var response ChatCompletionsStreamResponse
 | 
			
		||||
	response.Object = "chat.completion.chunk"
 | 
			
		||||
	response.Model = "palm2"
 | 
			
		||||
	response.Choices = []ChatCompletionsStreamResponseChoice{choice}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func palmStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
 | 
			
		||||
	responseText := ""
 | 
			
		||||
	responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
 | 
			
		||||
	createdTime := common.GetTimestamp()
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("error reading stream response: " + err.Error())
 | 
			
		||||
			stopChan <- true
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("error closing stream response: " + err.Error())
 | 
			
		||||
			stopChan <- true
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		var palmResponse PaLMChatResponse
 | 
			
		||||
		err = json.Unmarshal(responseBody, &palmResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
			stopChan <- true
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		fullTextResponse := streamResponsePaLM2OpenAI(&palmResponse)
 | 
			
		||||
		fullTextResponse.Id = responseId
 | 
			
		||||
		fullTextResponse.Created = createdTime
 | 
			
		||||
		if len(palmResponse.Candidates) > 0 {
 | 
			
		||||
			responseText = palmResponse.Candidates[0].Content
 | 
			
		||||
		}
 | 
			
		||||
		jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
			stopChan <- true
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		dataChan <- string(jsonResponse)
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + data})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
 | 
			
		||||
	}
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func palmHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	var palmResponse PaLMChatResponse
 | 
			
		||||
	err = json.Unmarshal(responseBody, &palmResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if palmResponse.Error.Code != 0 || len(palmResponse.Candidates) == 0 {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: palmResponse.Error.Message,
 | 
			
		||||
				Type:    palmResponse.Error.Status,
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    palmResponse.Error.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responsePaLM2OpenAI(&palmResponse)
 | 
			
		||||
	completionTokens := countTokenText(palmResponse.Candidates[0].Content, model)
 | 
			
		||||
	usage := Usage{
 | 
			
		||||
		PromptTokens:     promptTokens,
 | 
			
		||||
		CompletionTokens: completionTokens,
 | 
			
		||||
		TotalTokens:      promptTokens + completionTokens,
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse.Usage = usage
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &usage
 | 
			
		||||
	request := PaLMChatRequest{
 | 
			
		||||
		Prompt:         nil,
 | 
			
		||||
		Temperature:    openAIRequest.Temperature,
 | 
			
		||||
		CandidateCount: openAIRequest.N,
 | 
			
		||||
		TopP:           openAIRequest.TopP,
 | 
			
		||||
		TopK:           openAIRequest.MaxTokens,
 | 
			
		||||
	}
 | 
			
		||||
	// TODO: forward request to PaLM & convert response
 | 
			
		||||
	fmt.Print(request)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,22 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	APITypeOpenAI = iota
 | 
			
		||||
	APITypeClaude
 | 
			
		||||
	APITypePaLM
 | 
			
		||||
	APITypeBaidu
 | 
			
		||||
	APITypeZhipu
 | 
			
		||||
	APITypeAli
 | 
			
		||||
	APITypeXunfei
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var httpClient *http.Client
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	httpClient = &http.Client{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	channelType := c.GetInt("channel")
 | 
			
		||||
	tokenId := c.GetInt("token_id")
 | 
			
		||||
@@ -75,7 +62,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	// map model name
 | 
			
		||||
	modelMapping := c.GetString("model_mapping")
 | 
			
		||||
	isModelMapped := false
 | 
			
		||||
	if modelMapping != "" && modelMapping != "{}" {
 | 
			
		||||
	if modelMapping != "" {
 | 
			
		||||
		modelMap := make(map[string]string)
 | 
			
		||||
		err := json.Unmarshal([]byte(modelMapping), &modelMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -86,83 +73,60 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			isModelMapped = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	apiType := APITypeOpenAI
 | 
			
		||||
	switch channelType {
 | 
			
		||||
	case common.ChannelTypeAnthropic:
 | 
			
		||||
		apiType = APITypeClaude
 | 
			
		||||
	case common.ChannelTypeBaidu:
 | 
			
		||||
		apiType = APITypeBaidu
 | 
			
		||||
	case common.ChannelTypePaLM:
 | 
			
		||||
		apiType = APITypePaLM
 | 
			
		||||
	case common.ChannelTypeZhipu:
 | 
			
		||||
		apiType = APITypeZhipu
 | 
			
		||||
	case common.ChannelTypeAli:
 | 
			
		||||
		apiType = APITypeAli
 | 
			
		||||
	case common.ChannelTypeXunfei:
 | 
			
		||||
		apiType = APITypeXunfei
 | 
			
		||||
 | 
			
		||||
	// Get token info
 | 
			
		||||
	tokenInfo, err := model.GetTokenById(tokenId)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "get_token_info_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hasModelAvailable := func() bool {
 | 
			
		||||
		for _, token := range strings.Split(tokenInfo.Models, ",") {
 | 
			
		||||
			if token == textRequest.Model {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if !hasModelAvailable {
 | 
			
		||||
		return errorWrapper(errors.New("model not available for use"), "model_not_available_for_use", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
			
		||||
	requestURL := c.Request.URL.String()
 | 
			
		||||
	if c.GetString("base_url") != "" {
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
	}
 | 
			
		||||
	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
			
		||||
	switch apiType {
 | 
			
		||||
	case APITypeOpenAI:
 | 
			
		||||
		if channelType == common.ChannelTypeAzure {
 | 
			
		||||
			// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
 | 
			
		||||
			query := c.Request.URL.Query()
 | 
			
		||||
			apiVersion := query.Get("api-version")
 | 
			
		||||
			if apiVersion == "" {
 | 
			
		||||
				apiVersion = c.GetString("api_version")
 | 
			
		||||
			}
 | 
			
		||||
			requestURL := strings.Split(requestURL, "?")[0]
 | 
			
		||||
			requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 | 
			
		||||
			baseURL = c.GetString("base_url")
 | 
			
		||||
			task := strings.TrimPrefix(requestURL, "/v1/")
 | 
			
		||||
			model_ := textRequest.Model
 | 
			
		||||
			model_ = strings.Replace(model_, ".", "", -1)
 | 
			
		||||
			// https://github.com/songquanpeng/one-api/issues/67
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0301")
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0314")
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0613")
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
			
		||||
	if channelType == common.ChannelTypeAzure {
 | 
			
		||||
		// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
 | 
			
		||||
		query := c.Request.URL.Query()
 | 
			
		||||
		apiVersion := query.Get("api-version")
 | 
			
		||||
		if apiVersion == "" {
 | 
			
		||||
			apiVersion = c.GetString("api_version")
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeClaude:
 | 
			
		||||
		fullRequestURL = "https://api.anthropic.com/v1/complete"
 | 
			
		||||
		if baseURL != "" {
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s/v1/complete", baseURL)
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeBaidu:
 | 
			
		||||
		switch textRequest.Model {
 | 
			
		||||
		case "ERNIE-Bot":
 | 
			
		||||
			fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"
 | 
			
		||||
		case "ERNIE-Bot-turbo":
 | 
			
		||||
			fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
 | 
			
		||||
		case "BLOOMZ-7B":
 | 
			
		||||
			fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1"
 | 
			
		||||
		case "Embedding-V1":
 | 
			
		||||
			fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1"
 | 
			
		||||
		}
 | 
			
		||||
		apiKey := c.Request.Header.Get("Authorization")
 | 
			
		||||
		apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
			
		||||
		fullRequestURL += "?access_token=" + apiKey // TODO: access token expire in 30 days
 | 
			
		||||
	case APITypePaLM:
 | 
			
		||||
		fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
 | 
			
		||||
		if baseURL != "" {
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s/v1beta2/models/chat-bison-001:generateMessage", baseURL)
 | 
			
		||||
		}
 | 
			
		||||
		apiKey := c.Request.Header.Get("Authorization")
 | 
			
		||||
		apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
			
		||||
		fullRequestURL += "?key=" + apiKey
 | 
			
		||||
	case APITypeZhipu:
 | 
			
		||||
		method := "invoke"
 | 
			
		||||
		if textRequest.Stream {
 | 
			
		||||
			method = "sse-invoke"
 | 
			
		||||
		}
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method)
 | 
			
		||||
	case APITypeAli:
 | 
			
		||||
		fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
 | 
			
		||||
		requestURL := strings.Split(requestURL, "?")[0]
 | 
			
		||||
		requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
		task := strings.TrimPrefix(requestURL, "/v1/")
 | 
			
		||||
		model_ := textRequest.Model
 | 
			
		||||
		model_ = strings.Replace(model_, ".", "", -1)
 | 
			
		||||
		// https://github.com/songquanpeng/one-api/issues/67
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0301")
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0314")
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0613")
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
			
		||||
	} else if channelType == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
		// remove /v1/chat/completions from request url
 | 
			
		||||
		requestURL := strings.Split(requestURL, "/v1/chat/completions")[0]
 | 
			
		||||
		requestURL += "/api/chat-process"
 | 
			
		||||
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
			
		||||
	} else if channelType == common.ChannelTypePaLM {
 | 
			
		||||
		err := relayPaLM(textRequest, c)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	var promptTokens int
 | 
			
		||||
	var completionTokens int
 | 
			
		||||
@@ -205,107 +169,135 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	} else {
 | 
			
		||||
		requestBody = c.Request.Body
 | 
			
		||||
	}
 | 
			
		||||
	switch apiType {
 | 
			
		||||
	case APITypeClaude:
 | 
			
		||||
		claudeRequest := requestOpenAI2Claude(textRequest)
 | 
			
		||||
		jsonStr, err := json.Marshal(claudeRequest)
 | 
			
		||||
		bodyBytes, err := io.ReadAll(c.Request.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	case APITypeBaidu:
 | 
			
		||||
		var jsonData []byte
 | 
			
		||||
		var err error
 | 
			
		||||
		switch relayMode {
 | 
			
		||||
		case RelayModeEmbeddings:
 | 
			
		||||
			baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(textRequest)
 | 
			
		||||
			jsonData, err = json.Marshal(baiduEmbeddingRequest)
 | 
			
		||||
		default:
 | 
			
		||||
			baiduRequest := requestOpenAI2Baidu(textRequest)
 | 
			
		||||
			jsonData, err = json.Marshal(baiduRequest)
 | 
			
		||||
			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, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
			return errorWrapper(err, "unmarshal_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonData)
 | 
			
		||||
	case APITypePaLM:
 | 
			
		||||
		palmRequest := requestOpenAI2PaLM(textRequest)
 | 
			
		||||
		jsonStr, err := json.Marshal(palmRequest)
 | 
			
		||||
 | 
			
		||||
		// 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_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
			return errorWrapper(err, "marshal_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	case APITypeZhipu:
 | 
			
		||||
		zhipuRequest := requestOpenAI2Zhipu(textRequest)
 | 
			
		||||
		jsonStr, err := json.Marshal(zhipuRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	case APITypeAli:
 | 
			
		||||
		aliRequest := requestOpenAI2Ali(textRequest)
 | 
			
		||||
		jsonStr, err := json.Marshal(aliRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
 | 
			
		||||
		requestBody = bytes.NewBuffer(bodyBytes)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var req *http.Request
 | 
			
		||||
	var resp *http.Response
 | 
			
		||||
	isStream := textRequest.Stream
 | 
			
		||||
	if channelType == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
		// Get system message from Message json, Role == "system"
 | 
			
		||||
		var reqBody ChatRequest
 | 
			
		||||
		var systemMessage Message
 | 
			
		||||
 | 
			
		||||
		// Parse requestBody into systemMessage
 | 
			
		||||
		err := json.NewDecoder(requestBody).Decode(&reqBody)
 | 
			
		||||
 | 
			
		||||
	if apiType != APITypeXunfei { // cause xunfei use websocket
 | 
			
		||||
		req, err = http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
			return errorWrapper(err, "decode_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		apiKey := c.Request.Header.Get("Authorization")
 | 
			
		||||
		apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
			
		||||
		switch apiType {
 | 
			
		||||
		case APITypeOpenAI:
 | 
			
		||||
			if channelType == common.ChannelTypeAzure {
 | 
			
		||||
				req.Header.Set("api-key", apiKey)
 | 
			
		||||
			} else {
 | 
			
		||||
				req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
			}
 | 
			
		||||
		case APITypeClaude:
 | 
			
		||||
			req.Header.Set("x-api-key", apiKey)
 | 
			
		||||
			anthropicVersion := c.Request.Header.Get("anthropic-version")
 | 
			
		||||
			if anthropicVersion == "" {
 | 
			
		||||
				anthropicVersion = "2023-06-01"
 | 
			
		||||
			}
 | 
			
		||||
			req.Header.Set("anthropic-version", anthropicVersion)
 | 
			
		||||
		case APITypeZhipu:
 | 
			
		||||
			token := getZhipuToken(apiKey)
 | 
			
		||||
			req.Header.Set("Authorization", token)
 | 
			
		||||
		case APITypeAli:
 | 
			
		||||
			req.Header.Set("Authorization", "Bearer "+apiKey)
 | 
			
		||||
			if textRequest.Stream {
 | 
			
		||||
				req.Header.Set("X-DashScope-SSE", "enable")
 | 
			
		||||
 | 
			
		||||
		for _, message := range reqBody.Messages {
 | 
			
		||||
			if message.Role == "system" {
 | 
			
		||||
				systemMessage = message
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		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"))
 | 
			
		||||
		resp, err = httpClient.Do(req)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
 | 
			
		||||
 | 
			
		||||
		var prompt string
 | 
			
		||||
 | 
			
		||||
		// Get all the Message, Roles from request.Messages, and format it into string by
 | 
			
		||||
		// ||> role: content
 | 
			
		||||
		for _, message := range reqBody.Messages {
 | 
			
		||||
			// Exclude system message
 | 
			
		||||
			if message.Role == "system" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			prompt += "||> " + message.Role + ": " + message.Content + "\n"
 | 
			
		||||
		}
 | 
			
		||||
		err = req.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
 | 
			
		||||
		// Construct json data without adding escape character
 | 
			
		||||
		map1 := map[string]string{
 | 
			
		||||
			"prompt":        prompt,
 | 
			
		||||
			"systemMessage": systemMessage.Content,
 | 
			
		||||
			"temperature":   strconv.FormatFloat(reqBody.Temperature, 'f', 2, 64),
 | 
			
		||||
			"top_p":         strconv.FormatFloat(reqBody.TopP, 'f', 2, 64),
 | 
			
		||||
		}
 | 
			
		||||
		err = c.Request.Body.Close()
 | 
			
		||||
 | 
			
		||||
		// Convert map to json string
 | 
			
		||||
		jsonData, err := json.Marshal(map1)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			return errorWrapper(err, "marshal_json_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		isStream = strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
 | 
			
		||||
		// Convert json string to io.Reader
 | 
			
		||||
		requestBody = bytes.NewReader(jsonData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	if channelType == common.ChannelTypeAzure {
 | 
			
		||||
		key := c.Request.Header.Get("Authorization")
 | 
			
		||||
		key = strings.TrimPrefix(key, "Bearer ")
 | 
			
		||||
		req.Header.Set("api-key", key)
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("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"))
 | 
			
		||||
 | 
			
		||||
	if c.GetBool("enable_ip_randomization") == true {
 | 
			
		||||
		// Generate random IP
 | 
			
		||||
		ip := common.GenerateIP()
 | 
			
		||||
		req.Header.Set("X-Forwarded-For", ip)
 | 
			
		||||
		req.Header.Set("X-Real-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Client-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Forwarded-Host", ip)
 | 
			
		||||
		req.Header.Set("X-Originating-IP", ip)
 | 
			
		||||
		req.RemoteAddr = ip
 | 
			
		||||
		req.Header.Set("X-Remote-IP", ip)
 | 
			
		||||
		req.Header.Set("X-Remote-Addr", ip)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		// Print the body in string
 | 
			
		||||
		if resp.Body != nil {
 | 
			
		||||
			buf := new(bytes.Buffer)
 | 
			
		||||
			buf.ReadFrom(resp.Body)
 | 
			
		||||
			log.Printf("Error Channel (%s): %s", baseURL, buf.String())
 | 
			
		||||
			return errorWrapper(err, "request_failed", resp.StatusCode)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return errorWrapper(err, "request_failed", resp.StatusCode)
 | 
			
		||||
	}
 | 
			
		||||
	err = req.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 TextResponse
 | 
			
		||||
	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") || strings.HasPrefix(resp.Header.Get("Content-Type"), "application/octet-stream")
 | 
			
		||||
	var streamResponseText string
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if consumeQuota {
 | 
			
		||||
@@ -317,10 +309,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			if strings.HasPrefix(textRequest.Model, "gpt-4") {
 | 
			
		||||
				completionRatio = 2
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			promptTokens = textResponse.Usage.PromptTokens
 | 
			
		||||
			completionTokens = textResponse.Usage.CompletionTokens
 | 
			
		||||
 | 
			
		||||
			if isStream {
 | 
			
		||||
				completionTokens = countTokenText(streamResponseText, textRequest.Model)
 | 
			
		||||
			} else {
 | 
			
		||||
				promptTokens = textResponse.Usage.PromptTokens
 | 
			
		||||
				completionTokens = textResponse.Usage.CompletionTokens
 | 
			
		||||
			}
 | 
			
		||||
			quota = promptTokens + int(float64(completionTokens)*completionRatio)
 | 
			
		||||
			quota = int(float64(quota) * ratio)
 | 
			
		||||
			if ratio != 0 && quota <= 0 {
 | 
			
		||||
@@ -351,155 +345,194 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	switch apiType {
 | 
			
		||||
	case APITypeOpenAI:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, responseText := openaiStreamHandler(c, resp, relayMode)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			textResponse.Usage.PromptTokens = promptTokens
 | 
			
		||||
			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
 | 
			
		||||
			return nil
 | 
			
		||||
 | 
			
		||||
	if isStream {
 | 
			
		||||
		dataChan := make(chan string)
 | 
			
		||||
		stopChan := make(chan bool)
 | 
			
		||||
 | 
			
		||||
		if channelType == common.ChannelTypeChatGPTWeb {
 | 
			
		||||
			scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
			go func() {
 | 
			
		||||
				for scanner.Scan() {
 | 
			
		||||
					var chatResponse ChatGptWebChatResponse
 | 
			
		||||
					err = json.Unmarshal(scanner.Bytes(), &chatResponse)
 | 
			
		||||
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						log.Println("error unmarshal chat response: " + err.Error())
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// if response role is assistant and contains delta, append the content to streamResponseText
 | 
			
		||||
					if chatResponse.Role == "assistant" && chatResponse.Detail != nil {
 | 
			
		||||
						for _, choice := range chatResponse.Detail.Choices {
 | 
			
		||||
							streamResponseText += choice.Delta.Content
 | 
			
		||||
 | 
			
		||||
							returnObj := map[string]interface{}{
 | 
			
		||||
								"id":      chatResponse.ID,
 | 
			
		||||
								"object":  chatResponse.Detail.Object,
 | 
			
		||||
								"created": chatResponse.Detail.Created,
 | 
			
		||||
								"model":   chatResponse.Detail.Model,
 | 
			
		||||
								"choices": []map[string]interface{}{
 | 
			
		||||
									// set finish_reason to null in json
 | 
			
		||||
									{
 | 
			
		||||
										"finish_reason": nil,
 | 
			
		||||
										"index":         0,
 | 
			
		||||
										"delta": map[string]interface{}{
 | 
			
		||||
											"content": choice.Delta.Content,
 | 
			
		||||
										},
 | 
			
		||||
									},
 | 
			
		||||
								},
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							jsonData, _ := json.Marshal(returnObj)
 | 
			
		||||
 | 
			
		||||
							dataChan <- "data: " + string(jsonData)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				stopChan <- true
 | 
			
		||||
			}()
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := openaiHandler(c, resp, consumeQuota)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
			scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
			scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
				if atEOF && len(data) == 0 {
 | 
			
		||||
					return 0, nil, nil
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
					return i + 1, data[0:i], nil
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if atEOF {
 | 
			
		||||
					return len(data), data, nil
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return 0, nil, nil
 | 
			
		||||
			})
 | 
			
		||||
			go func() {
 | 
			
		||||
				for scanner.Scan() {
 | 
			
		||||
					data := scanner.Text()
 | 
			
		||||
					if len(data) < 6 { // must be something wrong!
 | 
			
		||||
						// common.SysError("invalid stream response: " + data)
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					// If data has event: event content inside, remove it, it can be prefix or inside the data
 | 
			
		||||
					if strings.HasPrefix(data, "event:") || strings.Contains(data, "event:") {
 | 
			
		||||
						// Remove event: event in the front or back
 | 
			
		||||
						data = strings.TrimPrefix(data, "event: event")
 | 
			
		||||
						data = strings.TrimSuffix(data, "event: event")
 | 
			
		||||
						// Remove everything, only keep `data: {...}` <--- this is the json
 | 
			
		||||
						// Find the start and end indices of `data: {...}` substring
 | 
			
		||||
						startIndex := strings.Index(data, "data:")
 | 
			
		||||
						endIndex := strings.LastIndex(data, "}")
 | 
			
		||||
 | 
			
		||||
						// If both indices are found and end index is greater than start index
 | 
			
		||||
						if startIndex != -1 && endIndex != -1 && endIndex > startIndex {
 | 
			
		||||
							// Extract the `data: {...}` substring
 | 
			
		||||
							data = data[startIndex : endIndex+1]
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						// Trim whitespace and newlines from the modified data string
 | 
			
		||||
						data = strings.TrimSpace(data)
 | 
			
		||||
					}
 | 
			
		||||
					if !strings.HasPrefix(data, "data:") {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					dataChan <- data
 | 
			
		||||
					data = data[6:]
 | 
			
		||||
					if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
						switch relayMode {
 | 
			
		||||
						case RelayModeChatCompletions:
 | 
			
		||||
							var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
							err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
								return
 | 
			
		||||
							}
 | 
			
		||||
							for _, choice := range streamResponse.Choices {
 | 
			
		||||
								streamResponseText += choice.Delta.Content
 | 
			
		||||
							}
 | 
			
		||||
						case RelayModeCompletions:
 | 
			
		||||
							var streamResponse CompletionsStreamResponse
 | 
			
		||||
							err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
								return
 | 
			
		||||
							}
 | 
			
		||||
							for _, choice := range streamResponse.Choices {
 | 
			
		||||
								streamResponseText += choice.Text
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				stopChan <- true
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeClaude:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, responseText := claudeStreamHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
 | 
			
		||||
		c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
		c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
		c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
		c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
		c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
		c.Stream(func(w io.Writer) bool {
 | 
			
		||||
			select {
 | 
			
		||||
			case data := <-dataChan:
 | 
			
		||||
				if strings.HasPrefix(data, "data: [DONE]") {
 | 
			
		||||
					data = data[:12]
 | 
			
		||||
				}
 | 
			
		||||
				// some implementations may add \r at the end of data
 | 
			
		||||
				data = strings.TrimSuffix(data, "\r")
 | 
			
		||||
				c.Render(-1, common.CustomEvent{Data: data})
 | 
			
		||||
				return true
 | 
			
		||||
			case <-stopChan:
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			textResponse.Usage.PromptTokens = promptTokens
 | 
			
		||||
			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		})
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeBaidu:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, usage := baiduStreamHandler(c, resp)
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		if consumeQuota {
 | 
			
		||||
			responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			var err *OpenAIErrorWithStatusCode
 | 
			
		||||
			var usage *Usage
 | 
			
		||||
			switch relayMode {
 | 
			
		||||
			case RelayModeEmbeddings:
 | 
			
		||||
				err, usage = baiduEmbeddingHandler(c, resp)
 | 
			
		||||
			default:
 | 
			
		||||
				err, usage = baiduHandler(c, resp)
 | 
			
		||||
				return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			err = resp.Body.Close()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
				return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			err = json.Unmarshal(responseBody, &textResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
			if textResponse.Error.Type != "" {
 | 
			
		||||
				return &OpenAIErrorWithStatusCode{
 | 
			
		||||
					OpenAIError: textResponse.Error,
 | 
			
		||||
					StatusCode:  resp.StatusCode,
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// Reset response body
 | 
			
		||||
			resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
		}
 | 
			
		||||
	case APITypePaLM:
 | 
			
		||||
		if textRequest.Stream { // PaLM2 API does not support stream
 | 
			
		||||
			err, responseText := palmStreamHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			textResponse.Usage.PromptTokens = promptTokens
 | 
			
		||||
			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := palmHandler(c, resp, promptTokens, textRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		// We shouldn't set the header before we parse the response body, because the parse part may fail.
 | 
			
		||||
		// And then we will have to send an error response, but in this case, the header has already been set.
 | 
			
		||||
		// So the client will be confused by the response.
 | 
			
		||||
		// For example, Postman will report error, and we cannot check the response at all.
 | 
			
		||||
		for k, v := range resp.Header {
 | 
			
		||||
			c.Writer.Header().Set(k, v[0])
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeZhipu:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, usage := zhipuStreamHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			// zhipu's API does not return prompt tokens & completion tokens
 | 
			
		||||
			textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := zhipuHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			// zhipu's API does not return prompt tokens & completion tokens
 | 
			
		||||
			textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
 | 
			
		||||
			return nil
 | 
			
		||||
		c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
		_, err = io.Copy(c.Writer, resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeAli:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, usage := aliStreamHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := aliHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeXunfei:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			auth := c.Request.Header.Get("Authorization")
 | 
			
		||||
			auth = strings.TrimPrefix(auth, "Bearer ")
 | 
			
		||||
			splits := strings.Split(auth, "|")
 | 
			
		||||
			if len(splits) != 3 {
 | 
			
		||||
				return errorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest)
 | 
			
		||||
			}
 | 
			
		||||
			err, usage := xunfeiStreamHandler(c, textRequest, splits[0], splits[1], splits[2])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return errorWrapper(errors.New("xunfei api does not support non-stream mode"), "invalid_api_type", http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -91,16 +91,3 @@ func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatus
 | 
			
		||||
		StatusCode:  statusCode,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shouldDisableChannel(err *OpenAIError) bool {
 | 
			
		||||
	if !common.AutomaticDisableChannelEnabled {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,278 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/hmac"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://console.xfyun.cn/services/cbm
 | 
			
		||||
// https://www.xfyun.cn/doc/spark/Web.html
 | 
			
		||||
 | 
			
		||||
type XunfeiMessage struct {
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XunfeiChatRequest struct {
 | 
			
		||||
	Header struct {
 | 
			
		||||
		AppId string `json:"app_id"`
 | 
			
		||||
	} `json:"header"`
 | 
			
		||||
	Parameter struct {
 | 
			
		||||
		Chat struct {
 | 
			
		||||
			Domain      string  `json:"domain,omitempty"`
 | 
			
		||||
			Temperature float64 `json:"temperature,omitempty"`
 | 
			
		||||
			TopK        int     `json:"top_k,omitempty"`
 | 
			
		||||
			MaxTokens   int     `json:"max_tokens,omitempty"`
 | 
			
		||||
			Auditing    bool    `json:"auditing,omitempty"`
 | 
			
		||||
		} `json:"chat"`
 | 
			
		||||
	} `json:"parameter"`
 | 
			
		||||
	Payload struct {
 | 
			
		||||
		Message struct {
 | 
			
		||||
			Text []XunfeiMessage `json:"text"`
 | 
			
		||||
		} `json:"message"`
 | 
			
		||||
	} `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XunfeiChatResponseTextItem struct {
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Index   int    `json:"index"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XunfeiChatResponse struct {
 | 
			
		||||
	Header struct {
 | 
			
		||||
		Code    int    `json:"code"`
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
		Sid     string `json:"sid"`
 | 
			
		||||
		Status  int    `json:"status"`
 | 
			
		||||
	} `json:"header"`
 | 
			
		||||
	Payload struct {
 | 
			
		||||
		Choices struct {
 | 
			
		||||
			Status int                          `json:"status"`
 | 
			
		||||
			Seq    int                          `json:"seq"`
 | 
			
		||||
			Text   []XunfeiChatResponseTextItem `json:"text"`
 | 
			
		||||
		} `json:"choices"`
 | 
			
		||||
	} `json:"payload"`
 | 
			
		||||
	Usage struct {
 | 
			
		||||
		//Text struct {
 | 
			
		||||
		//	QuestionTokens   string `json:"question_tokens"`
 | 
			
		||||
		//	PromptTokens     string `json:"prompt_tokens"`
 | 
			
		||||
		//	CompletionTokens string `json:"completion_tokens"`
 | 
			
		||||
		//	TotalTokens      string `json:"total_tokens"`
 | 
			
		||||
		//} `json:"text"`
 | 
			
		||||
		Text Usage `json:"text"`
 | 
			
		||||
	} `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
 | 
			
		||||
	messages := make([]XunfeiMessage, 0, len(request.Messages))
 | 
			
		||||
	for _, message := range request.Messages {
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
			messages = append(messages, XunfeiMessage{
 | 
			
		||||
				Role:    "user",
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
			messages = append(messages, XunfeiMessage{
 | 
			
		||||
				Role:    "assistant",
 | 
			
		||||
				Content: "Okay",
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			messages = append(messages, XunfeiMessage{
 | 
			
		||||
				Role:    message.Role,
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	xunfeiRequest := XunfeiChatRequest{}
 | 
			
		||||
	xunfeiRequest.Header.AppId = xunfeiAppId
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.Domain = "general"
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.Temperature = request.Temperature
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.TopK = request.N
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens
 | 
			
		||||
	xunfeiRequest.Payload.Message.Text = messages
 | 
			
		||||
	return &xunfeiRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
 | 
			
		||||
	if len(response.Payload.Choices.Text) == 0 {
 | 
			
		||||
		response.Payload.Choices.Text = []XunfeiChatResponseTextItem{
 | 
			
		||||
			{
 | 
			
		||||
				Content: "",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	choice := OpenAITextResponseChoice{
 | 
			
		||||
		Index: 0,
 | 
			
		||||
		Message: Message{
 | 
			
		||||
			Role:    "assistant",
 | 
			
		||||
			Content: response.Payload.Choices.Text[0].Content,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Choices: []OpenAITextResponseChoice{choice},
 | 
			
		||||
		Usage:   response.Usage.Text,
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	if len(xunfeiResponse.Payload.Choices.Text) == 0 {
 | 
			
		||||
		xunfeiResponse.Payload.Choices.Text = []XunfeiChatResponseTextItem{
 | 
			
		||||
			{
 | 
			
		||||
				Content: "",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Model:   "SparkDesk",
 | 
			
		||||
		Choices: []ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string {
 | 
			
		||||
	HmacWithShaToBase64 := func(algorithm, data, key string) string {
 | 
			
		||||
		mac := hmac.New(sha256.New, []byte(key))
 | 
			
		||||
		mac.Write([]byte(data))
 | 
			
		||||
		encodeData := mac.Sum(nil)
 | 
			
		||||
		return base64.StdEncoding.EncodeToString(encodeData)
 | 
			
		||||
	}
 | 
			
		||||
	ul, err := url.Parse(hostUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
	date := time.Now().UTC().Format(time.RFC1123)
 | 
			
		||||
	signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
 | 
			
		||||
	sign := strings.Join(signString, "\n")
 | 
			
		||||
	sha := HmacWithShaToBase64("hmac-sha256", sign, apiSecret)
 | 
			
		||||
	authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
 | 
			
		||||
		"hmac-sha256", "host date request-line", sha)
 | 
			
		||||
	authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
 | 
			
		||||
	v := url.Values{}
 | 
			
		||||
	v.Add("host", ul.Host)
 | 
			
		||||
	v.Add("date", date)
 | 
			
		||||
	v.Add("authorization", authorization)
 | 
			
		||||
	callUrl := hostUrl + "?" + v.Encode()
 | 
			
		||||
	return callUrl
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var usage Usage
 | 
			
		||||
	d := websocket.Dialer{
 | 
			
		||||
		HandshakeTimeout: 5 * time.Second,
 | 
			
		||||
	}
 | 
			
		||||
	hostUrl := "wss://aichat.xf-yun.com/v1/chat"
 | 
			
		||||
	conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil)
 | 
			
		||||
	if err != nil || resp.StatusCode != 101 {
 | 
			
		||||
		return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	data := requestOpenAI2Xunfei(textRequest, appId)
 | 
			
		||||
	err = conn.WriteJSON(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	dataChan := make(chan XunfeiChatResponse)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			_, msg, err := conn.ReadMessage()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error reading stream response: " + err.Error())
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			var response XunfeiChatResponse
 | 
			
		||||
			err = json.Unmarshal(msg, &response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			dataChan <- response
 | 
			
		||||
			if response.Payload.Choices.Status == 2 {
 | 
			
		||||
				err := conn.Close()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					common.SysError("error closing websocket connection: " + err.Error())
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case xunfeiResponse := <-dataChan:
 | 
			
		||||
			usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens
 | 
			
		||||
			usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens
 | 
			
		||||
			usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens
 | 
			
		||||
			response := streamResponseXunfei2OpenAI(&xunfeiResponse)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	return nil, &usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func xunfeiHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var xunfeiResponse XunfeiChatResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &xunfeiResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if xunfeiResponse.Header.Code != 0 {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: xunfeiResponse.Header.Message,
 | 
			
		||||
				Type:    "xunfei_error",
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    xunfeiResponse.Header.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseXunfei2OpenAI(&xunfeiResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
@@ -1,301 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/golang-jwt/jwt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://open.bigmodel.cn/doc/api#chatglm_std
 | 
			
		||||
// chatglm_std, chatglm_lite
 | 
			
		||||
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke
 | 
			
		||||
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke
 | 
			
		||||
 | 
			
		||||
type ZhipuMessage struct {
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ZhipuRequest struct {
 | 
			
		||||
	Prompt      []ZhipuMessage `json:"prompt"`
 | 
			
		||||
	Temperature float64        `json:"temperature,omitempty"`
 | 
			
		||||
	TopP        float64        `json:"top_p,omitempty"`
 | 
			
		||||
	RequestId   string         `json:"request_id,omitempty"`
 | 
			
		||||
	Incremental bool           `json:"incremental,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ZhipuResponseData struct {
 | 
			
		||||
	TaskId     string         `json:"task_id"`
 | 
			
		||||
	RequestId  string         `json:"request_id"`
 | 
			
		||||
	TaskStatus string         `json:"task_status"`
 | 
			
		||||
	Choices    []ZhipuMessage `json:"choices"`
 | 
			
		||||
	Usage      `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ZhipuResponse struct {
 | 
			
		||||
	Code    int               `json:"code"`
 | 
			
		||||
	Msg     string            `json:"msg"`
 | 
			
		||||
	Success bool              `json:"success"`
 | 
			
		||||
	Data    ZhipuResponseData `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ZhipuStreamMetaResponse struct {
 | 
			
		||||
	RequestId  string `json:"request_id"`
 | 
			
		||||
	TaskId     string `json:"task_id"`
 | 
			
		||||
	TaskStatus string `json:"task_status"`
 | 
			
		||||
	Usage      `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type zhipuTokenData struct {
 | 
			
		||||
	Token      string
 | 
			
		||||
	ExpiryTime time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var zhipuTokens sync.Map
 | 
			
		||||
var expSeconds int64 = 24 * 3600
 | 
			
		||||
 | 
			
		||||
func getZhipuToken(apikey string) string {
 | 
			
		||||
	data, ok := zhipuTokens.Load(apikey)
 | 
			
		||||
	if ok {
 | 
			
		||||
		tokenData := data.(zhipuTokenData)
 | 
			
		||||
		if time.Now().Before(tokenData.ExpiryTime) {
 | 
			
		||||
			return tokenData.Token
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	split := strings.Split(apikey, ".")
 | 
			
		||||
	if len(split) != 2 {
 | 
			
		||||
		common.SysError("invalid zhipu key: " + apikey)
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	id := split[0]
 | 
			
		||||
	secret := split[1]
 | 
			
		||||
 | 
			
		||||
	expMillis := time.Now().Add(time.Duration(expSeconds)*time.Second).UnixNano() / 1e6
 | 
			
		||||
	expiryTime := time.Now().Add(time.Duration(expSeconds) * time.Second)
 | 
			
		||||
 | 
			
		||||
	timestamp := time.Now().UnixNano() / 1e6
 | 
			
		||||
 | 
			
		||||
	payload := jwt.MapClaims{
 | 
			
		||||
		"api_key":   id,
 | 
			
		||||
		"exp":       expMillis,
 | 
			
		||||
		"timestamp": timestamp,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
 | 
			
		||||
 | 
			
		||||
	token.Header["alg"] = "HS256"
 | 
			
		||||
	token.Header["sign_type"] = "SIGN"
 | 
			
		||||
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(secret))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	zhipuTokens.Store(apikey, zhipuTokenData{
 | 
			
		||||
		Token:      tokenString,
 | 
			
		||||
		ExpiryTime: expiryTime,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return tokenString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
 | 
			
		||||
	messages := make([]ZhipuMessage, 0, len(request.Messages))
 | 
			
		||||
	for _, message := range request.Messages {
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
			messages = append(messages, ZhipuMessage{
 | 
			
		||||
				Role:    "system",
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
			messages = append(messages, ZhipuMessage{
 | 
			
		||||
				Role:    "user",
 | 
			
		||||
				Content: "Okay",
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			messages = append(messages, ZhipuMessage{
 | 
			
		||||
				Role:    message.Role,
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &ZhipuRequest{
 | 
			
		||||
		Prompt:      messages,
 | 
			
		||||
		Temperature: request.Temperature,
 | 
			
		||||
		TopP:        request.TopP,
 | 
			
		||||
		Incremental: false,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Id:      response.Data.TaskId,
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Choices: make([]OpenAITextResponseChoice, 0, len(response.Data.Choices)),
 | 
			
		||||
		Usage:   response.Data.Usage,
 | 
			
		||||
	}
 | 
			
		||||
	for i, choice := range response.Data.Choices {
 | 
			
		||||
		openaiChoice := OpenAITextResponseChoice{
 | 
			
		||||
			Index: i,
 | 
			
		||||
			Message: Message{
 | 
			
		||||
				Role:    choice.Role,
 | 
			
		||||
				Content: strings.Trim(choice.Content, "\""),
 | 
			
		||||
			},
 | 
			
		||||
			FinishReason: "",
 | 
			
		||||
		}
 | 
			
		||||
		if i == len(response.Data.Choices)-1 {
 | 
			
		||||
			openaiChoice.FinishReason = "stop"
 | 
			
		||||
		}
 | 
			
		||||
		fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice)
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = zhipuResponse
 | 
			
		||||
	choice.FinishReason = ""
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Model:   "chatglm",
 | 
			
		||||
		Choices: []ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
 | 
			
		||||
	var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
	choice.Delta.Content = ""
 | 
			
		||||
	choice.FinishReason = "stop"
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Id:      zhipuResponse.RequestId,
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Model:   "chatglm",
 | 
			
		||||
		Choices: []ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response, &zhipuResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var usage *Usage
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	metaChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			data = strings.Trim(data, "\"")
 | 
			
		||||
			if len(data) < 5 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if data[:5] == "data:" {
 | 
			
		||||
				dataChan <- data[5:]
 | 
			
		||||
			} else if data[:5] == "meta:" {
 | 
			
		||||
				metaChan <- data[5:]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
	c.Writer.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
			
		||||
	c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			response := streamResponseZhipu2OpenAI(data)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case data := <-metaChan:
 | 
			
		||||
			var zhipuResponse ZhipuStreamMetaResponse
 | 
			
		||||
			err := json.Unmarshal([]byte(data), &zhipuResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			response, zhipuUsage := streamMetaResponseZhipu2OpenAI(&zhipuResponse)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			usage = zhipuUsage
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func zhipuHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var zhipuResponse ZhipuResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &zhipuResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if !zhipuResponse.Success {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: zhipuResponse.Msg,
 | 
			
		||||
				Type:    "zhipu_error",
 | 
			
		||||
				Param:   "",
 | 
			
		||||
				Code:    zhipuResponse.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseZhipu2OpenAI(&zhipuResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
@@ -45,7 +45,11 @@ type GeneralOpenAIRequest struct {
 | 
			
		||||
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"`
 | 
			
		||||
	// -1.0 to 1.0
 | 
			
		||||
	Temperature float64 `json:"temperature"`
 | 
			
		||||
	TopP        float64 `json:"top_p"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TextRequest struct {
 | 
			
		||||
@@ -85,33 +89,6 @@ type TextResponse struct {
 | 
			
		||||
	Error OpenAIError `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAITextResponseChoice struct {
 | 
			
		||||
	Index        int `json:"index"`
 | 
			
		||||
	Message      `json:"message"`
 | 
			
		||||
	FinishReason string `json:"finish_reason"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAITextResponse struct {
 | 
			
		||||
	Id      string                     `json:"id"`
 | 
			
		||||
	Object  string                     `json:"object"`
 | 
			
		||||
	Created int64                      `json:"created"`
 | 
			
		||||
	Choices []OpenAITextResponseChoice `json:"choices"`
 | 
			
		||||
	Usage   `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAIEmbeddingResponseItem struct {
 | 
			
		||||
	Object    string    `json:"object"`
 | 
			
		||||
	Index     int       `json:"index"`
 | 
			
		||||
	Embedding []float64 `json:"embedding"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAIEmbeddingResponse struct {
 | 
			
		||||
	Object string                        `json:"object"`
 | 
			
		||||
	Data   []OpenAIEmbeddingResponseItem `json:"data"`
 | 
			
		||||
	Model  string                        `json:"model"`
 | 
			
		||||
	Usage  `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageResponse struct {
 | 
			
		||||
	Created int `json:"created"`
 | 
			
		||||
	Data    []struct {
 | 
			
		||||
@@ -119,19 +96,13 @@ type ImageResponse struct {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatCompletionsStreamResponseChoice struct {
 | 
			
		||||
	Delta struct {
 | 
			
		||||
		Content string `json:"content"`
 | 
			
		||||
	} `json:"delta"`
 | 
			
		||||
	FinishReason string `json:"finish_reason,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatCompletionsStreamResponse struct {
 | 
			
		||||
	Id      string                                `json:"id"`
 | 
			
		||||
	Object  string                                `json:"object"`
 | 
			
		||||
	Created int64                                 `json:"created"`
 | 
			
		||||
	Model   string                                `json:"model"`
 | 
			
		||||
	Choices []ChatCompletionsStreamResponseChoice `json:"choices"`
 | 
			
		||||
	Choices []struct {
 | 
			
		||||
		Delta struct {
 | 
			
		||||
			Content string `json:"content"`
 | 
			
		||||
		} `json:"delta"`
 | 
			
		||||
		FinishReason string `json:"finish_reason"`
 | 
			
		||||
	} `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CompletionsStreamResponse struct {
 | 
			
		||||
@@ -141,6 +112,32 @@ type CompletionsStreamResponse struct {
 | 
			
		||||
	} `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatGptWebDetail struct {
 | 
			
		||||
	ID      string             `json:"id"`
 | 
			
		||||
	Object  string             `json:"object"`
 | 
			
		||||
	Created int                `json:"created"`
 | 
			
		||||
	Model   string             `json:"model"`
 | 
			
		||||
	Choices []ChatGptWebChoice `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatGptWebChoice struct {
 | 
			
		||||
	Delta struct {
 | 
			
		||||
		Content string `json:"content"`
 | 
			
		||||
		Role    string `json:"role"`
 | 
			
		||||
	} `json:"delta"`
 | 
			
		||||
	Index         int    `json:"index"`
 | 
			
		||||
	Finish_Reason string `json:"finish_reason"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatGptWebChatResponse struct {
 | 
			
		||||
	Role            string            `json:"role"`
 | 
			
		||||
	ID              string            `json:"id"`
 | 
			
		||||
	ParentMessageID string            `json:"parentMessageId"`
 | 
			
		||||
	Text            string            `json:"text"`
 | 
			
		||||
	Delta           string            `json:"delta"`
 | 
			
		||||
	Detail          *ChatGptWebDetail `json:"detail"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Relay(c *gin.Context) {
 | 
			
		||||
	relayMode := RelayModeUnknown
 | 
			
		||||
	if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") {
 | 
			
		||||
@@ -184,7 +181,7 @@ func Relay(c *gin.Context) {
 | 
			
		||||
		channelId := c.GetInt("channel_id")
 | 
			
		||||
		common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
 | 
			
		||||
		// https://platform.openai.com/docs/guides/error-codes/api-errors
 | 
			
		||||
		if shouldDisableChannel(&err.OpenAIError) {
 | 
			
		||||
		if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated") {
 | 
			
		||||
			channelId := c.GetInt("channel_id")
 | 
			
		||||
			channelName := c.GetString("channel_name")
 | 
			
		||||
			disableChannel(channelId, channelName, err.Message)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetAllTokens(c *gin.Context) {
 | 
			
		||||
@@ -125,6 +126,7 @@ func AddToken(c *gin.Context) {
 | 
			
		||||
		ExpiredTime:    token.ExpiredTime,
 | 
			
		||||
		RemainQuota:    token.RemainQuota,
 | 
			
		||||
		UnlimitedQuota: token.UnlimitedQuota,
 | 
			
		||||
		Models:         token.Models,
 | 
			
		||||
	}
 | 
			
		||||
	err = cleanToken.Insert()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -203,6 +205,7 @@ func UpdateToken(c *gin.Context) {
 | 
			
		||||
		cleanToken.ExpiredTime = token.ExpiredTime
 | 
			
		||||
		cleanToken.RemainQuota = token.RemainQuota
 | 
			
		||||
		cleanToken.UnlimitedQuota = token.UnlimitedQuota
 | 
			
		||||
		cleanToken.Models = token.Models
 | 
			
		||||
	}
 | 
			
		||||
	err = cleanToken.Update()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -483,7 +483,7 @@ func DeleteSelf(c *gin.Context) {
 | 
			
		||||
	if user.Role == common.RoleRootUser {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": "不能删除超级管理员账户",
 | 
			
		||||
			"message": "无权删除超级管理员",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 && VITE_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"]
 | 
			
		||||
							
								
								
									
										40
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								go.mod
									
									
									
									
									
								
							@@ -9,49 +9,51 @@ 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/golang-jwt/jwt v3.2.2+incompatible
 | 
			
		||||
	github.com/google/uuid v1.3.0
 | 
			
		||||
	github.com/gorilla/websocket v1.5.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.5
 | 
			
		||||
	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/bytedance/sonic v1.9.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 | 
			
		||||
	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // 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
 | 
			
		||||
	github.com/gorilla/securecookie v1.1.1 // indirect
 | 
			
		||||
	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/pelletier/go-toml/v2 v2.0.9 // 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
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,8 +1,14 @@
 | 
			
		||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
 | 
			
		||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
 | 
			
		||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 | 
			
		||||
github.com/bytedance/sonic v1.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=
 | 
			
		||||
@@ -14,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=
 | 
			
		||||
@@ -45,17 +53,25 @@ 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-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 | 
			
		||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 | 
			
		||||
github.com/golang/protobuf v1.3.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=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
@@ -65,21 +81,24 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
 | 
			
		||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
 | 
			
		||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 | 
			
		||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 | 
			
		||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
 | 
			
		||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 | 
			
		||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 | 
			
		||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 | 
			
		||||
github.com/jinzhu/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=
 | 
			
		||||
@@ -111,11 +130,21 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 | 
			
		||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 | 
			
		||||
github.com/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/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/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=
 | 
			
		||||
@@ -132,6 +161,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 | 
			
		||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 | 
			
		||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
			
		||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 | 
			
		||||
@@ -143,34 +173,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=
 | 
			
		||||
@@ -187,9 +239,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=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								i18n/en.json
									
									
									
									
									
								
							@@ -443,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",
 | 
			
		||||
@@ -504,11 +505,25 @@
 | 
			
		||||
  "请输入自定义渠道的 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",
 | 
			
		||||
  "请为通道命名": "Please name the channel",
 | 
			
		||||
  "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
 | 
			
		||||
  "模型重定向": "Model redirection",
 | 
			
		||||
  "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
 | 
			
		||||
  "注意,": "Note that, ",
 | 
			
		||||
  ",图片演示。": "related image demo.",
 | 
			
		||||
  "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!"
 | 
			
		||||
  "允许通过 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, ",
 | 
			
		||||
  "请选择此密钥支持的模型": "Please select the models supported by this key",
 | 
			
		||||
  "将IP随机地址传递给HTTP头": "Pass the IP random address to the HTTP header"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								main.go
									
									
									
									
									
								
							@@ -2,9 +2,6 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-contrib/sessions/cookie"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/controller"
 | 
			
		||||
	"one-api/middleware"
 | 
			
		||||
@@ -12,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
 | 
			
		||||
@@ -21,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" {
 | 
			
		||||
@@ -54,7 +58,6 @@ func main() {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		common.SyncFrequency = frequency
 | 
			
		||||
		go model.SyncOptions(frequency)
 | 
			
		||||
		if common.RedisEnabled {
 | 
			
		||||
			go model.SyncChannelCache(frequency)
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,7 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
			}
 | 
			
		||||
			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
 | 
			
		||||
				message := "无可用渠道"
 | 
			
		||||
				if channel != nil {
 | 
			
		||||
					common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
 | 
			
		||||
					message = "数据库一致性已被破坏,请联系管理员"
 | 
			
		||||
@@ -105,6 +105,7 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
		c.Set("channel_id", channel.Id)
 | 
			
		||||
		c.Set("channel_name", channel.Name)
 | 
			
		||||
		c.Set("model_mapping", channel.ModelMapping)
 | 
			
		||||
		c.Set("enable_ip_randomization", channel.EnableIpRandomization)
 | 
			
		||||
		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
 | 
			
		||||
		c.Set("base_url", channel.BaseURL)
 | 
			
		||||
		if channel.Type == common.ChannelTypeAzure {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,11 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	TokenCacheSeconds         = common.SyncFrequency
 | 
			
		||||
	UserId2GroupCacheSeconds  = common.SyncFrequency
 | 
			
		||||
	UserId2QuotaCacheSeconds  = common.SyncFrequency
 | 
			
		||||
	UserId2StatusCacheSeconds = common.SyncFrequency
 | 
			
		||||
const (
 | 
			
		||||
	TokenCacheSeconds         = 60 * 60
 | 
			
		||||
	UserId2GroupCacheSeconds  = 60 * 60
 | 
			
		||||
	UserId2QuotaCacheSeconds  = 10 * 60
 | 
			
		||||
	UserId2StatusCacheSeconds = 60 * 60
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func CacheGetTokenByKey(key string) (*Token, error) {
 | 
			
		||||
@@ -35,7 +35,7 @@ func CacheGetTokenByKey(key string) (*Token, error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), TokenCacheSeconds*time.Second)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("Redis set token error: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
@@ -55,7 +55,7 @@ func CacheGetUserGroup(id int) (group string, err error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, time.Duration(UserId2GroupCacheSeconds)*time.Second)
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, UserId2GroupCacheSeconds*time.Second)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("Redis set user group error: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
@@ -73,7 +73,7 @@ func CacheGetUserQuota(id int) (quota int, err error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("Redis set user quota error: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
@@ -91,7 +91,7 @@ func CacheUpdateUserQuota(id int) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
 | 
			
		||||
	err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -106,7 +106,7 @@ func CacheIsUserEnabled(userId int) bool {
 | 
			
		||||
			status = common.UserStatusEnabled
 | 
			
		||||
		}
 | 
			
		||||
		enabled = fmt.Sprintf("%d", status)
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, time.Duration(UserId2StatusCacheSeconds)*time.Second)
 | 
			
		||||
		err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, UserId2StatusCacheSeconds*time.Second)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("Redis set user enabled error: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Channel struct {
 | 
			
		||||
@@ -23,6 +24,9 @@ type Channel struct {
 | 
			
		||||
	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:''"`
 | 
			
		||||
 | 
			
		||||
	// Additional fields, default value is false
 | 
			
		||||
	EnableIpRandomization bool `json:"enable_ip_randomization"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ 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)
 | 
			
		||||
@@ -39,8 +40,6 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
 | 
			
		||||
	common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
 | 
			
		||||
	common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
 | 
			
		||||
	common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled)
 | 
			
		||||
	common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",")
 | 
			
		||||
	common.OptionMap["SMTPServer"] = ""
 | 
			
		||||
	common.OptionMap["SMTPFrom"] = ""
 | 
			
		||||
	common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
 | 
			
		||||
@@ -55,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"] = ""
 | 
			
		||||
@@ -135,6 +136,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":
 | 
			
		||||
@@ -143,8 +146,6 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
			common.TurnstileCheckEnabled = boolValue
 | 
			
		||||
		case "RegisterEnabled":
 | 
			
		||||
			common.RegisterEnabled = boolValue
 | 
			
		||||
		case "EmailDomainRestrictionEnabled":
 | 
			
		||||
			common.EmailDomainRestrictionEnabled = boolValue
 | 
			
		||||
		case "AutomaticDisableChannelEnabled":
 | 
			
		||||
			common.AutomaticDisableChannelEnabled = boolValue
 | 
			
		||||
		case "ApproximateTokenEnabled":
 | 
			
		||||
@@ -158,8 +159,6 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	switch key {
 | 
			
		||||
	case "EmailDomainWhitelist":
 | 
			
		||||
		common.EmailDomainWhitelist = strings.Split(value, ",")
 | 
			
		||||
	case "SMTPServer":
 | 
			
		||||
		common.SMTPServer = value
 | 
			
		||||
	case "SMTPPort":
 | 
			
		||||
@@ -177,6 +176,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":
 | 
			
		||||
 
 | 
			
		||||
@@ -51,21 +51,20 @@ func Redeem(key string, userId int) (quota int, err error) {
 | 
			
		||||
	redemption := &Redemption{}
 | 
			
		||||
 | 
			
		||||
	err = DB.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		err := tx.Set("gorm:query_option", "FOR UPDATE").Where("`key` = ?", key).First(redemption).Error
 | 
			
		||||
		err := DB.Where("`key` = ?", key).First(redemption).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("无效的兑换码")
 | 
			
		||||
		}
 | 
			
		||||
		if redemption.Status != common.RedemptionCodeStatusEnabled {
 | 
			
		||||
			return errors.New("该兑换码已被使用")
 | 
			
		||||
		}
 | 
			
		||||
		err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
 | 
			
		||||
		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 = tx.Save(redemption).Error
 | 
			
		||||
		return err
 | 
			
		||||
		return redemption.SelectUpdate()
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, errors.New("兑换失败," + err.Error())
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,9 @@ package model
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Token struct {
 | 
			
		||||
@@ -19,6 +20,7 @@ type Token struct {
 | 
			
		||||
	RemainQuota    int    `json:"remain_quota" gorm:"default:0"`
 | 
			
		||||
	UnlimitedQuota bool   `json:"unlimited_quota" gorm:"default:false"`
 | 
			
		||||
	UsedQuota      int    `json:"used_quota" gorm:"default:0"` // used quota
 | 
			
		||||
	Models         string `json:"models"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
 | 
			
		||||
@@ -99,7 +101,7 @@ func (token *Token) Insert() error {
 | 
			
		||||
// Update Make sure your token's fields is completed, because this will update non-zero values
 | 
			
		||||
func (token *Token) Update() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota").Updates(token).Error
 | 
			
		||||
	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models").Updates(token).Error
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,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)
 | 
			
		||||
@@ -28,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("/")
 | 
			
		||||
@@ -61,11 +62,13 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
			optionRoute.PUT("/", controller.UpdateOption)
 | 
			
		||||
		}
 | 
			
		||||
		channelRoute := apiRouter.Group("/channel")
 | 
			
		||||
 | 
			
		||||
		channelRoute.Use(middleware.UserAuth()).GET("/models", controller.ListModels)
 | 
			
		||||
 | 
			
		||||
		channelRoute.Use(middleware.AdminAuth())
 | 
			
		||||
		{
 | 
			
		||||
			channelRoute.GET("/", controller.GetAllChannels)
 | 
			
		||||
			channelRoute.GET("/search", controller.SearchChannels)
 | 
			
		||||
			channelRoute.GET("/models", controller.ListModels)
 | 
			
		||||
			channelRoute.GET("/:id", controller.GetChannel)
 | 
			
		||||
			channelRoute.GET("/test", controller.TestAllChannels)
 | 
			
		||||
			channelRoute.GET("/test/:id", controller.TestChannel)
 | 
			
		||||
@@ -75,6 +78,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
			channelRoute.PUT("/", controller.UpdateChannel)
 | 
			
		||||
			channelRoute.DELETE("/:id", controller.DeleteChannel)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tokenRoute := apiRouter.Group("/token")
 | 
			
		||||
		tokenRoute.Use(middleware.UserAuth())
 | 
			
		||||
		{
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ func SetRelayRouter(router *gin.Engine) {
 | 
			
		||||
	modelsRouter := router.Group("/v1/models")
 | 
			
		||||
	modelsRouter.Use(middleware.TokenAuth())
 | 
			
		||||
	{
 | 
			
		||||
		modelsRouter.GET("", controller.ListModels)
 | 
			
		||||
		modelsRouter.GET("/", controller.ListModels)
 | 
			
		||||
		modelsRouter.GET("/:model", controller.RetrieveModel)
 | 
			
		||||
	}
 | 
			
		||||
	relayV1Router := router.Group("/v1")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,12 @@ npm start
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
 | 
			
		||||
for example: `REACT_APP_SERVER=http://your.domain.com`.
 | 
			
		||||
If you want to change the default server, please set `VITE_REACT_APP_SERVER` environment variables before build,
 | 
			
		||||
for example: `VITE_REACT_APP_SERVER=http://your.domain.com`.
 | 
			
		||||
 | 
			
		||||
Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
 | 
			
		||||
 | 
			
		||||
## Reference
 | 
			
		||||
 | 
			
		||||
1. https://github.com/OIerDb-ng/OIerDb
 | 
			
		||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
 | 
			
		||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
@@ -14,5 +14,6 @@
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="./src/index.jsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										1799
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1799
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,24 +3,21 @@
 | 
			
		||||
  "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",
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
    "start": "vite preview",
 | 
			
		||||
    "build": "vite build"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": [
 | 
			
		||||
@@ -41,7 +38,10 @@
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "prettier": "^2.7.1"
 | 
			
		||||
    "@vitejs/plugin-react": "^4.0.3",
 | 
			
		||||
    "prettier": "3.0.0",
 | 
			
		||||
    "terser": "^5.19.0",
 | 
			
		||||
    "vite": "^4.4.4"
 | 
			
		||||
  },
 | 
			
		||||
  "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
 | 
			
		||||
@@ -1,30 +1,43 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumber } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let type2label = undefined;
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  if (!type2label) {
 | 
			
		||||
    type2label = new Map;
 | 
			
		||||
    type2label = new Map();
 | 
			
		||||
    for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
 | 
			
		||||
      type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
 | 
			
		||||
    }
 | 
			
		||||
    type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
 | 
			
		||||
  }
 | 
			
		||||
  return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>;
 | 
			
		||||
  return (
 | 
			
		||||
    <Label basic color={type2label[type].color}>
 | 
			
		||||
      {type2label[type].text}
 | 
			
		||||
    </Label>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderBalance(type, balance) {
 | 
			
		||||
@@ -132,7 +145,11 @@ const ChannelsTable = () => {
 | 
			
		||||
  const renderStatus = (status) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case 1:
 | 
			
		||||
        return <Label basic color='green'>已启用</Label>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='green'>
 | 
			
		||||
            已启用
 | 
			
		||||
          </Label>
 | 
			
		||||
        );
 | 
			
		||||
      case 2:
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='red'>
 | 
			
		||||
@@ -152,15 +169,35 @@ const ChannelsTable = () => {
 | 
			
		||||
    let time = responseTime / 1000;
 | 
			
		||||
    time = time.toFixed(2) + ' 秒';
 | 
			
		||||
    if (responseTime === 0) {
 | 
			
		||||
      return <Label basic color='grey'>未测试</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          未测试
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 1000) {
 | 
			
		||||
      return <Label basic color='green'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 3000) {
 | 
			
		||||
      return <Label basic color='olive'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='olive'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 5000) {
 | 
			
		||||
      return <Label basic color='yellow'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='yellow'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return <Label basic color='red'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -342,7 +379,7 @@ const ChannelsTable = () => {
 | 
			
		||||
          {channels
 | 
			
		||||
            .slice(
 | 
			
		||||
              (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
              activePage * ITEMS_PER_PAGE
 | 
			
		||||
              activePage * ITEMS_PER_PAGE,
 | 
			
		||||
            )
 | 
			
		||||
            .map((channel, idx) => {
 | 
			
		||||
              if (channel.deleted) return <></>;
 | 
			
		||||
@@ -355,7 +392,11 @@ const ChannelsTable = () => {
 | 
			
		||||
                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
 | 
			
		||||
                      content={
 | 
			
		||||
                        channel.test_time
 | 
			
		||||
                          ? renderTimestamp(channel.test_time)
 | 
			
		||||
                          : '未测试'
 | 
			
		||||
                      }
 | 
			
		||||
                      key={channel.id}
 | 
			
		||||
                      trigger={renderResponseTime(channel.response_time)}
 | 
			
		||||
                      basic
 | 
			
		||||
@@ -363,12 +404,13 @@ const ChannelsTable = () => {
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      trigger={<span onClick={() => {
 | 
			
		||||
                        updateChannelBalance(channel.id, channel.name, idx);
 | 
			
		||||
                      }} style={{ cursor: 'pointer' }}>
 | 
			
		||||
                      {renderBalance(channel.type, channel.balance)}
 | 
			
		||||
                    </span>}
 | 
			
		||||
                      content="点击更新"
 | 
			
		||||
                      content={
 | 
			
		||||
                        channel.balance_updated_time
 | 
			
		||||
                          ? renderTimestamp(channel.balance_updated_time)
 | 
			
		||||
                          : '未更新'
 | 
			
		||||
                      }
 | 
			
		||||
                      key={channel.id}
 | 
			
		||||
                      trigger={renderBalance(channel.type, channel.balance)}
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
@@ -383,16 +425,16 @@ const ChannelsTable = () => {
 | 
			
		||||
                      >
 | 
			
		||||
                        测试
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      {/*<Button*/}
 | 
			
		||||
                      {/*  size={'small'}*/}
 | 
			
		||||
                      {/*  positive*/}
 | 
			
		||||
                      {/*  loading={updatingBalance}*/}
 | 
			
		||||
                      {/*  onClick={() => {*/}
 | 
			
		||||
                      {/*    updateChannelBalance(channel.id, channel.name, idx);*/}
 | 
			
		||||
                      {/*  }}*/}
 | 
			
		||||
                      {/*>*/}
 | 
			
		||||
                      {/*  更新余额*/}
 | 
			
		||||
                      {/*</Button>*/}
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        positive
 | 
			
		||||
                        loading={updatingBalance}
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          updateChannelBalance(channel.id, channel.name, idx);
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        更新余额
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative>
 | 
			
		||||
@@ -418,7 +460,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                          manageChannel(
 | 
			
		||||
                            channel.id,
 | 
			
		||||
                            channel.status === 1 ? 'disable' : 'enable',
 | 
			
		||||
                            idx
 | 
			
		||||
                            idx,
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
@@ -441,14 +483,24 @@ const ChannelsTable = () => {
 | 
			
		||||
        <Table.Footer>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell colSpan='8'>
 | 
			
		||||
              <Button size='small' as={Link} to='/channel/add' loading={loading}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/channel/add'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                添加新的渠道
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' loading={loading} onClick={testAllChannels}>
 | 
			
		||||
                测试所有已启用通道
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={updateAllChannelsBalance}
 | 
			
		||||
                      loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                onClick={updateAllChannelsBalance}
 | 
			
		||||
                loading={loading || updatingBalance}
 | 
			
		||||
              >
 | 
			
		||||
                更新所有已启用通道余额
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Pagination
 | 
			
		||||
                floated='right'
 | 
			
		||||
                activePage={activePage}
 | 
			
		||||
@@ -460,7 +512,9 @@ const ChannelsTable = () => {
 | 
			
		||||
                  (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                刷新
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
          </Table.Row>
 | 
			
		||||
        </Table.Footer>
 | 
			
		||||
							
								
								
									
										57
									
								
								web/src/components/DiscordOAuth.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/src/components/DiscordOAuth.jsx
									
									
									
									
									
										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;
 | 
			
		||||
@@ -37,11 +37,8 @@ const Footer = () => {
 | 
			
		||||
          ></div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className='custom-footer'>
 | 
			
		||||
            <a
 | 
			
		||||
              href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
              target='_blank'
 | 
			
		||||
            >
 | 
			
		||||
              {systemName} {process.env.REACT_APP_VERSION}{' '}
 | 
			
		||||
            <a href='https://github.com/songquanpeng/one-api' target='_blank'>
 | 
			
		||||
              {systemName} {import.meta.env.VITE_REACT_APP_VERSION}{' '}
 | 
			
		||||
            </a>
 | 
			
		||||
            由{' '}
 | 
			
		||||
            <a href='https://github.com/songquanpeng' target='_blank'>
 | 
			
		||||
@@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
 | 
			
		||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Container,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Icon,
 | 
			
		||||
  Menu,
 | 
			
		||||
  Segment,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  getLogo,
 | 
			
		||||
  getSystemName,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import '../index.css';
 | 
			
		||||
 | 
			
		||||
// Header Buttons
 | 
			
		||||
@@ -11,58 +25,58 @@ let headerButtons = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '首页',
 | 
			
		||||
    to: '/',
 | 
			
		||||
    icon: 'home'
 | 
			
		||||
    icon: 'home',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '渠道',
 | 
			
		||||
    to: '/channel',
 | 
			
		||||
    icon: 'sitemap',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '令牌',
 | 
			
		||||
    to: '/token',
 | 
			
		||||
    icon: 'key'
 | 
			
		||||
    icon: 'key',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '兑换',
 | 
			
		||||
    to: '/redemption',
 | 
			
		||||
    icon: 'dollar sign',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '充值',
 | 
			
		||||
    to: '/topup',
 | 
			
		||||
    icon: 'cart'
 | 
			
		||||
    icon: 'cart',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '用户',
 | 
			
		||||
    to: '/user',
 | 
			
		||||
    icon: 'user',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '日志',
 | 
			
		||||
    to: '/log',
 | 
			
		||||
    icon: 'book'
 | 
			
		||||
    icon: 'book',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '设置',
 | 
			
		||||
    to: '/setting',
 | 
			
		||||
    icon: 'setting'
 | 
			
		||||
    icon: 'setting',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '关于',
 | 
			
		||||
    to: '/about',
 | 
			
		||||
    icon: 'info circle'
 | 
			
		||||
  }
 | 
			
		||||
    icon: 'info circle',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('chat_link')) {
 | 
			
		||||
  headerButtons.splice(1, 0, {
 | 
			
		||||
    name: '聊天',
 | 
			
		||||
    to: '/chat',
 | 
			
		||||
    icon: 'comments'
 | 
			
		||||
    icon: 'comments',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -120,21 +134,17 @@ const Header = () => {
 | 
			
		||||
          style={
 | 
			
		||||
            showSidebar
 | 
			
		||||
              ? {
 | 
			
		||||
                borderBottom: 'none',
 | 
			
		||||
                marginBottom: '0',
 | 
			
		||||
                borderTop: 'none',
 | 
			
		||||
                height: '51px'
 | 
			
		||||
              }
 | 
			
		||||
                  borderBottom: 'none',
 | 
			
		||||
                  marginBottom: '0',
 | 
			
		||||
                  borderTop: 'none',
 | 
			
		||||
                  height: '51px',
 | 
			
		||||
                }
 | 
			
		||||
              : { borderTop: 'none', height: '52px' }
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Container>
 | 
			
		||||
            <Menu.Item as={Link} to='/'>
 | 
			
		||||
              <img
 | 
			
		||||
                src={logo}
 | 
			
		||||
                alt='logo'
 | 
			
		||||
                style={{ marginRight: '0.75em' }}
 | 
			
		||||
              />
 | 
			
		||||
              <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
              <div style={{ fontSize: '20px' }}>
 | 
			
		||||
                <b>{systemName}</b>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -1,20 +1,35 @@
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Message,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Segment,
 | 
			
		||||
} 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({
 | 
			
		||||
    username: '',
 | 
			
		||||
    password: '',
 | 
			
		||||
    wechat_verification_code: ''
 | 
			
		||||
    wechat_verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  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({});
 | 
			
		||||
  const logo = getLogo();
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +41,11 @@ const LoginForm = () => {
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      setStatus(status);
 | 
			
		||||
 | 
			
		||||
      if (status.turnstile_check) {
 | 
			
		||||
        setTurnstileEnabled(true);
 | 
			
		||||
        setTurnstileSiteKey(status.turnstile_site_key);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +53,13 @@ const LoginForm = () => {
 | 
			
		||||
 | 
			
		||||
  const onGitHubOAuthClicked = () => {
 | 
			
		||||
    window.open(
 | 
			
		||||
      `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
 | 
			
		||||
      `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onDiscordOAuthClicked = () => {
 | 
			
		||||
    window.open(
 | 
			
		||||
      `https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +69,7 @@ const LoginForm = () => {
 | 
			
		||||
 | 
			
		||||
  const onSubmitWeChatVerificationCode = async () => {
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`
 | 
			
		||||
      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -65,10 +91,18 @@ const LoginForm = () => {
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
    setSubmitted(true);
 | 
			
		||||
    if (username && password) {
 | 
			
		||||
      const res = await API.post(`/api/user/login`, {
 | 
			
		||||
        username,
 | 
			
		||||
        password
 | 
			
		||||
      });
 | 
			
		||||
      if (turnstileEnabled && turnstileToken === '') {
 | 
			
		||||
        showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const res = await API.post(
 | 
			
		||||
        `/api/user/login?turnstile=${turnstileToken}`,
 | 
			
		||||
        {
 | 
			
		||||
          username,
 | 
			
		||||
          password,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        userDispatch({ type: 'login', payload: data });
 | 
			
		||||
@@ -108,7 +142,17 @@ const LoginForm = () => {
 | 
			
		||||
              value={password}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Button color='green' fluid size='large' onClick={handleSubmit}>
 | 
			
		||||
            {turnstileEnabled ? (
 | 
			
		||||
              <Turnstile
 | 
			
		||||
                sitekey={turnstileSiteKey}
 | 
			
		||||
                onVerify={(token) => {
 | 
			
		||||
                  setTurnstileToken(token);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button color='' fluid size='large' onClick={handleSubmit}>
 | 
			
		||||
              登录
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
@@ -123,28 +167,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}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
@@ -1,21 +1,26 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Header,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Select,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, isAdmin, showError, timestamp2string } from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MODE_OPTIONS = [
 | 
			
		||||
  { key: 'all', text: '全部用户', value: 'all' },
 | 
			
		||||
  { key: 'self', text: '当前用户', value: 'self' }
 | 
			
		||||
  { key: 'self', text: '当前用户', value: 'self' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const LOG_OPTIONS = [
 | 
			
		||||
@@ -23,21 +28,46 @@ const LOG_OPTIONS = [
 | 
			
		||||
  { key: '1', text: '充值', value: 1 },
 | 
			
		||||
  { key: '2', text: '消费', value: 2 },
 | 
			
		||||
  { key: '3', text: '管理', value: 3 },
 | 
			
		||||
  { key: '4', text: '系统', value: 4 }
 | 
			
		||||
  { key: '4', text: '系统', value: 4 },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'> 充值 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          充值{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='olive'> 消费 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='olive'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          消费{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='orange'> 管理 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='orange'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          管理{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Label basic color='purple'> 系统 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='purple'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          系统{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -55,13 +85,14 @@ const LogsTable = () => {
 | 
			
		||||
    token_name: '',
 | 
			
		||||
    model_name: '',
 | 
			
		||||
    start_timestamp: timestamp2string(0),
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
  });
 | 
			
		||||
  const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs;
 | 
			
		||||
  const { username, token_name, model_name, start_timestamp, end_timestamp } =
 | 
			
		||||
    inputs;
 | 
			
		||||
 | 
			
		||||
  const [stat, setStat] = useState({
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    token: 0
 | 
			
		||||
    token: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
@@ -71,7 +102,9 @@ const LogsTable = () => {
 | 
			
		||||
  const getLogSelfStat = async () => {
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
    let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
 | 
			
		||||
    let res = await API.get(
 | 
			
		||||
      `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setStat(data);
 | 
			
		||||
@@ -83,7 +116,9 @@ const LogsTable = () => {
 | 
			
		||||
  const getLogStat = async () => {
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
    let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
 | 
			
		||||
    let res = await API.get(
 | 
			
		||||
      `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setStat(data);
 | 
			
		||||
@@ -129,7 +164,7 @@ const LogsTable = () => {
 | 
			
		||||
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    setActivePage(1)
 | 
			
		||||
    setActivePage(1);
 | 
			
		||||
    await loadLogs(0);
 | 
			
		||||
    if (isAdminUser) {
 | 
			
		||||
      getLogStat().then();
 | 
			
		||||
@@ -169,7 +204,7 @@ const LogsTable = () => {
 | 
			
		||||
    if (logs.length === 0) return;
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    let sortedLogs = [...logs];
 | 
			
		||||
    if (typeof sortedLogs[0][key] === 'string'){
 | 
			
		||||
    if (typeof sortedLogs[0][key] === 'string') {
 | 
			
		||||
      sortedLogs.sort((a, b) => {
 | 
			
		||||
        return ('' + a[key]).localeCompare(b[key]);
 | 
			
		||||
      });
 | 
			
		||||
@@ -190,28 +225,61 @@ const LogsTable = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment>
 | 
			
		||||
        <Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
 | 
			
		||||
        <Header as='h3'>
 | 
			
		||||
          使用明细(总消耗额度:{renderQuota(stat.quota)})
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form>
 | 
			
		||||
          <Form.Group>
 | 
			
		||||
            {
 | 
			
		||||
              isAdminUser && (
 | 
			
		||||
                <Form.Input fluid label={'用户名称'} width={2} value={username}
 | 
			
		||||
                            placeholder={'可选值'} name='username'
 | 
			
		||||
                            onChange={handleInputChange} />
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            <Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name}
 | 
			
		||||
                        placeholder={'可选值'} name='token_name' onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值'
 | 
			
		||||
                        name='model_name'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
 | 
			
		||||
                        name='start_timestamp'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
 | 
			
		||||
                        name='end_timestamp'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
 | 
			
		||||
            {isAdminUser && (
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                label={'用户名称'}
 | 
			
		||||
                width={2}
 | 
			
		||||
                value={username}
 | 
			
		||||
                placeholder={'可选值'}
 | 
			
		||||
                name='username'
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label={'令牌名称'}
 | 
			
		||||
              width={isAdminUser ? 2 : 3}
 | 
			
		||||
              value={token_name}
 | 
			
		||||
              placeholder={'可选值'}
 | 
			
		||||
              name='token_name'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='模型名称'
 | 
			
		||||
              width={isAdminUser ? 2 : 3}
 | 
			
		||||
              value={model_name}
 | 
			
		||||
              placeholder='可选值'
 | 
			
		||||
              name='model_name'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='起始时间'
 | 
			
		||||
              width={4}
 | 
			
		||||
              value={start_timestamp}
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
              name='start_timestamp'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='结束时间'
 | 
			
		||||
              width={4}
 | 
			
		||||
              value={end_timestamp}
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
              name='end_timestamp'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Button fluid label='操作' width={2} onClick={refresh}>
 | 
			
		||||
              查询
 | 
			
		||||
            </Form.Button>
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Table basic compact size='small'>
 | 
			
		||||
@@ -226,8 +294,8 @@ const LogsTable = () => {
 | 
			
		||||
              >
 | 
			
		||||
                时间
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              {
 | 
			
		||||
                isAdminUser && <Table.HeaderCell
 | 
			
		||||
              {isAdminUser && (
 | 
			
		||||
                <Table.HeaderCell
 | 
			
		||||
                  style={{ cursor: 'pointer' }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    sortLog('username');
 | 
			
		||||
@@ -236,7 +304,7 @@ const LogsTable = () => {
 | 
			
		||||
                >
 | 
			
		||||
                  用户
 | 
			
		||||
                </Table.HeaderCell>
 | 
			
		||||
              }
 | 
			
		||||
              )}
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
@@ -307,24 +375,42 @@ const LogsTable = () => {
 | 
			
		||||
            {logs
 | 
			
		||||
              .slice(
 | 
			
		||||
                (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
                activePage * ITEMS_PER_PAGE
 | 
			
		||||
                activePage * ITEMS_PER_PAGE,
 | 
			
		||||
              )
 | 
			
		||||
              .map((log, idx) => {
 | 
			
		||||
                if (log.deleted) return <></>;
 | 
			
		||||
                return (
 | 
			
		||||
                  <Table.Row key={log.created_at}>
 | 
			
		||||
                    <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
 | 
			
		||||
                    {
 | 
			
		||||
                      isAdminUser && (
 | 
			
		||||
                        <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    <Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
 | 
			
		||||
                    {isAdminUser && (
 | 
			
		||||
                      <Table.Cell>
 | 
			
		||||
                        {log.username ? <Label>{log.username}</Label> : ''}
 | 
			
		||||
                      </Table.Cell>
 | 
			
		||||
                    )}
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.token_name ? (
 | 
			
		||||
                        <Label basic>{log.token_name}</Label>
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        ''
 | 
			
		||||
                      )}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    <Table.Cell>{renderType(log.type)}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.model_name ? (
 | 
			
		||||
                        <Label basic>{log.model_name}</Label>
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        ''
 | 
			
		||||
                      )}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.prompt_tokens ? log.prompt_tokens : ''}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.completion_tokens ? log.completion_tokens : ''}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.quota ? renderQuota(log.quota, 6) : ''}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.content}</Table.Cell>
 | 
			
		||||
                  </Table.Row>
 | 
			
		||||
                );
 | 
			
		||||
@@ -344,7 +430,9 @@ const LogsTable = () => {
 | 
			
		||||
                    setLogType(value);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
                <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                  刷新
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Pagination
 | 
			
		||||
                  floated='right'
 | 
			
		||||
                  activePage={activePage}
 | 
			
		||||
@@ -54,7 +54,7 @@ const OperationSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -76,11 +76,22 @@ const OperationSetting = () => {
 | 
			
		||||
  const submitConfig = async (group) => {
 | 
			
		||||
    switch (group) {
 | 
			
		||||
      case 'monitor':
 | 
			
		||||
        if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
 | 
			
		||||
          await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
 | 
			
		||||
        if (
 | 
			
		||||
          originInputs['ChannelDisableThreshold'] !==
 | 
			
		||||
          inputs.ChannelDisableThreshold
 | 
			
		||||
        ) {
 | 
			
		||||
          await updateOption(
 | 
			
		||||
            'ChannelDisableThreshold',
 | 
			
		||||
            inputs.ChannelDisableThreshold,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
 | 
			
		||||
          await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
 | 
			
		||||
        if (
 | 
			
		||||
          originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
 | 
			
		||||
        ) {
 | 
			
		||||
          await updateOption(
 | 
			
		||||
            'QuotaRemindThreshold',
 | 
			
		||||
            inputs.QuotaRemindThreshold,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'ratio':
 | 
			
		||||
@@ -134,9 +145,7 @@ const OperationSetting = () => {
 | 
			
		||||
    <Grid columns={1}>
 | 
			
		||||
      <Grid.Column>
 | 
			
		||||
        <Form loading={loading}>
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            通用设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>通用设置</Header>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='充值链接'
 | 
			
		||||
@@ -204,13 +213,15 @@ const OperationSetting = () => {
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('general').then();
 | 
			
		||||
          }}>保存通用设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('general').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存通用设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            监控设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>监控设置</Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='最长响应时间'
 | 
			
		||||
@@ -241,13 +252,15 @@ const OperationSetting = () => {
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('monitor').then();
 | 
			
		||||
          }}>保存监控设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('monitor').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存监控设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            额度设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>额度设置</Header>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='新用户初始额度'
 | 
			
		||||
@@ -290,13 +303,15 @@ const OperationSetting = () => {
 | 
			
		||||
              placeholder='例如:1000'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('quota').then();
 | 
			
		||||
          }}>保存额度设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('quota').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存额度设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            倍率设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>倍率设置</Header>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label='模型倍率'
 | 
			
		||||
@@ -319,9 +334,13 @@ const OperationSetting = () => {
 | 
			
		||||
              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('ratio').then();
 | 
			
		||||
          }}>保存倍率设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('ratio').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存倍率设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Message,
 | 
			
		||||
  Modal,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showSuccess } from '../helpers';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
 | 
			
		||||
@@ -10,13 +18,13 @@ const OtherSetting = () => {
 | 
			
		||||
    About: '',
 | 
			
		||||
    SystemName: '',
 | 
			
		||||
    Logo: '',
 | 
			
		||||
    HomePageContent: ''
 | 
			
		||||
    HomePageContent: '',
 | 
			
		||||
  });
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
  const [showUpdateModal, setShowUpdateModal] = useState(false);
 | 
			
		||||
  const [updateData, setUpdateData] = useState({
 | 
			
		||||
    tag_name: '',
 | 
			
		||||
    content: ''
 | 
			
		||||
    content: '',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getOptions = async () => {
 | 
			
		||||
@@ -43,7 +51,7 @@ const OtherSetting = () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -83,21 +91,20 @@ const OtherSetting = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openGitHubRelease = () => {
 | 
			
		||||
    window.location =
 | 
			
		||||
      'https://github.com/songquanpeng/one-api/releases/latest';
 | 
			
		||||
    window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const checkUpdate = async () => {
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
 | 
			
		||||
      'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
 | 
			
		||||
    );
 | 
			
		||||
    const { tag_name, body } = res.data;
 | 
			
		||||
    if (tag_name === process.env.REACT_APP_VERSION) {
 | 
			
		||||
    if (tag_name === import.meta.env.VITE_REACT_APP_VERSION) {
 | 
			
		||||
      showSuccess(`已是最新版本:${tag_name}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      setUpdateData({
 | 
			
		||||
        tag_name: tag_name,
 | 
			
		||||
        content: marked.parse(body)
 | 
			
		||||
        content: marked.parse(body),
 | 
			
		||||
      });
 | 
			
		||||
      setShowUpdateModal(true);
 | 
			
		||||
    }
 | 
			
		||||
@@ -153,7 +160,9 @@ const OtherSetting = () => {
 | 
			
		||||
              style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button>
 | 
			
		||||
          <Form.Button onClick={() => submitOption('HomePageContent')}>
 | 
			
		||||
            保存首页内容
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label='关于'
 | 
			
		||||
@@ -165,7 +174,10 @@ const OtherSetting = () => {
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitAbout}>保存关于</Form.Button>
 | 
			
		||||
          <Message>移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。</Message>
 | 
			
		||||
          <Message>
 | 
			
		||||
            移除 One API
 | 
			
		||||
            的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。
 | 
			
		||||
          </Message>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='页脚'
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showNotice,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import { useSearchParams } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const PasswordResetConfirm = () => {
 | 
			
		||||
@@ -12,11 +19,6 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [disableButton, setDisableButton] = useState(false);
 | 
			
		||||
  const [countdown, setCountdown] = useState(30);
 | 
			
		||||
 | 
			
		||||
  const [newPassword, setNewPassword] = useState('');
 | 
			
		||||
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let token = searchParams.get('token');
 | 
			
		||||
@@ -27,21 +29,7 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let countdownInterval = null;
 | 
			
		||||
    if (disableButton && countdown > 0) {
 | 
			
		||||
      countdownInterval = setInterval(() => {
 | 
			
		||||
        setCountdown(countdown - 1);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    } else if (countdown === 0) {
 | 
			
		||||
      setDisableButton(false);
 | 
			
		||||
      setCountdown(30);
 | 
			
		||||
    }
 | 
			
		||||
    return () => clearInterval(countdownInterval); 
 | 
			
		||||
  }, [disableButton, countdown]);
 | 
			
		||||
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
    setDisableButton(true);
 | 
			
		||||
    if (!email) return;
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.post(`/api/user/reset`, {
 | 
			
		||||
@@ -51,15 +39,14 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let password = res.data.data;
 | 
			
		||||
      setNewPassword(password);
 | 
			
		||||
      await copy(password);
 | 
			
		||||
      showNotice(`新密码已复制到剪贴板:${password}`);
 | 
			
		||||
      showNotice(`密码已重置并已复制到剪贴板:${password}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
@@ -77,37 +64,20 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
              value={email}
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
            {newPassword && (
 | 
			
		||||
              <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='新密码'
 | 
			
		||||
              name='newPassword'
 | 
			
		||||
              value={newPassword}
 | 
			
		||||
              readOnly
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                e.target.select();
 | 
			
		||||
                navigator.clipboard.writeText(newPassword);
 | 
			
		||||
                showNotice(`密码已复制到剪贴板:${newPassword}`);
 | 
			
		||||
              }}
 | 
			
		||||
            />            
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              color=''
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
            >
 | 
			
		||||
              {disableButton ? `密码重置完成` : '提交'}
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );  
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default PasswordResetConfirm;
 | 
			
		||||
@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
 | 
			
		||||
 | 
			
		||||
const PasswordResetForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    email: ''
 | 
			
		||||
    email: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { email } = inputs;
 | 
			
		||||
 | 
			
		||||
@@ -13,29 +13,24 @@ const PasswordResetForm = () => {
 | 
			
		||||
  const [turnstileEnabled, setTurnstileEnabled] = useState(false);
 | 
			
		||||
  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
 | 
			
		||||
  const [turnstileToken, setTurnstileToken] = useState('');
 | 
			
		||||
  const [disableButton, setDisableButton] = useState(false);
 | 
			
		||||
  const [countdown, setCountdown] = useState(30);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let countdownInterval = null;
 | 
			
		||||
    if (disableButton && countdown > 0) {
 | 
			
		||||
      countdownInterval = setInterval(() => {
 | 
			
		||||
        setCountdown(countdown - 1);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    } else if (countdown === 0) {
 | 
			
		||||
      setDisableButton(false);
 | 
			
		||||
      setCountdown(30);
 | 
			
		||||
    let status = localStorage.getItem('status');
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      if (status.turnstile_check) {
 | 
			
		||||
        setTurnstileEnabled(true);
 | 
			
		||||
        setTurnstileSiteKey(status.turnstile_site_key);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return () => clearInterval(countdownInterval);
 | 
			
		||||
  }, [disableButton, countdown]);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  function handleChange(e) {
 | 
			
		||||
    const { name, value } = e.target;
 | 
			
		||||
    setInputs(inputs => ({ ...inputs, [name]: value }));
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
    setDisableButton(true);
 | 
			
		||||
    if (!email) return;
 | 
			
		||||
    if (turnstileEnabled && turnstileToken === '') {
 | 
			
		||||
      showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
 | 
			
		||||
@@ -43,7 +38,7 @@ const PasswordResetForm = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -83,14 +78,13 @@ const PasswordResetForm = () => {
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              color=''
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
            >
 | 
			
		||||
              {disableButton ? `重试 (${countdown})` : '提交'}
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
@@ -1,19 +1,35 @@
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
 | 
			
		||||
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 {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showNotice,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
 | 
			
		||||
const PersonalSetting = () => {
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  const [userDispatch] = useContext(UserContext);
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    wechat_verification_code: '',
 | 
			
		||||
    email_verification_code: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    self_account_deletion_confirmation: ''
 | 
			
		||||
    self_account_deletion_confirmation: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [status, setStatus] = useState({});
 | 
			
		||||
  const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
 | 
			
		||||
@@ -23,8 +39,6 @@ const PersonalSetting = () => {
 | 
			
		||||
  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
 | 
			
		||||
  const [turnstileToken, setTurnstileToken] = useState('');
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
  const [disableButton, setDisableButton] = useState(false);
 | 
			
		||||
  const [countdown, setCountdown] = useState(30);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let status = localStorage.getItem('status');
 | 
			
		||||
@@ -38,19 +52,6 @@ const PersonalSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let countdownInterval = null;
 | 
			
		||||
    if (disableButton && countdown > 0) {
 | 
			
		||||
      countdownInterval = setInterval(() => {
 | 
			
		||||
        setCountdown(countdown - 1);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    } else if (countdown === 0) {
 | 
			
		||||
      setDisableButton(false);
 | 
			
		||||
      setCountdown(30);
 | 
			
		||||
    }
 | 
			
		||||
    return () => clearInterval(countdownInterval); // Clean up on unmount
 | 
			
		||||
  }, [disableButton, countdown]);
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
@@ -79,8 +80,8 @@ const PersonalSetting = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteAccount = async () => {
 | 
			
		||||
    if (inputs.self_account_deletion_confirmation !== userState.user.username) {
 | 
			
		||||
      showError('请输入你的账户名以确认删除!');
 | 
			
		||||
    if (inputs.self_account_deletion_confirmation !== 'CONFIRM') {
 | 
			
		||||
      showError('请确认您要删除账户!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -101,7 +102,7 @@ const PersonalSetting = () => {
 | 
			
		||||
  const bindWeChat = async () => {
 | 
			
		||||
    if (inputs.wechat_verification_code === '') return;
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
 | 
			
		||||
      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -114,12 +115,17 @@ const PersonalSetting = () => {
 | 
			
		||||
 | 
			
		||||
  const openGitHubOAuth = () => {
 | 
			
		||||
    window.open(
 | 
			
		||||
      `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
 | 
			
		||||
      `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const openDiscordOAuth = () => {
 | 
			
		||||
    window.open(
 | 
			
		||||
      `https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&scope=identify%20email&response_type=code&redirect_uri=${window.location.origin}/oauth/discord`,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sendVerificationCode = async () => {
 | 
			
		||||
    setDisableButton(true);
 | 
			
		||||
    if (inputs.email === '') return;
 | 
			
		||||
    if (turnstileEnabled && turnstileToken === '') {
 | 
			
		||||
      showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
 | 
			
		||||
@@ -127,7 +133,7 @@ const PersonalSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -142,7 +148,7 @@ const PersonalSetting = () => {
 | 
			
		||||
    if (inputs.email_verification_code === '') return;
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
 | 
			
		||||
      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -158,29 +164,33 @@ const PersonalSetting = () => {
 | 
			
		||||
    <div style={{ lineHeight: '40px' }}>
 | 
			
		||||
      <Header as='h3'>通用设置</Header>
 | 
			
		||||
      <Message>
 | 
			
		||||
        注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
 | 
			
		||||
        注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI
 | 
			
		||||
        相关的服务,请知悉。
 | 
			
		||||
      </Message>
 | 
			
		||||
      <Button as={Link} to={`/user/edit/`}>
 | 
			
		||||
        更新个人信息
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
 | 
			
		||||
      <Button onClick={getAffLink}>复制邀请链接</Button>
 | 
			
		||||
      <Button onClick={() => {
 | 
			
		||||
        setShowAccountDeleteModal(true);
 | 
			
		||||
      }}>删除个人账户</Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setShowAccountDeleteModal(true);
 | 
			
		||||
        }}
 | 
			
		||||
        color='red'
 | 
			
		||||
      >
 | 
			
		||||
        删除个人账户
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <Header as='h3'>账号绑定</Header>
 | 
			
		||||
      {
 | 
			
		||||
        status.wechat_login && (
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setShowWeChatBindModal(true);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            绑定微信账号
 | 
			
		||||
          </Button>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      {status.wechat_login && (
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setShowWeChatBindModal(true);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          绑定微信账号
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      <Modal
 | 
			
		||||
        onClose={() => setShowWeChatBindModal(false)}
 | 
			
		||||
        onOpen={() => setShowWeChatBindModal(true)}
 | 
			
		||||
@@ -210,11 +220,12 @@ const PersonalSetting = () => {
 | 
			
		||||
          </Modal.Description>
 | 
			
		||||
        </Modal.Content>
 | 
			
		||||
      </Modal>
 | 
			
		||||
      {
 | 
			
		||||
        status.github_oauth && (
 | 
			
		||||
          <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      {status.github_oauth && (
 | 
			
		||||
        <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {status.discord_oauth && (
 | 
			
		||||
        <Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setShowEmailBindModal(true);
 | 
			
		||||
@@ -240,8 +251,8 @@ const PersonalSetting = () => {
 | 
			
		||||
                name='email'
 | 
			
		||||
                type='email'
 | 
			
		||||
                action={
 | 
			
		||||
                  <Button onClick={sendVerificationCode} disabled={disableButton || loading}>
 | 
			
		||||
                    {disableButton ? `重新发送(${countdown})` : '获取验证码'}
 | 
			
		||||
                  <Button onClick={sendVerificationCode} disabled={loading}>
 | 
			
		||||
                    获取验证码
 | 
			
		||||
                  </Button>
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
@@ -282,13 +293,14 @@ const PersonalSetting = () => {
 | 
			
		||||
        size={'tiny'}
 | 
			
		||||
        style={{ maxWidth: '450px' }}
 | 
			
		||||
      >
 | 
			
		||||
        <Modal.Header>确认删除自己的帐户</Modal.Header>
 | 
			
		||||
        <Modal.Header>您是否确认删除自己的帐户?</Modal.Header>
 | 
			
		||||
        <Modal.Content>
 | 
			
		||||
          <Modal.Description>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
 | 
			
		||||
                label='请输入 "CONFIRM" 以删除您的帐户。'
 | 
			
		||||
                placeholder='确认文字'
 | 
			
		||||
                name='self_account_deletion_confirmation'
 | 
			
		||||
                value={inputs.self_account_deletion_confirmation}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
@@ -2,7 +2,6 @@ import { Navigate } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { history } from '../helpers';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function PrivateRoute({ children }) {
 | 
			
		||||
  if (!localStorage.getItem('user')) {
 | 
			
		||||
    return <Navigate to='/login' state={{ from: history.location }} />;
 | 
			
		||||
@@ -10,4 +9,4 @@ function PrivateRoute({ children }) {
 | 
			
		||||
  return children;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { PrivateRoute };
 | 
			
		||||
export { PrivateRoute };
 | 
			
		||||
@@ -1,29 +1,59 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Message,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  showWarning,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'>未使用</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          未使用
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='red'> 已禁用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='grey'> 已使用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已使用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知状态 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +140,9 @@ const RedemptionsTable = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearching(true);
 | 
			
		||||
    const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/redemption/search?keyword=${searchKeyword}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setRedemptions(data);
 | 
			
		||||
@@ -212,18 +244,26 @@ const RedemptionsTable = () => {
 | 
			
		||||
          {redemptions
 | 
			
		||||
            .slice(
 | 
			
		||||
              (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
              activePage * ITEMS_PER_PAGE
 | 
			
		||||
              activePage * ITEMS_PER_PAGE,
 | 
			
		||||
            )
 | 
			
		||||
            .map((redemption, idx) => {
 | 
			
		||||
              if (redemption.deleted) return <></>;
 | 
			
		||||
              return (
 | 
			
		||||
                <Table.Row key={redemption.id}>
 | 
			
		||||
                  <Table.Cell>{redemption.id}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {redemption.name ? redemption.name : '无'}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {renderTimestamp(redemption.created_time)}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {redemption.redeemed_time
 | 
			
		||||
                      ? renderTimestamp(redemption.redeemed_time)
 | 
			
		||||
                      : '尚未兑换'}{' '}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <Button
 | 
			
		||||
@@ -233,7 +273,9 @@ const RedemptionsTable = () => {
 | 
			
		||||
                          if (await copy(redemption.key)) {
 | 
			
		||||
                            showSuccess('已复制到剪贴板!');
 | 
			
		||||
                          } else {
 | 
			
		||||
                            showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。')
 | 
			
		||||
                            showWarning(
 | 
			
		||||
                              '无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。',
 | 
			
		||||
                            );
 | 
			
		||||
                            setSearchKeyword(redemption.key);
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
@@ -251,12 +293,12 @@ const RedemptionsTable = () => {
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        disabled={redemption.status === 3}  // used
 | 
			
		||||
                        disabled={redemption.status === 3} // used
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          manageRedemption(
 | 
			
		||||
                            redemption.id,
 | 
			
		||||
                            redemption.status === 1 ? 'disable' : 'enable',
 | 
			
		||||
                            idx
 | 
			
		||||
                            idx,
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
@@ -279,7 +321,12 @@ const RedemptionsTable = () => {
 | 
			
		||||
        <Table.Footer>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell colSpan='8'>
 | 
			
		||||
              <Button size='small' as={Link} to='/redemption/add' loading={loading}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/redemption/add'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                添加新的兑换码
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Pagination
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
@@ -10,7 +18,7 @@ const RegisterForm = () => {
 | 
			
		||||
    password: '',
 | 
			
		||||
    password2: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    verification_code: ''
 | 
			
		||||
    verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, password, password2 } = inputs;
 | 
			
		||||
  const [showEmailVerification, setShowEmailVerification] = useState(false);
 | 
			
		||||
@@ -65,7 +73,7 @@ const RegisterForm = () => {
 | 
			
		||||
      inputs.aff_code = affCode;
 | 
			
		||||
      const res = await API.post(
 | 
			
		||||
        `/api/user/register?turnstile=${turnstileToken}`,
 | 
			
		||||
        inputs
 | 
			
		||||
        inputs,
 | 
			
		||||
      );
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -86,7 +94,7 @@ const RegisterForm = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -170,7 +178,7 @@ const RegisterForm = () => {
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              color=''
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react';
 | 
			
		||||
import { API, removeTrailingSlash, showError } from '../helpers';
 | 
			
		||||
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
 | 
			
		||||
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
 | 
			
		||||
 | 
			
		||||
const SystemSetting = () => {
 | 
			
		||||
  let [inputs, setInputs] = useState({
 | 
			
		||||
@@ -8,8 +8,11 @@ const SystemSetting = () => {
 | 
			
		||||
    PasswordRegisterEnabled: '',
 | 
			
		||||
    EmailVerificationEnabled: '',
 | 
			
		||||
    GitHubOAuthEnabled: '',
 | 
			
		||||
    DiscordOAuthEnabled: '',
 | 
			
		||||
    GitHubClientId: '',
 | 
			
		||||
    GitHubClientSecret: '',
 | 
			
		||||
    DiscordClientId: '',
 | 
			
		||||
    DiscordClientSecret: '',
 | 
			
		||||
    Notice: '',
 | 
			
		||||
    SMTPServer: '',
 | 
			
		||||
    SMTPPort: '',
 | 
			
		||||
@@ -26,13 +29,9 @@ const SystemSetting = () => {
 | 
			
		||||
    TurnstileSiteKey: '',
 | 
			
		||||
    TurnstileSecretKey: '',
 | 
			
		||||
    RegisterEnabled: '',
 | 
			
		||||
    EmailDomainRestrictionEnabled: '',
 | 
			
		||||
    EmailDomainWhitelist: ''
 | 
			
		||||
  });
 | 
			
		||||
  const [originInputs, setOriginInputs] = useState({});
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
  const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
 | 
			
		||||
  const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
 | 
			
		||||
 | 
			
		||||
  const getOptions = async () => {
 | 
			
		||||
    const res = await API.get('/api/option/');
 | 
			
		||||
@@ -42,15 +41,8 @@ const SystemSetting = () => {
 | 
			
		||||
      data.forEach((item) => {
 | 
			
		||||
        newInputs[item.key] = item.value;
 | 
			
		||||
      });
 | 
			
		||||
      setInputs({
 | 
			
		||||
        ...newInputs,
 | 
			
		||||
        EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
 | 
			
		||||
      });
 | 
			
		||||
      setInputs(newInputs);
 | 
			
		||||
      setOriginInputs(newInputs);
 | 
			
		||||
 | 
			
		||||
      setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
 | 
			
		||||
        return { key: item, text: item, value: item };
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -67,9 +59,9 @@ const SystemSetting = () => {
 | 
			
		||||
      case 'PasswordRegisterEnabled':
 | 
			
		||||
      case 'EmailVerificationEnabled':
 | 
			
		||||
      case 'GitHubOAuthEnabled':
 | 
			
		||||
      case 'DiscordOAuthEnabled':
 | 
			
		||||
      case 'WeChatAuthEnabled':
 | 
			
		||||
      case 'TurnstileCheckEnabled':
 | 
			
		||||
      case 'EmailDomainRestrictionEnabled':
 | 
			
		||||
      case 'RegisterEnabled':
 | 
			
		||||
        value = inputs[key] === 'true' ? 'false' : 'true';
 | 
			
		||||
        break;
 | 
			
		||||
@@ -78,16 +70,11 @@ const SystemSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (key === 'EmailDomainWhitelist') {
 | 
			
		||||
        value = value.split(',');
 | 
			
		||||
      }
 | 
			
		||||
      setInputs((inputs) => ({
 | 
			
		||||
        ...inputs, [key]: value
 | 
			
		||||
      }));
 | 
			
		||||
      setInputs((inputs) => ({ ...inputs, [key]: value }));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -99,14 +86,15 @@ const SystemSetting = () => {
 | 
			
		||||
      name === 'Notice' ||
 | 
			
		||||
      name.startsWith('SMTP') ||
 | 
			
		||||
      name === 'ServerAddress' ||
 | 
			
		||||
      name === 'DiscordClientId' ||
 | 
			
		||||
      name === 'DiscordClientSecret' ||
 | 
			
		||||
      name === 'GitHubClientId' ||
 | 
			
		||||
      name === 'GitHubClientSecret' ||
 | 
			
		||||
      name === 'WeChatServerAddress' ||
 | 
			
		||||
      name === 'WeChatServerToken' ||
 | 
			
		||||
      name === 'WeChatAccountQRCodeImageURL' ||
 | 
			
		||||
      name === 'TurnstileSiteKey' ||
 | 
			
		||||
      name === 'TurnstileSecretKey' ||
 | 
			
		||||
      name === 'EmailDomainWhitelist'
 | 
			
		||||
      name === 'TurnstileSecretKey'
 | 
			
		||||
    ) {
 | 
			
		||||
      setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -143,21 +131,11 @@ const SystemSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const submitEmailDomainWhitelist = async () => {
 | 
			
		||||
    if (
 | 
			
		||||
      originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
 | 
			
		||||
      inputs.SMTPToken !== ''
 | 
			
		||||
    ) {
 | 
			
		||||
      await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const submitWeChat = async () => {
 | 
			
		||||
    if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
 | 
			
		||||
      await updateOption(
 | 
			
		||||
        'WeChatServerAddress',
 | 
			
		||||
        removeTrailingSlash(inputs.WeChatServerAddress)
 | 
			
		||||
        removeTrailingSlash(inputs.WeChatServerAddress),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
@@ -166,7 +144,7 @@ const SystemSetting = () => {
 | 
			
		||||
    ) {
 | 
			
		||||
      await updateOption(
 | 
			
		||||
        'WeChatAccountQRCodeImageURL',
 | 
			
		||||
        inputs.WeChatAccountQRCodeImageURL
 | 
			
		||||
        inputs.WeChatAccountQRCodeImageURL,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
@@ -189,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);
 | 
			
		||||
@@ -201,69 +191,59 @@ const SystemSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const submitNewRestrictedDomain = () => {
 | 
			
		||||
    const localDomainList = inputs.EmailDomainWhitelist;
 | 
			
		||||
    if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
 | 
			
		||||
      setRestrictedDomainInput('');
 | 
			
		||||
      setInputs({
 | 
			
		||||
        ...inputs,
 | 
			
		||||
        EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
 | 
			
		||||
      });
 | 
			
		||||
      setEmailDomainWhitelist([...EmailDomainWhitelist, {
 | 
			
		||||
        key: restrictedDomainInput,
 | 
			
		||||
        text: restrictedDomainInput,
 | 
			
		||||
        value: restrictedDomainInput,
 | 
			
		||||
      }]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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}
 | 
			
		||||
            />
 | 
			
		||||
@@ -271,130 +251,125 @@ 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'>
 | 
			
		||||
            配置邮箱域名白名单
 | 
			
		||||
            <Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              label='启用邮箱域名白名单'
 | 
			
		||||
              name='EmailDomainRestrictionEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              checked={inputs.EmailDomainRestrictionEnabled === 'true'}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths={2}>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='允许的邮箱域名'
 | 
			
		||||
              placeholder='允许的邮箱域名'
 | 
			
		||||
              name='EmailDomainWhitelist'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.EmailDomainWhitelist}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={EmailDomainWhitelist}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='添加新的允许的邮箱域名'
 | 
			
		||||
              action={
 | 
			
		||||
                <Button type='button' onClick={() => {
 | 
			
		||||
                  submitNewRestrictedDomain();
 | 
			
		||||
                }}>填入</Button>
 | 
			
		||||
              }
 | 
			
		||||
              onKeyDown={(e) => {
 | 
			
		||||
                if (e.key === 'Enter') {
 | 
			
		||||
                  submitNewRestrictedDomain();
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='输入新的允许的邮箱域名'
 | 
			
		||||
              value={restrictedDomainInput}
 | 
			
		||||
              onChange={(e, { value }) => {
 | 
			
		||||
                setRestrictedDomainInput(value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
 | 
			
		||||
          <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'
 | 
			
		||||
              checked={inputs.RegisterEnabled === 'true'}
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
              value={inputs.SMTPToken}
 | 
			
		||||
              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}>
 | 
			
		||||
@@ -404,7 +379,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'
 | 
			
		||||
@@ -413,65 +388,66 @@ 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}>
 | 
			
		||||
@@ -481,7 +457,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'
 | 
			
		||||
@@ -490,11 +466,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>
 | 
			
		||||
@@ -1,31 +1,66 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  showWarning,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'>已启用</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          已启用
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='red'> 已禁用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='yellow'> 已过期 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='yellow'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已过期{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Label basic color='grey'> 已耗尽 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已耗尽{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知状态 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +103,7 @@ const TokensTable = () => {
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await loadTokens(activePage - 1);
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadTokens(0)
 | 
			
		||||
@@ -221,7 +256,7 @@ const TokensTable = () => {
 | 
			
		||||
          {tokens
 | 
			
		||||
            .slice(
 | 
			
		||||
              (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
              activePage * ITEMS_PER_PAGE
 | 
			
		||||
              activePage * ITEMS_PER_PAGE,
 | 
			
		||||
            )
 | 
			
		||||
            .map((token, idx) => {
 | 
			
		||||
              if (token.deleted) return <></>;
 | 
			
		||||
@@ -230,20 +265,30 @@ const TokensTable = () => {
 | 
			
		||||
                  <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(token.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {token.unlimited_quota
 | 
			
		||||
                      ? '无限制'
 | 
			
		||||
                      : renderQuota(token.remain_quota, 2)}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {token.expired_time === -1
 | 
			
		||||
                      ? '永不过期'
 | 
			
		||||
                      : renderTimestamp(token.expired_time)}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        positive
 | 
			
		||||
                        onClick={async () => {
 | 
			
		||||
                          let key = "sk-" + token.key;
 | 
			
		||||
                          let key = 'sk-' + token.key;
 | 
			
		||||
                          if (await copy(key)) {
 | 
			
		||||
                            showSuccess('已复制到剪贴板!');
 | 
			
		||||
                          } else {
 | 
			
		||||
                            showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
 | 
			
		||||
                            showWarning(
 | 
			
		||||
                              '无法复制到剪贴板,请手动复制,已将令牌填入搜索框。',
 | 
			
		||||
                            );
 | 
			
		||||
                            setSearchKeyword(key);
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
@@ -275,7 +320,7 @@ const TokensTable = () => {
 | 
			
		||||
                          manageToken(
 | 
			
		||||
                            token.id,
 | 
			
		||||
                            token.status === 1 ? 'disable' : 'enable',
 | 
			
		||||
                            idx
 | 
			
		||||
                            idx,
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
@@ -301,7 +346,9 @@ const TokensTable = () => {
 | 
			
		||||
              <Button size='small' as={Link} to='/token/add' loading={loading}>
 | 
			
		||||
                添加新的令牌
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                刷新
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Pagination
 | 
			
		||||
                floated='right'
 | 
			
		||||
                activePage={activePage}
 | 
			
		||||
@@ -1,10 +1,22 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showSuccess } from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  renderGroup,
 | 
			
		||||
  renderNumber,
 | 
			
		||||
  renderQuota,
 | 
			
		||||
  renderText,
 | 
			
		||||
} from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderRole(role) {
 | 
			
		||||
  switch (role) {
 | 
			
		||||
@@ -65,7 +77,7 @@ const UsersTable = () => {
 | 
			
		||||
    (async () => {
 | 
			
		||||
      const res = await API.post('/api/user/manage', {
 | 
			
		||||
        username,
 | 
			
		||||
        action
 | 
			
		||||
        action,
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -215,7 +227,7 @@ const UsersTable = () => {
 | 
			
		||||
          {users
 | 
			
		||||
            .slice(
 | 
			
		||||
              (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
              activePage * ITEMS_PER_PAGE
 | 
			
		||||
              activePage * ITEMS_PER_PAGE,
 | 
			
		||||
            )
 | 
			
		||||
            .map((user, idx) => {
 | 
			
		||||
              if (user.deleted) return <></>;
 | 
			
		||||
@@ -226,8 +238,10 @@ const UsersTable = () => {
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={user.email ? user.email : '未绑定邮箱地址'}
 | 
			
		||||
                      key={user.username}
 | 
			
		||||
                      header={user.display_name ? user.display_name : user.username}
 | 
			
		||||
                      trigger={<span>{renderText(user.username, 15)}</span>}
 | 
			
		||||
                      header={
 | 
			
		||||
                        user.display_name ? user.display_name : user.username
 | 
			
		||||
                      }
 | 
			
		||||
                      trigger={<span>{renderText(user.username, 10)}</span>}
 | 
			
		||||
                      hoverable
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
@@ -236,9 +250,22 @@ const UsersTable = () => {
 | 
			
		||||
                  {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
 | 
			
		||||
                  {/*</Table.Cell>*/}
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <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>} />
 | 
			
		||||
                    <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>
 | 
			
		||||
@@ -266,7 +293,11 @@ const UsersTable = () => {
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative disabled={user.role === 100}>
 | 
			
		||||
                          <Button
 | 
			
		||||
                            size='small'
 | 
			
		||||
                            negative
 | 
			
		||||
                            disabled={user.role === 100}
 | 
			
		||||
                          >
 | 
			
		||||
                            删除
 | 
			
		||||
                          </Button>
 | 
			
		||||
                        }
 | 
			
		||||
@@ -289,7 +320,7 @@ const UsersTable = () => {
 | 
			
		||||
                          manageUser(
 | 
			
		||||
                            user.username,
 | 
			
		||||
                            user.status === 1 ? 'disable' : 'enable',
 | 
			
		||||
                            idx
 | 
			
		||||
                            idx,
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                        disabled={user.role === 100}
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
export const CHANNEL_OPTIONS = [
 | 
			
		||||
  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
 | 
			
		||||
  { key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
 | 
			
		||||
  { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
 | 
			
		||||
  { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
 | 
			
		||||
  { key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
 | 
			
		||||
  { key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
 | 
			
		||||
  { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
 | 
			
		||||
  { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
 | 
			
		||||
  { key: 8, text: '自定义渠道', value: 8, color: 'pink' },
 | 
			
		||||
  { key: 2, text: '代理:API2D', value: 2, color: 'blue' },
 | 
			
		||||
  { key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
 | 
			
		||||
  { key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
 | 
			
		||||
  { key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
 | 
			
		||||
  { key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
 | 
			
		||||
  { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
 | 
			
		||||
  { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
 | 
			
		||||
  { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
 | 
			
		||||
  { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										17
									
								
								web/src/constants/channel.constants.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/src/constants/channel.constants.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
export const CHANNEL_OPTIONS = [
 | 
			
		||||
  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
 | 
			
		||||
  { key: 8, text: '自定义', value: 8, color: 'pink' },
 | 
			
		||||
  { key: 3, text: 'Azure', value: 3, color: 'olive' },
 | 
			
		||||
  { key: 2, text: 'API2D', value: 2, color: 'blue' },
 | 
			
		||||
  { key: 4, text: 'CloseAI', value: 4, color: 'teal' },
 | 
			
		||||
  { key: 5, text: 'OpenAI-SB', value: 5, color: 'brown' },
 | 
			
		||||
  { key: 6, text: 'OpenAI Max', value: 6, color: 'violet' },
 | 
			
		||||
  { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
 | 
			
		||||
  { key: 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: 13, text: 'AIGC2D', value: 13, color: 'purple' },
 | 
			
		||||
 | 
			
		||||
  //
 | 
			
		||||
  { key: 14, text: 'Chanzhaoyu/chatgpt-web', value: 14, color: 'purple' },
 | 
			
		||||
];
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export * from './toast.constants';
 | 
			
		||||
export * from './user.constants';
 | 
			
		||||
export * from './common.constant';
 | 
			
		||||
export * from './channel.constants';
 | 
			
		||||
export * from './channel.constants';
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
export const toastConstants = {
 | 
			
		||||
  SUCCESS_TIMEOUT: 1500,
 | 
			
		||||
  SUCCESS_TIMEOUT: 500,
 | 
			
		||||
  INFO_TIMEOUT: 3000,
 | 
			
		||||
  ERROR_TIMEOUT: 5000,
 | 
			
		||||
  WARNING_TIMEOUT: 10000,
 | 
			
		||||
  NOTICE_TIMEOUT: 20000
 | 
			
		||||
  NOTICE_TIMEOUT: 20000,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
export const userConstants = {
 | 
			
		||||
    REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
 | 
			
		||||
    REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
 | 
			
		||||
    REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
 | 
			
		||||
 | 
			
		||||
    LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
 | 
			
		||||
    LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
 | 
			
		||||
    LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
 | 
			
		||||
    
 | 
			
		||||
    LOGOUT: 'USERS_LOGOUT',
 | 
			
		||||
 | 
			
		||||
    GETALL_REQUEST: 'USERS_GETALL_REQUEST',
 | 
			
		||||
    GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
 | 
			
		||||
    GETALL_FAILURE: 'USERS_GETALL_FAILURE',
 | 
			
		||||
 | 
			
		||||
    DELETE_REQUEST: 'USERS_DELETE_REQUEST',
 | 
			
		||||
    DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
 | 
			
		||||
    DELETE_FAILURE: 'USERS_DELETE_FAILURE'    
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										19
									
								
								web/src/constants/user.constants.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								web/src/constants/user.constants.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
export const userConstants = {
 | 
			
		||||
  REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
 | 
			
		||||
  REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
 | 
			
		||||
  REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
 | 
			
		||||
 | 
			
		||||
  LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
 | 
			
		||||
  LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
 | 
			
		||||
  LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
 | 
			
		||||
 | 
			
		||||
  LOGOUT: 'USERS_LOGOUT',
 | 
			
		||||
 | 
			
		||||
  GETALL_REQUEST: 'USERS_GETALL_REQUEST',
 | 
			
		||||
  GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
 | 
			
		||||
  GETALL_FAILURE: 'USERS_GETALL_FAILURE',
 | 
			
		||||
 | 
			
		||||
  DELETE_REQUEST: 'USERS_DELETE_REQUEST',
 | 
			
		||||
  DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
 | 
			
		||||
  DELETE_FAILURE: 'USERS_DELETE_FAILURE',
 | 
			
		||||
};
 | 
			
		||||
@@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => {
 | 
			
		||||
      {children}
 | 
			
		||||
    </StatusContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
// contexts/User/index.jsx
 | 
			
		||||
 | 
			
		||||
import React from "react"
 | 
			
		||||
import { reducer, initialState } from "./reducer"
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { reducer, initialState } from './reducer';
 | 
			
		||||
 | 
			
		||||
export const UserContext = React.createContext({
 | 
			
		||||
  state: initialState,
 | 
			
		||||
  dispatch: () => null
 | 
			
		||||
})
 | 
			
		||||
  dispatch: () => null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const UserProvider = ({ children }) => {
 | 
			
		||||
  const [state, dispatch] = React.useReducer(reducer, initialState)
 | 
			
		||||
  const [state, dispatch] = React.useReducer(reducer, initialState);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <UserContext.Provider value={[ state, dispatch ]}>
 | 
			
		||||
      { children }
 | 
			
		||||
    <UserContext.Provider value={[state, dispatch]}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </UserContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
 | 
			
		||||
    case 'login':
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: action.payload
 | 
			
		||||
        user: action.payload,
 | 
			
		||||
      };
 | 
			
		||||
    case 'logout':
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: undefined
 | 
			
		||||
        user: undefined,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const initialState = {
 | 
			
		||||
  user: undefined
 | 
			
		||||
};
 | 
			
		||||
  user: undefined,
 | 
			
		||||
};
 | 
			
		||||
@@ -2,12 +2,12 @@ import { showError } from './utils';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
export const API = axios.create({
 | 
			
		||||
  baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '',
 | 
			
		||||
  baseURL: import.meta.env.VITE_REACT_APP_SERVER ? import.meta.env.VITE_REACT_APP_SERVER : '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
API.interceptors.response.use(
 | 
			
		||||
  (response) => response,
 | 
			
		||||
  (error) => {
 | 
			
		||||
    showError(error);
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
export function authHeader() {
 | 
			
		||||
    // return authorization header with jwt token
 | 
			
		||||
    let user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
 | 
			
		||||
    if (user && user.token) {
 | 
			
		||||
        return { 'Authorization': 'Bearer ' + user.token };
 | 
			
		||||
    } else {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								web/src/helpers/auth-header.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/src/helpers/auth-header.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
export function authHeader() {
 | 
			
		||||
  // return authorization header with jwt token
 | 
			
		||||
  let user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
 | 
			
		||||
  if (user && user.token) {
 | 
			
		||||
    return { Authorization: 'Bearer ' + user.token };
 | 
			
		||||
  } else {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
import { createBrowserHistory } from 'history';
 | 
			
		||||
 | 
			
		||||
export const history = createBrowserHistory();
 | 
			
		||||
export const history = createBrowserHistory();
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export * from './history';
 | 
			
		||||
export * from './auth-header';
 | 
			
		||||
export * from './utils';
 | 
			
		||||
export * from './api';
 | 
			
		||||
export * from './api';
 | 
			
		||||
@@ -13,16 +13,18 @@ export function renderGroup(group) {
 | 
			
		||||
  }
 | 
			
		||||
  let groups = group.split(',');
 | 
			
		||||
  groups.sort();
 | 
			
		||||
  return <>
 | 
			
		||||
    {groups.map((group) => {
 | 
			
		||||
      if (group === 'vip' || group === 'pro') {
 | 
			
		||||
        return <Label color='yellow'>{group}</Label>;
 | 
			
		||||
      } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
        return <Label color='red'>{group}</Label>;
 | 
			
		||||
      }
 | 
			
		||||
      return <Label>{group}</Label>;
 | 
			
		||||
    })}
 | 
			
		||||
  </>;
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {groups.map((group) => {
 | 
			
		||||
        if (group === 'vip' || group === 'pro') {
 | 
			
		||||
          return <Label color='yellow'>{group}</Label>;
 | 
			
		||||
        } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
          return <Label color='red'>{group}</Label>;
 | 
			
		||||
        }
 | 
			
		||||
        return <Label>{group}</Label>;
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderNumber(num) {
 | 
			
		||||
@@ -55,4 +57,4 @@ export function renderQuotaWithPrompt(quota, digits) {
 | 
			
		||||
    return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,7 @@ export function getSystemName() {
 | 
			
		||||
export function getLogo() {
 | 
			
		||||
  let logo = localStorage.getItem('logo');
 | 
			
		||||
  if (!logo) return '/logo.png';
 | 
			
		||||
  return logo
 | 
			
		||||
  return logo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFooterHTML() {
 | 
			
		||||
@@ -147,17 +147,7 @@ export function timestamp2string(timestamp) {
 | 
			
		||||
    second = '0' + second;
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    year +
 | 
			
		||||
    '-' +
 | 
			
		||||
    month +
 | 
			
		||||
    '-' +
 | 
			
		||||
    day +
 | 
			
		||||
    ' ' +
 | 
			
		||||
    hour +
 | 
			
		||||
    ':' +
 | 
			
		||||
    minute +
 | 
			
		||||
    ':' +
 | 
			
		||||
    second
 | 
			
		||||
    year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -177,4 +167,4 @@ export const verifyJSON = (str) => {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
@@ -1,35 +1,37 @@
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding-top: 55px;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
 | 
			
		||||
    -webkit-font-smoothing: antialiased;
 | 
			
		||||
    -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding-top: 55px;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
 | 
			
		||||
    sans-serif;
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
 | 
			
		||||
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
 | 
			
		||||
    monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-content {
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
  padding: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small-icon .icon {
 | 
			
		||||
    font-size: 1em !important;
 | 
			
		||||
  font-size: 1em !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-footer {
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
  font-size: 1.1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 600px) {
 | 
			
		||||
    .hide-on-mobile {
 | 
			
		||||
        display: none !important;
 | 
			
		||||
    }
 | 
			
		||||
  .hide-on-mobile {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,5 +27,5 @@ root.render(
 | 
			
		||||
        </BrowserRouter>
 | 
			
		||||
      </UserProvider>
 | 
			
		||||
    </StatusProvider>
 | 
			
		||||
  </React.StrictMode>
 | 
			
		||||
  </React.StrictMode>,
 | 
			
		||||
);
 | 
			
		||||
@@ -31,8 +31,8 @@ const About = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {
 | 
			
		||||
        aboutLoaded && about === '' ? <>
 | 
			
		||||
      {aboutLoaded && about === '' ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Header as='h3'>关于</Header>
 | 
			
		||||
            <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
 | 
			
		||||
@@ -41,18 +41,26 @@ const About = () => {
 | 
			
		||||
              https://github.com/songquanpeng/one-api
 | 
			
		||||
            </a>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </> : <>
 | 
			
		||||
          {
 | 
			
		||||
            about.startsWith('https://') ? <iframe
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {about.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={about}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
 | 
			
		||||
          }
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Segment>
 | 
			
		||||
              <div
 | 
			
		||||
                style={{ fontSize: 'larger' }}
 | 
			
		||||
                dangerouslySetInnerHTML={{ __html: about }}
 | 
			
		||||
              ></div>
 | 
			
		||||
            </Segment>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      }
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default About;
 | 
			
		||||
@@ -1,391 +0,0 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
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;
 | 
			
		||||
  const isEdit = channelId !== undefined;
 | 
			
		||||
  const [loading, setLoading] = useState(isEdit);
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    type: 1,
 | 
			
		||||
    key: '',
 | 
			
		||||
    base_url: '',
 | 
			
		||||
    other: '',
 | 
			
		||||
    model_mapping: '',
 | 
			
		||||
    models: [],
 | 
			
		||||
    groups: ['default']
 | 
			
		||||
  };
 | 
			
		||||
  const [batch, setBatch] = useState(false);
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const [originModelOptions, setOriginModelOptions] = useState([]);
 | 
			
		||||
  const [modelOptions, setModelOptions] = useState([]);
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const [basicModels, setBasicModels] = useState([]);
 | 
			
		||||
  const [fullModels, setFullModels] = useState([]);
 | 
			
		||||
  const [customModel, setCustomModel] = useState('');
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
    if (name === 'type' && inputs.models.length === 0) {
 | 
			
		||||
      let localModels = [];
 | 
			
		||||
      switch (value) {
 | 
			
		||||
        case 14:
 | 
			
		||||
          localModels = ['claude-instant-1', 'claude-2'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 11:
 | 
			
		||||
          localModels = ['PaLM-2'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 15:
 | 
			
		||||
          localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'Embedding-V1'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 17:
 | 
			
		||||
          localModels = ['qwen-v1', 'qwen-plus-v1'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 16:
 | 
			
		||||
          localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 18:
 | 
			
		||||
          localModels = ['SparkDesk'];
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      setInputs((inputs) => ({ ...inputs, models: localModels }));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadChannel = async () => {
 | 
			
		||||
    let res = await API.get(`/api/channel/${channelId}`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (data.models === '') {
 | 
			
		||||
        data.models = [];
 | 
			
		||||
      } else {
 | 
			
		||||
        data.models = data.models.split(',');
 | 
			
		||||
      }
 | 
			
		||||
      if (data.group === '') {
 | 
			
		||||
        data.groups = [];
 | 
			
		||||
      } 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);
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchModels = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/channel/models`);
 | 
			
		||||
      let localModelOptions = res.data.data.map((model) => ({
 | 
			
		||||
        key: model.id,
 | 
			
		||||
        text: model.id,
 | 
			
		||||
        value: model.id
 | 
			
		||||
      }));
 | 
			
		||||
      setOriginModelOptions(localModelOptions);
 | 
			
		||||
      setFullModels(res.data.data.map((model) => model.id));
 | 
			
		||||
      setBasicModels(res.data.data.filter((model) => {
 | 
			
		||||
        return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
 | 
			
		||||
      }).map((model) => model.id));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchGroups = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      setGroupOptions(res.data.data.map((group) => ({
 | 
			
		||||
        key: group,
 | 
			
		||||
        text: group,
 | 
			
		||||
        value: group
 | 
			
		||||
      })));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let localModelOptions = [...originModelOptions];
 | 
			
		||||
    inputs.models.forEach((model) => {
 | 
			
		||||
      if (!localModelOptions.find((option) => option.key === model)) {
 | 
			
		||||
        localModelOptions.push({
 | 
			
		||||
          key: model,
 | 
			
		||||
          text: model,
 | 
			
		||||
          value: model
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    setModelOptions(localModelOptions);
 | 
			
		||||
  }, [originModelOptions, inputs.models]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      loadChannel().then();
 | 
			
		||||
    }
 | 
			
		||||
    fetchModels().then();
 | 
			
		||||
    fetchGroups().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const submit = async () => {
 | 
			
		||||
    if (!isEdit && (inputs.name === '' || inputs.key === '')) {
 | 
			
		||||
      showInfo('请填写渠道名称和渠道密钥!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (inputs.models.length === 0) {
 | 
			
		||||
      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);
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.type === 3 && localInputs.other === '') {
 | 
			
		||||
      localInputs.other = '2023-06-01-preview';
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.model_mapping === '') {
 | 
			
		||||
      localInputs.model_mapping = '{}';
 | 
			
		||||
    }
 | 
			
		||||
    let res;
 | 
			
		||||
    localInputs.models = localInputs.models.join(',');
 | 
			
		||||
    localInputs.group = localInputs.groups.join(',');
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/channel/`, localInputs);
 | 
			
		||||
    }
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (isEdit) {
 | 
			
		||||
        showSuccess('渠道更新成功!');
 | 
			
		||||
      } else {
 | 
			
		||||
        showSuccess('渠道创建成功!');
 | 
			
		||||
        setInputs(originInputs);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              label='类型'
 | 
			
		||||
              name='type'
 | 
			
		||||
              required
 | 
			
		||||
              options={CHANNEL_OPTIONS}
 | 
			
		||||
              value={inputs.type}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 3 && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Message>
 | 
			
		||||
                  注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
 | 
			
		||||
                  参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
 | 
			
		||||
                                                                    href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
 | 
			
		||||
                </Message>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='AZURE_OPENAI_ENDPOINT'
 | 
			
		||||
                    name='base_url'
 | 
			
		||||
                    placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.base_url}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='默认 API 版本'
 | 
			
		||||
                    name='other'
 | 
			
		||||
                    placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.other}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 8 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Base URL'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              required
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请为渠道命名'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='分组'
 | 
			
		||||
              placeholder={'请选择可以使用该渠道的分组'}
 | 
			
		||||
              name='groups'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              allowAdditions
 | 
			
		||||
              additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.groups}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={groupOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='模型'
 | 
			
		||||
              placeholder={'请选择该渠道所支持的模型'}
 | 
			
		||||
              name='models'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.models}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={modelOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              handleInputChange(null, { name: 'models', value: basicModels });
 | 
			
		||||
            }}>填入基础模型</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              handleInputChange(null, { name: 'models', value: fullModels });
 | 
			
		||||
            }}>填入所有模型</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
            }}>清除所有模型</Button>
 | 
			
		||||
            <Input
 | 
			
		||||
              action={
 | 
			
		||||
                <Button type={'button'} onClick={() => {
 | 
			
		||||
                  if (customModel.trim() === '') return;
 | 
			
		||||
                  if (inputs.models.includes(customModel)) return;
 | 
			
		||||
                  let localModels = [...inputs.models];
 | 
			
		||||
                  localModels.push(customModel);
 | 
			
		||||
                  let localModelOptions = [];
 | 
			
		||||
                  localModelOptions.push({
 | 
			
		||||
                    key: customModel,
 | 
			
		||||
                    text: customModel,
 | 
			
		||||
                    value: customModel
 | 
			
		||||
                  });
 | 
			
		||||
                  setModelOptions(modelOptions => {
 | 
			
		||||
                    return [...modelOptions, ...localModelOptions];
 | 
			
		||||
                  });
 | 
			
		||||
                  setCustomModel('');
 | 
			
		||||
                  handleInputChange(null, { name: 'models', value: localModels });
 | 
			
		||||
                }}>填入</Button>
 | 
			
		||||
              }
 | 
			
		||||
              placeholder='输入自定义模型名称'
 | 
			
		||||
              value={customModel}
 | 
			
		||||
              onChange={(e, { value }) => {
 | 
			
		||||
                setCustomModel(value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </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}
 | 
			
		||||
                style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field> : <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            !isEdit && (
 | 
			
		||||
              <Form.Checkbox
 | 
			
		||||
                checked={batch}
 | 
			
		||||
                label='批量创建'
 | 
			
		||||
                name='batch'
 | 
			
		||||
                onChange={() => setBatch(!batch)}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 3 && inputs.type !== 8 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='镜像'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EditChannel;
 | 
			
		||||
							
								
								
									
										438
									
								
								web/src/pages/Channel/EditChannel.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								web/src/pages/Channel/EditChannel.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,438 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Header,
 | 
			
		||||
  Input,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
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;
 | 
			
		||||
  const isEdit = channelId !== undefined;
 | 
			
		||||
  const [loading, setLoading] = useState(isEdit);
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    type: 1,
 | 
			
		||||
    key: '',
 | 
			
		||||
    base_url: '',
 | 
			
		||||
    other: '',
 | 
			
		||||
    model_mapping: '',
 | 
			
		||||
    models: [],
 | 
			
		||||
    groups: ['default'],
 | 
			
		||||
    enable_ip_randomization: false,
 | 
			
		||||
  };
 | 
			
		||||
  const [batch, setBatch] = useState(false);
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const [modelOptions, setModelOptions] = useState([]);
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const [basicModels, setBasicModels] = useState([]);
 | 
			
		||||
  const [fullModels, setFullModels] = useState([]);
 | 
			
		||||
  const [customModel, setCustomModel] = useState('');
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    console.log(name, value);
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadChannel = async () => {
 | 
			
		||||
    let res = await API.get(`/api/channel/${channelId}`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (data.models === '') {
 | 
			
		||||
        data.models = [];
 | 
			
		||||
      } else {
 | 
			
		||||
        data.models = data.models.split(',');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          let localModelOptions = [...modelOptions];
 | 
			
		||||
          data.models.forEach((model) => {
 | 
			
		||||
            if (!localModelOptions.find((option) => option.key === model)) {
 | 
			
		||||
              localModelOptions.push({
 | 
			
		||||
                key: model,
 | 
			
		||||
                text: model,
 | 
			
		||||
                value: model,
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          setModelOptions(localModelOptions);
 | 
			
		||||
        }, 1000);
 | 
			
		||||
      }
 | 
			
		||||
      if (data.group === '') {
 | 
			
		||||
        data.groups = [];
 | 
			
		||||
      } 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);
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchModels = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/channel/models`);
 | 
			
		||||
      setModelOptions(
 | 
			
		||||
        res.data.data.map((model) => ({
 | 
			
		||||
          key: model.id,
 | 
			
		||||
          text: model.id,
 | 
			
		||||
          value: model.id,
 | 
			
		||||
        })),
 | 
			
		||||
      );
 | 
			
		||||
      setFullModels(res.data.data.map((model) => model.id));
 | 
			
		||||
      setBasicModels(
 | 
			
		||||
        res.data.data
 | 
			
		||||
          .filter((model) => !model.id.startsWith('gpt-4'))
 | 
			
		||||
          .map((model) => model.id),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const fetchGroups = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      setGroupOptions(
 | 
			
		||||
        res.data.data.map((group) => ({
 | 
			
		||||
          key: group,
 | 
			
		||||
          text: group,
 | 
			
		||||
          value: group,
 | 
			
		||||
        })),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      loadChannel().then();
 | 
			
		||||
    }
 | 
			
		||||
    fetchModels().then();
 | 
			
		||||
    fetchGroups().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const submit = async () => {
 | 
			
		||||
    if (!isEdit && (inputs.name === '' || inputs.key === '')) {
 | 
			
		||||
      showInfo('请填写渠道名称和渠道密钥!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (inputs.models.length === 0) {
 | 
			
		||||
      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,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.type === 3 && localInputs.other === '') {
 | 
			
		||||
      localInputs.other = '2023-03-15-preview';
 | 
			
		||||
    }
 | 
			
		||||
    let res;
 | 
			
		||||
    localInputs.models = localInputs.models.join(',');
 | 
			
		||||
    localInputs.group = localInputs.groups.join(',');
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/channel/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(channelId),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/channel/`, localInputs);
 | 
			
		||||
    }
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (isEdit) {
 | 
			
		||||
        showSuccess('渠道更新成功!');
 | 
			
		||||
      } else {
 | 
			
		||||
        showSuccess('渠道创建成功!');
 | 
			
		||||
        setInputs(originInputs);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              label='类型'
 | 
			
		||||
              name='type'
 | 
			
		||||
              required
 | 
			
		||||
              options={CHANNEL_OPTIONS}
 | 
			
		||||
              value={inputs.type}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {inputs.type === 3 && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Message>
 | 
			
		||||
                注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为
 | 
			
		||||
                One API 会把请求体中的 model
 | 
			
		||||
                参数替换为你的部署名称(模型名称中的点会被剔除),
 | 
			
		||||
                <a
 | 
			
		||||
                  target='_blank'
 | 
			
		||||
                  href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
 | 
			
		||||
                >
 | 
			
		||||
                  图片演示
 | 
			
		||||
                </a>
 | 
			
		||||
                。
 | 
			
		||||
              </Message>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='AZURE_OPENAI_ENDPOINT'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='默认 API 版本'
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          {inputs.type === 8 && (
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='Base URL'
 | 
			
		||||
                name='base_url'
 | 
			
		||||
                placeholder={
 | 
			
		||||
                  '请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'
 | 
			
		||||
                }
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.base_url}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
          )}
 | 
			
		||||
          {inputs.type !== 3 && inputs.type !== 8 && (
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='镜像'
 | 
			
		||||
                name='base_url'
 | 
			
		||||
                placeholder={
 | 
			
		||||
                  '此项可选,输入镜像站地址,格式为:https://domain.com'
 | 
			
		||||
                }
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.base_url}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
          )}
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              required
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请输入名称'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='分组'
 | 
			
		||||
              placeholder={'请选择分组'}
 | 
			
		||||
              name='groups'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              allowAdditions
 | 
			
		||||
              additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.groups}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={groupOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='模型'
 | 
			
		||||
              placeholder={'请选择该通道所支持的模型'}
 | 
			
		||||
              name='models'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.models}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={modelOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={'button'}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                handleInputChange(null, { name: 'models', value: basicModels });
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              填入基础模型
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={'button'}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                handleInputChange(null, { name: 'models', value: fullModels });
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              填入所有模型
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={'button'}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              清除所有模型
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Input
 | 
			
		||||
              action={
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    let localModels = [...inputs.models];
 | 
			
		||||
                    localModels.push(customModel);
 | 
			
		||||
                    let localModelOptions = [...modelOptions];
 | 
			
		||||
                    localModelOptions.push({
 | 
			
		||||
                      key: customModel,
 | 
			
		||||
                      text: customModel,
 | 
			
		||||
                      value: customModel,
 | 
			
		||||
                    });
 | 
			
		||||
                    setModelOptions(localModelOptions);
 | 
			
		||||
                    handleInputChange(null, {
 | 
			
		||||
                      name: 'models',
 | 
			
		||||
                      value: localModels,
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入
 | 
			
		||||
                </Button>
 | 
			
		||||
              }
 | 
			
		||||
              placeholder='输入自定义模型名称'
 | 
			
		||||
              value={customModel}
 | 
			
		||||
              onChange={(e, { value }) => {
 | 
			
		||||
                setCustomModel(value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              name='enable_ip_randomization'
 | 
			
		||||
              label='将IP随机地址传递给HTTP头'
 | 
			
		||||
              onChange={(e, { name, checked }) => {
 | 
			
		||||
                handleInputChange(e, { name, value: checked });
 | 
			
		||||
              }}
 | 
			
		||||
              checked={inputs.enable_ip_randomization}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.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'
 | 
			
		||||
                placeholder={'请输入密钥,一行一个'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
                style={{
 | 
			
		||||
                  minHeight: 150,
 | 
			
		||||
                  fontFamily: 'JetBrains Mono, Consolas',
 | 
			
		||||
                }}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={'请输入密钥'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
          )}
 | 
			
		||||
          {!isEdit && (
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={batch}
 | 
			
		||||
              label='批量创建'
 | 
			
		||||
              name='batch'
 | 
			
		||||
              onChange={() => setBatch(!batch)}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>
 | 
			
		||||
            提交
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EditChannel;
 | 
			
		||||
@@ -11,5 +11,4 @@ const Chat = () => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default Chat;
 | 
			
		||||
@@ -52,8 +52,8 @@ const Home = () => {
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {
 | 
			
		||||
        homePageContentLoaded && homePageContent === '' ? <>
 | 
			
		||||
      {homePageContentLoaded && homePageContent === '' ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Header as='h3'>系统状况</Header>
 | 
			
		||||
            <Grid columns={2} stackable>
 | 
			
		||||
@@ -97,6 +97,12 @@ const Home = () => {
 | 
			
		||||
                          ? '已启用'
 | 
			
		||||
                          : '未启用'}
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p>
 | 
			
		||||
                        Discord 身份验证:
 | 
			
		||||
                        {statusState?.status?.discord_oauth === true
 | 
			
		||||
                          ? '已启用'
 | 
			
		||||
                          : '未启用'}
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p>
 | 
			
		||||
                        微信身份验证:
 | 
			
		||||
                        {statusState?.status?.wechat_login === true
 | 
			
		||||
@@ -115,16 +121,22 @@ const Home = () => {
 | 
			
		||||
              </Grid.Column>
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </> : <>
 | 
			
		||||
          {
 | 
			
		||||
            homePageContent.startsWith('https://') ? <iframe
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {homePageContent.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={homePageContent}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
 | 
			
		||||
          }
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: homePageContent }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user