smgc commited on
Commit
598c08c
·
verified ·
1 Parent(s): 5518851

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +295 -237
app.js CHANGED
@@ -1,274 +1,332 @@
1
- // app.js
2
  const express = require('express');
3
  const { v4: uuidv4 } = require('uuid');
4
- require('dotenv').config();
 
5
 
6
- const app = express();
 
 
 
 
7
 
8
- // 中间件配置
9
- app.use(express.json());
10
- app.use(express.urlencoded({ extended: true }));
11
-
12
- // Helper function to convert string to hex bytes
13
- function stringToHex(str, modelName) {
14
- const bytes = Buffer.from(str, 'utf-8');
15
- const byteLength = bytes.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- // Calculate lengths and fields similar to Python version
18
- const FIXED_HEADER = 2;
19
- const SEPARATOR = 1;
20
- const FIXED_SUFFIX_LENGTH = 0xA3 + modelName.length;
21
 
22
- // 计算文本长度字段 (类似 Python 中的 base_length1)
23
- let textLengthField1, textLengthFieldSize1;
24
- if (byteLength < 128) {
25
- textLengthField1 = byteLength.toString(16).padStart(2, '0');
26
- textLengthFieldSize1 = 1;
27
- } else {
28
- const lowByte1 = (byteLength & 0x7F) | 0x80;
29
- const highByte1 = (byteLength >> 7) & 0xFF;
30
- textLengthField1 = lowByte1.toString(16).padStart(2, '0') + highByte1.toString(16).padStart(2, '0');
31
- textLengthFieldSize1 = 2;
32
- }
33
 
34
- // 计算基础长度 (类似 Python 中的 base_length)
35
- const baseLength = byteLength + 0x2A;
36
- let textLengthField, textLengthFieldSize;
37
- if (baseLength < 128) {
38
- textLengthField = baseLength.toString(16).padStart(2, '0');
39
- textLengthFieldSize = 1;
40
- } else {
41
- const lowByte = (baseLength & 0x7F) | 0x80;
42
- const highByte = (baseLength >> 7) & 0xFF;
43
- textLengthField = lowByte.toString(16).padStart(2, '0') + highByte.toString(16).padStart(2, '0');
44
- textLengthFieldSize = 2;
45
- }
46
 
47
- // 计算总消息长度
48
- const messageTotalLength = FIXED_HEADER + textLengthFieldSize + SEPARATOR +
49
- textLengthFieldSize1 + byteLength + FIXED_SUFFIX_LENGTH;
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- const messageLengthHex = messageTotalLength.toString(16).padStart(10, '0');
 
52
 
53
- // 构造完整的十六进制字符串
54
- const hexString = (
55
- messageLengthHex +
56
- '12' +
57
- textLengthField +
58
- '0A' +
59
- textLengthField1 +
60
- bytes.toString('hex') +
61
- '10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612' +
62
- '2002A132F643A2F6964656150726F2F656475626F73733A1E0A' +
63
- // 将模型名称长度转换为两位十六进制,并确保是大写
64
- Buffer.from(modelName, 'utf-8').length.toString(16).padStart(2, '0').toUpperCase() +
65
- Buffer.from(modelName, 'utf-8').toString('hex').toUpperCase() +
66
- '22004A' +
67
- '24' + '61383761396133342D323164642D343863372D623434662D616636633365636536663765' +
68
- '680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061' +
69
- '800101B00100C00100E00100E80100'
70
- ).toUpperCase();
71
- return Buffer.from(hexString, 'hex');
72
  }
73
 
