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
|