Update server.js
Browse files
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.
|
205 |
console.error(`Space ${repoId} 未找到或无 Token 配置`);
|
206 |
return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
|
207 |
}
|
208 |
|
209 |
-
const headers = { 'Authorization': `Bearer ${space.
|
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.
|
232 |
console.error(`Space ${repoId} 未找到或无 Token 配置`);
|
233 |
return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
|
234 |
}
|
235 |
|
236 |
-
const headers = { 'Authorization': `Bearer ${space.
|
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 |
-
//
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
|
|
|
|
367 |
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
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
|
381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
'Accept': 'text/event-stream',
|
383 |
'Cache-Control': 'no-cache',
|
384 |
'Connection': 'keep-alive'
|
385 |
};
|
386 |
-
|
387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
}
|
|
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
});
|
|
|
397 |
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
402 |
});
|
403 |
-
response.data.pipe(res);
|
404 |
|
405 |
-
|
406 |
-
|
|
|
|
|
|
|
|
|
|
|
407 |
});
|
408 |
-
|
409 |
-
|
410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|