Spaces:
Runtime error
Runtime error
Upload 25 files
Browse files- Dockerfile +28 -0
- LICENSE +21 -0
- README.md +442 -4
- README_en.md +436 -0
- api/chatgpt/access_token.go +176 -0
- api/chatgpt/api.go +300 -0
- api/chatgpt/constant.go +26 -0
- api/chatgpt/health_check.go +75 -0
- api/chatgpt/typings.go +54 -0
- api/common.go +110 -0
- api/platform/access_token.go +116 -0
- api/platform/api.go +224 -0
- api/platform/constant.go +35 -0
- api/platform/typings.go +93 -0
- compose.yaml +11 -0
- env/env.go +10 -0
- example/chatgpt.http +54 -0
- example/http-client.env.json +8 -0
- example/platform.http +8 -0
- go.mod +44 -0
- go.sum +136 -0
- main.go +119 -0
- middleware/check_header.go +22 -0
- middleware/cors.go +22 -0
- util/logger/logger.go +37 -0
Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM linweiyuan/chatgpt-proxy-server-warp
|
2 |
+
|
3 |
+
ENV SUDO_USER_NAME dvc890
|
4 |
+
ENV MIRROR_URL 'https://mirrors.bfsu.edu.cn/archlinux/$repo/os/$arch'
|
5 |
+
|
6 |
+
WORKDIR /app
|
7 |
+
|
8 |
+
RUN pacman -Sy --needed --noconfirm go
|
9 |
+
ENV PATH="/usr/local/go/bin:${PATH}"
|
10 |
+
|
11 |
+
COPY . .
|
12 |
+
RUN go build -ldflags="-w -s" -o go-chatgpt-api main.go
|
13 |
+
|
14 |
+
COPY --from=builder /app/go-chatgpt-api .
|
15 |
+
RUN apk add --no-cache tzdata
|
16 |
+
ENV TZ=Asia/Shanghai
|
17 |
+
EXPOSE 8080
|
18 |
+
EXPOSE 9515
|
19 |
+
EXPOSE 40000
|
20 |
+
EXPOSE 65535
|
21 |
+
|
22 |
+
ENV CHATGPT_PROXY_SERVER=http://localhost:9515
|
23 |
+
ENV GO_CHATGPT_API_PROXY=socks5://0.0.0.0:65535
|
24 |
+
ENV LOG_LEVEL=OFF
|
25 |
+
|
26 |
+
RUN mkdir -p /var/lib/cloudflare-warp
|
27 |
+
|
28 |
+
CMD ["bash", "-c", "/bin/bash /run.sh & sleep 3 && exec /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
@@ -1,10 +1,448 @@
|
|
1 |
---
|
2 |
title: Go Chatgpt Api
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
---
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
title: Go Chatgpt Api
|
3 |
+
emoji: 💩
|
4 |
+
colorFrom: green
|
5 |
+
colorTo: indigo
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
+
app_port: 8080
|
9 |
+
|
10 |
+
# go-chatgpt-api
|
11 |
+
|
12 |
+
### [English Doc](README_en.md)
|
13 |
+
|
14 |
+
## 一个尝试绕过 `Cloudflare 403` 和 `Access denied` 的正向代理程序
|
15 |
+
|
16 |
+
### 实验性质项目,不保证稳定性和向后兼容,使用风险自负
|
17 |
+
|
18 |
+
---
|
19 |
+
|
20 |
+
### 使用的过程中遇到问题应该如何解决
|
21 |
+
|
22 |
+
汇总贴:https://github.com/linweiyuan/go-chatgpt-api/issues/74
|
23 |
+
|
24 |
+
---
|
25 |
+
|
26 |
+
### 支持的 API(URL 和参数基本保持着和官网一致,部分接口有些许改动)
|
27 |
+
|
28 |
+
---
|
29 |
+
|
30 |
+
### ChatGPT APIs
|
31 |
+
|
32 |
+
---
|
33 |
+
|
34 |
+
- `ChatGPT` 登录(返回 `accessToken`)(目前仅支持 `ChatGPT` 账号,谷歌或微软账号没有测试)
|
35 |
+
|
36 |
+
`POST /chatgpt/login`
|
37 |
+
|
38 |
+
<details>
|
39 |
+
|
40 |
+
```json
|
41 |
+
{
|
42 |
+
"username": "email",
|
43 |
+
"password": "password"
|
44 |
+
}
|
45 |
+
```
|
46 |
+
|
47 |
+
</details>
|
48 |
+
|
49 |
+
---
|
50 |
+
|
51 |
+
- 获取对话列表(历史记录)
|
52 |
+
|
53 |
+
`GET /chatgpt/conversations?offset=0&limit=20`
|
54 |
+
|
55 |
+
`offset` 不传默认为 0, `limit` 不传默认为 20 (最大为 100)
|
56 |
+
|
57 |
+
---
|
58 |
+
|
59 |
+
- 获取对话内容
|
60 |
+
|
61 |
+
`GET /chatgpt/conversation/{conversationID}`
|
62 |
+
|
63 |
+
---
|
64 |
+
|
65 |
+
- 新建对话
|
66 |
+
|
67 |
+
`POST /chatgpt/conversation`
|
68 |
+
|
69 |
+
<details>
|
70 |
+
|
71 |
+
```json
|
72 |
+
{
|
73 |
+
"action": "next",
|
74 |
+
"messages": [
|
75 |
+
{
|
76 |
+
"id": "message id",
|
77 |
+
"author": {
|
78 |
+
"role": "user"
|
79 |
+
},
|
80 |
+
"content": {
|
81 |
+
"content_type": "text",
|
82 |
+
"parts": [
|
83 |
+
"Hello World"
|
84 |
+
]
|
85 |
+
}
|
86 |
+
}
|
87 |
+
],
|
88 |
+
"parent_message_id": "parent message id",
|
89 |
+
"conversation_id": "conversation id",
|
90 |
+
"model": "text-davinci-002-render-sha",
|
91 |
+
"timezone_offset_min": -480,
|
92 |
+
"history_and_training_disabled": false
|
93 |
+
}
|
94 |
+
```
|
95 |
+
|
96 |
+
</details>
|
97 |
+
|
98 |
+
---
|
99 |
+
|
100 |
+
- 生成对话标题
|
101 |
+
|
102 |
+
`POST /chatgpt/conversation/gen_title/{conversationID}`
|
103 |
+
|
104 |
+
<details>
|
105 |
+
|
106 |
+
```json
|
107 |
+
{
|
108 |
+
"message_id": "role assistant response message id"
|
109 |
+
}
|
110 |
+
```
|
111 |
+
|
112 |
+
</details>
|
113 |
+
|
114 |
---
|
115 |
|
116 |
+
- 重命名对话标题
|
117 |
+
|
118 |
+
`PATCH /chatgpt/conversation/{conversationID}`
|
119 |
+
|
120 |
+
<details>
|
121 |
+
|
122 |
+
```json
|
123 |
+
{
|
124 |
+
"title": "new title"
|
125 |
+
}
|
126 |
+
```
|
127 |
+
|
128 |
+
</details>
|
129 |
+
|
130 |
+
---
|
131 |
+
|
132 |
+
- 删除单个对话
|
133 |
+
|
134 |
+
`PATCH /chatgpt/conversation/{conversationID}`
|
135 |
+
|
136 |
+
<details>
|
137 |
+
|
138 |
+
```json
|
139 |
+
{
|
140 |
+
"is_visible": false
|
141 |
+
}
|
142 |
+
```
|
143 |
+
|
144 |
+
</details>
|
145 |
+
|
146 |
+
---
|
147 |
+
|
148 |
+
- 删除所有对话
|
149 |
+
|
150 |
+
`PATCH /chatgpt/conversations`
|
151 |
+
|
152 |
+
<details>
|
153 |
+
|
154 |
+
```json
|
155 |
+
{
|
156 |
+
"is_visible": false
|
157 |
+
}
|
158 |
+
```
|
159 |
+
|
160 |
+
</details>
|
161 |
+
|
162 |
+
---
|
163 |
+
|
164 |
+
- 消息反馈
|
165 |
+
|
166 |
+
`POST /chatgpt/conversation/message_feedback`
|
167 |
+
|
168 |
+
<details>
|
169 |
+
|
170 |
+
```json
|
171 |
+
{
|
172 |
+
"message_id": "message id",
|
173 |
+
"conversation_id": "conversation id",
|
174 |
+
"rating": "thumbsUp/thumbsDown"
|
175 |
+
}
|
176 |
+
```
|
177 |
+
|
178 |
+
</details>
|
179 |
+
|
180 |
+
---
|
181 |
+
|
182 |
+
### Platform APIs
|
183 |
+
|
184 |
+
---
|
185 |
+
|
186 |
+
- `platform` 登录(返回 `sessionKey`)
|
187 |
+
|
188 |
+
`POST /platform/login`
|
189 |
+
|
190 |
+
<details>
|
191 |
+
|
192 |
+
```json
|
193 |
+
{
|
194 |
+
"username": "email",
|
195 |
+
"password": "password"
|
196 |
+
}
|
197 |
+
```
|
198 |
+
|
199 |
+
</details>
|
200 |
+
|
201 |
+
---
|
202 |
+
|
203 |
+
- [List models](https://platform.openai.com/docs/api-reference/models/list)
|
204 |
+
|
205 |
+
`GET /platform/v1/models`
|
206 |
+
|
207 |
+
---
|
208 |
+
|
209 |
+
- [Retrieve model](https://platform.openai.com/docs/api-reference/models/retrieve)
|
210 |
+
|
211 |
+
`GET /platform/v1/models/{model}`
|
212 |
+
|
213 |
+
---
|
214 |
+
|
215 |
+
- [Create completion](https://platform.openai.com/docs/api-reference/completions/create)
|
216 |
+
|
217 |
+
`POST /platform/v1/completions`
|
218 |
+
|
219 |
+
<details>
|
220 |
+
|
221 |
+
```json
|
222 |
+
{
|
223 |
+
"model": "text-davinci-003",
|
224 |
+
"prompt": "Say this is a test",
|
225 |
+
"max_tokens": 7,
|
226 |
+
"temperature": 0,
|
227 |
+
"stream": true
|
228 |
+
}
|
229 |
+
```
|
230 |
+
|
231 |
+
</details>
|
232 |
+
|
233 |
+
---
|
234 |
+
|
235 |
+
- [Create chat completion](https://platform.openai.com/docs/api-reference/chat/create)
|
236 |
+
|
237 |
+
`POST /platform/v1/chat/completions`
|
238 |
+
|
239 |
+
<details>
|
240 |
+
|
241 |
+
```json
|
242 |
+
{
|
243 |
+
"messages": [
|
244 |
+
{
|
245 |
+
"role": "user",
|
246 |
+
"content": "Hello World"
|
247 |
+
}
|
248 |
+
],
|
249 |
+
"model": "gpt-3.5-turbo",
|
250 |
+
"stream": true
|
251 |
+
}
|
252 |
+
```
|
253 |
+
|
254 |
+
</details>
|
255 |
+
|
256 |
+
---
|
257 |
+
|
258 |
+
- [Create edit](https://platform.openai.com/docs/api-reference/edits/create)
|
259 |
+
|
260 |
+
`POST /platform/v1/edits`
|
261 |
+
|
262 |
+
<details>
|
263 |
+
|
264 |
+
```json
|
265 |
+
{
|
266 |
+
"model": "text-davinci-edit-001",
|
267 |
+
"input": "What day of the wek is it?",
|
268 |
+
"instruction": "Fix the spelling mistakes"
|
269 |
+
}
|
270 |
+
```
|
271 |
+
|
272 |
+
</details>
|
273 |
+
|
274 |
+
---
|
275 |
+
|
276 |
+
- [Create image](https://platform.openai.com/docs/api-reference/images/create)
|
277 |
+
|
278 |
+
`POST /platform/v1/images/generations`
|
279 |
+
|
280 |
+
<details>
|
281 |
+
|
282 |
+
```json
|
283 |
+
{
|
284 |
+
"prompt": "A cute dog",
|
285 |
+
"n": 2,
|
286 |
+
"size": "1024x1024"
|
287 |
+
}
|
288 |
+
```
|
289 |
+
|
290 |
+
</details>
|
291 |
+
|
292 |
+
---
|
293 |
+
|
294 |
+
- [Create embeddings](https://platform.openai.com/docs/api-reference/embeddings/create)
|
295 |
+
|
296 |
+
`POST /platform/v1/embeddings`
|
297 |
+
|
298 |
+
<details>
|
299 |
+
|
300 |
+
```json
|
301 |
+
{
|
302 |
+
"model": "text-embedding-ada-002",
|
303 |
+
"input": "The food was delicious and the waiter..."
|
304 |
+
}
|
305 |
+
```
|
306 |
+
|
307 |
+
</details>
|
308 |
+
|
309 |
+
---
|
310 |
+
|
311 |
+
- [Create moderations](https://platform.openai.com/docs/api-reference/moderations/create)
|
312 |
+
|
313 |
+
`POST /platform/v1/moderations`
|
314 |
+
|
315 |
+
<details>
|
316 |
+
|
317 |
+
```json
|
318 |
+
{
|
319 |
+
"model": "text-moderation-stable",
|
320 |
+
"input": "I want to kill them."
|
321 |
+
}
|
322 |
+
```
|
323 |
+
|
324 |
+
</details>
|
325 |
+
|
326 |
+
---
|
327 |
+
|
328 |
+
- [List files](https://platform.openai.com/docs/api-reference/files/list)
|
329 |
+
|
330 |
+
`GET /platform/v1/files`
|
331 |
+
|
332 |
+
---
|
333 |
+
|
334 |
+
- 获取 `credit grants` (只能传 `sessionKey`)
|
335 |
+
|
336 |
+
`GET /platform/dashboard/billing/credit_grants`
|
337 |
+
|
338 |
+
---
|
339 |
+
|
340 |
+
- 获取 `subscription` (只能传 `sessionKey`)
|
341 |
+
|
342 |
+
`GET /platform/dashboard/billing/subscription`
|
343 |
+
|
344 |
+
---
|
345 |
+
|
346 |
+
- 获取 `api keys` (只能传 `sessionKey`)
|
347 |
+
|
348 |
+
`GET /platform/dashboard/user/api_keys`
|
349 |
+
|
350 |
+
---
|
351 |
+
|
352 |
+
如需设置代理,可以设置环境变量 `GO_CHATGPT_API_PROXY`,比如 `GO_CHATGPT_API_PROXY=http://127.0.0.1:20171`
|
353 |
+
或者 `GO_CHATGPT_API_PROXY=socks5://127.0.0.1:20170`,注释掉或者留空则不启用
|
354 |
+
|
355 |
+
如需配合 `warp` 使用:`GO_CHATGPT_API_PROXY=socks5://chatgpt-proxy-server-warp:65535`,因为需要设置 `warp`
|
356 |
+
的场景已经默认可以直接访问 `ChatGPT` 官网,因此共用一个变量不冲突
|
357 |
+
|
358 |
+
---
|
359 |
+
|
360 |
+
`docker-compose` 配置文件:
|
361 |
+
|
362 |
+
```yaml
|
363 |
+
services:
|
364 |
+
go-chatgpt-api:
|
365 |
+
container_name: go-chatgpt-api
|
366 |
+
image: linweiyuan/go-chatgpt-api
|
367 |
+
ports:
|
368 |
+
- 8080:8080
|
369 |
+
environment:
|
370 |
+
- GO_CHATGPT_API_PROXY=
|
371 |
+
restart: unless-stopped
|
372 |
+
```
|
373 |
+
|
374 |
+
我仅仅在 `Arch Linux` 上进行开发和测试,这是一个滚动更新的版本,意味着系统上所有东西都是最新的,如果你在使用的过程中 `yaml`
|
375 |
+
报错了,则可以加上 `version: '3'` 在 `services:` 前面
|
376 |
+
|
377 |
+
如果遇到 `Access denied`,但是你的服务器确实在[被支持的国家或地区](https://platform.openai.com/docs/supported-countries)
|
378 |
+
,尝试一下这个配置(不保证能解决问题,比如你的服务器在 A 地区,但 A 地不在支持列表内,即使用上了 `warp` 后是 `Cloudflare IP`
|
379 |
+
,结果也会是 `403`):
|
380 |
+
|
381 |
+
```yaml
|
382 |
+
services:
|
383 |
+
go-chatgpt-api:
|
384 |
+
container_name: go-chatgpt-api
|
385 |
+
image: linweiyuan/go-chatgpt-api
|
386 |
+
ports:
|
387 |
+
- 8080:8080
|
388 |
+
environment:
|
389 |
+
- GO_CHATGPT_API_PROXY=socks5://chatgpt-proxy-server-warp:65535
|
390 |
+
depends_on:
|
391 |
+
- chatgpt-proxy-server-warp
|
392 |
+
restart: unless-stopped
|
393 |
+
|
394 |
+
chatgpt-proxy-server-warp:
|
395 |
+
container_name: chatgpt-proxy-server-warp
|
396 |
+
image: linweiyuan/chatgpt-proxy-server-warp
|
397 |
+
environment:
|
398 |
+
- LOG_LEVEL=OFF
|
399 |
+
restart: unless-stopped
|
400 |
+
```
|
401 |
+
|
402 |
+
如果你知道什么是 `teams-enroll-token`,可以通过环境变量 `TEAMS_ENROLL_TOKEN` 设置它的值
|
403 |
+
|
404 |
+
然后利用这条命令来检查是否生效:
|
405 |
+
|
406 |
+
`docker-compose exec chatgpt-proxy-server-warp warp-cli --accept-tos account | awk 'NR==1'`
|
407 |
+
|
408 |
+
```
|
409 |
+
Account type: Free (没有生效)
|
410 |
+
|
411 |
+
Account type: Team (设置正常)
|
412 |
+
```
|
413 |
+
|
414 |
+
---
|
415 |
+
|
416 |
+
如果要让运行的镜像总是保持最新,可以配合这个一起使用:
|
417 |
+
|
418 |
+
```yaml
|
419 |
+
services:
|
420 |
+
watchtower:
|
421 |
+
container_name: watchtower
|
422 |
+
image: containrrr/watchtower
|
423 |
+
volumes:
|
424 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
425 |
+
command: --interval 3600
|
426 |
+
restart: unless-stopped
|
427 |
+
```
|
428 |
+
|
429 |
+
<details>
|
430 |
+
|
431 |
+
<summary>广告位</summary>
|
432 |
+
|
433 |
+
`Vultr` 推荐链接:https://www.vultr.com/?ref=7372562
|
434 |
+
|
435 |
+
---
|
436 |
+
|
437 |
+
个人微信(没有验证,谁都能加,添加即通过,不用打招呼,直接把问题发出来,日常和私人问题不聊,不进群;可以解答程序使用问题,但最好自己要有一定的基础;可以远程调试,仅限 `SSH`
|
438 |
+
或`ToDesk`,但不保证能解决):
|
439 |
+
|
440 |
+
![](https://linweiyuan.github.io/about/mmqrcode.png)
|
441 |
+
|
442 |
+
---
|
443 |
+
|
444 |
+
微信赞赏码(经济条件允许的可以考虑支持下):
|
445 |
+
|
446 |
+
![](https://linweiyuan.github.io/about/mm_reward_qrcode.png)
|
447 |
+
|
448 |
+
</details>
|
README_en.md
ADDED
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# go-chatgpt-api
|
2 |
+
|
3 |
+
### [中文文档](README.md)
|
4 |
+
|
5 |
+
## A forward proxy program attempting to bypass `Cloudflare 403` and `Access Denied`.
|
6 |
+
|
7 |
+
### Experimental project, with no guarantee of stability and backward compatibility, use at your own risk.
|
8 |
+
|
9 |
+
---
|
10 |
+
|
11 |
+
### Troubleshooting
|
12 |
+
|
13 |
+
English countries does not have the "Great Firewall", so many issues are gone.
|
14 |
+
|
15 |
+
More details: https://github.com/linweiyuan/go-chatgpt-api/issues/74
|
16 |
+
|
17 |
+
---
|
18 |
+
|
19 |
+
### Supported APIs (URL and parameters are mostly consistent with the official website, with slight modifications to some interfaces).
|
20 |
+
|
21 |
+
---
|
22 |
+
|
23 |
+
## ChatGPT APIs
|
24 |
+
|
25 |
+
---
|
26 |
+
|
27 |
+
- `ChatGPT` user login (`accessToken` will be returned) (currently `Google` or `Microsoft` accounts are not supported).
|
28 |
+
|
29 |
+
`POST /chatgpt/login`
|
30 |
+
|
31 |
+
<details>
|
32 |
+
|
33 |
+
```json
|
34 |
+
{
|
35 |
+
"username": "email",
|
36 |
+
"password": "password"
|
37 |
+
}
|
38 |
+
```
|
39 |
+
|
40 |
+
</details>
|
41 |
+
|
42 |
+
---
|
43 |
+
|
44 |
+
- get conversation list
|
45 |
+
|
46 |
+
`GET /chatgpt/conversations?offset=0&limit=20`
|
47 |
+
|
48 |
+
`offset` defaults to 0, `limit` defaults to 20 (max 100).
|
49 |
+
|
50 |
+
---
|
51 |
+
|
52 |
+
- get conversation content
|
53 |
+
|
54 |
+
`GET /chatgpt/conversation/{conversationID}`
|
55 |
+
|
56 |
+
---
|
57 |
+
|
58 |
+
- create conversation
|
59 |
+
|
60 |
+
`POST /chatgpt/conversation`
|
61 |
+
|
62 |
+
<details>
|
63 |
+
|
64 |
+
```json
|
65 |
+
{
|
66 |
+
"action": "next",
|
67 |
+
"messages": [
|
68 |
+
{
|
69 |
+
"id": "message id",
|
70 |
+
"author": {
|
71 |
+
"role": "user"
|
72 |
+
},
|
73 |
+
"content": {
|
74 |
+
"content_type": "text",
|
75 |
+
"parts": [
|
76 |
+
"Hello World"
|
77 |
+
]
|
78 |
+
}
|
79 |
+
}
|
80 |
+
],
|
81 |
+
"parent_message_id": "parent message id",
|
82 |
+
"conversation_id": "conversation id",
|
83 |
+
"model": "text-davinci-002-render-sha",
|
84 |
+
"timezone_offset_min": -480,
|
85 |
+
"history_and_training_disabled": false
|
86 |
+
}
|
87 |
+
```
|
88 |
+
|
89 |
+
</details>
|
90 |
+
|
91 |
+
---
|
92 |
+
|
93 |
+
- generate conversation title
|
94 |
+
|
95 |
+
`POST /chatgpt/conversation/gen_title/{conversationID}`
|
96 |
+
|
97 |
+
<details>
|
98 |
+
|
99 |
+
```json
|
100 |
+
{
|
101 |
+
"message_id": "role assistant response message id"
|
102 |
+
}
|
103 |
+
```
|
104 |
+
|
105 |
+
</details>
|
106 |
+
|
107 |
+
---
|
108 |
+
|
109 |
+
- rename conversation
|
110 |
+
|
111 |
+
`PATCH /chatgpt/conversation/{conversationID}`
|
112 |
+
|
113 |
+
<details>
|
114 |
+
|
115 |
+
```json
|
116 |
+
{
|
117 |
+
"title": "new title"
|
118 |
+
}
|
119 |
+
```
|
120 |
+
|
121 |
+
</details>
|
122 |
+
|
123 |
+
---
|
124 |
+
|
125 |
+
- delete conversation
|
126 |
+
|
127 |
+
`PATCH /chatgpt/conversation/{conversationID}`
|
128 |
+
|
129 |
+
<details>
|
130 |
+
|
131 |
+
```json
|
132 |
+
{
|
133 |
+
"is_visible": false
|
134 |
+
}
|
135 |
+
```
|
136 |
+
|
137 |
+
</details>
|
138 |
+
|
139 |
+
---
|
140 |
+
|
141 |
+
- delete all conversations
|
142 |
+
|
143 |
+
`PATCH /chatgpt/conversations`
|
144 |
+
|
145 |
+
<details>
|
146 |
+
|
147 |
+
```json
|
148 |
+
{
|
149 |
+
"is_visible": false
|
150 |
+
}
|
151 |
+
```
|
152 |
+
|
153 |
+
</details>
|
154 |
+
|
155 |
+
---
|
156 |
+
|
157 |
+
- feedback message
|
158 |
+
|
159 |
+
`POST /chatgpt/conversation/message_feedback`
|
160 |
+
|
161 |
+
<details>
|
162 |
+
|
163 |
+
```json
|
164 |
+
{
|
165 |
+
"message_id": "message id",
|
166 |
+
"conversation_id": "conversation id",
|
167 |
+
"rating": "thumbsUp/thumbsDown"
|
168 |
+
}
|
169 |
+
```
|
170 |
+
|
171 |
+
</details>
|
172 |
+
|
173 |
+
---
|
174 |
+
|
175 |
+
## Platform APIs
|
176 |
+
|
177 |
+
---
|
178 |
+
|
179 |
+
- `platform` user login (`sessionKey` will be returned)
|
180 |
+
|
181 |
+
`POST /platform/login`
|
182 |
+
|
183 |
+
<details>
|
184 |
+
|
185 |
+
```json
|
186 |
+
{
|
187 |
+
"username": "email",
|
188 |
+
"password": "password"
|
189 |
+
}
|
190 |
+
```
|
191 |
+
|
192 |
+
</details>
|
193 |
+
|
194 |
+
---
|
195 |
+
|
196 |
+
- [List models](https://platform.openai.com/docs/api-reference/models/list)
|
197 |
+
|
198 |
+
`GET /platform/v1/models`
|
199 |
+
|
200 |
+
---
|
201 |
+
|
202 |
+
- [Retrieve model](https://platform.openai.com/docs/api-reference/models/retrieve)
|
203 |
+
|
204 |
+
`GET /platform/v1/models/{model}`
|
205 |
+
|
206 |
+
---
|
207 |
+
|
208 |
+
- [Create completion](https://platform.openai.com/docs/api-reference/completions/create)
|
209 |
+
|
210 |
+
`POST /platform/v1/completions`
|
211 |
+
|
212 |
+
<details>
|
213 |
+
|
214 |
+
```json
|
215 |
+
{
|
216 |
+
"model": "text-davinci-003",
|
217 |
+
"prompt": "Say this is a test",
|
218 |
+
"max_tokens": 7,
|
219 |
+
"temperature": 0,
|
220 |
+
"stream": true
|
221 |
+
}
|
222 |
+
```
|
223 |
+
|
224 |
+
</details>
|
225 |
+
|
226 |
+
---
|
227 |
+
|
228 |
+
- [Create chat completion](https://platform.openai.com/docs/api-reference/chat/create)
|
229 |
+
|
230 |
+
`POST /platform/v1/chat/completions`
|
231 |
+
|
232 |
+
<details>
|
233 |
+
|
234 |
+
```json
|
235 |
+
{
|
236 |
+
"messages": [
|
237 |
+
{
|
238 |
+
"role": "user",
|
239 |
+
"content": "Hello World"
|
240 |
+
}
|
241 |
+
],
|
242 |
+
"model": "gpt-3.5-turbo",
|
243 |
+
"stream": true
|
244 |
+
}
|
245 |
+
```
|
246 |
+
|
247 |
+
</details>
|
248 |
+
|
249 |
+
---
|
250 |
+
|
251 |
+
- [Create edit](https://platform.openai.com/docs/api-reference/edits/create)
|
252 |
+
|
253 |
+
`POST /platform/v1/edits`
|
254 |
+
|
255 |
+
<details>
|
256 |
+
|
257 |
+
```json
|
258 |
+
{
|
259 |
+
"model": "text-davinci-edit-001",
|
260 |
+
"input": "What day of the wek is it?",
|
261 |
+
"instruction": "Fix the spelling mistakes"
|
262 |
+
}
|
263 |
+
```
|
264 |
+
|
265 |
+
</details>
|
266 |
+
|
267 |
+
---
|
268 |
+
|
269 |
+
- [Create image](https://platform.openai.com/docs/api-reference/images/create)
|
270 |
+
|
271 |
+
`POST /platform/v1/images/generations`
|
272 |
+
|
273 |
+
<details>
|
274 |
+
|
275 |
+
```json
|
276 |
+
{
|
277 |
+
"prompt": "A cute dog",
|
278 |
+
"n": 2,
|
279 |
+
"size": "1024x1024"
|
280 |
+
}
|
281 |
+
```
|
282 |
+
|
283 |
+
</details>
|
284 |
+
|
285 |
+
---
|
286 |
+
|
287 |
+
- [Create embeddings](https://platform.openai.com/docs/api-reference/embeddings/create)
|
288 |
+
|
289 |
+
`POST /platform/v1/embeddings`
|
290 |
+
|
291 |
+
<details>
|
292 |
+
|
293 |
+
```json
|
294 |
+
{
|
295 |
+
"model": "text-embedding-ada-002",
|
296 |
+
"input": "The food was delicious and the waiter..."
|
297 |
+
}
|
298 |
+
```
|
299 |
+
|
300 |
+
</details>
|
301 |
+
|
302 |
+
---
|
303 |
+
|
304 |
+
- [Create moderations](https://platform.openai.com/docs/api-reference/moderations/create)
|
305 |
+
|
306 |
+
`POST /platform/v1/moderations`
|
307 |
+
|
308 |
+
<details>
|
309 |
+
|
310 |
+
```json
|
311 |
+
{
|
312 |
+
"model": "text-moderation-stable",
|
313 |
+
"input": "I want to kill them."
|
314 |
+
}
|
315 |
+
```
|
316 |
+
|
317 |
+
</details>
|
318 |
+
|
319 |
+
---
|
320 |
+
|
321 |
+
- [List files](https://platform.openai.com/docs/api-reference/files/list)
|
322 |
+
|
323 |
+
`GET /platform/v1/files`
|
324 |
+
|
325 |
+
---
|
326 |
+
|
327 |
+
- get `credit grants` (only support `sessionkey`)
|
328 |
+
|
329 |
+
`GET /platform/dashboard/billing/credit_grants`
|
330 |
+
|
331 |
+
---
|
332 |
+
|
333 |
+
- get `subscription` (only support `sessionkey`)
|
334 |
+
|
335 |
+
`GET /platform/dashboard/billing/subscription`
|
336 |
+
|
337 |
+
---
|
338 |
+
|
339 |
+
- get `api keys` (only support `sessionkey`)
|
340 |
+
|
341 |
+
`GET /platform/dashboard/user/api_keys`
|
342 |
+
|
343 |
+
---
|
344 |
+
|
345 |
+
### Configuration
|
346 |
+
|
347 |
+
To set a proxy, you can use the environment variable `GO_CHATGPT_API_PROXY`, such
|
348 |
+
as `GO_CHATGPT_API_PROXY=http://127.0.0.1:20171` or `GO_CHATGPT_API_PROXY=socks5://127.0.0.1:20170`. If it is commented
|
349 |
+
out or left blank, it will not be enabled.
|
350 |
+
|
351 |
+
To use with `warp`: `GO_CHATGPT_API_PROXY=socks5://chatgpt-proxy-server-warp:65535`. Since the scenario that requires
|
352 |
+
setting up `warp` can directly access the `ChatGPT` website by default, using the same variable will not cause
|
353 |
+
conflicts.
|
354 |
+
|
355 |
+
---
|
356 |
+
|
357 |
+
`docker-compose.yaml`:
|
358 |
+
|
359 |
+
```yaml
|
360 |
+
services:
|
361 |
+
go-chatgpt-api:
|
362 |
+
container_name: go-chatgpt-api
|
363 |
+
image: linweiyuan/go-chatgpt-api
|
364 |
+
ports:
|
365 |
+
- 8080:8080
|
366 |
+
environment:
|
367 |
+
- GO_CHATGPT_API_PROXY=
|
368 |
+
restart: unless-stopped
|
369 |
+
```
|
370 |
+
|
371 |
+
I only develop and test on `Arch Linux`, which is a `rolling` release version, meaning that everything on the system is
|
372 |
+
`up-to-date`. If you encounter a `yaml` error while using it, you can add `version: '3'` in front of `services:`.
|
373 |
+
|
374 |
+
If you encounter an `Access denied` error, but your server is indeed
|
375 |
+
in [Supported countries and territories](https://platform.openai.com/docs/supported-countries), try this
|
376 |
+
configuration (it is not guaranteed to solve the problem, for example, if your server is in `Zone A`, but `Zone A`
|
377 |
+
is not on the list of supported countries, even if you use `warp` to change to a `Cloudflare IP`, the result will still
|
378 |
+
be
|
379 |
+
`403`):
|
380 |
+
|
381 |
+
```yaml
|
382 |
+
services:
|
383 |
+
go-chatgpt-api:
|
384 |
+
container_name: go-chatgpt-api
|
385 |
+
image: linweiyuan/go-chatgpt-api
|
386 |
+
ports:
|
387 |
+
- 8080:8080
|
388 |
+
environment:
|
389 |
+
- GO_CHATGPT_API_PROXY=socks5://chatgpt-proxy-server-warp:65535
|
390 |
+
depends_on:
|
391 |
+
- chatgpt-proxy-server-warp
|
392 |
+
restart: unless-stopped
|
393 |
+
|
394 |
+
chatgpt-proxy-server-warp:
|
395 |
+
container_name: chatgpt-proxy-server-warp
|
396 |
+
image: linweiyuan/chatgpt-proxy-server-warp
|
397 |
+
environment:
|
398 |
+
- LOG_LEVEL=OFF
|
399 |
+
restart: unless-stopped
|
400 |
+
```
|
401 |
+
|
402 |
+
If you know what `teams-enroll-token` is and want to set its value, you can do so through the environment
|
403 |
+
variable `TEAMS_ENROLL_TOKEN`.
|
404 |
+
|
405 |
+
Run this command to check the result:
|
406 |
+
|
407 |
+
`docker-compose exec chatgpt-proxy-server-warp warp-cli --accept-tos account | awk 'NR==1'`
|
408 |
+
|
409 |
+
```
|
410 |
+
Account type: Free (wrong)
|
411 |
+
|
412 |
+
Account type: Team (correct)
|
413 |
+
```
|
414 |
+
|
415 |
+
---
|
416 |
+
|
417 |
+
If you want to make sure the image is always latest, try this:
|
418 |
+
|
419 |
+
```yaml
|
420 |
+
services:
|
421 |
+
watchtower:
|
422 |
+
container_name: watchtower
|
423 |
+
image: containrrr/watchtower
|
424 |
+
volumes:
|
425 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
426 |
+
command: --interval 3600
|
427 |
+
restart: unless-stopped
|
428 |
+
```
|
429 |
+
|
430 |
+
<details>
|
431 |
+
|
432 |
+
<summary>AD</summary>
|
433 |
+
|
434 |
+
`Vultr` Referral Program: https://www.vultr.com/?ref=7372562
|
435 |
+
|
436 |
+
</details>
|
api/chatgpt/access_token.go
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"errors"
|
6 |
+
"fmt"
|
7 |
+
"io"
|
8 |
+
"strings"
|
9 |
+
|
10 |
+
"github.com/PuerkitoBio/goquery"
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
|
13 |
+
http "github.com/bogdanfinn/fhttp"
|
14 |
+
)
|
15 |
+
|
16 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
17 |
+
func (userLogin *UserLogin) GetAuthorizedUrl(csrfToken string) (string, int, error) {
|
18 |
+
params := fmt.Sprintf(
|
19 |
+
"callbackUrl=/&csrfToken=%s&json=true",
|
20 |
+
csrfToken,
|
21 |
+
)
|
22 |
+
req, err := http.NewRequest(http.MethodPost, promptLoginUrl, strings.NewReader(params))
|
23 |
+
req.Header.Set("Content-Type", api.ContentType)
|
24 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
25 |
+
resp, err := userLogin.client.Do(req)
|
26 |
+
if err != nil {
|
27 |
+
return "", http.StatusInternalServerError, err
|
28 |
+
}
|
29 |
+
|
30 |
+
defer resp.Body.Close()
|
31 |
+
if resp.StatusCode != http.StatusOK {
|
32 |
+
return "", resp.StatusCode, errors.New(api.GetAuthorizedUrlErrorMessage)
|
33 |
+
}
|
34 |
+
|
35 |
+
responseMap := make(map[string]string)
|
36 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
37 |
+
return responseMap["url"], http.StatusOK, nil
|
38 |
+
}
|
39 |
+
|
40 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
41 |
+
func (userLogin *UserLogin) GetState(authorizedUrl string) (string, int, error) {
|
42 |
+
req, err := http.NewRequest(http.MethodGet, authorizedUrl, nil)
|
43 |
+
req.Header.Set("Content-Type", api.ContentType)
|
44 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
45 |
+
resp, err := userLogin.client.Do(req)
|
46 |
+
if err != nil {
|
47 |
+
return "", http.StatusInternalServerError, err
|
48 |
+
}
|
49 |
+
|
50 |
+
defer resp.Body.Close()
|
51 |
+
if resp.StatusCode != http.StatusOK {
|
52 |
+
return "", resp.StatusCode, errors.New(api.GetStateErrorMessage)
|
53 |
+
}
|
54 |
+
|
55 |
+
doc, _ := goquery.NewDocumentFromReader(resp.Body)
|
56 |
+
state, _ := doc.Find("input[name=state]").Attr("value")
|
57 |
+
return state, http.StatusOK, nil
|
58 |
+
}
|
59 |
+
|
60 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
61 |
+
func (userLogin *UserLogin) CheckUsername(state string, username string) (int, error) {
|
62 |
+
formParams := fmt.Sprintf(
|
63 |
+
"state=%s&username=%s&js-available=true&webauthn-available=true&is-brave=false&webauthn-platform-available=false&action=default",
|
64 |
+
state,
|
65 |
+
username,
|
66 |
+
)
|
67 |
+
req, _ := http.NewRequest(http.MethodPost, api.LoginUsernameUrl+state, strings.NewReader(formParams))
|
68 |
+
req.Header.Set("Content-Type", api.ContentType)
|
69 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
70 |
+
resp, err := userLogin.client.Do(req)
|
71 |
+
if err != nil {
|
72 |
+
return http.StatusInternalServerError, err
|
73 |
+
}
|
74 |
+
|
75 |
+
defer resp.Body.Close()
|
76 |
+
if resp.StatusCode != http.StatusOK {
|
77 |
+
return resp.StatusCode, errors.New(api.EmailInvalidErrorMessage)
|
78 |
+
}
|
79 |
+
|
80 |
+
return http.StatusOK, nil
|
81 |
+
}
|
82 |
+
|
83 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
84 |
+
func (userLogin *UserLogin) CheckPassword(state string, username string, password string) (string, int, error) {
|
85 |
+
formParams := fmt.Sprintf(
|
86 |
+
"state=%s&username=%s&password=%s&action=default",
|
87 |
+
state,
|
88 |
+
username,
|
89 |
+
password,
|
90 |
+
)
|
91 |
+
req, err := http.NewRequest(http.MethodPost, api.LoginPasswordUrl+state, strings.NewReader(formParams))
|
92 |
+
req.Header.Set("Content-Type", api.ContentType)
|
93 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
94 |
+
userLogin.client.SetFollowRedirect(false)
|
95 |
+
resp, err := userLogin.client.Do(req)
|
96 |
+
if err != nil {
|
97 |
+
return "", http.StatusInternalServerError, err
|
98 |
+
}
|
99 |
+
|
100 |
+
defer resp.Body.Close()
|
101 |
+
if resp.StatusCode == http.StatusBadRequest {
|
102 |
+
doc, _ := goquery.NewDocumentFromReader(resp.Body)
|
103 |
+
alert := doc.Find("#prompt-alert").Text()
|
104 |
+
if alert != "" {
|
105 |
+
return "", resp.StatusCode, errors.New(strings.TrimSpace(alert))
|
106 |
+
}
|
107 |
+
|
108 |
+
return "", resp.StatusCode, errors.New(api.EmailOrPasswordInvalidErrorMessage)
|
109 |
+
}
|
110 |
+
|
111 |
+
if resp.StatusCode == http.StatusFound {
|
112 |
+
req, _ := http.NewRequest(http.MethodGet, api.Auth0Url+resp.Header.Get("Location"), nil)
|
113 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
114 |
+
resp, err := userLogin.client.Do(req)
|
115 |
+
if err != nil {
|
116 |
+
return "", http.StatusInternalServerError, err
|
117 |
+
}
|
118 |
+
|
119 |
+
defer resp.Body.Close()
|
120 |
+
if resp.StatusCode == http.StatusFound {
|
121 |
+
location := resp.Header.Get("Location")
|
122 |
+
if strings.HasPrefix(location, "/u/mfa-otp-challenge") {
|
123 |
+
return "", http.StatusBadRequest, errors.New("Login with two-factor authentication enabled is not supported currently.")
|
124 |
+
}
|
125 |
+
|
126 |
+
req, _ := http.NewRequest(http.MethodGet, location, nil)
|
127 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
128 |
+
resp, err := userLogin.client.Do(req)
|
129 |
+
if err != nil {
|
130 |
+
return "", http.StatusInternalServerError, err
|
131 |
+
}
|
132 |
+
|
133 |
+
defer resp.Body.Close()
|
134 |
+
if resp.StatusCode == http.StatusFound {
|
135 |
+
return "", http.StatusOK, nil
|
136 |
+
}
|
137 |
+
|
138 |
+
if resp.StatusCode == http.StatusTemporaryRedirect {
|
139 |
+
errorDescription := req.URL.Query().Get("error_description")
|
140 |
+
if errorDescription != "" {
|
141 |
+
return "", resp.StatusCode, errors.New(errorDescription)
|
142 |
+
}
|
143 |
+
}
|
144 |
+
|
145 |
+
return "", resp.StatusCode, errors.New(api.GetAccessTokenErrorMessage)
|
146 |
+
}
|
147 |
+
|
148 |
+
return "", resp.StatusCode, errors.New(api.EmailOrPasswordInvalidErrorMessage)
|
149 |
+
}
|
150 |
+
|
151 |
+
return "", resp.StatusCode, nil
|
152 |
+
}
|
153 |
+
|
154 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat,GoUnusedParameter
|
155 |
+
func (userLogin *UserLogin) GetAccessToken(code string) (string, int, error) {
|
156 |
+
req, err := http.NewRequest(http.MethodGet, authSessionUrl, nil)
|
157 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
158 |
+
resp, err := userLogin.client.Do(req)
|
159 |
+
if err != nil {
|
160 |
+
return "", http.StatusInternalServerError, err
|
161 |
+
}
|
162 |
+
|
163 |
+
defer resp.Body.Close()
|
164 |
+
if resp.StatusCode != http.StatusOK {
|
165 |
+
if resp.StatusCode == http.StatusTooManyRequests {
|
166 |
+
responseMap := make(map[string]string)
|
167 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
168 |
+
return "", resp.StatusCode, errors.New(responseMap["detail"])
|
169 |
+
}
|
170 |
+
|
171 |
+
return "", resp.StatusCode, errors.New(api.GetAccessTokenErrorMessage)
|
172 |
+
}
|
173 |
+
|
174 |
+
data, _ := io.ReadAll(resp.Body)
|
175 |
+
return string(data), http.StatusOK, nil
|
176 |
+
}
|
api/chatgpt/api.go
ADDED
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bytes"
|
5 |
+
"encoding/json"
|
6 |
+
"fmt"
|
7 |
+
"io"
|
8 |
+
"strings"
|
9 |
+
|
10 |
+
"github.com/PuerkitoBio/goquery"
|
11 |
+
"github.com/gin-gonic/gin"
|
12 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
13 |
+
|
14 |
+
http "github.com/bogdanfinn/fhttp"
|
15 |
+
)
|
16 |
+
|
17 |
+
//goland:noinspection GoUnhandledErrorResult
|
18 |
+
func GetConversations(c *gin.Context) {
|
19 |
+
offset, ok := c.GetQuery("offset")
|
20 |
+
if !ok {
|
21 |
+
offset = "0"
|
22 |
+
}
|
23 |
+
limit, ok := c.GetQuery("limit")
|
24 |
+
if !ok {
|
25 |
+
limit = "20"
|
26 |
+
}
|
27 |
+
handleGet(c, apiPrefix+"/conversations?offset="+offset+"&limit="+limit, getConversationsErrorMessage)
|
28 |
+
}
|
29 |
+
|
30 |
+
//goland:noinspection GoUnhandledErrorResult
|
31 |
+
func CreateConversation(c *gin.Context) {
|
32 |
+
var request CreateConversationRequest
|
33 |
+
if err := c.BindJSON(&request); err != nil {
|
34 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage))
|
35 |
+
return
|
36 |
+
}
|
37 |
+
|
38 |
+
if request.ConversationID == nil || *request.ConversationID == "" {
|
39 |
+
request.ConversationID = nil
|
40 |
+
}
|
41 |
+
if request.Messages[0].Author.Role == "" {
|
42 |
+
request.Messages[0].Author.Role = defaultRole
|
43 |
+
}
|
44 |
+
|
45 |
+
if request.Model == gpt4Model {
|
46 |
+
formParams := fmt.Sprintf(
|
47 |
+
"public_key=%s",
|
48 |
+
gpt4PublicKey,
|
49 |
+
)
|
50 |
+
req, _ := http.NewRequest(http.MethodPost, gpt4TokenUrl, strings.NewReader(formParams))
|
51 |
+
req.Header.Set("Content-Type", api.ContentType)
|
52 |
+
resp, err := api.Client.Do(req)
|
53 |
+
if err != nil {
|
54 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
55 |
+
return
|
56 |
+
}
|
57 |
+
|
58 |
+
responseMap := make(map[string]string)
|
59 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
60 |
+
request.ArkoseToken = responseMap["token"]
|
61 |
+
}
|
62 |
+
|
63 |
+
jsonBytes, _ := json.Marshal(request)
|
64 |
+
req, _ := http.NewRequest(http.MethodPost, apiPrefix+"/conversation", bytes.NewBuffer(jsonBytes))
|
65 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
66 |
+
req.Header.Set("Authorization", api.GetAccessToken(c.GetHeader(api.AuthorizationHeader)))
|
67 |
+
req.Header.Set("Accept", "text/event-stream")
|
68 |
+
resp, err := api.Client.Do(req)
|
69 |
+
if err != nil {
|
70 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
71 |
+
return
|
72 |
+
}
|
73 |
+
|
74 |
+
defer resp.Body.Close()
|
75 |
+
if resp.StatusCode != http.StatusOK {
|
76 |
+
responseMap := make(map[string]interface{})
|
77 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
78 |
+
c.AbortWithStatusJSON(resp.StatusCode, responseMap)
|
79 |
+
return
|
80 |
+
}
|
81 |
+
|
82 |
+
api.HandleConversationResponse(c, resp)
|
83 |
+
}
|
84 |
+
|
85 |
+
//goland:noinspection GoUnhandledErrorResult
|
86 |
+
func GenerateTitle(c *gin.Context) {
|
87 |
+
var request GenerateTitleRequest
|
88 |
+
if err := c.BindJSON(&request); err != nil {
|
89 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage))
|
90 |
+
return
|
91 |
+
}
|
92 |
+
|
93 |
+
jsonBytes, _ := json.Marshal(request)
|
94 |
+
handlePost(c, apiPrefix+"/conversation/gen_title/"+c.Param("id"), string(jsonBytes), generateTitleErrorMessage)
|
95 |
+
}
|
96 |
+
|
97 |
+
//goland:noinspection GoUnhandledErrorResult
|
98 |
+
func GetConversation(c *gin.Context) {
|
99 |
+
handleGet(c, apiPrefix+"/conversation/"+c.Param("id"), getContentErrorMessage)
|
100 |
+
}
|
101 |
+
|
102 |
+
//goland:noinspection GoUnhandledErrorResult
|
103 |
+
func UpdateConversation(c *gin.Context) {
|
104 |
+
var request PatchConversationRequest
|
105 |
+
if err := c.BindJSON(&request); err != nil {
|
106 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage))
|
107 |
+
return
|
108 |
+
}
|
109 |
+
|
110 |
+
// bool default to false, then will hide (delete) the conversation
|
111 |
+
if request.Title != nil {
|
112 |
+
request.IsVisible = true
|
113 |
+
}
|
114 |
+
jsonBytes, _ := json.Marshal(request)
|
115 |
+
handlePatch(c, apiPrefix+"/conversation/"+c.Param("id"), string(jsonBytes), updateConversationErrorMessage)
|
116 |
+
}
|
117 |
+
|
118 |
+
//goland:noinspection GoUnhandledErrorResult
|
119 |
+
func FeedbackMessage(c *gin.Context) {
|
120 |
+
var request FeedbackMessageRequest
|
121 |
+
if err := c.BindJSON(&request); err != nil {
|
122 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage))
|
123 |
+
return
|
124 |
+
}
|
125 |
+
|
126 |
+
jsonBytes, _ := json.Marshal(request)
|
127 |
+
handlePost(c, apiPrefix+"/conversation/message_feedback", string(jsonBytes), feedbackMessageErrorMessage)
|
128 |
+
}
|
129 |
+
|
130 |
+
//goland:noinspection GoUnhandledErrorResult
|
131 |
+
func ClearConversations(c *gin.Context) {
|
132 |
+
jsonBytes, _ := json.Marshal(PatchConversationRequest{
|
133 |
+
IsVisible: false,
|
134 |
+
})
|
135 |
+
handlePatch(c, apiPrefix+"/conversations", string(jsonBytes), clearConversationsErrorMessage)
|
136 |
+
}
|
137 |
+
|
138 |
+
//goland:noinspection GoUnhandledErrorResult
|
139 |
+
func GetModels(c *gin.Context) {
|
140 |
+
handleGet(c, apiPrefix+"/models", getModelsErrorMessage)
|
141 |
+
}
|
142 |
+
|
143 |
+
func GetAccountCheck(c *gin.Context) {
|
144 |
+
handleGet(c, apiPrefix+"/accounts/check", getAccountCheckErrorMessage)
|
145 |
+
}
|
146 |
+
|
147 |
+
//goland:noinspection GoUnhandledErrorResult
|
148 |
+
func Login(c *gin.Context) {
|
149 |
+
var loginInfo api.LoginInfo
|
150 |
+
if err := c.ShouldBindJSON(&loginInfo); err != nil {
|
151 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage))
|
152 |
+
return
|
153 |
+
}
|
154 |
+
|
155 |
+
userLogin := UserLogin{
|
156 |
+
client: api.NewHttpClient(),
|
157 |
+
}
|
158 |
+
|
159 |
+
// get csrf token
|
160 |
+
req, _ := http.NewRequest(http.MethodGet, csrfUrl, nil)
|
161 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
162 |
+
resp, err := userLogin.client.Do(req)
|
163 |
+
if err != nil {
|
164 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
165 |
+
return
|
166 |
+
}
|
167 |
+
|
168 |
+
defer resp.Body.Close()
|
169 |
+
if resp.StatusCode != http.StatusOK {
|
170 |
+
if resp.StatusCode == http.StatusForbidden {
|
171 |
+
doc, _ := goquery.NewDocumentFromReader(resp.Body)
|
172 |
+
alert := doc.Find(".message").Text()
|
173 |
+
if alert != "" {
|
174 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(strings.TrimSpace(alert)))
|
175 |
+
return
|
176 |
+
}
|
177 |
+
}
|
178 |
+
|
179 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(getCsrfTokenErrorMessage))
|
180 |
+
return
|
181 |
+
}
|
182 |
+
|
183 |
+
// get authorized url
|
184 |
+
responseMap := make(map[string]string)
|
185 |
+
json.NewDecoder(resp.Body).Decode(&responseMap)
|
186 |
+
authorizedUrl, statusCode, err := userLogin.GetAuthorizedUrl(responseMap["csrfToken"])
|
187 |
+
if err != nil {
|
188 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
189 |
+
return
|
190 |
+
}
|
191 |
+
|
192 |
+
// get state
|
193 |
+
state, statusCode, err := userLogin.GetState(authorizedUrl)
|
194 |
+
if err != nil {
|
195 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
196 |
+
return
|
197 |
+
}
|
198 |
+
|
199 |
+
// check username
|
200 |
+
statusCode, err = userLogin.CheckUsername(state, loginInfo.Username)
|
201 |
+
if err != nil {
|
202 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
203 |
+
return
|
204 |
+
}
|
205 |
+
|
206 |
+
// check password
|
207 |
+
_, statusCode, err = userLogin.CheckPassword(state, loginInfo.Username, loginInfo.Password)
|
208 |
+
if err != nil {
|
209 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
210 |
+
return
|
211 |
+
}
|
212 |
+
|
213 |
+
// get access token
|
214 |
+
accessToken, statusCode, err := userLogin.GetAccessToken("")
|
215 |
+
if err != nil {
|
216 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
217 |
+
return
|
218 |
+
}
|
219 |
+
|
220 |
+
c.Writer.WriteString(accessToken)
|
221 |
+
}
|
222 |
+
|
223 |
+
func Fallback(c *gin.Context) {
|
224 |
+
method := c.Request.Method
|
225 |
+
url := apiPrefix + c.Request.URL.Path
|
226 |
+
queryParams := c.Request.URL.Query().Encode()
|
227 |
+
if queryParams != "" {
|
228 |
+
url += "?" + queryParams
|
229 |
+
}
|
230 |
+
|
231 |
+
var requestBody string
|
232 |
+
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPatch {
|
233 |
+
body, _ := io.ReadAll(c.Request.Body)
|
234 |
+
requestBody = string(body)
|
235 |
+
}
|
236 |
+
|
237 |
+
c.Status(http.StatusOK)
|
238 |
+
|
239 |
+
switch method {
|
240 |
+
case http.MethodGet:
|
241 |
+
handleGet(c, url, fallbackErrorMessage)
|
242 |
+
case http.MethodPost:
|
243 |
+
handlePost(c, url, requestBody, fallbackErrorMessage)
|
244 |
+
case http.MethodPatch:
|
245 |
+
handlePatch(c, url, requestBody, fallbackErrorMessage)
|
246 |
+
default:
|
247 |
+
c.JSON(http.StatusMethodNotAllowed, gin.H{"message": fallbackMethodNotAllowedMessage})
|
248 |
+
}
|
249 |
+
}
|
250 |
+
|
251 |
+
//goland:noinspection GoUnhandledErrorResult
|
252 |
+
func handleGet(c *gin.Context, url string, errorMessage string) {
|
253 |
+
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
254 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
255 |
+
req.Header.Set("Authorization", api.GetAccessToken(c.GetHeader(api.AuthorizationHeader)))
|
256 |
+
resp, err := api.Client.Do(req)
|
257 |
+
if err != nil {
|
258 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
259 |
+
return
|
260 |
+
}
|
261 |
+
|
262 |
+
defer resp.Body.Close()
|
263 |
+
if resp.StatusCode != http.StatusOK {
|
264 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(errorMessage))
|
265 |
+
return
|
266 |
+
}
|
267 |
+
|
268 |
+
io.Copy(c.Writer, resp.Body)
|
269 |
+
}
|
270 |
+
|
271 |
+
//goland:noinspection GoUnhandledErrorResult
|
272 |
+
func handlePost(c *gin.Context, url string, requestBody string, errorMessage string) {
|
273 |
+
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(requestBody))
|
274 |
+
handlePostOrPatch(c, req, errorMessage)
|
275 |
+
}
|
276 |
+
|
277 |
+
//goland:noinspection GoUnhandledErrorResult
|
278 |
+
func handlePatch(c *gin.Context, url string, requestBody string, errorMessage string) {
|
279 |
+
req, _ := http.NewRequest(http.MethodPatch, url, strings.NewReader(requestBody))
|
280 |
+
handlePostOrPatch(c, req, errorMessage)
|
281 |
+
}
|
282 |
+
|
283 |
+
//goland:noinspection GoUnhandledErrorResult
|
284 |
+
func handlePostOrPatch(c *gin.Context, req *http.Request, errorMessage string) {
|
285 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
286 |
+
req.Header.Set("Authorization", api.GetAccessToken(c.GetHeader(api.AuthorizationHeader)))
|
287 |
+
resp, err := api.Client.Do(req)
|
288 |
+
if err != nil {
|
289 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
290 |
+
return
|
291 |
+
}
|
292 |
+
|
293 |
+
defer resp.Body.Close()
|
294 |
+
if resp.StatusCode != http.StatusOK {
|
295 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(errorMessage))
|
296 |
+
return
|
297 |
+
}
|
298 |
+
|
299 |
+
io.Copy(c.Writer, resp.Body)
|
300 |
+
}
|
api/chatgpt/constant.go
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
const (
|
4 |
+
apiPrefix = "https://chat.openai.com/backend-api"
|
5 |
+
defaultRole = "user"
|
6 |
+
getConversationsErrorMessage = "Failed to get conversations."
|
7 |
+
generateTitleErrorMessage = "Failed to generate title."
|
8 |
+
getContentErrorMessage = "Failed to get content."
|
9 |
+
updateConversationErrorMessage = "Failed to update conversation."
|
10 |
+
clearConversationsErrorMessage = "Failed to clear conversations."
|
11 |
+
feedbackMessageErrorMessage = "Failed to add feedback."
|
12 |
+
getModelsErrorMessage = "Failed to get models."
|
13 |
+
getAccountCheckErrorMessage = "Check failed." // Placeholder. Never encountered.
|
14 |
+
parseJsonErrorMessage = "Failed to parse json request body."
|
15 |
+
fallbackErrorMessage = "Fallback failed."
|
16 |
+
fallbackMethodNotAllowedMessage = "Fallback method not allowed."
|
17 |
+
|
18 |
+
csrfUrl = "https://chat.openai.com/api/auth/csrf"
|
19 |
+
promptLoginUrl = "https://chat.openai.com/api/auth/signin/auth0?prompt=login"
|
20 |
+
getCsrfTokenErrorMessage = "Failed to get CSRF token."
|
21 |
+
authSessionUrl = "https://chat.openai.com/api/auth/session"
|
22 |
+
|
23 |
+
gpt4Model = "gpt-4"
|
24 |
+
gpt4PublicKey = "35536E1E-65B4-4D96-9D97-6ADB7EFF8147"
|
25 |
+
gpt4TokenUrl = "https://tcr9i.chat.openai.com/fc/gt2/public_key/" + gpt4PublicKey
|
26 |
+
)
|
api/chatgpt/health_check.go
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
import (
|
4 |
+
"os"
|
5 |
+
"time"
|
6 |
+
|
7 |
+
"github.com/PuerkitoBio/goquery"
|
8 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
9 |
+
"github.com/linweiyuan/go-chatgpt-api/util/logger"
|
10 |
+
|
11 |
+
http "github.com/bogdanfinn/fhttp"
|
12 |
+
)
|
13 |
+
|
14 |
+
//goland:noinspection SpellCheckingInspection
|
15 |
+
const (
|
16 |
+
healthCheckUrl = "https://chat.openai.com/backend-api/accounts/check"
|
17 |
+
readyHint = "Service go-chatgpt-api is ready."
|
18 |
+
errorHintBlock = "Looks like you have bean blocked -> curl https://chat.openai.com | grep '<p>' | awk '{$1=$1;print}'"
|
19 |
+
errorHint403 = "Failed to handle 403, have a look at https://github.com/linweiyuan/java-chatgpt-api or use other more powerful alternatives (do not raise new issue about 403)."
|
20 |
+
sleepHours = 8760 // 365 days
|
21 |
+
)
|
22 |
+
|
23 |
+
//goland:noinspection GoUnhandledErrorResult,SpellCheckingInspection
|
24 |
+
func init() {
|
25 |
+
proxyUrl := os.Getenv("GO_CHATGPT_API_PROXY")
|
26 |
+
if proxyUrl != "" {
|
27 |
+
logger.Info("GO_CHATGPT_API_PROXY: " + proxyUrl)
|
28 |
+
api.Client.SetProxy(proxyUrl)
|
29 |
+
|
30 |
+
for {
|
31 |
+
resp, err := healthCheck()
|
32 |
+
if err != nil {
|
33 |
+
// wait for proxy to be ready
|
34 |
+
time.Sleep(time.Second)
|
35 |
+
continue
|
36 |
+
}
|
37 |
+
|
38 |
+
checkHealthCheckStatus(resp)
|
39 |
+
break
|
40 |
+
}
|
41 |
+
} else {
|
42 |
+
resp, err := healthCheck()
|
43 |
+
if err != nil {
|
44 |
+
logger.Error("Health check failed: " + err.Error())
|
45 |
+
os.Exit(1)
|
46 |
+
}
|
47 |
+
|
48 |
+
checkHealthCheckStatus(resp)
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
func healthCheck() (resp *http.Response, err error) {
|
53 |
+
req, _ := http.NewRequest(http.MethodGet, healthCheckUrl, nil)
|
54 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
55 |
+
resp, err = api.Client.Do(req)
|
56 |
+
return
|
57 |
+
}
|
58 |
+
|
59 |
+
//goland:noinspection GoUnhandledErrorResult
|
60 |
+
func checkHealthCheckStatus(resp *http.Response) {
|
61 |
+
defer resp.Body.Close()
|
62 |
+
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
|
63 |
+
logger.Info(readyHint)
|
64 |
+
} else {
|
65 |
+
doc, _ := goquery.NewDocumentFromReader(resp.Body)
|
66 |
+
alert := doc.Find(".message").Text()
|
67 |
+
if alert != "" {
|
68 |
+
logger.Error(errorHintBlock)
|
69 |
+
} else {
|
70 |
+
logger.Error(errorHint403)
|
71 |
+
}
|
72 |
+
time.Sleep(time.Hour * sleepHours)
|
73 |
+
os.Exit(1)
|
74 |
+
}
|
75 |
+
}
|
api/chatgpt/typings.go
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package chatgpt
|
2 |
+
|
3 |
+
//goland:noinspection GoSnakeCaseUsage
|
4 |
+
import tls_client "github.com/bogdanfinn/tls-client"
|
5 |
+
|
6 |
+
type UserLogin struct {
|
7 |
+
client tls_client.HttpClient
|
8 |
+
}
|
9 |
+
|
10 |
+
type CreateConversationRequest struct {
|
11 |
+
Action string `json:"action"`
|
12 |
+
Messages []Message `json:"messages"`
|
13 |
+
Model string `json:"model"`
|
14 |
+
ParentMessageID string `json:"parent_message_id"`
|
15 |
+
ConversationID *string `json:"conversation_id"`
|
16 |
+
TimezoneOffsetMin int `json:"timezone_offset_min"`
|
17 |
+
ArkoseToken string `json:"arkose_token"`
|
18 |
+
}
|
19 |
+
|
20 |
+
type Message struct {
|
21 |
+
Author Author `json:"author"`
|
22 |
+
Content Content `json:"content"`
|
23 |
+
ID string `json:"id"`
|
24 |
+
}
|
25 |
+
|
26 |
+
type Author struct {
|
27 |
+
Role string `json:"role"`
|
28 |
+
}
|
29 |
+
|
30 |
+
type Content struct {
|
31 |
+
ContentType string `json:"content_type"`
|
32 |
+
Parts []string `json:"parts"`
|
33 |
+
}
|
34 |
+
|
35 |
+
type FeedbackMessageRequest struct {
|
36 |
+
MessageID string `json:"message_id"`
|
37 |
+
ConversationID string `json:"conversation_id"`
|
38 |
+
Rating string `json:"rating"`
|
39 |
+
}
|
40 |
+
|
41 |
+
type GenerateTitleRequest struct {
|
42 |
+
MessageID string `json:"message_id"`
|
43 |
+
}
|
44 |
+
|
45 |
+
type PatchConversationRequest struct {
|
46 |
+
Title *string `json:"title"`
|
47 |
+
IsVisible bool `json:"is_visible"`
|
48 |
+
}
|
49 |
+
|
50 |
+
type Cookie struct {
|
51 |
+
Name string `json:"name"`
|
52 |
+
Value string `json:"value"`
|
53 |
+
Expiry int64 `json:"expiry"`
|
54 |
+
}
|
api/common.go
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package api
|
2 |
+
|
3 |
+
//goland:noinspection GoSnakeCaseUsage
|
4 |
+
import (
|
5 |
+
"bufio"
|
6 |
+
"os"
|
7 |
+
"strings"
|
8 |
+
|
9 |
+
"github.com/gin-gonic/gin"
|
10 |
+
_ "github.com/linweiyuan/go-chatgpt-api/env"
|
11 |
+
|
12 |
+
http "github.com/bogdanfinn/fhttp"
|
13 |
+
tls_client "github.com/bogdanfinn/tls-client"
|
14 |
+
)
|
15 |
+
|
16 |
+
const (
|
17 |
+
defaultErrorMessageKey = "errorMessage"
|
18 |
+
AuthorizationHeader = "Authorization"
|
19 |
+
ContentType = "application/x-www-form-urlencoded"
|
20 |
+
UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
21 |
+
Auth0Url = "https://auth0.openai.com"
|
22 |
+
LoginUsernameUrl = Auth0Url + "/u/login/identifier?state="
|
23 |
+
LoginPasswordUrl = Auth0Url + "/u/login/password?state="
|
24 |
+
ParseUserInfoErrorMessage = "Failed to parse user login info."
|
25 |
+
GetAuthorizedUrlErrorMessage = "Failed to get authorized url."
|
26 |
+
GetStateErrorMessage = "Failed to get state."
|
27 |
+
EmailInvalidErrorMessage = "Email is not valid."
|
28 |
+
EmailOrPasswordInvalidErrorMessage = "Email or password is not correct."
|
29 |
+
GetAccessTokenErrorMessage = "Failed to get access token."
|
30 |
+
defaultTimeoutSeconds = 300 // 5 minutes
|
31 |
+
)
|
32 |
+
|
33 |
+
var Client tls_client.HttpClient
|
34 |
+
|
35 |
+
type LoginInfo struct {
|
36 |
+
Username string `json:"username"`
|
37 |
+
Password string `json:"password"`
|
38 |
+
}
|
39 |
+
|
40 |
+
type AuthLogin interface {
|
41 |
+
GetAuthorizedUrl(csrfToken string) (string, int, error)
|
42 |
+
GetState(authorizedUrl string) (string, int, error)
|
43 |
+
CheckUsername(state string, username string) (int, error)
|
44 |
+
CheckPassword(state string, username string, password string) (string, int, error)
|
45 |
+
GetAccessToken(code string) (string, int, error)
|
46 |
+
}
|
47 |
+
|
48 |
+
//goland:noinspection GoUnhandledErrorResult
|
49 |
+
func init() {
|
50 |
+
Client, _ = tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{
|
51 |
+
tls_client.WithCookieJar(tls_client.NewCookieJar()),
|
52 |
+
tls_client.WithTimeoutSeconds(defaultTimeoutSeconds),
|
53 |
+
tls_client.WithClientProfile(tls_client.Okhttp4Android13),
|
54 |
+
}...)
|
55 |
+
}
|
56 |
+
|
57 |
+
func ReturnMessage(msg string) gin.H {
|
58 |
+
return gin.H{
|
59 |
+
defaultErrorMessageKey: msg,
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
func GetAccessToken(accessToken string) string {
|
64 |
+
if !strings.HasPrefix(accessToken, "Bearer") {
|
65 |
+
return "Bearer " + accessToken
|
66 |
+
}
|
67 |
+
return accessToken
|
68 |
+
}
|
69 |
+
|
70 |
+
//goland:noinspection GoUnhandledErrorResult
|
71 |
+
func HandleConversationResponse(c *gin.Context, resp *http.Response) {
|
72 |
+
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
73 |
+
|
74 |
+
reader := bufio.NewReader(resp.Body)
|
75 |
+
for {
|
76 |
+
if c.Request.Context().Err() != nil {
|
77 |
+
break
|
78 |
+
}
|
79 |
+
|
80 |
+
line, err := reader.ReadString('\n')
|
81 |
+
if err != nil {
|
82 |
+
break
|
83 |
+
}
|
84 |
+
|
85 |
+
line = strings.TrimSpace(line)
|
86 |
+
if strings.HasPrefix(line, "event") ||
|
87 |
+
strings.HasPrefix(line, "data: 20") ||
|
88 |
+
line == "" {
|
89 |
+
continue
|
90 |
+
}
|
91 |
+
|
92 |
+
c.Writer.Write([]byte(line + "\n\n"))
|
93 |
+
c.Writer.Flush()
|
94 |
+
}
|
95 |
+
}
|
96 |
+
|
97 |
+
//goland:noinspection GoUnhandledErrorResult,SpellCheckingInspection
|
98 |
+
func NewHttpClient() tls_client.HttpClient {
|
99 |
+
client, _ := tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{
|
100 |
+
tls_client.WithCookieJar(tls_client.NewCookieJar()),
|
101 |
+
tls_client.WithClientProfile(tls_client.Okhttp4Android13),
|
102 |
+
}...)
|
103 |
+
|
104 |
+
proxyUrl := os.Getenv("GO_CHATGPT_API_PROXY")
|
105 |
+
if proxyUrl != "" {
|
106 |
+
client.SetProxy(proxyUrl)
|
107 |
+
}
|
108 |
+
|
109 |
+
return client
|
110 |
+
}
|
api/platform/access_token.go
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import (
|
4 |
+
"encoding/json"
|
5 |
+
"errors"
|
6 |
+
"fmt"
|
7 |
+
"io"
|
8 |
+
"net/url"
|
9 |
+
"strings"
|
10 |
+
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
|
13 |
+
http "github.com/bogdanfinn/fhttp"
|
14 |
+
)
|
15 |
+
|
16 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat,GoUnusedParameter
|
17 |
+
func (userLogin *UserLogin) GetAuthorizedUrl(csrfToken string) (string, int, error) {
|
18 |
+
urlParams := url.Values{
|
19 |
+
"client_id": {platformAuthClientID},
|
20 |
+
"audience": {platformAuthAudience},
|
21 |
+
"redirect_uri": {platformAuthRedirectURL},
|
22 |
+
"scope": {platformAuthScope},
|
23 |
+
"response_type": {platformAuthResponseType},
|
24 |
+
}
|
25 |
+
req, _ := http.NewRequest(http.MethodGet, platformAuth0Url+urlParams.Encode(), nil)
|
26 |
+
req.Header.Set("Content-Type", api.ContentType)
|
27 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
28 |
+
resp, err := userLogin.client.Do(req)
|
29 |
+
if err != nil {
|
30 |
+
return "", http.StatusInternalServerError, err
|
31 |
+
}
|
32 |
+
|
33 |
+
defer resp.Body.Close()
|
34 |
+
if resp.StatusCode != http.StatusOK {
|
35 |
+
return "", resp.StatusCode, errors.New(api.GetAuthorizedUrlErrorMessage)
|
36 |
+
}
|
37 |
+
|
38 |
+
return resp.Request.URL.String(), http.StatusOK, nil
|
39 |
+
}
|
40 |
+
|
41 |
+
func (userLogin *UserLogin) GetState(authorizedUrl string) (string, int, error) {
|
42 |
+
split := strings.Split(authorizedUrl, "=")
|
43 |
+
return split[1], http.StatusOK, nil
|
44 |
+
}
|
45 |
+
|
46 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
47 |
+
func (userLogin *UserLogin) CheckUsername(state string, username string) (int, error) {
|
48 |
+
formParams := fmt.Sprintf(
|
49 |
+
"state=%s&username=%s&js-available=true&webauthn-available=true&is-brave=false&webauthn-platform-available=false&action=default",
|
50 |
+
state,
|
51 |
+
username,
|
52 |
+
)
|
53 |
+
req, err := http.NewRequest(http.MethodPost, api.LoginUsernameUrl+state, strings.NewReader(formParams))
|
54 |
+
req.Header.Set("Content-Type", api.ContentType)
|
55 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
56 |
+
resp, err := userLogin.client.Do(req)
|
57 |
+
if err != nil {
|
58 |
+
return http.StatusInternalServerError, err
|
59 |
+
}
|
60 |
+
|
61 |
+
defer resp.Body.Close()
|
62 |
+
if resp.StatusCode != http.StatusOK {
|
63 |
+
return resp.StatusCode, errors.New(api.EmailInvalidErrorMessage)
|
64 |
+
}
|
65 |
+
|
66 |
+
return http.StatusOK, nil
|
67 |
+
}
|
68 |
+
|
69 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
70 |
+
func (userLogin *UserLogin) CheckPassword(state string, username string, password string) (string, int, error) {
|
71 |
+
formParams := fmt.Sprintf(
|
72 |
+
"state=%s&username=%s&password=%s&action=default",
|
73 |
+
state,
|
74 |
+
username,
|
75 |
+
password,
|
76 |
+
)
|
77 |
+
req, err := http.NewRequest(http.MethodPost, api.LoginPasswordUrl+state, strings.NewReader(formParams))
|
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 |
+
//goland:noinspection GoUnhandledErrorResult,GoErrorStringFormat
|
94 |
+
func (userLogin *UserLogin) GetAccessToken(code string) (string, int, error) {
|
95 |
+
jsonBytes, _ := json.Marshal(GetAccessTokenRequest{
|
96 |
+
ClientID: platformAuthClientID,
|
97 |
+
Code: code,
|
98 |
+
GrantType: platformAuthGrantType,
|
99 |
+
RedirectURI: platformAuthRedirectURL,
|
100 |
+
})
|
101 |
+
req, err := http.NewRequest(http.MethodPost, getTokenUrl, strings.NewReader(string(jsonBytes)))
|
102 |
+
req.Header.Set("Content-Type", "application/json")
|
103 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
104 |
+
resp, err := userLogin.client.Do(req)
|
105 |
+
if err != nil {
|
106 |
+
return "", http.StatusInternalServerError, err
|
107 |
+
}
|
108 |
+
|
109 |
+
defer resp.Body.Close()
|
110 |
+
if resp.StatusCode != http.StatusOK {
|
111 |
+
return "", resp.StatusCode, errors.New(api.GetAccessTokenErrorMessage)
|
112 |
+
}
|
113 |
+
|
114 |
+
data, _ := io.ReadAll(resp.Body)
|
115 |
+
return string(data), http.StatusOK, nil
|
116 |
+
}
|
api/platform/api.go
ADDED
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import (
|
4 |
+
"bytes"
|
5 |
+
"encoding/json"
|
6 |
+
"fmt"
|
7 |
+
"io"
|
8 |
+
"strings"
|
9 |
+
|
10 |
+
"github.com/gin-gonic/gin"
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
12 |
+
|
13 |
+
http "github.com/bogdanfinn/fhttp"
|
14 |
+
)
|
15 |
+
|
16 |
+
func ListModels(c *gin.Context) {
|
17 |
+
handleGet(c, apiListModels)
|
18 |
+
}
|
19 |
+
|
20 |
+
func RetrieveModel(c *gin.Context) {
|
21 |
+
model := c.Param("model")
|
22 |
+
handleGet(c, fmt.Sprintf(apiRetrieveModel, model))
|
23 |
+
}
|
24 |
+
|
25 |
+
//goland:noinspection GoUnhandledErrorResult
|
26 |
+
func CreateCompletions(c *gin.Context) {
|
27 |
+
var request CreateCompletionsRequest
|
28 |
+
c.ShouldBindJSON(&request)
|
29 |
+
data, _ := json.Marshal(request)
|
30 |
+
resp, err := handlePost(c, apiCreateCompletions, data, request.Stream)
|
31 |
+
if err != nil {
|
32 |
+
return
|
33 |
+
}
|
34 |
+
|
35 |
+
defer resp.Body.Close()
|
36 |
+
if request.Stream {
|
37 |
+
api.HandleConversationResponse(c, resp)
|
38 |
+
} else {
|
39 |
+
io.Copy(c.Writer, resp.Body)
|
40 |
+
}
|
41 |
+
}
|
42 |
+
|
43 |
+
//goland:noinspection GoUnhandledErrorResult
|
44 |
+
func CreateChatCompletions(c *gin.Context) {
|
45 |
+
var request ChatCompletionsRequest
|
46 |
+
c.ShouldBindJSON(&request)
|
47 |
+
data, _ := json.Marshal(request)
|
48 |
+
resp, err := handlePost(c, apiCreataeChatCompletions, data, request.Stream)
|
49 |
+
if err != nil {
|
50 |
+
return
|
51 |
+
}
|
52 |
+
|
53 |
+
defer resp.Body.Close()
|
54 |
+
if request.Stream {
|
55 |
+
api.HandleConversationResponse(c, resp)
|
56 |
+
} else {
|
57 |
+
io.Copy(c.Writer, resp.Body)
|
58 |
+
}
|
59 |
+
}
|
60 |
+
|
61 |
+
//goland:noinspection GoUnhandledErrorResult
|
62 |
+
func CreateEdit(c *gin.Context) {
|
63 |
+
var request CreateEditRequest
|
64 |
+
c.ShouldBindJSON(&request)
|
65 |
+
data, _ := json.Marshal(request)
|
66 |
+
resp, err := handlePost(c, apiCreateEdit, data, false)
|
67 |
+
if err != nil {
|
68 |
+
return
|
69 |
+
}
|
70 |
+
|
71 |
+
defer resp.Body.Close()
|
72 |
+
io.Copy(c.Writer, resp.Body)
|
73 |
+
}
|
74 |
+
|
75 |
+
//goland:noinspection GoUnhandledErrorResult
|
76 |
+
func CreateImage(c *gin.Context) {
|
77 |
+
var request CreateImageRequest
|
78 |
+
c.ShouldBindJSON(&request)
|
79 |
+
data, _ := json.Marshal(request)
|
80 |
+
resp, err := handlePost(c, apiCreateImage, data, false)
|
81 |
+
if err != nil {
|
82 |
+
return
|
83 |
+
}
|
84 |
+
|
85 |
+
defer resp.Body.Close()
|
86 |
+
io.Copy(c.Writer, resp.Body)
|
87 |
+
}
|
88 |
+
|
89 |
+
//goland:noinspection GoUnhandledErrorResult
|
90 |
+
func CreateEmbeddings(c *gin.Context) {
|
91 |
+
var request CreateEmbeddingsRequest
|
92 |
+
c.ShouldBindJSON(&request)
|
93 |
+
data, _ := json.Marshal(request)
|
94 |
+
resp, err := handlePost(c, apiCreateEmbeddings, data, false)
|
95 |
+
if err != nil {
|
96 |
+
return
|
97 |
+
}
|
98 |
+
|
99 |
+
defer resp.Body.Close()
|
100 |
+
io.Copy(c.Writer, resp.Body)
|
101 |
+
}
|
102 |
+
|
103 |
+
func CreateModeration(c *gin.Context) {
|
104 |
+
var request CreateModerationRequest
|
105 |
+
c.ShouldBindJSON(&request)
|
106 |
+
data, _ := json.Marshal(request)
|
107 |
+
resp, err := handlePost(c, apiCreateModeration, data, false)
|
108 |
+
if err != nil {
|
109 |
+
return
|
110 |
+
}
|
111 |
+
|
112 |
+
defer resp.Body.Close()
|
113 |
+
io.Copy(c.Writer, resp.Body)
|
114 |
+
}
|
115 |
+
|
116 |
+
func ListFiles(c *gin.Context) {
|
117 |
+
handleGet(c, apiListFiles)
|
118 |
+
}
|
119 |
+
|
120 |
+
func GetCreditGrants(c *gin.Context) {
|
121 |
+
handleGet(c, apiGetCreditGrants)
|
122 |
+
}
|
123 |
+
|
124 |
+
//goland:noinspection GoUnhandledErrorResult
|
125 |
+
func Login(c *gin.Context) {
|
126 |
+
var loginInfo api.LoginInfo
|
127 |
+
if err := c.ShouldBindJSON(&loginInfo); err != nil {
|
128 |
+
c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage))
|
129 |
+
return
|
130 |
+
}
|
131 |
+
|
132 |
+
userLogin := UserLogin{
|
133 |
+
client: api.NewHttpClient(),
|
134 |
+
}
|
135 |
+
|
136 |
+
// hard refresh cookies
|
137 |
+
resp, _ := userLogin.client.Get(auth0LogoutUrl)
|
138 |
+
defer resp.Body.Close()
|
139 |
+
|
140 |
+
// get authorized url
|
141 |
+
authorizedUrl, statusCode, err := userLogin.GetAuthorizedUrl("")
|
142 |
+
if err != nil {
|
143 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
144 |
+
return
|
145 |
+
}
|
146 |
+
|
147 |
+
// get state
|
148 |
+
state, _, _ := userLogin.GetState(authorizedUrl)
|
149 |
+
|
150 |
+
// check username
|
151 |
+
statusCode, err = userLogin.CheckUsername(state, loginInfo.Username)
|
152 |
+
if err != nil {
|
153 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
154 |
+
return
|
155 |
+
}
|
156 |
+
|
157 |
+
// check password
|
158 |
+
code, statusCode, err := userLogin.CheckPassword(state, loginInfo.Username, loginInfo.Password)
|
159 |
+
if err != nil {
|
160 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
161 |
+
return
|
162 |
+
}
|
163 |
+
|
164 |
+
// get access token
|
165 |
+
accessToken, statusCode, err := userLogin.GetAccessToken(code)
|
166 |
+
if err != nil {
|
167 |
+
c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error()))
|
168 |
+
return
|
169 |
+
}
|
170 |
+
|
171 |
+
// get session key
|
172 |
+
var getAccessTokenResponse GetAccessTokenResponse
|
173 |
+
json.Unmarshal([]byte(accessToken), &getAccessTokenResponse)
|
174 |
+
req, _ := http.NewRequest(http.MethodPost, dashboardLoginUrl, strings.NewReader("{}"))
|
175 |
+
req.Header.Set("Content-Type", "application/json")
|
176 |
+
req.Header.Set("User-Agent", api.UserAgent)
|
177 |
+
req.Header.Set("Authorization", api.GetAccessToken(getAccessTokenResponse.AccessToken))
|
178 |
+
resp, err = userLogin.client.Do(req)
|
179 |
+
if err != nil {
|
180 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
181 |
+
return
|
182 |
+
}
|
183 |
+
|
184 |
+
defer resp.Body.Close()
|
185 |
+
if resp.StatusCode != http.StatusOK {
|
186 |
+
c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(getSessionKeyErrorMessage))
|
187 |
+
return
|
188 |
+
}
|
189 |
+
|
190 |
+
io.Copy(c.Writer, resp.Body)
|
191 |
+
}
|
192 |
+
|
193 |
+
func GetSubscription(c *gin.Context) {
|
194 |
+
handleGet(c, apiGetSubscription)
|
195 |
+
}
|
196 |
+
|
197 |
+
func GetApiKeys(c *gin.Context) {
|
198 |
+
handleGet(c, apiGetApiKeys)
|
199 |
+
}
|
200 |
+
|
201 |
+
//goland:noinspection GoUnhandledErrorResult
|
202 |
+
func handleGet(c *gin.Context, url string) {
|
203 |
+
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
204 |
+
req.Header.Set("Authorization", api.GetAccessToken(c.GetHeader(api.AuthorizationHeader)))
|
205 |
+
resp, _ := api.Client.Do(req)
|
206 |
+
defer resp.Body.Close()
|
207 |
+
io.Copy(c.Writer, resp.Body)
|
208 |
+
}
|
209 |
+
|
210 |
+
func handlePost(c *gin.Context, url string, data []byte, stream bool) (*http.Response, error) {
|
211 |
+
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
|
212 |
+
req.Header.Set("Authorization", api.GetAccessToken(c.GetHeader(api.AuthorizationHeader)))
|
213 |
+
if stream {
|
214 |
+
req.Header.Set("Accept", "text/event-stream")
|
215 |
+
}
|
216 |
+
req.Header.Set("Content-Type", "application/json")
|
217 |
+
resp, err := api.Client.Do(req)
|
218 |
+
if err != nil {
|
219 |
+
c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error()))
|
220 |
+
return nil, err
|
221 |
+
}
|
222 |
+
|
223 |
+
return resp, nil
|
224 |
+
}
|
api/platform/constant.go
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
import "github.com/linweiyuan/go-chatgpt-api/api"
|
4 |
+
|
5 |
+
//goland:noinspection SpellCheckingInspection
|
6 |
+
const (
|
7 |
+
apiUrl = "https://api.openai.com"
|
8 |
+
|
9 |
+
apiListModels = apiUrl + "/v1/models"
|
10 |
+
apiRetrieveModel = apiUrl + "/v1/models/%s"
|
11 |
+
apiCreateCompletions = apiUrl + "/v1/completions"
|
12 |
+
apiCreataeChatCompletions = apiUrl + "/v1/chat/completions"
|
13 |
+
apiCreateEdit = apiUrl + "/v1/edits"
|
14 |
+
apiCreateImage = apiUrl + "/v1/images/generations"
|
15 |
+
apiCreateEmbeddings = apiUrl + "/v1/embeddings"
|
16 |
+
apiListFiles = apiUrl + "/v1/files"
|
17 |
+
apiCreateModeration = apiUrl + "/v1/moderations"
|
18 |
+
|
19 |
+
apiGetCreditGrants = apiUrl + "/dashboard/billing/credit_grants"
|
20 |
+
apiGetSubscription = apiUrl + "/dashboard/billing/subscription"
|
21 |
+
apiGetApiKeys = apiUrl + "/dashboard/user/api_keys"
|
22 |
+
|
23 |
+
platformAuthClientID = "DRivsnm2Mu42T3KOpqdtwB3NYviHYzwD"
|
24 |
+
platformAuthAudience = "https://api.openai.com/v1"
|
25 |
+
platformAuthRedirectURL = "https://platform.openai.com/auth/callback"
|
26 |
+
platformAuthScope = "openid profile email offline_access"
|
27 |
+
platformAuthResponseType = "code"
|
28 |
+
platformAuthGrantType = "authorization_code"
|
29 |
+
platformAuth0Url = api.Auth0Url + "/authorize?"
|
30 |
+
getTokenUrl = api.Auth0Url + "/oauth/token"
|
31 |
+
auth0Client = "eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMjEuMCJ9" // '{"name":"auth0-spa-js","version":"1.21.0"}'
|
32 |
+
auth0LogoutUrl = api.Auth0Url + "/v2/logout?returnTo=https%3A%2F%2Fplatform.openai.com%2Floggedout&client_id=" + platformAuthClientID + "&auth0Client=" + auth0Client
|
33 |
+
dashboardLoginUrl = "https://api.openai.com/dashboard/onboarding/login"
|
34 |
+
getSessionKeyErrorMessage = "Failed to get session key."
|
35 |
+
)
|
api/platform/typings.go
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package platform
|
2 |
+
|
3 |
+
//goland:noinspection GoSnakeCaseUsage
|
4 |
+
import tls_client "github.com/bogdanfinn/tls-client"
|
5 |
+
|
6 |
+
type UserLogin struct {
|
7 |
+
client tls_client.HttpClient
|
8 |
+
}
|
9 |
+
|
10 |
+
type GetAccessTokenRequest struct {
|
11 |
+
ClientID string `json:"client_id"`
|
12 |
+
GrantType string `json:"grant_type"`
|
13 |
+
Code string `json:"code"`
|
14 |
+
RedirectURI string `json:"redirect_uri"`
|
15 |
+
}
|
16 |
+
|
17 |
+
type GetAccessTokenResponse struct {
|
18 |
+
AccessToken string `json:"access_token"`
|
19 |
+
RefreshToken string `json:"refresh_token"`
|
20 |
+
IDToken string `json:"id_token"`
|
21 |
+
Scope string `json:"scope"`
|
22 |
+
ExpiresIn int `json:"expires_in"`
|
23 |
+
TokenType string `json:"token_type"`
|
24 |
+
}
|
25 |
+
|
26 |
+
//goland:noinspection SpellCheckingInspection
|
27 |
+
type CreateCompletionsRequest struct {
|
28 |
+
Model string `json:"model"`
|
29 |
+
Prompt string `json:"prompt,omitempty"`
|
30 |
+
Suffix string `json:"suffix,omitempty"`
|
31 |
+
MaxTokens int `json:"max_tokens,omitempty"`
|
32 |
+
Temperature int `json:"temperature,omitempty"`
|
33 |
+
TopP int `json:"top_p,omitempty"`
|
34 |
+
N int `json:"n,omitempty"`
|
35 |
+
Stream bool `json:"stream,omitempty"`
|
36 |
+
Logprobs int `json:"logprobs,omitempty"`
|
37 |
+
Echo bool `json:"echo,omitempty"`
|
38 |
+
Stop string `json:"stop,omitempty"`
|
39 |
+
PresencePenalty int `json:"presence_penalty,omitempty"`
|
40 |
+
FrequencyPenalty int `json:"frequency_penalty,omitempty"`
|
41 |
+
BestOf int `json:"best_of,omitempty"`
|
42 |
+
LogitBias map[string]interface{} `json:"logit_bias,omitempty"`
|
43 |
+
User string `json:"user,omitempty"`
|
44 |
+
}
|
45 |
+
|
46 |
+
type ChatCompletionsRequest struct {
|
47 |
+
Model string `json:"model"`
|
48 |
+
Messages []ChatCompletionsMessage `json:"messages"`
|
49 |
+
Temperature int `json:"temperature,omitempty"`
|
50 |
+
TopP int `json:"top_p,omitempty"`
|
51 |
+
N int `json:"n,omitempty"`
|
52 |
+
Stream bool `json:"stream,omitempty"`
|
53 |
+
Stop string `json:"stop,omitempty"`
|
54 |
+
MaxTokens int `json:"max_tokens,omitempty"`
|
55 |
+
PresencePenalty int `json:"presence_penalty,omitempty"`
|
56 |
+
FrequencyPenalty int `json:"frequency_penalty,omitempty"`
|
57 |
+
LogitBias map[string]interface{} `json:"logit_bias,omitempty"`
|
58 |
+
User string `json:"user,omitempty"`
|
59 |
+
}
|
60 |
+
|
61 |
+
type ChatCompletionsMessage struct {
|
62 |
+
Role string `json:"role"`
|
63 |
+
Content string `json:"content"`
|
64 |
+
Name string `json:"name,omitempty"`
|
65 |
+
}
|
66 |
+
|
67 |
+
type CreateEditRequest struct {
|
68 |
+
Model string `json:"model"`
|
69 |
+
Input string `json:"input"`
|
70 |
+
Instruction string `json:"instruction"`
|
71 |
+
N int `json:"n,omitempty"`
|
72 |
+
Temperature int `json:"temperature,omitempty"`
|
73 |
+
TopP int `json:"top_p,omitempty"`
|
74 |
+
}
|
75 |
+
|
76 |
+
type CreateImageRequest struct {
|
77 |
+
Prompt string `json:"prompt"`
|
78 |
+
N int `json:"n,omitempty"`
|
79 |
+
Size string `json:"size,omitempty"`
|
80 |
+
ResponseFormat string `json:"response_format,omitempty"`
|
81 |
+
User string `json:"user,omitempty"`
|
82 |
+
}
|
83 |
+
|
84 |
+
type CreateEmbeddingsRequest struct {
|
85 |
+
Model string `json:"model"`
|
86 |
+
Input string `json:"input"`
|
87 |
+
User string `json:"user,omitempty"`
|
88 |
+
}
|
89 |
+
|
90 |
+
type CreateModerationRequest struct {
|
91 |
+
Model string `json:"model"`
|
92 |
+
Input string `json:"input"`
|
93 |
+
}
|
compose.yaml
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
- TZ=Asia/Shanghai
|
10 |
+
- GO_CHATGPT_API_PROXY=
|
11 |
+
restart: unless-stopped
|
env/env.go
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package env
|
2 |
+
|
3 |
+
import (
|
4 |
+
"github.com/joho/godotenv"
|
5 |
+
)
|
6 |
+
|
7 |
+
//goland:noinspection GoUnhandledErrorResult
|
8 |
+
func init() {
|
9 |
+
godotenv.Load()
|
10 |
+
}
|
example/chatgpt.http
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### login
|
2 |
+
POST http://127.0.0.1:8080/chatgpt/login
|
3 |
+
Content-Type: application/json
|
4 |
+
|
5 |
+
{
|
6 |
+
"username": "{{username}}",
|
7 |
+
"password": "{{password}}"
|
8 |
+
}
|
9 |
+
|
10 |
+
### get conversations
|
11 |
+
GET http://127.0.0.1:8080/chatgpt/conversations
|
12 |
+
Authorization: Bearer {{accessToken}}
|
13 |
+
|
14 |
+
### create conversation
|
15 |
+
POST http://127.0.0.1:8080/chatgpt/conversation
|
16 |
+
Authorization: Bearer {{accessToken}}
|
17 |
+
Content-Type: application/json
|
18 |
+
Accept: text/event-stream
|
19 |
+
|
20 |
+
{
|
21 |
+
"action": "next",
|
22 |
+
"messages": [
|
23 |
+
{
|
24 |
+
"id": "{{$random.uuid}}",
|
25 |
+
"author": {
|
26 |
+
"role": "user"
|
27 |
+
},
|
28 |
+
"content": {
|
29 |
+
"content_type": "text",
|
30 |
+
"parts": [
|
31 |
+
"hello"
|
32 |
+
]
|
33 |
+
},
|
34 |
+
"metadata": {}
|
35 |
+
}
|
36 |
+
],
|
37 |
+
"model": "gpt-4",
|
38 |
+
"timezone_offset_min": -480,
|
39 |
+
"history_and_training_disabled": false
|
40 |
+
}
|
41 |
+
|
42 |
+
> {%
|
43 |
+
response.body.onEachLine((data) => {
|
44 |
+
client.log(data.toString());
|
45 |
+
})
|
46 |
+
%}
|
47 |
+
|
48 |
+
### get models
|
49 |
+
GET http://127.0.0.1:8080/chatgpt/models
|
50 |
+
Authorization: Bearer {{accessToken}}
|
51 |
+
|
52 |
+
### check account
|
53 |
+
GET http://127.0.0.1:8080/chatgpt/accounts/check
|
54 |
+
Authorization: Bearer {{accessToken}}
|
example/http-client.env.json
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"dev": {
|
3 |
+
"username": "",
|
4 |
+
"password": "",
|
5 |
+
"accessToken": "",
|
6 |
+
"apiKey": ""
|
7 |
+
}
|
8 |
+
}
|
example/platform.http
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### login
|
2 |
+
POST http://127.0.0.1:8080/platform/login
|
3 |
+
Content-Type: application/json
|
4 |
+
|
5 |
+
{
|
6 |
+
"username": "{{username}}",
|
7 |
+
"password": "{{password}}"
|
8 |
+
}
|
go.mod
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module github.com/linweiyuan/go-chatgpt-api
|
2 |
+
|
3 |
+
go 1.20
|
4 |
+
|
5 |
+
require (
|
6 |
+
github.com/PuerkitoBio/goquery v1.8.1
|
7 |
+
github.com/bogdanfinn/fhttp v0.5.22
|
8 |
+
github.com/bogdanfinn/tls-client v1.3.11
|
9 |
+
github.com/gin-gonic/gin v1.9.1
|
10 |
+
github.com/joho/godotenv v1.5.1
|
11 |
+
github.com/sirupsen/logrus v1.9.0
|
12 |
+
)
|
13 |
+
|
14 |
+
require (
|
15 |
+
github.com/andybalholm/brotli v1.0.4 // indirect
|
16 |
+
github.com/andybalholm/cascadia v1.3.1 // indirect
|
17 |
+
github.com/bogdanfinn/utls v1.5.16 // indirect
|
18 |
+
github.com/bytedance/sonic v1.9.1 // indirect
|
19 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
20 |
+
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
21 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
22 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
23 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
24 |
+
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
25 |
+
github.com/goccy/go-json v0.10.2 // indirect
|
26 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
27 |
+
github.com/klauspost/compress v1.15.12 // indirect
|
28 |
+
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
29 |
+
github.com/leodido/go-urn v1.2.4 // indirect
|
30 |
+
github.com/mattn/go-isatty v0.0.19 // indirect
|
31 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
32 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
33 |
+
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
34 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
|
35 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
36 |
+
github.com/ugorji/go/codec v1.2.11 // indirect
|
37 |
+
golang.org/x/arch v0.3.0 // indirect
|
38 |
+
golang.org/x/crypto v0.9.0 // indirect
|
39 |
+
golang.org/x/net v0.10.0 // indirect
|
40 |
+
golang.org/x/sys v0.8.0 // indirect
|
41 |
+
golang.org/x/text v0.9.0 // indirect
|
42 |
+
google.golang.org/protobuf v1.30.0 // indirect
|
43 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
44 |
+
)
|
go.sum
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
4 |
+
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
5 |
+
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
6 |
+
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
7 |
+
github.com/bogdanfinn/fhttp v0.5.22 h1:U1jhZRtuaOanWWcm1WdMFnwMvSxUQgvO6berqAVTc5o=
|
8 |
+
github.com/bogdanfinn/fhttp v0.5.22/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I=
|
9 |
+
github.com/bogdanfinn/tls-client v1.3.11 h1:3rI+ysCEtnLdmDYlL7cPq2kF3Sj+bSvhgaHPmjLbjOE=
|
10 |
+
github.com/bogdanfinn/tls-client v1.3.11/go.mod h1:+TLNqnOtUQmYu/qrd/qMuLleoHoTRCcZazkmvNYuiVc=
|
11 |
+
github.com/bogdanfinn/utls v1.5.16 h1:NhhWkegEcYETBMj9nvgO4lwvc6NcLH+znrXzO3gnw4M=
|
12 |
+
github.com/bogdanfinn/utls v1.5.16/go.mod h1:mHeRCi69cUiEyVBkKONB1cAbLjRcZnlJbGzttmiuK4o=
|
13 |
+
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
14 |
+
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
15 |
+
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
16 |
+
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
17 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
18 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
19 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
20 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
21 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
22 |
+
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
23 |
+
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
24 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
25 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
26 |
+
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
27 |
+
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
28 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
29 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
30 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
31 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
32 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
33 |
+
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
34 |
+
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
35 |
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
36 |
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
37 |
+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
38 |
+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
39 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
40 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
41 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
42 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
43 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
44 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
45 |
+
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
46 |
+
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
47 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
48 |
+
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
49 |
+
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
50 |
+
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
51 |
+
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
52 |
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
53 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
54 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
55 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
56 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
57 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
58 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
59 |
+
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
60 |
+
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
61 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
62 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
63 |
+
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
64 |
+
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
65 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
66 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
67 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
68 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
69 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
70 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
71 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
72 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
73 |
+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
74 |
+
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
75 |
+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
76 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
|
77 |
+
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
|
78 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
79 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
80 |
+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
81 |
+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
82 |
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
83 |
+
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
84 |
+
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
85 |
+
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
86 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
87 |
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
88 |
+
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
89 |
+
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
90 |
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
91 |
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
92 |
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
93 |
+
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
94 |
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
95 |
+
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
96 |
+
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
97 |
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
98 |
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
99 |
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
100 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
101 |
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
102 |
+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
103 |
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
104 |
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
105 |
+
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
106 |
+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
107 |
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
108 |
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
109 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
110 |
+
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
111 |
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
112 |
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
113 |
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
114 |
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
115 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
116 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
117 |
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
118 |
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
119 |
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
120 |
+
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
121 |
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
122 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
123 |
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
124 |
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
125 |
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
126 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
127 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
128 |
+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
129 |
+
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
130 |
+
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
131 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
132 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
133 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
134 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
135 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
136 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
main.go
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package main
|
2 |
+
|
3 |
+
import (
|
4 |
+
"log"
|
5 |
+
"net/http"
|
6 |
+
"os"
|
7 |
+
"strings"
|
8 |
+
|
9 |
+
"github.com/gin-gonic/gin"
|
10 |
+
"github.com/linweiyuan/go-chatgpt-api/api/chatgpt"
|
11 |
+
"github.com/linweiyuan/go-chatgpt-api/api/platform"
|
12 |
+
_ "github.com/linweiyuan/go-chatgpt-api/env"
|
13 |
+
"github.com/linweiyuan/go-chatgpt-api/middleware"
|
14 |
+
)
|
15 |
+
|
16 |
+
func init() {
|
17 |
+
gin.ForceConsoleColor()
|
18 |
+
gin.SetMode(gin.ReleaseMode)
|
19 |
+
}
|
20 |
+
|
21 |
+
func main() {
|
22 |
+
router := gin.Default()
|
23 |
+
router.Use(middleware.CORSMiddleware())
|
24 |
+
router.Use(middleware.CheckHeaderMiddleware())
|
25 |
+
|
26 |
+
setupChatGPTAPIs(router)
|
27 |
+
|
28 |
+
setupPlatformAPIs(router)
|
29 |
+
|
30 |
+
router.NoRoute(handleFallbackRoute)
|
31 |
+
|
32 |
+
port := os.Getenv("GO_CHATGPT_API_PORT")
|
33 |
+
if port == "" {
|
34 |
+
port = "8080"
|
35 |
+
}
|
36 |
+
err := router.Run(":" + port)
|
37 |
+
if err != nil {
|
38 |
+
log.Fatal("Failed to start server: " + err.Error())
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
func setupChatGPTAPIs(router *gin.Engine) {
|
43 |
+
chatgptGroup := router.Group("/chatgpt")
|
44 |
+
{
|
45 |
+
chatgptGroup.POST("/login", chatgpt.Login)
|
46 |
+
|
47 |
+
conversationsGroup := chatgptGroup.Group("/conversations")
|
48 |
+
{
|
49 |
+
conversationsGroup.GET("", chatgpt.GetConversations)
|
50 |
+
|
51 |
+
// PATCH is official method, POST is added for Java support
|
52 |
+
conversationsGroup.PATCH("", chatgpt.ClearConversations)
|
53 |
+
conversationsGroup.POST("", chatgpt.ClearConversations)
|
54 |
+
}
|
55 |
+
|
56 |
+
conversationGroup := chatgptGroup.Group("/conversation")
|
57 |
+
{
|
58 |
+
conversationGroup.POST("", chatgpt.CreateConversation)
|
59 |
+
conversationGroup.POST("/gen_title/:id", chatgpt.GenerateTitle)
|
60 |
+
conversationGroup.GET("/:id", chatgpt.GetConversation)
|
61 |
+
|
62 |
+
// rename or delete conversation use a same API with different parameters
|
63 |
+
conversationGroup.PATCH("/:id", chatgpt.UpdateConversation)
|
64 |
+
conversationGroup.POST("/:id", chatgpt.UpdateConversation)
|
65 |
+
|
66 |
+
conversationGroup.POST("/message_feedback", chatgpt.FeedbackMessage)
|
67 |
+
}
|
68 |
+
|
69 |
+
// misc
|
70 |
+
chatgptGroup.GET("/models", chatgpt.GetModels)
|
71 |
+
chatgptGroup.GET("/accounts/check", chatgpt.GetAccountCheck)
|
72 |
+
}
|
73 |
+
}
|
74 |
+
|
75 |
+
func setupPlatformAPIs(router *gin.Engine) {
|
76 |
+
platformGroup := router.Group("/platform")
|
77 |
+
{
|
78 |
+
platformGroup.POST("/login", platform.Login)
|
79 |
+
|
80 |
+
apiGroup := platformGroup.Group("/v1")
|
81 |
+
{
|
82 |
+
apiGroup.GET("/models", platform.ListModels)
|
83 |
+
apiGroup.GET("/models/:model", platform.RetrieveModel)
|
84 |
+
apiGroup.POST("/completions", platform.CreateCompletions)
|
85 |
+
apiGroup.POST("/chat/completions", platform.CreateChatCompletions)
|
86 |
+
apiGroup.POST("/edits", platform.CreateEdit)
|
87 |
+
apiGroup.POST("/images/generations", platform.CreateImage)
|
88 |
+
apiGroup.POST("/embeddings", platform.CreateEmbeddings)
|
89 |
+
apiGroup.GET("/files", platform.ListFiles)
|
90 |
+
apiGroup.POST("/moderations", platform.CreateModeration)
|
91 |
+
}
|
92 |
+
|
93 |
+
dashboardGroup := platformGroup.Group("/dashboard")
|
94 |
+
{
|
95 |
+
billingGroup := dashboardGroup.Group("/billing")
|
96 |
+
{
|
97 |
+
billingGroup.GET("/credit_grants", platform.GetCreditGrants)
|
98 |
+
billingGroup.GET("/subscription", platform.GetSubscription)
|
99 |
+
}
|
100 |
+
|
101 |
+
userGroup := dashboardGroup.Group("/user")
|
102 |
+
{
|
103 |
+
userGroup.GET("/api_keys", platform.GetApiKeys)
|
104 |
+
}
|
105 |
+
}
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
func handleFallbackRoute(c *gin.Context) {
|
110 |
+
path := c.Request.URL.Path
|
111 |
+
|
112 |
+
if strings.HasPrefix(path, "/chatgpt") {
|
113 |
+
trimmedPath := strings.TrimPrefix(path, "/chatgpt")
|
114 |
+
c.Request.URL.Path = trimmedPath
|
115 |
+
chatgpt.Fallback(c)
|
116 |
+
} else {
|
117 |
+
c.JSON(http.StatusNotFound, gin.H{"message": "Route not found"})
|
118 |
+
}
|
119 |
+
}
|
middleware/check_header.go
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package middleware
|
2 |
+
|
3 |
+
import (
|
4 |
+
"net/http"
|
5 |
+
|
6 |
+
"github.com/gin-gonic/gin"
|
7 |
+
"github.com/linweiyuan/go-chatgpt-api/api"
|
8 |
+
)
|
9 |
+
|
10 |
+
func CheckHeaderMiddleware() gin.HandlerFunc {
|
11 |
+
return func(c *gin.Context) {
|
12 |
+
if c.GetHeader(api.AuthorizationHeader) == "" &&
|
13 |
+
c.Request.URL.Path != "/chatgpt/login" &&
|
14 |
+
c.Request.URL.Path != "/platform/login" {
|
15 |
+
c.AbortWithStatusJSON(http.StatusUnauthorized, api.ReturnMessage("Missing accessToken."))
|
16 |
+
return
|
17 |
+
}
|
18 |
+
|
19 |
+
c.Header("Content-Type", "application/json")
|
20 |
+
c.Next()
|
21 |
+
}
|
22 |
+
}
|
middleware/cors.go
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package middleware
|
2 |
+
|
3 |
+
import (
|
4 |
+
"github.com/gin-gonic/gin"
|
5 |
+
|
6 |
+
http "github.com/bogdanfinn/fhttp"
|
7 |
+
)
|
8 |
+
|
9 |
+
func CORSMiddleware() gin.HandlerFunc {
|
10 |
+
return func(c *gin.Context) {
|
11 |
+
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
12 |
+
c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
|
13 |
+
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
|
14 |
+
|
15 |
+
if c.Request.Method == http.MethodOptions {
|
16 |
+
c.AbortWithStatus(http.StatusNoContent)
|
17 |
+
return
|
18 |
+
}
|
19 |
+
|
20 |
+
c.Next()
|
21 |
+
}
|
22 |
+
}
|
util/logger/logger.go
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package logger
|
2 |
+
|
3 |
+
import (
|
4 |
+
"fmt"
|
5 |
+
|
6 |
+
"github.com/sirupsen/logrus"
|
7 |
+
)
|
8 |
+
|
9 |
+
func init() {
|
10 |
+
logrus.SetFormatter(&logrus.TextFormatter{
|
11 |
+
ForceColors: true,
|
12 |
+
})
|
13 |
+
}
|
14 |
+
|
15 |
+
func Ansi(colorString string) func(...interface{}) string {
|
16 |
+
return func(args ...interface{}) string {
|
17 |
+
return fmt.Sprintf(colorString, fmt.Sprint(args...))
|
18 |
+
}
|
19 |
+
}
|
20 |
+
|
21 |
+
var (
|
22 |
+
Green = Ansi("\033[1;32m%s\033[0m")
|
23 |
+
Yellow = Ansi("\033[1;33m%s\033[0m")
|
24 |
+
Red = Ansi("\033[1;31m%s\033[0m")
|
25 |
+
)
|
26 |
+
|
27 |
+
func Info(msg string) {
|
28 |
+
logrus.Info(Green(msg))
|
29 |
+
}
|
30 |
+
|
31 |
+
func Warn(msg string) {
|
32 |
+
logrus.Warn(Yellow(msg))
|
33 |
+
}
|
34 |
+
|
35 |
+
func Error(msg string) {
|
36 |
+
logrus.Error(Red(msg))
|
37 |
+
}
|