i0110 commited on
Commit
4b68b82
·
verified ·
1 Parent(s): b62daec

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +385 -0
server.js CHANGED
@@ -216,6 +216,391 @@ app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) =>
216
  }
217
  });
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  // 代理重建 Space(需要认证)
220
  app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
221
  try {
 
216
  }
217
  });
218
 
219
+ // 代理重建 Space(需要认证)
220
+ app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
221
+ try {
222
+ const { repoId } = req.params;
223
+ console.log(`尝试重建 Space: ${repoId}`);
224
+ const spaces = spaceCache.getAll();
225
+ const space = spaces.find(s => s.repo_id === repoId);
226
+ if (!space || !space.token) {
227
+ console.error(`Space ${repoId} 未找到或无 Token 配置`);
228
+ return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
229
+ }
230
+
231
+ const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
232
+ const payload = { factory_reboot: true }; // 修改为官方 API 文档指定的字段名
233
+ console.log(`发送重建请求,payload: ${JSON.stringify(payload)}`);
234
+ const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
235
+ console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);
236
+ res.json({ success: true, message: `Space ${repoId} 重建成功` });
237
+ } catch (error) {
238
+ console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
239
+ if (error.response) {
240
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
241
+ res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });
242
+ } else {
243
+ res.status(500).json({ error: '重建 space 失败', details: error.message });
244
+ }
245
+ }
246
+ });
247
+
248
+ // 外部 API 服务(类似于 Flask 的 /api/v1)
249
+ app.get('/api/v1/info/:token', async (req, res) => {
250
+ try {
251
+ const { token } = req.params;
252
+ const authHeader = req.headers.authorization;
253
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
254
+ return res.status(401).json({ error: '无效的 API 密钥' });
255
+ }
256
+
257
+ const headers = { 'Authorization': `Bearer ${token}` };
258
+ const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
259
+ const username = userInfoResponse.data.name;
260
+ const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
261
+ const spaces = spacesResponse.data;
262
+ const spaceList = [];
263
+
264
+ for (const space of spaces) {
265
+ try {
266
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
267
+ spaceList.push(spaceInfoResponse.data.id);
268
+ } catch (error) {
269
+ console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
270
+ }
271
+ }
272
+
273
+ res.json({ spaces: spaceList, total: spaceList.length });
274
+ } catch (error) {
275
+ console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
276
+ res.status(500).json({ error: error.message });
277
+ }
278
+ });
279
+
280
+ app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
281
+ try {
282
+ const { token, spaceId } = req.params;
283
+ const authHeader = req.headers.authorization;
284
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
285
+ return res.status(401).json({ error: '无效的 API 密钥' });
286
+ }
287
+
288
+ const headers = { 'Authorization': `Bearer ${token}` };
289
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
290
+ const spaceInfo = spaceInfoResponse.data;
291
+ const spaceRuntime = spaceInfo.runtime || {};
292
+
293
+ res.json({
294
+ id: spaceInfo.id,
295
+ status: spaceRuntime.stage || 'unknown',
296
+ last_modified: spaceInfo.lastModified || null,
297
+ created_at: spaceInfo.createdAt || null,
298
+ sdk: spaceInfo.sdk || 'unknown',
299
+ tags: spaceInfo.tags || [],
300
+ private: spaceInfo.private || false
301
+ });
302
+ } catch (error) {
303
+ console.error(`获取 space 信息失败 (外部 API):`, error.message);
304
+ res.status(error.response?.status || 404).json({ error: error.message });
305
+ }
306
+ });
307
+
308
+ app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
309
+ try {
310
+ const { token, spaceId } = req.params;
311
+ const authHeader = req.headers.authorization;
312
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
313
+ return res.status(401).json({ error: '无效的 API 密钥' });
314
+ }
315
+
316
+ const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
317
+ await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });
318
+ res.json({ success: true, message: `Space ${spaceId} 重启成功` });
319
+ } catch (error) {
320
+ console.error(`重启 space 失败 (外部 API):`, error.message);
321
+ res.status(error.response?.status || 500).json({ success: false, error: error.message });
322
+ }
323
+ });
324
+
325
+ app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
326
+ try {
327
+ const { token, spaceId } = req.params;
328
+ const authHeader = req.headers.authorization;
329
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
330
+ return res.status(401).json({ error: '无效的 API 密钥' });
331
+ }
332
+
333
+ const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
334
+ const payload = { factory_reboot: true }; // 修改为官方 API 文档指定的字段名
335
+ console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);
336
+ const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
337
+ console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);
338
+ res.json({ success: true, message: `Space ${spaceId} 重建成功` });
339
+ } catch (error) {
340
+ console.error(`重建 space 失败 (外部 API):`, error.message);
341
+ if (error.response) {
342
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
343
+ res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
344
+ } else {
345
+ res.status(500).json({ success: false, error: error.message });
346
+ }
347
+ }
348
+ });
349
+
350
+ // 代理 HuggingFace API:获取实时监控数据(SSE)
351
+ app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
352
+ try {
353
+ const { username, instanceId } = req.params;
354
+ const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
355
+
356
+ // 检查实例状态,决定是否继续请求
357
+ const spaces = spaceCache.getAll();
358
+ const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
359
+ if (!space) {
360
+ console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
361
+ return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
362
+ }
363
+ if (space.status.toLowerCase() !== 'running') {
364
+ console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
365
+ return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
366
+ }
367
+
368
+ const token = userTokenMapping[username];
369
+ let headers = {
370
+ 'Accept': 'text/event-stream',
371
+ 'Cache-Control': 'no-cache',
372
+ 'Connection': 'keep-alive'
373
+ };
374
+ if (token) {
375
+ headers['Authorization'] = `Bearer ${token}`;
376
+ }
377
+
378
+ const response = await axios({
379
+ method: 'get',
380
+ url,
381
+ headers,
382
+ responseType: 'stream',
383
+ timeout: 10000
384
+ });
385
+
386
+ res.set({
387
+ 'Content-Type': 'text/event-stream',
388
+ 'Cache-Control': 'no-cache',
389
+ 'Connection': 'keep-alive'
390
+ });
391
+ response.data.pipe(res);
392
+
393
+ req.on('close', () => {
394
+ response.data.destroy();
395
+ });
396
+ } catch (error) {
397
+ console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
398
+ res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
399
+ }
400
+ });
401
+
402
+ // 处理其他请求,重定向到 index.html
403
+ app.get('*', (req, res) => {
404
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
405
+ });
406
+
407
+ // 定期清理过期的会话
408
+ setInterval(() => {
409
+ const now = Date.now();
410
+ for (const [token, session] of sessions.entries()) {
411
+ if (session.expiresAt < now) {
412
+ sessions.delete(token);
413
+ console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);
414
+ }
415
+ }
416
+ }, 60 * 60 * 1000); // 每小时清理一次
417
+
418
+ app.listen(port, () => {
419
+ console.log(`Server running on port ${port}`);
420
+ console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
421
+ console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
422
+ });
423
+ // 缓存管理
424
+ class SpaceCache {
425
+ constructor() {
426
+ this.spaces = {};
427
+ this.lastUpdate = null;
428
+ }
429
+
430
+ updateAll(spacesData) {
431
+ this.spaces = spacesData.reduce((acc, space) => ({ ...acc, [space.repo_id]: space }), {});
432
+ this.lastUpdate = Date.now();
433
+ }
434
+
435
+ getAll() {
436
+ return Object.values(this.spaces);
437
+ }
438
+
439
+ isExpired(expireMinutes = 5) {
440
+ if (!this.lastUpdate) return true;
441
+ return (Date.now() - this.lastUpdate) > (expireMinutes * 60 * 1000);
442
+ }
443
+ }
444
+
445
+ const spaceCache = new SpaceCache();
446
+
447
+ // 提供静态文件(前端文件)
448
+ app.use(express.static(path.join(__dirname, 'public')));
449
+
450
+ // 提供配置信息的 API 接口
451
+ app.get('/api/config', (req, res) => {
452
+ res.json({ usernames: usernames.join(',') });
453
+ });
454
+
455
+ // 登录 API 接口
456
+ app.post('/api/login', (req, res) => {
457
+ const { username, password } = req.body;
458
+ if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
459
+ // 生成一个随机 token 作为会话标识
460
+ const token = crypto.randomBytes(16).toString('hex');
461
+ const expiresAt = Date.now() + SESSION_TIMEOUT;
462
+ sessions.set(token, { username, expiresAt });
463
+ console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);
464
+ res.json({ success: true, token });
465
+ } else {
466
+ console.log(`用户 ${username} 登录失败,凭据无效`);
467
+ res.status(401).json({ success: false, message: '用户名或密码错误' });
468
+ }
469
+ });
470
+
471
+ // 验证登录状态 API 接口
472
+ app.post('/api/verify-token', (req, res) => {
473
+ const { token } = req.body;
474
+ const session = sessions.get(token);
475
+ if (session && session.expiresAt > Date.now()) {
476
+ res.json({ success: true, message: 'Token 有效' });
477
+ } else {
478
+ if (session) {
479
+ sessions.delete(token); // 删除过期的 token
480
+ console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);
481
+ }
482
+ res.status(401).json({ success: false, message: 'Token 无效或已过期' });
483
+ }
484
+ });
485
+
486
+ // 登出 API 接口
487
+ app.post('/api/logout', (req, res) => {
488
+ const { token } = req.body;
489
+ sessions.delete(token);
490
+ console.log(`Token ${token.slice(0, 8)}... 已手动登出`);
491
+ res.json({ success: true, message: '登出成功' });
492
+ });
493
+
494
+ // 中间件:验证请求中的 token
495
+ const authenticateToken = (req, res, next) => {
496
+ const authHeader = req.headers['authorization'];
497
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
498
+ return res.status(401).json({ error: '未提供有效的认证令牌' });
499
+ }
500
+ const token = authHeader.split(' ')[1];
501
+ const session = sessions.get(token);
502
+ if (session && session.expiresAt > Date.now()) {
503
+ req.session = session;
504
+ next();
505
+ } else {
506
+ if (session) {
507
+ sessions.delete(token); // 删除过期的 token
508
+ console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
509
+ }
510
+ return res.status(401).json({ error: '认证令牌无效或已过期' });
511
+ }
512
+ };
513
+
514
+ // 获取所有 spaces 列表(包括私有)
515
+ app.get('/api/proxy/spaces', async (req, res) => {
516
+ try {
517
+ if (!spaceCache.isExpired()) {
518
+ console.log('从缓存获取 Spaces 数据');
519
+ return res.json(spaceCache.getAll());
520
+ }
521
+
522
+ const allSpaces = [];
523
+ for (const username of usernames) {
524
+ const token = userTokenMapping[username];
525
+ if (!token) {
526
+ console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
527
+ }
528
+
529
+ try {
530
+ // 调用 HuggingFace API 获取 Spaces 列表
531
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
532
+ const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
533
+ const spaces = response.data;
534
+ console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
535
+
536
+ for (const space of spaces) {
537
+ try {
538
+ // 获取 Space 详细信息
539
+ const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
540
+ const spaceInfo = spaceInfoResponse.data;
541
+ const spaceRuntime = spaceInfo.runtime || {};
542
+
543
+ allSpaces.push({
544
+ repo_id: spaceInfo.id,
545
+ name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
546
+ owner: spaceInfo.author,
547
+ username: username,
548
+ token: token || '',
549
+ url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
550
+ status: spaceRuntime.stage || 'unknown',
551
+ last_modified: spaceInfo.lastModified || 'unknown',
552
+ created_at: spaceInfo.createdAt || 'unknown',
553
+ sdk: spaceInfo.sdk || 'unknown',
554
+ tags: spaceInfo.tags || [],
555
+ private: spaceInfo.private || false,
556
+ app_port: spaceInfo.cardData?.app_port || 'unknown'
557
+ });
558
+ } catch (error) {
559
+ console.error(`处理 Space ${space.id} 失败:`, error.message);
560
+ }
561
+ }
562
+ } catch (error) {
563
+ console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
564
+ }
565
+ }
566
+
567
+ allSpaces.sort((a, b) => a.name.localeCompare(b.name));
568
+ spaceCache.updateAll(allSpaces);
569
+ console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
570
+ res.json(allSpaces);
571
+ } catch (error) {
572
+ console.error(`代理获取 spaces 列表失败:`, error.message);
573
+ res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
574
+ }
575
+ });
576
+
577
+ // 代理重启 Space(需要认证)
578
+ app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
579
+ try {
580
+ const { repoId } = req.params;
581
+ console.log(`尝试重启 Space: ${repoId}`);
582
+ const spaces = spaceCache.getAll();
583
+ const space = spaces.find(s => s.repo_id === repoId);
584
+ if (!space || !space.token) {
585
+ console.error(`Space ${repoId} 未找到或无 Token 配置`);
586
+ return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
587
+ }
588
+
589
+ const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
590
+ const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
591
+ console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
592
+ res.json({ success: true, message: `Space ${repoId} 重启成功` });
593
+ } catch (error) {
594
+ console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
595
+ if (error.response) {
596
+ console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
597
+ res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });
598
+ } else {
599
+ res.status(500).json({ error: '重启 space 失败', details: error.message });
600
+ }
601
+ }
602
+ });
603
+
604
  // 代理重建 Space(需要认证)
605
  app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
606
  try {