protocol-proxy 2.3.4 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-store.js +36 -1
- package/lib/logger.js +58 -0
- package/lib/proxy-manager.js +4 -0
- package/lib/proxy-server.js +312 -178
- package/lib/stats-store.js +3 -5
- package/package.json +51 -51
- package/public/app.js +272 -22
- package/public/index.html +316 -277
- package/public/style.css +159 -4
- package/server.js +120 -28
package/public/style.css
CHANGED
|
@@ -249,7 +249,7 @@ header p {
|
|
|
249
249
|
.proxy-header {
|
|
250
250
|
display: flex;
|
|
251
251
|
justify-content: space-between;
|
|
252
|
-
align-items:
|
|
252
|
+
align-items: center;
|
|
253
253
|
margin-bottom: 14px;
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -314,13 +314,16 @@ header p {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
.target-table th:nth-child(1),
|
|
317
|
-
.target-table td:nth-child(1) { width:
|
|
317
|
+
.target-table td:nth-child(1) { width: 25%; }
|
|
318
318
|
|
|
319
319
|
.target-table th:nth-child(2),
|
|
320
|
-
.target-table td:nth-child(2) { width:
|
|
320
|
+
.target-table td:nth-child(2) { width: 25%; }
|
|
321
321
|
|
|
322
322
|
.target-table th:nth-child(3),
|
|
323
|
-
.target-table td:nth-child(3) { width:
|
|
323
|
+
.target-table td:nth-child(3) { width: 25%; }
|
|
324
|
+
|
|
325
|
+
.target-table th:nth-child(4),
|
|
326
|
+
.target-table td:nth-child(4) { width: 25%; text-align: center; }
|
|
324
327
|
|
|
325
328
|
.target-table th,
|
|
326
329
|
.target-table td {
|
|
@@ -1187,3 +1190,155 @@ form {
|
|
|
1187
1190
|
font-size: 2rem;
|
|
1188
1191
|
}
|
|
1189
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
.proxy-routing-badge {
|
|
1195
|
+
font-size: 0.75rem;
|
|
1196
|
+
padding: 3px 10px;
|
|
1197
|
+
border-radius: 6px;
|
|
1198
|
+
background: rgba(51, 65, 85, 0.5);
|
|
1199
|
+
color: #94a3b8;
|
|
1200
|
+
font-weight: 500;
|
|
1201
|
+
white-space: nowrap;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.provider-tag {
|
|
1205
|
+
font-size: 0.7rem;
|
|
1206
|
+
padding: 1px 6px;
|
|
1207
|
+
border-radius: 4px;
|
|
1208
|
+
background: rgba(148, 163, 184, 0.15);
|
|
1209
|
+
color: #94a3b8;
|
|
1210
|
+
margin-left: 6px;
|
|
1211
|
+
vertical-align: middle;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.provider-pool-group {
|
|
1215
|
+
grid-column: 1 / -1;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.provider-pool-picker {
|
|
1219
|
+
margin-bottom: 12px;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.provider-pool-list {
|
|
1223
|
+
display: flex;
|
|
1224
|
+
flex-direction: column;
|
|
1225
|
+
gap: 10px;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.provider-pool-empty {
|
|
1229
|
+
padding: 12px 14px;
|
|
1230
|
+
border: 1px dashed rgba(71, 85, 105, 0.6);
|
|
1231
|
+
border-radius: 10px;
|
|
1232
|
+
color: #64748b;
|
|
1233
|
+
font-size: 0.9rem;
|
|
1234
|
+
background: rgba(6, 8, 15, 0.35);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.provider-pool-item {
|
|
1238
|
+
display: grid;
|
|
1239
|
+
grid-template-columns: minmax(0, 1fr) 140px 120px auto;
|
|
1240
|
+
gap: 12px;
|
|
1241
|
+
align-items: center;
|
|
1242
|
+
padding: 12px 14px;
|
|
1243
|
+
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
1244
|
+
border-radius: 12px;
|
|
1245
|
+
background: rgba(6, 8, 15, 0.45);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.provider-pool-main {
|
|
1249
|
+
min-width: 0;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.provider-pool-name {
|
|
1253
|
+
color: #e2e8f0;
|
|
1254
|
+
font-weight: 600;
|
|
1255
|
+
overflow: hidden;
|
|
1256
|
+
text-overflow: ellipsis;
|
|
1257
|
+
white-space: nowrap;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
.provider-pool-meta {
|
|
1261
|
+
color: #64748b;
|
|
1262
|
+
font-size: 0.82rem;
|
|
1263
|
+
overflow: hidden;
|
|
1264
|
+
text-overflow: ellipsis;
|
|
1265
|
+
white-space: nowrap;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.provider-pool-model label {
|
|
1269
|
+
display: block;
|
|
1270
|
+
margin-bottom: 4px;
|
|
1271
|
+
font-size: 0.75rem;
|
|
1272
|
+
color: #94a3b8;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.provider-pool-model-value {
|
|
1276
|
+
color: #e2e8f0;
|
|
1277
|
+
font-size: 0.85rem;
|
|
1278
|
+
overflow: hidden;
|
|
1279
|
+
text-overflow: ellipsis;
|
|
1280
|
+
white-space: nowrap;
|
|
1281
|
+
display: block;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.pool-provider-group {
|
|
1285
|
+
position: relative;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.pool-provider-trigger {
|
|
1289
|
+
display: flex;
|
|
1290
|
+
align-items: center;
|
|
1291
|
+
gap: 4px;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.pool-provider-arrow {
|
|
1295
|
+
margin-left: auto;
|
|
1296
|
+
font-size: 12px;
|
|
1297
|
+
color: #64748b;
|
|
1298
|
+
transition: transform 0.15s;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.pool-provider-group.open > .pool-provider-trigger .pool-provider-arrow {
|
|
1302
|
+
transform: rotate(90deg);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.pool-model-sublist {
|
|
1306
|
+
display: none;
|
|
1307
|
+
padding-left: 16px;
|
|
1308
|
+
border-left: 2px solid rgba(71, 85, 105, 0.3);
|
|
1309
|
+
margin-left: 8px;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
.pool-provider-group.open > .pool-model-sublist {
|
|
1313
|
+
display: block;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.pool-model-sublist .model-option {
|
|
1317
|
+
font-size: 13px;
|
|
1318
|
+
padding: 6px 12px;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.provider-pool-weight label {
|
|
1322
|
+
display: block;
|
|
1323
|
+
margin-bottom: 4px;
|
|
1324
|
+
font-size: 0.75rem;
|
|
1325
|
+
color: #94a3b8;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.provider-pool-weight input {
|
|
1329
|
+
width: 100%;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.provider-pool-remove {
|
|
1333
|
+
padding: 8px 12px;
|
|
1334
|
+
border-radius: 10px;
|
|
1335
|
+
border: 1px solid rgba(127, 29, 29, 0.6);
|
|
1336
|
+
background: rgba(127, 29, 29, 0.2);
|
|
1337
|
+
color: #fca5a5;
|
|
1338
|
+
cursor: pointer;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
.provider-pool-remove:hover {
|
|
1342
|
+
background: rgba(127, 29, 29, 0.35);
|
|
1343
|
+
border-color: rgba(248, 113, 113, 0.8);
|
|
1344
|
+
}
|
package/server.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { exec, spawn } = require('child_process');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const logger = require('./lib/logger');
|
|
6
7
|
|
|
7
8
|
// ==================== CLI ====================
|
|
8
9
|
|
|
@@ -137,7 +138,7 @@ async function init() {
|
|
|
137
138
|
command = `xdg-open "${url}"`;
|
|
138
139
|
}
|
|
139
140
|
exec(command, (err) => {
|
|
140
|
-
if (err)
|
|
141
|
+
if (err) logger.error('[Browser] 打开浏览器失败:', err.message);
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -149,7 +150,7 @@ async function init() {
|
|
|
149
150
|
const start = Date.now();
|
|
150
151
|
res.on('finish', () => {
|
|
151
152
|
const duration = Date.now() - start;
|
|
152
|
-
|
|
153
|
+
logger.log(`[HTTP] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
153
154
|
});
|
|
154
155
|
next();
|
|
155
156
|
});
|
|
@@ -159,20 +160,90 @@ async function init() {
|
|
|
159
160
|
// ==================== 辅助函数 ====================
|
|
160
161
|
|
|
161
162
|
function resolveTarget(proxy) {
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
163
|
+
const primaryProvider = configStore.getProviderById(proxy.providerId);
|
|
164
|
+
if (!primaryProvider) return null;
|
|
165
|
+
|
|
166
|
+
const pool = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
|
|
169
|
+
// Primary provider (no model override)
|
|
170
|
+
const primaryKey = `${primaryProvider.id}\0`;
|
|
171
|
+
seen.add(primaryKey);
|
|
172
|
+
pool.push({
|
|
173
|
+
providerId: primaryProvider.id,
|
|
174
|
+
providerName: primaryProvider.name,
|
|
175
|
+
providerUrl: primaryProvider.url,
|
|
176
|
+
protocol: primaryProvider.protocol,
|
|
177
|
+
apiKey: primaryProvider.apiKey,
|
|
178
|
+
models: primaryProvider.models,
|
|
179
|
+
azureDeployment: primaryProvider.azureDeployment || '',
|
|
180
|
+
azureApiVersion: primaryProvider.azureApiVersion || '',
|
|
181
|
+
model: '',
|
|
182
|
+
weight: Math.max(1, parseInt(proxy.providerWeight, 10) || 1),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Pool entries (may include model override)
|
|
186
|
+
for (const entry of (proxy.providerPool || [])) {
|
|
187
|
+
if (!entry || !entry.providerId) continue;
|
|
188
|
+
const model = typeof entry.model === 'string' ? entry.model.trim() : '';
|
|
189
|
+
const key = `${entry.providerId}\0${model}`;
|
|
190
|
+
if (seen.has(key)) continue;
|
|
191
|
+
seen.add(key);
|
|
192
|
+
const provider = configStore.getProviderById(entry.providerId);
|
|
193
|
+
if (!provider) continue;
|
|
194
|
+
pool.push({
|
|
195
|
+
providerId: provider.id,
|
|
196
|
+
providerName: provider.name,
|
|
197
|
+
providerUrl: provider.url,
|
|
198
|
+
protocol: provider.protocol,
|
|
199
|
+
apiKey: provider.apiKey,
|
|
200
|
+
models: provider.models,
|
|
201
|
+
azureDeployment: provider.azureDeployment || '',
|
|
202
|
+
azureApiVersion: provider.azureApiVersion || '',
|
|
203
|
+
model,
|
|
204
|
+
weight: Math.max(1, parseInt(entry.weight, 10) || 1),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (pool.length === 0) return null;
|
|
209
|
+
const protocol = pool[0].protocol;
|
|
210
|
+
if (pool.some(p => p.protocol !== protocol)) return null;
|
|
211
|
+
|
|
164
212
|
return {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
apiKey: provider.apiKey,
|
|
213
|
+
protocol,
|
|
214
|
+
routingStrategy: proxy.routingStrategy || 'primary_fallback',
|
|
215
|
+
providerPool: pool,
|
|
169
216
|
defaultModel: proxy.defaultModel,
|
|
170
|
-
models: provider.models,
|
|
171
|
-
azureDeployment: provider.azureDeployment || '',
|
|
172
|
-
azureApiVersion: provider.azureApiVersion || '',
|
|
173
217
|
};
|
|
174
218
|
}
|
|
175
219
|
|
|
220
|
+
function normalizeProviderPoolInput(pool) {
|
|
221
|
+
if (!Array.isArray(pool)) return [];
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const result = [];
|
|
224
|
+
for (const item of pool) {
|
|
225
|
+
if (!item || typeof item !== 'object') continue;
|
|
226
|
+
const providerId = typeof item.providerId === 'string' ? item.providerId.trim() : '';
|
|
227
|
+
if (!providerId) continue;
|
|
228
|
+
const model = typeof item.model === 'string' ? item.model.trim() : '';
|
|
229
|
+
const key = `${providerId}\0${model}`;
|
|
230
|
+
if (seen.has(key)) continue;
|
|
231
|
+
seen.add(key);
|
|
232
|
+
result.push({
|
|
233
|
+
providerId,
|
|
234
|
+
model,
|
|
235
|
+
weight: Math.max(1, parseInt(item.weight, 10) || 1),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeRoutingStrategyInput(strategy) {
|
|
242
|
+
return ['primary_fallback', 'round_robin', 'weighted', 'fastest'].includes(strategy)
|
|
243
|
+
? strategy
|
|
244
|
+
: 'primary_fallback';
|
|
245
|
+
}
|
|
246
|
+
|
|
176
247
|
async function startProxyWithProvider(proxy) {
|
|
177
248
|
const target = resolveTarget(proxy);
|
|
178
249
|
if (!target) throw new Error(`供应商 ${proxy.providerId} 不存在`);
|
|
@@ -269,6 +340,9 @@ async function init() {
|
|
|
269
340
|
providerUrl: provider?.url || '',
|
|
270
341
|
protocol: provider?.protocol || '',
|
|
271
342
|
defaultModel: p.defaultModel || '',
|
|
343
|
+
providerWeight: Math.max(1, parseInt(p.providerWeight, 10) || 1),
|
|
344
|
+
routingStrategy: p.routingStrategy || 'primary_fallback',
|
|
345
|
+
providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
|
|
272
346
|
hasApiKey: !!provider?.apiKey,
|
|
273
347
|
running: proxyManager.isRunning(p.id),
|
|
274
348
|
};
|
|
@@ -286,13 +360,15 @@ async function init() {
|
|
|
286
360
|
providerName: provider?.name || '',
|
|
287
361
|
providerUrl: provider?.url || '',
|
|
288
362
|
protocol: provider?.protocol || '',
|
|
363
|
+
routingStrategy: proxy.routingStrategy || 'primary_fallback',
|
|
364
|
+
providerPool: Array.isArray(proxy.providerPool) ? proxy.providerPool : [],
|
|
289
365
|
hasApiKey: !!provider?.apiKey,
|
|
290
366
|
});
|
|
291
367
|
});
|
|
292
368
|
|
|
293
369
|
// 创建代理
|
|
294
370
|
app.post('/api/proxies', async (req, res) => {
|
|
295
|
-
const { name, port, requireAuth, authToken, providerId, defaultModel } = req.body;
|
|
371
|
+
const { name, port, requireAuth, authToken, providerId, defaultModel, routingStrategy, providerPool, providerWeight } = req.body;
|
|
296
372
|
|
|
297
373
|
if (!name || !port || !providerId) {
|
|
298
374
|
return res.status(400).json({ error: 'name, port and providerId are required' });
|
|
@@ -317,6 +393,9 @@ async function init() {
|
|
|
317
393
|
authToken: authToken || null,
|
|
318
394
|
providerId,
|
|
319
395
|
defaultModel: defaultModel || '',
|
|
396
|
+
providerWeight: Math.max(1, parseInt(providerWeight, 10) || 1),
|
|
397
|
+
routingStrategy: normalizeRoutingStrategyInput(routingStrategy),
|
|
398
|
+
providerPool: normalizeProviderPoolInput(providerPool),
|
|
320
399
|
});
|
|
321
400
|
|
|
322
401
|
try {
|
|
@@ -345,6 +424,9 @@ async function init() {
|
|
|
345
424
|
updates.providerId = req.body.providerId;
|
|
346
425
|
}
|
|
347
426
|
if (req.body.defaultModel !== undefined) updates.defaultModel = req.body.defaultModel;
|
|
427
|
+
if (req.body.providerWeight !== undefined) updates.providerWeight = Math.max(1, parseInt(req.body.providerWeight, 10) || 1);
|
|
428
|
+
if (req.body.routingStrategy !== undefined) updates.routingStrategy = normalizeRoutingStrategyInput(req.body.routingStrategy);
|
|
429
|
+
if (req.body.providerPool !== undefined) updates.providerPool = normalizeProviderPoolInput(req.body.providerPool);
|
|
348
430
|
|
|
349
431
|
const needRestart = updates.port !== undefined && updates.port !== existing.port;
|
|
350
432
|
if (needRestart) {
|
|
@@ -453,6 +535,8 @@ async function init() {
|
|
|
453
535
|
authToken: p.authToken,
|
|
454
536
|
providerId: p.providerId,
|
|
455
537
|
defaultModel: p.defaultModel || '',
|
|
538
|
+
routingStrategy: p.routingStrategy || 'primary_fallback',
|
|
539
|
+
providerPool: Array.isArray(p.providerPool) ? p.providerPool : [],
|
|
456
540
|
providerName: provider?.name || '',
|
|
457
541
|
};
|
|
458
542
|
});
|
|
@@ -502,6 +586,8 @@ async function init() {
|
|
|
502
586
|
authToken: p.authToken || null,
|
|
503
587
|
providerId: p.providerId,
|
|
504
588
|
defaultModel: p.defaultModel || '',
|
|
589
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
590
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
505
591
|
})),
|
|
506
592
|
};
|
|
507
593
|
configStore.saveConfig(newConfig);
|
|
@@ -521,6 +607,8 @@ async function init() {
|
|
|
521
607
|
protocol: p.protocol,
|
|
522
608
|
apiKey: p.apiKey || '',
|
|
523
609
|
models: Array.isArray(p.models) ? p.models : [],
|
|
610
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
611
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
524
612
|
});
|
|
525
613
|
}
|
|
526
614
|
|
|
@@ -535,16 +623,18 @@ async function init() {
|
|
|
535
623
|
error: `端口 ${p.port} 已被代理「${conflict.name}」占用,无法导入代理「${p.name}」`,
|
|
536
624
|
});
|
|
537
625
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
626
|
+
proxyMap.set(p.id, {
|
|
627
|
+
id: p.id,
|
|
628
|
+
name: p.name,
|
|
629
|
+
port: p.port,
|
|
630
|
+
requireAuth: !!p.requireAuth,
|
|
631
|
+
authToken: p.authToken || null,
|
|
632
|
+
providerId: p.providerId,
|
|
633
|
+
defaultModel: p.defaultModel || '',
|
|
634
|
+
routingStrategy: normalizeRoutingStrategyInput(p.routingStrategy),
|
|
635
|
+
providerPool: normalizeProviderPoolInput(p.providerPool),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
548
638
|
|
|
549
639
|
const merged = {
|
|
550
640
|
providers: Array.from(providerMap.values()),
|
|
@@ -570,6 +660,7 @@ async function init() {
|
|
|
570
660
|
});
|
|
571
661
|
|
|
572
662
|
// 启动
|
|
663
|
+
logger.init();
|
|
573
664
|
writePid();
|
|
574
665
|
|
|
575
666
|
// 启动所有已配置的代理
|
|
@@ -578,21 +669,22 @@ async function init() {
|
|
|
578
669
|
try {
|
|
579
670
|
await startProxyWithProvider(proxy);
|
|
580
671
|
} catch (err) {
|
|
581
|
-
|
|
672
|
+
logger.error(`[Init] Failed to start proxy ${proxy.name}:`, err.message);
|
|
582
673
|
}
|
|
583
674
|
}));
|
|
584
675
|
|
|
585
676
|
app.listen(PORT, () => {
|
|
586
677
|
const adminUrl = `http://localhost:${PORT}`;
|
|
587
|
-
|
|
588
|
-
|
|
678
|
+
logger.log(`[Admin] Management server running on ${adminUrl}`);
|
|
679
|
+
logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
|
|
680
|
+
logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
|
|
589
681
|
openBrowser(adminUrl);
|
|
590
682
|
});
|
|
591
683
|
}
|
|
592
684
|
|
|
593
685
|
// 优雅关闭
|
|
594
686
|
process.on('SIGINT', async () => {
|
|
595
|
-
|
|
687
|
+
logger.log('[Shutdown] Shutting down...');
|
|
596
688
|
removePid();
|
|
597
689
|
try {
|
|
598
690
|
const proxyManager = require('./lib/proxy-manager');
|
|
@@ -600,7 +692,7 @@ process.on('SIGINT', async () => {
|
|
|
600
692
|
statsStore.flush();
|
|
601
693
|
await proxyManager.stopAll();
|
|
602
694
|
} catch (err) {
|
|
603
|
-
|
|
695
|
+
logger.error('[Shutdown] stopAll error:', err.message);
|
|
604
696
|
}
|
|
605
697
|
process.exit(0);
|
|
606
698
|
});
|
|
@@ -613,7 +705,7 @@ process.on('SIGTERM', async () => {
|
|
|
613
705
|
statsStore.flush();
|
|
614
706
|
await proxyManager.stopAll();
|
|
615
707
|
} catch (err) {
|
|
616
|
-
|
|
708
|
+
logger.error('[Shutdown] stopAll error:', err.message);
|
|
617
709
|
}
|
|
618
710
|
process.exit(0);
|
|
619
711
|
});
|