dakaca commited on
Commit
8c25279
·
1 Parent(s): a2a61f5

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +294 -0
app.js ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express')
2
+ const fetch = require('cross-fetch')
3
+ const app = express()
4
+ var multer = require('multer');
5
+ var forms = multer({limits: { fieldSize: 10*1024*1024 }});
6
+ app.use(forms.array());
7
+ const cors = require('cors');
8
+ app.use(cors());
9
+
10
+ const bodyParser = require('body-parser')
11
+ app.use(bodyParser.json({limit : '50mb' }));
12
+ app.use(bodyParser.urlencoded({ extended: true }));
13
+
14
+ const tencentcloud = require("tencentcloud-sdk-nodejs");
15
+ const TmsClient = tencentcloud.tms.v20201229.Client;
16
+ const clientConfig = {
17
+ credential: {
18
+ secretId: process.env.TENCENT_CLOUD_SID,
19
+ secretKey: process.env.TENCENT_CLOUD_SKEY,
20
+ },
21
+ region: process.env.TENCENT_CLOUD_AP||"ap-singapore",
22
+ profile: {
23
+ httpProfile: {
24
+ endpoint: "tms.tencentcloudapi.com",
25
+ },
26
+ },
27
+ };
28
+ const mdClient = process.env.TENCENT_CLOUD_SID && process.env.TENCENT_CLOUD_SKEY ? new TmsClient(clientConfig) : false;
29
+
30
+ const controller = new AbortController();
31
+
32
+ app.all(`*`, async (req, res) => {
33
+
34
+ if(req.originalUrl) req.url = req.originalUrl;
35
+ let url = `https://api.openai.com${req.url}`;
36
+ // 从 header 中取得 Authorization': 'Bearer 后的 token
37
+ const token = req.headers.authorization?.split(' ')[1];
38
+ if( !token ) return res.status(403).send('Forbidden');
39
+
40
+ const openai_key = process.env.OPENAI_KEY||token.split(':')[0];
41
+ if( !openai_key ) return res.status(403).send('Forbidden');
42
+ if( openai_key.startsWith("fk") ) url = url.replaceAll( "api.openai.com", "openai.api2d.net" );
43
+
44
+ const proxy_key = token.split(':')[1]||"";
45
+ if( process.env.PROXY_KEY && proxy_key !== process.env.PROXY_KEY )
46
+ return res.status(403).send('Forbidden');
47
+
48
+ // console.log( req );
49
+ const { moderation, moderation_level, ...restBody } = req.body;
50
+ let sentence = "";
51
+ // 建立一个句子缓冲区
52
+ let sentence_buffer = [];
53
+ let processing = false;
54
+ let processing_stop = false;
55
+
56
+ async function process_buffer(res)
57
+ {
58
+ if( processing_stop )
59
+ {
60
+ console.log("processing_stop",processing_stop);
61
+ return false;
62
+ }
63
+
64
+ console.log("句子缓冲区" + new Date(), sentence_buffer);
65
+
66
+ // 处理句子缓冲区
67
+ if( processing )
68
+ {
69
+ // 有正在处理的,1秒钟后重试
70
+ console.log("有正在处理的,1秒钟后重试");
71
+ setTimeout( () => process_buffer(res), 1000 );
72
+ return false;
73
+ }
74
+
75
+ processing = true;
76
+ const sentence = sentence_buffer.shift();
77
+ console.log("取出句子", sentence);
78
+ if( sentence )
79
+ {
80
+ if( sentence === '[DONE]' )
81
+ {
82
+ console.log("[DONE]", "结束输出");
83
+ res.write("data: "+sentence+"\n\n" );
84
+ processing = false;
85
+ res.end();
86
+ return true;
87
+ }else
88
+ {
89
+ // 开始对句子进行审核
90
+ let data_array = JSON.parse(sentence);
91
+ console.log("解析句子数据为array",data_array);
92
+
93
+ const sentence_content = data_array.choices[0]?.delta?.content;
94
+ console.log("sentence_content", sentence_content);
95
+ if( sentence_content )
96
+ {
97
+ const params = {"Content": Buffer.from(sentence_content).toString('base64')};
98
+ const md_result = await mdClient.TextModeration(params);
99
+ // console.log("审核结果", md_result);
100
+ let md_check = moderation_level == 'high' ? md_result.Suggestion != 'Pass' : md_result.Suggestion == 'Block';
101
+ if( md_check )
102
+ {
103
+ // 终止输出
104
+ console.log("审核不通过", sentence_content, md_result);
105
+ let forbidden_array = data_array;
106
+ forbidden_array.choices[0].delta.content = "这个话题不适合讨论,换个话题吧。";
107
+ res.write("data: "+JSON.stringify(forbidden_array)+"\n\n" );
108
+ res.write("data: [DONE]\n\n" );
109
+ res.end();
110
+ controller.abort();
111
+ processing = false;
112
+ processing_stop = true;
113
+ return false;
114
+ }else
115
+ {
116
+ console.log("审核通过", sentence_content);
117
+ res.write("data: "+sentence+"\n\n" );
118
+ processing = false;
119
+ console.log("processing",processing);
120
+ return true;
121
+ }
122
+ }
123
+
124
+ }
125
+ }else
126
+ {
127
+ // console.log("句子缓冲区为空");
128
+ }
129
+
130
+ processing = false;
131
+ }
132
+
133
+
134
+ const options = {
135
+ method: req.method,
136
+ timeout: process.env.TIMEOUT||30000,
137
+ signal: controller.signal,
138
+ headers: {
139
+ 'Content-Type': 'application/json; charset=utf-8',
140
+ 'Authorization': 'Bearer '+ openai_key,
141
+ },
142
+ onMessage: async (data) => {
143
+ // console.log(data);
144
+ if( data === '[DONE]' )
145
+ {
146
+ sentence_buffer.push(data);
147
+ await process_buffer(res);
148
+ }else
149
+ {
150
+ if( moderation && mdClient )
151
+ {
152
+ try {
153
+ let data_array = JSON.parse(data);
154
+ const char = data_array.choices[0]?.delta?.content;
155
+ if( char ) sentence += char;
156
+ // console.log("sentence",sentence );
157
+ if( char == '。' || char == '?' || char == '!' || char == "\n" )
158
+ {
159
+ // 将 sentence 送审
160
+ console.log("遇到句号,将句子放入缓冲区", sentence);
161
+ data_array.choices[0].delta.content = sentence;
162
+ sentence = "";
163
+ sentence_buffer.push(JSON.stringify(data_array));
164
+ await process_buffer(res);
165
+ }
166
+ } catch (error) {
167
+ // 因为开头已经处理的了 [DONE] 的情况,这里应该不会出现无法解析json的情况
168
+ console.log( "error", error );
169
+ }
170
+ }else
171
+ {
172
+ // 如果没有文本审核参数或者设置,直接输出
173
+ res.write("data: "+data+"\n\n" );
174
+ }
175
+ }
176
+ }
177
+ };
178
+
179
+ if( req.method.toLocaleLowerCase() === 'post' && req.body ) options.body = JSON.stringify(restBody);
180
+ // console.log({url, options});
181
+
182
+ try {
183
+
184
+ // 如果是 chat completion 和 text completion,使用 SSE
185
+ if( (req.url.startsWith('/v1/completions') || req.url.startsWith('/v1/chat/completions')) && req.body.stream ) {
186
+ console.log("使用 SSE");
187
+ const response = await myFetch(url, options);
188
+ if( response.ok )
189
+ {
190
+ // write header
191
+ res.writeHead(200, {
192
+ 'Content-Type': 'text/event-stream',
193
+ 'Cache-Control': 'no-cache',
194
+ 'Connection': 'keep-alive',
195
+ });
196
+ const { createParser } = await import("eventsource-parser");
197
+ const parser = createParser((event) => {
198
+ // console.log(event);
199
+ if (event.type === "event") {
200
+ options.onMessage(event.data);
201
+ }
202
+ });
203
+ if (!response.body.getReader) {
204
+ const body = response.body;
205
+ if (!body.on || !body.read) {
206
+ throw new error('unsupported "fetch" implementation');
207
+ }
208
+ body.on("readable", () => {
209
+ let chunk;
210
+ while (null !== (chunk = body.read())) {
211
+ // console.log(chunk.toString());
212
+ parser.feed(chunk.toString());
213
+ }
214
+ });
215
+ } else {
216
+ for await (const chunk of streamAsyncIterable(response.body)) {
217
+ const str = new TextDecoder().decode(chunk);
218
+ parser.feed(str);
219
+ }
220
+ }
221
+ }else
222
+ {
223
+ const body = await response.text();
224
+ res.status(response.status).send(body);
225
+ }
226
+
227
+ }else
228
+ {
229
+ console.log("使用 fetch");
230
+ const response = await myFetch(url, options);
231
+ // console.log(response);
232
+ const data = await response.json();
233
+ // 审核结果
234
+ if( moderation && mdClient )
235
+ {
236
+ const params = {"Content": Buffer.from(data.choices[0].message.content).toString('base64')};
237
+ const md_result = await mdClient.TextModeration(params);
238
+ // console.log("审核结果", md_result);
239
+ let md_check = moderation_level == 'high' ? md_result.Suggestion != 'Pass' : md_result.Suggestion == 'Block';
240
+ if( md_check )
241
+ {
242
+ // 终止输出
243
+ console.log("审核不通过", data.choices[0].message.content, md_result);
244
+ data.choices[0].message.content = "这个话题不适合讨论,换个话题吧。";
245
+ }else
246
+ {
247
+ console.log("审核通过", data.choices[0].message.content);
248
+ }
249
+ }
250
+
251
+ res.json(data);
252
+ }
253
+
254
+
255
+ } catch (error) {
256
+ console.error(error);
257
+ res.status(500).json({"error":error.toString()});
258
+ }
259
+ })
260
+
261
+ async function* streamAsyncIterable(stream) {
262
+ const reader = stream.getReader();
263
+ try {
264
+ while (true) {
265
+ const { done, value } = await reader.read();
266
+ if (done) {
267
+ return;
268
+ }
269
+ yield value;
270
+ }
271
+ } finally {
272
+ reader.releaseLock();
273
+ }
274
+ }
275
+
276
+ async function myFetch(url, options) {
277
+ const {timeout, ...fetchOptions} = options;
278
+ const controller = new AbortController();
279
+ const timeoutId = setTimeout(() => controller.abort(), timeout||30000)
280
+ const res = await fetch(url, {...fetchOptions,signal:controller.signal});
281
+ clearTimeout(timeoutId);
282
+ return res;
283
+ }
284
+
285
+ // Error handler
286
+ app.use(function(err, req, res, next) {
287
+ console.error(err)
288
+ res.status(500).send('Internal Serverless Error')
289
+ })
290
+
291
+ const port = process.env.PORT||7860;
292
+ app.listen(port, () => {
293
+ console.log(`Server start on http://localhost:${port}`);
294
+ })