Spaces:
Paused
Paused
Upload 30 files
Browse files- Dockerfile +12 -0
- LICENSE +21 -0
- README.md +231 -1
- api/chatgpt/api.go +188 -0
- api/chatgpt/constant.go +12 -0
- api/chatgpt/health_check.go +75 -0
- api/chatgpt/login.go +27 -0
- api/chatgpt/typings.go +96 -0
- api/common.go +204 -0
- api/imitate/api.go +293 -0
- api/imitate/convert.go +36 -0
- api/imitate/request.go +54 -0
- api/imitate/response.go +146 -0
- api/platform/access_token.go +115 -0
- api/platform/api.go +97 -0
- api/platform/constant.go +21 -0
- api/platform/login.go +80 -0
- api/platform/typings.go +23 -0
- compose.yaml +19 -0
- env/env.go +9 -0
- example/chatgpt.http +129 -0
- example/imitate.http +19 -0
- example/ios.http +50 -0
- example/platform.http +65 -0
- go.mod +48 -0
- go.sum +154 -0
- main.go +94 -0
- middleware/authorization.go +92 -0
- middleware/cors.go +21 -0
- render.yaml +7 -0
Dockerfile
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM golang:alpine AS builder
|
2 |
+
WORKDIR /app
|
3 |
+
COPY . .
|
4 |
+
RUN go build -ldflags="-w -s" -o go-chatgpt-api main.go
|
5 |
+
|
6 |
+
FROM alpine
|
7 |
+
WORKDIR /app
|
8 |
+
COPY --from=builder /app/go-chatgpt-api .
|
9 |
+
RUN apk add --no-cache tzdata
|
10 |
+
ENV TZ=Asia/Shanghai
|
11 |
+
EXPOSE 8080
|
12 |
+
CMD ["/app/go-chatgpt-api"]
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 linweiyuan
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -6,5 +6,235 @@ colorTo: green
|
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
|
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
9 |
+
# go-chatgpt-api
|
10 |
|
11 |
+
## 一个尝试绕过 `Cloudflare` 来使用 `ChatGPT` 接口的程序
|
12 |
+
|
13 |
+
(本项目没什么问题的话,基本不会有什么大的更新了,够用了,同时欢迎 PR)
|
14 |
+
|
15 |
+
---
|
16 |
+
|
17 |
+
### 支持接口
|
18 |
+
|
19 |
+
- https://chat.openai.com/auth/login 登录返回 `accessToken`(谷歌和微软账号暂不支持登录,但可正常使用其他接口)
|
20 |
+
- 模型和插件查询
|
21 |
+
- `GPT-3.5` 和 `GPT-4` 对话增删改查及分享
|
22 |
+
- https://platform.openai.com/playground 登录返回 `apiKey`
|
23 |
+
- `apiKey` 余额查询
|
24 |
+
- 等等 ...
|
25 |
+
- 支持 `ChatGPT` 转 `API`,接口 `/imitate/v1/chat/completions`,利用 `accessToken` 模拟 `apiKey`,实现伪免费使用 `API`,从而支持集成仅支持 `apiKey` 调用的第三方客户端项目,分享一个好用的脚本测试 `web-to-api` (https://github.com/linweiyuan/go-chatgpt-api/issues/251)
|
26 |
+
|
27 |
+
```python
|
28 |
+
import openai
|
29 |
+
|
30 |
+
openai.api_key = "这里填 access token,不是 api key"
|
31 |
+
openai.api_base = "http://127.0.0.1:8080/imitate/v1"
|
32 |
+
|
33 |
+
while True:
|
34 |
+
text = input("请输入问题:")
|
35 |
+
response = openai.ChatCompletion.create(
|
36 |
+
model='gpt-3.5-turbo',
|
37 |
+
messages=[
|
38 |
+
{'role': 'user', 'content': text},
|
39 |
+
],
|
40 |
+
stream=True
|
41 |
+
)
|
42 |
+
|
43 |
+
for chunk in response:
|
44 |
+
print(chunk.choices[0].delta.get("content", ""), end="", flush=True)
|
45 |
+
print("\n")
|
46 |
+
```
|
47 |
+
|
48 |
+
范例(URL 和参数基本保持着和官网一致,部分接口有些许改动),部分例子,不是全部,**理论上**全部基于文本传输的接口都支持
|
49 |
+
|
50 |
+
https://github.com/linweiyuan/go-chatgpt-api/tree/main/example
|
51 |
+
|
52 |
+
---
|
53 |
+
|
54 |
+
### 使用的过程中遇到问题应该如何解决
|
55 |
+
|
56 |
+
汇总贴:https://github.com/linweiyuan/go-chatgpt-api/issues/74
|
57 |
+
|
58 |
+
如果有疑问而不是什么程序出错其实可以在 [Discussions](https://github.com/linweiyuan/go-chatgpt-api/discussions) 里发而不是新增 Issue
|
59 |
+
|
60 |
+
群聊:https://github.com/linweiyuan/go-chatgpt-api/discussions/197
|
61 |
+
|
62 |
+
再说一遍,不要来 `Issues` 提你的疑问(再提不回复直接关闭),有讨论区,有群,不要提脑残问题,反面教材:https://github.com/linweiyuan/go-chatgpt-api/issues/255
|
63 |
+
|
64 |
+
---
|
65 |
+
|
66 |
+
### 配置
|
67 |
+
|
68 |
+
如需设置代理,可以设置环境变量 `PROXY`,比如 `PROXY=http://127.0.0.1:20171` 或者 `PROXY=socks5://127.0.0.1:20170`,注释掉或者留空则不启用
|
69 |
+
|
70 |
+
如果代理需账号密码验证,则 `http://username:password@ip:port` 或者 `socks5://username:password@ip:port`
|
71 |
+
|
72 |
+
如需配合 `warp` 使用:`PROXY=socks5://chatgpt-proxy-server-warp:65535`,因为需要设置 `warp` 的场景已经默认可以直接访问 `ChatGPT` 官网,因此共用一个变量不冲突(国内 `VPS` 不在讨论范围内,请自行配置网络环境,`warp` 服务在魔法环境下才能正常工作)
|
73 |
+
|
74 |
+
家庭网络无需跑 `warp` 服务,跑了也没用,会报错,仅在服务器需要
|
75 |
+
|
76 |
+
`CONTINUE_SIGNAL=1`,开启 `/imitate` 接口自动继续会话功能,留空关闭,默认关闭
|
77 |
+
|
78 |
+
---
|
79 |
+
|
80 |
+
`GPT-4` 相关模型目前需要验证 `arkose_token`,社区已经有很多解决方案,请自行查找,其中一个能用的:https://github.com/linweiyuan/go-chatgpt-api/issues/252
|
81 |
+
|
82 |
+
参考配置视频(拉到文章最下面点开视频,需要自己有一定的动手能力,根据你的环境不同自行微调配置):[如何生成 GPT-4 arkose_token](https://linweiyuan.github.io/2023/06/24/%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90-GPT-4-arkose-token.html)
|
83 |
+
|
84 |
+
---
|
85 |
+
|
86 |
+
根据你的网络环境不同,可以展开查看对应配置,下面例子是基本参数,更多参数查看 [compose.yaml](https://github.com/linweiyuan/go-chatgpt-api/blob/main/compose.yaml)
|
87 |
+
|
88 |
+
<details>
|
89 |
+
|
90 |
+
<summary>直接利用现成的服务</summary>
|
91 |
+
|
92 |
+
服务器不定时维护,不保证高可用,利用这些服务导致的账号安全问题,与本项目无关
|
93 |
+
|
94 |
+
- https://go-chatgpt-api.linweiyuan.com
|
95 |
+
|
96 |
+
</details>
|
97 |
+
|
98 |
+
<details>
|
99 |
+
|
100 |
+
<summary>网络在直连或者通过代理的情况下可以正常访问 ChatGPT</summary>
|
101 |
+
|
102 |
+
```yaml
|
103 |
+
services:
|
104 |
+
go-chatgpt-api:
|
105 |
+
container_name: go-chatgpt-api
|
106 |
+
image: linweiyuan/go-chatgpt-api
|
107 |
+
ports:
|
108 |
+
- 8080:8080
|
109 |
+
environment:
|
110 |
+
- TZ=Asia/Shanghai
|
111 |
+
restart: unless-stopped
|
112 |
+
```
|
113 |
+
|
114 |
+
</details>
|
115 |
+
|
116 |
+
<details>
|
117 |
+
|
118 |
+
<summary>服务器无法正常访问 ChatGPT</summary>
|
119 |
+
|
120 |
+
```yaml
|
121 |
+
services:
|
122 |
+
go-chatgpt-api:
|
123 |
+
container_name: go-chatgpt-api
|
124 |
+
image: linweiyuan/go-chatgpt-api
|
125 |
+
ports:
|
126 |
+
- 8080:8080
|
127 |
+
environment:
|
128 |
+
- TZ=Asia/Shanghai
|
129 |
+
- PROXY=socks5://chatgpt-proxy-server-warp:65535
|
130 |
+
depends_on:
|
131 |
+
- chatgpt-proxy-server-warp
|
132 |
+
restart: unless-stopped
|
133 |
+
|
134 |
+
chatgpt-proxy-server-warp:
|
135 |
+
container_name: chatgpt-proxy-server-warp
|
136 |
+
image: linweiyuan/chatgpt-proxy-server-warp
|
137 |
+
restart: unless-stopped
|
138 |
+
```
|
139 |
+
|
140 |
+
</details>
|
141 |
+
|
142 |
+
---
|
143 |
+
|
144 |
+
目前 `warp` 容器检测到流量超过 1G 会自动���启,如果你知道什么是 `teams-enroll-token` (不知道就跳过),可以通过环境变量 `TEAMS_ENROLL_TOKEN` 设置它的值,然后利用这条命令来检查是否生效
|
145 |
+
|
146 |
+
`docker-compose exec chatgpt-proxy-server-warp warp-cli --accept-tos account | awk 'NR==1'`
|
147 |
+
|
148 |
+
```
|
149 |
+
Account type: Free (没有生效)
|
150 |
+
|
151 |
+
Account type: Team (设置正常)
|
152 |
+
```
|
153 |
+
|
154 |
+
---
|
155 |
+
|
156 |
+
### Render部署
|
157 |
+
|
158 |
+
点击下面的按钮一键部署,缺点是免费版本冷启动比较慢
|
159 |
+
|
160 |
+
[](https://render.com/deploy?repo=https://github.com/linweiyuan/go-chatgpt-api)
|
161 |
+
|
162 |
+
---
|
163 |
+
|
164 |
+
### 如何集成其他第三方客户端(下面的内容不一定是最新,有问题请去各自项目查看)
|
165 |
+
|
166 |
+
- [moeakwak/chatgpt-web-share](https://github.com/moeakwak/chatgpt-web-share)
|
167 |
+
|
168 |
+
环境变量
|
169 |
+
|
170 |
+
```
|
171 |
+
CHATGPT_BASE_URL=http://go-chatgpt-api:8080/chatgpt/backend-api/
|
172 |
+
```
|
173 |
+
|
174 |
+
- [lss233/chatgpt-mirai-qq-bot](https://github.com/lss233/chatgpt-mirai-qq-bot)
|
175 |
+
|
176 |
+
`config.cfg`
|
177 |
+
|
178 |
+
```
|
179 |
+
[openai]
|
180 |
+
browserless_endpoint = "http://go-chatgpt-api:8080/chatgpt/backend-api/"
|
181 |
+
```
|
182 |
+
|
183 |
+
- [Kerwin1202/chatgpt-web](https://github.com/Kerwin1202/chatgpt-web) | [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web)
|
184 |
+
|
185 |
+
环境变量
|
186 |
+
|
187 |
+
```
|
188 |
+
API_REVERSE_PROXY=http://go-chatgpt-api:8080/chatgpt/backend-api/conversation
|
189 |
+
```
|
190 |
+
|
191 |
+
- [pengzhile/pandora](https://github.com/pengzhile/pandora)(不完全兼容)
|
192 |
+
|
193 |
+
环境变量
|
194 |
+
|
195 |
+
```
|
196 |
+
CHATGPT_API_PREFIX=http://go-chatgpt-api:8080
|
197 |
+
```
|
198 |
+
|
199 |
+
---
|
200 |
+
|
201 |
+
- [1130600015/feishu-chatgpt](https://github.com/1130600015/feishu-chatgpt)
|
202 |
+
|
203 |
+
`application.yaml`
|
204 |
+
|
205 |
+
```yaml
|
206 |
+
proxy:
|
207 |
+
url: http://go-chatgpt-api:8080
|
208 |
+
```
|
209 |
+
|
210 |
+
---
|
211 |
+
|
212 |
+
- [Yidadaa/ChatGPT-Next-Web](https://github.com/Yidadaa/ChatGPT-Next-Web)
|
213 |
+
|
214 |
+
环境变量
|
215 |
+
|
216 |
+
```
|
217 |
+
BASE_URL=http://go-chatgpt-api:8080/imitate
|
218 |
+
```
|
219 |
+
|
220 |
+
---
|
221 |
+
|
222 |
+
### 相关博客(程序更新很多次,文章的内容可能和现在的不一样,仅供参考):[ChatGPT](https://linweiyuan.github.io/categories/ChatGPT/)
|
223 |
+
|
224 |
+
- [如何生成 GPT-4 arkose_token](https://linweiyuan.github.io/2023/06/24/%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90-GPT-4-arkose-token.html)
|
225 |
+
- [利用 HTTP Client 来调试 go-chatgpt-api](https://linweiyuan.github.io/2023/06/18/%E5%88%A9%E7%94%A8-HTTP-Client-%E6%9D%A5%E8%B0%83%E8%AF%95-go-chatgpt-api.html)
|
226 |
+
- [一种解决 ChatGPT Access denied 的方法](https://linweiyuan.github.io/2023/04/15/%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3-ChatGPT-Access-denied-%E7%9A%84%E6%96%B9%E6%B3%95.html)
|
227 |
+
- [ChatGPT 如何自建代理](https://linweiyuan.github.io/2023/04/08/ChatGPT-%E5%A6%82%E4%BD%95%E8%87%AA%E5%BB%BA%E4%BB%A3%E7%90%86.html)
|
228 |
+
- [一种取巧的方式绕过 Cloudflare v2 验证](https://linweiyuan.github.io/2023/03/14/%E4%B8%80%E7%A7%8D%E5%8F%96%E5%B7%A7%E7%9A%84%E6%96%B9%E5%BC%8F%E7%BB%95%E8%BF%87-Cloudflare-v2-%E9%AA%8C%E8%AF%81.html)
|
229 |
+
|
230 |
+
---
|
231 |
+
|
232 |
+
### 最后感谢各位同学
|
233 |
+
|
234 |
+
<a href="https://github.com/linweiyuan/go-chatgpt-api/graphs/contributors">
|
235 |
+
<img src="https://contrib.rocks/image?repo=linweiyuan/go-chatgpt-api" alt=""/>
|
236 |
+
</a>
|
237 |
+
|
238 |
+
---
|
239 |
+
|
240 |
+

|
api/chatgpt/api.go
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bufio"
|
5 |
+
"bytes"
|
6 |
+
"encoding/json"
|
7 |
+
"fmt"
|
8 |
+
"io"
|
9 |
+
"strings"
|
10 |
+
|
11 |
+
http "github.com/bogdanfinn/fhttp"
|
12 |
+
"github.com/gin-gonic/gin"
|
13 |
+
|
14 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
15 |
+
"github.com/linweiyuan/go-logger/logger"
|
16 |
+
)
|
17 |
+
|
18 |
+
func CreateConversation(c *gin.Context) {
|
19 |
+
var request CreateConversationRequest
|
20 |
+
if err := c.BindJSON(&request); err != nil {
|
21 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage))
|
22 |
+
return
|
23 |
+
}
|
24 |
+
|
25 |
+
if request.ConversationID == nil || *request.ConversationID == "" {
|
26 |
+
request.ConversationID = nil
|
27 |
+
}
|
28 |
+
|
29 |
+
if len(request.Messages) != 0 {
|
30 |
+
message := request.Messages[0]
|
31 |
+
if message.Author.Role == "" {
|
32 |
+
message.Author.Role = defaultRole
|
33 |
+
}
|
34 |
+
|
35 |
+
if message.Metadata == nil {
|
36 |
+
message.Metadata = map[string]string{}
|
37 |
+
}
|
38 |
+
|
39 |
+
request.Messages[0] = message
|
40 |
+
}
|
41 |
+
|
42 |
+
if strings.HasPrefix(request.Model, gpt4Model) && request.ArkoseToken == "" {
|
43 |
+
arkoseToken, err := api.GetArkoseToken()
|
44 |
+
if err != nil || arkoseToken == "" {
|
45 |
+
c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(err.Error()))
|
46 |
+
return
|
47 |
+
}
|
48 |
+
|
49 |
+
request.ArkoseToken = arkoseToken
|
50 |
+
}
|
51 |
+
|
52 |
+
resp, done := sendConversationRequest(c, request)
|
53 |
+
if done {
|
54 |
+
return
|
55 |
+
}
|
56 |
+
|
57 |
+
handleConversationResponse(c, resp, request)
|
58 |
+
}
|
59 |
+
|
60 |
+
func sendConversationRequest(c *gin.Context, request CreateConversationRequest) (*http.Response, bool) {
|
61 |
+
jsonBytes, _ := json.Marshal(request)
|
62 |
+
req, _ := http.NewRequest(http.MethodPost, api.ChatGPTApiUrlPrefix+"/backend-api/conversation", bytes.NewBuffer(jsonBytes))
|
63 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
64 |
+
req.Header.Set(api.AuthorizationHeader, api.GetAccessToken(c))
|
65 |
+
req.Header.Set("Accept", "text/event-stream")
|
66 |
+
if api.PUID != "" {
|
67 |
+
req.Header.Set("Cookie", "_puid="+api.PUID)
|
68 |
+
}
|
69 |
+
resp, err := api.Client.Do(req)
|
70 |
+
if err != nil {
|
71 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
72 |
+
return nil, true
|
73 |
+
}
|
74 |
+
|
75 |
+
if resp.StatusCode != http.StatusOK {
|
76 |
+
defer resp.Body.Close()
|
77 |
+
|
78 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
79 |
+
logger.Error(fmt.Sprintf(api.AccountDeactivatedErrorMessage, c.GetString(api.EmailKey)))
|
80 |
+
responseMap := make(map[string]interface{})
|
81 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
82 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
83 |
+
return nil, true
|
84 |
+
}
|
85 |
+
|
86 |
+
req, _ := http.NewRequest(http.MethodGet, api.ChatGPTApiUrlPrefix+"/backend-api/models?history_and_training_disabled=false", nil)
|
87 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
88 |
+
req.Header.Set(api.AuthorizationHeader, api.GetAccessToken(c))
|
89 |
+
response, err := api.Client.Do(req)
|
90 |
+
if err != nil {
|
91 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
92 |
+
return nil, true
|
93 |
+
}
|
94 |
+
|
95 |
+
defer response.Body.Close()
|
96 |
+
modelAvailable := false
|
97 |
+
var getModelsResponse GetModelsResponse
|
98 |
+
json.NewDecoder(response.Body).Decode(&getModelsResponse)
|
99 |
+
for _, model := range getModelsResponse.Models {
|
100 |
+
if model.Slug == request.Model {
|
101 |
+
modelAvailable = true
|
102 |
+
break
|
103 |
+
}
|
104 |
+
}
|
105 |
+
if !modelAvailable {
|
106 |
+
c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(noModelPermissionErrorMessage))
|
107 |
+
return nil, true
|
108 |
+
}
|
109 |
+
|
110 |
+
data, _ := io.ReadAll(resp.Body)
|
111 |
+
logger.Warn(string(data))
|
112 |
+
|
113 |
+
responseMap := make(map[string]interface{})
|
114 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
115 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
116 |
+
return nil, true
|
117 |
+
}
|
118 |
+
|
119 |
+
return resp, false
|
120 |
+
}
|
121 |
+
|
122 |
+
func handleConversationResponse(c *gin.Context, resp *http.Response, request CreateConversationRequest) {
|
123 |
+
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
124 |
+
|
125 |
+
isMaxTokens := false
|
126 |
+
continueParentMessageID := ""
|
127 |
+
continueConversationID := ""
|
128 |
+
|
129 |
+
defer resp.Body.Close()
|
130 |
+
reader := bufio.NewReader(resp.Body)
|
131 |
+
for {
|
132 |
+
if c.Request.Context().Err() != nil {
|
133 |
+
break
|
134 |
+
}
|
135 |
+
|
136 |
+
line, err := reader.ReadString('\n')
|
137 |
+
if err != nil {
|
138 |
+
break
|
139 |
+
}
|
140 |
+
|
141 |
+
line = strings.TrimSpace(line)
|
142 |
+
if strings.HasPrefix(line, "event") ||
|
143 |
+
strings.HasPrefix(line, "data: 20") ||
|
144 |
+
strings.HasPrefix(line, `data: {"conversation_id"`) ||
|
145 |
+
line == "" {
|
146 |
+
continue
|
147 |
+
}
|
148 |
+
|
149 |
+
responseJson := line[6:]
|
150 |
+
if strings.HasPrefix(responseJson, "[DONE]") && isMaxTokens && request.AutoContinue {
|
151 |
+
continue
|
152 |
+
}
|
153 |
+
|
154 |
+
// no need to unmarshal every time, but if response content has this "max_tokens", need to further check
|
155 |
+
if strings.TrimSpace(responseJson) != "" && strings.Contains(responseJson, responseTypeMaxTokens) {
|
156 |
+
var createConversationResponse CreateConversationResponse
|
157 |
+
json.Unmarshal([]byte(responseJson), &createConversationResponse)
|
158 |
+
message := createConversationResponse.Message
|
159 |
+
if message.Metadata.FinishDetails.Type == responseTypeMaxTokens && createConversationResponse.Message.Status == responseStatusFinishedSuccessfully {
|
160 |
+
isMaxTokens = true
|
161 |
+
continueParentMessageID = message.ID
|
162 |
+
continueConversationID = createConversationResponse.ConversationID
|
163 |
+
}
|
164 |
+
}
|
165 |
+
|
166 |
+
c.Writer.Write([]byte(line + "\n\n"))
|
167 |
+
c.Writer.Flush()
|
168 |
+
}
|
169 |
+
|
170 |
+
if isMaxTokens && request.AutoContinue {
|
171 |
+
continueConversationRequest := CreateConversationRequest{
|
172 |
+
ArkoseToken: request.ArkoseToken,
|
173 |
+
HistoryAndTrainingDisabled: request.HistoryAndTrainingDisabled,
|
174 |
+
Model: request.Model,
|
175 |
+
TimezoneOffsetMin: request.TimezoneOffsetMin,
|
176 |
+
|
177 |
+
Action: actionContinue,
|
178 |
+
ParentMessageID: continueParentMessageID,
|
179 |
+
ConversationID: &continueConversationID,
|
180 |
+
}
|
181 |
+
resp, done := sendConversationRequest(c, continueConversationRequest)
|
182 |
+
if done {
|
183 |
+
return
|
184 |
+
}
|
185 |
+
|
186 |
+
handleConversationResponse(c, resp, continueConversationRequest)
|
187 |
+
}
|
188 |
+
}
|
api/chatgpt/constant.go
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
const (
|
4 |
+
defaultRole = "user"
|
5 |
+
parseJsonErrorMessage = "failed to parse json request body"
|
6 |
+
|
7 |
+
gpt4Model = "gpt-4"
|
8 |
+
actionContinue = "continue"
|
9 |
+
responseTypeMaxTokens = "max_tokens"
|
10 |
+
responseStatusFinishedSuccessfully = "finished_successfully"
|
11 |
+
noModelPermissionErrorMessage = "you have no permission to use this model"
|
12 |
+
)
|
api/chatgpt/health_check.go
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"fmt"
|
5 |
+
"os"
|
6 |
+
"time"
|
7 |
+
|
8 |
+
"github.com/PuerkitoBio/goquery"
|
9 |
+
http "github.com/bogdanfinn/fhttp"
|
10 |
+
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
"github.com/linweiyuan/go-logger/logger"
|
13 |
+
)
|
14 |
+
|
15 |
+
const (
|
16 |
+
healthCheckUrl = "https://chat.openai.com/backend-api/accounts/check"
|
17 |
+
errorHintBlock = "looks like you have bean blocked by OpenAI, please change to a new IP or have a try with WARP"
|
18 |
+
errorHintFailedToStart = "failed to start, please try again later: %s"
|
19 |
+
sleepHours = 8760 // 365 days
|
20 |
+
)
|
21 |
+
|
22 |
+
func init() {
|
23 |
+
proxyUrl := os.Getenv("PROXY")
|
24 |
+
if proxyUrl != "" {
|
25 |
+
logger.Info("PROXY: " + proxyUrl)
|
26 |
+
api.Client.SetProxy(proxyUrl)
|
27 |
+
|
28 |
+
for {
|
29 |
+
resp, err := healthCheck()
|
30 |
+
if err != nil {
|
31 |
+
// wait for proxy to be ready
|
32 |
+
time.Sleep(time.Second)
|
33 |
+
continue
|
34 |
+
}
|
35 |
+
|
36 |
+
checkHealthCheckStatus(resp)
|
37 |
+
break
|
38 |
+
}
|
39 |
+
} else {
|
40 |
+
resp, err := healthCheck()
|
41 |
+
if err != nil {
|
42 |
+
logger.Error("failed to health check: " + err.Error())
|
43 |
+
os.Exit(1)
|
44 |
+
}
|
45 |
+
|
46 |
+
checkHealthCheckStatus(resp)
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
func healthCheck() (resp *http.Response, err error) {
|
51 |
+
req, _ := http.NewRequest(http.MethodGet, healthCheckUrl, nil)
|
52 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
53 |
+
resp, err = api.Client.Do(req)
|
54 |
+
return
|
55 |
+
}
|
56 |
+
|
57 |
+
func checkHealthCheckStatus(resp *http.Response) {
|
58 |
+
if resp != nil {
|
59 |
+
defer resp.Body.Close()
|
60 |
+
|
61 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
62 |
+
logger.Info(api.ReadyHint)
|
63 |
+
} else {
|
64 |
+
doc, _ := goquery.NewDocumentFromReader(resp.Body)
|
65 |
+
alert := doc.Find(".message").Text()
|
66 |
+
if alert != "" {
|
67 |
+
logger.Error(errorHintBlock)
|
68 |
+
} else {
|
69 |
+
logger.Error(fmt.Sprintf(errorHintFailedToStart, resp.Status))
|
70 |
+
}
|
71 |
+
time.Sleep(time.Hour * sleepHours)
|
72 |
+
os.Exit(1)
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
api/chatgpt/login.go
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
http "github.com/bogdanfinn/fhttp"
|
5 |
+
"github.com/gin-gonic/gin"
|
6 |
+
"github.com/xqdoo00o/OpenAIAuth/auth"
|
7 |
+
|
8 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
9 |
+
)
|
10 |
+
|
11 |
+
func Login(c *gin.Context) {
|
12 |
+
var loginInfo api.LoginInfo
|
13 |
+
if err := c.ShouldBindJSON(&loginInfo); err != nil {
|
14 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage))
|
15 |
+
return
|
16 |
+
}
|
17 |
+
|
18 |
+
authenticator := auth.NewAuthenticator(loginInfo.Username, loginInfo.Password, api.ProxyUrl)
|
19 |
+
if err := authenticator.Begin(); err != nil {
|
20 |
+
c.AbortWithStatusJSON(err.StatusCode, api.ReturnMessage(err.Details))
|
21 |
+
return
|
22 |
+
}
|
23 |
+
|
24 |
+
c.JSON(http.StatusOK, gin.H{
|
25 |
+
"accessToken": authenticator.GetAccessToken(),
|
26 |
+
})
|
27 |
+
}
|
api/chatgpt/typings.go
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"github.com/google/uuid"
|
5 |
+
)
|
6 |
+
|
7 |
+
type CreateConversationRequest struct {
|
8 |
+
Action string `json:"action"`
|
9 |
+
Messages []Message `json:"messages"`
|
10 |
+
Model string `json:"model"`
|
11 |
+
ParentMessageID string `json:"parent_message_id"`
|
12 |
+
ConversationID *string `json:"conversation_id"`
|
13 |
+
PluginIDs []string `json:"plugin_ids"`
|
14 |
+
TimezoneOffsetMin int `json:"timezone_offset_min"`
|
15 |
+
ArkoseToken string `json:"arkose_token"`
|
16 |
+
HistoryAndTrainingDisabled bool `json:"history_and_training_disabled"`
|
17 |
+
AutoContinue bool `json:"auto_continue"`
|
18 |
+
Suggestions []string `json:"suggestions"`
|
19 |
+
}
|
20 |
+
|
21 |
+
func (c *CreateConversationRequest) AddMessage(role string, content string) {
|
22 |
+
c.Messages = append(c.Messages, Message{
|
23 |
+
ID: uuid.New().String(),
|
24 |
+
Author: Author{Role: role},
|
25 |
+
Content: Content{ContentType: "text", Parts: []interface{}{content}},
|
26 |
+
Metadata: map[string]string{},
|
27 |
+
})
|
28 |
+
}
|
29 |
+
|
30 |
+
type Message struct {
|
31 |
+
Author Author `json:"author"`
|
32 |
+
Content Content `json:"content"`
|
33 |
+
ID string `json:"id"`
|
34 |
+
Metadata interface{} `json:"metadata"`
|
35 |
+
}
|
36 |
+
|
37 |
+
type Author struct {
|
38 |
+
Role string `json:"role"`
|
39 |
+
}
|
40 |
+
|
41 |
+
type Content struct {
|
42 |
+
ContentType string `json:"content_type"`
|
43 |
+
Parts []interface{} `json:"parts"`
|
44 |
+
}
|
45 |
+
|
46 |
+
type CreateConversationResponse struct {
|
47 |
+
Message struct {
|
48 |
+
ID string `json:"id"`
|
49 |
+
Author struct {
|
50 |
+
Role string `json:"role"`
|
51 |
+
Name interface{} `json:"name"`
|
52 |
+
Metadata struct {
|
53 |
+
} `json:"metadata"`
|
54 |
+
} `json:"author"`
|
55 |
+
CreateTime float64 `json:"create_time"`
|
56 |
+
UpdateTime interface{} `json:"update_time"`
|
57 |
+
Content struct {
|
58 |
+
ContentType string `json:"content_type"`
|
59 |
+
Parts []string `json:"parts"`
|
60 |
+
} `json:"content"`
|
61 |
+
Status string `json:"status"`
|
62 |
+
EndTurn bool `json:"end_turn"`
|
63 |
+
Weight float64 `json:"weight"`
|
64 |
+
Metadata struct {
|
65 |
+
MessageType string `json:"message_type"`
|
66 |
+
ModelSlug string `json:"model_slug"`
|
67 |
+
FinishDetails struct {
|
68 |
+
Type string `json:"type"`
|
69 |
+
} `json:"finish_details"`
|
70 |
+
} `json:"metadata"`
|
71 |
+
Recipient string `json:"recipient"`
|
72 |
+
} `json:"message"`
|
73 |
+
ConversationID string `json:"conversation_id"`
|
74 |
+
Error interface{} `json:"error"`
|
75 |
+
}
|
76 |
+
|
77 |
+
type GetModelsResponse struct {
|
78 |
+
Models []struct {
|
79 |
+
Slug string `json:"slug"`
|
80 |
+
MaxTokens int `json:"max_tokens"`
|
81 |
+
Title string `json:"title"`
|
82 |
+
Description string `json:"description"`
|
83 |
+
Tags []string `json:"tags"`
|
84 |
+
Capabilities struct {
|
85 |
+
} `json:"capabilities"`
|
86 |
+
EnabledTools []string `json:"enabled_tools,omitempty"`
|
87 |
+
} `json:"models"`
|
88 |
+
Categories []struct {
|
89 |
+
Category string `json:"category"`
|
90 |
+
HumanCategoryName string `json:"human_category_name"`
|
91 |
+
SubscriptionLevel string `json:"subscription_level"`
|
92 |
+
DefaultModel string `json:"default_model"`
|
93 |
+
CodeInterpreterModel string `json:"code_interpreter_model"`
|
94 |
+
PluginsModel string `json:"plugins_model"`
|
95 |
+
} `json:"categories"`
|
96 |
+
}
|
api/common.go
ADDED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package api
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bytes"
|
5 |
+
"encoding/json"
|
6 |
+
"fmt"
|
7 |
+
"io"
|
8 |
+
"os"
|
9 |
+
"strings"
|
10 |
+
"time"
|
11 |
+
|
12 |
+
http "github.com/bogdanfinn/fhttp"
|
13 |
+
tls_client "github.com/bogdanfinn/tls-client"
|
14 |
+
"github.com/bogdanfinn/tls-client/profiles"
|
15 |
+
"github.com/gin-gonic/gin"
|
16 |
+
"github.com/xqdoo00o/OpenAIAuth/auth"
|
17 |
+
"github.com/xqdoo00o/funcaptcha"
|
18 |
+
|
19 |
+
"github.com/linweiyuan/go-logger/logger"
|
20 |
+
)
|
21 |
+
|
22 |
+
const (
|
23 |
+
ChatGPTApiPrefix = "/chatgpt"
|
24 |
+
ImitateApiPrefix = "/imitate/v1"
|
25 |
+
ChatGPTApiUrlPrefix = "https://chat.openai.com"
|
26 |
+
|
27 |
+
PlatformApiPrefix = "/platform"
|
28 |
+
PlatformApiUrlPrefix = "https://api.openai.com"
|
29 |
+
|
30 |
+
defaultErrorMessageKey = "errorMessage"
|
31 |
+
AuthorizationHeader = "Authorization"
|
32 |
+
XAuthorizationHeader = "X-Authorization"
|
33 |
+
ContentType = "application/x-www-form-urlencoded"
|
34 |
+
UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
|
35 |
+
Auth0Url = "https://auth0.openai.com"
|
36 |
+
LoginUsernameUrl = Auth0Url + "/u/login/identifier?state="
|
37 |
+
LoginPasswordUrl = Auth0Url + "/u/login/password?state="
|
38 |
+
ParseUserInfoErrorMessage = "failed to parse user login info"
|
39 |
+
GetAuthorizedUrlErrorMessage = "failed to get authorized url"
|
40 |
+
EmailInvalidErrorMessage = "email is not valid"
|
41 |
+
EmailOrPasswordInvalidErrorMessage = "email or password is not correct"
|
42 |
+
GetAccessTokenErrorMessage = "failed to get access token"
|
43 |
+
defaultTimeoutSeconds = 600 // 10 minutes
|
44 |
+
|
45 |
+
EmailKey = "email"
|
46 |
+
AccountDeactivatedErrorMessage = "account %s is deactivated"
|
47 |
+
|
48 |
+
ReadyHint = "service go-chatgpt-api is ready"
|
49 |
+
|
50 |
+
refreshPuidErrorMessage = "failed to refresh PUID"
|
51 |
+
)
|
52 |
+
|
53 |
+
var (
|
54 |
+
Client tls_client.HttpClient
|
55 |
+
ArkoseClient tls_client.HttpClient
|
56 |
+
PUID string
|
57 |
+
ProxyUrl string
|
58 |
+
)
|
59 |
+
|
60 |
+
type LoginInfo struct {
|
61 |
+
Username string `json:"username"`
|
62 |
+
Password string `json:"password"`
|
63 |
+
}
|
64 |
+
|
65 |
+
type AuthLogin interface {
|
66 |
+
GetAuthorizedUrl(csrfToken string) (string, int, error)
|
67 |
+
GetState(authorizedUrl string) (string, int, error)
|
68 |
+
CheckUsername(state string, username string) (int, error)
|
69 |
+
CheckPassword(state string, username string, password string) (string, int, error)
|
70 |
+
GetAccessToken(code string) (string, int, error)
|
71 |
+
GetAccessTokenFromHeader(c *gin.Context) (string, int, error)
|
72 |
+
}
|
73 |
+
|
74 |
+
func init() {
|
75 |
+
Client, _ = tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{
|
76 |
+
tls_client.WithCookieJar(tls_client.NewCookieJar()),
|
77 |
+
tls_client.WithTimeoutSeconds(defaultTimeoutSeconds),
|
78 |
+
tls_client.WithClientProfile(profiles.Okhttp4Android13),
|
79 |
+
}...)
|
80 |
+
ArkoseClient = getHttpClient()
|
81 |
+
|
82 |
+
setupPUID()
|
83 |
+
}
|
84 |
+
|
85 |
+
func NewHttpClient() tls_client.HttpClient {
|
86 |
+
client := getHttpClient()
|
87 |
+
|
88 |
+
ProxyUrl = os.Getenv("PROXY")
|
89 |
+
if ProxyUrl != "" {
|
90 |
+
client.SetProxy(ProxyUrl)
|
91 |
+
}
|
92 |
+
|
93 |
+
return client
|
94 |
+
}
|
95 |
+
|
96 |
+
func getHttpClient() tls_client.HttpClient {
|
97 |
+
client, _ := tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{
|
98 |
+
tls_client.WithCookieJar(tls_client.NewCookieJar()),
|
99 |
+
tls_client.WithClientProfile(profiles.Okhttp4Android13),
|
100 |
+
}...)
|
101 |
+
return client
|
102 |
+
}
|
103 |
+
|
104 |
+
func Proxy(c *gin.Context) {
|
105 |
+
url := c.Request.URL.Path
|
106 |
+
if strings.Contains(url, ChatGPTApiPrefix) {
|
107 |
+
url = strings.ReplaceAll(url, ChatGPTApiPrefix, ChatGPTApiUrlPrefix)
|
108 |
+
} else if strings.Contains(url, ImitateApiPrefix) {
|
109 |
+
url = strings.ReplaceAll(url, ImitateApiPrefix, ChatGPTApiUrlPrefix+"/backend-api")
|
110 |
+
} else {
|
111 |
+
url = strings.ReplaceAll(url, PlatformApiPrefix, PlatformApiUrlPrefix)
|
112 |
+
}
|
113 |
+
|
114 |
+
method := c.Request.Method
|
115 |
+
queryParams := c.Request.URL.Query().Encode()
|
116 |
+
if queryParams != "" {
|
117 |
+
url += "?" + queryParams
|
118 |
+
}
|
119 |
+
|
120 |
+
// if not set, will return 404
|
121 |
+
c.Status(http.StatusOK)
|
122 |
+
|
123 |
+
var req *http.Request
|
124 |
+
if method == http.MethodGet {
|
125 |
+
req, _ = http.NewRequest(http.MethodGet, url, nil)
|
126 |
+
} else {
|
127 |
+
body, _ := io.ReadAll(c.Request.Body)
|
128 |
+
req, _ = http.NewRequest(method, url, bytes.NewReader(body))
|
129 |
+
}
|
130 |
+
req.Header.Set("User-Agent", UserAgent)
|
131 |
+
req.Header.Set(AuthorizationHeader, GetAccessToken(c))
|
132 |
+
resp, err := Client.Do(req)
|
133 |
+
if err != nil {
|
134 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, ReturnMessage(err.Error()))
|
135 |
+
return
|
136 |
+
}
|
137 |
+
|
138 |
+
defer resp.Body.Close()
|
139 |
+
if resp.StatusCode != http.StatusOK {
|
140 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
141 |
+
logger.Error(fmt.Sprintf(AccountDeactivatedErrorMessage, c.GetString(EmailKey)))
|
142 |
+
}
|
143 |
+
|
144 |
+
responseMap := make(map[string]interface{})
|
145 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
146 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
147 |
+
return
|
148 |
+
}
|
149 |
+
|
150 |
+
io.Copy(c.Writer, resp.Body)
|
151 |
+
}
|
152 |
+
|
153 |
+
func ReturnMessage(msg string) gin.H {
|
154 |
+
logger.Warn(msg)
|
155 |
+
|
156 |
+
return gin.H{
|
157 |
+
defaultErrorMessageKey: msg,
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
func GetAccessToken(c *gin.Context) string {
|
162 |
+
accessToken := c.GetString(AuthorizationHeader)
|
163 |
+
if !strings.HasPrefix(accessToken, "Bearer") {
|
164 |
+
return "Bearer " + accessToken
|
165 |
+
}
|
166 |
+
|
167 |
+
return accessToken
|
168 |
+
}
|
169 |
+
|
170 |
+
func GetArkoseToken() (string, error) {
|
171 |
+
return funcaptcha.GetOpenAIToken(PUID, ProxyUrl)
|
172 |
+
}
|
173 |
+
|
174 |
+
func setupPUID() {
|
175 |
+
username := os.Getenv("OPENAI_EMAIL")
|
176 |
+
password := os.Getenv("OPENAI_PASSWORD")
|
177 |
+
if username != "" && password != "" {
|
178 |
+
go func() {
|
179 |
+
for {
|
180 |
+
authenticator := auth.NewAuthenticator(username, password, ProxyUrl)
|
181 |
+
if err := authenticator.Begin(); err != nil {
|
182 |
+
logger.Warn(fmt.Sprintf("%s: %s", refreshPuidErrorMessage, err.Details))
|
183 |
+
return
|
184 |
+
}
|
185 |
+
|
186 |
+
accessToken := authenticator.GetAccessToken()
|
187 |
+
if accessToken == "" {
|
188 |
+
logger.Error(refreshPuidErrorMessage)
|
189 |
+
return
|
190 |
+
}
|
191 |
+
|
192 |
+
puid, err := authenticator.GetPUID()
|
193 |
+
if err != nil {
|
194 |
+
logger.Error(refreshPuidErrorMessage)
|
195 |
+
return
|
196 |
+
}
|
197 |
+
|
198 |
+
PUID = puid
|
199 |
+
|
200 |
+
time.Sleep(time.Hour * 24 * 7)
|
201 |
+
}
|
202 |
+
}()
|
203 |
+
}
|
204 |
+
}
|
api/imitate/api.go
ADDED
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package imitate
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bufio"
|
5 |
+
"bytes"
|
6 |
+
"encoding/base64"
|
7 |
+
"encoding/json"
|
8 |
+
"fmt"
|
9 |
+
"io"
|
10 |
+
"os"
|
11 |
+
"regexp"
|
12 |
+
"strings"
|
13 |
+
|
14 |
+
http "github.com/bogdanfinn/fhttp"
|
15 |
+
"github.com/gin-gonic/gin"
|
16 |
+
"github.com/google/uuid"
|
17 |
+
|
18 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
19 |
+
"github.com/linweiyuan/go-chatgpt-api/api/chatgpt"
|
20 |
+
"github.com/linweiyuan/go-logger/logger"
|
21 |
+
)
|
22 |
+
|
23 |
+
var (
|
24 |
+
reg *regexp.Regexp
|
25 |
+
)
|
26 |
+
|
27 |
+
func init() {
|
28 |
+
reg, _ = regexp.Compile("[^a-zA-Z0-9]+")
|
29 |
+
}
|
30 |
+
|
31 |
+
func CreateChatCompletions(c *gin.Context) {
|
32 |
+
var originalRequest APIRequest
|
33 |
+
err := c.BindJSON(&originalRequest)
|
34 |
+
if err != nil {
|
35 |
+
c.JSON(400, gin.H{"error": gin.H{
|
36 |
+
"message": "Request must be proper JSON",
|
37 |
+
"type": "invalid_request_error",
|
38 |
+
"param": nil,
|
39 |
+
"code": err.Error(),
|
40 |
+
}})
|
41 |
+
return
|
42 |
+
}
|
43 |
+
|
44 |
+
authHeader := c.GetHeader(api.AuthorizationHeader)
|
45 |
+
token := os.Getenv("IMITATE_ACCESS_TOKEN")
|
46 |
+
if authHeader != "" {
|
47 |
+
customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1)
|
48 |
+
// Check if customAccessToken starts with sk-
|
49 |
+
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
|
50 |
+
token = customAccessToken
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
// 将聊天请求转换为ChatGPT请求。
|
55 |
+
translatedRequest, model := convertAPIRequest(originalRequest)
|
56 |
+
|
57 |
+
response, done := sendConversationRequest(c, translatedRequest, token)
|
58 |
+
if done {
|
59 |
+
return
|
60 |
+
}
|
61 |
+
|
62 |
+
defer func(Body io.ReadCloser) {
|
63 |
+
err := Body.Close()
|
64 |
+
if err != nil {
|
65 |
+
return
|
66 |
+
}
|
67 |
+
}(response.Body)
|
68 |
+
|
69 |
+
if HandleRequestError(c, response) {
|
70 |
+
return
|
71 |
+
}
|
72 |
+
|
73 |
+
var fullResponse string
|
74 |
+
|
75 |
+
id := generateId()
|
76 |
+
|
77 |
+
for i := 3; i > 0; i-- {
|
78 |
+
var continueInfo *ContinueInfo
|
79 |
+
var responsePart string
|
80 |
+
var continueSignal string
|
81 |
+
responsePart, continueInfo = Handler(c, response, originalRequest.Stream, id, model)
|
82 |
+
fullResponse += responsePart
|
83 |
+
continueSignal = os.Getenv("CONTINUE_SIGNAL")
|
84 |
+
if continueInfo == nil || continueSignal == "" {
|
85 |
+
break
|
86 |
+
}
|
87 |
+
println("Continuing conversation")
|
88 |
+
translatedRequest.Messages = nil
|
89 |
+
translatedRequest.Action = "continue"
|
90 |
+
translatedRequest.ConversationID = &continueInfo.ConversationID
|
91 |
+
translatedRequest.ParentMessageID = continueInfo.ParentID
|
92 |
+
response, done = sendConversationRequest(c, translatedRequest, token)
|
93 |
+
|
94 |
+
if done {
|
95 |
+
return
|
96 |
+
}
|
97 |
+
|
98 |
+
// 以下修复代码来自ChatGPT
|
99 |
+
// 在循环内部创建一个局部作用域,并将资源的引用传递给匿名函数,保证资源将在每次迭代结束时被正确释放
|
100 |
+
func() {
|
101 |
+
defer func(Body io.ReadCloser) {
|
102 |
+
err := Body.Close()
|
103 |
+
if err != nil {
|
104 |
+
return
|
105 |
+
}
|
106 |
+
}(response.Body)
|
107 |
+
}()
|
108 |
+
|
109 |
+
if HandleRequestError(c, response) {
|
110 |
+
return
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
if !originalRequest.Stream {
|
115 |
+
c.JSON(200, newChatCompletion(fullResponse, model, id))
|
116 |
+
} else {
|
117 |
+
c.String(200, "data: [DONE]\n\n")
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
func generateId() string {
|
122 |
+
id := uuid.NewString()
|
123 |
+
id = strings.ReplaceAll(id, "-", "")
|
124 |
+
id = base64.StdEncoding.EncodeToString([]byte(id))
|
125 |
+
id = reg.ReplaceAllString(id, "")
|
126 |
+
return "chatcmpl-" + id
|
127 |
+
}
|
128 |
+
|
129 |
+
func convertAPIRequest(apiRequest APIRequest) (chatgpt.CreateConversationRequest, string) {
|
130 |
+
chatgptRequest := NewChatGPTRequest()
|
131 |
+
|
132 |
+
var model = "gpt-3.5-turbo-0613"
|
133 |
+
|
134 |
+
if strings.HasPrefix(apiRequest.Model, "gpt-3.5") {
|
135 |
+
chatgptRequest.Model = "text-davinci-002-render-sha"
|
136 |
+
}
|
137 |
+
|
138 |
+
if strings.HasPrefix(apiRequest.Model, "gpt-4") {
|
139 |
+
arkoseToken, err := api.GetArkoseToken()
|
140 |
+
if err == nil {
|
141 |
+
chatgptRequest.ArkoseToken = arkoseToken
|
142 |
+
} else {
|
143 |
+
fmt.Println("Error getting Arkose token: ", err)
|
144 |
+
}
|
145 |
+
chatgptRequest.Model = apiRequest.Model
|
146 |
+
model = "gpt-4-0613"
|
147 |
+
}
|
148 |
+
|
149 |
+
if apiRequest.PluginIDs != nil {
|
150 |
+
chatgptRequest.PluginIDs = apiRequest.PluginIDs
|
151 |
+
chatgptRequest.Model = "gpt-4-plugins"
|
152 |
+
}
|
153 |
+
|
154 |
+
for _, apiMessage := range apiRequest.Messages {
|
155 |
+
if apiMessage.Role == "system" {
|
156 |
+
apiMessage.Role = "critic"
|
157 |
+
}
|
158 |
+
chatgptRequest.AddMessage(apiMessage.Role, apiMessage.Content)
|
159 |
+
}
|
160 |
+
|
161 |
+
return chatgptRequest, model
|
162 |
+
}
|
163 |
+
|
164 |
+
func NewChatGPTRequest() chatgpt.CreateConversationRequest {
|
165 |
+
enableHistory := os.Getenv("ENABLE_HISTORY") == ""
|
166 |
+
return chatgpt.CreateConversationRequest{
|
167 |
+
Action: "next",
|
168 |
+
ParentMessageID: uuid.NewString(),
|
169 |
+
Model: "text-davinci-002-render-sha",
|
170 |
+
HistoryAndTrainingDisabled: !enableHistory,
|
171 |
+
}
|
172 |
+
}
|
173 |
+
|
174 |
+
func sendConversationRequest(c *gin.Context, request chatgpt.CreateConversationRequest, accessToken string) (*http.Response, bool) {
|
175 |
+
jsonBytes, _ := json.Marshal(request)
|
176 |
+
req, _ := http.NewRequest(http.MethodPost, api.ChatGPTApiUrlPrefix+"/backend-api/conversation", bytes.NewBuffer(jsonBytes))
|
177 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
178 |
+
req.Header.Set(api.AuthorizationHeader, accessToken)
|
179 |
+
req.Header.Set("Accept", "text/event-stream")
|
180 |
+
if api.PUID != "" {
|
181 |
+
req.Header.Set("Cookie", "_puid="+api.PUID)
|
182 |
+
}
|
183 |
+
resp, err := api.Client.Do(req)
|
184 |
+
if err != nil {
|
185 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
186 |
+
return nil, true
|
187 |
+
}
|
188 |
+
|
189 |
+
if resp.StatusCode != http.StatusOK {
|
190 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
191 |
+
logger.Error(fmt.Sprintf(api.AccountDeactivatedErrorMessage, c.GetString(api.EmailKey)))
|
192 |
+
}
|
193 |
+
|
194 |
+
responseMap := make(map[string]interface{})
|
195 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
196 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
197 |
+
return nil, true
|
198 |
+
}
|
199 |
+
|
200 |
+
return resp, false
|
201 |
+
}
|
202 |
+
|
203 |
+
func Handler(c *gin.Context, response *http.Response, stream bool, id string, model string) (string, *ContinueInfo) {
|
204 |
+
maxTokens := false
|
205 |
+
|
206 |
+
// Create a bufio.Reader from the response body
|
207 |
+
reader := bufio.NewReader(response.Body)
|
208 |
+
|
209 |
+
// Read the response byte by byte until a newline character is encountered
|
210 |
+
if stream {
|
211 |
+
// Response content type is text/event-stream
|
212 |
+
c.Header("Content-Type", "text/event-stream")
|
213 |
+
} else {
|
214 |
+
// Response content type is application/json
|
215 |
+
c.Header("Content-Type", "application/json")
|
216 |
+
}
|
217 |
+
var finishReason string
|
218 |
+
var previousText StringStruct
|
219 |
+
var originalResponse ChatGPTResponse
|
220 |
+
var isRole = true
|
221 |
+
for {
|
222 |
+
line, err := reader.ReadString('\n')
|
223 |
+
if err != nil {
|
224 |
+
if err == io.EOF {
|
225 |
+
break
|
226 |
+
}
|
227 |
+
return "", nil
|
228 |
+
}
|
229 |
+
if len(line) < 6 {
|
230 |
+
continue
|
231 |
+
}
|
232 |
+
// Remove "data: " from the beginning of the line
|
233 |
+
line = line[6:]
|
234 |
+
// Check if line starts with [DONE]
|
235 |
+
if !strings.HasPrefix(line, "[DONE]") {
|
236 |
+
// Parse the line as JSON
|
237 |
+
|
238 |
+
err = json.Unmarshal([]byte(line), &originalResponse)
|
239 |
+
if err != nil {
|
240 |
+
continue
|
241 |
+
}
|
242 |
+
if originalResponse.Error != nil {
|
243 |
+
c.JSON(500, gin.H{"error": originalResponse.Error})
|
244 |
+
return "", nil
|
245 |
+
}
|
246 |
+
if originalResponse.Message.Author.Role != "assistant" || originalResponse.Message.Content.Parts == nil {
|
247 |
+
continue
|
248 |
+
}
|
249 |
+
if originalResponse.Message.Metadata.MessageType != "next" && originalResponse.Message.Metadata.MessageType != "continue" || originalResponse.Message.EndTurn != nil {
|
250 |
+
continue
|
251 |
+
}
|
252 |
+
if (len(originalResponse.Message.Content.Parts) == 0 || originalResponse.Message.Content.Parts[0] == "") && !isRole {
|
253 |
+
continue
|
254 |
+
}
|
255 |
+
responseString := ConvertToString(&originalResponse, &previousText, isRole, id, model)
|
256 |
+
isRole = false
|
257 |
+
if stream {
|
258 |
+
_, err = c.Writer.WriteString(responseString)
|
259 |
+
if err != nil {
|
260 |
+
return "", nil
|
261 |
+
}
|
262 |
+
}
|
263 |
+
// Flush the response writer buffer to ensure that the client receives each line as it's written
|
264 |
+
c.Writer.Flush()
|
265 |
+
|
266 |
+
if originalResponse.Message.Metadata.FinishDetails != nil {
|
267 |
+
if originalResponse.Message.Metadata.FinishDetails.Type == "max_tokens" {
|
268 |
+
maxTokens = true
|
269 |
+
}
|
270 |
+
finishReason = originalResponse.Message.Metadata.FinishDetails.Type
|
271 |
+
}
|
272 |
+
|
273 |
+
} else {
|
274 |
+
if stream {
|
275 |
+
if finishReason == "" {
|
276 |
+
finishReason = "stop"
|
277 |
+
}
|
278 |
+
finalLine := StopChunk(finishReason, id, model)
|
279 |
+
_, err := c.Writer.WriteString("data: " + finalLine.String() + "\n\n")
|
280 |
+
if err != nil {
|
281 |
+
return "", nil
|
282 |
+
}
|
283 |
+
}
|
284 |
+
}
|
285 |
+
}
|
286 |
+
if !maxTokens {
|
287 |
+
return previousText.Text, nil
|
288 |
+
}
|
289 |
+
return previousText.Text, &ContinueInfo{
|
290 |
+
ConversationID: originalResponse.ConversationID,
|
291 |
+
ParentID: originalResponse.Message.ID,
|
292 |
+
}
|
293 |
+
}
|
api/imitate/convert.go
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package imitate
|
2 |
+
|
3 |
+
import (
|
4 |
+
"fmt"
|
5 |
+
"strings"
|
6 |
+
)
|
7 |
+
|
8 |
+
func ConvertToString(chatgptResponse *ChatGPTResponse, previousText *StringStruct, role bool, id string, model string) string {
|
9 |
+
var text string
|
10 |
+
|
11 |
+
if len(chatgptResponse.Message.Content.Parts) == 1 {
|
12 |
+
if part, ok := chatgptResponse.Message.Content.Parts[0].(string); ok {
|
13 |
+
text = strings.ReplaceAll(part, previousText.Text, "")
|
14 |
+
previousText.Text = part
|
15 |
+
} else {
|
16 |
+
text = fmt.Sprintf("%v", chatgptResponse.Message.Content.Parts[0])
|
17 |
+
}
|
18 |
+
} else {
|
19 |
+
// When using GPT-4 messages with images (multimodal_text), the length of 'parts' might be 2.
|
20 |
+
// Since the chatgpt API currently does not support multimodal content
|
21 |
+
// and there is no official format for multimodal content,
|
22 |
+
// the content is temporarily returned as is.
|
23 |
+
var parts []string
|
24 |
+
for _, part := range chatgptResponse.Message.Content.Parts {
|
25 |
+
parts = append(parts, fmt.Sprintf("%v", part))
|
26 |
+
}
|
27 |
+
text = strings.Join(parts, ", ")
|
28 |
+
}
|
29 |
+
|
30 |
+
translatedResponse := NewChatCompletionChunk(text, id, model)
|
31 |
+
if role {
|
32 |
+
translatedResponse.Choices[0].Delta.Role = chatgptResponse.Message.Author.Role
|
33 |
+
}
|
34 |
+
|
35 |
+
return "data: " + translatedResponse.String() + "\n\n"
|
36 |
+
}
|
api/imitate/request.go
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package imitate
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"io"
|
6 |
+
|
7 |
+
http "github.com/bogdanfinn/fhttp"
|
8 |
+
"github.com/gin-gonic/gin"
|
9 |
+
)
|
10 |
+
|
11 |
+
type ContinueInfo struct {
|
12 |
+
ConversationID string `json:"conversation_id"`
|
13 |
+
ParentID string `json:"parent_id"`
|
14 |
+
}
|
15 |
+
|
16 |
+
type APIRequest struct {
|
17 |
+
Messages []ApiMessage `json:"messages"`
|
18 |
+
Stream bool `json:"stream"`
|
19 |
+
Model string `json:"model"`
|
20 |
+
PluginIDs []string `json:"plugin_ids"`
|
21 |
+
}
|
22 |
+
|
23 |
+
type ApiMessage struct {
|
24 |
+
Role string `json:"role"`
|
25 |
+
Content string `json:"content"`
|
26 |
+
}
|
27 |
+
|
28 |
+
func HandleRequestError(c *gin.Context, response *http.Response) bool {
|
29 |
+
if response.StatusCode != 200 {
|
30 |
+
// Try read response body as JSON
|
31 |
+
var errorResponse map[string]interface{}
|
32 |
+
err := json.NewDecoder(response.Body).Decode(&errorResponse)
|
33 |
+
if err != nil {
|
34 |
+
// Read response body
|
35 |
+
body, _ := io.ReadAll(response.Body)
|
36 |
+
c.JSON(500, gin.H{"error": gin.H{
|
37 |
+
"message": "Unknown error",
|
38 |
+
"type": "internal_server_error",
|
39 |
+
"param": nil,
|
40 |
+
"code": "500",
|
41 |
+
"details": string(body),
|
42 |
+
}})
|
43 |
+
return true
|
44 |
+
}
|
45 |
+
c.JSON(response.StatusCode, gin.H{"error": gin.H{
|
46 |
+
"message": errorResponse["detail"],
|
47 |
+
"type": response.Status,
|
48 |
+
"param": nil,
|
49 |
+
"code": "error",
|
50 |
+
}})
|
51 |
+
return true
|
52 |
+
}
|
53 |
+
return false
|
54 |
+
}
|
api/imitate/response.go
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package imitate
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"time"
|
6 |
+
|
7 |
+
"github.com/linweiyuan/go-chatgpt-api/api/chatgpt"
|
8 |
+
)
|
9 |
+
|
10 |
+
type ChatCompletionChunk struct {
|
11 |
+
ID string `json:"id"`
|
12 |
+
Object string `json:"object"`
|
13 |
+
Created int64 `json:"created"`
|
14 |
+
Model string `json:"model"`
|
15 |
+
Choices []Choices `json:"choices"`
|
16 |
+
}
|
17 |
+
|
18 |
+
func (chunk *ChatCompletionChunk) String() string {
|
19 |
+
resp, _ := json.Marshal(chunk)
|
20 |
+
return string(resp)
|
21 |
+
}
|
22 |
+
|
23 |
+
type Choices struct {
|
24 |
+
Delta Delta `json:"delta"`
|
25 |
+
Index int `json:"index"`
|
26 |
+
FinishReason interface{} `json:"finish_reason"`
|
27 |
+
}
|
28 |
+
|
29 |
+
type Delta struct {
|
30 |
+
Content string `json:"content,omitempty"`
|
31 |
+
Role string `json:"role,omitempty"`
|
32 |
+
}
|
33 |
+
|
34 |
+
func NewChatCompletionChunk(text string, id string, model string) ChatCompletionChunk {
|
35 |
+
return ChatCompletionChunk{
|
36 |
+
ID: id,
|
37 |
+
Object: "chat.completion.chunk",
|
38 |
+
Created: time.Now().Unix(),
|
39 |
+
Model: model,
|
40 |
+
Choices: []Choices{
|
41 |
+
{
|
42 |
+
Index: 0,
|
43 |
+
Delta: Delta{
|
44 |
+
Content: text,
|
45 |
+
},
|
46 |
+
FinishReason: nil,
|
47 |
+
},
|
48 |
+
},
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
func StopChunk(reason string, id string, model string) ChatCompletionChunk {
|
53 |
+
return ChatCompletionChunk{
|
54 |
+
ID: id,
|
55 |
+
Object: "chat.completion.chunk",
|
56 |
+
Created: time.Now().Unix(),
|
57 |
+
Model: model,
|
58 |
+
Choices: []Choices{
|
59 |
+
{
|
60 |
+
Index: 0,
|
61 |
+
FinishReason: reason,
|
62 |
+
},
|
63 |
+
},
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
type ChatCompletion struct {
|
68 |
+
ID string `json:"id"`
|
69 |
+
Object string `json:"object"`
|
70 |
+
Created int64 `json:"created"`
|
71 |
+
Model string `json:"model"`
|
72 |
+
Usage usage `json:"usage"`
|
73 |
+
Choices []Choice `json:"choices"`
|
74 |
+
}
|
75 |
+
type Msg struct {
|
76 |
+
Role string `json:"role"`
|
77 |
+
Content string `json:"content"`
|
78 |
+
}
|
79 |
+
type Choice struct {
|
80 |
+
Index int `json:"index"`
|
81 |
+
Message Msg `json:"message"`
|
82 |
+
FinishReason interface{} `json:"finish_reason"`
|
83 |
+
}
|
84 |
+
type usage struct {
|
85 |
+
PromptTokens int `json:"prompt_tokens"`
|
86 |
+
CompletionTokens int `json:"completion_tokens"`
|
87 |
+
TotalTokens int `json:"total_tokens"`
|
88 |
+
}
|
89 |
+
|
90 |
+
type ChatGPTResponse struct {
|
91 |
+
Message Message `json:"message"`
|
92 |
+
ConversationID string `json:"conversation_id"`
|
93 |
+
Error interface{} `json:"error"`
|
94 |
+
}
|
95 |
+
|
96 |
+
type Message struct {
|
97 |
+
ID string `json:"id"`
|
98 |
+
Author chatgpt.Author `json:"author"`
|
99 |
+
CreateTime float64 `json:"create_time"`
|
100 |
+
UpdateTime interface{} `json:"update_time"`
|
101 |
+
Content chatgpt.Content `json:"content"`
|
102 |
+
EndTurn interface{} `json:"end_turn"`
|
103 |
+
Weight float64 `json:"weight"`
|
104 |
+
Metadata Metadata `json:"metadata"`
|
105 |
+
Recipient string `json:"recipient"`
|
106 |
+
}
|
107 |
+
|
108 |
+
type Metadata struct {
|
109 |
+
Timestamp string `json:"timestamp_"`
|
110 |
+
MessageType string `json:"message_type"`
|
111 |
+
FinishDetails *FinishDetails `json:"finish_details"`
|
112 |
+
ModelSlug string `json:"model_slug"`
|
113 |
+
Recipient string `json:"recipient"`
|
114 |
+
}
|
115 |
+
|
116 |
+
type FinishDetails struct {
|
117 |
+
Type string `json:"type"`
|
118 |
+
Stop string `json:"stop"`
|
119 |
+
}
|
120 |
+
|
121 |
+
type StringStruct struct {
|
122 |
+
Text string `json:"text"`
|
123 |
+
}
|
124 |
+
|
125 |
+
func newChatCompletion(fullTest, model string, id string) ChatCompletion {
|
126 |
+
return ChatCompletion{
|
127 |
+
ID: id,
|
128 |
+
Object: "chat.completion",
|
129 |
+
Created: time.Now().Unix(),
|
130 |
+
Model: model,
|
131 |
+
Usage: usage{
|
132 |
+
PromptTokens: 0,
|
133 |
+
CompletionTokens: 0,
|
134 |
+
TotalTokens: 0,
|
135 |
+
},
|
136 |
+
Choices: []Choice{
|
137 |
+
{
|
138 |
+
Message: Msg{
|
139 |
+
Content: fullTest,
|
140 |
+
Role: "assistant",
|
141 |
+
},
|
142 |
+
Index: 0,
|
143 |
+
},
|
144 |
+
},
|
145 |
+
}
|
146 |
+
}
|
api/platform/access_token.go
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"errors"
|
6 |
+
"io"
|
7 |
+
"net/url"
|
8 |
+
"strings"
|
9 |
+
|
10 |
+
http "github.com/bogdanfinn/fhttp"
|
11 |
+
|
12 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
13 |
+
)
|
14 |
+
|
15 |
+
func (userLogin *UserLogin) GetAuthorizedUrl(csrfToken string) (string, int, error) {
|
16 |
+
urlParams := url.Values{
|
17 |
+
"client_id": {platformAuthClientID},
|
18 |
+
"audience": {platformAuthAudience},
|
19 |
+
"redirect_uri": {platformAuthRedirectURL},
|
20 |
+
"scope": {platformAuthScope},
|
21 |
+
"response_type": {platformAuthResponseType},
|
22 |
+
}
|
23 |
+
req, _ := http.NewRequest(http.MethodGet, platformAuth0Url+urlParams.Encode(), nil)
|
24 |
+
req.Header.Set("Content-Type", api.ContentType)
|
25 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
26 |
+
resp, err := userLogin.client.Do(req)
|
27 |
+
if err != nil {
|
28 |
+
return "", http.StatusInternalServerError, err
|
29 |
+
}
|
30 |
+
|
31 |
+
defer resp.Body.Close()
|
32 |
+
if resp.StatusCode != http.StatusOK {
|
33 |
+
return "", resp.StatusCode, errors.New(api.GetAuthorizedUrlErrorMessage)
|
34 |
+
}
|
35 |
+
|
36 |
+
return resp.Request.URL.String(), http.StatusOK, nil
|
37 |
+
}
|
38 |
+
|
39 |
+
func (userLogin *UserLogin) GetState(authorizedUrl string) (string, int, error) {
|
40 |
+
split := strings.Split(authorizedUrl, "=")
|
41 |
+
return split[1], http.StatusOK, nil
|
42 |
+
}
|
43 |
+
|
44 |
+
func (userLogin *UserLogin) CheckUsername(state string, username string) (int, error) {
|
45 |
+
formParams := url.Values{
|
46 |
+
"state": {state},
|
47 |
+
"username": {username},
|
48 |
+
"js-available": {"true"},
|
49 |
+
"webauthn-available": {"true"},
|
50 |
+
"is-brave": {"false"},
|
51 |
+
"webauthn-platform-available": {"false"},
|
52 |
+
"action": {"default"},
|
53 |
+
}
|
54 |
+
req, _ := http.NewRequest(http.MethodPost, api.LoginUsernameUrl+state, strings.NewReader(formParams.Encode()))
|
55 |
+
req.Header.Set("Content-Type", api.ContentType)
|
56 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
57 |
+
resp, err := userLogin.client.Do(req)
|
58 |
+
if err != nil {
|
59 |
+
return http.StatusInternalServerError, err
|
60 |
+
}
|
61 |
+
|
62 |
+
defer resp.Body.Close()
|
63 |
+
if resp.StatusCode != http.StatusOK {
|
64 |
+
return resp.StatusCode, errors.New(api.EmailInvalidErrorMessage)
|
65 |
+
}
|
66 |
+
|
67 |
+
return http.StatusOK, nil
|
68 |
+
}
|
69 |
+
|
70 |
+
func (userLogin *UserLogin) CheckPassword(state string, username string, password string) (string, int, error) {
|
71 |
+
formParams := url.Values{
|
72 |
+
"state": {state},
|
73 |
+
"username": {username},
|
74 |
+
"password": {password},
|
75 |
+
"action": {"default"},
|
76 |
+
}
|
77 |
+
req, _ := http.NewRequest(http.MethodPost, api.LoginPasswordUrl+state, strings.NewReader(formParams.Encode()))
|
78 |
+
req.Header.Set("Content-Type", api.ContentType)
|
79 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
80 |
+
resp, err := userLogin.client.Do(req)
|
81 |
+
if err != nil {
|
82 |
+
return "", http.StatusInternalServerError, err
|
83 |
+
}
|
84 |
+
|
85 |
+
defer resp.Body.Close()
|
86 |
+
if resp.StatusCode != http.StatusOK {
|
87 |
+
return "", resp.StatusCode, errors.New(api.EmailOrPasswordInvalidErrorMessage)
|
88 |
+
}
|
89 |
+
|
90 |
+
return resp.Request.URL.Query().Get("code"), http.StatusOK, nil
|
91 |
+
}
|
92 |
+
|
93 |
+
func (userLogin *UserLogin) GetAccessToken(code string) (string, int, error) {
|
94 |
+
jsonBytes, _ := json.Marshal(GetAccessTokenRequest{
|
95 |
+
ClientID: platformAuthClientID,
|
96 |
+
Code: code,
|
97 |
+
GrantType: platformAuthGrantType,
|
98 |
+
RedirectURI: platformAuthRedirectURL,
|
99 |
+
})
|
100 |
+
req, _ := http.NewRequest(http.MethodPost, getTokenUrl, strings.NewReader(string(jsonBytes)))
|
101 |
+
req.Header.Set("Content-Type", "application/json")
|
102 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
103 |
+
resp, err := userLogin.client.Do(req)
|
104 |
+
if err != nil {
|
105 |
+
return "", http.StatusInternalServerError, err
|
106 |
+
}
|
107 |
+
|
108 |
+
defer resp.Body.Close()
|
109 |
+
if resp.StatusCode != http.StatusOK {
|
110 |
+
return "", resp.StatusCode, errors.New(api.GetAccessTokenErrorMessage)
|
111 |
+
}
|
112 |
+
|
113 |
+
data, _ := io.ReadAll(resp.Body)
|
114 |
+
return string(data), http.StatusOK, nil
|
115 |
+
}
|
api/platform/api.go
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bufio"
|
5 |
+
"bytes"
|
6 |
+
"encoding/json"
|
7 |
+
"fmt"
|
8 |
+
"io"
|
9 |
+
"strings"
|
10 |
+
|
11 |
+
http "github.com/bogdanfinn/fhttp"
|
12 |
+
"github.com/gin-gonic/gin"
|
13 |
+
|
14 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
15 |
+
"github.com/linweiyuan/go-logger/logger"
|
16 |
+
)
|
17 |
+
|
18 |
+
func CreateChatCompletions(c *gin.Context) {
|
19 |
+
body, _ := io.ReadAll(c.Request.Body)
|
20 |
+
var request struct {
|
21 |
+
Stream bool `json:"stream"`
|
22 |
+
}
|
23 |
+
json.Unmarshal(body, &request)
|
24 |
+
|
25 |
+
url := c.Request.URL.Path
|
26 |
+
if strings.Contains(url, "/chat") {
|
27 |
+
url = apiCreateChatCompletions
|
28 |
+
} else {
|
29 |
+
url = apiCreateCompletions
|
30 |
+
}
|
31 |
+
|
32 |
+
resp, err := handlePost(c, url, body, request.Stream)
|
33 |
+
if err != nil {
|
34 |
+
return
|
35 |
+
}
|
36 |
+
|
37 |
+
defer resp.Body.Close()
|
38 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
39 |
+
logger.Error(fmt.Sprintf(api.AccountDeactivatedErrorMessage, c.GetString(api.EmailKey)))
|
40 |
+
responseMap := make(map[string]interface{})
|
41 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
42 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
43 |
+
return
|
44 |
+
}
|
45 |
+
|
46 |
+
if request.Stream {
|
47 |
+
handleCompletionsResponse(c, resp)
|
48 |
+
} else {
|
49 |
+
io.Copy(c.Writer, resp.Body)
|
50 |
+
}
|
51 |
+
}
|
52 |
+
|
53 |
+
func CreateCompletions(c *gin.Context) {
|
54 |
+
CreateChatCompletions(c)
|
55 |
+
}
|
56 |
+
|
57 |
+
func handleCompletionsResponse(c *gin.Context, resp *http.Response) {
|
58 |
+
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
59 |
+
|
60 |
+
reader := bufio.NewReader(resp.Body)
|
61 |
+
for {
|
62 |
+
if c.Request.Context().Err() != nil {
|
63 |
+
break
|
64 |
+
}
|
65 |
+
|
66 |
+
line, err := reader.ReadString('\n')
|
67 |
+
if err != nil {
|
68 |
+
break
|
69 |
+
}
|
70 |
+
|
71 |
+
line = strings.TrimSpace(line)
|
72 |
+
if strings.HasPrefix(line, "event") ||
|
73 |
+
strings.HasPrefix(line, "data: 20") ||
|
74 |
+
line == "" {
|
75 |
+
continue
|
76 |
+
}
|
77 |
+
|
78 |
+
c.Writer.Write([]byte(line + "\n\n"))
|
79 |
+
c.Writer.Flush()
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
func handlePost(c *gin.Context, url string, data []byte, stream bool) (*http.Response, error) {
|
84 |
+
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
|
85 |
+
req.Header.Set(api.AuthorizationHeader, api.GetAccessToken(c))
|
86 |
+
if stream {
|
87 |
+
req.Header.Set("Accept", "text/event-stream")
|
88 |
+
}
|
89 |
+
req.Header.Set("Content-Type", "application/json")
|
90 |
+
resp, err := api.Client.Do(req)
|
91 |
+
if err != nil {
|
92 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
93 |
+
return nil, err
|
94 |
+
}
|
95 |
+
|
96 |
+
return resp, nil
|
97 |
+
}
|
api/platform/constant.go
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import "github.com/linweiyuan/go-chatgpt-api/api"
|
4 |
+
|
5 |
+
const (
|
6 |
+
apiCreateChatCompletions = api.PlatformApiUrlPrefix + "/v1/chat/completions"
|
7 |
+
apiCreateCompletions = api.PlatformApiUrlPrefix + "/v1/completions"
|
8 |
+
|
9 |
+
platformAuthClientID = "DRivsnm2Mu42T3KOpqdtwB3NYviHYzwD"
|
10 |
+
platformAuthAudience = "https://api.openai.com/v1"
|
11 |
+
platformAuthRedirectURL = "https://platform.openai.com/auth/callback"
|
12 |
+
platformAuthScope = "openid profile email offline_access"
|
13 |
+
platformAuthResponseType = "code"
|
14 |
+
platformAuthGrantType = "authorization_code"
|
15 |
+
platformAuth0Url = api.Auth0Url + "/authorize?"
|
16 |
+
getTokenUrl = api.Auth0Url + "/oauth/token"
|
17 |
+
auth0Client = "eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMjEuMCJ9" // '{"name":"auth0-spa-js","version":"1.21.0"}'
|
18 |
+
auth0LogoutUrl = api.Auth0Url + "/v2/logout?returnTo=https%3A%2F%2Fplatform.openai.com%2Floggedout&client_id=" + platformAuthClientID + "&auth0Client=" + auth0Client
|
19 |
+
dashboardLoginUrl = "https://api.openai.com/dashboard/onboarding/login"
|
20 |
+
getSessionKeyErrorMessage = "failed to get session key"
|
21 |
+
)
|
api/platform/login.go
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"io"
|
6 |
+
"strings"
|
7 |
+
|
8 |
+
http "github.com/bogdanfinn/fhttp"
|
9 |
+
"github.com/gin-gonic/gin"
|
10 |
+
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
)
|
13 |
+
|
14 |
+
func Login(c *gin.Context) {
|
15 |
+
var loginInfo api.LoginInfo
|
16 |
+
if err := c.ShouldBindJSON(&loginInfo); err != nil {
|
17 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage))
|
18 |
+
return
|
19 |
+
}
|
20 |
+
|
21 |
+
userLogin := UserLogin{
|
22 |
+
client: api.NewHttpClient(),
|
23 |
+
}
|
24 |
+
|
25 |
+
// hard refresh cookies
|
26 |
+
resp, _ := userLogin.client.Get(auth0LogoutUrl)
|
27 |
+
defer resp.Body.Close()
|
28 |
+
|
29 |
+
// get authorized url
|
30 |
+
authorizedUrl, statusCode, err := userLogin.GetAuthorizedUrl("")
|
31 |
+
if err != nil {
|
32 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
33 |
+
return
|
34 |
+
}
|
35 |
+
|
36 |
+
// get state
|
37 |
+
state, _, _ := userLogin.GetState(authorizedUrl)
|
38 |
+
|
39 |
+
// check username
|
40 |
+
statusCode, err = userLogin.CheckUsername(state, loginInfo.Username)
|
41 |
+
if err != nil {
|
42 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
43 |
+
return
|
44 |
+
}
|
45 |
+
|
46 |
+
// check password
|
47 |
+
code, statusCode, err := userLogin.CheckPassword(state, loginInfo.Username, loginInfo.Password)
|
48 |
+
if err != nil {
|
49 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
50 |
+
return
|
51 |
+
}
|
52 |
+
|
53 |
+
// get access token
|
54 |
+
accessToken, statusCode, err := userLogin.GetAccessToken(code)
|
55 |
+
if err != nil {
|
56 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
57 |
+
return
|
58 |
+
}
|
59 |
+
|
60 |
+
// get session key
|
61 |
+
var getAccessTokenResponse GetAccessTokenResponse
|
62 |
+
json.Unmarshal([]byte(accessToken), &getAccessTokenResponse)
|
63 |
+
req, _ := http.NewRequest(http.MethodPost, dashboardLoginUrl, strings.NewReader("{}"))
|
64 |
+
req.Header.Set("Content-Type", "application/json")
|
65 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
66 |
+
req.Header.Set(api.AuthorizationHeader, "Bearer "+getAccessTokenResponse.AccessToken)
|
67 |
+
resp, err = userLogin.client.Do(req)
|
68 |
+
if err != nil {
|
69 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
70 |
+
return
|
71 |
+
}
|
72 |
+
|
73 |
+
defer resp.Body.Close()
|
74 |
+
if resp.StatusCode != http.StatusOK {
|
75 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(getSessionKeyErrorMessage))
|
76 |
+
return
|
77 |
+
}
|
78 |
+
|
79 |
+
io.Copy(c.Writer, resp.Body)
|
80 |
+
}
|
api/platform/typings.go
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import tls_client "github.com/bogdanfinn/tls-client"
|
4 |
+
|
5 |
+
type UserLogin struct {
|
6 |
+
client tls_client.HttpClient
|
7 |
+
}
|
8 |
+
|
9 |
+
type GetAccessTokenRequest struct {
|
10 |
+
ClientID string `json:"client_id"`
|
11 |
+
GrantType string `json:"grant_type"`
|
12 |
+
Code string `json:"code"`
|
13 |
+
RedirectURI string `json:"redirect_uri"`
|
14 |
+
}
|
15 |
+
|
16 |
+
type GetAccessTokenResponse struct {
|
17 |
+
AccessToken string `json:"access_token"`
|
18 |
+
RefreshToken string `json:"refresh_token"`
|
19 |
+
IDToken string `json:"id_token"`
|
20 |
+
Scope string `json:"scope"`
|
21 |
+
ExpiresIn int `json:"expires_in"`
|
22 |
+
TokenType string `json:"token_type"`
|
23 |
+
}
|
compose.yaml
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
go-chatgpt-api:
|
3 |
+
build: .
|
4 |
+
container_name: go-chatgpt-api
|
5 |
+
image: linweiyuan/go-chatgpt-api
|
6 |
+
ports:
|
7 |
+
- 8080:8080
|
8 |
+
environment:
|
9 |
+
- PORT=
|
10 |
+
- TZ=Asia/Shanghai
|
11 |
+
- PROXY=
|
12 |
+
- OPENAI_EMAIL=
|
13 |
+
- OPENAI_PASSWORD=
|
14 |
+
- CONTINUE_SIGNAL=
|
15 |
+
- ENABLE_HISTORY=
|
16 |
+
- IMITATE_ACCESS_TOKEN=
|
17 |
+
volumes:
|
18 |
+
- ./chat.openai.com.har:/app/chat.openai.com.har
|
19 |
+
restart: unless-stopped
|
env/env.go
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package env
|
2 |
+
|
3 |
+
import (
|
4 |
+
"github.com/joho/godotenv"
|
5 |
+
)
|
6 |
+
|
7 |
+
func init() {
|
8 |
+
godotenv.Load()
|
9 |
+
}
|
example/chatgpt.http
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### login
|
2 |
+
POST {{baseUrl}}/chatgpt/login
|
3 |
+
Content-Type: application/json
|
4 |
+
|
5 |
+
{
|
6 |
+
"username": "{{username}}",
|
7 |
+
"password": "{{password}}"
|
8 |
+
}
|
9 |
+
|
10 |
+
### get conversations
|
11 |
+
GET {{baseUrl}}/chatgpt/backend-api/conversations?offset=0&limit=3&order=updated
|
12 |
+
Authorization: Bearer {{accessToken}}
|
13 |
+
|
14 |
+
### get conversation
|
15 |
+
GET {{baseUrl}}/chatgpt/backend-api/conversation/id
|
16 |
+
Authorization: Bearer {{accessToken}}
|
17 |
+
|
18 |
+
### create conversation
|
19 |
+
POST {{baseUrl}}/chatgpt/backend-api/conversation
|
20 |
+
Authorization: Bearer {{accessToken}}
|
21 |
+
Content-Type: application/json
|
22 |
+
Accept: text/event-stream
|
23 |
+
|
24 |
+
{
|
25 |
+
"action": "next",
|
26 |
+
"messages": [
|
27 |
+
{
|
28 |
+
"id": "{{$guid}}",
|
29 |
+
"author": {
|
30 |
+
"role": "user"
|
31 |
+
},
|
32 |
+
"content": {
|
33 |
+
"content_type": "text",
|
34 |
+
"parts": [
|
35 |
+
"hello"
|
36 |
+
]
|
37 |
+
},
|
38 |
+
"metadata": {}
|
39 |
+
}
|
40 |
+
],
|
41 |
+
"model": "gpt-4",
|
42 |
+
"timezone_offset_min": -480,
|
43 |
+
"history_and_training_disabled": false
|
44 |
+
}
|
45 |
+
|
46 |
+
### get models
|
47 |
+
GET {{baseUrl}}/chatgpt/backend-api/models?history_and_training_disabled=false
|
48 |
+
Authorization: Bearer {{accessToken}}
|
49 |
+
|
50 |
+
### check account
|
51 |
+
GET {{baseUrl}}/chatgpt/backend-api/accounts/check
|
52 |
+
Authorization: Bearer {{accessToken}}
|
53 |
+
|
54 |
+
### check account v4
|
55 |
+
GET {{baseUrl}}/chatgpt/backend-api/accounts/check/v4-2023-04-27
|
56 |
+
Authorization: Bearer {{accessToken}}
|
57 |
+
|
58 |
+
### get settings beta features
|
59 |
+
GET {{baseUrl}}/chatgpt/backend-api/settings/beta_features
|
60 |
+
Authorization: Bearer {{accessToken}}
|
61 |
+
|
62 |
+
### get conversation limit (no need to pass access token)
|
63 |
+
GET {{baseUrl}}/chatgpt/public-api/conversation_limit
|
64 |
+
Authorization: Bearer {{accessToken}}
|
65 |
+
|
66 |
+
### get models with pandora enabled
|
67 |
+
GET {{baseUrl}}/api/models?history_and_training_disabled=false
|
68 |
+
Authorization: Bearer {{accessToken}}
|
69 |
+
|
70 |
+
### share link to chat
|
71 |
+
POST {{baseUrl}}/chatgpt/backend-api/share/create
|
72 |
+
Authorization: Bearer {{accessToken}}
|
73 |
+
Content-Type: application/json
|
74 |
+
|
75 |
+
{
|
76 |
+
"current_node_id": "9020711b-3dcf-4705-82ac-46b5af30fc7b",
|
77 |
+
"conversation_id": "74c406dd-a2e8-477a-b420-90ed57a55bf9",
|
78 |
+
"is_anonymous": false
|
79 |
+
}
|
80 |
+
|
81 |
+
### copy link
|
82 |
+
PATCH {{baseUrl}}/chatgpt/backend-api/share/{share_id}
|
83 |
+
Authorization: Bearer {{accessToken}}
|
84 |
+
Content-Type: application/json
|
85 |
+
|
86 |
+
{
|
87 |
+
"share_id": "49cd2432-d084-4ab7-8549-4ee18046812b",
|
88 |
+
"highlighted_message_id": null,
|
89 |
+
"title": "Summarize Request and Response 11122",
|
90 |
+
"is_public": false,
|
91 |
+
"is_visible": false,
|
92 |
+
"is_anonymous": true
|
93 |
+
}
|
94 |
+
|
95 |
+
### continue shared conversation
|
96 |
+
POST {{baseUrl}}/chatgpt/backend-api/conversation
|
97 |
+
Authorization: Bearer {{accessToken}}
|
98 |
+
Content-Type: application/json
|
99 |
+
|
100 |
+
{
|
101 |
+
"action": "next",
|
102 |
+
"messages": [{
|
103 |
+
"id": "{{$guid}}",
|
104 |
+
"author": {
|
105 |
+
"role": "user"
|
106 |
+
},
|
107 |
+
"content": {
|
108 |
+
"content_type": "text",
|
109 |
+
"parts": [
|
110 |
+
"hello again"
|
111 |
+
]
|
112 |
+
},
|
113 |
+
"metadata": {}
|
114 |
+
}],
|
115 |
+
"continue_from_shared_conversation_id": "this is the share_id",
|
116 |
+
"parent_message_id": "this is the current_node_id",
|
117 |
+
"model": "text-davinci-002-render-sha",
|
118 |
+
"timezone_offset_min": -480,
|
119 |
+
"history_and_training_disabled": false,
|
120 |
+
"arkose_token": null
|
121 |
+
}
|
122 |
+
|
123 |
+
### get plugins
|
124 |
+
GET {{baseUrl}}/chatgpt/backend-api/aip/p?offset=0&limit=250&statuses=approved
|
125 |
+
Authorization: Bearer {{accessToken}}
|
126 |
+
|
127 |
+
### get payment url
|
128 |
+
GET {{baseUrl}}/chatgpt/backend-api/payments/customer_portal
|
129 |
+
Authorization: Bearer {{accessToken}}
|
example/imitate.http
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### Create chat completion
|
2 |
+
POST {{baseUrl}}/imitate/v1/chat/completions
|
3 |
+
Content-Type: application/json
|
4 |
+
Authorization: Bearer {{accessToken}}
|
5 |
+
|
6 |
+
{
|
7 |
+
"model": "gpt-3.5-turbo",
|
8 |
+
"messages": [
|
9 |
+
{
|
10 |
+
"role": "system",
|
11 |
+
"content": "You are a helpful assistant."
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"role": "user",
|
15 |
+
"content": "Hello!"
|
16 |
+
}
|
17 |
+
],
|
18 |
+
"stream": true
|
19 |
+
}
|
example/ios.http
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### create conversation
|
2 |
+
POST https://ios.chat.openai.com/backend-api/conversation
|
3 |
+
Authorization: Bearer {{accessToken}}
|
4 |
+
Cookie: _devicecheck=user-xxx
|
5 |
+
Accept: text/event-stream
|
6 |
+
Content-Type: application/json
|
7 |
+
|
8 |
+
{
|
9 |
+
"action": "next",
|
10 |
+
"messages": [
|
11 |
+
{
|
12 |
+
"id": "{{$guid}}",
|
13 |
+
"author": {
|
14 |
+
"role": "user"
|
15 |
+
},
|
16 |
+
"content": {
|
17 |
+
"content_type": "text",
|
18 |
+
"parts": [
|
19 |
+
"hello"
|
20 |
+
]
|
21 |
+
},
|
22 |
+
"metadata": {}
|
23 |
+
}
|
24 |
+
],
|
25 |
+
"model": "gpt-4",
|
26 |
+
"timezone_offset_min": -480,
|
27 |
+
"history_and_training_disabled": false,
|
28 |
+
"supports_modapi": true
|
29 |
+
}
|
30 |
+
|
31 |
+
### get conversations
|
32 |
+
GET https://ios.chat.openai.com/backend-api/conversations?offset=0&limit=3&order=updated
|
33 |
+
Authorization: Bearer {{accessToken}}
|
34 |
+
Cookie: _devicecheck=user-xxx
|
35 |
+
|
36 |
+
### device check
|
37 |
+
POST https://ios.chat.openai.com/backend-api/devicecheck
|
38 |
+
Authorization: Bearer {{accessToken}}
|
39 |
+
Cookie: _preauth_devicecheck=xxx
|
40 |
+
Content-Type: application/json
|
41 |
+
|
42 |
+
{
|
43 |
+
"bundle_id": "com.openai.chat",
|
44 |
+
"device_token": "ad"
|
45 |
+
}
|
46 |
+
|
47 |
+
### me
|
48 |
+
GET https://ios.chat.openai.com/backend-api/me?include_groups=true
|
49 |
+
Authorization: Bearer {{accessToken}}
|
50 |
+
Cookie: _devicecheck=user-xxx
|
example/platform.http
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### login
|
2 |
+
POST {{baseUrl}}/platform/login
|
3 |
+
Content-Type: application/json
|
4 |
+
|
5 |
+
{
|
6 |
+
"username": "{{username}}",
|
7 |
+
"password": "{{password}}"
|
8 |
+
}
|
9 |
+
|
10 |
+
### get models
|
11 |
+
GET {{baseUrl}}/platform/v1/models
|
12 |
+
Authorization: Bearer {{apiKey}}
|
13 |
+
|
14 |
+
### get model
|
15 |
+
GET {{baseUrl}}/platform/v1/models/gpt-3.5-turbo-16k-0613
|
16 |
+
Authorization: Bearer {{apiKey}}
|
17 |
+
|
18 |
+
### Create chat completion
|
19 |
+
POST {{baseUrl}}/platform/v1/chat/completions
|
20 |
+
Content-Type: application/json
|
21 |
+
Authorization: Bearer {{apiKey}}
|
22 |
+
|
23 |
+
{
|
24 |
+
"model": "gpt-3.5-turbo",
|
25 |
+
"messages": [
|
26 |
+
{
|
27 |
+
"role": "system",
|
28 |
+
"content": "You are a helpful assistant."
|
29 |
+
},
|
30 |
+
{
|
31 |
+
"role": "user",
|
32 |
+
"content": "Hello!"
|
33 |
+
}
|
34 |
+
],
|
35 |
+
"stream": true
|
36 |
+
}
|
37 |
+
|
38 |
+
### Create completion
|
39 |
+
POST {{baseUrl}}/platform/v1/completions
|
40 |
+
Content-Type: application/json
|
41 |
+
Authorization: Bearer {{apiKey}}
|
42 |
+
|
43 |
+
{
|
44 |
+
"model": "text-davinci-003",
|
45 |
+
"prompt": "Say this is a test",
|
46 |
+
"max_tokens": 7,
|
47 |
+
"temperature": 0,
|
48 |
+
"stream": true
|
49 |
+
}
|
50 |
+
|
51 |
+
### get user api_keys
|
52 |
+
GET {{baseUrl}}/platform/dashboard/user/api_keys
|
53 |
+
Authorization: Bearer {{apiKey}}
|
54 |
+
|
55 |
+
### get billing credit_grants
|
56 |
+
GET {{baseUrl}}/platform/dashboard/billing/credit_grants
|
57 |
+
Authorization: Bearer {{apiKey}}
|
58 |
+
|
59 |
+
### get billing subscription
|
60 |
+
GET {{baseUrl}}/platform/dashboard/billing/subscription
|
61 |
+
Authorization: Bearer {{apiKey}}
|
62 |
+
|
63 |
+
### get billing usage
|
64 |
+
GET {{baseUrl}}/platform/dashboard/billing/usage?end_date=2023-07-01&start_date=2023-06-01
|
65 |
+
Authorization: Bearer {{apiKey}}
|
go.mod
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module github.com/linweiyuan/go-chatgpt-api
|
2 |
+
|
3 |
+
go 1.21
|
4 |
+
|
5 |
+
require (
|
6 |
+
github.com/PuerkitoBio/goquery v1.8.1
|
7 |
+
github.com/bogdanfinn/fhttp v0.5.24
|
8 |
+
github.com/bogdanfinn/tls-client v1.6.1
|
9 |
+
github.com/gin-gonic/gin v1.9.1
|
10 |
+
github.com/google/uuid v1.3.1
|
11 |
+
github.com/joho/godotenv v1.5.1
|
12 |
+
github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c
|
13 |
+
github.com/xqdoo00o/OpenAIAuth v0.0.0-20230928031215-356afd0d7a6b
|
14 |
+
github.com/xqdoo00o/funcaptcha v0.0.0-20230928030317-87dbaf7079cf
|
15 |
+
)
|
16 |
+
|
17 |
+
require (
|
18 |
+
github.com/andybalholm/brotli v1.0.5 // indirect
|
19 |
+
github.com/andybalholm/cascadia v1.3.2 // indirect
|
20 |
+
github.com/bogdanfinn/utls v1.5.16 // indirect
|
21 |
+
github.com/bytedance/sonic v1.9.1 // indirect
|
22 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
23 |
+
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
24 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
25 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
26 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
27 |
+
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
28 |
+
github.com/goccy/go-json v0.10.2 // indirect
|
29 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
30 |
+
github.com/klauspost/compress v1.17.0 // indirect
|
31 |
+
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
32 |
+
github.com/leodido/go-urn v1.2.4 // indirect
|
33 |
+
github.com/mattn/go-isatty v0.0.19 // indirect
|
34 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
35 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
36 |
+
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
37 |
+
github.com/sirupsen/logrus v1.9.3 // indirect
|
38 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
|
39 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
40 |
+
github.com/ugorji/go/codec v1.2.11 // indirect
|
41 |
+
golang.org/x/arch v0.3.0 // indirect
|
42 |
+
golang.org/x/crypto v0.13.0 // indirect
|
43 |
+
golang.org/x/net v0.15.0 // indirect
|
44 |
+
golang.org/x/sys v0.12.0 // indirect
|
45 |
+
golang.org/x/text v0.13.0 // indirect
|
46 |
+
google.golang.org/protobuf v1.30.0 // indirect
|
47 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
48 |
+
)
|
go.sum
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
2 |
+
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
3 |
+
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
4 |
+
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
5 |
+
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
6 |
+
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
7 |
+
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
8 |
+
github.com/bogdanfinn/fhttp v0.5.24 h1:OlyBKjvJp6a3TotN3wuj4mQHHRbfK7QUMrzCPOZGhRc=
|
9 |
+
github.com/bogdanfinn/fhttp v0.5.24/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I=
|
10 |
+
github.com/bogdanfinn/tls-client v1.6.1 h1:GTIqQssFoIvLaDf4btoYRzDhUzudLqYD4axvfUCXl3I=
|
11 |
+
github.com/bogdanfinn/tls-client v1.6.1/go.mod h1:FtwQ3DndVZ0xAOO704v4iNAgbHOcEc5kPk9tjICTNQ0=
|
12 |
+
github.com/bogdanfinn/utls v1.5.16 h1:NhhWkegEcYETBMj9nvgO4lwvc6NcLH+znrXzO3gnw4M=
|
13 |
+
github.com/bogdanfinn/utls v1.5.16/go.mod h1:mHeRCi69cUiEyVBkKONB1cAbLjRcZnlJbGzttmiuK4o=
|
14 |
+
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
15 |
+
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
16 |
+
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
17 |
+
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
18 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
19 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
20 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
21 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
22 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
23 |
+
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
24 |
+
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
25 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
26 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
27 |
+
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
28 |
+
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
29 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
30 |
+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
31 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
32 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
33 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
34 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
35 |
+
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
36 |
+
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
37 |
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
38 |
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
39 |
+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
40 |
+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
41 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
42 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
43 |
+
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
44 |
+
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
45 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
46 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
47 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
48 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
49 |
+
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
50 |
+
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
51 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
52 |
+
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
53 |
+
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
54 |
+
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
55 |
+
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
56 |
+
github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c h1:KJqkWkepk+PGSCC3i+ANrLnthM/x9qyfG4uCTGg2B8E=
|
57 |
+
github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c/go.mod h1:U37nbW0kovzaMnrX5sBmxOwidA7P4ymM3/K64oaTQJc=
|
58 |
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
59 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
60 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
61 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
62 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
63 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
64 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
65 |
+
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
66 |
+
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
67 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
68 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
69 |
+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
70 |
+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
71 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
72 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
73 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
74 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
75 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
76 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
77 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
78 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
79 |
+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
80 |
+
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
81 |
+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
82 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
|
83 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
|
84 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
85 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
86 |
+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
87 |
+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
88 |
+
github.com/xqdoo00o/OpenAIAuth v0.0.0-20230928031215-356afd0d7a6b h1:xQmc9iMdowpMKC1zctG7+sVAJjsn57f6JoDAPORAOKo=
|
89 |
+
github.com/xqdoo00o/OpenAIAuth v0.0.0-20230928031215-356afd0d7a6b/go.mod h1:DxzLlILrE7hGJbg5SSjG22kvd3ThXADknESpo0riCRI=
|
90 |
+
github.com/xqdoo00o/funcaptcha v0.0.0-20230928030317-87dbaf7079cf h1:hUf7xpllFrVRiGHfiSIp2m+wxdrGMFJgDuwivA+P1B8=
|
91 |
+
github.com/xqdoo00o/funcaptcha v0.0.0-20230928030317-87dbaf7079cf/go.mod h1:2frDREz1MeGS6sXyNSQGKJGd/0yCCydf5khEqyi4krI=
|
92 |
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
93 |
+
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
94 |
+
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
95 |
+
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
96 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
97 |
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
98 |
+
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
99 |
+
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
100 |
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
101 |
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
102 |
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
103 |
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
104 |
+
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
105 |
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
106 |
+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
107 |
+
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
108 |
+
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
109 |
+
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
110 |
+
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
111 |
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
112 |
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
113 |
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
114 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
115 |
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
116 |
+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
117 |
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
118 |
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
119 |
+
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
120 |
+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
121 |
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
122 |
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
123 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
124 |
+
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
125 |
+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
126 |
+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
127 |
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
128 |
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
129 |
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
130 |
+
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
131 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
132 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
133 |
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
134 |
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
135 |
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
136 |
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
137 |
+
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
138 |
+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
139 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
140 |
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
141 |
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
142 |
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
143 |
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
144 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
145 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
146 |
+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
147 |
+
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
148 |
+
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
149 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
150 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
151 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
152 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
153 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
154 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
main.go
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package main
|
2 |
+
|
3 |
+
import (
|
4 |
+
"log"
|
5 |
+
"os"
|
6 |
+
"strings"
|
7 |
+
|
8 |
+
http "github.com/bogdanfinn/fhttp"
|
9 |
+
"github.com/gin-gonic/gin"
|
10 |
+
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
"github.com/linweiyuan/go-chatgpt-api/api/chatgpt"
|
13 |
+
"github.com/linweiyuan/go-chatgpt-api/api/imitate"
|
14 |
+
"github.com/linweiyuan/go-chatgpt-api/api/platform"
|
15 |
+
_ "github.com/linweiyuan/go-chatgpt-api/env"
|
16 |
+
"github.com/linweiyuan/go-chatgpt-api/middleware"
|
17 |
+
)
|
18 |
+
|
19 |
+
func init() {
|
20 |
+
gin.ForceConsoleColor()
|
21 |
+
gin.SetMode(gin.ReleaseMode)
|
22 |
+
}
|
23 |
+
|
24 |
+
func main() {
|
25 |
+
router := gin.Default()
|
26 |
+
|
27 |
+
router.Use(middleware.CORS())
|
28 |
+
router.Use(middleware.Authorization())
|
29 |
+
|
30 |
+
setupChatGPTAPIs(router)
|
31 |
+
setupPlatformAPIs(router)
|
32 |
+
setupPandoraAPIs(router)
|
33 |
+
setupImitateAPIs(router)
|
34 |
+
router.NoRoute(api.Proxy)
|
35 |
+
|
36 |
+
router.GET("/", func(c *gin.Context) {
|
37 |
+
c.String(http.StatusOK, api.ReadyHint)
|
38 |
+
})
|
39 |
+
|
40 |
+
port := os.Getenv("PORT")
|
41 |
+
if port == "" {
|
42 |
+
port = "8080"
|
43 |
+
}
|
44 |
+
err := router.Run(":" + port)
|
45 |
+
if err != nil {
|
46 |
+
log.Fatal("failed to start server: " + err.Error())
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
func setupChatGPTAPIs(router *gin.Engine) {
|
51 |
+
chatgptGroup := router.Group("/chatgpt")
|
52 |
+
{
|
53 |
+
chatgptGroup.POST("/login", chatgpt.Login)
|
54 |
+
chatgptGroup.POST("/backend-api/login", chatgpt.Login) // add support for other projects
|
55 |
+
|
56 |
+
conversationGroup := chatgptGroup.Group("/backend-api/conversation")
|
57 |
+
{
|
58 |
+
conversationGroup.POST("", chatgpt.CreateConversation)
|
59 |
+
}
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
func setupPlatformAPIs(router *gin.Engine) {
|
64 |
+
platformGroup := router.Group("/platform")
|
65 |
+
{
|
66 |
+
platformGroup.POST("/login", platform.Login)
|
67 |
+
platformGroup.POST("/v1/login", platform.Login)
|
68 |
+
|
69 |
+
apiGroup := platformGroup.Group("/v1")
|
70 |
+
{
|
71 |
+
apiGroup.POST("/chat/completions", platform.CreateChatCompletions)
|
72 |
+
apiGroup.POST("/completions", platform.CreateCompletions)
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
func setupPandoraAPIs(router *gin.Engine) {
|
78 |
+
router.Any("/api/*path", func(c *gin.Context) {
|
79 |
+
c.Request.URL.Path = strings.ReplaceAll(c.Request.URL.Path, "/api", "/chatgpt/backend-api")
|
80 |
+
router.HandleContext(c)
|
81 |
+
})
|
82 |
+
}
|
83 |
+
|
84 |
+
func setupImitateAPIs(router *gin.Engine) {
|
85 |
+
imitateGroup := router.Group("/imitate")
|
86 |
+
{
|
87 |
+
imitateGroup.POST("/login", chatgpt.Login)
|
88 |
+
|
89 |
+
apiGroup := imitateGroup.Group("/v1")
|
90 |
+
{
|
91 |
+
apiGroup.POST("/chat/completions", imitate.CreateChatCompletions)
|
92 |
+
}
|
93 |
+
}
|
94 |
+
}
|
middleware/authorization.go
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package middleware
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/base64"
|
5 |
+
"encoding/json"
|
6 |
+
"fmt"
|
7 |
+
"net/http"
|
8 |
+
"os"
|
9 |
+
"strings"
|
10 |
+
"time"
|
11 |
+
|
12 |
+
"github.com/gin-gonic/gin"
|
13 |
+
|
14 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
15 |
+
)
|
16 |
+
|
17 |
+
const (
|
18 |
+
emptyAccessTokenErrorMessage = "please provide a valid access token or api key in 'Authorization' header"
|
19 |
+
accessTokenHasExpiredErrorMessage = "the accessToken for account %s has expired"
|
20 |
+
)
|
21 |
+
|
22 |
+
type AccessToken struct {
|
23 |
+
HTTPSAPIOpenaiComProfile struct {
|
24 |
+
Email string `json:"email"`
|
25 |
+
EmailVerified bool `json:"email_verified"`
|
26 |
+
} `json:"https://api.openai.com/profile"`
|
27 |
+
HTTPSAPIOpenaiComAuth struct {
|
28 |
+
UserID string `json:"user_id"`
|
29 |
+
} `json:"https://api.openai.com/auth"`
|
30 |
+
Iss string `json:"iss"`
|
31 |
+
Sub string `json:"sub"`
|
32 |
+
Aud []string `json:"aud"`
|
33 |
+
Iat int `json:"iat"`
|
34 |
+
Exp int `json:"exp"`
|
35 |
+
Azp string `json:"azp"`
|
36 |
+
Scope string `json:"scope"`
|
37 |
+
}
|
38 |
+
|
39 |
+
func Authorization() gin.HandlerFunc {
|
40 |
+
return func(c *gin.Context) {
|
41 |
+
authorization := c.GetHeader(api.AuthorizationHeader)
|
42 |
+
if authorization == "" {
|
43 |
+
authorization = c.GetHeader(api.XAuthorizationHeader)
|
44 |
+
}
|
45 |
+
|
46 |
+
if authorization == "" {
|
47 |
+
if c.Request.URL.Path == "/" {
|
48 |
+
c.Header("Content-Type", "text/plain")
|
49 |
+
} else if strings.HasSuffix(c.Request.URL.Path, "/login") ||
|
50 |
+
strings.HasPrefix(c.Request.URL.Path, "/chatgpt/public-api") ||
|
51 |
+
(strings.HasPrefix(c.Request.URL.Path, "/imitate") && os.Getenv("IMITATE_ACCESS_TOKEN") != "") {
|
52 |
+
c.Header("Content-Type", "application/json")
|
53 |
+
} else if c.Request.URL.Path == "/favicon.ico" {
|
54 |
+
c.Abort()
|
55 |
+
return
|
56 |
+
} else {
|
57 |
+
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ReturnMessage(emptyAccessTokenErrorMessage))
|
58 |
+
return
|
59 |
+
}
|
60 |
+
|
61 |
+
c.Next()
|
62 |
+
} else {
|
63 |
+
if expired := isExpired(c); expired {
|
64 |
+
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ReturnMessage(fmt.Sprintf(accessTokenHasExpiredErrorMessage, c.GetString(api.EmailKey))))
|
65 |
+
return
|
66 |
+
}
|
67 |
+
|
68 |
+
c.Set(api.AuthorizationHeader, authorization)
|
69 |
+
}
|
70 |
+
}
|
71 |
+
}
|
72 |
+
|
73 |
+
func isExpired(c *gin.Context) bool {
|
74 |
+
accessToken := c.GetHeader(api.AuthorizationHeader)
|
75 |
+
split := strings.Split(accessToken, ".")
|
76 |
+
if len(split) == 3 {
|
77 |
+
rawDecodedText, _ := base64.RawStdEncoding.DecodeString(split[1])
|
78 |
+
var accessToken AccessToken
|
79 |
+
json.Unmarshal(rawDecodedText, &accessToken)
|
80 |
+
|
81 |
+
c.Set(api.EmailKey, accessToken.HTTPSAPIOpenaiComProfile.Email)
|
82 |
+
|
83 |
+
exp := int64(accessToken.Exp)
|
84 |
+
expTime := time.Unix(exp, 0)
|
85 |
+
now := time.Now()
|
86 |
+
|
87 |
+
return now.After(expTime)
|
88 |
+
}
|
89 |
+
|
90 |
+
// apiKey
|
91 |
+
return false
|
92 |
+
}
|
middleware/cors.go
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package middleware
|
2 |
+
|
3 |
+
import (
|
4 |
+
http "github.com/bogdanfinn/fhttp"
|
5 |
+
"github.com/gin-gonic/gin"
|
6 |
+
)
|
7 |
+
|
8 |
+
func CORS() gin.HandlerFunc {
|
9 |
+
return func(c *gin.Context) {
|
10 |
+
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
11 |
+
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
|
12 |
+
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
|
13 |
+
|
14 |
+
if c.Request.Method == http.MethodOptions {
|
15 |
+
c.AbortWithStatus(http.StatusNoContent)
|
16 |
+
return
|
17 |
+
}
|
18 |
+
|
19 |
+
c.Next()
|
20 |
+
}
|
21 |
+
}
|
render.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
- type: web
|
3 |
+
name: go-chatgpt-api
|
4 |
+
runtime: go
|
5 |
+
plan: free
|
6 |
+
buildCommand: go build -ldflags="-w -s" -o go-chatgpt-api main.go
|
7 |
+
startCommand: ./go-chatgpt-api
|