74
- // 封装函数,用于将 chunk 转换为 UTF-8 字符串
75
- function chunkToUtf8String(chunk) {
76
- // 只处理以 0x00 0x00 0x00 0x00 开头的 chunk,其他不处理,不然会有乱码
77
- if (!(chunk[0] === 0x00 && chunk[1] === 0x00)) {
78
- console.log('Chunk does not start with 0x00 0x00, skipping.');
79
- return '';
80
- }
81
-
82
- console.log('chunk:', Buffer.from(chunk).toString('hex'));
83
- console.log('chunk string:', Buffer.from(chunk).toString('utf-8'));
84
-
85
- // 去掉 chunk 中 0x0A 以及之前的字符
86
- chunk = chunk.slice(chunk.indexOf(0x0A) + 1);
87
 
88
- let filteredChunk = [];
89
- let i = 0;
90
- while (i < chunk.length) {
91
- // 新的条件过滤:如果遇到连续4个0x00,则移除其之后所有的以 0 开头的字节(0x00 到 0x0F)
92
- if (chunk.slice(i, i + 4).every(byte => byte === 0x00)) {
93
- i += 4; // 跳过这4个0x00
94
- while (i < chunk.length && chunk[i] >= 0x00 && chunk[i] <= 0x0F) {
95
- i++; // 跳过所有以 0 开头的字节
96
- }
97
- continue;
98
- }
 
 
99
 
100
- if (chunk[i] === 0x0C) {
101
- // 遇到 0x0C 时,跳过 0x0C 以及后续的所有连续的 0x0A
102
- i++; // 跳过 0x0C
103
- while (i < chunk.length && chunk[i] === 0x0A) {
104
- i++; // 跳过所有连续的 0x0A
105
- }
106
- } else if (
107
- i > 0 &&
108
- chunk[i] === 0x0A &&
109
- chunk[i - 1] >= 0x00 &&
110
- chunk[i - 1] <= 0x09
111
- ) {
112
- // 如果当前字节是 0x0A,且前一个字节在 0x00 至 0x09 之间,跳过前一个字节和当前字节
113
- filteredChunk.pop(); // 移除已添加的前一个字节
114
- i++; // 跳过当前的 0x0A
115
- } else {
116
- filteredChunk.push(chunk[i]);
117
- i++;
118
  }
119
- }
120
-
121
- // 第二步:去除所有的 0x00 和 0x0C
122
- filteredChunk = filteredChunk.filter((byte) => byte !== 0x00 && byte !== 0x0C);
123
-
124
- // 去除小于 0x0A 的字节
125
- filteredChunk = filteredChunk.filter((byte) => byte >= 0x0A);
126
-
127
- const hexString = Buffer.from(filteredChunk).toString('hex');
128
- console.log('hexString:', hexString);
129
- const utf8String = Buffer.from(filteredChunk).toString('utf-8');
130
- console.log('utf8String:', utf8String);
131
- return utf8String;
132
  }
133
 
