gitdeem commited on
Commit
8181ee0
·
verified ·
1 Parent(s): ec8fa06

Upload 34 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts AS BUILD_IMAGE
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8
+
9
+ FROM node:lts-alpine
10
+
11
+ COPY --from=BUILD_IMAGE /app/public /app/public
12
+ COPY --from=BUILD_IMAGE /app/configs /app/configs
13
+ COPY --from=BUILD_IMAGE /app/package.json /app/package.json
14
+ COPY --from=BUILD_IMAGE /app/dist /app/dist
15
+ COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16
+
17
+ WORKDIR /app
18
+ RUN mkdir /app/logs
19
+ RUN chmod -R 777 /app/logs
20
+ EXPOSE 8000
21
+
22
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
- title: Kimi
3
  emoji: 👀
4
- colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: kimi
3
  emoji: 👀
4
+ colorFrom: pink
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ app_port: 8000
10
  ---
 
 
configs/dev/service.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # 服务名称
2
+ name: kimi-free-api
3
+ # 服务绑定主机地址
4
+ host: '0.0.0.0'
5
+ # 服务绑定端口
6
+ port: 8000
configs/dev/system.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 是否开启请求日志
2
+ requestLog: true
3
+ # 临时目录路径
4
+ tmpDir: ./tmp
5
+ # 日志目录路径
6
+ logDir: ./logs
7
+ # 日志写入间隔(毫秒)
8
+ logWriteInterval: 200
9
+ # 日志文件有效期(毫秒)
10
+ logFileExpires: 2626560000
11
+ # 公共目录路径
12
+ publicDir: ./public
13
+ # 临时文件有效期(毫秒)
14
+ tmpFileExpires: 86400000
package.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "kimi-free-api",
3
+ "version": "0.0.34",
4
+ "description": "Kimi Free API Server",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "directories": {
10
+ "dist": "dist"
11
+ },
12
+ "files": [
13
+ "dist/"
14
+ ],
15
+ "scripts": {
16
+ "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17
+ "start": "node dist/index.js",
18
+ "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19
+ },
20
+ "author": "Vinlic",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "axios": "^1.6.7",
24
+ "colors": "^1.4.0",
25
+ "crc-32": "^1.2.2",
26
+ "cron": "^3.1.6",
27
+ "date-fns": "^3.3.1",
28
+ "eventsource-parser": "^1.1.2",
29
+ "fs-extra": "^11.2.0",
30
+ "koa": "^2.15.0",
31
+ "koa-body": "^5.0.0",
32
+ "koa-bodyparser": "^4.4.1",
33
+ "koa-range": "^0.3.0",
34
+ "koa-router": "^12.0.1",
35
+ "koa2-cors": "^2.0.6",
36
+ "lodash": "^4.17.21",
37
+ "mime": "^4.0.1",
38
+ "minimist": "^1.2.8",
39
+ "randomstring": "^1.3.0",
40
+ "uuid": "^9.0.1",
41
+ "yaml": "^2.3.4"
42
+ },
43
+ "devDependencies": {
44
+ "@types/lodash": "^4.14.202",
45
+ "@types/mime": "^3.0.4",
46
+ "tsup": "^8.0.2",
47
+ "typescript": "^5.3.3"
48
+ }
49
+ }
public/welcome.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>🚀 服务已启动</title>
6
+ </head>
7
+ <body>
8
+ <p>kimi-free-api已启动!<br>请通过LobeChat / NextChat / Dify等客户端或OpenAI SDK接入!</p>
9
+ </body>
10
+ </html>
src/api/consts/exceptions.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+ API_TEST: [-9999, 'API异常错误'],
3
+ API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4
+ API_REQUEST_FAILED: [-2001, '请求失败'],
5
+ API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6
+ API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7
+ API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8
+ API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出']
9
+ }
src/api/controllers/chat.ts ADDED
@@ -0,0 +1,920 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PassThrough } from "stream";
2
+ import path from 'path';
3
+ import _ from 'lodash';
4
+ import mime from 'mime';
5
+ import axios, { AxiosResponse } from 'axios';
6
+
7
+ import APIException from "@/lib/exceptions/APIException.ts";
8
+ import EX from "@/api/consts/exceptions.ts";
9
+ import { createParser } from 'eventsource-parser'
10
+ import logger from '@/lib/logger.ts';
11
+ import util from '@/lib/util.ts';
12
+
13
+ // 模型名称
14
+ const MODEL_NAME = 'kimi';
15
+ // access_token有效期
16
+ const ACCESS_TOKEN_EXPIRES = 300;
17
+ // 最大重试次数
18
+ const MAX_RETRY_COUNT = 3;
19
+ // 重试延迟
20
+ const RETRY_DELAY = 5000;
21
+ // 伪装headers
22
+ const FAKE_HEADERS = {
23
+ 'Accept': '*/*',
24
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
25
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
26
+ 'Origin': 'https://kimi.moonshot.cn',
27
+ 'Cookie': util.generateCookie(),
28
+ 'R-Timezone': 'Asia/Shanghai',
29
+ 'Sec-Ch-Ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
30
+ 'Sec-Ch-Ua-Mobile': '?0',
31
+ 'Sec-Ch-Ua-Platform': '"Windows"',
32
+ 'Sec-Fetch-Dest': 'empty',
33
+ 'Sec-Fetch-Mode': 'cors',
34
+ 'Sec-Fetch-Site': 'same-origin',
35
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
36
+ };
37
+ // 文件最大大小
38
+ const FILE_MAX_SIZE = 100 * 1024 * 1024;
39
+ // access_token映射
40
+ const accessTokenMap = new Map();
41
+ // access_token请求队列映射
42
+ const accessTokenRequestQueueMap: Record<string, Function[]> = {};
43
+
44
+ /**
45
+ * 请求access_token
46
+ *
47
+ * 使用refresh_token去刷新获得access_token
48
+ *
49
+ * @param refreshToken 用于刷新access_token的refresh_token
50
+ */
51
+ async function requestToken(refreshToken: string) {
52
+ if (accessTokenRequestQueueMap[refreshToken])
53
+ return new Promise(resolve => accessTokenRequestQueueMap[refreshToken].push(resolve));
54
+ accessTokenRequestQueueMap[refreshToken] = [];
55
+ logger.info(`Refresh token: ${refreshToken}`);
56
+ const result = await (async () => {
57
+ const result = await axios.get('https://kimi.moonshot.cn/api/auth/token/refresh', {
58
+ headers: {
59
+ Accept: '*/*',
60
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
61
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
62
+ Authorization: `Bearer ${refreshToken}`,
63
+ 'Cache-Control': 'no-cache',
64
+ 'Cookie': util.generateCookie(),
65
+ Pragma: 'no-cache',
66
+ Referer: 'https://kimi.moonshot.cn/',
67
+ 'Sec-Ch-Ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
68
+ 'Sec-Ch-Ua-Mobile': '?0',
69
+ 'Sec-Ch-Ua-Platform': '"Windows"',
70
+ 'Sec-Fetch-Dest': 'empty',
71
+ 'Sec-Fetch-Mode': 'cors',
72
+ 'Sec-Fetch-Site': 'same-origin',
73
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
74
+ },
75
+ timeout: 15000,
76
+ validateStatus: () => true
77
+ });
78
+ const {
79
+ access_token,
80
+ refresh_token
81
+ } = checkResult(result, refreshToken);
82
+ const { id: userId } = await getUserInfo(access_token, refreshToken);
83
+ return {
84
+ userId,
85
+ accessToken: access_token,
86
+ refreshToken: refresh_token,
87
+ refreshTime: util.unixTimestamp() + ACCESS_TOKEN_EXPIRES
88
+ }
89
+ })()
90
+ .then(result => {
91
+ if (accessTokenRequestQueueMap[refreshToken]) {
92
+ accessTokenRequestQueueMap[refreshToken].forEach(resolve => resolve(result));
93
+ delete accessTokenRequestQueueMap[refreshToken];
94
+ }
95
+ logger.success(`Refresh successful`);
96
+ return result;
97
+ })
98
+ .catch(err => {
99
+ if (accessTokenRequestQueueMap[refreshToken]) {
100
+ accessTokenRequestQueueMap[refreshToken].forEach(resolve => resolve(err));
101
+ delete accessTokenRequestQueueMap[refreshToken];
102
+ }
103
+ return err;
104
+ });
105
+ if (_.isError(result))
106
+ throw result;
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * 获取缓存中的access_token
112
+ *
113
+ * 避免短时间大量刷新token,未加锁,如果有并发要求还需加锁
114
+ *
115
+ * @param refreshToken 用于刷新access_token的refresh_token
116
+ */
117
+ async function acquireToken(refreshToken: string): Promise<any> {
118
+ let result = accessTokenMap.get(refreshToken);
119
+ if (!result) {
120
+ result = await requestToken(refreshToken);
121
+ accessTokenMap.set(refreshToken, result);
122
+ }
123
+ if (util.unixTimestamp() > result.refreshTime) {
124
+ result = await requestToken(refreshToken);
125
+ accessTokenMap.set(refreshToken, result);
126
+ }
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * 获取用户信息
132
+ *
133
+ * @param refreshToken 用于刷新access_token的refresh_token
134
+ */
135
+ async function getUserInfo(accessToken: string, refreshToken: string) {
136
+ const result = await axios.get('https://kimi.moonshot.cn/api/user', {
137
+ headers: {
138
+ Authorization: `Bearer ${accessToken}`,
139
+ Referer: 'https://kimi.moonshot.cn/',
140
+ 'X-Msh-Platform': 'web',
141
+ 'X-Traffic-Id': `7${util.generateRandomString({ length: 18, charset: 'numeric' })}`,
142
+ ...FAKE_HEADERS
143
+ },
144
+ timeout: 15000,
145
+ validateStatus: () => true
146
+ });
147
+ return checkResult(result, refreshToken);
148
+ }
149
+
150
+ /**
151
+ * 创建会话
152
+ *
153
+ * 创建临时的会话用于对话补全
154
+ *
155
+ * @param refreshToken 用于刷新access_token的refresh_token
156
+ */
157
+ async function createConversation(model: string, name: string, refreshToken: string) {
158
+ const {
159
+ accessToken,
160
+ userId
161
+ } = await acquireToken(refreshToken);
162
+ const result = await axios.post('https://kimi.moonshot.cn/api/chat', {
163
+ born_from: '',
164
+ is_example: false,
165
+ kimiplus_id: /^[0-9a-z]{20}$/.test(model) ? model : 'kimi',
166
+ name
167
+ }, {
168
+ headers: {
169
+ Authorization: `Bearer ${accessToken}`,
170
+ Referer: 'https://kimi.moonshot.cn/',
171
+ 'X-Msh-Platform': 'web',
172
+ 'X-Traffic-Id': userId,
173
+ ...FAKE_HEADERS
174
+ },
175
+ timeout: 15000,
176
+ validateStatus: () => true
177
+ });
178
+ const {
179
+ id: convId
180
+ } = checkResult(result, refreshToken);
181
+ return convId;
182
+ }
183
+
184
+ /**
185
+ * 移除会话
186
+ *
187
+ * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中
188
+ *
189
+ * @param refreshToken 用于刷新access_token的refresh_token
190
+ */
191
+ async function removeConversation(convId: string, refreshToken: string) {
192
+ const {
193
+ accessToken,
194
+ userId
195
+ } = await acquireToken(refreshToken);
196
+ const result = await axios.delete(`https://kimi.moonshot.cn/api/chat/${convId}`, {
197
+ headers: {
198
+ Authorization: `Bearer ${accessToken}`,
199
+ Referer: `https://kimi.moonshot.cn/chat/${convId}`,
200
+ 'X-Msh-Platform': 'web',
201
+ 'X-Traffic-Id': userId,
202
+ ...FAKE_HEADERS
203
+ },
204
+ timeout: 15000,
205
+ validateStatus: () => true
206
+ });
207
+ checkResult(result, refreshToken);
208
+ }
209
+
210
+ /**
211
+ * prompt片段提交
212
+ *
213
+ * @param query prompt
214
+ * @param refreshToken 用于刷新access_token的refresh_token
215
+ */
216
+ async function promptSnippetSubmit(query: string, refreshToken: string) {
217
+ const {
218
+ accessToken,
219
+ userId
220
+ } = await acquireToken(refreshToken);
221
+ const result = await axios.post('https://kimi.moonshot.cn/api/prompt-snippet/instance', {
222
+ "offset": 0,
223
+ "size": 10,
224
+ "query": query.replace('user:', '').replace('assistant:', '')
225
+ }, {
226
+ headers: {
227
+ Authorization: `Bearer ${accessToken}`,
228
+ Referer: 'https://kimi.moonshot.cn/',
229
+ 'X-Msh-Platform': 'web',
230
+ 'X-Traffic-Id': userId,
231
+ ...FAKE_HEADERS
232
+ },
233
+ timeout: 15000,
234
+ validateStatus: () => true
235
+ });
236
+ checkResult(result, refreshToken);
237
+ }
238
+
239
+ /**
240
+ * 同步对话补全
241
+ *
242
+ * @param model 模型名称
243
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
244
+ * @param refreshToken 用于刷新access_token的refresh_token
245
+ * @param useSearch 是否开启联网搜索
246
+ * @param refConvId 引用会话ID
247
+ * @param retryCount 重试次数
248
+ */
249
+ async function createCompletion(model = MODEL_NAME, messages: any[], refreshToken: string, useSearch = true, refConvId?: string, retryCount = 0) {
250
+ return (async () => {
251
+ logger.info(messages);
252
+
253
+ // 提取引用文件URL并上传kimi获得引用的文件ID列表
254
+ const refFileUrls = extractRefFileUrls(messages);
255
+ const refs = refFileUrls.length ? await Promise.all(refFileUrls.map(fileUrl => uploadFile(fileUrl, refreshToken))) : [];
256
+
257
+ // 伪装调用获取用户信息
258
+ fakeRequest(refreshToken)
259
+ .catch(err => logger.error(err));
260
+
261
+ // 创建会话
262
+ const convId = /[0-9a-zA-Z]{20}/.test(refConvId) ? refConvId : await createConversation(model, "未命名会话", refreshToken);
263
+
264
+ // 请求流
265
+ const {
266
+ accessToken,
267
+ userId
268
+ } = await acquireToken(refreshToken);
269
+ const sendMessages = messagesPrepare(messages, !!refConvId);
270
+ const result = await axios.post(`https://kimi.moonshot.cn/api/chat/${convId}/completion/stream`, {
271
+ kimiplus_id: /^[0-9a-z]{20}$/.test(model) ? model : 'kimi',
272
+ messages: sendMessages,
273
+ refs,
274
+ is_pro_search: false,
275
+ use_search: useSearch
276
+ }, {
277
+ headers: {
278
+ Authorization: `Bearer ${accessToken}`,
279
+ Referer: `https://kimi.moonshot.cn/chat/${convId}`,
280
+ 'Priority': 'u=1, i',
281
+ 'X-Msh-Platform': 'web',
282
+ 'X-Traffic-Id': userId,
283
+ ...FAKE_HEADERS
284
+ },
285
+ // 120秒超时
286
+ timeout: 120000,
287
+ validateStatus: () => true,
288
+ responseType: 'stream'
289
+ });
290
+
291
+ const streamStartTime = util.timestamp();
292
+ // 接收流为输出文本
293
+ const answer = await receiveStream(model, convId, result.data);
294
+ logger.success(`Stream has completed transfer ${util.timestamp() - streamStartTime}ms`);
295
+
296
+ // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
297
+ // 如果引用会话将不会清除,因为我们不知道什么时候你会结束会话
298
+ !refConvId && removeConversation(convId, refreshToken)
299
+ .catch(err => console.error(err));
300
+ promptSnippetSubmit(sendMessages[0].content, refreshToken)
301
+ .catch(err => console.error(err));
302
+
303
+ return answer;
304
+ })()
305
+ .catch(err => {
306
+ if (retryCount < MAX_RETRY_COUNT) {
307
+ logger.error(`Stream response error: ${err.message}`);
308
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
309
+ return (async () => {
310
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
311
+ return createCompletion(model, messages, refreshToken, useSearch, refConvId, retryCount + 1);
312
+ })();
313
+ }
314
+ throw err;
315
+ });
316
+ }
317
+
318
+ /**
319
+ * 流式对话补全
320
+ *
321
+ * @param model 模型名称
322
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
323
+ * @param refreshToken 用于刷新access_token的refresh_token
324
+ * @param useSearch 是否开启联网搜索
325
+ * @param refConvId 引用会话ID
326
+ * @param retryCount 重试次数
327
+ */
328
+ async function createCompletionStream(model = MODEL_NAME, messages: any[], refreshToken: string, useSearch = true, refConvId?: string, retryCount = 0) {
329
+ return (async () => {
330
+ logger.info(messages);
331
+
332
+ // 提取引用文件URL并上传kimi获得引用的文件ID列表
333
+ const refFileUrls = extractRefFileUrls(messages);
334
+ const refs = refFileUrls.length ? await Promise.all(refFileUrls.map(fileUrl => uploadFile(fileUrl, refreshToken))) : [];
335
+
336
+ // 伪装调用获取用户信息
337
+ fakeRequest(refreshToken)
338
+ .catch(err => logger.error(err));
339
+
340
+ // 创建会话
341
+ const convId = /[0-9a-zA-Z]{20}/.test(refConvId) ? refConvId : await createConversation(model, "未命名会话", refreshToken);
342
+
343
+ // 请求流
344
+ const {
345
+ accessToken,
346
+ userId
347
+ } = await acquireToken(refreshToken);
348
+ const sendMessages = messagesPrepare(messages, !!refConvId);
349
+ const result = await axios.post(`https://kimi.moonshot.cn/api/chat/${convId}/completion/stream`, {
350
+ kimiplus_id: /^[0-9a-z]{20}$/.test(model) ? model : undefined,
351
+ messages: sendMessages,
352
+ refs,
353
+ use_search: useSearch
354
+ }, {
355
+ // 120秒超时
356
+ timeout: 120000,
357
+ headers: {
358
+ Authorization: `Bearer ${accessToken}`,
359
+ Referer: `https://kimi.moonshot.cn/chat/${convId}`,
360
+ 'Priority': 'u=1, i',
361
+ 'X-Msh-Platform': 'web',
362
+ 'X-Traffic-Id': userId,
363
+ ...FAKE_HEADERS
364
+ },
365
+ validateStatus: () => true,
366
+ responseType: 'stream'
367
+ });
368
+ const streamStartTime = util.timestamp();
369
+ // 创建转换流将消息格式转换为gpt兼容格式
370
+ return createTransStream(model, convId, result.data, () => {
371
+ logger.success(`Stream has completed transfer ${util.timestamp() - streamStartTime}ms`);
372
+ // 流传输结束后异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
373
+ // 如果引用会话将不会清除,因为我们不知道什么时候你会结束会话
374
+ !refConvId && removeConversation(convId, refreshToken)
375
+ .catch(err => console.error(err));
376
+ promptSnippetSubmit(sendMessages[0].content, refreshToken)
377
+ .catch(err => console.error(err));
378
+ });
379
+ })()
380
+ .catch(err => {
381
+ if (retryCount < MAX_RETRY_COUNT) {
382
+ logger.error(`Stream response error: ${err.message}`);
383
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
384
+ return (async () => {
385
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
386
+ return createCompletionStream(model, messages, refreshToken, useSearch, refConvId, retryCount + 1);
387
+ })();
388
+ }
389
+ throw err;
390
+ });
391
+ }
392
+
393
+ /**
394
+ * 调用一些接口伪装访问
395
+ *
396
+ * 随机挑一个
397
+ *
398
+ * @param refreshToken 用于刷新access_token的refresh_token
399
+ */
400
+ async function fakeRequest(refreshToken: string) {
401
+ const {
402
+ accessToken,
403
+ userId
404
+ } = await acquireToken(refreshToken);
405
+ const options = {
406
+ headers: {
407
+ Authorization: `Bearer ${accessToken}`,
408
+ Referer: `https://kimi.moonshot.cn/`,
409
+ 'X-Msh-Platform': 'web',
410
+ 'X-Traffic-Id': userId,
411
+ ...FAKE_HEADERS
412
+ }
413
+ };
414
+ await [
415
+ () => axios.get('https://kimi.moonshot.cn/api/user', options),
416
+ () => axios.get('https://kimi.moonshot.cn/api/chat_1m/user/status', options),
417
+ () => axios.post('https://kimi.moonshot.cn/api/chat/list', {
418
+ offset: 0,
419
+ size: 50
420
+ }, options),
421
+ () => axios.post('https://kimi.moonshot.cn/api/show_case/list', {
422
+ offset: 0,
423
+ size: 4,
424
+ enable_cache: true,
425
+ order: "asc"
426
+ }, options)
427
+ ][Math.floor(Math.random() * 4)]();
428
+ }
429
+
430
+ /**
431
+ * 提取消息中引用的文件URL
432
+ *
433
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
434
+ */
435
+ function extractRefFileUrls(messages: any[]) {
436
+ const urls = [];
437
+ // 如果没有消息,则返回[]
438
+ if (!messages.length) {
439
+ return urls;
440
+ }
441
+ // 只获取最新的消息
442
+ const lastMessage = messages[messages.length - 1];
443
+ if (_.isArray(lastMessage.content)) {
444
+ lastMessage.content.forEach(v => {
445
+ if (!_.isObject(v) || !['file', 'image_url'].includes(v['type']))
446
+ return;
447
+ // kimi-free-api支持格式
448
+ if (v['type'] == 'file' && _.isObject(v['file_url']) && _.isString(v['file_url']['url']))
449
+ urls.push(v['file_url']['url']);
450
+ // 兼容gpt-4-vision-preview API格式
451
+ else if (v['type'] == 'image_url' && _.isObject(v['image_url']) && _.isString(v['image_url']['url']))
452
+ urls.push(v['image_url']['url']);
453
+ });
454
+ }
455
+ logger.info("本次请求上传:" + urls.length + "个文件");
456
+ return urls;
457
+ }
458
+
459
+ /**
460
+ * 消息预处理
461
+ *
462
+ * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果
463
+ * user:旧消息1
464
+ * assistant:旧消息2
465
+ * user:新消息
466
+ *
467
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
468
+ * @param isRefConv 是否为引用会话
469
+ */
470
+ function messagesPrepare(messages: any[], isRefConv = false) {
471
+ let content;
472
+ if (isRefConv || messages.length < 2) {
473
+ content = messages.reduce((content, message) => {
474
+ if (_.isArray(message.content)) {
475
+ return message.content.reduce((_content, v) => {
476
+ if (!_.isObject(v) || v['type'] != 'text') return _content;
477
+ return _content + `${v["text"] || ""}\n`;
478
+ }, content);
479
+ }
480
+ return content += `${message.role == 'user' ? wrapUrlsToTags(message.content) : message.content}\n`;
481
+ }, '')
482
+ logger.info("\n透传内容:\n" + content);
483
+ }
484
+ else {
485
+ // 注入消息提升注意力
486
+ let latestMessage = messages[messages.length - 1];
487
+ let hasFileOrImage = Array.isArray(latestMessage.content)
488
+ && latestMessage.content.some(v => (typeof v === 'object' && ['file', 'image_url'].includes(v['type'])));
489
+ // 第二轮开始注入system prompt
490
+ if (hasFileOrImage) {
491
+ let newFileMessage = {
492
+ "content": "关注用户最新发送文件和消息",
493
+ "role": "system"
494
+ };
495
+ messages.splice(messages.length - 1, 0, newFileMessage);
496
+ logger.info("注入提升尾部文件注意力system prompt");
497
+ } else {
498
+ let newTextMessage = {
499
+ "content": "关注用户最新的消息",
500
+ "role": "system"
501
+ };
502
+ messages.splice(messages.length - 1, 0, newTextMessage);
503
+ logger.info("注入提升尾部消息注意力system prompt");
504
+ }
505
+ content = messages.reduce((content, message) => {
506
+ if (_.isArray(message.content)) {
507
+ return message.content.reduce((_content, v) => {
508
+ if (!_.isObject(v) || v['type'] != 'text') return _content;
509
+ return _content + `${message.role || "user"}:${v["text"] || ""}\n`;
510
+ }, content);
511
+ }
512
+ return content += `${message.role || "user"}:${message.role == 'user' ? wrapUrlsToTags(message.content) : message.content}\n`;
513
+ }, '')
514
+ logger.info("\n对话合并:\n" + content);
515
+ }
516
+
517
+ return [
518
+ { role: 'user', content }
519
+ ]
520
+ }
521
+
522
+ /**
523
+ * 将消息中的URL包装为HTML标签
524
+ *
525
+ * kimi网页版中会自动将url包装为url标签用于处理状态,此处也得模仿处理,否则无法成功解析
526
+ *
527
+ * @param content 消息内容
528
+ */
529
+ function wrapUrlsToTags(content: string) {
530
+ return content.replace(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi, url => `<url id="" type="url" status="" title="" wc="">${url}</url>`);
531
+ }
532
+
533
+ /**
534
+ * 获取预签名的文件URL
535
+ *
536
+ * @param filename 文件名称
537
+ * @param refreshToken 用于刷新access_token的refresh_token
538
+ */
539
+ async function preSignUrl(filename: string, refreshToken: string) {
540
+ const {
541
+ accessToken,
542
+ userId
543
+ } = await acquireToken(refreshToken);
544
+ const result = await axios.post('https://kimi.moonshot.cn/api/pre-sign-url', {
545
+ action: 'file',
546
+ name: filename
547
+ }, {
548
+ timeout: 15000,
549
+ headers: {
550
+ Authorization: `Bearer ${accessToken}`,
551
+ Referer: `https://kimi.moonshot.cn/`,
552
+ 'X-Msh-Platform': 'web',
553
+ 'X-Traffic-Id': userId,
554
+ ...FAKE_HEADERS
555
+ },
556
+ validateStatus: () => true
557
+ });
558
+ return checkResult(result, refreshToken);
559
+ }
560
+
561
+ /**
562
+ * 预检查文件URL有效性
563
+ *
564
+ * @param fileUrl 文件URL
565
+ */
566
+ async function checkFileUrl(fileUrl: string) {
567
+ if (util.isBASE64Data(fileUrl))
568
+ return;
569
+ const result = await axios.head(fileUrl, {
570
+ timeout: 15000,
571
+ validateStatus: () => true
572
+ });
573
+ if (result.status >= 400)
574
+ throw new APIException(EX.API_FILE_URL_INVALID, `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`);
575
+ // 检查文件大小
576
+ if (result.headers && result.headers['content-length']) {
577
+ const fileSize = parseInt(result.headers['content-length'], 10);
578
+ if (fileSize > FILE_MAX_SIZE)
579
+ throw new APIException(EX.API_FILE_EXECEEDS_SIZE, `File ${fileUrl} is not valid`);
580
+ }
581
+ }
582
+
583
+ /**
584
+ * 上传文件
585
+ *
586
+ * @param fileUrl 文件URL
587
+ * @param refreshToken 用于刷新access_token的refresh_token
588
+ */
589
+ async function uploadFile(fileUrl: string, refreshToken: string) {
590
+ // 预检查远程文件URL可用性
591
+ await checkFileUrl(fileUrl);
592
+
593
+ let filename, fileData, mimeType;
594
+ // 如果是BASE64数据则直接转换为Buffer
595
+ if (util.isBASE64Data(fileUrl)) {
596
+ mimeType = util.extractBASE64DataFormat(fileUrl);
597
+ const ext = mime.getExtension(mimeType);
598
+ filename = `${util.uuid()}.${ext}`;
599
+ fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), 'base64');
600
+ }
601
+ // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
602
+ else {
603
+ filename = path.basename(fileUrl);
604
+ ({ data: fileData } = await axios.get(fileUrl, {
605
+ responseType: 'arraybuffer',
606
+ // 100M限制
607
+ maxContentLength: FILE_MAX_SIZE,
608
+ // 60秒超时
609
+ timeout: 60000
610
+ }));
611
+ }
612
+
613
+ // 获取预签名文件URL
614
+ const {
615
+ url: uploadUrl,
616
+ object_name: objectName
617
+ } = await preSignUrl(filename, refreshToken);
618
+
619
+ // 获取文件的MIME类型
620
+ mimeType = mimeType || mime.getType(filename);
621
+ // 上传文件到目标OSS
622
+ const {
623
+ accessToken,
624
+ userId
625
+ } = await acquireToken(refreshToken);
626
+ let result = await axios.request({
627
+ method: 'PUT',
628
+ url: uploadUrl,
629
+ data: fileData,
630
+ // 100M限制
631
+ maxBodyLength: FILE_MAX_SIZE,
632
+ // 120秒超时
633
+ timeout: 120000,
634
+ headers: {
635
+ 'Content-Type': mimeType,
636
+ Authorization: `Bearer ${accessToken}`,
637
+ Referer: `https://kimi.moonshot.cn/`,
638
+ 'X-Msh-Platform': 'web',
639
+ 'X-Traffic-Id': userId,
640
+ ...FAKE_HEADERS
641
+ },
642
+ validateStatus: () => true
643
+ });
644
+ checkResult(result, refreshToken);
645
+
646
+ let fileId, status, startTime = Date.now();
647
+ while (status != 'initialized') {
648
+ if (Date.now() - startTime > 30000)
649
+ throw new Error('文件等待处理超时');
650
+ // 获取文件上传结果
651
+ result = await axios.post('https://kimi.moonshot.cn/api/file', {
652
+ type: 'file',
653
+ name: filename,
654
+ object_name: objectName,
655
+ timeout: 15000
656
+ }, {
657
+ headers: {
658
+ Authorization: `Bearer ${accessToken}`,
659
+ Referer: `https://kimi.moonshot.cn/`,
660
+ 'X-Msh-Platform': 'web',
661
+ 'X-Traffic-Id': userId,
662
+ ...FAKE_HEADERS
663
+ }
664
+ });
665
+ ({ id: fileId, status } = checkResult(result, refreshToken));
666
+ }
667
+
668
+ startTime = Date.now();
669
+ let parseFinish = false;
670
+ while (!parseFinish) {
671
+ if (Date.now() - startTime > 30000)
672
+ throw new Error('文件等待处理超时');
673
+ // 处理文件转换
674
+ parseFinish = await new Promise(resolve => {
675
+ axios.post('https://kimi.moonshot.cn/api/file/parse_process', {
676
+ ids: [fileId],
677
+ timeout: 120000
678
+ }, {
679
+ headers: {
680
+ Authorization: `Bearer ${accessToken}`,
681
+ Referer: `https://kimi.moonshot.cn/`,
682
+ 'X-Msh-Platform': 'web',
683
+ 'X-Traffic-Id': userId,
684
+ ...FAKE_HEADERS
685
+ }
686
+ })
687
+ .then(() => resolve(true))
688
+ .catch(() => resolve(false));
689
+ });
690
+ }
691
+
692
+ return fileId;
693
+ }
694
+
695
+ /**
696
+ * 检查请求结果
697
+ *
698
+ * @param result 结果
699
+ * @param refreshToken 用于刷新access_token的refresh_token
700
+ */
701
+ function checkResult(result: AxiosResponse, refreshToken: string) {
702
+ if (result.status == 401) {
703
+ accessTokenMap.delete(refreshToken);
704
+ throw new APIException(EX.API_REQUEST_FAILED);
705
+ }
706
+ if (!result.data)
707
+ return null;
708
+ const { error_type, message } = result.data;
709
+ if (!_.isString(error_type))
710
+ return result.data;
711
+ if (error_type == 'auth.token.invalid')
712
+ accessTokenMap.delete(refreshToken);
713
+ if (error_type == 'chat.user_stream_pushing')
714
+ throw new APIException(EX.API_CHAT_STREAM_PUSHING);
715
+ throw new APIException(EX.API_REQUEST_FAILED, `[请求kimi失败]: ${message}`);
716
+ }
717
+
718
+ /**
719
+ * 从流接收完整的消息内容
720
+ *
721
+ * @param model 模型名称
722
+ * @param convId 会话ID
723
+ * @param stream 消息流
724
+ */
725
+ async function receiveStream(model: string, convId: string, stream: any) {
726
+ return new Promise((resolve, reject) => {
727
+ // 消息初始化
728
+ const data = {
729
+ id: convId,
730
+ model,
731
+ object: 'chat.completion',
732
+ choices: [
733
+ { index: 0, message: { role: 'assistant', content: '' }, finish_reason: 'stop' }
734
+ ],
735
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
736
+ created: util.unixTimestamp()
737
+ };
738
+ let refContent = '';
739
+ const silentSearch = model.indexOf('silent_search') != -1;
740
+ const parser = createParser(event => {
741
+ try {
742
+ if (event.type !== "event") return;
743
+ // 解析JSON
744
+ const result = _.attempt(() => JSON.parse(event.data));
745
+ if (_.isError(result))
746
+ throw new Error(`Stream response invalid: ${event.data}`);
747
+ // 处理消息
748
+ if (result.event == 'cmpl' && result.text) {
749
+ const exceptCharIndex = result.text.indexOf("�");
750
+ data.choices[0].message.content += result.text.substring(0, exceptCharIndex == -1 ? result.text.length : exceptCharIndex);
751
+ }
752
+ // 处理结束或错误
753
+ else if (result.event == 'all_done' || result.event == 'error') {
754
+ data.choices[0].message.content += (result.event == 'error' ? '\n[内容由于不合规被停止生成,我们换个话题吧]' : '') + (refContent ? `\n\n搜索结果来自:\n${refContent}` : '');
755
+ refContent = '';
756
+ resolve(data);
757
+ }
758
+ // 处理联网搜索
759
+ else if (!silentSearch && result.event == 'search_plus' && result.msg && result.msg.type == 'get_res')
760
+ refContent += `${result.msg.title} - ${result.msg.url}\n`;
761
+ // else
762
+ // logger.warn(result.event, result);
763
+ }
764
+ catch (err) {
765
+ logger.error(err);
766
+ reject(err);
767
+ }
768
+ });
769
+ // 将流数据喂给SSE转换器
770
+ stream.on("data", buffer => parser.feed(buffer.toString()));
771
+ stream.once("error", err => reject(err));
772
+ stream.once("close", () => resolve(data));
773
+ });
774
+ }
775
+
776
+ /**
777
+ * 创建转换流
778
+ *
779
+ * 将流格式转换为gpt兼容流格式
780
+ *
781
+ * @param model 模型名称
782
+ * @param convId 会话ID
783
+ * @param stream 消息流
784
+ * @param endCallback 传输结束回调
785
+ */
786
+ function createTransStream(model: string, convId: string, stream: any, endCallback?: Function) {
787
+ // 消息创建时间
788
+ const created = util.unixTimestamp();
789
+ // 创建转换流
790
+ const transStream = new PassThrough();
791
+ let searchFlag = false;
792
+ const silentSearch = model.indexOf('silent_search') != -1;
793
+ !transStream.closed && transStream.write(`data: ${JSON.stringify({
794
+ id: convId,
795
+ model,
796
+ object: 'chat.completion.chunk',
797
+ choices: [
798
+ { index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null }
799
+ ],
800
+ created
801
+ })}\n\n`);
802
+ const parser = createParser(event => {
803
+ try {
804
+ if (event.type !== "event") return;
805
+ // 解析JSON
806
+ const result = _.attempt(() => JSON.parse(event.data));
807
+ if (_.isError(result))
808
+ throw new Error(`Stream response invalid: ${event.data}`);
809
+ // 处理消息
810
+ if (result.event == 'cmpl') {
811
+ const exceptCharIndex = result.text.indexOf("�");
812
+ const chunk = result.text.substring(0, exceptCharIndex == -1 ? result.text.length : exceptCharIndex);
813
+ const data = `data: ${JSON.stringify({
814
+ id: convId,
815
+ model,
816
+ object: 'chat.completion.chunk',
817
+ choices: [
818
+ { index: 0, delta: { content: (searchFlag ? '\n' : '') + chunk }, finish_reason: null }
819
+ ],
820
+ created
821
+ })}\n\n`;
822
+ if (searchFlag)
823
+ searchFlag = false;
824
+ !transStream.closed && transStream.write(data);
825
+ }
826
+ // 处理结束或错误
827
+ else if (result.event == 'all_done' || result.event == 'error') {
828
+ const data = `data: ${JSON.stringify({
829
+ id: convId,
830
+ model,
831
+ object: 'chat.completion.chunk',
832
+ choices: [
833
+ {
834
+ index: 0, delta: result.event == 'error' ? {
835
+ content: '\n[内容由于不合规被停止生成,我们换个话题吧]'
836
+ } : {}, finish_reason: 'stop'
837
+ }
838
+ ],
839
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
840
+ created
841
+ })}\n\n`;
842
+ !transStream.closed && transStream.write(data);
843
+ !transStream.closed && transStream.end('data: [DONE]\n\n');
844
+ endCallback && endCallback();
845
+ }
846
+ // 处理联网搜索
847
+ else if (!silentSearch && result.event == 'search_plus' && result.msg && result.msg.type == 'get_res') {
848
+ if (!searchFlag)
849
+ searchFlag = true;
850
+ const data = `data: ${JSON.stringify({
851
+ id: convId,
852
+ model,
853
+ object: 'chat.completion.chunk',
854
+ choices: [
855
+ {
856
+ index: 0, delta: {
857
+ content: `检索 ${result.msg.title} - ${result.msg.url} ...\n`
858
+ }, finish_reason: null
859
+ }
860
+ ],
861
+ created
862
+ })}\n\n`;
863
+ !transStream.closed && transStream.write(data);
864
+ }
865
+ // else
866
+ // logger.warn(result.event, result);
867
+ }
868
+ catch (err) {
869
+ logger.error(err);
870
+ !transStream.closed && transStream.end('\n\n');
871
+ }
872
+ });
873
+ // 将流数据喂给SSE转换器
874
+ stream.on("data", buffer => parser.feed(buffer.toString()));
875
+ stream.once("error", () => !transStream.closed && transStream.end('data: [DONE]\n\n'));
876
+ stream.once("close", () => !transStream.closed && transStream.end('data: [DONE]\n\n'));
877
+ return transStream;
878
+ }
879
+
880
+ /**
881
+ * Token切分
882
+ *
883
+ * @param authorization 认证字符串
884
+ */
885
+ function tokenSplit(authorization: string) {
886
+ return authorization.replace('Bearer ', '').split(',');
887
+ }
888
+
889
+ /**
890
+ * 获取Token存活状态
891
+ */
892
+ async function getTokenLiveStatus(refreshToken: string) {
893
+ const result = await axios.get('https://kimi.moonshot.cn/api/auth/token/refresh', {
894
+ headers: {
895
+ Authorization: `Bearer ${refreshToken}`,
896
+ Referer: 'https://kimi.moonshot.cn/',
897
+ ...FAKE_HEADERS
898
+ },
899
+ timeout: 15000,
900
+ validateStatus: () => true
901
+ });
902
+ try {
903
+ const {
904
+ access_token,
905
+ refresh_token
906
+ } = checkResult(result, refreshToken);
907
+ return !!(access_token && refresh_token)
908
+ }
909
+ catch (err) {
910
+ return false;
911
+ }
912
+ }
913
+
914
+ export default {
915
+ createConversation,
916
+ createCompletion,
917
+ createCompletionStream,
918
+ getTokenLiveStatus,
919
+ tokenSplit
920
+ };
src/api/routes/chat.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import chat from '@/api/controllers/chat.ts';
6
+ import logger from '@/lib/logger.ts';
7
+
8
+ export default {
9
+
10
+ prefix: '/deem/v1/chat',
11
+
12
+ post: {
13
+
14
+ '/completions': async (request: Request) => {
15
+ request
16
+ .validate('body.conversation_id', v => _.isUndefined(v) || _.isString(v))
17
+ .validate('body.messages', _.isArray)
18
+ .validate('headers.authorization', _.isString)
19
+ // refresh_token切分
20
+ const tokens = chat.tokenSplit(request.headers.authorization);
21
+ // 随机挑选一个refresh_token
22
+ const token = _.sample(tokens);
23
+ const { model, conversation_id: convId, messages, stream, use_search } = request.body;
24
+ if (stream) {
25
+ const stream = await chat.createCompletionStream(model, messages, token, use_search, convId);
26
+ return new Response(stream, {
27
+ type: "text/event-stream"
28
+ });
29
+ }
30
+ else
31
+ return await chat.createCompletion(model, messages, token, use_search, convId);
32
+ }
33
+
34
+ }
35
+
36
+ }
src/api/routes/index.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs-extra';
2
+
3
+ import Response from '@/lib/response/Response.ts';
4
+ import chat from "./chat.ts";
5
+ import ping from "./ping.ts";
6
+ import token from './token.ts';
7
+ import models from './models.ts';
8
+
9
+ export default [
10
+ {
11
+ get: {
12
+ '/': async () => {
13
+ const content = await fs.readFile('public/welcome.html');
14
+ return new Response(content, {
15
+ type: 'html',
16
+ headers: {
17
+ Expires: '-1'
18
+ }
19
+ });
20
+ }
21
+ }
22
+ },
23
+ chat,
24
+ ping,
25
+ token,
26
+ models
27
+ ];
src/api/routes/models.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ export default {
4
+
5
+ prefix: '/v1',
6
+
7
+ get: {
8
+ '/models': async () => {
9
+ return {
10
+ "data": [
11
+ {
12
+ "id": "moonshot-v1",
13
+ "object": "model",
14
+ "owned_by": "kimi-free-api"
15
+ },
16
+ {
17
+ "id": "moonshot-v1-8k",
18
+ "object": "model",
19
+ "owned_by": "kimi-free-api"
20
+ },
21
+ {
22
+ "id": "moonshot-v1-32k",
23
+ "object": "model",
24
+ "owned_by": "kimi-free-api"
25
+ },
26
+ {
27
+ "id": "moonshot-v1-128k",
28
+ "object": "model",
29
+ "owned_by": "kimi-free-api"
30
+ },
31
+ {
32
+ "id": "moonshot-v1-vision",
33
+ "object": "model",
34
+ "owned_by": "kimi-free-api"
35
+ }
36
+ ]
37
+ };
38
+ }
39
+
40
+ }
41
+ }
src/api/routes/ping.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ prefix: '/ping',
3
+ get: {
4
+ '': async () => "pong"
5
+ }
6
+ }
src/api/routes/token.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import chat from '@/api/controllers/chat.ts';
6
+ import logger from '@/lib/logger.ts';
7
+
8
+ export default {
9
+
10
+ prefix: '/token',
11
+
12
+ post: {
13
+
14
+ '/check': async (request: Request) => {
15
+ request
16
+ .validate('body.token', _.isString)
17
+ const live = await chat.getTokenLiveStatus(request.body.token);
18
+ return {
19
+ live
20
+ }
21
+ }
22
+
23
+ }
24
+
25
+ }
src/daemon.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 守护进程
3
+ */
4
+
5
+ import process from 'process';
6
+ import path from 'path';
7
+ import { spawn } from 'child_process';
8
+
9
+ import fs from 'fs-extra';
10
+ import { format as dateFormat } from 'date-fns';
11
+ import 'colors';
12
+
13
+ const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制
14
+ const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟
15
+ const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径
16
+ let crashCount = 0; //进程崩溃次数
17
+ let currentProcess; //当前运行进程
18
+
19
+ /**
20
+ * 写入守护进程日志
21
+ */
22
+ function daemonLog(value, color?: string) {
23
+ try {
24
+ const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `;
25
+ value = head + value;
26
+ console.log(color ? value[color] : value);
27
+ fs.ensureDirSync(path.dirname(LOG_PATH));
28
+ fs.appendFileSync(LOG_PATH, value + "\n");
29
+ }
30
+ catch(err) {
31
+ console.error("daemon log write error:", err);
32
+ }
33
+ }
34
+
35
+ daemonLog(`daemon pid: ${process.pid}`);
36
+
37
+ function createProcess() {
38
+ const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程
39
+ childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出
40
+ childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出
41
+ currentProcess = childProcess; //更新当前进程
42
+ daemonLog(`process(${childProcess.pid}) has started`);
43
+ childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red"));
44
+ childProcess.on("close", code => {
45
+ if(code === 0) //进程正常退出
46
+ daemonLog(`process(${childProcess.pid}) has exited`);
47
+ else if(code === 2) //进程已被杀死
48
+ daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow");
49
+ else if(code === 3) { //进程主动重启
50
+ daemonLog(`process(${childProcess.pid}) has restart`, "yellow");
51
+ createProcess(); //重新创建进程
52
+ }
53
+ else { //进程发生崩溃
54
+ if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启
55
+ daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed");
56
+ setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启
57
+ }
58
+ else //进程已崩溃,且无法重启
59
+ daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed");
60
+ }
61
+ }); //子进程关闭监听
62
+ }
63
+
64
+ process.on("exit", code => {
65
+ if(code === 0)
66
+ daemonLog("daemon process exited");
67
+ else if(code === 2)
68
+ daemonLog("daemon process has been killed!");
69
+ }); //守护进程退出事件
70
+
71
+ process.on("SIGTERM", () => {
72
+ daemonLog("received kill signal", "yellow");
73
+ currentProcess && currentProcess.kill("SIGINT");
74
+ process.exit(2);
75
+ }); //kill退出守护进程
76
+
77
+ process.on("SIGINT", () => {
78
+ currentProcess && currentProcess.kill("SIGINT");
79
+ process.exit(0);
80
+ }); //主动退出守护进程
81
+
82
+ createProcess(); //创建进程
src/index.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ import environment from "@/lib/environment.ts";
4
+ import config from "@/lib/config.ts";
5
+ import "@/lib/initialize.ts";
6
+ import server from "@/lib/server.ts";
7
+ import routes from "@/api/routes/index.ts";
8
+ import logger from "@/lib/logger.ts";
9
+
10
+ const startupTime = performance.now();
11
+
12
+ (async () => {
13
+ logger.header();
14
+
15
+ logger.info("<<<< kimi free server >>>>");
16
+ logger.info("Version:", environment.package.version);
17
+ logger.info("Process id:", process.pid);
18
+ logger.info("Environment:", environment.env);
19
+ logger.info("Service name:", config.service.name);
20
+
21
+ server.attachRoutes(routes);
22
+ await server.listen();
23
+
24
+ config.service.bindAddress &&
25
+ logger.success("Service bind address:", config.service.bindAddress);
26
+ })()
27
+ .then(() =>
28
+ logger.success(
29
+ `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30
+ )
31
+ )
32
+ .catch((err) => console.error(err));
src/lib/config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import serviceConfig from "./configs/service-config.ts";
2
+ import systemConfig from "./configs/system-config.ts";
3
+
4
+ class Config {
5
+
6
+ /** 服务配置 */
7
+ service = serviceConfig;
8
+
9
+ /** 系统配置 */
10
+ system = systemConfig;
11
+
12
+ }
13
+
14
+ export default new Config();
src/lib/configs/service-config.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import yaml from 'yaml';
5
+ import _ from 'lodash';
6
+
7
+ import environment from '../environment.ts';
8
+ import util from '../util.ts';
9
+
10
+ const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11
+
12
+ /**
13
+ * 服务配置
14
+ */
15
+ export class ServiceConfig {
16
+
17
+ /** 服务名称 */
18
+ name: string;
19
+ /** @type {string} 服务绑定主机地址 */
20
+ host;
21
+ /** @type {number} 服务绑定端口 */
22
+ port;
23
+ /** @type {string} 服务路由前缀 */
24
+ urlPrefix;
25
+ /** @type {string} 服务绑定地址(外部访问地址) */
26
+ bindAddress;
27
+
28
+ constructor(options?: any) {
29
+ const { name, host, port, urlPrefix, bindAddress } = options || {};
30
+ this.name = _.defaultTo(name, 'kimi-free-api');
31
+ this.host = _.defaultTo(host, '0.0.0.0');
32
+ this.port = _.defaultTo(port, 5566);
33
+ this.urlPrefix = _.defaultTo(urlPrefix, '');
34
+ this.bindAddress = bindAddress;
35
+ }
36
+
37
+ get addressHost() {
38
+ if(this.bindAddress) return this.bindAddress;
39
+ const ipAddresses = util.getIPAddressesByIPv4();
40
+ for(let ipAddress of ipAddresses) {
41
+ if(ipAddress === this.host)
42
+ return ipAddress;
43
+ }
44
+ return ipAddresses[0] || "127.0.0.1";
45
+ }
46
+
47
+ get address() {
48
+ return `${this.addressHost}:${this.port}`;
49
+ }
50
+
51
+ get pageDirUrl() {
52
+ return `http://127.0.0.1:${this.port}/page`;
53
+ }
54
+
55
+ get publicDirUrl() {
56
+ return `http://127.0.0.1:${this.port}/public`;
57
+ }
58
+
59
+ static load() {
60
+ const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61
+ if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63
+ return new ServiceConfig({ ...data, ...external });
64
+ }
65
+
66
+ }
67
+
68
+ export default ServiceConfig.load();
src/lib/configs/system-config.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import yaml from 'yaml';
5
+ import _ from 'lodash';
6
+
7
+ import environment from '../environment.ts';
8
+
9
+ const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10
+
11
+ /**
12
+ * 系统配置
13
+ */
14
+ export class SystemConfig {
15
+
16
+ /** 是否开启请求日志 */
17
+ requestLog: boolean;
18
+ /** 临时目录路径 */
19
+ tmpDir: string;
20
+ /** 日志目录路径 */
21
+ logDir: string;
22
+ /** 日志写入间隔(毫秒) */
23
+ logWriteInterval: number;
24
+ /** 日志文件有效期(毫秒) */
25
+ logFileExpires: number;
26
+ /** 公共目录路径 */
27
+ publicDir: string;
28
+ /** 临时文件有效期(毫秒) */
29
+ tmpFileExpires: number;
30
+ /** 请求体配置 */
31
+ requestBody: any;
32
+ /** 是否调试模式 */
33
+ debug: boolean;
34
+
35
+ constructor(options?: any) {
36
+ const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37
+ this.requestLog = _.defaultTo(requestLog, false);
38
+ this.tmpDir = _.defaultTo(tmpDir, './tmp');
39
+ this.logDir = _.defaultTo(logDir, './logs');
40
+ this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41
+ this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42
+ this.publicDir = _.defaultTo(publicDir, './public');
43
+ this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44
+ this.requestBody = Object.assign(requestBody || {}, {
45
+ enableTypes: ['json', 'form', 'text', 'xml'],
46
+ encoding: 'utf-8',
47
+ formLimit: '100mb',
48
+ jsonLimit: '100mb',
49
+ textLimit: '100mb',
50
+ xmlLimit: '100mb',
51
+ formidable: {
52
+ maxFileSize: '100mb'
53
+ },
54
+ multipart: true,
55
+ parsedMethods: ['POST', 'PUT', 'PATCH']
56
+ });
57
+ this.debug = _.defaultTo(debug, true);
58
+ }
59
+
60
+ get rootDirPath() {
61
+ return path.resolve();
62
+ }
63
+
64
+ get tmpDirPath() {
65
+ return path.resolve(this.tmpDir);
66
+ }
67
+
68
+ get logDirPath() {
69
+ return path.resolve(this.logDir);
70
+ }
71
+
72
+ get publicDirPath() {
73
+ return path.resolve(this.publicDir);
74
+ }
75
+
76
+ static load() {
77
+ if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79
+ return new SystemConfig(data);
80
+ }
81
+
82
+ }
83
+
84
+ export default SystemConfig.load();
src/lib/consts/exceptions.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export default {
2
+ SYSTEM_ERROR: [-1000, '系统异常'],
3
+ SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4
+ SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5
+ } as Record<string, [number, string]>
src/lib/environment.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import minimist from 'minimist';
5
+ import _ from 'lodash';
6
+
7
+ const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8
+ const envVars = process.env; //获取环境变量
9
+
10
+ class Environment {
11
+
12
+ /** 命令行参数 */
13
+ cmdArgs: any;
14
+ /** 环境变量 */
15
+ envVars: any;
16
+ /** 环境名称 */
17
+ env?: string;
18
+ /** 服务名称 */
19
+ name?: string;
20
+ /** 服务地址 */
21
+ host?: string;
22
+ /** 服务端口 */
23
+ port?: number;
24
+ /** 包参数 */
25
+ package: any;
26
+
27
+ constructor(options: any = {}) {
28
+ const { cmdArgs, envVars, package: _package } = options;
29
+ this.cmdArgs = cmdArgs;
30
+ this.envVars = envVars;
31
+ this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32
+ this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33
+ this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34
+ this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35
+ this.package = _package;
36
+ }
37
+
38
+ }
39
+
40
+ export default new Environment({
41
+ cmdArgs,
42
+ envVars,
43
+ package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44
+ });
src/lib/exceptions/APIException.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Exception from './Exception.js';
2
+
3
+ export default class APIException extends Exception {
4
+
5
+ /**
6
+ * 构造异常
7
+ *
8
+ * @param {[number, string]} exception 异常
9
+ */
10
+ constructor(exception: (string | number)[], errmsg?: string) {
11
+ super(exception, errmsg);
12
+ }
13
+
14
+ }
src/lib/exceptions/Exception.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'assert';
2
+
3
+ import _ from 'lodash';
4
+
5
+ export default class Exception extends Error {
6
+
7
+ /** 错误码 */
8
+ errcode: number;
9
+ /** 错误消息 */
10
+ errmsg: string;
11
+ /** 数据 */
12
+ data: any;
13
+ /** HTTP状态码 */
14
+ httpStatusCode: number;
15
+
16
+ /**
17
+ * 构造异常
18
+ *
19
+ * @param exception 异常
20
+ * @param _errmsg 异常消息
21
+ */
22
+ constructor(exception: (string | number)[], _errmsg?: string) {
23
+ assert(_.isArray(exception), 'Exception must be Array');
24
+ const [errcode, errmsg] = exception as [number, string];
25
+ assert(_.isFinite(errcode), 'Exception errcode invalid');
26
+ assert(_.isString(errmsg), 'Exception errmsg invalid');
27
+ super(_errmsg || errmsg);
28
+ this.errcode = errcode;
29
+ this.errmsg = _errmsg || errmsg;
30
+ }
31
+
32
+ compare(exception: (string | number)[]) {
33
+ const [errcode] = exception as [number, string];
34
+ return this.errcode == errcode;
35
+ }
36
+
37
+ setHTTPStatusCode(value: number) {
38
+ this.httpStatusCode = value;
39
+ return this;
40
+ }
41
+
42
+ setData(value: any) {
43
+ this.data = _.defaultTo(value, null);
44
+ return this;
45
+ }
46
+
47
+ }
src/lib/http-status-codes.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+
3
+ CONTINUE: 100, //客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
4
+ SWITCHING_PROTOCOLS: 101, //服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP 版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源
5
+ PROCESSING: 102, //处理将被继续执行
6
+
7
+ OK: 200, //请求已成功,请求所希望的响应头或数据体将随此响应返回
8
+ CREATED: 201, //请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,且其 URI 已经随Location 头信息返回。假如需要的资源无法及时建立的话,应当返回 '202 Accepted'
9
+ ACCEPTED: 202, //服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成
10
+ NON_AUTHORITATIVE_INFO: 203, //服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超级。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的
11
+ NO_CONTENT: 204, //服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾
12
+ RESET_CONTENT: 205, //服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束
13
+ PARTIAL_CONTENT: 206, //服务器已经成功处理了部分 GET 请求。类似于FlashGet或者迅雷这类的HTTP下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。响应必须包含如下的头部域:Content-Range 用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一段multipart中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。Date和ETag或Content-Location,假如同样的请求本应该返回200响应。Expires, Cache-Control,和/或 Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了 If-Range 强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了 If-Range 弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。假如 ETag 或 Latest-Modified 头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。任何不支持 Range 以及 Content-Range 头的缓存都禁止缓存206响应返回的内容
14
+ MULTIPLE_STATUS: 207, //代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码
15
+
16
+ MULTIPLE_CHOICES: 300, //被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616规范并没有规定这样的自动选择该如何进行。如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的 URI;浏览器可能会将这个 Location 值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的
17
+ MOVED_PERMANENTLY: 301, //被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:对于某些使用 HTTP/1.0 协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式
18
+ FOUND: 302, //请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI应当在响应的 Location 域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应
19
+ SEE_OTHER: 303, //对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用 GET 的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的 URI 不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。新的 URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。注意:许多 HTTP/1.1 版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的
20
+ NOT_MODIFIED: 304, //如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。该响应必须包含以下的头信息:Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。ETag或 Content-Location,假如同样的请求本应返回200响应。Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的 GET 请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。假如接收到一个要求更新某个缓存条目��304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值
21
+ USE_PROXY: 305, //被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能建立305响应。注意:RFC 2068中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器建立。忽视这些限制可能导致严重的安全后果
22
+ UNUSED: 306, //在最新版的规范中,306状态码已经不再被使用
23
+ TEMPORARY_REDIRECT: 307, //请求的资源现在临时从不同的URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI 的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的 URI 发出访问请求。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化
24
+
25
+ BAD_REQUEST: 400, //1.语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求 2.请求参数有误
26
+ UNAUTHORIZED: 401, //当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617
27
+ PAYMENT_REQUIRED: 402, //该状态码是为了将来可能的需求而预留的
28
+ FORBIDDEN: 403, //服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息
29
+ NOT_FOUND: 404, //请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下
30
+ METHOD_NOT_ALLOWED: 405, //请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误
31
+ NO_ACCEPTABLE: 406, //请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。除非这是一个 HEAD 请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准
32
+ PROXY_AUTHENTICATION_REQUIRED: 407, //与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617
33
+ REQUEST_TIMEOUT: 408, //请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改
34
+ CONFLICT: 409, //由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,���次PUT提交的对特定资源的修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本之间的差异比较,以便用户重新提交归并以后的新版本
35
+ GONE: 410, //被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为'410 Gone',以及是否需要保持此标记多长时间,完全取决于服务器拥有者
36
+ LENGTH_REQUIRED: 411, //服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求
37
+ PRECONDITION_FAILED: 412, //服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上
38
+ REQUEST_ENTITY_TOO_LARGE: 413, //服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试
39
+ REQUEST_URI_TOO_LONG: 414, //请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。重定向URI “黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码
40
+ UNSUPPORTED_MEDIA_TYPE: 415, //对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝
41
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416, //如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其 Content-Type
42
+ EXPECTION_FAILED: 417, //在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由的下一个节点上,Expect的内容无法被满足
43
+ TOO_MANY_CONNECTIONS: 421, //从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)。在这种情况下,连接数的计算可能涉及到不止一个终端用户
44
+ UNPROCESSABLE_ENTITY: 422, //请求格式正确,但是由于含有语义错误,无法响应
45
+ FAILED_DEPENDENCY: 424, //由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH
46
+ UNORDERED_COLLECTION: 425, //在WebDav Advanced Collections 草案中定义,但是未出现在《WebDAV 顺序集协议》(RFC 3658)中
47
+ UPGRADE_REQUIRED: 426, //客户端应当切换到TLS/1.0
48
+ RETRY_WITH: 449, //由微软扩展,代表请求应当在执行完适当的操作后进行重试
49
+
50
+ INTERNAL_SERVER_ERROR: 500, //服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都���在服务器的程序码出错时出现
51
+ NOT_IMPLEMENTED: 501, //服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求
52
+ BAD_GATEWAY: 502, //作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
53
+ SERVICE_UNAVAILABLE: 503, //由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间。如果没有给出这个 Retry-After 信息,那么客户端应当以处理500响应的方式处理它。注意:503状态码的存在并不意味着服务器在过载的时候必须使用它。某些服务器只不过是希望拒绝客户端的连接
54
+ GATEWAY_TIMEOUT: 504, //作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。注意:某些代理服务器在DNS查询超时时会返回400或者500错误
55
+ HTTP_VERSION_NOT_SUPPORTED: 505, //服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体
56
+ VARIANT_ALSO_NEGOTIATES: 506, //服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点
57
+ INSUFFICIENT_STORAGE: 507, //服务器无法存储完成请求所必须的内容。这个状况被认为是临时的
58
+ BANDWIDTH_LIMIT_EXCEEDED: 509, //服务器达到带宽限制。这不是一个官方的状态码,但是仍被广泛使用
59
+ NOT_EXTENDED: 510 //获取资源所需要的策略并没有没满足
60
+
61
+ };
src/lib/initialize.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logger from './logger.js';
2
+
3
+ // 允许无限量的监听器
4
+ process.setMaxListeners(Infinity);
5
+ // 输出未捕获异常
6
+ process.on("uncaughtException", (err, origin) => {
7
+ logger.error(`An unhandled error occurred: ${origin}`, err);
8
+ });
9
+ // 输出未处理的Promise.reject
10
+ process.on("unhandledRejection", (_, promise) => {
11
+ promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12
+ });
13
+ // 输出系统警告信息
14
+ process.on("warning", warning => logger.warn("System warning: ", warning));
15
+ // 进程退出监听
16
+ process.on("exit", () => {
17
+ logger.info("Service exit");
18
+ logger.footer();
19
+ });
20
+ // 进程被kill
21
+ process.on("SIGTERM", () => {
22
+ logger.warn("received kill signal");
23
+ process.exit(2);
24
+ });
25
+ // Ctrl-C进程退出
26
+ process.on("SIGINT", () => {
27
+ process.exit(0);
28
+ });
src/lib/interfaces/ICompletionMessage.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export default interface ICompletionMessage {
2
+ role: 'system' | 'assistant' | 'user' | 'function';
3
+ content: string;
4
+ }
src/lib/logger.ts ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import _util from 'util';
3
+
4
+ import 'colors';
5
+ import _ from 'lodash';
6
+ import fs from 'fs-extra';
7
+ import { format as dateFormat } from 'date-fns';
8
+
9
+ import config from './config.ts';
10
+ import util from './util.ts';
11
+
12
+ const isVercelEnv = process.env.VERCEL;
13
+
14
+ class LogWriter {
15
+
16
+ #buffers = [];
17
+
18
+ constructor() {
19
+ !isVercelEnv && fs.ensureDirSync(config.system.logDirPath);
20
+ !isVercelEnv && this.work();
21
+ }
22
+
23
+ push(content) {
24
+ const buffer = Buffer.from(content);
25
+ this.#buffers.push(buffer);
26
+ }
27
+
28
+ writeSync(buffer) {
29
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
30
+ }
31
+
32
+ async write(buffer) {
33
+ !isVercelEnv && await fs.appendFile(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
34
+ }
35
+
36
+ flush() {
37
+ if(!this.#buffers.length) return;
38
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), Buffer.concat(this.#buffers));
39
+ }
40
+
41
+ work() {
42
+ if (!this.#buffers.length) return setTimeout(this.work.bind(this), config.system.logWriteInterval);
43
+ const buffer = Buffer.concat(this.#buffers);
44
+ this.#buffers = [];
45
+ this.write(buffer)
46
+ .finally(() => setTimeout(this.work.bind(this), config.system.logWriteInterval))
47
+ .catch(err => console.error("Log write error:", err));
48
+ }
49
+
50
+ }
51
+
52
+ class LogText {
53
+
54
+ /** @type {string} 日志级别 */
55
+ level;
56
+ /** @type {string} 日志文本 */
57
+ text;
58
+ /** @type {string} 日志来源 */
59
+ source;
60
+ /** @type {Date} 日志发生时间 */
61
+ time = new Date();
62
+
63
+ constructor(level, ...params) {
64
+ this.level = level;
65
+ this.text = _util.format.apply(null, params);
66
+ this.source = this.#getStackTopCodeInfo();
67
+ }
68
+
69
+ #getStackTopCodeInfo() {
70
+ const unknownInfo = { name: "unknown", codeLine: 0, codeColumn: 0 };
71
+ const stackArray = new Error().stack.split("\n");
72
+ const text = stackArray[4];
73
+ if (!text)
74
+ return unknownInfo;
75
+ const match = text.match(/at (.+) \((.+)\)/) || text.match(/at (.+)/);
76
+ if (!match || !_.isString(match[2] || match[1]))
77
+ return unknownInfo;
78
+ const temp = match[2] || match[1];
79
+ const _match = temp.match(/([a-zA-Z0-9_\-\.]+)\:(\d+)\:(\d+)$/);
80
+ if (!_match)
81
+ return unknownInfo;
82
+ const [, scriptPath, codeLine, codeColumn] = _match as any;
83
+ return {
84
+ name: scriptPath ? scriptPath.replace(/.js$/, "") : "unknown",
85
+ path: scriptPath || null,
86
+ codeLine: parseInt(codeLine || 0),
87
+ codeColumn: parseInt(codeColumn || 0)
88
+ };
89
+ }
90
+
91
+ toString() {
92
+ return `[${dateFormat(this.time, "yyyy-MM-dd HH:mm:ss.SSS")}][${this.level}][${this.source.name}<${this.source.codeLine},${this.source.codeColumn}>] ${this.text}`;
93
+ }
94
+
95
+ }
96
+
97
+ class Logger {
98
+
99
+ /** @type {Object} 系统配置 */
100
+ config = {};
101
+ /** @type {Object} 日志级别映射 */
102
+ static Level = {
103
+ Success: "success",
104
+ Info: "info",
105
+ Log: "log",
106
+ Debug: "debug",
107
+ Warning: "warning",
108
+ Error: "error",
109
+ Fatal: "fatal"
110
+ };
111
+ /** @type {Object} 日志级别文本颜色樱色 */
112
+ static LevelColor = {
113
+ [Logger.Level.Success]: "green",
114
+ [Logger.Level.Info]: "brightCyan",
115
+ [Logger.Level.Debug]: "white",
116
+ [Logger.Level.Warning]: "brightYellow",
117
+ [Logger.Level.Error]: "brightRed",
118
+ [Logger.Level.Fatal]: "red"
119
+ };
120
+ #writer;
121
+
122
+ constructor() {
123
+ this.#writer = new LogWriter();
124
+ }
125
+
126
+ header() {
127
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG START ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
128
+ }
129
+
130
+ footer() {
131
+ this.#writer.flush(); //将未写入文件的日志缓存写入
132
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG END ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
133
+ }
134
+
135
+ success(...params) {
136
+ const content = new LogText(Logger.Level.Success, ...params).toString();
137
+ console.info(content[Logger.LevelColor[Logger.Level.Success]]);
138
+ this.#writer.push(content + "\n");
139
+ }
140
+
141
+ info(...params) {
142
+ const content = new LogText(Logger.Level.Info, ...params).toString();
143
+ console.info(content[Logger.LevelColor[Logger.Level.Info]]);
144
+ this.#writer.push(content + "\n");
145
+ }
146
+
147
+ log(...params) {
148
+ const content = new LogText(Logger.Level.Log, ...params).toString();
149
+ console.log(content[Logger.LevelColor[Logger.Level.Log]]);
150
+ this.#writer.push(content + "\n");
151
+ }
152
+
153
+ debug(...params) {
154
+ if(!config.system.debug) return; //非调试模式忽略debug
155
+ const content = new LogText(Logger.Level.Debug, ...params).toString();
156
+ console.debug(content[Logger.LevelColor[Logger.Level.Debug]]);
157
+ this.#writer.push(content + "\n");
158
+ }
159
+
160
+ warn(...params) {
161
+ const content = new LogText(Logger.Level.Warning, ...params).toString();
162
+ console.warn(content[Logger.LevelColor[Logger.Level.Warning]]);
163
+ this.#writer.push(content + "\n");
164
+ }
165
+
166
+ error(...params) {
167
+ const content = new LogText(Logger.Level.Error, ...params).toString();
168
+ console.error(content[Logger.LevelColor[Logger.Level.Error]]);
169
+ this.#writer.push(content);
170
+ }
171
+
172
+ fatal(...params) {
173
+ const content = new LogText(Logger.Level.Fatal, ...params).toString();
174
+ console.error(content[Logger.LevelColor[Logger.Level.Fatal]]);
175
+ this.#writer.push(content);
176
+ }
177
+
178
+ destory() {
179
+ this.#writer.destory();
180
+ }
181
+
182
+ }
183
+
184
+ export default new Logger();
src/lib/request/Request.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import APIException from '@/lib/exceptions/APIException.ts';
4
+ import EX from '@/api/consts/exceptions.ts';
5
+ import logger from '@/lib/logger.ts';
6
+ import util from '@/lib/util.ts';
7
+
8
+ export interface RequestOptions {
9
+ time?: number;
10
+ }
11
+
12
+ export default class Request {
13
+
14
+ /** 请求方法 */
15
+ method: string;
16
+ /** 请求URL */
17
+ url: string;
18
+ /** 请求路径 */
19
+ path: string;
20
+ /** 请求载荷类型 */
21
+ type: string;
22
+ /** 请求headers */
23
+ headers: any;
24
+ /** 请求原始查询字符串 */
25
+ search: string;
26
+ /** 请求查询参数 */
27
+ query: any;
28
+ /** 请求URL参数 */
29
+ params: any;
30
+ /** 请求载荷 */
31
+ body: any;
32
+ /** 上传的文件 */
33
+ files: any[];
34
+ /** 客户端IP地址 */
35
+ remoteIP: string | null;
36
+ /** 请求接受时间戳(毫秒) */
37
+ time: number;
38
+
39
+ constructor(ctx, options: RequestOptions = {}) {
40
+ const { time } = options;
41
+ this.method = ctx.request.method;
42
+ this.url = ctx.request.url;
43
+ this.path = ctx.request.path;
44
+ this.type = ctx.request.type;
45
+ this.headers = ctx.request.headers || {};
46
+ this.search = ctx.request.search;
47
+ this.query = ctx.query || {};
48
+ this.params = ctx.params || {};
49
+ this.body = ctx.request.body || {};
50
+ this.files = ctx.request.files || {};
51
+ this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52
+ this.time = Number(_.defaultTo(time, util.timestamp()));
53
+ }
54
+
55
+ validate(key: string, fn?: Function) {
56
+ try {
57
+ const value = _.get(this, key);
58
+ if (fn) {
59
+ if (fn(value) === false)
60
+ throw `[Mismatch] -> ${fn}`;
61
+ }
62
+ else if (_.isUndefined(value))
63
+ throw '[Undefined]';
64
+ }
65
+ catch (err) {
66
+ logger.warn(`Params ${key} invalid:`, err);
67
+ throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
68
+ }
69
+ return this;
70
+ }
71
+
72
+ }
src/lib/response/Body.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ export interface BodyOptions {
4
+ code?: number;
5
+ message?: string;
6
+ data?: any;
7
+ statusCode?: number;
8
+ }
9
+
10
+ export default class Body {
11
+
12
+ /** 状态码 */
13
+ code: number;
14
+ /** 状态消息 */
15
+ message: string;
16
+ /** 载荷 */
17
+ data: any;
18
+ /** HTTP状态码 */
19
+ statusCode: number;
20
+
21
+ constructor(options: BodyOptions = {}) {
22
+ const { code, message, data, statusCode } = options;
23
+ this.code = Number(_.defaultTo(code, 0));
24
+ this.message = _.defaultTo(message, 'OK');
25
+ this.data = _.defaultTo(data, null);
26
+ this.statusCode = Number(_.defaultTo(statusCode, 200));
27
+ }
28
+
29
+ toObject() {
30
+ return {
31
+ code: this.code,
32
+ message: this.message,
33
+ data: this.data
34
+ };
35
+ }
36
+
37
+ static isInstance(value) {
38
+ return value instanceof Body;
39
+ }
40
+
41
+ }
src/lib/response/FailureBody.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Body from './Body.ts';
4
+ import Exception from '../exceptions/Exception.ts';
5
+ import APIException from '../exceptions/APIException.ts';
6
+ import EX from '../consts/exceptions.ts';
7
+ import HTTP_STATUS_CODES from '../http-status-codes.ts';
8
+
9
+ export default class FailureBody extends Body {
10
+
11
+ constructor(error: APIException | Exception | Error, _data?: any) {
12
+ let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13
+ if(_.isString(error))
14
+ error = new Exception(EX.SYSTEM_ERROR, error);
15
+ else if(error instanceof APIException || error instanceof Exception)
16
+ ({ errcode, errmsg, data, httpStatusCode } = error);
17
+ else if(_.isError(error))
18
+ ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19
+ super({
20
+ code: errcode || -1,
21
+ message: errmsg || 'Internal error',
22
+ data,
23
+ statusCode: httpStatusCode
24
+ });
25
+ }
26
+
27
+ static isInstance(value) {
28
+ return value instanceof FailureBody;
29
+ }
30
+
31
+ }
src/lib/response/Response.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mime from 'mime';
2
+ import _ from 'lodash';
3
+
4
+ import Body from './Body.ts';
5
+ import util from '../util.ts';
6
+
7
+ export interface ResponseOptions {
8
+ statusCode?: number;
9
+ type?: string;
10
+ headers?: Record<string, any>;
11
+ redirect?: string;
12
+ body?: any;
13
+ size?: number;
14
+ time?: number;
15
+ }
16
+
17
+ export default class Response {
18
+
19
+ /** 响应HTTP状态码 */
20
+ statusCode: number;
21
+ /** 响应内容类型 */
22
+ type: string;
23
+ /** 响应headers */
24
+ headers: Record<string, any>;
25
+ /** 重定向目标 */
26
+ redirect: string;
27
+ /** 响应载荷 */
28
+ body: any;
29
+ /** 响应载荷大小 */
30
+ size: number;
31
+ /** 响应时间戳 */
32
+ time: number;
33
+
34
+ constructor(body: any, options: ResponseOptions = {}) {
35
+ const { statusCode, type, headers, redirect, size, time } = options;
36
+ this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37
+ this.type = type;
38
+ this.headers = headers;
39
+ this.redirect = redirect;
40
+ this.size = size;
41
+ this.time = Number(_.defaultTo(time, util.timestamp()));
42
+ this.body = body;
43
+ }
44
+
45
+ injectTo(ctx) {
46
+ this.redirect && ctx.redirect(this.redirect);
47
+ this.statusCode && (ctx.status = this.statusCode);
48
+ this.type && (ctx.type = mime.getType(this.type) || this.type);
49
+ const headers = this.headers || {};
50
+ if(this.size && !headers["Content-Length"] && !headers["content-length"])
51
+ headers["Content-Length"] = this.size;
52
+ ctx.set(headers);
53
+ if(Body.isInstance(this.body))
54
+ ctx.body = this.body.toObject();
55
+ else
56
+ ctx.body = this.body;
57
+ }
58
+
59
+ static isInstance(value) {
60
+ return value instanceof Response;
61
+ }
62
+
63
+ }
src/lib/response/SuccessfulBody.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Body from './Body.ts';
4
+
5
+ export default class SuccessfulBody extends Body {
6
+
7
+ constructor(data: any, message?: string) {
8
+ super({
9
+ code: 0,
10
+ message: _.defaultTo(message, "OK"),
11
+ data
12
+ });
13
+ }
14
+
15
+ static isInstance(value) {
16
+ return value instanceof SuccessfulBody;
17
+ }
18
+
19
+ }
src/lib/server.ts ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Koa from 'koa';
2
+ import KoaRouter from 'koa-router';
3
+ import koaRange from 'koa-range';
4
+ import koaCors from "koa2-cors";
5
+ import koaBody from 'koa-body';
6
+ import _ from 'lodash';
7
+
8
+ import Exception from './exceptions/Exception.ts';
9
+ import Request from './request/Request.ts';
10
+ import Response from './response/Response.js';
11
+ import FailureBody from './response/FailureBody.ts';
12
+ import EX from './consts/exceptions.ts';
13
+ import logger from './logger.ts';
14
+ import config from './config.ts';
15
+
16
+ class Server {
17
+
18
+ app;
19
+ router;
20
+
21
+ constructor() {
22
+ this.app = new Koa();
23
+ this.app.use(koaCors());
24
+ // 范围请求支持
25
+ this.app.use(koaRange);
26
+ this.router = new KoaRouter({ prefix: config.service.urlPrefix });
27
+ // 前置处理异常拦截
28
+ this.app.use(async (ctx: any, next: Function) => {
29
+ if(ctx.request.type === "application/xml" || ctx.request.type === "application/ssml+xml")
30
+ ctx.req.headers["content-type"] = "text/xml";
31
+ try { await next() }
32
+ catch (err) {
33
+ logger.error(err);
34
+ const failureBody = new FailureBody(err);
35
+ new Response(failureBody).injectTo(ctx);
36
+ }
37
+ });
38
+ // 载荷解析器支持
39
+ this.app.use(koaBody(_.clone(config.system.requestBody)));
40
+ this.app.on("error", (err: any) => {
41
+ // 忽略连接重试、中断、管道、取消错误
42
+ if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return;
43
+ logger.error(err);
44
+ });
45
+ logger.success("Server initialized");
46
+ }
47
+
48
+ /**
49
+ * 附加路由
50
+ *
51
+ * @param routes 路由列表
52
+ */
53
+ attachRoutes(routes: any[]) {
54
+ routes.forEach((route: any) => {
55
+ const prefix = route.prefix || "";
56
+ for (let method in route) {
57
+ if(method === "prefix") continue;
58
+ if (!_.isObject(route[method])) {
59
+ logger.warn(`Router ${prefix} ${method} invalid`);
60
+ continue;
61
+ }
62
+ for (let uri in route[method]) {
63
+ this.router[method](`${prefix}${uri}`, async ctx => {
64
+ const { request, response } = await this.#requestProcessing(ctx, route[method][uri]);
65
+ if(response != null && config.system.requestLog)
66
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
67
+ });
68
+ }
69
+ }
70
+ logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`);
71
+ });
72
+ this.app.use(this.router.routes());
73
+ this.app.use((ctx: any) => {
74
+ const request = new Request(ctx);
75
+ logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`);
76
+ // const failureBody = new FailureBody(new Exception(EX.SYSTEM_NOT_ROUTE_MATCHING, "Request is not supported"));
77
+ // const response = new Response(failureBody);
78
+ const message = `[请求有误]: 正确请求为 POST -> /v1/chat/completions,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`;
79
+ logger.warn(message);
80
+ const failureBody = new FailureBody(new Error(message));
81
+ const response = new Response(failureBody);
82
+ response.injectTo(ctx);
83
+ if(config.system.requestLog)
84
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * 请求处理
90
+ *
91
+ * @param ctx 上下文
92
+ * @param routeFn 路由方法
93
+ */
94
+ #requestProcessing(ctx: any, routeFn: Function): Promise<any> {
95
+ return new Promise(resolve => {
96
+ const request = new Request(ctx);
97
+ try {
98
+ if(config.system.requestLog)
99
+ logger.info(`-> ${request.method} ${request.url}`);
100
+ routeFn(request)
101
+ .then(response => {
102
+ try {
103
+ if(!Response.isInstance(response)) {
104
+ const _response = new Response(response);
105
+ _response.injectTo(ctx);
106
+ return resolve({ request, response: _response });
107
+ }
108
+ response.injectTo(ctx);
109
+ resolve({ request, response });
110
+ }
111
+ catch(err) {
112
+ logger.error(err);
113
+ const failureBody = new FailureBody(err);
114
+ const response = new Response(failureBody);
115
+ response.injectTo(ctx);
116
+ resolve({ request, response });
117
+ }
118
+ })
119
+ .catch(err => {
120
+ try {
121
+ logger.error(err);
122
+ const failureBody = new FailureBody(err);
123
+ const response = new Response(failureBody);
124
+ response.injectTo(ctx);
125
+ resolve({ request, response });
126
+ }
127
+ catch(err) {
128
+ logger.error(err);
129
+ const failureBody = new FailureBody(err);
130
+ const response = new Response(failureBody);
131
+ response.injectTo(ctx);
132
+ resolve({ request, response });
133
+ }
134
+ });
135
+ }
136
+ catch(err) {
137
+ logger.error(err);
138
+ const failureBody = new FailureBody(err);
139
+ const response = new Response(failureBody);
140
+ response.injectTo(ctx);
141
+ resolve({ request, response });
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * 监听端口
148
+ */
149
+ async listen() {
150
+ const host = config.service.host;
151
+ const port = config.service.port;
152
+ await Promise.all([
153
+ new Promise((resolve, reject) => {
154
+ if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1")
155
+ return resolve(null);
156
+ this.app.listen(port, "localhost", err => {
157
+ if(err) return reject(err);
158
+ resolve(null);
159
+ });
160
+ }),
161
+ new Promise((resolve, reject) => {
162
+ this.app.listen(port, host, err => {
163
+ if(err) return reject(err);
164
+ resolve(null);
165
+ });
166
+ })
167
+ ]);
168
+ logger.success(`Server listening on port ${port} (${host})`);
169
+ }
170
+
171
+ }
172
+
173
+ export default new Server();
src/lib/util.ts ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { Readable, Writable } from 'stream';
5
+
6
+ import 'colors';
7
+ import mime from 'mime';
8
+ import fs from 'fs-extra';
9
+ import { v1 as uuid } from 'uuid';
10
+ import { format as dateFormat } from 'date-fns';
11
+ import CRC32 from 'crc-32';
12
+ import randomstring from 'randomstring';
13
+ import _ from 'lodash';
14
+ import { CronJob } from 'cron';
15
+
16
+ import HTTP_STATUS_CODE from './http-status-codes.ts';
17
+
18
+ const autoIdMap = new Map();
19
+
20
+ const util = {
21
+
22
+ is2DArrays(value: any) {
23
+ return _.isArray(value) && (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1])));
24
+ },
25
+
26
+ uuid: (separator = true) => separator ? uuid() : uuid().replace(/\-/g, ""),
27
+
28
+ autoId: (prefix = '') => {
29
+ let index = autoIdMap.get(prefix);
30
+ if(index > 999999) index = 0; //超过最大数字则重置为0
31
+ autoIdMap.set(prefix, (index || 0) + 1);
32
+ return `${prefix}${index || 1}`;
33
+ },
34
+
35
+ ignoreJSONParse(value: string) {
36
+ const result = _.attempt(() => JSON.parse(value));
37
+ if(_.isError(result))
38
+ return null;
39
+ return result;
40
+ },
41
+
42
+ generateRandomString(options: any): string {
43
+ return randomstring.generate(options);
44
+ },
45
+
46
+ getResponseContentType(value: any): string | null {
47
+ return value.headers ? (value.headers["content-type"] || value.headers["Content-Type"]) : null;
48
+ },
49
+
50
+ generateCookie() {
51
+ const timestamp = util.unixTimestamp();
52
+ const items = [
53
+ `Hm_lvt_358cae4815e85d48f7e8ab7f3680a74b=${timestamp - Math.round(Math.random() * 2592000)}`,
54
+ `_ga=GA1.1.${util.generateRandomString({ length: 10, charset: 'numeric' })}.${timestamp - Math.round(Math.random() * 2592000)}`,
55
+ `_ga_YXD8W70SZP=GS1.1.${timestamp - Math.round(Math.random() * 2592000)}.1.1.${timestamp - Math.round(Math.random() * 2592000)}.0.0.0`,
56
+ `Hm_lpvt_358cae4815e85d48f7e8ab7f3680a74b=${timestamp - Math.round(Math.random() * 2592000)}`
57
+ ];
58
+ return items.join('; ');
59
+ },
60
+
61
+ mimeToExtension(value: string) {
62
+ let extension = mime.getExtension(value);
63
+ if(extension == "mpga")
64
+ return "mp3";
65
+ return extension;
66
+ },
67
+
68
+ extractURLExtension(value: string) {
69
+ const extname = path.extname(new URL(value).pathname);
70
+ return extname.substring(1).toLowerCase();
71
+ },
72
+
73
+ createCronJob(cronPatterns: any, callback?: Function) {
74
+ if(!_.isFunction(callback)) throw new Error("callback must be an Function");
75
+ return new CronJob(cronPatterns, () => callback(), null, false, "Asia/Shanghai");
76
+ },
77
+
78
+ getDateString(format = "yyyy-MM-dd", date = new Date()) {
79
+ return dateFormat(date, format);
80
+ },
81
+
82
+ getIPAddressesByIPv4(): string[] {
83
+ const interfaces = os.networkInterfaces();
84
+ const addresses = [];
85
+ for (let name in interfaces) {
86
+ const networks = interfaces[name];
87
+ const results = networks.filter(network => network.family === "IPv4" && network.address !== "127.0.0.1" && !network.internal);
88
+ if (results[0] && results[0].address)
89
+ addresses.push(results[0].address);
90
+ }
91
+ return addresses;
92
+ },
93
+
94
+ getMACAddressesByIPv4(): string[] {
95
+ const interfaces = os.networkInterfaces();
96
+ const addresses = [];
97
+ for (let name in interfaces) {
98
+ const networks = interfaces[name];
99
+ const results = networks.filter(network => network.family === "IPv4" && network.address !== "127.0.0.1" && !network.internal);
100
+ if (results[0] && results[0].mac)
101
+ addresses.push(results[0].mac);
102
+ }
103
+ return addresses;
104
+ },
105
+
106
+ generateSSEData(event?: string, data?: string, retry?: number) {
107
+ return `event: ${event || "message"}\ndata: ${(data || "").replace(/\n/g, "\\n").replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`;
108
+ },
109
+
110
+ buildDataBASE64(type, ext, buffer) {
111
+ return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString("base64")}`;
112
+ },
113
+
114
+ isLinux() {
115
+ return os.platform() !== "win32";
116
+ },
117
+
118
+ isIPAddress(value) {
119
+ return _.isString(value) && (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(value) || /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test(value));
120
+ },
121
+
122
+ isPort(value) {
123
+ return _.isNumber(value) && value > 0 && value < 65536;
124
+ },
125
+
126
+ isReadStream(value): boolean {
127
+ return value && (value instanceof Readable || "readable" in value || value.readable);
128
+ },
129
+
130
+ isWriteStream(value): boolean {
131
+ return value && (value instanceof Writable || "writable" in value || value.writable);
132
+ },
133
+
134
+ isHttpStatusCode(value) {
135
+ return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value);
136
+ },
137
+
138
+ isURL(value) {
139
+ return !_.isUndefined(value) && /^(http|https)/.test(value);
140
+ },
141
+
142
+ isSrc(value) {
143
+ return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value);
144
+ },
145
+
146
+ isBASE64(value) {
147
+ return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value);
148
+ },
149
+
150
+ isBASE64Data(value) {
151
+ return /^data:/.test(value);
152
+ },
153
+
154
+ extractBASE64DataFormat(value): string | null {
155
+ const match = value.trim().match(/^data:(.+);base64,/);
156
+ if(!match) return null;
157
+ return match[1];
158
+ },
159
+
160
+ removeBASE64DataHeader(value): string {
161
+ return value.replace(/^data:(.+);base64,/, "");
162
+ },
163
+
164
+ isDataString(value): boolean {
165
+ return /^(base64|json):/.test(value);
166
+ },
167
+
168
+ isStringNumber(value) {
169
+ return _.isFinite(Number(value));
170
+ },
171
+
172
+ isUnixTimestamp(value) {
173
+ return /^[0-9]{10}$/.test(`${value}`);
174
+ },
175
+
176
+ isTimestamp(value) {
177
+ return /^[0-9]{13}$/.test(`${value}`);
178
+ },
179
+
180
+ isEmail(value) {
181
+ return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(value);
182
+ },
183
+
184
+ isAsyncFunction(value) {
185
+ return Object.prototype.toString.call(value) === "[object AsyncFunction]";
186
+ },
187
+
188
+ async isAPNG(filePath) {
189
+ let head;
190
+ const readStream = fs.createReadStream(filePath, { start: 37, end: 40 });
191
+ const readPromise = new Promise((resolve, reject) => {
192
+ readStream.once("end", resolve);
193
+ readStream.once("error", reject);
194
+ });
195
+ readStream.once("data", data => head = data);
196
+ await readPromise;
197
+ return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0;
198
+ },
199
+
200
+ unixTimestamp() {
201
+ return parseInt(`${Date.now() / 1000}`);
202
+ },
203
+
204
+ timestamp() {
205
+ return Date.now();
206
+ },
207
+
208
+ urlJoin(...values) {
209
+ let url = "";
210
+ for (let i = 0; i < values.length; i++)
211
+ url += `${i > 0 ? "/" : ""}${values[i].replace(/^\/*/, "").replace(/\/*$/, "")}`;
212
+ return url;
213
+ },
214
+
215
+ millisecondsToHmss(milliseconds) {
216
+ if (_.isString(milliseconds)) return milliseconds;
217
+ milliseconds = parseInt(milliseconds);
218
+ const sec = Math.floor(milliseconds / 1000);
219
+ const hours = Math.floor(sec / 3600);
220
+ const minutes = Math.floor((sec - hours * 3600) / 60);
221
+ const seconds = sec - hours * 3600 - minutes * 60;
222
+ const ms = milliseconds % 60000 - seconds * 1000;
223
+ return `${hours > 9 ? hours : "0" + hours}:${minutes > 9 ? minutes : "0" + minutes}:${seconds > 9 ? seconds : "0" + seconds}.${ms}`;
224
+ },
225
+
226
+ millisecondsToTimeString(milliseconds) {
227
+ if(milliseconds < 1000)
228
+ return `${milliseconds}ms`;
229
+ if(milliseconds < 60000)
230
+ return `${parseFloat((milliseconds / 1000).toFixed(2))}s`;
231
+ return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor(milliseconds / 1000 % 60)}s`;
232
+ },
233
+
234
+ rgbToHex(r, g, b): string {
235
+ return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
236
+ },
237
+
238
+ hexToRgb(hex) {
239
+ const value = parseInt(hex.replace(/^#/, ""), 16);
240
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
241
+ },
242
+
243
+ md5(value) {
244
+ return crypto.createHash("md5").update(value).digest("hex");
245
+ },
246
+
247
+ crc32(value) {
248
+ return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value);
249
+ },
250
+
251
+ arrayParse(value): any[] {
252
+ return _.isArray(value) ? value : [value];
253
+ },
254
+
255
+ booleanParse(value) {
256
+ return value === "true" || value === true ? true : false
257
+ },
258
+
259
+ encodeBASE64(value) {
260
+ return Buffer.from(value).toString("base64");
261
+ },
262
+
263
+ decodeBASE64(value) {
264
+ return Buffer.from(value, "base64").toString();
265
+ },
266
+
267
+ };
268
+
269
+ export default util;
tsconfig.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "allowImportingTsExtensions": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "noEmit": true,
9
+ "paths": {
10
+ "@/*": ["src/*"]
11
+ },
12
+ "outDir": "./dist"
13
+ },
14
+ "include": ["src/**/*", "libs.d.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }