nbugs commited on
Commit
4ccfda1
·
verified ·
1 Parent(s): 96b1c0e

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +416 -124
server.js CHANGED
@@ -9,14 +9,6 @@ const port = process.env.PORT || 8080;
9
  app.use(express.json());
10
  app.use(express.urlencoded({ extended: true }));
11
 
12
- // 添加安全响应头
13
- app.use((req, res, next) => {
14
- res.setHeader('X-Content-Type-Options', 'nosniff');
15
- res.setHeader('X-Frame-Options', 'DENY');
16
- res.setHeader('X-XSS-Protection', '1; mode=block');
17
- next();
18
- });
19
-
20
  // 从环境变量获取 HuggingFace 用户名和对应的 API Token 映射
21
  const userTokenMapping = {};
22
  const usernames = [];
@@ -39,11 +31,15 @@ if (hfUserConfig) {
39
  const ADMIN_USERNAME = process.env.USER_NAME || 'admin';
40
  const ADMIN_PASSWORD = process.env.USER_PASSWORD || 'password';
41
 
 
 
 
 
42
  // 存储会话 token 的简单内存数据库(生产环境中应使用数据库或 Redis)
43
  const sessions = new Map();
44
  const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24小时超时
45
 
46
- // 缓存管理 - 修改后的类
47
  class SpaceCache {
48
  constructor() {
49
  this.spaces = {};
@@ -58,33 +54,53 @@ class SpaceCache {
58
  getAll() {
59
  return Object.values(this.spaces);
60
  }
61
-
62
- // 新增:获取安全的空间数据(不包含token)
63
- getSafeSpaces() {
64
- return Object.values(this.spaces).map(space => {
65
- const { token, ...safeSpace } = space;
66
- return safeSpace;
67
- });
68
- }
69
 
70
  isExpired(expireMinutes = 5) {
71
  if (!this.lastUpdate) return true;
72
  return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);
73
  }
74
-
75
- // 根据repo_id获取单个space的token
76
- getToken(repoId) {
77
- return this.spaces[repoId]?.token || '';
78
  }
79
  }
80
 
81
  const spaceCache = new SpaceCache();
82
 
83
- // 添加API请求日志中间件
84
- app.use('/api/proxy', (req, res, next) => {
85
- console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - IP: ${req.ip}`);
86
- next();
87
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  // 提供静态文件(前端文件)
90
  app.use(express.static(path.join(__dirname, 'public')));
@@ -153,67 +169,111 @@ const authenticateToken = (req, res, next) => {
153
  }
154
  };
155
 
156
- // 获取所有 spaces 列表(包括私有)- 修改后的路由处理函数
157
  app.get('/api/proxy/spaces', async (req, res) => {
158
  try {
159
- if (!spaceCache.isExpired()) {
160
- console.log('从缓存获取 Spaces 数据');
161
- // 返回不包含token的安全数据
162
- return res.json(spaceCache.getSafeSpaces());
163
- }
164
-
165
- const allSpaces = [];
166
- for (const username of usernames) {
167
- const token = userTokenMapping[username];
168
- if (!token) {
169
- console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
 
 
 
 
170
  }
 
 
 
171
 
172
- try {
173
- // 调用 HuggingFace API 获取 Spaces 列表
174
- const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
175
- const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
176
- const spaces = response.data;
177
- console.log(`获取到 ${spaces.length} Spaces for ${username}`);
 
 
 
 
178
 
179
- for (const space of spaces) {
180
- try {
181
- // 获取 Space 详细信息
182
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
183
- const spaceInfo = spaceInfoResponse.data;
184
- const spaceRuntime = spaceInfo.runtime || {};
185
-
186
- // 存储完整信息到缓存(包括token)
187
- allSpaces.push({
188
- repo_id: spaceInfo.id,
189
- name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
190
- owner: spaceInfo.author,
191
- username: username,
192
- token: token || '', // 仍然存储token到缓存中,用于后续API调用
193
- url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
194
- status: spaceRuntime.stage || 'unknown',
195
- last_modified: spaceInfo.lastModified || 'unknown',
196
- created_at: spaceInfo.createdAt || 'unknown',
197
- sdk: spaceInfo.sdk || 'unknown',
198
- tags: spaceInfo.tags || [],
199
- private: spaceInfo.private || false,
200
- app_port: spaceInfo.cardData?.app_port || 'unknown'
201
- });
202
- } catch (error) {
203
- console.error(`处理 Space ${space.id} 失败:`, error.message);
 
 
 
204
  }
 
 
205
  }
206
- } catch (error) {
207
- console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
208
  }
209
- }
210
 
211
- allSpaces.sort((a, b) => a.name.localeCompare(b.name));
212
- spaceCache.updateAll(allSpaces);
213
- console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
214
-
215
- // 返回不包含token的安全数据
216
- res.json(spaceCache.getSafeSpaces());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  } catch (error) {
218
  console.error(`代理获取 spaces 列表失败:`, error.message);
219
  res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
@@ -225,15 +285,14 @@ app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) =>
225
  try {
226
  const { repoId } = req.params;
227
  console.log(`尝试重启 Space: ${repoId}`);
228
-
229
- // 从缓存中获取token,而不是完整的space对象
230
- const token = spaceCache.getToken(repoId);
231
- if (!token) {
232
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
233
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
234
  }
235
 
236
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
237
  const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
238
  console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
239
  res.json({ success: true, message: `Space ${repoId} 重启成功` });
@@ -253,15 +312,14 @@ app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) =>
253
  try {
254
  const { repoId } = req.params;
255
  console.log(`尝试重建 Space: ${repoId}`);
256
-
257
- // 从缓存中获取token,而不是完整的space对象
258
- const token = spaceCache.getToken(repoId);
259
- if (!token) {
260
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
261
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
262
  }
263
 
264
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
265
  // 将 factory_reboot 参数作为查询参数传递,而非请求体
266
  const response = await axios.post(
267
  `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,
@@ -387,56 +445,230 @@ app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
387
  }
388
  });
389
 
390
- // 代理 HuggingFace API:获取实时监控数据(SSE)
391
- app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
392
- try {
393
- const { username, instanceId } = req.params;
394
- const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
 
 
395
 
396
- // 检查实例状态,决定是否继续请求
397
- const spaces = spaceCache.getAll();
398
- const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
399
- if (!space) {
400
- console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
401
- return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
402
- }
403
- if (space.status.toLowerCase() !== 'running') {
404
- console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
405
- return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
406
  }
407
 
408
- const token = userTokenMapping[username];
409
- let headers = {
 
 
 
 
 
 
 
410
  'Accept': 'text/event-stream',
411
  'Cache-Control': 'no-cache',
412
  'Connection': 'keep-alive'
413
  };
414
- if (token) {
415
- headers['Authorization'] = `Bearer ${token}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  }
 
417
 
418
- const response = await axios({
419
- method: 'get',
420
- url,
421
- headers,
422
- responseType: 'stream',
423
- timeout: 10000
 
 
 
 
 
 
 
424
  });
 
425
 
426
- res.set({
427
- 'Content-Type': 'text/event-stream',
428
- 'Cache-Control': 'no-cache',
429
- 'Connection': 'keep-alive'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  });
431
- response.data.pipe(res);
432
 
433
- req.on('close', () => {
434
- response.data.destroy();
 
 
 
 
 
435
  });
436
- } catch (error) {
437
- console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
438
- res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  });
441
 
442
  // 处理其他请求,重定向到 index.html
@@ -455,8 +687,68 @@ setInterval(() => {
455
  }
456
  }, 60 * 60 * 1000); // 每小时清理一次
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  app.listen(port, () => {
459
  console.log(`Server running on port ${port}`);
460
  console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
461
  console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
462
- });
 
 
9
  app.use(express.json());
10
  app.use(express.urlencoded({ extended: true }));
11
 
 
 
 
 
 
 
 
 
12
  // 从环境变量获取 HuggingFace 用户名和对应的 API Token 映射
13
  const userTokenMapping = {};
14
  const usernames = [];
 
31
  const ADMIN_USERNAME = process.env.USER_NAME || 'admin';
32
  const ADMIN_PASSWORD = process.env.USER_PASSWORD || 'password';
33
 
34
+ // 从环境变量获取是否在未登录时展示 private 实例的配置,默认值为 false
35
+ const SHOW_PRIVATE = process.env.SHOW_PRIVATE === 'true';
36
+ console.log(`SHOW_PRIVATE 配置: ${SHOW_PRIVATE ? '未登录时展示 private 实例' : '未登录时隐藏 private 实例'}`);
37
+
38
  // 存储会话 token 的简单内存数据库(生产环境中应使用数据库或 Redis)
39
  const sessions = new Map();
40
  const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24小时超时
41
 
42
+ // 缓存管理
43
  class SpaceCache {
44
  constructor() {
45
  this.spaces = {};
 
54
  getAll() {
55
  return Object.values(this.spaces);
56
  }
 
 
 
 
 
 
 
 
57
 
58
  isExpired(expireMinutes = 5) {
59
  if (!this.lastUpdate) return true;
60
  return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);
61
  }
62
+
63
+ invalidate() {
64
+ this.lastUpdate = null;
 
65
  }
66
  }
67
 
68
  const spaceCache = new SpaceCache();
69
 
70
+ // 用于获取 Spaces 数据的函数,带有重试机制
71
+ async function fetchSpacesWithRetry(username, token, maxRetries = 3, retryDelay = 2000) {
72
+ let retries = 0;
73
+ while (retries < maxRetries) {
74
+ try {
75
+ // 仅在 token 存在时添加 Authorization 头
76
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
77
+ const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, {
78
+ headers,
79
+ timeout: 10000 // 设置 10 秒超时
80
+ });
81
+ const spaces = response.data;
82
+ console.log(`获取到 ${spaces.length} 个 Spaces for ${username} (尝试 ${retries + 1}/${maxRetries}),使用 ${token ? 'Token 认证' : '无认证'}`);
83
+ return spaces;
84
+ } catch (error) {
85
+ retries++;
86
+ let errorDetail = error.message;
87
+ if (error.response) {
88
+ errorDetail += `, HTTP Status: ${error.response.status}`;
89
+ } else if (error.request) {
90
+ errorDetail += ', No response received (possible network issue)';
91
+ }
92
+ console.error(`获取 Spaces 列表失败 for ${username} (尝试 ${retries}/${maxRetries}): ${errorDetail},使用 ${token ? 'Token 认证' : '无认证'}`);
93
+ if (retries < maxRetries) {
94
+ console.log(`等待 ${retryDelay/1000} 秒后重试...`);
95
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
96
+ } else {
97
+ console.error(`达到最大重试次数 (${maxRetries}),放弃重试 for ${username}`);
98
+ return [];
99
+ }
100
+ }
101
+ }
102
+ return [];
103
+ }
104
 
105
  // 提供静态文件(前端文件)
106
  app.use(express.static(path.join(__dirname, 'public')));
 
169
  }
170
  };
171
 
172
+ // 获取所有 spaces 列表(包括私有)
173
  app.get('/api/proxy/spaces', async (req, res) => {
174
  try {
175
+ // 检查是否登录
176
+ let isAuthenticated = false;
177
+ const authHeader = req.headers['authorization'];
178
+ if (authHeader && authHeader.startsWith('Bearer ')) {
179
+ const token = authHeader.split(' ')[1];
180
+ const session = sessions.get(token);
181
+ if (session && session.expiresAt > Date.now()) {
182
+ isAuthenticated = true;
183
+ console.log(`用户已登录,Token: ${token.slice(0, 8)}...`);
184
+ } else {
185
+ if (session) {
186
+ sessions.delete(token); // 删除过期的 token
187
+ console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访��`);
188
+ }
189
+ console.log('用户认证失败,无有效 Token');
190
  }
191
+ } else {
192
+ console.log('用户未提供认证令牌');
193
+ }
194
 
195
+ // 如果缓存为空或已过期,强制重新获取数据
196
+ const cachedSpaces = spaceCache.getAll();
197
+ if (cachedSpaces.length === 0 || spaceCache.isExpired()) {
198
+ console.log(cachedSpaces.length === 0 ? '缓存为空,强制重新获取数据' : '缓存已过期,重新获取数据');
199
+ const allSpaces = [];
200
+ for (const username of usernames) {
201
+ const token = userTokenMapping[username];
202
+ if (!token) {
203
+ console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
204
+ }
205
 
206
+ try {
207
+ const spaces = await fetchSpacesWithRetry(username, token);
208
+ for (const space of spaces) {
209
+ try {
210
+ // 仅在 token 存在时添加 Authorization 头
211
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
212
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
213
+ const spaceInfo = spaceInfoResponse.data;
214
+ const spaceRuntime = spaceInfo.runtime || {};
215
+
216
+ allSpaces.push({
217
+ repo_id: spaceInfo.id,
218
+ name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
219
+ owner: spaceInfo.author,
220
+ username: username,
221
+ url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
222
+ status: spaceRuntime.stage || 'unknown',
223
+ last_modified: spaceInfo.lastModified || 'unknown',
224
+ created_at: spaceInfo.createdAt || 'unknown',
225
+ sdk: spaceInfo.sdk || 'unknown',
226
+ tags: spaceInfo.tags || [],
227
+ private: spaceInfo.private || false,
228
+ app_port: spaceInfo.cardData?.app_port || 'unknown',
229
+ short_description: spaceInfo.cardData?.short_description || '' // 新增字段,确保为空时返回空字符串
230
+ });
231
+ } catch (error) {
232
+ console.error(`处理 Space ${space.id} 失败:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`);
233
+ }
234
  }
235
+ } catch (error) {
236
+ console.error(`获取 Spaces 列表失败 for ${username}:`, error.message, `使用 ${token ? 'Token 认证' : '无认证'}`);
237
  }
 
 
238
  }
 
239
 
240
+ allSpaces.sort((a, b) => a.name.localeCompare(b.name));
241
+ spaceCache.updateAll(allSpaces);
242
+ console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
243
+
244
+ const safeSpaces = allSpaces.map(space => {
245
+ const { token, ...safeSpace } = space;
246
+ return safeSpace;
247
+ });
248
+
249
+ if (isAuthenticated) {
250
+ console.log('用户已登录,返回所有实例(包括 private)');
251
+ res.json(safeSpaces);
252
+ } else if (SHOW_PRIVATE) {
253
+ console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有实例');
254
+ res.json(safeSpaces);
255
+ } else {
256
+ console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例');
257
+ res.json(safeSpaces.filter(space => !space.private));
258
+ }
259
+ } else {
260
+ console.log('从缓存获取 Spaces 数据');
261
+ const safeSpaces = cachedSpaces.map(space => {
262
+ const { token, ...safeSpace } = space;
263
+ return safeSpace;
264
+ });
265
+
266
+ if (isAuthenticated) {
267
+ console.log('用户已登录,返回所有缓存实例(包括 private)');
268
+ return res.json(safeSpaces);
269
+ } else if (SHOW_PRIVATE) {
270
+ console.log('用户未登录,但 SHOW_PRIVATE 为 true,返回所有缓存实例');
271
+ return res.json(safeSpaces);
272
+ } else {
273
+ console.log('用户未登录,SHOW_PRIVATE 为 false,过滤 private 实例');
274
+ return res.json(safeSpaces.filter(space => !space.private));
275
+ }
276
+ }
277
  } catch (error) {
278
  console.error(`代理获取 spaces 列表失败:`, error.message);
279
  res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
 
285
  try {
286
  const { repoId } = req.params;
287
  console.log(`尝试重启 Space: ${repoId}`);
288
+ const spaces = spaceCache.getAll();
289
+ const space = spaces.find(s => s.repo_id === repoId);
290
+ if (!space || !userTokenMapping[space.username]) {
 
291
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
292
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
293
  }
294
 
295
+ const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };
296
  const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
297
  console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
298
  res.json({ success: true, message: `Space ${repoId} 重启成功` });
 
312
  try {
313
  const { repoId } = req.params;
314
  console.log(`尝试重建 Space: ${repoId}`);
315
+ const spaces = spaceCache.getAll();
316
+ const space = spaces.find(s => s.repo_id === repoId);
317
+ if (!space || !userTokenMapping[space.username]) {
 
318
  console.error(`Space ${repoId} 未找到或无 Token 配置`);
319
  return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
320
  }
321
 
322
+ const headers = { 'Authorization': `Bearer ${userTokenMapping[space.username]}`, 'Content-Type': 'application/json' };
323
  // 将 factory_reboot 参数作为查询参数传递,而非请求体
324
  const response = await axios.post(
325
  `https://huggingface.co/api/spaces/${repoId}/restart?factory=true`,
 
445
  }
446
  });
