i0110 commited on
Commit
6e2ff83
·
verified ·
1 Parent(s): 59d638b

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +193 -40
server.js CHANGED
@@ -165,7 +165,6 @@ app.get('/api/proxy/spaces', async (req, res) => {
165
  name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
166
  owner: spaceInfo.author,
167
  username: username,
168
- // 不再直接包含 token 字段,而是仅在后端使用
169
  url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
170
  status: spaceRuntime.stage || 'unknown',
171
  last_modified: spaceInfo.lastModified || 'unknown',
@@ -201,12 +200,12 @@ app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) =>
201
  console.log(`尝试重启 Space: ${repoId}`);
202
  const spaces = spaceCache.getAll();
203
  const space = spaces.find(s => s.repo_id === repoId);
204
- if (!space || !space.token) {
205
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
206
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
207
  }
208
 
209
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
210
  const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
211
  console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
212
  res.json({ success: true, message: `Space ${repoId} 重启成功` });
@@ -228,12 +227,12 @@ app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) =>
228
  console.log(`尝试重建 Space: ${repoId}`);
229
  const spaces = spaceCache.getAll();
230
  const space = spaces.find(s => s.repo_id === repoId);
231
- if (!space || !space.token) {
232
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
233
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
234
  }
235
 
236
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
237
  // 将 factory_reboot 参数作为查询参数传递,而非请求体
238
  const response = await axios.post(
239
  `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,
@@ -359,56 +358,210 @@ app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
359
  }
360
  });
361
 
362
- // 代理 HuggingFace API:获取实时监控数据(SSE)
363
- app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
364
- try {
365
- const { username, instanceId } = req.params;
366
- const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
 
 
367
 
368
- // 检查实例状态,决定是否继续请求
369
- const spaces = spaceCache.getAll();
370
- const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
371
- if (!space) {
372
- console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
373
- return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
374
- }
375
- if (space.status.toLowerCase() !== 'running') {
376
- console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
377
- return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
378
  }
379
 
380
- const token = userTokenMapping[username];
381
- let headers = {
 
 
 
 
 
 
382
  'Accept': 'text/event-stream',
383
  'Cache-Control': 'no-cache',
384
  'Connection': 'keep-alive'
385
  };
386
- if (token) {
387
- headers['Authorization'] = `Bearer ${token}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
 
389
 
390
- const response = await axios({
391
- method: 'get',
392
- url,
393
- headers,
394
- responseType: 'stream',
395
- timeout: 10000
 
 
 
 
 
 
 
396
  });
 
397
 
398
- res.set({
399
- 'Content-Type': 'text/event-stream',
400
- 'Cache-Control': 'no-cache',
401
- 'Connection': 'keep-alive'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  });
403
- response.data.pipe(res);
404
 
405
- req.on('close', () => {
406
- response.data.destroy();
 
 
 
 
 
407
  });
408
- } catch (error) {
409
- console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
410
- res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  });
413
 
414
  // 处理其他请求,重定向到 index.html
 
165
  name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
166
  owner: spaceInfo.author,
167
  username: username,
 
168
  url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
169
  status: spaceRuntime.stage || 'unknown',
170
  last_modified: spaceInfo.lastModified || 'unknown',
 
200
  console.log(`尝试重启 Space: ${repoId}`);
201
  const spaces = spaceCache.getAll();
202
  const space = spaces.find(s => s.repo_id === repoId);
203
+ if (!space || !userTokenMapping[space.username]) {
204
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
205
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
206
  }
207
 
208
+ const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };
209
  const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
210
  console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
211
  res.json({ success: true, message: `Space ${repoId} 重启成功` });
 
227
  console.log(`尝试重建 Space: ${repoId}`);
228
  const spaces = spaceCache.getAll();
229
  const space = spaces.find(s => s.repo_id === repoId);
230
+ if (!space || !userTokenMapping[space.username]) {
231
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
232
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
233
  }
234
 
235
+ const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };
236
  // 将 factory_reboot 参数作为查询参数传递,而非请求体