134
- app.post('/ai/v1/chat/completions', async (req, res) => {
135
- console.log('Received request:', req.body);
136
-
137
- // o1开头的模型,不支持流式输出
138
- if (req.body.model.startsWith('o1-') && req.body.stream) {
139
- console.log('Model not supported stream:', req.body.model);
140
- return res.status(400).json({
141
- error: 'Model not supported stream'
 
 
 
 
 
 
142
  });
143
- }
144
 
145
- let currentKeyIndex = 0;
146
- try {
147
- const { model, messages, stream = false } = req.body;
148
- let authToken = req.headers.authorization?.replace('Bearer ', '');
149
- // 处理逗号分隔的密钥
150
- const keys = authToken.split(',').map(key => key.trim());
151
- if (keys.length > 0) {
152
- // 确保 currentKeyIndex 不会越界
153
- if (currentKeyIndex >= keys.length) {
154
- currentKeyIndex = 0;
155
- }
156
- // 使用当前索引获取密钥
157
- authToken = keys[currentKeyIndex];
158
- // 更新索引
159
- currentKeyIndex = (currentKeyIndex + 1);
160
- }
161
- if (authToken && authToken.includes('%3A%3A')) {
162
- authToken = authToken.split('%3A%3A')[1];
163
- }
164
- if (!messages || !Array.isArray(messages) || messages.length === 0 || !authToken) {
165
- console.log('Invalid request:', { messages, authToken });
166
- return res.status(400).json({
167
- error: 'Invalid request. Messages should be a non-empty array and authorization is required'
168
- });
169
  }
 
 
 
170
 
171
- const formattedMessages = messages.map(msg => `${msg.role}:${msg.content}`).join('\n');
172
- const hexData = stringToHex(formattedMessages, model);
 
 
173
 
174
- console.log('Sending request to external API with token:', authToken);
175
- const response = await fetch('https://api2.cursor.sh/aiserver.v1.AiService/StreamChat', {
176
- method: 'POST',
177
- headers: {
178
- 'Content-Type': 'application/connect+proto',
179
- authorization: `Bearer ${authToken}`,
180
- 'connect-accept-encoding': 'gzip,br',
181
- 'connect-protocol-version': '1',
182
- 'user-agent': 'connect-es/1.4.0',
183
- 'x-amzn-trace-id': `Root=${uuidv4()}`,
184
- 'x-cursor-checksum': 'zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef',
185
- 'x-cursor-client-version': '0.42.3',
186
- 'x-cursor-timezone': 'Asia/Shanghai',
187
- 'x-ghost-mode': 'false',
188
- 'x-request-id': uuidv4(),
189
- Host: 'api2.cursor.sh'
190
- },
191
- body: hexData
192
- });
193
 
194
- console.log('Received response from external API:', response.status, response.statusText);
195
 
196
- if (stream) {
197
- res.setHeader('Content-Type', 'text/event-stream');
198
- res.setHeader('Cache-Control', 'no-cache');
199
- res.setHeader('Connection', 'keep-alive');
200
 
201
- const responseId = `chatcmpl-${uuidv4()}`;
 
 
 
 
 
 
 
202
 
203
- // 使用封装的函数处理 chunk
204
- for await (const chunk of response.body) {
205
- const text = chunkToUtf8String(chunk);
206
 
207
- if (text.length > 0) {
208
- console.log('Sending chunk:', text);
209
- res.write(`data: ${JSON.stringify({
210
- id: responseId,
211
- object: 'chat.completion.chunk',
212
- created: Math.floor(Date.now() / 1000),
213
- model,
214
- choices: [{
215
- index: 0,
216
- delta: {
217
- content: text
218
- }
219
- }]
220
- })}\n\n`);
221
  }
222
- }
223
 
224
- res.write('data: [DONE]\n\n');
225
- return res.end();
226
- } else {
227
- let text = '';
228
- // 在非流模式下也使用封装的函数
229
- for await (const chunk of response.body) {
230
- text += chunkToUtf8String(chunk);
231
- }
232
- // 对解析后的字符串进行进一步处理
233
- text = text.replace(/^.*<\|END_USER\|>/s, '');
234
- text = text.replace(/^\n[a-zA-Z]?/, '').trim();
235
- console.log('Final text:', text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
- return res.json({
238
- id: `chatcmpl-${uuidv4()}`,
239
- object: 'chat.completion',
240
- created: Math.floor(Date.now() / 1000),
241
- model,
242
- choices: [{
243
- index: 0,
244
- message: {
245
- role: 'assistant',
246
- content: text
247
- },
248
- finish_reason: 'stop'
249
- }],
250
- usage: {
251
- prompt_tokens: 0,
252
- completion_tokens: 0,
253
- total_tokens: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
- });
256
- }
257
- } catch (error) {
258
- console.error('Error:', error);
259
- if (!res.headersSent) {
260
- if (req.body.stream) {
261
- res.write(`data: ${JSON.stringify({ error: 'Internal server error' })}\n\n`);
262
- return res.end();
263
- } else {
264
- return res.status(500).json({ error: 'Internal server error' });
265
- }
266
  }
267
- }
268
  });
269
 
270
  // 启动服务器
271
  const PORT = process.env.PORT || 3000;
272
  app.listen(PORT, () => {
273
- console.log(`服务器运行在端口 ${PORT}`);
274
- });
 
 
 
 
1
  const express = require('express');
2
  const { v4: uuidv4 } = require('uuid');
3
+ const zlib = require('zlib');
4
+ const $protobuf = require('protobufjs/minimal');
5
 
6
+ // 初始化 protobuf
7
+ const $Reader = $protobuf.Reader;
8
+ const $Writer = $protobuf.Writer;
9
+ const $util = $protobuf.util;
10
+ const $root = $protobuf.roots['default'] || ($protobuf.roots['default'] = {});
11
 
12
+ // Proto 消息定义
13
+ const ChatMessageDefinition = {
14
+ ChatMessage: {
15
+ nested: {
16
+ FileContent: {
17
+ fields: {
18
+ filename: { type: "string", id: 1 },
19
+ content: { type: "string", id: 2 },
20
+ position: { type: "Position", id: 3 },
21
+ language: { type: "string", id: 5 },
22
+ range: { type: "Range", id: 6 },
23
+ length: { type: "int32", id: 8 },
24
+ type: { type: "int32", id: 9 },
25
+ error_code: { type: "int32", id: 11 }
26
+ },
27
+ nested: {
28
+ Position: {
29
+ fields: {
30
+ line: { type: "int32", id: 1 },
31
+ column: { type: "int32", id: 2 }
32
+ }
33
+ },
34
+ Range: {
35
+ fields: {
36
+ start: { type: "Position", id: 1 },
37
+ end: { type: "Position", id: 2 }
38
+ }
39
+ }
40
+ }
41
+ },
42
+ UserMessage: {
43
+ fields: {
44
+ content: { type: "string", id: 1 },
45
+ role: { type: "int32", id: 2 },
46
+ message_id: { type: "string", id: 13 }
47
+ }
48
+ },
49
+ Instructions: {
50
+ fields: {
51
+ instruction: { type: "string", id: 1 }
52
+ }
53
+ },
54
+ Model: {
55
+ fields: {
56
+ name: { type: "string", id: 1 },
57
+ empty: { type: "string", id: 4 }
58
+ }
59
+ }
60
+ },
61
+ fields: {
62
+ messages: { rule: "repeated", type: "UserMessage", id: 2 },
63
+ instructions: { type: "Instructions", id: 4 },
64
+ projectPath: { type: "string", id: 5 },
65
+ model: { type: "Model", id: 7 },
66
+ requestId: { type: "string", id: 9 },
67
+ summary: { type: "string", id: 11 },
68
+ conversationId: { type: "string", id: 15 }
69
+ }
70
+ },
71
+ ResMessage: {
72
+ fields: {
73
+ msg: { type: "string", id: 1 }
74
+ }
75
+ }
76
+ };
77
 
78
+ // 创建消息类型
79
+ $root.ChatMessage = $protobuf.Root.fromJSON(ChatMessageDefinition).lookupType("ChatMessage");
80
+ $root.ResMessage = $protobuf.Root.fromJSON(ChatMessageDefinition).lookupType("ResMessage");
 
81
 
82
+ // 工具函数
83
+ const regex = /<\|BEGIN_SYSTEM\|>.*?<\|END_SYSTEM\|>.*?<\|BEGIN_USER\|>.*?<\|END_USER\|>/s;
 
 
 
 
 
 
 
 
 
84
 
85
+ async function stringToHex(messages, modelName) {
86
+ const formattedMessages = messages.map((msg) => ({
87
+ ...msg,
88
+ role: msg.role === 'user' ? 1 : 2,
89
+ message_id: uuidv4(),
90
+ }));
 
 
 
 
 
 
91
 
92
+ const message = {
93
+ messages: formattedMessages,
94
+ instructions: {
95
+ instruction: 'Always respond in 中文',
96
+ },
97
+ projectPath: '/path/to/project',
98
+ model: {
99
+ name: modelName,
100
+ empty: '',
101
+ },
102
+ requestId: uuidv4(),
103
+ summary: '',
104
+ conversationId: uuidv4(),
105
+ };
106
 
107
+ const errMsg = $root.ChatMessage.verify(message);
108
+ if (errMsg) throw Error(errMsg);
109
 
110
+ const messageInstance = $root.ChatMessage.create(message);
111
+ const buffer = $root.ChatMessage.encode(messageInstance).finish();
112
+ const hexString = (buffer.length.toString(16).padStart(10, '0') +
113
+ buffer.toString('hex')).toUpperCase();
114
+ return Buffer.from(hexString, 'hex');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
 
117
+ async function chunkToUtf8String(chunk) {
118
+ try {
119
+ let hex = Buffer.from(chunk).toString('hex');
120
+ let offset = 0;
121
+ let results = [];
 
 
 
 
 
 
 
 
122
 
123
+ while (offset < hex.length) {
124
+ if (offset + 10 > hex.length) break;
125
+ const dataLength = parseInt(hex.slice(offset, offset + 10), 16);
126
+ offset += 10;
127
+
128
+ if (offset + dataLength * 2 > hex.length) break;
129
+ const messageHex = hex.slice(offset, offset + dataLength * 2);
130
+ offset += dataLength * 2;
131
+
132
+ const messageBuffer = Buffer.from(messageHex, 'hex');
133
+ const message = $root.ResMessage.decode(messageBuffer);
134
+ results.push(message.msg);
135
+ }
136
 
137
+ if (results.length == 0) {
138
+ return gunzip(chunk);
139
+ }
140
+ return results.join('');
141
+ } catch (err) {
142
+ return gunzip(chunk);
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
 
146
+ function gunzip(chunk) {
147
+ return new Promise((resolve, reject) => {
148
+ zlib.gunzip(chunk.slice(5), (err, decompressed) => {
149
+ if (err) {
150
+ resolve('');
151
+ } else {
152
+ const text = decompressed.toString('utf-8');
153
+ if (regex.test(text)) {
154
+ resolve('');
155
+ } else {
156
+ resolve(text);
157
+ }
158
+ }
159
+ });
160
  });
161
+ }
162
 
163
+ function getRandomIDPro({ size, dictType, customDict }) {
164
+ let random = '';
165
+ if (!customDict) {
166
+ switch (dictType) {
167
+ case 'alphabet':
168
+ customDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
169
+ break;
170
+ case 'max':
171
+ customDict = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-';
172
+ break;
173
+ default:
174
+ customDict = '0123456789';
175
+ }
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
+ for (; size--; ) random += customDict[(Math.random() * customDict.length) | 0];
178
+ return random;
179
+ }
180
 
181
+ // Express 应用设置
182
+ const app = express();
183
+ app.use(express.json());
184
+ app.use(express.urlencoded({ extended: true }));
185
 
186
+ // 主要路由处理
187
+ app.post('/ai/v1/chat/completions', async (req, res) => {
188
+ // o1开头的模型,不支持流式输出
189
+ if (req.body.model.startsWith('o1-') && req.body.stream) {
190
+ return res.status(400).json({
191
+ error: 'Model not supported stream',
192
+ });
193
+ }
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ let currentKeyIndex = 0;
196
 
197
+ try {
198
+ const { model, messages, stream = false } = req.body;
199
+ let authToken = req.headers.authorization?.replace('Bearer ', '');
 
200
 
201
+ // 处理认证token
202
+ const keys = authToken.split(',').map((key) => key.trim());
203
+ if (keys.length > 0) {
204
+ if (currentKeyIndex >= keys.length) {
205
+ currentKeyIndex = 0;
206
+ }
207
+ authToken = keys[currentKeyIndex];
208
+ }
209
 
210
+ if (authToken && authToken.includes('%3A%3A')) {
211
+ authToken = authToken.split('%3A%3A')[1];
212
+ }
213
 
214
+ // 验证请求
215
+ if (!messages || !Array.isArray(messages) || messages.length === 0 || !authToken) {
216
+ return res.status(400).json({
217
+ error: 'Invalid request. Messages should be a non-empty array and authorization is required',
218
+ });
 
 
 
 
 
 
 
 
 
219
  }
 
220
 
221
+ const hexData = await stringToHex(messages, model);
222
+
223
+ // 获取checksum
224
+ const checksum =
225
+ req.headers['x-cursor-checksum'] ??
226
+ process.env['x-cursor-checksum'] ??
227
+ `zo${getRandomIDPro({ dictType: 'max', size: 6 })}${getRandomIDPro({ dictType: 'max', size: 64 })}/${getRandomIDPro({ dictType: 'max', size: 64 })}`;
228
+
229
+ // API请求配置
230
+ const response = await fetch('https://api2.cursor.sh/aiserver.v1.AiService/StreamChat', {
231
+ method: 'POST',
232
+ headers: {
233
+ 'Content-Type': 'application/connect+proto',
234
+ 'authorization': `Bearer ${authToken}`,
235
+ 'connect-accept-encoding': 'gzip,br',
236
+ 'connect-protocol-version': '1',
237
+ 'user-agent': 'connect-es/1.4.0',
238
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
239
+ 'x-cursor-checksum': checksum,
240
+ 'x-cursor-client-version': '0.42.3',
241
+ 'x-cursor-timezone': 'Asia/Shanghai',
242
+ 'x-ghost-mode': 'false',
243
+ 'x-request-id': uuidv4(),
244
+ 'host': 'api2.cursor.sh',
245
+ },
246
+ body: hexData,
247
+ });
248
 
249
+ // 处理流式响应
250
+ if (stream) {
251
+ res.setHeader('Content-Type', 'text/event-stream');
252
+ res.setHeader('Cache-Control', 'no-cache');
253
+ res.setHeader('Connection', 'keep-alive');
254
+
255
+ const responseId = `chatcmpl-${uuidv4()}`;
256
+
257
+ for await (const chunk of response.body) {
258
+ const text = await chunkToUtf8String(chunk);
259
+ if (text.length > 0) {
260
+ res.write(
261
+ `data: ${JSON.stringify({
262
+ id: responseId,
263
+ object: 'chat.completion.chunk',
264
+ created: Math.floor(Date.now() / 1000),
265
+ model,
266
+ choices: [
267
+ {
268
+ index: 0,
269
+ delta: {
270
+ content: text,
271
+ },
272
+ },
273
+ ],
274
+ })}\n\n`,
275
+ );
276
+ }
277
+ }
278
+
279
+ res.write('data: [DONE]\n\n');
280
+ return res.end();
281
+ } else {
282
+ // 处理非流式响应
283
+ let text = '';
284
+ for await (const chunk of response.body) {
285
+ text += await chunkToUtf8String(chunk);
286
+ }
287
+
288
+ text = text.replace(/^.*<\|END_USER\|>/s, '');
289
+ text = text.replace(/^\n[a-zA-Z]?/, '').trim();
290
+
291
+ return res.json({
292
+ id: `chatcmpl-${uuidv4()}`,
293
+ object: 'chat.completion',
294
+ created: Math.floor(Date.now() / 1000),
295
+ model,
296
+ choices: [
297
+ {
298
+ index: 0,
299
+ message: {
300
+ role: 'assistant',
301
+ content: text,
302
+ },
303
+ finish_reason: 'stop',
304
+ },
305
+ ],
306
+ usage: {
307
+ prompt_tokens: 0,
308
+ completion_tokens: 0,
309
+ total_tokens: 0,
310
+ },
311
+ });
312
+ }
313
+ } catch (error) {
314
+ console.error('Error:', error);
315
+ if (!res.headersSent) {
316
+ if (req.body.stream) {
317
+ res.write(`data: ${JSON.stringify({ error: 'Internal server error' })}\n\n`);
318
+ return res.end();
319
+ } else {
320
+ return res.status(500).json({ error: 'Internal server error' });
321
+ }
322
  }
 
 
 
 
 
 
 
 
 
 
 
323
  }
 
324
  });
325
 
326
  // 启动服务器
327
  const PORT = process.env.PORT || 3000;
328
  app.listen(PORT, () => {
329
+ console.log(`服务器运行在端口 ${PORT}`);
330
+ });
331
+
332
+ module.exports = app;