447
 
448
+ // 监控数据管理类
449
+ class MetricsConnectionManager {
450
+ constructor() {
451
+ this.connections = new Map(); // 存储 HuggingFace API 的监控连接
452
+ this.clients = new Map(); // 存储前端客户端的 SSE 连接
453
+ this.instanceData = new Map(); // 存储每个实例的最新监控数据
454
+ }
455
 
456
+ // 建立到 HuggingFace API 的监控连接
457
+ async connectToInstance(repoId, username, token) {
458
+ if (this.connections.has(repoId)) {
459
+ return this.connections.get(repoId);
 
 
 
 
 
 
460
  }
461
 
462
+ const instanceId = repoId.split('/')[1];
463
+ const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
464
+ // 仅在 token 存在且非空时添加 Authorization 头
465
+ const headers = token ? {
466
+ 'Authorization': `Bearer ${token}`,
467
+ 'Accept': 'text/event-stream',
468
+ 'Cache-Control': 'no-cache',
469
+ 'Connection': 'keep-alive'
470
+ } : {
471
  'Accept': 'text/event-stream',
472
  'Cache-Control': 'no-cache',
473
  'Connection': 'keep-alive'
474
  };
475
+
476
+ try {
477
+ const response = await axios({
478
+ method: 'get',
479
+ url,
480
+ headers,
481
+ responseType: 'stream',
482
+ timeout: 10000
483
+ });
484
+
485
+ const stream = response.data;
486
+ stream.on('data', (chunk) => {
487
+ const chunkStr = chunk.toString();
488
+ if (chunkStr.includes('event: metric')) {
489
+ const dataMatch = chunkStr.match(/data: (.*)/);
490
+ if (dataMatch && dataMatch[1]) {
491
+ try {
492
+ const metrics = JSON.parse(dataMatch[1]);
493
+ this.instanceData.set(repoId, metrics);
494
+ // 推送给所有订阅了该实例的客户端
495
+ this.clients.forEach((clientRes, clientId) => {
496
+ if (clientRes.subscribedInstances && clientRes.subscribedInstances.includes(repoId)) {
497
+ clientRes.write(`event: metric\n`);
498
+ clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
499
+ }
500
+ });
501
+ } catch (error) {
502
+ console.error(`解析监控数据失败 (${repoId}):`, error.message);
503
+ }
504
+ }
505
+ }
506
+ });
507
+
508
+ stream.on('error', (error) => {
509
+ console.error(`监控连接错误 (${repoId}):`, error.message);
510
+ this.connections.delete(repoId);
511
+ this.instanceData.delete(repoId);
512
+ });
513
+
514
+ stream.on('end', () => {
515
+ console.log(`监控连接结束 (${repoId})`);
516
+ this.connections.delete(repoId);
517
+ this.instanceData.delete(repoId);
518
+ });
519
+
520
+ this.connections.set(repoId, stream);
521
+ console.log(`已建立监控连接 (${repoId}),使用 ${token ? 'Token 认证' : '无认证'}`);
522
+ return stream;
523
+ } catch (error) {
524
+ console.error(`无法连接到监控端点 (${repoId}):`, error.message);
525
+ this.connections.delete(repoId);
526
+ return null;
527
  }
528
+ }
529
 
530
+ // 注册前端客户端的 SSE 连接
531
+ registerClient(clientId, res, subscribedInstances) {
532
+ res.subscribedInstances = subscribedInstances || [];
533
+ this.clients.set(clientId, res);
534
+ console.log(`客户端 ${clientId} 注册,订阅实例: ${res.subscribedInstances.join(', ') || '无'}`);
535
+
536
+ // 首次连接时,推送已缓存的最新数据
537
+ res.subscribedInstances.forEach(repoId => {
538
+ if (this.instanceData.has(repoId)) {
539
+ const metrics = this.instanceData.get(repoId);
540
+ res.write(`event: metric\n`);
541
+ res.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
542
+ }
543
  });
544
+ }
545
 
546
+ // 客户端断开连接
547
+ unregisterClient(clientId) {
548
+ this.clients.delete(clientId);
549
+ console.log(`客户端 ${clientId} 断开连接`);
550
+ this.cleanupConnections();
551
+ }
552
+
553
+ // 更新客户端订阅的实例列表
554
+ updateClientSubscriptions(clientId, subscribedInstances) {
555
+ const clientRes = this.clients.get(clientId);
556
+ if (clientRes) {
557
+ clientRes.subscribedInstances = subscribedInstances || [];
558
+ console.log(`客户端 ${clientId} 更新订阅: ${clientRes.subscribedInstances.join(', ') || '无'}`);
559
+ // 更新后推送最新的缓存数据
560
+ subscribedInstances.forEach(repoId => {
561
+ if (this.instanceData.has(repoId)) {
562
+ const metrics = this.instanceData.get(repoId);
563
+ clientRes.write(`event: metric\n`);
564
+ clientRes.write(`data: ${JSON.stringify({ repoId, metrics })}\n\n`);
565
+ }
566
+ });
567
+ }
568
+ this.cleanupConnections();
569
+ }
570
+
571
+ // 清理未被任何客户端订阅的连接
572
+ cleanupConnections() {
573
+ const subscribedRepoIds = new Set();
574
+ this.clients.forEach(clientRes => {
575
+ clientRes.subscribedInstances.forEach(repoId => subscribedRepoIds.add(repoId));
576
  });
 
577
 
578
+ const toRemove = [];
579
+ this.connections.forEach((stream, repoId) => {
580
+ if (!subscribedRepoIds.has(repoId)) {
581
+ toRemove.push(repoId);
582
+ stream.destroy();
583
+ console.log(`清理未订阅的监控连接 (${repoId})`);
584
+ }
585
  });
586
+
587
+ toRemove.forEach(repoId => {
588
+ this.connections.delete(repoId);
589
+ this.instanceData.delete(repoId);
590
+ });
591
+ }
592
+ }
593
+
594
+ const metricsManager = new MetricsConnectionManager();
595
+
596
+ // 新增统一监控数据的SSE端点
597
+ app.get('/api/proxy/live-metrics-stream', (req, res) => {
598
+ // 设置 SSE 所需的响应头
599
+ res.set({
600
+ 'Content-Type': 'text/event-stream',
601
+ 'Cache-Control': 'no-cache',
602
+ 'Connection': 'keep-alive'
603
+ });
604
+
605
+ // 生成唯一的客户端ID
606
+ const clientId = crypto.randomBytes(8).toString('hex');
607
+
608
+ // 获取查询参数中的实例列表和 token
609
+ const instancesParam = req.query.instances || '';
610
+ const token = req.query.token || '';
611
+ const subscribedInstances = instancesParam.split(',').filter(id => id.trim() !== '');
612
+
613
+ // 检查登录状态
614
+ let isAuthenticated = false;
615
+ if (token) {
616
+ const session = sessions.get(token);
617
+ if (session && session.expiresAt > Date.now()) {
618
+ isAuthenticated = true;
619
+ console.log(`SSE 用户已登录,Token: ${token.slice(0, 8)}...`);
620
+ } else {
621
+ if (session) {
622
+ sessions.delete(token);
623
+ console.log(`SSE Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
624
+ }
625
+ console.log('SSE 用户认证失败,无有效 Token');
626
+ }
627
+ } else {
628
+ console.log('SSE 用户未提供认证令牌');
629
  }
630
+
631
+ // 注册客户端
632
+ metricsManager.registerClient(clientId, res, subscribedInstances);
633
+
634
+ // 根据订阅列表建立监控连接
635
+ const spaces = spaceCache.getAll();
636
+ subscribedInstances.forEach(repoId => {
637
+ const space = spaces.find(s => s.repo_id === repoId);
638
+ if (space) {
639
+ const username = space.username;
640
+ const token = userTokenMapping[username] || '';
641
+ metricsManager.connectToInstance(repoId, username, token);
642
+ }
643
+ });
644
+
645
+ // 监听客户端断开连接
646
+ req.on('close', () => {
647
+ metricsManager.unregisterClient(clientId);
648
+ console.log(`客户端 ${clientId} 断开 SSE 连接`);
649
+ });
650
+ });
651
+
652
+ // 新增接口:更新客户端订阅的实例列表
653
+ app.post('/api/proxy/update-subscriptions', (req, res) => {
654
+ const { clientId, instances } = req.body;
655
+ if (!clientId || !instances || !Array.isArray(instances)) {
656
+ return res.status(400).json({ error: '缺少 clientId 或 instances 参数' });
657
+ }
658
+
659
+ metricsManager.updateClientSubscriptions(clientId, instances);
660
+ // 根据新订阅列表建立监控连接
661
+ const spaces = spaceCache.getAll();
662
+ instances.forEach(repoId => {
663
+ const space = spaces.find(s => s.repo_id === repoId);
664
+ if (space) {
665
+ const username = space.username;
666
+ const token = userTokenMapping[username] || '';
667
+ metricsManager.connectToInstance(repoId, username, token);
668
+ }
669
+ });
670
+
671
+ res.json({ success: true, message: '订阅列表已更新' });
672
  });
673
 
674
  // 处理其他请求,重定向到 index.html
 
687
  }
688
  }, 60 * 60 * 1000); // 每小时清理一次
689
 
690
+ // 定时刷新缓存任务
691
+ const REFRESH_INTERVAL = 5 * 60 * 1000; // 每 5 分钟检查一次
692
+ async function refreshSpacesCachePeriodically() {
693
+ console.log('启动定时刷新缓存任务...');
694
+ setInterval(async () => {
695
+ try {
696
+ const cachedSpaces = spaceCache.getAll();
697
+ if (spaceCache.isExpired() || cachedSpaces.length === 0) {
698
+ console.log('定时任务:缓存已过期或为空,重新获取 Spaces 数据');
699
+ const allSpaces = [];
700
+ for (const username of usernames) {
701
+ const token = userTokenMapping[username];
702
+ if (!token) {
703
+ console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
704
+ }
705
+ try {
706
+ const spaces = await fetchSpacesWithRetry(username, token);
707
+ for (const space of spaces) {
708
+ try {
709
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
710
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
711
+ const spaceInfo = spaceInfoResponse.data;
712
+ const spaceRuntime = spaceInfo.runtime || {};
713
+
714
+ allSpaces.push({
715
+ repo_id: spaceInfo.id,
716
+ name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
717
+ owner: spaceInfo.author,
718
+ username: username,
719
+ url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
720
+ status: spaceRuntime.stage || 'unknown',
721
+ last_modified: spaceInfo.lastModified || 'unknown',
722
+ created_at: spaceInfo.createdAt || 'unknown',
723
+ sdk: spaceInfo.sdk || 'unknown',
724
+ tags: spaceInfo.tags || [],
725
+ private: spaceInfo.private || false,
726
+ app_port: spaceInfo.cardData?.app_port || 'unknown',
727
+ short_description: spaceInfo.cardData?.short_description || '' // 新增字段,确保为空时返回空字符串
728
+ });
729
+ } catch (error) {
730
+ console.error(`处理 Space ${space.id} 失败:`, error.message);
731
+ }
732
+ }
733
+ } catch (error) {
734
+ console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
735
+ }
736
+ }
737
+ allSpaces.sort((a, b) => a.name.localeCompare(b.name));
738
+ spaceCache.updateAll(allSpaces);
739
+ console.log(`定时任务:总共获取到 ${allSpaces.length} 个 Spaces,缓存已更新`);
740
+ } else {
741
+ console.log('定时任务:缓存有效且不为空,无需更新');
742
+ }
743
+ } catch (error) {
744
+ console.error('定时任务:刷新缓存失败:', error.message);
745
+ }
746
+ }, REFRESH_INTERVAL);
747
+ }
748
+
749
  app.listen(port, () => {
750
  console.log(`Server running on port ${port}`);
751
  console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
752
  console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
753
+ refreshSpacesCachePeriodically(); // 启动定时任务
754
+ });