237
  const response = await axios.post(
238
  `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,
 
358
  }
359
  });
360
 
361
+ // 监控数据管理类
362
+ class MetricsConnectionManager {
363
+ constructor() {
364
+ this.connections = new Map(); // 存储 HuggingFace API 的监控连接
365
+ this.clients = new Map(); // 存储前端客户端的 SSE 连接
366
+ this.instanceData = new Map(); // 存储每个实例的最新监控数据
367
+ }
368
 
369
+ // 建立到 HuggingFace API 的监控连接
370
+ async connectToInstance(repoId, username, token) {
371
+ if (this.connections.has(repoId)) {
372
+ return this.connections.get(repoId);
 
 
 
 
 
 
373
  }
374
 
375
+ const instanceId = repoId.split('/')[1];
376
+ const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
377
+ const headers = token ? {
378
+ 'Authorization': `Bearer ${token}`,
379
+ 'Accept': 'text/event-stream',
380
+ 'Cache-Control': 'no-cache',
381
+ 'Connection': 'keep-alive'
382
+ } : {
383
  'Accept': 'text/event-stream',
384
  'Cache-Control': 'no-cache',
385
  'Connection': 'keep-alive'
386
  };
387
+
388
+ try {
389
+ const response = await axios({
390
+ method: 'get',
391
+ url,
392
+ headers,
393
+ responseType: 'stream',
394
+ timeout: 10000
395
+ });
396
+
397
+ const stream = response.data;
398
+ stream.on('data', (chunk) => {
399
+ const chunkStr = chunk.toString();
400
+ if (chunkStr.includes('event: metric')) {
401
+ const dataMatch = chunkStr.match(/data: (.*)/);
402
+ if (dataMatch && dataMatch[1]) {
403
+ try {
404
+ const metrics = JSON.parse(dataMatch[1]);
405
+ this.instanceData.set(repoId, metrics);
406
+ // 推送给所有订阅了该实例的客户端
407
+ this.clients.forEach((clientRes, clientId) => {
408
+ if (clientRes.subscribedInstances && clientRes.subscribedInstances.includes(repoId)) {
409
+ clientRes.write(`event: metric\n`);
410
+ clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
411
+ }
412
+ });
413
+ } catch (error) {
414
+ console.error(`解析监控数据失败 (${repoId}):`, error.message);
415
+ }
416
+ }
417
+ }
418
+ });
419
+
420
+ stream.on('error', (error) => {
421
+ console.error(`监控连接错误 (${repoId}):`, error.message);
422
+ this.connections.delete(repoId);
423
+ this.instanceData.delete(repoId);
424
+ });
425
+
426
+ stream.on('end', () => {
427
+ console.log(`监控连接结束 (${repoId})`);
428
+ this.connections.delete(repoId);
429
+ this.instanceData.delete(repoId);
430
+ });
431
+
432
+ this.connections.set(repoId, stream);
433
+ console.log(`已建立监控连接 (${repoId})`);
434
+ return stream;
435
+ } catch (error) {
436
+ console.error(`无法连接到监控端点 (${repoId}):`, error.message);
437
+ this.connections.delete(repoId);
438
+ return null;
439
  }
440
+ }
441
 
442
+ // 注册前端客户端的 SSE 连接
443
+ registerClient(clientId, res, subscribedInstances) {
444
+ res.subscribedInstances = subscribedInstances || [];
445
+ this.clients.set(clientId, res);
446
+ console.log(`客户端 ${clientId} 注册,订阅实例: ${res.subscribedInstances.join(', ') || '无'}`);
447
+
448
+ // 首次连接时,推送已缓存的最新数据
449
+ res.subscribedInstances.forEach(repoId => {
450
+ if (this.instanceData.has(repoId)) {
451
+ const metrics = this.instanceData.get(repoId);
452
+ res.write(`event: metric\n`);
453
+ res.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
454
+ }
455
  });
456
+ }
457
 
458
+ // 客户端断开连接
459
+ unregisterClient(clientId) {
460
+ this.clients.delete(clientId);
461
+ console.log(`客户端 ${clientId} 断开连接`);
462
+ this.cleanupConnections();
463
+ }
464
+
465
+ // 更新客户端订阅的实例列表
466
+ updateClientSubscriptions(clientId, subscribedInstances) {
467
+ const clientRes = this.clients.get(clientId);
468
+ if (clientRes) {
469
+ clientRes.subscribedInstances = subscribedInstances || [];
470
+ console.log(`客户端 ${clientId} 更新订阅: ${clientRes.subscribedInstances.join(', ') || '无'}`);
471
+ // 更新后推送最新的缓存数据
472
+ subscribedInstances.forEach(repoId => {
473
+ if (this.instanceData.has(repoId)) {
474
+ const metrics = this.instanceData.get(repoId);
475
+ clientRes.write(`event: metric\n`);
476
+ clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
477
+ }
478
+ });
479
+ }
480
+ this.cleanupConnections();
481
+ }
482
+
483
+ // 清理未被任何客户端订阅的连接
484
+ cleanupConnections() {
485
+ const subscribedRepoIds = new Set();
486
+ this.clients.forEach(clientRes => {
487
+ clientRes.subscribedInstances.forEach(repoId => subscribedRepoIds.add(repoId));
488
  });
 
489
 
490
+ const toRemove = [];
491
+ this.connections.forEach((stream, repoId) => {
492
+ if (!subscribedRepoIds.has(repoId)) {
493
+ toRemove.push(repoId);
494
+ stream.destroy();
495
+ console.log(`清理未订阅的监控连接 (${repoId})`);
496
+ }
497
  });
498
+
499
+ toRemove.forEach(repoId => {
500
+ this.connections.delete(repoId);
501
+ this.instanceData.delete(repoId);
502
+ });
503
+ }
504
+ }
505
+
506
+ const metricsManager = new MetricsConnectionManager();
507
+
508
+ // 新增统一监控数据的SSE端点
509
+ app.get('/api/proxy/live-metrics-stream', (req, res) => {
510
+ // 设置 SSE 所需的响应头
511
+ res.set({
512
+ 'Content-Type': 'text/event-stream',
513
+ 'Cache-Control': 'no-cache',
514
+ 'Connection': 'keep-alive'
515
+ });
516
+
517
+ // 生成唯一的客户端ID
518
+ const clientId = crypto.randomBytes(8).toString('hex');
519
+
520
+ // 获取查询参数中的实例列表
521
+ const instancesParam = req.query.instances || '';
522
+ const subscribedInstances = instancesParam.split(',').filter(id => id.trim() !== '');
523
+
524
+ // 注册客户端
525
+ metricsManager.registerClient(clientId, res, subscribedInstances);
526
+
527
+ // 根据订阅列表建立监控连接
528
+ const spaces = spaceCache.getAll();
529
+ subscribedInstances.forEach(repoId => {
530
+ const space = spaces.find(s => s.repo_id === repoId);
531
+ if (space) {
532
+ const username = space.username;
533
+ const token = userTokenMapping[username] || '';
534
+ metricsManager.connectToInstance(repoId, username, token);
535
+ }
536
+ });
537
+
538
+ // 监听客户端断开连接
539
+ req.on('close', () => {
540
+ metricsManager.unregisterClient(clientId);
541
+ console.log(`客户端 ${clientId} 断开 SSE 连接`);
542
+ });
543
+ });
544
+
545
+ // 新增接口:更新客户端订阅的实例列表
546
+ app.post('/api/proxy/update-subscriptions', (req, res) => {
547
+ const { clientId, instances } = req.body;
548
+ if (!clientId || !instances || !Array.isArray(instances)) {
549
+ return res.status(400).json({ error: '缺少 clientId 或 instances 参数' });
550
  }
551
+
552
+ metricsManager.updateClientSubscriptions(clientId, instances);
553
+ // 根据新订阅列表建立监控连接
554
+ const spaces = spaceCache.getAll();
555
+ instances.forEach(repoId => {
556
+ const space = spaces.find(s => s.repo_id === repoId);
557
+ if (space) {
558
+ const username = space.username;
559
+ const token = userTokenMapping[username] || '';
560
+ metricsManager.connectToInstance(repoId, username, token);
561
+ }
562
+ });
563
+
564
+ res.json({ success: true, message: '订阅列表已更新' });
565
  });
566
 
567
  // 处理其他请求,重定向到 index.html