i0110 commited on
Commit
a86e672
·
verified ·
1 Parent(s): 4c47c41

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +0 -1411
server.js CHANGED
@@ -419,1415 +419,4 @@ 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
- // 提供静态文件(前端文件)
425
- app.use(express.static(path.join(__dirname, 'public')));
426
-
427
- // 提供配置信息的 API 接口
428
- app.get('/api/config', (req, res) => {
429
- res.json({ usernames: usernames.join(',') });
430
- });
431
-
432
- // 登录 API 接口
433
- app.post('/api/login', (req, res) => {
434
- const { username, password } = req.body;
435
- if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
436
- // 生成一个随机 token 作为会话标识
437
- const token = crypto.randomBytes(16).toString('hex');
438
- const expiresAt = Date.now() + SESSION_TIMEOUT;
439
- sessions.set(token, { username, expiresAt });
440
- console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);
441
- res.json({ success: true, token });
442
- } else {
443
- console.log(`用户 ${username} 登录失败,凭据无效`);
444
- res.status(401).json({ success: false, message: '用户名或密码错误' });
445
- }
446
- });
447
-
448
- // 验证登录状态 API 接口
449
- app.post('/api/verify-token', (req, res) => {
450
- const { token } = req.body;
451
- const session = sessions.get(token);
452
- if (session && session.expiresAt > Date.now()) {
453
- res.json({ success: true, message: 'Token 有效' });
454
- } else {
455
- if (session) {
456
- sessions.delete(token); // 删除过期的 token
457
- console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);
458
- }
459
- res.status(401).json({ success: false, message: 'Token 无效或已过期' });
460
- }
461
- });
462
-
463
- // 登出 API 接口
464
- app.post('/api/logout', (req, res) => {
465
- const { token } = req.body;
466
- sessions.delete(token);
467
- console.log(`Token ${token.slice(0, 8)}... 已手动登出`);
468
- res.json({ success: true, message: '登出成功' });
469
- });
470
-
471
- // 中间件:验证请求中的 token
472
- const authenticateToken = (req, res, next) => {
473
- const authHeader = req.headers['authorization'];
474
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
475
- return res.status(401).json({ error: '未提供有效的认证令牌' });
476
- }
477
- const token = authHeader.split(' ')[1];
478
- const session = sessions.get(token);
479
- if (session && session.expiresAt > Date.now()) {
480
- req.session = session;
481
- next();
482
- } else {
483
- if (session) {
484
- sessions.delete(token); // 删除过期的 token
485
- console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
486
- }
487
- return res.status(401).json({ error: '认证令牌无效或已过期' });
488
- }
489
- };
490
-
491
- // 获取所有 spaces 列表(包括私有)
492
- app.get('/api/proxy/spaces', async (req, res) => {
493
- try {
494
- if (!spaceCache.isExpired()) {
495
- console.log('从缓存获取 Spaces 数据');
496
- return res.json(spaceCache.getAll());
497
- }
498
-
499
- const allSpaces = [];
500
- for (const username of usernames) {
501
- const token = userTokenMapping[username];
502
- if (!token) {
503
- console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
504
- }
505
-
506
- try {
507
- // 调用 HuggingFace API 获取 Spaces 列表
508
- const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
509
- const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
510
- const spaces = response.data;
511
- console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
512
-
513
- for (const space of spaces) {
514
- try {
515
- // 获取 Space 详细信息
516
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
517
- const spaceInfo = spaceInfoResponse.data;
518
- const spaceRuntime = spaceInfo.runtime || {};
519
-
520
- allSpaces.push({
521
- repo_id: spaceInfo.id,
522
- name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
523
- owner: spaceInfo.author,
524
- username: username,
525
- token: token || '',
526
- url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
527
- status: spaceRuntime.stage || 'unknown',
528
- last_modified: spaceInfo.lastModified || 'unknown',
529
- created_at: spaceInfo.createdAt || 'unknown',
530
- sdk: spaceInfo.sdk || 'unknown',
531
- tags: spaceInfo.tags || [],
532
- private: spaceInfo.private || false,
533
- app_port: spaceInfo.cardData?.app_port || 'unknown'
534
- });
535
- } catch (error) {
536
- console.error(`处理 Space ${space.id} 失败:`, error.message);
537
- }
538
- }
539
- } catch (error) {
540
- console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
541
- }
542
- }
543
-
544
- allSpaces.sort((a, b) => a.name.localeCompare(b.name));
545
- spaceCache.updateAll(allSpaces);
546
- console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
547
- res.json(allSpaces);
548
- } catch (error) {
549
- console.error(`代理获取 spaces 列表失败:`, error.message);
550
- res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
551
- }
552
- });
553
-
554
- // 代理重启 Space(需要认证)
555
- app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
556
- try {
557
- const { repoId } = req.params;
558
- console.log(`尝试重启 Space: ${repoId}`);
559
- const spaces = spaceCache.getAll();
560
- const space = spaces.find(s => s.repo_id === repoId);
561
- if (!space || !space.token) {
562
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
563
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
564
- }
565
-
566
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
567
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
568
- console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
569
- res.json({ success: true, message: `Space ${repoId} 重启成功` });
570
- } catch (error) {
571
- console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
572
- if (error.response) {
573
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
574
- res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });
575
- } else {
576
- res.status(500).json({ error: '重启 space 失败', details: error.message });
577
- }
578
- }
579
- });
580
-
581
- // 代理重建 Space(需要认证)
582
- app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
583
- try {
584
- const { repoId } = req.params;
585
- console.log(`尝试重建 Space: ${repoId}`);
586
- const spaces = spaceCache.getAll();
587
- const space = spaces.find(s => s.repo_id === repoId);
588
- if (!space || !space.token) {
589
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
590
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
591
- }
592
-
593
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
594
- const payload = { factoryReboot: true };
595
- console.log(`发送重建请求,payload: ${JSON.stringify(payload)}`);
596
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
597
- console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);
598
- res.json({ success: true, message: `Space ${repoId} 重建成功` });
599
- } catch (error) {
600
- console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
601
- if (error.response) {
602
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
603
- res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });
604
- } else {
605
- res.status(500).json({ error: '重建 space 失败', details: error.message });
606
- }
607
- }
608
- });
609
-
610
- // 外部 API 服务(类似于 Flask 的 /api/v1)
611
- app.get('/api/v1/info/:token', async (req, res) => {
612
- try {
613
- const { token } = req.params;
614
- const authHeader = req.headers.authorization;
615
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
616
- return res.status(401).json({ error: '无效的 API 密钥' });
617
- }
618
-
619
- const headers = { 'Authorization': `Bearer ${token}` };
620
- const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
621
- const username = userInfoResponse.data.name;
622
- const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
623
- const spaces = spacesResponse.data;
624
- const spaceList = [];
625
-
626
- for (const space of spaces) {
627
- try {
628
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
629
- spaceList.push(spaceInfoResponse.data.id);
630
- } catch (error) {
631
- console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
632
- }
633
- }
634
-
635
- res.json({ spaces: spaceList, total: spaceList.length });
636
- } catch (error) {
637
- console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
638
- res.status(500).json({ error: error.message });
639
- }
640
- });
641
-
642
- app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
643
- try {
644
- const { token, spaceId } = req.params;
645
- const authHeader = req.headers.authorization;
646
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
647
- return res.status(401).json({ error: '无效的 API 密钥' });
648
- }
649
-
650
- const headers = { 'Authorization': `Bearer ${token}` };
651
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
652
- const spaceInfo = spaceInfoResponse.data;
653
- const spaceRuntime = spaceInfo.runtime || {};
654
-
655
- res.json({
656
- id: spaceInfo.id,
657
- status: spaceRuntime.stage || 'unknown',
658
- last_modified: spaceInfo.lastModified || null,
659
- created_at: spaceInfo.createdAt || null,
660
- sdk: spaceInfo.sdk || 'unknown',
661
- tags: spaceInfo.tags || [],
662
- private: spaceInfo.private || false
663
- });
664
- } catch (error) {
665
- console.error(`获取 space 信息失败 (外部 API):`, error.message);
666
- res.status(error.response?.status || 404).json({ error: error.message });
667
- }
668
- });
669
-
670
- app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
671
- try {
672
- const { token, spaceId } = req.params;
673
- const authHeader = req.headers.authorization;
674
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
675
- return res.status(401).json({ error: '无效的 API 密钥' });
676
- }
677
-
678
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
679
- await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });
680
- res.json({ success: true, message: `Space ${spaceId} 重启成功` });
681
- } catch (error) {
682
- console.error(`重启 space 失败 (外部 API):`, error.message);
683
- res.status(error.response?.status || 500).json({ success: false, error: error.message });
684
- }
685
- });
686
-
687
- app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
688
- try {
689
- const { token, spaceId } = req.params;
690
- const authHeader = req.headers.authorization;
691
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
692
- return res.status(401).json({ error: '无效的 API 密钥' });
693
- }
694
-
695
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
696
- const payload = { factoryReboot: true };
697
- console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);
698
- const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
699
- console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);
700
- res.json({ success: true, message: `Space ${spaceId} 重建成功` });
701
- } catch (error) {
702
- console.error(`重建 space 失败 (外部 API):`, error.message);
703
- if (error.response) {
704
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
705
- res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
706
- } else {
707
- res.status(500).json({ success: false, error: error.message });
708
- }
709
- }
710
- });
711
-
712
- // 代理 HuggingFace API:获取实时监控数据(SSE)
713
- app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
714
- try {
715
- const { username, instanceId } = req.params;
716
- const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
717
-
718
- // 检查实例状态,决定是否继续请求
719
- const spaces = spaceCache.getAll();
720
- const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
721
- if (!space) {
722
- console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
723
- return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
724
- }
725
- if (space.status.toLowerCase() !== 'running') {
726
- console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
727
- return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
728
- }
729
-
730
- const token = userTokenMapping[username];
731
- let headers = {
732
- 'Accept': 'text/event-stream',
733
- 'Cache-Control': 'no-cache',
734
- 'Connection': 'keep-alive'
735
- };
736
- if (token) {
737
- headers['Authorization'] = `Bearer ${token}`;
738
- }
739
-
740
- const response = await axios({
741
- method: 'get',
742
- url,
743
- headers,
744
- responseType: 'stream',
745
- timeout: 10000
746
- });
747
-
748
- res.set({
749
- 'Content-Type': 'text/event-stream',
750
- 'Cache-Control': 'no-cache',
751
- 'Connection': 'keep-alive'
752
- });
753
- response.data.pipe(res);
754
-
755
- req.on('close', () => {
756
- response.data.destroy();
757
- });
758
- } catch (error) {
759
- console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
760
- res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
761
- }
762
- });
763
-
764
- // 处理其他请求,重定向到 index.html
765
- app.get('*', (req, res) => {
766
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
767
- });
768
-
769
- // 定期清理过期的会话
770
- setInterval(() => {
771
- const now = Date.now();
772
- for (const [token, session] of sessions.entries()) {
773
- if (session.expiresAt < now) {
774
- sessions.delete(token);
775
- console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);
776
- }
777
- }
778
- }, 60 * 60 * 1000); // 每小时清理一次
779
-
780
- app.listen(port, () => {
781
- console.log(`Server running on port ${port}`);
782
- console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
783
- console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
784
- });
785
-
786
- // 提供静态文件(前端文件)
787
- app.use(express.static(path.join(__dirname, 'public')));
788
-
789
- // 提供配置信息的 API 接口
790
- app.get('/api/config', (req, res) => {
791
- res.json({ usernames: usernames.join(',') });
792
- });
793
-
794
- // 登录 API 接口
795
- app.post('/api/login', (req, res) => {
796
- const { username, password } = req.body;
797
- if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
798
- // 生成一个随机 token 作为会话标识
799
- const token = crypto.randomBytes(16).toString('hex');
800
- const expiresAt = Date.now() + SESSION_TIMEOUT;
801
- sessions.set(token, { username, expiresAt });
802
- console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);
803
- res.json({ success: true, token });
804
- } else {
805
- console.log(`用户 ${username} 登录失败,凭据无效`);
806
- res.status(401).json({ success: false, message: '用户名或密码错误' });
807
- }
808
- });
809
-
810
- // 验证登录状态 API 接口
811
- app.post('/api/verify-token', (req, res) => {
812
- const { token } = req.body;
813
- const session = sessions.get(token);
814
- if (session && session.expiresAt > Date.now()) {
815
- res.json({ success: true, message: 'Token 有效' });
816
- } else {
817
- if (session) {
818
- sessions.delete(token); // 删除过期的 token
819
- console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);
820
- }
821
- res.status(401).json({ success: false, message: 'Token 无效或已过期' });
822
- }
823
- });
824
-
825
- // 登出 API 接口
826
- app.post('/api/logout', (req, res) => {
827
- const { token } = req.body;
828
- sessions.delete(token);
829
- console.log(`Token ${token.slice(0, 8)}... 已手动登出`);
830
- res.json({ success: true, message: '登出成功' });
831
- });
832
-
833
- // 中间件:验证请求中的 token
834
- const authenticateToken = (req, res, next) => {
835
- const authHeader = req.headers['authorization'];
836
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
837
- return res.status(401).json({ error: '未提供有效的认证令牌' });
838
- }
839
- const token = authHeader.split(' ')[1];
840
- const session = sessions.get(token);
841
- if (session && session.expiresAt > Date.now()) {
842
- req.session = session;
843
- next();
844
- } else {
845
- if (session) {
846
- sessions.delete(token); // 删除过期的 token
847
- console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
848
- }
849
- return res.status(401).json({ error: '认证令牌无效或已过期' });
850
- }
851
- };
852
-
853
- // 获取所有 spaces 列表(包括私有)
854
- app.get('/api/proxy/spaces', async (req, res) => {
855
- try {
856
- if (!spaceCache.isExpired()) {
857
- console.log('从缓存获取 Spaces 数据');
858
- return res.json(spaceCache.getAll());
859
- }
860
-
861
- const allSpaces = [];
862
- for (const username of usernames) {
863
- const token = userTokenMapping[username];
864
- if (!token) {
865
- console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
866
- }
867
-
868
- try {
869
- // 调用 HuggingFace API 获取 Spaces 列表
870
- const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
871
- const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
872
- const spaces = response.data;
873
- console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
874
-
875
- for (const space of spaces) {
876
- try {
877
- // 获取 Space 详细信息
878
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
879
- const spaceInfo = spaceInfoResponse.data;
880
- const spaceRuntime = spaceInfo.runtime || {};
881
-
882
- allSpaces.push({
883
- repo_id: spaceInfo.id,
884
- name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
885
- owner: spaceInfo.author,
886
- username: username,
887
- token: token || '',
888
- url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
889
- status: spaceRuntime.stage || 'unknown',
890
- last_modified: spaceInfo.lastModified || 'unknown',
891
- created_at: spaceInfo.createdAt || 'unknown',
892
- sdk: spaceInfo.sdk || 'unknown',
893
- tags: spaceInfo.tags || [],
894
- private: spaceInfo.private || false,
895
- app_port: spaceInfo.cardData?.app_port || 'unknown'
896
- });
897
- } catch (error) {
898
- console.error(`处理 Space ${space.id} 失败:`, error.message);
899
- }
900
- }
901
- } catch (error) {
902
- console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
903
- }
904
- }
905
-
906
- allSpaces.sort((a, b) => a.name.localeCompare(b.name));
907
- spaceCache.updateAll(allSpaces);
908
- console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
909
- res.json(allSpaces);
910
- } catch (error) {
911
- console.error(`代理获取 spaces 列表失败:`, error.message);
912
- res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
913
- }
914
- });
915
-
916
- // 代理重启 Space(需要认证)
917
- app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
918
- try {
919
- const { repoId } = req.params;
920
- console.log(`尝试重启 Space: ${repoId}`);
921
- const spaces = spaceCache.getAll();
922
- const space = spaces.find(s => s.repo_id === repoId);
923
- if (!space || !space.token) {
924
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
925
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
926
- }
927
-
928
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
929
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
930
- console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
931
- res.json({ success: true, message: `Space ${repoId} 重启成功` });
932
- } catch (error) {
933
- console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
934
- if (error.response) {
935
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
936
- res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });
937
- } else {
938
- res.status(500).json({ error: '重启 space 失败', details: error.message });
939
- }
940
- }
941
- });
942
-
943
- // 代理重建 Space(需要认证)
944
- app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
945
- try {
946
- const { repoId } = req.params;
947
- console.log(`尝试重建 Space: ${repoId}`);
948
- const spaces = spaceCache.getAll();
949
- const space = spaces.find(s => s.repo_id === repoId);
950
- if (!space || !space.token) {
951
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
952
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
953
- }
954
-
955
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
956
- const payload = { factoryReboot: true };
957
- console.log(`发送重建请求,payload: ${JSON.stringify(payload)}`);
958
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
959
- console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);
960
- res.json({ success: true, message: `Space ${repoId} 重建成功` });
961
- } catch (error) {
962
- console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
963
- if (error.response) {
964
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
965
- res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });
966
- } else {
967
- res.status(500).json({ error: '重建 space 失败', details: error.message });
968
- }
969
- }
970
- });
971
-
972
- // 外部 API 服务(类似于 Flask 的 /api/v1)
973
- app.get('/api/v1/info/:token', async (req, res) => {
974
- try {
975
- const { token } = req.params;
976
- const authHeader = req.headers.authorization;
977
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
978
- return res.status(401).json({ error: '无效的 API 密钥' });
979
- }
980
-
981
- const headers = { 'Authorization': `Bearer ${token}` };
982
- const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
983
- const username = userInfoResponse.data.name;
984
- const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
985
- const spaces = spacesResponse.data;
986
- const spaceList = [];
987
-
988
- for (const space of spaces) {
989
- try {
990
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
991
- spaceList.push(spaceInfoResponse.data.id);
992
- } catch (error) {
993
- console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
994
- }
995
- }
996
-
997
- res.json({ spaces: spaceList, total: spaceList.length });
998
- } catch (error) {
999
- console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
1000
- res.status(500).json({ error: error.message });
1001
- }
1002
- });
1003
-
1004
- app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
1005
- try {
1006
- const { token, spaceId } = req.params;
1007
- const authHeader = req.headers.authorization;
1008
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1009
- return res.status(401).json({ error: '无效的 API 密钥' });
1010
- }
1011
-
1012
- const headers = { 'Authorization': `Bearer ${token}` };
1013
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
1014
- const spaceInfo = spaceInfoResponse.data;
1015
- const spaceRuntime = spaceInfo.runtime || {};
1016
-
1017
- res.json({
1018
- id: spaceInfo.id,
1019
- status: spaceRuntime.stage || 'unknown',
1020
- last_modified: spaceInfo.lastModified || null,
1021
- created_at: spaceInfo.createdAt || null,
1022
- sdk: spaceInfo.sdk || 'unknown',
1023
- tags: spaceInfo.tags || [],
1024
- private: spaceInfo.private || false
1025
- });
1026
- } catch (error) {
1027
- console.error(`获取 space 信息失败 (外部 API):`, error.message);
1028
- res.status(error.response?.status || 404).json({ error: error.message });
1029
- }
1030
- });
1031
-
1032
- app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
1033
- try {
1034
- const { token, spaceId } = req.params;
1035
- const authHeader = req.headers.authorization;
1036
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1037
- return res.status(401).json({ error: '无效的 API 密钥' });
1038
- }
1039
-
1040
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1041
- await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });
1042
- res.json({ success: true, message: `Space ${spaceId} 重启成功` });
1043
- } catch (error) {
1044
- console.error(`重启 space 失败 (外部 API):`, error.message);
1045
- res.status(error.response?.status || 500).json({ success: false, error: error.message });
1046
- }
1047
- });
1048
-
1049
- app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
1050
- try {
1051
- const { token, spaceId } = req.params;
1052
- const authHeader = req.headers.authorization;
1053
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1054
- return res.status(401).json({ error: '无效的 API 密钥' });
1055
- }
1056
-
1057
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1058
- const payload = { factoryReboot: true };
1059
- console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);
1060
- const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
1061
- console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);
1062
- res.json({ success: true, message: `Space ${spaceId} 重建成功` });
1063
- } catch (error) {
1064
- console.error(`重建 space 失败 (外部 API):`, error.message);
1065
- if (error.response) {
1066
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
1067
- res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
1068
- } else {
1069
- res.status(500).json({ success: false, error: error.message });
1070
- }
1071
- }
1072
- });
1073
-
1074
- // 代理 HuggingFace API:获取实时监控数据(SSE)
1075
- app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
1076
- try {
1077
- const { username, instanceId } = req.params;
1078
- const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
1079
-
1080
- // 检查实例状态,决定是否继续请求
1081
- const spaces = spaceCache.getAll();
1082
- const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
1083
- if (!space) {
1084
- console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
1085
- return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
1086
- }
1087
- if (space.status.toLowerCase() !== 'running') {
1088
- console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
1089
- return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
1090
- }
1091
-
1092
- const token = userTokenMapping[username];
1093
- let headers = {
1094
- 'Accept': 'text/event-stream',
1095
- 'Cache-Control': 'no-cache',
1096
- 'Connection': 'keep-alive'
1097
- };
1098
- if (token) {
1099
- headers['Authorization'] = `Bearer ${token}`;
1100
- }
1101
-
1102
- const response = await axios({
1103
- method: 'get',
1104
- url,
1105
- headers,
1106
- responseType: 'stream',
1107
- timeout: 10000
1108
- });
1109
-
1110
- res.set({
1111
- 'Content-Type': 'text/event-stream',
1112
- 'Cache-Control': 'no-cache',
1113
- 'Connection': 'keep-alive'
1114
- });
1115
- response.data.pipe(res);
1116
-
1117
- req.on('close', () => {
1118
- response.data.destroy();
1119
- });
1120
- } catch (error) {
1121
- console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
1122
- res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
1123
- }
1124
- });
1125
-
1126
- // 处理其他请求,重定向到 index.html
1127
- app.get('*', (req, res) => {
1128
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
1129
- });
1130
-
1131
- // 定期清理过期的会话
1132
- setInterval(() => {
1133
- const now = Date.now();
1134
- for (const [token, session] of sessions.entries()) {
1135
- if (session.expiresAt < now) {
1136
- sessions.delete(token);
1137
- console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);
1138
- }
1139
- }
1140
- }, 60 * 60 * 1000); // 每小时清理一次
1141
-
1142
- app.listen(port, () => {
1143
- console.log(`Server running on port ${port}`);
1144
- console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
1145
- console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
1146
- });
1147
-
1148
- // 提供静态文件(前端文件)
1149
- app.use(express.static(path.join(__dirname, 'public')));
1150
-
1151
- // 提供配置信息的 API 接口
1152
- app.get('/api/config', (req, res) => {
1153
- res.json({ usernames: usernames.join(',') });
1154
- });
1155
-
1156
- // 登录 API 接口
1157
- app.post('/api/login', (req, res) => {
1158
- const { username, password } = req.body;
1159
- if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
1160
- // 生成一个随机 token 作为会话标识
1161
- const token = crypto.randomBytes(16).toString('hex');
1162
- const expiresAt = Date.now() + SESSION_TIMEOUT;
1163
- sessions.set(token, { username, expiresAt });
1164
- console.log(`用户 ${username} 登录成功,生成 token: ${token.slice(0, 8)}...`);
1165
- res.json({ success: true, token });
1166
- } else {
1167
- console.log(`用户 ${username} 登录失败,凭据无效`);
1168
- res.status(401).json({ success: false, message: '用户名或密码错误' });
1169
- }
1170
- });
1171
-
1172
- // 验证登录状态 API 接口
1173
- app.post('/api/verify-token', (req, res) => {
1174
- const { token } = req.body;
1175
- const session = sessions.get(token);
1176
- if (session && session.expiresAt > Date.now()) {
1177
- res.json({ success: true, message: 'Token 有效' });
1178
- } else {
1179
- if (session) {
1180
- sessions.delete(token); // 删除过期的 token
1181
- console.log(`Token ${token.slice(0, 8)}... 已过期,已删除`);
1182
- }
1183
- res.status(401).json({ success: false, message: 'Token 无效或已过期' });
1184
- }
1185
- });
1186
-
1187
- // 登出 API 接口
1188
- app.post('/api/logout', (req, res) => {
1189
- const { token } = req.body;
1190
- sessions.delete(token);
1191
- console.log(`Token ${token.slice(0, 8)}... 已手动登出`);
1192
- res.json({ success: true, message: '登出成功' });
1193
- });
1194
-
1195
- // 中间件:验证请求中的 token
1196
- const authenticateToken = (req, res, next) => {
1197
- const authHeader = req.headers['authorization'];
1198
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
1199
- return res.status(401).json({ error: '未提供有效的认证令牌' });
1200
- }
1201
- const token = authHeader.split(' ')[1];
1202
- const session = sessions.get(token);
1203
- if (session && session.expiresAt > Date.now()) {
1204
- req.session = session;
1205
- next();
1206
- } else {
1207
- if (session) {
1208
- sessions.delete(token); // 删除过期的 token
1209
- console.log(`Token ${token.slice(0, 8)}... 已过期,拒绝访问`);
1210
- }
1211
- return res.status(401).json({ error: '认证令牌无效或已过期' });
1212
- }
1213
- };
1214
-
1215
- // 获取所有 spaces 列表(包括私有)
1216
- app.get('/api/proxy/spaces', async (req, res) => {
1217
- try {
1218
- if (!spaceCache.isExpired()) {
1219
- console.log('从缓存获取 Spaces 数据');
1220
- return res.json(spaceCache.getAll());
1221
- }
1222
-
1223
- const allSpaces = [];
1224
- for (const username of usernames) {
1225
- const token = userTokenMapping[username];
1226
- if (!token) {
1227
- console.warn(`用户 ${username} 没有配置 API Token,将尝试无认证访问公开数据`);
1228
- }
1229
-
1230
- try {
1231
- // 调用 HuggingFace API 获取 Spaces 列表
1232
- const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
1233
- const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
1234
- const spaces = response.data;
1235
- console.log(`获取到 ${spaces.length} 个 Spaces for ${username}`);
1236
-
1237
- for (const space of spaces) {
1238
- try {
1239
- // 获取 Space 详细信息
1240
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
1241
- const spaceInfo = spaceInfoResponse.data;
1242
- const spaceRuntime = spaceInfo.runtime || {};
1243
-
1244
- allSpaces.push({
1245
- repo_id: spaceInfo.id,
1246
- name: spaceInfo.cardData?.title || spaceInfo.id.split('/')[1],
1247
- owner: spaceInfo.author,
1248
- username: username,
1249
- token: token || '',
1250
- url: `https://${spaceInfo.author}-${spaceInfo.id.split('/')[1]}.hf.space`,
1251
- status: spaceRuntime.stage || 'unknown',
1252
- last_modified: spaceInfo.lastModified || 'unknown',
1253
- created_at: spaceInfo.createdAt || 'unknown',
1254
- sdk: spaceInfo.sdk || 'unknown',
1255
- tags: spaceInfo.tags || [],
1256
- private: spaceInfo.private || false,
1257
- app_port: spaceInfo.cardData?.app_port || 'unknown'
1258
- });
1259
- } catch (error) {
1260
- console.error(`处理 Space ${space.id} 失败:`, error.message);
1261
- }
1262
- }
1263
- } catch (error) {
1264
- console.error(`获取 Spaces 列表失败 for ${username}:`, error.message);
1265
- }
1266
- }
1267
-
1268
- allSpaces.sort((a, b) => a.name.localeCompare(b.name));
1269
- spaceCache.updateAll(allSpaces);
1270
- console.log(`总共获取到 ${allSpaces.length} 个 Spaces`);
1271
- res.json(allSpaces);
1272
- } catch (error) {
1273
- console.error(`代理获取 spaces 列表失败:`, error.message);
1274
- res.status(500).json({ error: '获取 spaces 列表失败', details: error.message });
1275
- }
1276
- });
1277
-
1278
- // 代理重启 Space(需要认证)
1279
- app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
1280
- try {
1281
- const { repoId } = req.params;
1282
- console.log(`尝试重启 Space: ${repoId}`);
1283
- const spaces = spaceCache.getAll();
1284
- const space = spaces.find(s => s.repo_id === repoId);
1285
- if (!space || !space.token) {
1286
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
1287
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
1288
- }
1289
-
1290
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
1291
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, {}, { headers });
1292
- console.log(`重启 Space ${repoId} 成功,状态码: ${response.status}`);
1293
- res.json({ success: true, message: `Space ${repoId} 重启成功` });
1294
- } catch (error) {
1295
- console.error(`重启 space 失败 (${req.params.repoId}):`, error.message);
1296
- if (error.response) {
1297
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
1298
- res.status(error.response.status || 500).json({ error: '重启 space 失败', details: error.response.data?.message || error.message });
1299
- } else {
1300
- res.status(500).json({ error: '重启 space 失败', details: error.message });
1301
- }
1302
- }
1303
- });
1304
-
1305
- // 代理重建 Space(需要认证)
1306
- app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
1307
- try {
1308
- const { repoId } = req.params;
1309
- console.log(`尝试重建 Space: ${repoId}`);
1310
- const spaces = spaceCache.getAll();
1311
- const space = spaces.find(s => s.repo_id === repoId);
1312
- if (!space || !space.token) {
1313
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
1314
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
1315
- }
1316
-
1317
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
1318
- const payload = { factory_reboot: true };
1319
- console.log(`发送重建请求,payload: ${JSON.stringify(payload)}`);
1320
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
1321
- console.log(`重建 Space ${repoId} 成功,状态码: ${response.status}`);
1322
- res.json({ success: true, message: `Space ${repoId} 重建成功` });
1323
- } catch (error) {
1324
- console.error(`重建 space 失败 (${req.params.repoId}):`, error.message);
1325
- if (error.response) {
1326
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
1327
- res.status(error.response.status || 500).json({ error: '重建 space 失败', details: error.response.data?.message || error.message });
1328
- } else {
1329
- res.status(500).json({ error: '重建 space 失败', details: error.message });
1330
- }
1331
- }
1332
- });
1333
-
1334
- // 外部 API 服务(类似于 Flask 的 /api/v1)
1335
- app.get('/api/v1/info/:token', async (req, res) => {
1336
- try {
1337
- const { token } = req.params;
1338
- const authHeader = req.headers.authorization;
1339
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1340
- return res.status(401).json({ error: '无效的 API 密钥' });
1341
- }
1342
-
1343
- const headers = { 'Authorization': `Bearer ${token}` };
1344
- const userInfoResponse = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
1345
- const username = userInfoResponse.data.name;
1346
- const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}`, { headers });
1347
- const spaces = spacesResponse.data;
1348
- const spaceList = [];
1349
-
1350
- for (const space of spaces) {
1351
- try {
1352
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${space.id}`, { headers });
1353
- spaceList.push(spaceInfoResponse.data.id);
1354
- } catch (error) {
1355
- console.error(`获取 Space 信息失败 (${space.id}):`, error.message);
1356
- }
1357
- }
1358
-
1359
- res.json({ spaces: spaceList, total: spaceList.length });
1360
- } catch (error) {
1361
- console.error(`获取 spaces 列表失败 (外部 API):`, error.message);
1362
- res.status(500).json({ error: error.message });
1363
- }
1364
- });
1365
-
1366
- app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
1367
- try {
1368
- const { token, spaceId } = req.params;
1369
- const authHeader = req.headers.authorization;
1370
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1371
- return res.status(401).json({ error: '无效的 API 密钥' });
1372
- }
1373
-
1374
- const headers = { 'Authorization': `Bearer ${token}` };
1375
- const spaceInfoResponse = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
1376
- const spaceInfo = spaceInfoResponse.data;
1377
- const spaceRuntime = spaceInfo.runtime || {};
1378
-
1379
- res.json({
1380
- id: spaceInfo.id,
1381
- status: spaceRuntime.stage || 'unknown',
1382
- last_modified: spaceInfo.lastModified || null,
1383
- created_at: spaceInfo.createdAt || null,
1384
- sdk: spaceInfo.sdk || 'unknown',
1385
- tags: spaceInfo.tags || [],
1386
- private: spaceInfo.private || false
1387
- });
1388
- } catch (error) {
1389
- console.error(`获取 space 信息失败 (外部 API):`, error.message);
1390
- res.status(error.response?.status || 404).json({ error: error.message });
1391
- }
1392
- });
1393
-
1394
- app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
1395
- try {
1396
- const { token, spaceId } = req.params;
1397
- const authHeader = req.headers.authorization;
1398
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1399
- return res.status(401).json({ error: '无效的 API 密钥' });
1400
- }
1401
-
1402
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1403
- await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, {}, { headers });
1404
- res.json({ success: true, message: `Space ${spaceId} 重启成功` });
1405
- } catch (error) {
1406
- console.error(`重启 space 失败 (外部 API):`, error.message);
1407
- res.status(error.response?.status || 500).json({ success: false, error: error.message });
1408
- }
1409
- });
1410
-
1411
- app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
1412
- try {
1413
- const { token, spaceId } = req.params;
1414
- const authHeader = req.headers.authorization;
1415
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1416
- return res.status(401).json({ error: '无效的 API 密钥' });
1417
- }
1418
-
1419
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1420
- const payload = { factory_reboot: true };
1421
- console.log(`外部 API 发送重建请求,spaceId: ${spaceId}`);
1422
- const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
1423
- console.log(`外部 API 重建 Space ${spaceId} 成功,状态码: ${response.status}`);
1424
- res.json({ success: true, message: `Space ${spaceId} 重建成功` });
1425
- } catch (error) {
1426
- console.error(`重建 space 失败 (外部 API):`, error.message);
1427
- if (error.response) {
1428
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data);
1429
- res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
1430
- } else {
1431
- res.status(500).json({ success: false, error: error.message });
1432
- }
1433
- }
1434
- });
1435
-
1436
- // 代理 HuggingFace API:获取实时监控数据(SSE)
1437
- app.get('/api/proxy/live-metrics/:username/:instanceId', async (req, res) => {
1438
- try {
1439
- const { username, instanceId } = req.params;
1440
- const url = `https://api.hf.space/v1/${username}/${instanceId}/live-metrics/sse`;
1441
-
1442
- // 检查实例状态,决定是否继续请求
1443
- const spaces = spaceCache.getAll();
1444
- const space = spaces.find(s => s.repo_id === `${username}/${instanceId}`);
1445
- if (!space) {
1446
- console.log(`实例 ${username}/${instanceId} 未找到,不尝试获取监控数据`);
1447
- return res.status(404).json({ error: '实例未找到,无法获取监控数据' });
1448
- }
1449
- if (space.status.toLowerCase() !== 'running') {
1450
- console.log(`实例 ${username}/${instanceId} 状态为 ${space.status},不尝试获取监控数据`);
1451
- return res.status(400).json({ error: '实例未运行,无法获取监控数据' });
1452
- }
1453
-
1454
- const token = userTokenMapping[username];
1455
- let headers = {
1456
- 'Accept': 'text/event-stream',
1457
- 'Cache-Control': 'no-cache',
1458
- 'Connection': 'keep-alive'
1459
- };
1460
- if (token) {
1461
- headers['Authorization'] = `Bearer ${token}`;
1462
- }
1463
-
1464
- const response = await axios({
1465
- method: 'get',
1466
- url,
1467
- headers,
1468
- responseType: 'stream',
1469
- timeout: 10000
1470
- });
1471
-
1472
- res.set({
1473
- 'Content-Type': 'text/event-stream',
1474
- 'Cache-Control': 'no-cache',
1475
- 'Connection': 'keep-alive'
1476
- });
1477
- response.data.pipe(res);
1478
-
1479
- req.on('close', () => {
1480
- response.data.destroy();
1481
- });
1482
- } catch (error) {
1483
- console.error(`代理获取直播监控数据失败 (${req.params.username}/${req.params.instanceId}):`, error.message);
1484
- res.status(error.response?.status || 500).json({ error: '获取监控数据失败', details: error.message });
1485
- }
1486
- });
1487
-
1488
- // 处理其他请求,重定向到 index.html
1489
- app.get('*', (req, res) => {
1490
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
1491
- });
1492
-
1493
- // 定期清理过期的会话
1494
- setInterval(() => {
1495
- const now = Date.now();
1496
- for (const [token, session] of sessions.entries()) {
1497
- if (session.expiresAt < now) {
1498
- sessions.delete(token);
1499
- console.log(`Token ${token.slice(0, 8)}... 已过期,自动清理`);
1500
- }
1501
- }
1502
- }, 60 * 60 * 1000); // 每小时清理一次
1503
-
1504
- app.listen(port, () => {
1505
- console.log(`Server running on port ${port}`);
1506
- console.log(`User configurations:`, usernames.map(user => `${user}: ${userTokenMapping[user] ? 'Token Configured' : 'No Token'}`).join(', ') || 'None');
1507
- console.log(`Admin login enabled: Username=${ADMIN_USERNAME}, Password=${ADMIN_PASSWORD ? 'Configured' : 'Not Configured'}`);
1508
- }); sessions[token] = { username, timestamp: Date.now() }; // 存储会话
1509
- console.log(`用户登录成功,生成 Token: ${token}`);
1510
- res.json({ success: true, token });
1511
- } else {
1512
- res.status(401).json({ success: false, error: '用户名或密码错误' });
1513
- }
1514
- });
1515
-
1516
- // 检查登录状态 API
1517
- app.get('/api/check-login', authenticateToken, (req, res) => {
1518
- res.json({ success: true, loggedIn: true });
1519
- });
1520
-
1521
- // 缓存对象,用于存储空间信息
1522
- const spaceCache = {
1523
- data: [],
1524
- lastUpdated: 0,
1525
- isUpdating: false,
1526
- getAll: function() {
1527
- return this.data;
1528
- },
1529
- update: function(spaces) {
1530
- this.data = spaces;
1531
- this.lastUpdated = Date.now();
1532
- this.isUpdating = false;
1533
- }
1534
- };
1535
-
1536
- // 通过 HuggingFace API 获取所有用户的 Space 信息
1537
- async function fetchSpacesFromAPI() {
1538
- const hfUsers = process.env.HF_USER ? process.env.HF_USER.split(',').map(u => u.trim()) : [];
1539
- if (hfUsers.length === 0) {
1540
- console.warn('未配置 HuggingFace 用户(HF_USER 环境变量为空)');
1541
- return [];
1542
- }
1543
-
1544
- const allSpaces = [];
1545
- for (const userTokenPair of hfUsers) {
1546
- let username, token;
1547
- if (userTokenPair.includes(':')) {
1548
- [username, token] = userTokenPair.split(':').map(s => s.trim());
1549
- } else {
1550
- username = userTokenPair.trim();
1551
- token = '';
1552
- }
1553
- if (!username) continue;
1554
-
1555
- try {
1556
- console.log(`正在获取用户 ${username} 的 Space 信息...`);
1557
- const headers = token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : {};
1558
- const response = await axios.get(`https://huggingface.co/api/spaces?author=${username}&full=username`, { headers });
1559
- if (Array.isArray(response.data)) {
1560
- const spaces = response.data.map(space => ({
1561
- repo_id: space.id,
1562
- author: space.author || username,
1563
- title: space.title || space.id.split('/')[1],
1564
- token: token || '',
1565
- private: space.private || false
1566
- }));
1567
- console.log(`用户 ${username} 的 Space 获取成功,共找到 ${spaces.length} 个 Space`);
1568
- allSpaces.push(...spaces);
1569
- } else {
1570
- console.warn(`用户 ${username} 的 Space 列表获取失败,API 返回数据不是数组`);
1571
- }
1572
- } catch (error) {
1573
- console.error(`获取用户 ${username} 的 Space 列表时出错:`, error.message);
1574
- }
1575
- }
1576
- return allSpaces;
1577
- }
1578
-
1579
- // 定时更新缓存
1580
- async function updateSpaceCache() {
1581
- if (spaceCache.isUpdating) {
1582
- console.log('已有更新任务在运行,跳过本次更新');
1583
- return;
1584
- }
1585
- spaceCache.isUpdating = true;
1586
- console.log('开始更新 Space 信息...');
1587
- try {
1588
- const spaces = await fetchSpacesFromAPI();
1589
- spaceCache.update(spaces);
1590
- console.log(`Space 信息更新完成,总计 ${spaces.length} 个 Space`);
1591
- } catch (error) {
1592
- console.error('更新 Space 信息时出错:', error.message);
1593
- spaceCache.isUpdating = false;
1594
- }
1595
- }
1596
-
1597
- // 初始更新
1598
- updateSpaceCache();
1599
-
1600
- // 每 5 分钟更新一次
1601
- setInterval(updateSpaceCache, 5 * 60 * 1000);
1602
-
1603
- // 获取 Space 监控数据
1604
- async function fetchSpaceMetrics(repoId, token) {
1605
- try {
1606
- const headers = token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : {};
1607
- const metricsUrl = `https://huggingface.co/api/spaces/${repoId}/metrics`;
1608
- console.log(`正在获取 Space 监控数据: ${repoId}, 使用 Token: ${token ? '已提供' : '未提供'}`);
1609
- const response = await axios.get(metricsUrl, { headers });
1610
- console.log(`Space ${repoId} 监控数据获取成功`);
1611
- return response.data;
1612
- } catch (error) {
1613
- console.error(`获取 Space 监控数据失败 (${repoId}):`, error.message);
1614
- if (error.response) {
1615
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data || '无额外数据');
1616
- }
1617
- return null;
1618
- }
1619
- }
1620
-
1621
- // 实时监控数据 API(使用 EventSource)
1622
- app.get('/api/monitor', async (req, res) => {
1623
- res.writeHead(200, {
1624
- 'Content-Type': 'text/event-stream',
1625
- 'Cache-Control': 'no-cache',
1626
- 'Connection': 'keep-alive'
1627
- });
1628
-
1629
- console.log('客户端已连接,开始实时监控数据传输...');
1630
-
1631
- const sendEvent = (data) => {
1632
- res.write(`data: ${JSON.stringify(data)}\n\n`);
1633
- };
1634
-
1635
- const intervalId = setInterval(async () => {
1636
- try {
1637
- const spaces = spaceCache.getAll();
1638
- if (spaces.length === 0) {
1639
- sendEvent({ spaces: [], message: '暂无 Space 信息,请配置 HF_USER 环境变量' });
1640
- return;
1641
- }
1642
-
1643
- const updatedSpaces = [];
1644
- for (const space of spaces) {
1645
- const metrics = await fetchSpaceMetrics(space.repo_id, space.token);
1646
- updatedSpaces.push({ ...space, metrics });
1647
- }
1648
- sendEvent({ spaces: updatedSpaces });
1649
- } catch (error) {
1650
- console.error('监控数据处理出错:', error.message);
1651
- sendEvent({ error: '监控数据获取失败,请稍后再试' });
1652
- }
1653
- }, 10000);
1654
-
1655
- req.on('close', () => {
1656
- console.log('客户端断开连接,停止数据传输');
1657
- clearInterval(intervalId);
1658
- res.end();
1659
- });
1660
- });
1661
-
1662
- // 获取配置的用户名列表(隐藏 Token)
1663
- app.get('/api/users', (req, res) => {
1664
- const hfUsers = process.env.HF_USER ? process.env.HF_USER.split(',').map(u => u.trim()) : [];
1665
- const usernames = hfUsers.map(userTokenPair => {
1666
- if (userTokenPair.includes(':')) {
1667
- return userTokenPair.split(':')[0].trim();
1668
- }
1669
- return userTokenPair.trim();
1670
- }).filter(username => username);
1671
- res.json({ users: usernames });
1672
- });
1673
-
1674
- // 代理重启 Space
1675
- app.post('/api/proxy/restart/:repoId(*)', authenticateToken, async (req, res) => {
1676
- try {
1677
- const { repoId } = req.params;
1678
- console.log(`尝试重启 Space: ${repoId}`);
1679
- const spaces = spaceCache.getAll();
1680
- const space = spaces.find(s => s.repo_id === repoId);
1681
- if (!space || !space.token) {
1682
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
1683
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
1684
- }
1685
-
1686
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
1687
- const payload = {}; // 普通重启不包含 factory_reboot 参数
1688
- console.log(`发送普通重启请求到 HuggingFace API,repoId: ${repoId}`);
1689
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
1690
- console.log(`重启 Space ${repoId} 请求完成,状态码: ${response.status}, 响应数据: ${JSON.stringify(response.data || {})}`);
1691
- res.json({ success: true, message: `Space ${repoId} 重启请求已发送` });
1692
- } catch (error) {
1693
- console.error(`重启 Space 失败 (${req.params.repoId}):`, error.message);
1694
- if (error.response) {
1695
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data || '无额外数据');
1696
- res.status(error.response.status || 500).json({ error: '重启 Space 失败', details: error.response.data?.message || error.message });
1697
- } else {
1698
- res.status(500).json({ error: '重启 Space 失败', details: error.message });
1699
- }
1700
- }
1701
- });
1702
-
1703
- // 代理重建 Space(使用正确的 factory_reboot 字段)
1704
- app.post('/api/proxy/rebuild/:repoId(*)', authenticateToken, async (req, res) => {
1705
- try {
1706
- const { repoId } = req.params;
1707
- console.log(`尝试重建 Space: ${repoId}`);
1708
- const spaces = spaceCache.getAll();
1709
- const space = spaces.find(s => s.repo_id === repoId);
1710
- if (!space || !space.token) {
1711
- console.error(`Space ${repoId} 未找到或无 Token 配置`);
1712
- return res.status(404).json({ error: 'Space 未找到或无 Token 配置' });
1713
- }
1714
-
1715
- const headers = { 'Authorization': `Bearer ${space.token}`, 'Content-Type': 'application/json' };
1716
- const payload = { factory_reboot: true }; // 修正为 factory_reboot,确保符合 API 规范
1717
- console.log(`发送重建请求到 HuggingFace API,repoId: ${repoId}, payload: ${JSON.stringify(payload)}`);
1718
- const response = await axios.post(`https://huggingface.co/api/spaces/${repoId}/restart`, payload, { headers });
1719
- console.log(`重建 Space ${repoId} 请求完成,状态码: ${response.status}, 响应数据: ${JSON.stringify(response.data || {})}`);
1720
- res.json({ success: true, message: `Space ${repoId} 重建请求已发送` });
1721
- } catch (error) {
1722
- console.error(`重建 Space 失败 (${req.params.repoId}):`, error.message);
1723
- if (error.response) {
1724
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data || '无额外数据');
1725
- res.status(error.response.status || 500).json({ error: '重建 Space 失败', details: error.response.data?.message || error.message });
1726
- } else {
1727
- res.status(500).json({ error: '重建 Space 失败', details: error.message });
1728
- }
1729
- }
1730
- });
1731
-
1732
- // 外部 API:获取用户信息
1733
- app.get('/api/v1/info/:token', async (req, res) => {
1734
- try {
1735
- const { token } = req.params;
1736
- const authHeader = req.headers.authorization;
1737
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1738
- return res.status(401).json({ error: '无效的 API 密钥' });
1739
- }
1740
-
1741
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1742
- const response = await axios.get('https://huggingface.co/api/whoami-v2', { headers });
1743
- const username = response.data.name;
1744
- const spacesResponse = await axios.get(`https://huggingface.co/api/spaces?author=${username}&full=username`, { headers });
1745
- res.json({ spaces: spacesResponse.data.map(s => s.id), total: spacesResponse.data.length });
1746
- } catch (error) {
1747
- console.error('外部 API 获取用户信息失败:', error.message);
1748
- if (error.response) {
1749
- res.status(error.response.status || 500).json({ error: error.response.data?.message || error.message });
1750
- } else {
1751
- res.status(500).json({ error: error.message });
1752
- }
1753
- }
1754
- });
1755
-
1756
- // 外部 API:获取 Space 信息
1757
- app.get('/api/v1/info/:token/:spaceId(*)', async (req, res) => {
1758
- try {
1759
- const { token, spaceId } = req.params;
1760
- const authHeader = req.headers.authorization;
1761
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1762
- return res.status(401).json({ error: '无效的 API 密钥' });
1763
- }
1764
-
1765
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1766
- const response = await axios.get(`https://huggingface.co/api/spaces/${spaceId}`, { headers });
1767
- res.json(response.data);
1768
- } catch (error) {
1769
- console.error(`外部 API 获取 Space 信息失败 (${req.params.spaceId}):`, error.message);
1770
- if (error.response) {
1771
- res.status(error.response.status || 500).json({ error: error.response.data?.message || error.message });
1772
- } else {
1773
- res.status(500).json({ error: error.message });
1774
- }
1775
- }
1776
- });
1777
-
1778
- // 外部 API:重启 Space
1779
- app.post('/api/v1/action/:token/:spaceId(*)/restart', async (req, res) => {
1780
- try {
1781
- const { token, spaceId } = req.params;
1782
- const authHeader = req.headers.authorization;
1783
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1784
- return res.status(401).json({ error: '无效的 API 密钥' });
1785
- }
1786
-
1787
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1788
- const payload = {}; // 普通重启
1789
- console.log(`外部 API 发送重启请求,spaceId: ${spaceId}`);
1790
- const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
1791
- console.log(`外部 API 重启 Space ${spaceId} 请求完成,状态码: ${response.status}, 响应数据: ${JSON.stringify(response.data || {})}`);
1792
- res.json({ success: true, message: `Space ${spaceId} 重启成功` });
1793
- } catch (error) {
1794
- console.error(`重启 Space 失败 (外部 API):`, error.message);
1795
- if (error.response) {
1796
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data || '无额外数据');
1797
- res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
1798
- } else {
1799
- res.status(500).json({ success: false, error: error.message });
1800
- }
1801
- }
1802
- });
1803
-
1804
- // 外部 API:重建 Space(使用 factory_reboot)
1805
- app.post('/api/v1/action/:token/:spaceId(*)/rebuild', async (req, res) => {
1806
- try {
1807
- const { token, spaceId } = req.params;
1808
- const authHeader = req.headers.authorization;
1809
- if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== process.env.API_KEY) {
1810
- return res.status(401).json({ error: '无效的 API 密钥' });
1811
- }
1812
-
1813
- const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
1814
- const payload = { factory_reboot: true }; // 修正为 factory_reboot
1815
- console.log(`外部 API 发送重建请求,spaceId: ${spaceId}, payload: ${JSON.stringify(payload)}`);
1816
- const response = await axios.post(`https://huggingface.co/api/spaces/${spaceId}/restart`, payload, { headers });
1817
- console.log(`外部 API 重建 Space ${spaceId} 请求完成,状态码: ${response.status}, 响应数据: ${JSON.stringify(response.data || {})}`);
1818
- res.json({ success: true, message: `Space ${spaceId} 重建成功` });
1819
- } catch (error) {
1820
- console.error(`重建 Space 失败 (外部 API):`, error.message);
1821
- if (error.response) {
1822
- console.error(`状态码: ${error.response.status}, 响应数据:`, error.response.data || '无额外数据');
1823
- res.status(error.response.status || 500).json({ success: false, error: error.response.data?.message || error.message });
1824
- } else {
1825
- res.status(500).json({ success: false, error: error.message });
1826
- }
1827
- }
1828
- });
1829
-
1830
- // 启动服务器
1831
- app.listen(port, () => {
1832
- console.log(`服务器启动,监听端口: ${port}`);
1833
  });
 
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
  });