CatPtain commited on
Commit
0afb612
·
verified ·
1 Parent(s): 7b673de

Upload 11 files

Browse files
backend/package.json CHANGED
@@ -20,7 +20,8 @@
20
  "dotenv": "^16.4.5",
21
  "express-rate-limit": "^7.4.1",
22
  "uuid": "^10.0.0",
23
- "puppeteer": "^21.0.0"
 
24
  },
25
  "devDependencies": {
26
  "nodemon": "^3.1.7"
 
20
  "dotenv": "^16.4.5",
21
  "express-rate-limit": "^7.4.1",
22
  "uuid": "^10.0.0",
23
+ "puppeteer": "^21.0.0",
24
+ "playwright": "^1.40.0"
25
  },
26
  "devDependencies": {
27
  "nodemon": "^3.1.7"
backend/src/app.js CHANGED
@@ -5,18 +5,29 @@ import rateLimit from 'express-rate-limit';
5
  import dotenv from 'dotenv';
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
 
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  import authRoutes from './routes/auth.js';
10
  import pptRoutes from './routes/ppt.js';
11
  import publicRoutes from './routes/public.js';
12
  import { authenticateToken } from './middleware/auth.js';
13
  import { errorHandler } from './middleware/errorHandler.js';
14
 
15
- dotenv.config();
16
-
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = path.dirname(__filename);
19
-
20
  const app = express();
21
  const PORT = process.env.PORT || 7860; // 修改为7860端口
22
 
@@ -28,12 +39,17 @@ app.use(helmet({
28
  contentSecurityPolicy: false, // 为了兼容前端静态文件
29
  }));
30
 
31
- // 限流中间件
32
  const limiter = rateLimit({
33
  windowMs: 15 * 60 * 1000, // 15分钟
34
  max: 100, // 每个IP每15分钟最多100个请求
35
- message: 'Too many requests from this IP, please try again later.'
 
 
 
36
  });
 
 
37
  app.use('/api', limiter);
38
 
39
  // CORS配置
@@ -45,8 +61,10 @@ app.use(cors({
45
  app.use(express.json({ limit: '50mb' }));
46
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
47
 
48
- // 提供前端静态文件
49
- app.use(express.static(path.join(__dirname, '../../frontend/dist')));
 
 
50
 
51
  // 提供数据文件
52
  app.use('/data', express.static(path.join(__dirname, '../../frontend/public/mocks')));
@@ -465,6 +483,129 @@ app.get('/api/github/test', async (req, res) => {
465
  }
466
  });
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  // 添加仓库初始化端点
469
  app.post('/api/github/initialize', async (req, res) => {
470
  try {
@@ -508,6 +649,467 @@ app.post('/api/github/initialize', async (req, res) => {
508
  }
509
  });
510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  // 添加路由注册日志
512
  console.log('Importing route modules...');
513
  console.log('Auth routes imported:', !!authRoutes);
@@ -539,9 +1141,23 @@ app.use('/api/*', (req, res) => {
539
  res.status(404).json({ error: 'API route not found', path: req.path });
540
  });
541
 
542
- // 前端路由处理 - 必须在API路由之后
543
  app.get('*', (req, res) => {
544
- res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  });
546
 
547
  // 错误处理中间件
 
5
  import dotenv from 'dotenv';
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
8
+ import axios from 'axios';
9
+ import fs from 'fs'; // 添加ES模块fs导入
10
 
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ // 首先加载环境变量 - 必须在导入服务之前
15
+ dotenv.config({ path: path.join(__dirname, '../../.env') });
16
+
17
+ // 验证环境变量是否正确加载
18
+ console.log('=== Environment Variables Check ===');
19
+ console.log('GITHUB_TOKEN configured:', !!process.env.GITHUB_TOKEN);
20
+ console.log('GITHUB_TOKEN length:', process.env.GITHUB_TOKEN ? process.env.GITHUB_TOKEN.length : 0);
21
+ console.log('GITHUB_REPOS configured:', !!process.env.GITHUB_REPOS);
22
+ console.log('GITHUB_REPOS value:', process.env.GITHUB_REPOS);
23
+
24
+ // 现在导入需要环境变量的模块
25
  import authRoutes from './routes/auth.js';
26
  import pptRoutes from './routes/ppt.js';
27
  import publicRoutes from './routes/public.js';
28
  import { authenticateToken } from './middleware/auth.js';
29
  import { errorHandler } from './middleware/errorHandler.js';
30
 
 
 
 
 
 
31
  const app = express();
32
  const PORT = process.env.PORT || 7860; // 修改为7860端口
33
 
 
39
  contentSecurityPolicy: false, // 为了兼容前端静态文件
40
  }));
41
 
42
+ // 修复限流配置 - 针对Huggingface Space环境
43
  const limiter = rateLimit({
44
  windowMs: 15 * 60 * 1000, // 15分钟
45
  max: 100, // 每个IP每15分钟最多100个请求
46
+ message: 'Too many requests from this IP, please try again later.',
47
+ trustProxy: false, // 在本地测试环境设为false
48
+ standardHeaders: true,
49
+ legacyHeaders: false
50
  });
51
+
52
+ // 应用限流中间件
53
  app.use('/api', limiter);
54
 
55
  // CORS配置
 
61
  app.use(express.json({ limit: '50mb' }));
62
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
63
 
64
+ // 提供前端静态文件 - 修复路径配置
65
+ const frontendDistPath = path.join(__dirname, '../../frontend/dist');
66
+ console.log('Frontend dist path:', frontendDistPath);
67
+ app.use(express.static(frontendDistPath));
68
 
69
  // 提供数据文件
70
  app.use('/data', express.static(path.join(__dirname, '../../frontend/public/mocks')));
 
483
  }
484
  });
485
 
486
+ // 添加GitHub调试路由
487
+ app.get('/api/debug/github', async (req, res) => {
488
+ try {
489
+ console.log('=== GitHub Debug Information ===');
490
+
491
+ const { default: githubService } = await import('./services/githubService.js');
492
+
493
+ const debugInfo = {
494
+ timestamp: new Date().toISOString(),
495
+ environment: {
496
+ tokenConfigured: !!process.env.GITHUB_TOKEN,
497
+ tokenLength: process.env.GITHUB_TOKEN ? process.env.GITHUB_TOKEN.length : 0,
498
+ reposConfigured: !!process.env.GITHUB_REPOS,
499
+ reposList: process.env.GITHUB_REPOS ? process.env.GITHUB_REPOS.split(',') : [],
500
+ nodeEnv: process.env.NODE_ENV
501
+ },
502
+ service: {
503
+ hasToken: !!githubService.token,
504
+ repositoriesCount: githubService.repositories?.length || 0,
505
+ repositories: githubService.repositories || [],
506
+ apiUrl: githubService.apiUrl
507
+ }
508
+ };
509
+
510
+ // 测试连接
511
+ try {
512
+ const connectionTest = await githubService.validateConnection();
513
+ debugInfo.connectionTest = connectionTest;
514
+ } catch (connError) {
515
+ debugInfo.connectionError = connError.message;
516
+ }
517
+
518
+ console.log('Debug info:', debugInfo);
519
+ res.json(debugInfo);
520
+
521
+ } catch (error) {
522
+ console.error('Debug route error:', error);
523
+ res.status(500).json({
524
+ error: error.message,
525
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
526
+ });
527
+ }
528
+ });
529
+
530
+ // 添加PPT调试路由
531
+ app.get('/api/debug/ppt/:userId', async (req, res) => {
532
+ try {
533
+ const { userId } = req.params;
534
+ console.log(`=== PPT Debug for User: ${userId} ===`);
535
+
536
+ const { default: githubService } = await import('./services/githubService.js');
537
+
538
+ const debugInfo = {
539
+ timestamp: new Date().toISOString(),
540
+ userId: userId,
541
+ repositories: []
542
+ };
543
+
544
+ // 检查每个仓库中用户的文件
545
+ for (let i = 0; i < githubService.repositories.length; i++) {
546
+ const repoInfo = {
547
+ index: i,
548
+ url: githubService.repositories[i],
549
+ accessible: false,
550
+ userDirectoryExists: false,
551
+ files: []
552
+ };
553
+
554
+ try {
555
+ const { owner, repo } = githubService.parseRepoUrl(githubService.repositories[i]);
556
+
557
+ // 检查仓库是否可访问
558
+ await axios.get(`https://api.github.com/repos/${owner}/${repo}`, {
559
+ headers: {
560
+ 'Authorization': `token ${githubService.token}`,
561
+ 'Accept': 'application/vnd.github.v3+json'
562
+ }
563
+ });
564
+ repoInfo.accessible = true;
565
+
566
+ // 检查用户目录
567
+ try {
568
+ const userDirResponse = await axios.get(
569
+ `https://api.github.com/repos/${owner}/${repo}/contents/users/${userId}`,
570
+ {
571
+ headers: {
572
+ 'Authorization': `token ${githubService.token}`,
573
+ 'Accept': 'application/vnd.github.v3+json'
574
+ }
575
+ }
576
+ );
577
+
578
+ repoInfo.userDirectoryExists = true;
579
+ repoInfo.files = userDirResponse.data
580
+ .filter(item => item.type === 'file' && item.name.endsWith('.json'))
581
+ .map(file => ({
582
+ name: file.name,
583
+ size: file.size,
584
+ sha: file.sha
585
+ }));
586
+ } catch (userDirError) {
587
+ repoInfo.userDirectoryError = userDirError.response?.status === 404 ? 'Directory not found' : userDirError.message;
588
+ }
589
+
590
+ } catch (repoError) {
591
+ repoInfo.error = repoError.message;
592
+ }
593
+
594
+ debugInfo.repositories.push(repoInfo);
595
+ }
596
+
597
+ console.log('PPT Debug info:', debugInfo);
598
+ res.json(debugInfo);
599
+
600
+ } catch (error) {
601
+ console.error('PPT Debug route error:', error);
602
+ res.status(500).json({
603
+ error: error.message,
604
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
605
+ });
606
+ }
607
+ });
608
+
609
  // 添加仓库初始化端点
610
  app.post('/api/github/initialize', async (req, res) => {
611
  try {
 
649
  }
650
  });
651
 
652
+ // 添加GitHub权限详细检查路由
653
+ app.get('/api/debug/github-permissions', async (req, res) => {
654
+ try {
655
+ console.log('=== GitHub Token Permissions Check ===');
656
+
657
+ const { default: githubService } = await import('./services/githubService.js');
658
+
659
+ if (!githubService.token) {
660
+ return res.status(400).json({ error: 'No GitHub token configured' });
661
+ }
662
+
663
+ const debugInfo = {
664
+ timestamp: new Date().toISOString(),
665
+ tokenInfo: {},
666
+ repositoryTests: []
667
+ };
668
+
669
+ // 1. 检查token基本信息和权限
670
+ try {
671
+ const userResponse = await axios.get('https://api.github.com/user', {
672
+ headers: {
673
+ 'Authorization': `token ${githubService.token}`,
674
+ 'Accept': 'application/vnd.github.v3+json'
675
+ }
676
+ });
677
+
678
+ debugInfo.tokenInfo = {
679
+ login: userResponse.data.login,
680
+ id: userResponse.data.id,
681
+ type: userResponse.data.type,
682
+ company: userResponse.data.company,
683
+ publicRepos: userResponse.data.public_repos,
684
+ privateRepos: userResponse.data.total_private_repos,
685
+ tokenScopes: userResponse.headers['x-oauth-scopes'] || 'Unknown'
686
+ };
687
+
688
+ console.log('Token info:', debugInfo.tokenInfo);
689
+
690
+ } catch (tokenError) {
691
+ debugInfo.tokenError = {
692
+ status: tokenError.response?.status,
693
+ message: tokenError.message
694
+ };
695
+ }
696
+
697
+ // 2. 检查每个仓库的详细状态
698
+ for (let i = 0; i < githubService.repositories.length; i++) {
699
+ const repoUrl = githubService.repositories[i];
700
+ const repoTest = {
701
+ index: i,
702
+ url: repoUrl,
703
+ tests: {}
704
+ };
705
+
706
+ try {
707
+ const { owner, repo } = githubService.parseRepoUrl(repoUrl);
708
+ repoTest.owner = owner;
709
+ repoTest.repo = repo;
710
+
711
+ // 测试1: 基本仓库访问
712
+ try {
713
+ const repoResponse = await axios.get(`https://api.github.com/repos/${owner}/${repo}`, {
714
+ headers: {
715
+ 'Authorization': `token ${githubService.token}`,
716
+ 'Accept': 'application/vnd.github.v3+json'
717
+ }
718
+ });
719
+
720
+ repoTest.tests.basicAccess = {
721
+ success: true,
722
+ repoExists: true,
723
+ private: repoResponse.data.private,
724
+ permissions: repoResponse.data.permissions,
725
+ defaultBranch: repoResponse.data.default_branch,
726
+ size: repoResponse.data.size
727
+ };
728
+
729
+ } catch (repoError) {
730
+ repoTest.tests.basicAccess = {
731
+ success: false,
732
+ status: repoError.response?.status,
733
+ message: repoError.message,
734
+ details: repoError.response?.data
735
+ };
736
+
737
+ // 如果是404,检查是否是权限问题还是仓库不存在
738
+ if (repoError.response?.status === 404) {
739
+ // 尝试不使用认证访问(如果是公开仓库应该能访问)
740
+ try {
741
+ await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
742
+ repoTest.tests.basicAccess.possibleCause = 'Repository exists but token lacks permission';
743
+ } catch (publicError) {
744
+ if (publicError.response?.status === 404) {
745
+ repoTest.tests.basicAccess.possibleCause = 'Repository does not exist';
746
+ }
747
+ }
748
+ }
749
+ }
750
+
751
+ // 测试2: 检查是否能列出用户的仓库
752
+ try {
753
+ const userReposResponse = await axios.get(`https://api.github.com/users/${owner}/repos?per_page=100`, {
754
+ headers: {
755
+ 'Authorization': `token ${githubService.token}`,
756
+ 'Accept': 'application/vnd.github.v3+json'
757
+ }
758
+ });
759
+
760
+ const hasRepo = userReposResponse.data.some(r => r.name === repo);
761
+ repoTest.tests.userReposList = {
762
+ success: true,
763
+ totalRepos: userReposResponse.data.length,
764
+ targetRepoFound: hasRepo,
765
+ repoNames: userReposResponse.data.slice(0, 10).map(r => ({ name: r.name, private: r.private }))
766
+ };
767
+
768
+ } catch (userReposError) {
769
+ repoTest.tests.userReposList = {
770
+ success: false,
771
+ status: userReposError.response?.status,
772
+ message: userReposError.message
773
+ };
774
+ }
775
+
776
+ } catch (parseError) {
777
+ repoTest.parseError = parseError.message;
778
+ }
779
+
780
+ debugInfo.repositoryTests.push(repoTest);
781
+ }
782
+
783
+ // 3. 提供修复建议
784
+ const suggestions = [];
785
+
786
+ if (debugInfo.tokenError) {
787
+ suggestions.push('Token authentication failed - check if GITHUB_TOKEN is valid');
788
+ }
789
+
790
+ debugInfo.repositoryTests.forEach((repoTest, index) => {
791
+ if (!repoTest.tests.basicAccess?.success) {
792
+ if (repoTest.tests.basicAccess?.status === 404) {
793
+ if (repoTest.tests.basicAccess?.possibleCause === 'Repository does not exist') {
794
+ suggestions.push(`Repository ${repoTest.url} does not exist - please create it on GitHub`);
795
+ } else {
796
+ suggestions.push(`Repository ${repoTest.url} exists but token lacks permission - check token scopes`);
797
+ }
798
+ } else if (repoTest.tests.basicAccess?.status === 403) {
799
+ suggestions.push(`Permission denied for ${repoTest.url} - check if token has 'repo' scope`);
800
+ }
801
+ }
802
+ });
803
+
804
+ debugInfo.suggestions = suggestions;
805
+
806
+ console.log('GitHub permissions debug completed');
807
+ res.json(debugInfo);
808
+
809
+ } catch (error) {
810
+ console.error('GitHub permissions check error:', error);
811
+ res.status(500).json({
812
+ error: error.message,
813
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
814
+ });
815
+ }
816
+ });
817
+
818
+ // 添加大文件处理调试端点
819
+ app.get('/api/debug/large-files/:userId', async (req, res) => {
820
+ try {
821
+ const { userId } = req.params;
822
+ console.log(`=== Large Files Debug for User: ${userId} ===`);
823
+
824
+ const { default: githubService } = await import('./services/githubService.js');
825
+
826
+ const debugInfo = {
827
+ timestamp: new Date().toISOString(),
828
+ userId: userId,
829
+ fileAnalysis: []
830
+ };
831
+
832
+ // 检查用户的所有PPT文件
833
+ const pptList = await githubService.getUserPPTList(userId);
834
+
835
+ for (const ppt of pptList) {
836
+ const fileInfo = {
837
+ pptId: ppt.name,
838
+ title: ppt.title,
839
+ fileName: `${ppt.name}.json`,
840
+ analysis: {}
841
+ };
842
+
843
+ try {
844
+ // 获取文件内容
845
+ const result = await githubService.getFile(userId, `${ppt.name}.json`, ppt.repoIndex || 0);
846
+
847
+ if (result && result.content) {
848
+ const content = result.content;
849
+ const jsonString = JSON.stringify(content);
850
+ const fileSize = Buffer.byteLength(jsonString, 'utf8');
851
+
852
+ fileInfo.analysis = {
853
+ fileSize: fileSize,
854
+ fileSizeKB: (fileSize / 1024).toFixed(2),
855
+ slidesCount: content.slides?.length || 0,
856
+ isChunked: !!content.isChunked,
857
+ chunkedInfo: content.isChunked ? {
858
+ totalChunks: content.totalChunks,
859
+ totalSlides: content.totalSlides
860
+ } : null,
861
+ wasReassembled: !!result.isReassembled,
862
+ metadata: content.metadata || 'No metadata',
863
+ status: fileSize > 1024 * 1024 ? 'LARGE' : fileSize > 800 * 1024 ? 'MEDIUM' : 'NORMAL'
864
+ };
865
+
866
+ // 分析每个slide的大小
867
+ if (content.slides && content.slides.length > 0) {
868
+ const slideSizes = content.slides.map((slide, index) => {
869
+ const slideJson = JSON.stringify(slide);
870
+ const slideSize = Buffer.byteLength(slideJson, 'utf8');
871
+ return {
872
+ index: index,
873
+ size: slideSize,
874
+ sizeKB: (slideSize / 1024).toFixed(2),
875
+ elementsCount: slide.elements?.length || 0
876
+ };
877
+ });
878
+
879
+ // 找出最大的slides
880
+ const largestSlides = slideSizes
881
+ .sort((a, b) => b.size - a.size)
882
+ .slice(0, 3);
883
+
884
+ fileInfo.analysis.slideSummary = {
885
+ averageSlideSize: (fileSize / content.slides.length).toFixed(0),
886
+ largestSlides: largestSlides
887
+ };
888
+ }
889
+ } else {
890
+ fileInfo.analysis = { error: 'Could not read file content' };
891
+ }
892
+
893
+ } catch (error) {
894
+ fileInfo.analysis = {
895
+ error: error.message,
896
+ errorType: error.name
897
+ };
898
+ }
899
+
900
+ debugInfo.fileAnalysis.push(fileInfo);
901
+ }
902
+
903
+ // 添加统计摘要
904
+ debugInfo.summary = {
905
+ totalFiles: debugInfo.fileAnalysis.length,
906
+ largeFiles: debugInfo.fileAnalysis.filter(f => f.analysis.status === 'LARGE').length,
907
+ chunkedFiles: debugInfo.fileAnalysis.filter(f => f.analysis.isChunked).length,
908
+ errors: debugInfo.fileAnalysis.filter(f => f.analysis.error).length
909
+ };
910
+
911
+ console.log('Large files debug completed');
912
+ res.json(debugInfo);
913
+
914
+ } catch (error) {
915
+ console.error('Large files debug error:', error);
916
+ res.status(500).json({
917
+ error: error.message,
918
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
919
+ });
920
+ }
921
+ });
922
+
923
+ // 添加分块文件修复端点
924
+ app.post('/api/debug/fix-chunked-file/:userId/:pptId', async (req, res) => {
925
+ try {
926
+ const { userId, pptId } = req.params;
927
+ console.log(`=== Fixing Chunked File: ${userId}/${pptId} ===`);
928
+
929
+ const { default: githubService } = await import('./services/githubService.js');
930
+ const fileName = `${pptId}.json`;
931
+
932
+ const result = {
933
+ timestamp: new Date().toISOString(),
934
+ userId,
935
+ pptId,
936
+ fileName,
937
+ status: 'unknown',
938
+ details: {},
939
+ actions: []
940
+ };
941
+
942
+ // 1. 检查主文件是否存在
943
+ let mainFile = null;
944
+ let mainFileRepo = -1;
945
+
946
+ for (let i = 0; i < githubService.repositories.length; i++) {
947
+ try {
948
+ const fileResult = await githubService.getFile(userId, fileName, i);
949
+ if (fileResult) {
950
+ mainFile = fileResult;
951
+ mainFileRepo = i;
952
+ result.details.mainFileFound = true;
953
+ result.details.mainFileRepo = i;
954
+ result.actions.push(`Main file found in repository ${i}`);
955
+ break;
956
+ }
957
+ } catch (error) {
958
+ continue;
959
+ }
960
+ }
961
+
962
+ if (!mainFile) {
963
+ result.status = 'error';
964
+ result.details.error = 'Main file not found in any repository';
965
+ return res.json(result);
966
+ }
967
+
968
+ const content = mainFile.content;
969
+
970
+ // 2. 检查是否是分块文件
971
+ if (!content.isChunked) {
972
+ result.status = 'normal';
973
+ result.details.isChunked = false;
974
+ result.details.slideCount = content.slides?.length || 0;
975
+ result.actions.push('File is not chunked, no action needed');
976
+ return res.json(result);
977
+ }
978
+
979
+ // 3. 分析分块文件状态
980
+ result.details.isChunked = true;
981
+ result.details.totalChunks = content.totalChunks;
982
+ result.details.totalSlides = content.totalSlides;
983
+ result.details.mainFileSlides = content.slides?.length || 0;
984
+
985
+ // 4. 检查所有chunk文件
986
+ const chunkStatus = [];
987
+ let totalFoundSlides = content.slides?.length || 0;
988
+
989
+ for (let i = 1; i < content.totalChunks; i++) {
990
+ const chunkFileName = fileName.replace('.json', `_chunk_${i}.json`);
991
+ const chunkInfo = {
992
+ index: i,
993
+ fileName: chunkFileName,
994
+ found: false,
995
+ slides: 0,
996
+ error: null
997
+ };
998
+
999
+ try {
1000
+ const repoUrl = githubService.repositories[mainFileRepo];
1001
+ const { owner, repo } = githubService.parseRepoUrl(repoUrl);
1002
+ const path = `users/${userId}/${chunkFileName}`;
1003
+
1004
+ const response = await axios.get(
1005
+ `${githubService.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
1006
+ {
1007
+ headers: {
1008
+ 'Authorization': `token ${githubService.token}`,
1009
+ 'Accept': 'application/vnd.github.v3+json'
1010
+ },
1011
+ timeout: 30000
1012
+ }
1013
+ );
1014
+
1015
+ const chunkContent = Buffer.from(response.data.content, 'base64').toString('utf8');
1016
+ const chunkData = JSON.parse(chunkContent);
1017
+
1018
+ chunkInfo.found = true;
1019
+ chunkInfo.slides = chunkData.slides?.length || 0;
1020
+ totalFoundSlides += chunkInfo.slides;
1021
+
1022
+ result.actions.push(`Chunk ${i} found: ${chunkInfo.slides} slides`);
1023
+ } catch (error) {
1024
+ chunkInfo.error = error.message;
1025
+ result.actions.push(`Chunk ${i} missing or error: ${error.message}`);
1026
+ }
1027
+
1028
+ chunkStatus.push(chunkInfo);
1029
+ }
1030
+
1031
+ result.details.chunks = chunkStatus;
1032
+ result.details.totalFoundSlides = totalFoundSlides;
1033
+ result.details.missingSlides = content.totalSlides - totalFoundSlides;
1034
+
1035
+ // 5. 判断状态和建议修复方案
1036
+ const missingChunks = chunkStatus.filter(chunk => !chunk.found);
1037
+
1038
+ if (missingChunks.length === 0 && totalFoundSlides === content.totalSlides) {
1039
+ result.status = 'healthy';
1040
+ result.actions.push('All chunks found, file should load correctly');
1041
+ } else if (missingChunks.length > 0) {
1042
+ result.status = 'incomplete';
1043
+ result.details.missingChunks = missingChunks.map(c => c.index);
1044
+ result.actions.push(`Missing chunks: ${missingChunks.map(c => c.index).join(', ')}`);
1045
+
1046
+ // 提供修复建议
1047
+ if (totalFoundSlides >= content.totalSlides * 0.8) {
1048
+ result.actions.push('Recommendation: Reassemble available slides into single file');
1049
+ result.details.recommendation = 'reassemble';
1050
+ } else {
1051
+ result.actions.push('Recommendation: File may be corrupted, consider restoration from backup');
1052
+ result.details.recommendation = 'restore';
1053
+ }
1054
+ } else {
1055
+ result.status = 'mismatch';
1056
+ result.actions.push('Slide count mismatch detected');
1057
+ }
1058
+
1059
+ console.log('Chunked file analysis completed:', result);
1060
+ res.json(result);
1061
+
1062
+ } catch (error) {
1063
+ console.error('Chunked file fix error:', error);
1064
+ res.status(500).json({
1065
+ error: error.message,
1066
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
1067
+ });
1068
+ }
1069
+ });
1070
+
1071
+ // 添加分块文件重组端点
1072
+ app.post('/api/debug/reassemble-chunked-file/:userId/:pptId', async (req, res) => {
1073
+ try {
1074
+ const { userId, pptId } = req.params;
1075
+ console.log(`=== Reassembling Chunked File: ${userId}/${pptId} ===`);
1076
+
1077
+ const { default: githubService } = await import('./services/githubService.js');
1078
+ const fileName = `${pptId}.json`;
1079
+
1080
+ // 强制重新组装文件
1081
+ const result = await githubService.getFile(userId, fileName, 0);
1082
+
1083
+ if (!result) {
1084
+ return res.status(404).json({ error: 'File not found' });
1085
+ }
1086
+
1087
+ if (result.isReassembled) {
1088
+ res.json({
1089
+ success: true,
1090
+ message: 'File reassembled successfully',
1091
+ slideCount: result.content.slides?.length || 0,
1092
+ wasChunked: !!result.content.reassembledInfo,
1093
+ reassembledInfo: result.content.reassembledInfo
1094
+ });
1095
+ } else {
1096
+ res.json({
1097
+ success: true,
1098
+ message: 'File was not chunked',
1099
+ slideCount: result.content.slides?.length || 0,
1100
+ wasChunked: false
1101
+ });
1102
+ }
1103
+
1104
+ } catch (error) {
1105
+ console.error('File reassembly error:', error);
1106
+ res.status(500).json({
1107
+ error: error.message,
1108
+ details: error.stack
1109
+ });
1110
+ }
1111
+ });
1112
+
1113
  // 添加路由注册日志
1114
  console.log('Importing route modules...');
1115
  console.log('Auth routes imported:', !!authRoutes);
 
1141
  res.status(404).json({ error: 'API route not found', path: req.path });
1142
  });
1143
 
1144
+ // 前端路由处理 - 修复ES模块兼容性
1145
  app.get('*', (req, res) => {
1146
+ const indexPath = path.join(frontendDistPath, 'index.html');
1147
+ console.log(`Serving frontend route: ${req.path}, index.html path: ${indexPath}`);
1148
+
1149
+ // 使用ES模块的fs检查文件是否存在
1150
+ if (fs.existsSync(indexPath)) {
1151
+ res.sendFile(indexPath);
1152
+ } else {
1153
+ console.error('index.html not found at:', indexPath);
1154
+ res.status(404).send(`
1155
+ <h1>前端文件未找到</h1>
1156
+ <p>index.html路径: ${indexPath}</p>
1157
+ <p>请确保前端已正确构建</p>
1158
+ <a href="/test">访问API测试页面</a>
1159
+ `);
1160
+ }
1161
  });
1162
 
1163
  // 错误处理中间件
backend/src/middleware/errorHandler.js CHANGED
@@ -16,20 +16,86 @@ export const errorHandler = (err, req, res, next) => {
16
  const message = err.response.data?.message || 'GitHub API error';
17
 
18
  if (status === 401) {
19
- return res.status(500).json({ error: 'GitHub authentication failed' });
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
  if (status === 404) {
23
- return res.status(404).json({ error: 'Resource not found in GitHub' });
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
- return res.status(500).json({ error: `GitHub API error: ${message}` });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  // 默认错误
 
 
30
  res.status(500).json({
31
- error: process.env.NODE_ENV === 'production'
32
- ? 'Internal server error'
33
- : err.message
 
34
  });
35
  };
 
16
  const message = err.response.data?.message || 'GitHub API error';
17
 
18
  if (status === 401) {
19
+ return res.status(500).json({
20
+ error: 'GitHub authentication failed',
21
+ details: 'GitHub token may be invalid or expired',
22
+ suggestion: 'Check GitHub token configuration'
23
+ });
24
+ }
25
+
26
+ if (status === 403) {
27
+ return res.status(500).json({
28
+ error: 'GitHub access forbidden',
29
+ details: 'Insufficient permissions or rate limit exceeded',
30
+ suggestion: 'Check repository permissions or wait for rate limit reset'
31
+ });
32
  }
33
 
34
  if (status === 404) {
35
+ return res.status(404).json({
36
+ error: 'Resource not found in GitHub',
37
+ details: message,
38
+ suggestion: 'Check if the repository or file exists'
39
+ });
40
+ }
41
+
42
+ if (status === 422) {
43
+ return res.status(413).json({
44
+ error: 'File too large for GitHub',
45
+ details: message,
46
+ suggestion: 'Try reducing file size or splitting into smaller files'
47
+ });
48
  }
49
 
50
+ return res.status(500).json({
51
+ error: `GitHub API error: ${message}`,
52
+ details: `HTTP ${status}: ${message}`,
53
+ suggestion: 'Check GitHub service status or try again later'
54
+ });
55
+ }
56
+
57
+ // Axios 网络错误
58
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
59
+ return res.status(503).json({
60
+ error: 'Service unavailable',
61
+ details: 'Cannot connect to GitHub API',
62
+ suggestion: 'Check network connection or GitHub service status'
63
+ });
64
+ }
65
+
66
+ if (err.code === 'ETIMEDOUT') {
67
+ return res.status(504).json({
68
+ error: 'Request timeout',
69
+ details: 'GitHub API request timed out',
70
+ suggestion: 'Try again with smaller data or check network connection'
71
+ });
72
+ }
73
+
74
+ // 自定义应用错误
75
+ if (err.message.includes('Too many slides failed to save')) {
76
+ return res.status(500).json({
77
+ error: 'Partial save failure',
78
+ details: err.message,
79
+ suggestion: 'Some slides could not be saved. Check individual slide content and try again.',
80
+ partialFailure: true
81
+ });
82
+ }
83
+
84
+ if (err.message.includes('Failed to save PPT')) {
85
+ return res.status(500).json({
86
+ error: 'PPT save failed',
87
+ details: err.message,
88
+ suggestion: 'Check PPT content and try saving again. Consider reducing file size if needed.'
89
+ });
90
  }
91
 
92
  // 默认错误
93
+ const isDevelopment = process.env.NODE_ENV === 'development';
94
+
95
  res.status(500).json({
96
+ error: isDevelopment ? err.message : 'Internal server error',
97
+ details: isDevelopment ? err.stack : 'An unexpected error occurred',
98
+ suggestion: 'If the problem persists, please contact support',
99
+ timestamp: new Date().toISOString()
100
  });
101
  };
backend/src/routes/ppt.js CHANGED
@@ -1,7 +1,6 @@
1
  import express from 'express';
2
  import { v4 as uuidv4 } from 'uuid';
3
  import githubService from '../services/githubService.js';
4
- import memoryStorageService from '../services/memoryStorageService.js';
5
 
6
  const router = express.Router();
7
 
@@ -11,21 +10,11 @@ router.use((req, res, next) => {
11
  next();
12
  });
13
 
14
- // 选择存储服务
15
- const getStorageService = () => {
16
- // 如果GitHub Token未配置,使用内存存储
17
- if (!process.env.GITHUB_TOKEN) {
18
- return memoryStorageService;
19
- }
20
- return githubService;
21
- };
22
-
23
  // 获取用户的PPT列表
24
  router.get('/list', async (req, res, next) => {
25
  try {
26
  const userId = req.user.userId;
27
- const storageService = getStorageService();
28
- const pptList = await storageService.getUserPPTList(userId);
29
  res.json(pptList);
30
  } catch (error) {
31
  next(error);
@@ -37,77 +26,409 @@ router.get('/:pptId', async (req, res, next) => {
37
  try {
38
  const userId = req.user.userId;
39
  const { pptId } = req.params;
40
- const fileName = `${pptId}.json`;
41
- const storageService = getStorageService();
42
 
43
  let pptData = null;
 
44
 
45
- // 如果是GitHub服务,尝试所有仓库
46
- if (storageService === githubService && storageService.repositories) {
47
- for (let i = 0; i < storageService.repositories.length; i++) {
 
 
 
 
 
 
 
 
 
 
48
  try {
49
- const result = await storageService.getFile(userId, fileName, i);
50
- if (result) {
 
51
  pptData = result.content;
 
 
52
  break;
53
  }
54
  } catch (error) {
 
55
  continue;
56
  }
57
  }
58
- } else {
59
- // 内存存储服务
60
- const result = await storageService.getFile(userId, fileName);
61
- if (result) {
62
- pptData = result.content;
63
- }
64
  }
65
 
66
  if (!pptData) {
67
- return res.status(404).json({ error: 'PPT not found' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
- res.json(pptData);
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  } catch (error) {
 
72
  next(error);
73
  }
74
  });
75
 
76
- // 保存PPT数据
77
  router.post('/save', async (req, res, next) => {
78
  try {
79
  const userId = req.user.userId;
80
  const { pptId, title, slides, theme, viewportSize, viewportRatio } = req.body;
81
 
 
 
 
 
 
 
 
 
 
 
82
  if (!pptId || !slides) {
 
83
  return res.status(400).json({ error: 'PPT ID and slides are required' });
84
  }
85
 
86
- const fileName = `${pptId}.json`;
 
 
 
 
 
 
87
  const pptData = {
88
  id: pptId,
89
  title: title || '未命名演示文稿',
90
- slides: slides,
91
- theme: theme || {},
92
- // 保存关键的尺寸信息
93
- viewportSize: viewportSize,
94
- viewportRatio: viewportRatio,
 
 
 
 
 
 
 
 
 
95
  createdAt: new Date().toISOString(),
96
  updatedAt: new Date().toISOString()
97
  };
98
 
99
- const storageService = getStorageService();
 
 
100
 
101
- // 如果是GitHub服务,保存到第一个仓库
102
- if (storageService === githubService) {
103
- await storageService.saveFile(userId, fileName, pptData, 0);
104
- } else {
105
- // 内存存储服务
106
- await storageService.saveFile(userId, fileName, pptData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
 
109
- res.json({ message: 'PPT saved successfully', pptId });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  } catch (error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  next(error);
112
  }
113
  });
@@ -125,6 +446,7 @@ router.post('/create', async (req, res, next) => {
125
  const pptId = uuidv4();
126
  const now = new Date().toISOString();
127
 
 
128
  const pptData = {
129
  id: pptId,
130
  title,
@@ -159,42 +481,25 @@ router.post('/create', async (req, res, next) => {
159
  }
160
  }
161
  ],
 
 
 
162
  createdAt: now,
163
  updatedAt: now
164
  };
165
 
166
- const storageService = getStorageService();
167
  const fileName = `${pptId}.json`;
168
 
169
- console.log(`Creating PPT for user ${userId}, using storage: ${storageService === githubService ? 'GitHub' : 'Memory'}`);
170
 
171
- try {
172
- await storageService.saveFile(userId, fileName, pptData);
173
- console.log(`PPT created successfully: ${pptId}`);
174
- res.json({ success: true, pptId, pptData });
175
- } catch (saveError) {
176
- console.error('Error saving PPT:', saveError.message);
177
-
178
- // 如果GitHub保存失败,尝试使用内存存储作为fallback
179
- if (storageService === githubService) {
180
- console.log('GitHub save failed, falling back to memory storage');
181
- try {
182
- await memoryStorageService.saveFile(userId, fileName, pptData);
183
- console.log(`PPT saved to memory storage: ${pptId}`);
184
- res.json({
185
- success: true,
186
- pptId,
187
- pptData,
188
- warning: 'Saved to temporary storage due to GitHub connection issue'
189
- });
190
- } catch (memoryError) {
191
- console.error('Memory storage also failed:', memoryError.message);
192
- throw new Error('Failed to save PPT to any storage');
193
- }
194
- } else {
195
- throw saveError;
196
- }
197
  }
 
 
 
198
  } catch (error) {
199
  console.error('PPT creation error:', error);
200
  next(error);
@@ -207,21 +512,30 @@ router.delete('/:pptId', async (req, res, next) => {
207
  const userId = req.user.userId;
208
  const { pptId } = req.params;
209
  const fileName = `${pptId}.json`;
210
- const storageService = getStorageService();
211
 
212
- // 如果是GitHub服务,从所有仓库中删除
213
- if (storageService === githubService && storageService.repositories) {
214
- for (let i = 0; i < storageService.repositories.length; i++) {
 
 
 
 
 
 
 
215
  try {
216
- await storageService.deleteFile(userId, fileName, i);
 
 
217
  } catch (error) {
218
  // 继续尝试其他仓库
219
  continue;
220
  }
221
  }
222
- } else {
223
- // 内存存储服务
224
- await storageService.deleteFile(userId, fileName);
 
225
  }
226
 
227
  res.json({ message: 'PPT deleted successfully' });
@@ -237,27 +551,26 @@ router.post('/:pptId/copy', async (req, res, next) => {
237
  const { pptId } = req.params;
238
  const { title } = req.body;
239
  const sourceFileName = `${pptId}.json`;
240
- const storageService = getStorageService();
241
 
242
  // 获取源PPT数据
243
  let sourcePPT = null;
244
- if (storageService === githubService && storageService.repositories) {
245
- for (let i = 0; i < storageService.repositories.length; i++) {
 
 
 
 
 
246
  try {
247
- const result = await storageService.getFile(userId, sourceFileName, i);
248
  if (result) {
249
- sourcePPT = result.content;
250
  break;
251
  }
252
  } catch (error) {
253
  continue;
254
  }
255
  }
256
- } else {
257
- const result = await storageService.getFile(userId, sourceFileName);
258
- if (result) {
259
- sourcePPT = result.content;
260
- }
261
  }
262
 
263
  if (!sourcePPT) {
@@ -276,10 +589,10 @@ router.post('/:pptId/copy', async (req, res, next) => {
276
  };
277
 
278
  // 保存复制的PPT
279
- if (storageService === githubService) {
280
- await storageService.saveFile(userId, newFileName, newPPTData, 0);
281
  } else {
282
- await storageService.saveFile(userId, newFileName, newPPTData);
283
  }
284
 
285
  res.json({
 
1
  import express from 'express';
2
  import { v4 as uuidv4 } from 'uuid';
3
  import githubService from '../services/githubService.js';
 
4
 
5
  const router = express.Router();
6
 
 
10
  next();
11
  });
12
 
 
 
 
 
 
 
 
 
 
13
  // 获取用户的PPT列表
14
  router.get('/list', async (req, res, next) => {
15
  try {
16
  const userId = req.user.userId;
17
+ const pptList = await githubService.getUserPPTList(userId);
 
18
  res.json(pptList);
19
  } catch (error) {
20
  next(error);
 
26
  try {
27
  const userId = req.user.userId;
28
  const { pptId } = req.params;
29
+
30
+ console.log(`🔍 Fetching PPT: ${pptId} for user: ${userId}`);
31
 
32
  let pptData = null;
33
+ let foundInRepo = -1;
34
 
35
+ if (githubService.useMemoryStorage) {
36
+ // 内存存储模式
37
+ const result = await githubService.getPPTFromMemory(userId, pptId);
38
+ if (result && result.content) {
39
+ pptData = result.content;
40
+ foundInRepo = 0;
41
+ console.log(`✅ PPT found in memory storage`);
42
+ }
43
+ } else {
44
+ // GitHub存储模式 - 尝试所有仓库
45
+ console.log(`Available repositories: ${githubService.repositories.length}`);
46
+
47
+ for (let i = 0; i < githubService.repositories.length; i++) {
48
  try {
49
+ console.log(`📂 Checking repository ${i}: ${githubService.repositories[i]}`);
50
+ const result = await githubService.getPPT(userId, pptId, i);
51
+ if (result && result.content) {
52
  pptData = result.content;
53
+ foundInRepo = i;
54
+ console.log(`✅ PPT found in repository ${i}${result.isReassembled ? ' (reassembled from chunks)' : ''}`);
55
  break;
56
  }
57
  } catch (error) {
58
+ console.log(`❌ PPT not found in repository ${i}: ${error.message}`);
59
  continue;
60
  }
61
  }
 
 
 
 
 
 
62
  }
63
 
64
  if (!pptData) {
65
+ console.log(`❌ PPT ${pptId} not found for user ${userId}`);
66
+ return res.status(404).json({
67
+ error: 'PPT not found',
68
+ pptId: pptId,
69
+ userId: userId,
70
+ storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
71
+ });
72
+ }
73
+
74
+ // 🔧 修复:标准化PPT数据格式,确保前端兼容性
75
+ const standardizedPptData = {
76
+ // 确保基本字段存在
77
+ id: pptData.id || pptData.pptId || pptId,
78
+ pptId: pptData.pptId || pptData.id || pptId,
79
+ title: pptData.title || '未命名演示文稿',
80
+
81
+ // 标准化slides数组
82
+ slides: Array.isArray(pptData.slides) ? pptData.slides.map((slide, index) => ({
83
+ id: slide.id || `slide-${index}`,
84
+ elements: Array.isArray(slide.elements) ? slide.elements : [],
85
+ background: slide.background || { type: 'solid', color: '#ffffff' },
86
+ ...slide
87
+ })) : [],
88
+
89
+ // 标准化主题
90
+ theme: pptData.theme || {
91
+ backgroundColor: '#ffffff',
92
+ themeColor: '#d14424',
93
+ fontColor: '#333333',
94
+ fontName: 'Microsoft YaHei'
95
+ },
96
+
97
+ // 🔧 关键修复:确保视口信息正确传递
98
+ viewportSize: pptData.viewportSize || 1000,
99
+ viewportRatio: pptData.viewportRatio || 0.5625,
100
+
101
+ // 时间戳
102
+ createdAt: pptData.createdAt || new Date().toISOString(),
103
+ updatedAt: pptData.updatedAt || new Date().toISOString(),
104
+
105
+ // 保留其他可能的属性
106
+ ...pptData
107
+ };
108
+
109
+ // 🔧 新增:数据验证和修复
110
+ if (standardizedPptData.slides.length === 0) {
111
+ console.log(`⚠️ PPT ${pptId} has no slides, creating default slide`);
112
+ standardizedPptData.slides = [{
113
+ id: 'default-slide',
114
+ elements: [],
115
+ background: { type: 'solid', color: '#ffffff' }
116
+ }];
117
+ }
118
+
119
+ // 验证视口比例的合理性
120
+ if (standardizedPptData.viewportRatio <= 0 || standardizedPptData.viewportRatio > 2) {
121
+ console.log(`⚠️ Invalid viewportRatio ${standardizedPptData.viewportRatio}, resetting to 0.5625`);
122
+ standardizedPptData.viewportRatio = 0.5625;
123
  }
124
 
125
+ // 验证视口尺寸的合理性
126
+ if (standardizedPptData.viewportSize <= 0 || standardizedPptData.viewportSize > 2000) {
127
+ console.log(`⚠️ Invalid viewportSize ${standardizedPptData.viewportSize}, resetting to 1000`);
128
+ standardizedPptData.viewportSize = 1000;
129
+ }
130
+
131
+ console.log(`✅ Successfully found and standardized PPT ${pptId}:`, {
132
+ slidesCount: standardizedPptData.slides.length,
133
+ viewportSize: standardizedPptData.viewportSize,
134
+ viewportRatio: standardizedPptData.viewportRatio,
135
+ storageMode: githubService.useMemoryStorage ? 'memory' : `repository ${foundInRepo}`
136
+ });
137
+
138
+ res.json(standardizedPptData);
139
  } catch (error) {
140
+ console.error(`❌ Error fetching PPT ${req.params.pptId}:`, error);
141
  next(error);
142
  }
143
  });
144
 
145
+ // 保存PPT数据 - 新架构版本
146
  router.post('/save', async (req, res, next) => {
147
  try {
148
  const userId = req.user.userId;
149
  const { pptId, title, slides, theme, viewportSize, viewportRatio } = req.body;
150
 
151
+ console.log(`🔄 Starting PPT save for user ${userId}, PPT ID: ${pptId}`);
152
+ console.log(`📊 Request body analysis:`, {
153
+ pptId: !!pptId,
154
+ title: title?.length || 0,
155
+ slidesCount: Array.isArray(slides) ? slides.length : 'not array',
156
+ theme: !!theme,
157
+ requestSize: req.get('content-length'),
158
+ storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
159
+ });
160
+
161
  if (!pptId || !slides) {
162
+ console.log(`❌ Missing required fields - pptId: ${!!pptId}, slides: ${!!slides}`);
163
  return res.status(400).json({ error: 'PPT ID and slides are required' });
164
  }
165
 
166
+ // 验证slides数组
167
+ if (!Array.isArray(slides) || slides.length === 0) {
168
+ console.log(`❌ Invalid slides array - isArray: ${Array.isArray(slides)}, length: ${slides?.length || 0}`);
169
+ return res.status(400).json({ error: 'Slides must be a non-empty array' });
170
+ }
171
+
172
+ // 构建标准化的PPT数据结构
173
  const pptData = {
174
  id: pptId,
175
  title: title || '未命名演示文稿',
176
+ slides: slides.map((slide, index) => ({
177
+ id: slide.id || `slide-${index}`,
178
+ elements: Array.isArray(slide.elements) ? slide.elements : [],
179
+ background: slide.background || { type: 'solid', color: '#ffffff' },
180
+ ...slide
181
+ })),
182
+ theme: theme || {
183
+ backgroundColor: '#ffffff',
184
+ themeColor: '#d14424',
185
+ fontColor: '#333333',
186
+ fontName: 'Microsoft YaHei'
187
+ },
188
+ viewportSize: viewportSize || 1000,
189
+ viewportRatio: viewportRatio || 0.5625,
190
  createdAt: new Date().toISOString(),
191
  updatedAt: new Date().toISOString()
192
  };
193
 
194
+ // 计算文件大小分析
195
+ const jsonData = JSON.stringify(pptData);
196
+ const totalDataSize = Buffer.byteLength(jsonData, 'utf8');
197
 
198
+ console.log(`📊 PPT data analysis:`);
199
+ console.log(` - Total slides: ${slides.length}`);
200
+ console.log(` - Total data size: ${totalDataSize} bytes (${(totalDataSize / 1024).toFixed(2)} KB)`);
201
+ console.log(` - Average slide size: ${(totalDataSize / slides.length / 1024).toFixed(2)} KB`);
202
+
203
+ // 分析每个slide的大小
204
+ const slideAnalysis = slides.map((slide, index) => {
205
+ const slideJson = JSON.stringify(slide);
206
+ const slideSize = Buffer.byteLength(slideJson, 'utf8');
207
+ return {
208
+ index,
209
+ size: slideSize,
210
+ sizeKB: (slideSize / 1024).toFixed(2),
211
+ elementsCount: slide.elements?.length || 0,
212
+ hasLargeContent: slideSize > 800 * 1024 // 800KB+
213
+ };
214
+ });
215
+
216
+ const largeSlides = slideAnalysis.filter(s => s.hasLargeContent);
217
+ const maxSlideSize = Math.max(...slideAnalysis.map(s => s.size));
218
+
219
+ console.log(`📋 Slide size analysis:`);
220
+ console.log(` - Largest slide: ${(maxSlideSize / 1024).toFixed(2)} KB`);
221
+ console.log(` - Large slides (>800KB): ${largeSlides.length}`);
222
+ if (largeSlides.length > 0) {
223
+ console.log(` - Large slide indices: ${largeSlides.map(s => s.index).join(', ')}`);
224
  }
225
 
226
+ try {
227
+ let result;
228
+
229
+ console.log(`💾 Using storage mode: ${githubService.useMemoryStorage ? 'memory' : 'GitHub folder architecture'}`);
230
+
231
+ if (githubService.useMemoryStorage) {
232
+ // 内存存储模式
233
+ result = await githubService.savePPTToMemory(userId, pptId, pptData);
234
+ console.log(`✅ PPT saved to memory storage:`, result);
235
+ } else {
236
+ // GitHub存储模式 - 使用新的文件夹架构
237
+ console.log(`🐙 Using GitHub folder storage for ${pptId}`);
238
+ console.log(`📂 Available repositories: ${githubService.repositories?.length || 0}`);
239
+
240
+ if (!githubService.repositories || githubService.repositories.length === 0) {
241
+ throw new Error('No GitHub repositories configured');
242
+ }
243
+
244
+ console.log(`🔍 Pre-save validation:`);
245
+ console.log(` - Repository 0: ${githubService.repositories[0]}`);
246
+ console.log(` - Token configured: ${!!githubService.token}`);
247
+ console.log(` - API URL: ${githubService.apiUrl}`);
248
+
249
+ result = await githubService.savePPT(userId, pptId, pptData, 0);
250
+ console.log(`✅ PPT saved to GitHub folder storage:`, result);
251
+ }
252
+
253
+ console.log(`✅ PPT save completed successfully:`, {
254
+ pptId,
255
+ userId,
256
+ slideCount: slides.length,
257
+ totalDataSize,
258
+ storageType: result.storage || 'unknown',
259
+ compressionApplied: result.compressionSummary?.compressedSlides > 0,
260
+ storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
261
+ });
262
+
263
+ const response = {
264
+ message: 'PPT saved successfully',
265
+ pptId,
266
+ slidesCount: slides.length,
267
+ savedAt: new Date().toISOString(),
268
+ totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
269
+ storageType: result.storage || 'unknown',
270
+ storageMode: githubService.useMemoryStorage ? 'memory' : 'github',
271
+ architecture: 'folder-based',
272
+ slideAnalysis: {
273
+ totalSlides: slides.length,
274
+ largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
275
+ largeSlides: largeSlides.length,
276
+ averageSlideSize: `${(totalDataSize / slides.length / 1024).toFixed(2)} KB`
277
+ }
278
+ };
279
+
280
+ // 添加压缩信息
281
+ if (result.compressionSummary) {
282
+ response.compression = {
283
+ applied: result.compressionSummary.compressedSlides > 0,
284
+ compressedSlides: result.compressionSummary.compressedSlides,
285
+ totalSlides: result.compressionSummary.totalSlides,
286
+ savedSlides: result.compressionSummary.savedSlides,
287
+ failedSlides: result.compressionSummary.failedSlides,
288
+ originalSize: `${(result.compressionSummary.totalOriginalSize / 1024).toFixed(2)} KB`,
289
+ finalSize: `${(result.compressionSummary.totalFinalSize / 1024).toFixed(2)} KB`,
290
+ savedSpace: `${((result.compressionSummary.totalOriginalSize - result.compressionSummary.totalFinalSize) / 1024).toFixed(2)} KB`,
291
+ compressionRatio: `${(result.compressionSummary.compressionRatio * 100).toFixed(1)}%`
292
+ };
293
+
294
+ if (result.compressionSummary.compressedSlides > 0) {
295
+ response.message = `PPT saved successfully with ${result.compressionSummary.compressedSlides} slides optimized for storage`;
296
+ response.optimization = `Automatic compression applied to ${result.compressionSummary.compressedSlides} large slides, saving ${response.compression.savedSpace}`;
297
+ }
298
+
299
+ // 添加保存警告信息
300
+ if (result.compressionSummary.failedSlides > 0) {
301
+ response.warning = `${result.compressionSummary.failedSlides} slides failed to save`;
302
+ response.partialSave = true;
303
+ response.savedSlides = result.compressionSummary.savedSlides;
304
+ response.failedSlides = result.compressionSummary.failedSlides;
305
+
306
+ if (result.compressionSummary.errors) {
307
+ response.slideErrors = result.compressionSummary.errors;
308
+ }
309
+ }
310
+ }
311
+
312
+ // 添加存储路径信息
313
+ if (result.folderPath) {
314
+ response.storagePath = result.folderPath;
315
+ response.architecture = 'folder-based (meta.json + individual slide files)';
316
+ }
317
+
318
+ // 添加警告信息
319
+ if (result.warnings) {
320
+ response.warnings = result.warnings;
321
+ }
322
+
323
+ res.json(response);
324
+
325
+ } catch (saveError) {
326
+ console.error(`❌ Save operation failed:`, {
327
+ error: saveError.message,
328
+ stack: saveError.stack,
329
+ userId,
330
+ pptId,
331
+ slidesCount: slides.length,
332
+ totalDataSize,
333
+ errorName: saveError.name,
334
+ errorCode: saveError.code
335
+ });
336
+
337
+ let errorResponse = {
338
+ error: 'PPT save failed',
339
+ details: saveError.message,
340
+ pptId: pptId,
341
+ slidesCount: slides.length,
342
+ totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
343
+ timestamp: new Date().toISOString(),
344
+ slideAnalysis: {
345
+ largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
346
+ largeSlides: largeSlides.length
347
+ }
348
+ };
349
+
350
+ // 根据错误类型提供具体建议
351
+ if (saveError.message.includes('File too large') || saveError.message.includes('exceeds')) {
352
+ errorResponse.error = 'Slide file too large for storage';
353
+ errorResponse.suggestion = 'Some slides are too large even after compression. Try reducing image sizes or slide complexity.';
354
+ errorResponse.limits = {
355
+ maxSlideSize: '999 KB per slide',
356
+ largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
357
+ oversizeSlides: largeSlides.length
358
+ };
359
+ return res.status(413).json(errorResponse);
360
+ }
361
+
362
+ if (saveError.message.includes('Failed to compress slide')) {
363
+ errorResponse.error = 'Slide compression failed';
364
+ errorResponse.suggestion = 'Unable to compress slides to acceptable size. Try manually reducing content complexity.';
365
+ errorResponse.compressionError = true;
366
+ return res.status(500).json(errorResponse);
367
+ }
368
+
369
+ if (saveError.message.includes('GitHub API') || saveError.message.includes('GitHub')) {
370
+ errorResponse.error = 'GitHub storage service error';
371
+ errorResponse.suggestion = 'Temporary GitHub service issue. Please try again in a few minutes.';
372
+ errorResponse.githubError = true;
373
+ return res.status(502).json(errorResponse);
374
+ }
375
+
376
+ if (saveError.message.includes('No GitHub repositories')) {
377
+ errorResponse.error = 'Storage configuration error';
378
+ errorResponse.suggestion = 'GitHub repositories not properly configured. Please contact support.';
379
+ errorResponse.configError = true;
380
+ return res.status(500).json(errorResponse);
381
+ }
382
+
383
+ // 网络相关错误
384
+ if (saveError.code === 'ETIMEDOUT' || saveError.message.includes('timeout')) {
385
+ errorResponse.error = 'Storage operation timeout';
386
+ errorResponse.suggestion = 'The save operation took too long. Try reducing file size or try again later.';
387
+ errorResponse.timeoutError = true;
388
+ return res.status(504).json(errorResponse);
389
+ }
390
+
391
+ // 默认错误
392
+ errorResponse.suggestion = 'Unknown save error. Please try again or contact support if the problem persists.';
393
+ errorResponse.unknownError = true;
394
+ return res.status(500).json(errorResponse);
395
+ }
396
+
397
  } catch (error) {
398
+ console.error(`❌ PPT save failed for user ${req.user?.userId}, PPT ID: ${req.body?.pptId}:`, {
399
+ error: error.message,
400
+ stack: error.stack,
401
+ userId: req.user?.userId,
402
+ pptId: req.body?.pptId,
403
+ requestSize: req.get('content-length'),
404
+ slidesCount: req.body?.slides?.length,
405
+ errorName: error.name,
406
+ errorCode: error.code
407
+ });
408
+
409
+ // 提供更详细的错误处理
410
+ let errorResponse = {
411
+ error: 'PPT save processing failed',
412
+ details: error.message,
413
+ timestamp: new Date().toISOString(),
414
+ userId: req.user?.userId,
415
+ pptId: req.body?.pptId,
416
+ architecture: 'folder-based'
417
+ };
418
+
419
+ if (error.message.includes('Invalid slide data')) {
420
+ errorResponse.error = 'Invalid slide data detected';
421
+ errorResponse.suggestion = 'One or more slides contain invalid or corrupted data';
422
+ return res.status(400).json(errorResponse);
423
+ }
424
+
425
+ if (error.message.includes('JSON')) {
426
+ errorResponse.error = 'Invalid data format';
427
+ errorResponse.suggestion = 'Check slide data structure and content';
428
+ return res.status(400).json(errorResponse);
429
+ }
430
+
431
+ errorResponse.suggestion = 'Unknown processing error. Please try again or contact support if the problem persists.';
432
  next(error);
433
  }
434
  });
 
446
  const pptId = uuidv4();
447
  const now = new Date().toISOString();
448
 
449
+ // 确保数据格式与前端store一致
450
  const pptData = {
451
  id: pptId,
452
  title,
 
481
  }
482
  }
483
  ],
484
+ // 确保视口信息与前端一致
485
+ viewportSize: 1000,
486
+ viewportRatio: 0.5625,
487
  createdAt: now,
488
  updatedAt: now
489
  };
490
 
 
491
  const fileName = `${pptId}.json`;
492
 
493
+ console.log(`Creating PPT for user ${userId}, using ${githubService.useMemoryStorage ? 'memory' : 'GitHub'} storage`);
494
 
495
+ if (githubService.useMemoryStorage) {
496
+ await githubService.saveToMemory(userId, fileName, pptData);
497
+ } else {
498
+ await githubService.saveFile(userId, fileName, pptData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
500
+
501
+ console.log(`PPT created successfully: ${pptId}`);
502
+ res.json({ success: true, pptId, pptData });
503
  } catch (error) {
504
  console.error('PPT creation error:', error);
505
  next(error);
 
512
  const userId = req.user.userId;
513
  const { pptId } = req.params;
514
  const fileName = `${pptId}.json`;
 
515
 
516
+ if (githubService.useMemoryStorage) {
517
+ // 内存存储模式
518
+ const deleted = githubService.memoryStorage.delete(`users/${userId}/${fileName}`);
519
+ if (!deleted) {
520
+ return res.status(404).json({ error: 'PPT not found' });
521
+ }
522
+ } else {
523
+ // GitHub存储模式 - 从所有仓库中删除
524
+ let deleted = false;
525
+ for (let i = 0; i < githubService.repositories.length; i++) {
526
  try {
527
+ await githubService.deleteFile(userId, fileName, i);
528
+ deleted = true;
529
+ break; // 只需要从一个仓库删除成功即可
530
  } catch (error) {
531
  // 继续尝试其他仓库
532
  continue;
533
  }
534
  }
535
+
536
+ if (!deleted) {
537
+ return res.status(404).json({ error: 'PPT not found in any repository' });
538
+ }
539
  }
540
 
541
  res.json({ message: 'PPT deleted successfully' });
 
551
  const { pptId } = req.params;
552
  const { title } = req.body;
553
  const sourceFileName = `${pptId}.json`;
 
554
 
555
  // 获取源PPT数据
556
  let sourcePPT = null;
557
+
558
+ if (githubService.useMemoryStorage) {
559
+ // 内存存储模式
560
+ sourcePPT = await githubService.getFromMemory(userId, sourceFileName);
561
+ } else {
562
+ // GitHub存储模式
563
+ for (let i = 0; i < githubService.repositories.length; i++) {
564
  try {
565
+ const result = await githubService.getFile(userId, sourceFileName, i);
566
  if (result) {
567
+ sourcePPT = result;
568
  break;
569
  }
570
  } catch (error) {
571
  continue;
572
  }
573
  }
 
 
 
 
 
574
  }
575
 
576
  if (!sourcePPT) {
 
589
  };
590
 
591
  // 保存复制的PPT
592
+ if (githubService.useMemoryStorage) {
593
+ await githubService.saveToMemory(userId, newFileName, newPPTData);
594
  } else {
595
+ await githubService.saveFile(userId, newFileName, newPPTData, 0);
596
  }
597
 
598
  res.json({
backend/src/routes/public.js CHANGED
@@ -1,566 +1,108 @@
1
  import express from 'express';
2
  import githubService from '../services/githubService.js';
3
- import memoryStorageService from '../services/memoryStorageService.js';
4
  import screenshotService from '../services/screenshotService.js';
 
 
5
 
6
  const router = express.Router();
7
 
8
- // 选择存储服务
9
- const getStorageService = () => {
10
- // 如果GitHub Token未配置,使用内存存储
11
- if (!process.env.GITHUB_TOKEN) {
12
- return memoryStorageService;
13
- }
14
- return githubService;
15
- };
16
-
17
- // 生成HTML页面来显示PPT幻灯片
18
- const generateSlideHTML = (pptData, slideIndex, title) => {
19
- const slide = pptData.slides[slideIndex];
20
- const theme = pptData.theme || {};
21
-
22
- // 精确计算PPT内容的真实尺寸 - 基于填满画布的图像元素推断
23
- const calculatePptDimensions = (slide) => {
24
- console.log('开始计算PPT尺寸,当前slide元素数量:', slide.elements?.length || 0);
25
-
26
- // 1. 优先使用PPT数据中的viewportSize和viewportRatio(编辑器的真实尺寸)
27
- if (pptData.viewportSize && pptData.viewportRatio) {
28
- const result = {
29
- width: Math.ceil(pptData.viewportSize),
30
- height: Math.ceil(pptData.viewportSize * pptData.viewportRatio)
31
- };
32
- console.log(`使用编辑器真实尺寸: ${result.width}x${result.height} (viewportSize: ${pptData.viewportSize}, viewportRatio: ${pptData.viewportRatio})`);
33
- return result;
34
- }
35
-
36
- // 2. 使用PPT数据中的预设尺寸
37
- if (pptData.slideSize && pptData.slideSize.width && pptData.slideSize.height) {
38
- const result = {
39
- width: Math.ceil(pptData.slideSize.width),
40
- height: Math.ceil(pptData.slideSize.height)
41
- };
42
- console.log(`使用PPT预设尺寸: ${result.width}x${result.height}`);
43
- return result;
44
- }
45
-
46
- // 3. 如果PPT数据中有width和height
47
- if (pptData.width && pptData.height) {
48
- const result = {
49
- width: Math.ceil(pptData.width),
50
- height: Math.ceil(pptData.height)
51
- };
52
- console.log(`使用PPT根级尺寸: ${result.width}x${result.height}`);
53
- return result;
54
- }
55
-
56
- // 4. 使用slide级别的尺寸设置
57
- if (slide.width && slide.height) {
58
- const result = {
59
- width: Math.ceil(slide.width),
60
- height: Math.ceil(slide.height)
61
- };
62
- console.log(`使用slide尺寸: ${result.width}x${result.height}`);
63
- return result;
64
- }
65
-
66
- // 5. 基于填满画布的图像元素精确推断PPT尺寸
67
- if (slide.elements && slide.elements.length > 0) {
68
- // 寻找可能填满画布的图像元素(left=0, top=0 或接近0,且尺寸较大)
69
- const candidateImages = slide.elements.filter(element =>
70
- element.type === 'image' &&
71
- Math.abs(element.left || 0) <= 10 && // 允许小偏差
72
- Math.abs(element.top || 0) <= 10 && // 允许小偏差
73
- (element.width || 0) >= 800 && // 宽度至少800
74
- (element.height || 0) >= 400 // 高度至少400
75
- );
76
-
77
- if (candidateImages.length > 0) {
78
- // 使用面积最大的图像作为画布尺寸参考
79
- const referenceImage = candidateImages.reduce((max, current) => {
80
- const maxArea = (max.width || 0) * (max.height || 0);
81
- const currentArea = (current.width || 0) * (current.height || 0);
82
- return currentArea > maxArea ? current : max;
83
- });
84
-
85
- const result = {
86
- width: Math.ceil(referenceImage.width || 1000),
87
- height: Math.ceil(referenceImage.height || 562)
88
- };
89
-
90
- console.log(`基于填满画布的图像推断PPT尺寸: ${result.width}x${result.height} (参考图像: ${referenceImage.id})`);
91
- console.log(`参考图像信息: left=${referenceImage.left}, top=${referenceImage.top}, width=${referenceImage.width}, height=${referenceImage.height}`);
92
-
93
- return result;
94
- }
95
-
96
- // 如果没有找到填满画布的图像,回退到边界计算
97
- let maxRight = 0;
98
- let maxBottom = 0;
99
- let minLeft = Infinity;
100
- let minTop = Infinity;
101
-
102
- // 计算所有元素的实际边界
103
- slide.elements.forEach(element => {
104
- const left = element.left || 0;
105
- const top = element.top || 0;
106
- const width = element.width || 0;
107
- const height = element.height || 0;
108
-
109
- const elementRight = left + width;
110
- const elementBottom = top + height;
111
-
112
- maxRight = Math.max(maxRight, elementRight);
113
- maxBottom = Math.max(maxBottom, elementBottom);
114
- minLeft = Math.min(minLeft, left);
115
- minTop = Math.min(minTop, top);
116
-
117
- console.log(`元素 ${element.id}: type=${element.type}, left=${left}, top=${top}, width=${width}, height=${height}, right=${elementRight}, bottom=${elementBottom}`);
118
- });
119
-
120
- // 重置无限值
121
- if (minLeft === Infinity) minLeft = 0;
122
- if (minTop === Infinity) minTop = 0;
123
-
124
- console.log(`元素边界: minLeft=${minLeft}, minTop=${minTop}, maxRight=${maxRight}, maxBottom=${maxBottom}`);
125
-
126
- // 根据实际元素分布确定画布尺寸
127
- const canvasLeft = Math.min(0, minLeft);
128
- const canvasTop = Math.min(0, minTop);
129
- const canvasRight = Math.max(maxRight, 1000); // 最小宽度1000(基于GitHub数据)
130
- const canvasBottom = Math.max(maxBottom, 562); // 最小高度562(基于GitHub数据)
131
-
132
- // 计算最终的画布尺寸
133
- let finalWidth = canvasRight - canvasLeft;
134
- let finalHeight = canvasBottom - canvasTop;
135
-
136
- // 基于从GitHub仓库观察到的实际比例进行智能调整
137
- const currentRatio = finalWidth / finalHeight;
138
- console.log(`当前计算比例: ${currentRatio.toFixed(3)}`);
139
-
140
- // 从GitHub数据分析:1000x562.5 ≈ 1.78 (接近16:9的1.77)
141
- const targetRatio = 1000 / 562.5; // ≈ 1.778
142
-
143
- // 如果比例接近观察到的标准比例,调整为精确比例
144
- if (Math.abs(currentRatio - targetRatio) < 0.2) {
145
- if (finalWidth > finalHeight * targetRatio) {
146
- finalHeight = finalWidth / targetRatio;
147
- } else {
148
- finalWidth = finalHeight * targetRatio;
149
  }
150
- console.log(`调整为GitHub观察到的标准比例: ${targetRatio.toFixed(3)}`);
151
- }
152
- // 如果比例接近16:9,调整为标准16:9
153
- else if (Math.abs(currentRatio - 16/9) < 0.1) {
154
- if (finalWidth > finalHeight * 16/9) {
155
- finalHeight = finalWidth / (16/9);
156
- } else {
157
- finalWidth = finalHeight * (16/9);
158
  }
159
- console.log(`调整为16:9比例`);
160
- }
161
- // 如果比例接近4:3,调整为标准4:3
162
- else if (Math.abs(currentRatio - 4/3) < 0.1) {
163
- if (finalWidth > finalHeight * 4/3) {
164
- finalHeight = finalWidth / (4/3);
165
- } else {
166
- finalWidth = finalHeight * (4/3);
 
 
 
 
 
167
  }
168
- console.log(`调整为4:3比例`);
169
- }
170
-
171
- // 确保尺寸为偶数(避免半像素问题)
172
- finalWidth = Math.ceil(finalWidth / 2) * 2;
173
- finalHeight = Math.ceil(finalHeight / 2) * 2;
174
-
175
- const result = { width: finalWidth, height: finalHeight };
176
- console.log(`根据元素边界计算最终尺寸: ${result.width}x${result.height}, 比例: ${(result.width/result.height).toFixed(3)}`);
177
-
178
- return result;
179
- }
180
-
181
- // 6. 如果没有元素,使用从GitHub数据分析得出的标准尺寸
182
- const result = { width: 1000, height: 562 }; // 基于GitHub仓库中观察到的实际数据
183
- console.log(`使用GitHub数据分析的默认尺寸: ${result.width}x${result.height} (1000x562, 比例≈1.78)`);
184
- return result;
185
- };
186
-
187
- const pptDimensions = calculatePptDimensions(slide);
188
-
189
- // 渲染幻灯片元素 - 使用原始像素值,完全保真
190
- const renderElements = (elements) => {
191
- if (!elements || elements.length === 0) return '';
192
-
193
- return elements.map(element => {
194
- const style = `
195
- position: absolute;
196
- left: ${element.left || 0}px;
197
- top: ${element.top || 0}px;
198
- width: ${element.width || 0}px;
199
- height: ${element.height || 0}px;
200
- transform: rotate(${element.rotate || 0}deg);
201
- z-index: ${element.zIndex || 1};
202
- `;
203
-
204
- switch (element.type) {
205
- case 'text':
206
- return `
207
- <div style="${style}
208
- font-size: ${element.fontSize || 14}px;
209
- font-family: ${element.fontName || 'Arial'};
210
- color: ${element.defaultColor || '#000'};
211
- font-weight: ${element.bold ? 'bold' : 'normal'};
212
- font-style: ${element.italic ? 'italic' : 'normal'};
213
- text-decoration: ${element.underline ? 'underline' : 'none'};
214
- text-align: ${element.align || 'left'};
215
- line-height: ${element.lineHeight || 1.2};
216
- padding: 10px;
217
- word-wrap: break-word;
218
- overflow: hidden;
219
- box-sizing: border-box;
220
- ">
221
- ${element.content || ''}
222
- </div>
223
- `;
224
- case 'image':
225
- return `
226
- <div style="${style}">
227
- <img src="${element.src}" alt="" style="width: 100%; height: 100%; object-fit: ${element.objectFit || 'cover'}; display: block;" />
228
- </div>
229
- `;
230
- case 'shape':
231
- const shapeStyle = element.fill ? `background-color: ${element.fill};` : '';
232
- const borderStyle = element.outline ? `border: ${element.outline.width || 1}px ${element.outline.style || 'solid'} ${element.outline.color || '#000'};` : '';
233
- return `
234
- <div style="${style} ${shapeStyle} ${borderStyle} box-sizing: border-box;"></div>
235
- `;
236
- default:
237
- return `<div style="${style}"></div>`;
238
- }
239
- }).join('');
240
- };
241
-
242
- return `
243
- <!DOCTYPE html>
244
- <html lang="zh-CN">
245
- <head>
246
- <meta charset="UTF-8">
247
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
248
- <title>${title} - 第${slideIndex + 1}页</title>
249
- <style>
250
- /* 完全重置所有默认样式 */
251
- *, *::before, *::after {
252
- margin: 0 !important;
253
- padding: 0 !important;
254
- border: 0 !important;
255
- outline: 0 !important;
256
- box-sizing: border-box !important;
257
- }
258
-
259
- /* HTML - 填满整个窗口,黑色背景 */
260
- html {
261
- width: 100vw !important;
262
- height: 100vh !important;
263
- background: #000000 !important;
264
- overflow: hidden !important;
265
- position: fixed !important;
266
- top: 0 !important;
267
- left: 0 !important;
268
- }
269
-
270
- /* Body - 填满整个窗口,黑色背景,居中显示PPT */
271
- body {
272
- width: 100vw !important;
273
- height: 100vh !important;
274
- background: #000000 !important;
275
- overflow: hidden !important;
276
- font-family: 'Microsoft YaHei', Arial, sans-serif !important;
277
- position: fixed !important;
278
- top: 0 !important;
279
- left: 0 !important;
280
- display: flex !important;
281
- align-items: center !important;
282
- justify-content: center !important;
283
- }
284
-
285
- /* PPT容器 - 使用PPT原始尺寸 */
286
- .slide-container {
287
- width: ${pptDimensions.width}px !important;
288
- height: ${pptDimensions.height}px !important;
289
- background-color: ${slide.background?.color || theme.backgroundColor || '#ffffff'} !important;
290
- position: relative !important;
291
- overflow: hidden !important;
292
- transform-origin: center center !important;
293
- /* 缩放将通过JavaScript动态设置 */
294
- }
295
-
296
- /* 背景图片处理 */
297
- ${slide.background?.type === 'image' ? `
298
- .slide-container::before {
299
- content: '';
300
- position: absolute !important;
301
- top: 0 !important;
302
- left: 0 !important;
303
- width: 100% !important;
304
- height: 100% !important;
305
- background-image: url('${slide.background.image}') !important;
306
- background-size: cover !important;
307
- background-position: center !important;
308
- background-repeat: no-repeat !important;
309
- z-index: 0 !important;
310
- }
311
- ` : ''}
312
-
313
- /* 隐藏所有滚动条 */
314
- ::-webkit-scrollbar {
315
- display: none !important;
316
- }
317
-
318
- html {
319
- scrollbar-width: none !important;
320
- -ms-overflow-style: none !important;
321
- }
322
-
323
- /* 禁用用户交互 */
324
- * {
325
- -webkit-user-select: none !important;
326
- -moz-user-select: none !important;
327
- -ms-user-select: none !important;
328
- user-select: none !important;
329
- -webkit-user-drag: none !important;
330
- -khtml-user-drag: none !important;
331
- -moz-user-drag: none !important;
332
- -o-user-drag: none !important;
333
- user-drag: none !important;
334
- }
335
-
336
- /* 禁用缩放 */
337
- body {
338
- zoom: 1 !important;
339
- -webkit-text-size-adjust: 100% !important;
340
- -ms-text-size-adjust: 100% !important;
341
- }
342
- </style>
343
- </head>
344
- <body>
345
- <div class="slide-container" id="slideContainer">
346
- ${renderElements(slide.elements)}
347
  </div>
348
-
349
- <script>
350
- // PPT尺寸信息
351
- window.PPT_DIMENSIONS = {
352
- width: ${pptDimensions.width},
353
- height: ${pptDimensions.height}
354
- };
355
-
356
- console.log('PPT页面初始化 - 原始尺寸整体缩放模式:', window.PPT_DIMENSIONS);
357
-
358
- // 计算整体缩放比例,让PPT适应窗口大小
359
- function calculateScale() {
360
- const windowWidth = window.innerWidth;
361
- const windowHeight = window.innerHeight;
362
- const pptWidth = ${pptDimensions.width};
363
- const pptHeight = ${pptDimensions.height};
364
-
365
- // 计算缩放比例,保持PPT长宽比
366
- const scaleX = windowWidth / pptWidth;
367
- const scaleY = windowHeight / pptHeight;
368
-
369
- // 使用较小的缩放比例,确保PPT完全可见(会有黑边)
370
- const scale = Math.min(scaleX, scaleY);
371
-
372
- return Math.min(scale, 1); // 最大不超过1(不放大)
373
- }
374
-
375
- // 应用缩放变换
376
- function scalePPTToFitWindow() {
377
- const container = document.getElementById('slideContainer');
378
- if (!container) return;
379
-
380
- const scale = calculateScale();
381
-
382
- // 应用缩放变换
383
- container.style.transform = \`scale(\${scale})\`;
384
-
385
- console.log(\`PPT整体缩放: \${scale.toFixed(3)}x (窗口: \${window.innerWidth}x\${window.innerHeight}, PPT: \${${pptDimensions.width}}x\${${pptDimensions.height}})\`);
386
- }
387
-
388
- // 页面加载完成后初始化
389
- window.addEventListener('load', function() {
390
- const html = document.documentElement;
391
- const body = document.body;
392
- const container = document.getElementById('slideContainer');
393
-
394
- console.log('页面加载完成,开始整体缩放布局');
395
-
396
- // 确保页面元素填满窗口
397
- html.style.width = '100vw';
398
- html.style.height = '100vh';
399
- html.style.background = '#000000';
400
- html.style.overflow = 'hidden';
401
- html.style.margin = '0';
402
- html.style.padding = '0';
403
-
404
- body.style.width = '100vw';
405
- body.style.height = '100vh';
406
- body.style.background = '#000000';
407
- body.style.overflow = 'hidden';
408
- body.style.margin = '0';
409
- body.style.padding = '0';
410
- body.style.display = 'flex';
411
- body.style.alignItems = 'center';
412
- body.style.justifyContent = 'center';
413
-
414
- // 确保PPT容器使用原始尺寸
415
- if (container) {
416
- container.style.width = '${pptDimensions.width}px';
417
- container.style.height = '${pptDimensions.height}px';
418
- container.style.transformOrigin = 'center center';
419
- }
420
-
421
- // 执行整体缩放
422
- scalePPTToFitWindow();
423
-
424
- // 禁用各种用户交互
425
- document.addEventListener('wheel', function(e) {
426
- if (e.ctrlKey) e.preventDefault();
427
- }, { passive: false });
428
-
429
- document.addEventListener('keydown', function(e) {
430
- if ((e.ctrlKey && (e.key === '+' || e.key === '-' || e.key === '0')) || e.key === 'F11') {
431
- e.preventDefault();
432
- }
433
- }, false);
434
-
435
- document.addEventListener('contextmenu', function(e) {
436
- e.preventDefault();
437
- }, false);
438
-
439
- // 禁用触摸缩放
440
- let lastTouchEnd = 0;
441
- document.addEventListener('touchend', function(e) {
442
- const now = new Date().getTime();
443
- if (now - lastTouchEnd <= 300) {
444
- e.preventDefault();
445
- }
446
- lastTouchEnd = now;
447
- }, false);
448
-
449
- document.addEventListener('touchstart', function(e) {
450
- if (e.touches.length > 1) {
451
- e.preventDefault();
452
- }
453
- }, { passive: false });
454
-
455
- console.log('原始尺寸PPT页面初始化完成');
456
-
457
- // 验证最终效果
458
- setTimeout(() => {
459
- const actualScale = container.style.transform.match(/scale\\(([^)]+)\\)/);
460
- const scaleValue = actualScale ? parseFloat(actualScale[1]) : 1;
461
-
462
- console.log(\`✅ PPT以原始尺寸(\${${pptDimensions.width}}x\${${pptDimensions.height}})显示,整体缩放: \${scaleValue.toFixed(3)}x\`);
463
- console.log('🎯 显示效果与编辑器完全一致,周围黑边正常');
464
- }, 200);
465
- });
466
-
467
- // 监听窗口大小变化,实时调整缩放
468
- window.addEventListener('resize', function() {
469
- console.log('窗口大小变化,重新计算整体缩放');
470
- scalePPTToFitWindow();
471
- });
472
-
473
- // 监听屏幕方向变化(移动设备)
474
- window.addEventListener('orientationchange', function() {
475
- console.log('屏幕方向变化,重新计算整体缩放');
476
- setTimeout(scalePPTToFitWindow, 100);
477
- });
478
- </script>
479
- </body>
480
- </html>
481
  `;
482
- };
483
 
484
- // 公开访问PPT页面 - 返回HTML页面
485
  router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
486
  try {
487
  const { userId, pptId, slideIndex = 0 } = req.params;
488
  const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex);
489
  const fileName = `${pptId}.json`;
490
- const storageService = getStorageService();
491
 
492
  let pptData = null;
493
 
494
- // 如果是GitHub服务,尝试所有仓库
495
- if (storageService === githubService && storageService.repositories) {
496
- for (let i = 0; i < storageService.repositories.length; i++) {
497
- try {
498
- const result = await storageService.getFile(userId, fileName, i);
499
- if (result) {
500
- pptData = result.content;
501
- break;
502
- }
503
- } catch (error) {
504
- continue;
505
  }
506
- }
507
- } else {
508
- // 内存存储服务
509
- const result = await storageService.getFile(userId, fileName);
510
- if (result) {
511
- pptData = result.content;
512
  }
513
  }
514
 
515
  if (!pptData) {
516
- return res.status(404).send(`
517
- <!DOCTYPE html>
518
- <html lang="zh-CN">
519
- <head>
520
- <meta charset="UTF-8">
521
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
522
- <title>PPT未找到</title>
523
- <style>
524
- body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
525
- .error { color: #e74c3c; font-size: 18px; }
526
- </style>
527
- </head>
528
- <body>
529
- <div class="error">
530
- <h2>PPT未找到</h2>
531
- <p>请检查链接是否正确</p>
532
- </div>
533
- </body>
534
- </html>
535
- `);
536
  }
537
 
538
  const slideIdx = querySlideIndex;
539
  if (slideIdx >= pptData.slides.length || slideIdx < 0) {
540
- return res.status(404).send(`
541
- <!DOCTYPE html>
542
- <html lang="zh-CN">
543
- <head>
544
- <meta charset="UTF-8">
545
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
546
- <title>幻灯片未找到</title>
547
- <style>
548
- body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
549
- .error { color: #e74c3c; font-size: 18px; }
550
- </style>
551
- </head>
552
- <body>
553
- <div class="error">
554
- <h2>幻灯片未找到</h2>
555
- <p>请检查幻灯片索引是否正确</p>
556
- </div>
557
- </body>
558
- </html>
559
- `);
560
  }
561
 
562
- // 返回HTML页面
563
- const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title);
564
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
565
  res.send(htmlContent);
566
  } catch (error) {
@@ -568,33 +110,24 @@ router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
568
  }
569
  });
570
 
571
- // API接口:获取PPT数据和指定幻灯片(JSON格式)
572
  router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
573
  try {
574
  const { userId, pptId, slideIndex = 0 } = req.params;
575
  const fileName = `${pptId}.json`;
576
- const storageService = getStorageService();
577
 
578
  let pptData = null;
579
 
580
- // 如果是GitHub服务,尝试所有仓库
581
- if (storageService === githubService && storageService.repositories) {
582
- for (let i = 0; i < storageService.repositories.length; i++) {
583
- try {
584
- const result = await storageService.getFile(userId, fileName, i);
585
- if (result) {
586
- pptData = result.content;
587
- break;
588
- }
589
- } catch (error) {
590
- continue;
591
  }
592
- }
593
- } else {
594
- // 内存存储服务
595
- const result = await storageService.getFile(userId, fileName);
596
- if (result) {
597
- pptData = result.content;
598
  }
599
  }
600
 
@@ -607,7 +140,7 @@ router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
607
  return res.status(404).json({ error: 'Slide not found' });
608
  }
609
 
610
- // 返回PPT数据和指定幻灯片
611
  res.json({
612
  id: pptData.id,
613
  title: pptData.title,
@@ -622,33 +155,24 @@ router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
622
  }
623
  });
624
 
625
- // 获取完整PPT数据(只读模式)
626
  router.get('/ppt/:userId/:pptId', async (req, res, next) => {
627
  try {
628
  const { userId, pptId } = req.params;
629
  const fileName = `${pptId}.json`;
630
- const storageService = getStorageService();
631
 
632
  let pptData = null;
633
 
634
- // 如果是GitHub服务,尝试所有仓库
635
- if (storageService === githubService && storageService.repositories) {
636
- for (let i = 0; i < storageService.repositories.length; i++) {
637
- try {
638
- const result = await storageService.getFile(userId, fileName, i);
639
- if (result) {
640
- pptData = result.content;
641
- break;
642
- }
643
- } catch (error) {
644
- continue;
645
  }
646
- }
647
- } else {
648
- // 内存存储服务
649
- const result = await storageService.getFile(userId, fileName);
650
- if (result) {
651
- pptData = result.content;
652
  }
653
  }
654
 
@@ -656,7 +180,7 @@ router.get('/ppt/:userId/:pptId', async (req, res, next) => {
656
  return res.status(404).json({ error: 'PPT not found' });
657
  }
658
 
659
- // 返回只读版本的PPT数据
660
  res.json({
661
  ...pptData,
662
  isPublicView: true,
@@ -667,7 +191,7 @@ router.get('/ppt/:userId/:pptId', async (req, res, next) => {
667
  }
668
  });
669
 
670
- // 生成PPT分享链接
671
  router.post('/generate-share-link', async (req, res, next) => {
672
  try {
673
  const { userId, pptId, slideIndex = 0 } = req.body;
@@ -678,64 +202,26 @@ router.post('/generate-share-link', async (req, res, next) => {
678
 
679
  console.log(`Generating share link for PPT: ${pptId}, User: ${userId}`);
680
 
681
- // 验证PPT是否存在
682
  const fileName = `${pptId}.json`;
683
- const storageService = getStorageService();
684
  let pptExists = false;
685
  let pptData = null;
686
 
687
- console.log(`Using storage service: ${storageService === githubService ? 'GitHub' : 'Memory'}`);
688
 
689
- // 如果是GitHub服务,尝试所有仓库
690
- if (storageService === githubService && storageService.repositories) {
691
- console.log(`Checking ${storageService.repositories.length} GitHub repositories...`);
692
-
693
- for (let i = 0; i < storageService.repositories.length; i++) {
694
- try {
695
- console.log(`Checking repository ${i}: ${storageService.repositories[i]}`);
696
- const result = await storageService.getFile(userId, fileName, i);
697
- if (result) {
698
- pptExists = true;
699
- pptData = result.content;
700
- console.log(`PPT found in repository ${i}`);
701
- break;
702
- }
703
- } catch (error) {
704
- console.log(`PPT not found in repository ${i}: ${error.message}`);
705
- continue;
706
- }
707
- }
708
- } else {
709
- // 内存存储服务
710
- console.log('Checking memory storage...');
711
  try {
712
- const result = await storageService.getFile(userId, fileName);
 
713
  if (result) {
714
  pptExists = true;
715
  pptData = result.content;
716
- console.log('PPT found in memory storage');
 
717
  }
718
  } catch (error) {
719
- console.log(`PPT not found in memory storage: ${error.message}`);
720
- }
721
- }
722
-
723
- if (!pptExists) {
724
- console.log('PPT not found in any storage location');
725
-
726
- // 额外尝试:如果GitHub失败,检查memory storage作为fallback
727
- if (storageService === githubService) {
728
- console.log('GitHub lookup failed, trying memory storage fallback...');
729
- try {
730
- const memoryResult = await memoryStorageService.getFile(userId, fileName);
731
- if (memoryResult) {
732
- pptExists = true;
733
- pptData = memoryResult.content;
734
- console.log('PPT found in memory storage fallback');
735
- }
736
- } catch (memoryError) {
737
- console.log(`Memory storage fallback also failed: ${memoryError.message}`);
738
- }
739
  }
740
  }
741
 
@@ -743,7 +229,7 @@ router.post('/generate-share-link', async (req, res, next) => {
743
  return res.status(404).json({
744
  error: 'PPT not found',
745
  details: `PPT ${pptId} not found for user ${userId}`,
746
- searchedLocations: storageService === githubService ? 'GitHub repositories and memory storage' : 'Memory storage only'
747
  });
748
  }
749
 
@@ -751,15 +237,15 @@ router.post('/generate-share-link', async (req, res, next) => {
751
  const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol;
752
 
753
  const shareLinks = {
754
- // 单页分享链接
755
  slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
756
- // 完整PPT分享链接
757
  pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
758
- // 前端查看链接
759
  viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
760
- // 新增:截图链接
761
  screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`,
762
- // 添加PPT信息
763
  pptInfo: {
764
  id: pptId,
765
  title: pptData?.title || 'Unknown Title',
@@ -770,134 +256,253 @@ router.post('/generate-share-link', async (req, res, next) => {
770
  console.log('Share links generated successfully:', shareLinks);
771
  res.json(shareLinks);
772
  } catch (error) {
773
- console.error('Share link generation error:', error);
774
  next(error);
775
  }
776
  });
777
 
778
- // 截图功能 - 返回JPEG图片
779
  router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => {
780
  try {
781
- console.log('📸 Screenshot request received:', req.params);
782
-
783
  const { userId, pptId, slideIndex = 0 } = req.params;
784
- const slideIdx = parseInt(slideIndex);
 
 
 
 
785
  const fileName = `${pptId}.json`;
786
- const storageService = getStorageService();
787
-
788
- console.log(`🎯 Generating screenshot for: ${userId}/${pptId}/${slideIdx}`);
789
-
790
  let pptData = null;
791
 
792
- // 获取PPT数据(复用现有逻辑)
793
- if (storageService === githubService && storageService.repositories) {
794
- console.log('📂 Checking GitHub repositories...');
795
- for (let i = 0; i < storageService.repositories.length; i++) {
796
- try {
797
- const result = await storageService.getFile(userId, fileName, i);
798
- if (result) {
799
- pptData = result.content;
800
- console.log(`✅ PPT data found in repository ${i}`);
801
- break;
802
- }
803
- } catch (error) {
804
- console.log(`❌ Repository ${i} check failed:`, error.message);
805
- continue;
806
- }
807
- }
808
- } else {
809
- console.log('📂 Checking memory storage...');
810
  try {
811
- const result = await storageService.getFile(userId, fileName);
812
  if (result) {
813
  pptData = result.content;
814
- console.log('✅ PPT data found in memory storage');
815
  }
816
  } catch (error) {
817
- console.log('❌ Memory storage check failed:', error.message);
818
  }
819
  }
820
 
821
- // 如果GitHub失败,尝试内存存储fallback
822
- if (!pptData && storageService === githubService) {
823
- console.log('🔄 Trying memory storage fallback...');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
  try {
825
- const memoryResult = await memoryStorageService.getFile(userId, fileName);
826
- if (memoryResult) {
827
- pptData = memoryResult.content;
828
- console.log('✅ PPT data found in memory storage fallback');
829
  }
830
- } catch (memoryError) {
831
- console.log('❌ Memory storage fallback failed:', memoryError.message);
832
  }
833
  }
834
 
835
  if (!pptData) {
836
- console.log(' PPT not found anywhere');
837
-
838
- // 生成"PPT未找到"的错误图片
839
- const errorImage = screenshotService.generateFallbackImage(960, 720, 'PPT未找到');
840
- res.setHeader('Content-Type', 'image/svg+xml');
841
- res.setHeader('Cache-Control', 'no-cache');
842
- return res.send(errorImage);
843
  }
844
 
 
845
  if (slideIdx >= pptData.slides.length || slideIdx < 0) {
846
- console.log(`❌ Slide index out of bounds: ${slideIdx}/${pptData.slides.length}`);
847
-
848
- // 生成"幻灯片不存在"的错误图片
849
- const errorImage = screenshotService.generateFallbackImage(960, 720, '幻灯片不存在');
850
- res.setHeader('Content-Type', 'image/svg+xml');
851
- res.setHeader('Cache-Control', 'no-cache');
852
- return res.send(errorImage);
853
  }
854
 
855
- console.log('📝 Generating HTML content...');
856
- // 生成HTML内容(复用现有函数)
857
- const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title);
858
-
859
- console.log('🎯 Calling screenshot service...');
860
- // 生成截图
861
- const screenshot = await screenshotService.generateScreenshot(htmlContent, {
862
- format: 'jpeg',
863
- quality: 90
864
  });
865
 
866
- console.log(`✅ Screenshot generated successfully, size: ${screenshot.length} bytes`);
 
 
 
867
 
868
- // 检查是否是SVG fallback(通过内容检测)
869
- const isSvgFallback = screenshot.toString().includes('<svg');
 
 
 
 
 
 
 
 
 
 
870
 
871
- if (isSvgFallback) {
872
- console.log('📋 Returning SVG fallback image');
873
- res.setHeader('Content-Type', 'image/svg+xml');
874
- res.setHeader('Cache-Control', 'no-cache');
875
- res.setHeader('Content-Disposition', `inline; filename="${pptData.title}-${slideIdx + 1}-fallback.svg"`);
876
- } else {
877
- console.log('📸 Returning JPEG screenshot');
878
- res.setHeader('Content-Type', 'image/jpeg');
879
- res.setHeader('Cache-Control', 'public, max-age=300'); // 5分钟缓存
880
- res.setHeader('Content-Disposition', `inline; filename="${pptData.title}-${slideIdx + 1}.jpg"`);
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
882
 
883
- res.send(screenshot);
 
884
 
885
  } catch (error) {
886
- console.error('❌ Screenshot route error:', error);
887
- console.error('Stack trace:', error.stack);
888
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  try {
890
- // 生成错误截图
891
- const errorImage = screenshotService.generateFallbackImage(960, 720, '截图生成失败');
892
- res.setHeader('Content-Type', 'image/svg+xml');
893
- res.setHeader('Cache-Control', 'no-cache');
894
- res.setHeader('Content-Disposition', 'inline; filename="error.svg"');
895
- res.send(errorImage);
896
- } catch (responseError) {
897
- console.error('❌ Error sending error response:', responseError);
898
- // 如果连错误响应都发送失败,调用next
899
- next(error);
900
  }
 
 
 
 
 
 
 
 
901
  }
902
  });
903
 
 
1
  import express from 'express';
2
  import githubService from '../services/githubService.js';
 
3
  import screenshotService from '../services/screenshotService.js';
4
+ // 修正导入路径:从 backend/src 向上两级到 app,再进入 shared
5
+ import { generateSlideHTML, generateExportPage } from '../../../shared/export-utils.js';
6
 
7
  const router = express.Router();
8
 
9
+ // Generate error page for frontend display
10
+ function generateErrorPage(title, message) {
11
+ return `
12
+ <!DOCTYPE html>
13
+ <html lang="zh-CN">
14
+ <head>
15
+ <meta charset="UTF-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
+ <title>${title} - PPT导出</title>
18
+ <style>
19
+ body {
20
+ font-family: 'Microsoft YaHei', sans-serif;
21
+ margin: 0;
22
+ padding: 0;
23
+ background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
24
+ min-height: 100vh;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
+ .error-container {
30
+ background: white;
31
+ padding: 40px;
32
+ border-radius: 15px;
33
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
34
+ text-align: center;
35
+ max-width: 500px;
36
+ margin: 20px;
37
  }
38
+ .error-icon { font-size: 64px; margin-bottom: 20px; }
39
+ .error-title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
40
+ .error-message { font-size: 16px; color: #666; line-height: 1.6; margin-bottom: 30px; }
41
+ .error-actions { display: flex; gap: 15px; justify-content: center; }
42
+ .btn {
43
+ padding: 12px 24px;
44
+ border: none;
45
+ border-radius: 5px;
46
+ font-size: 16px;
47
+ cursor: pointer;
48
+ text-decoration: none;
49
+ display: inline-block;
50
+ transition: all 0.3s;
51
  }
52
+ .btn-primary { background: #4CAF50; color: white; }
53
+ .btn-primary:hover { background: #45a049; }
54
+ .btn-secondary { background: #f8f9fa; color: #333; border: 1px solid #ddd; }
55
+ .btn-secondary:hover { background: #e9ecef; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <div class="error-container">
60
+ <div class="error-icon">❌</div>
61
+ <div class="error-title">${title}</div>
62
+ <div class="error-message">${message}</div>
63
+ <div class="error-actions">
64
+ <button class="btn btn-secondary" onclick="history.back()">返回上页</button>
65
+ <a href="/" class="btn btn-primary">回到首页</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
+ </div>
68
+ </body>
69
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  `;
71
+ }
72
 
73
+ // Publicly access PPT page - return HTML page
74
  router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
75
  try {
76
  const { userId, pptId, slideIndex = 0 } = req.params;
77
  const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex);
78
  const fileName = `${pptId}.json`;
 
79
 
80
  let pptData = null;
81
 
82
+ // Try all GitHub repositories
83
+ for (let i = 0; i < githubService.repositories.length; i++) {
84
+ try {
85
+ const result = await githubService.getFile(userId, fileName, i);
86
+ if (result) {
87
+ pptData = result.content;
88
+ break;
 
 
 
 
89
  }
90
+ } catch (error) {
91
+ continue;
 
 
 
 
92
  }
93
  }
94
 
95
  if (!pptData) {
96
+ return res.status(404).send(generateErrorPage('PPT Not Found', 'Please check if the link is correct'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  }
98
 
99
  const slideIdx = querySlideIndex;
100
  if (slideIdx >= pptData.slides.length || slideIdx < 0) {
101
+ return res.status(404).send(generateErrorPage('Slide Not Found', 'Please check if the slide index is correct'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
 
104
+ // 使用共享模块生成HTML
105
+ const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'view' });
106
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
107
  res.send(htmlContent);
108
  } catch (error) {
 
110
  }
111
  });
112
 
113
+ // API endpoint: Get PPT data and specified slide (JSON format)
114
  router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
115
  try {
116
  const { userId, pptId, slideIndex = 0 } = req.params;
117
  const fileName = `${pptId}.json`;
 
118
 
119
  let pptData = null;
120
 
121
+ // Try all GitHub repositories
122
+ for (let i = 0; i < githubService.repositories.length; i++) {
123
+ try {
124
+ const result = await githubService.getFile(userId, fileName, i);
125
+ if (result) {
126
+ pptData = result.content;
127
+ break;
 
 
 
 
128
  }
129
+ } catch (error) {
130
+ continue;
 
 
 
 
131
  }
132
  }
133
 
 
140
  return res.status(404).json({ error: 'Slide not found' });
141
  }
142
 
143
+ // Return PPT data and specified slide
144
  res.json({
145
  id: pptData.id,
146
  title: pptData.title,
 
155
  }
156
  });
157
 
158
+ // Get complete PPT data (read-only mode)
159
  router.get('/ppt/:userId/:pptId', async (req, res, next) => {
160
  try {
161
  const { userId, pptId } = req.params;
162
  const fileName = `${pptId}.json`;
 
163
 
164
  let pptData = null;
165
 
166
+ // Try all GitHub repositories
167
+ for (let i = 0; i < githubService.repositories.length; i++) {
168
+ try {
169
+ const result = await githubService.getFile(userId, fileName, i);
170
+ if (result) {
171
+ pptData = result.content;
172
+ break;
 
 
 
 
173
  }
174
+ } catch (error) {
175
+ continue;
 
 
 
 
176
  }
177
  }
178
 
 
180
  return res.status(404).json({ error: 'PPT not found' });
181
  }
182
 
183
+ // Return read-only version of PPT data
184
  res.json({
185
  ...pptData,
186
  isPublicView: true,
 
191
  }
192
  });
193
 
194
+ // Generate PPT share link
195
  router.post('/generate-share-link', async (req, res, next) => {
196
  try {
197
  const { userId, pptId, slideIndex = 0 } = req.body;
 
202
 
203
  console.log(`Generating share link for PPT: ${pptId}, User: ${userId}`);
204
 
205
+ // Verify if PPT exists
206
  const fileName = `${pptId}.json`;
 
207
  let pptExists = false;
208
  let pptData = null;
209
 
210
+ console.log(`Checking ${githubService.repositories.length} GitHub repositories...`);
211
 
212
+ for (let i = 0; i < githubService.repositories.length; i++) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  try {
214
+ console.log(`Checking repository ${i}: ${githubService.repositories[i]}`);
215
+ const result = await githubService.getFile(userId, fileName, i);
216
  if (result) {
217
  pptExists = true;
218
  pptData = result.content;
219
+ console.log(`PPT found in repository ${i}`);
220
+ break;
221
  }
222
  } catch (error) {
223
+ console.log(`PPT not found in repository ${i}: ${error.message}`);
224
+ continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  }
226
  }
227
 
 
229
  return res.status(404).json({
230
  error: 'PPT not found',
231
  details: `PPT ${pptId} not found for user ${userId}`,
232
+ searchedLocations: 'GitHub repositories'
233
  });
234
  }
235
 
 
237
  const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol;
238
 
239
  const shareLinks = {
240
+ // Single page share link
241
  slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
242
+ // Complete PPT share link
243
  pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
244
+ // Frontend view link
245
  viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
246
+ // Screenshot link
247
  screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`,
248
+ // Add PPT information
249
  pptInfo: {
250
  id: pptId,
251
  title: pptData?.title || 'Unknown Title',
 
256
  console.log('Share links generated successfully:', shareLinks);
257
  res.json(shareLinks);
258
  } catch (error) {
 
259
  next(error);
260
  }
261
  });
262
 
263
+ // Screenshot endpoint - Frontend Export Strategy
264
  router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => {
265
  try {
 
 
266
  const { userId, pptId, slideIndex = 0 } = req.params;
267
+ const { format = 'jpeg', quality = 90, strategy = 'frontend-first', returnHtml = 'true' } = req.query;
268
+
269
+ console.log(`Screenshot request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, strategy=${strategy}, returnHtml=${returnHtml}`);
270
+
271
+ // Get PPT data
272
  const fileName = `${pptId}.json`;
 
 
 
 
273
  let pptData = null;
274
 
275
+ // Try all GitHub repositories
276
+ for (let i = 0; i < githubService.repositories.length; i++) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  try {
278
+ const result = await githubService.getFile(userId, fileName, i);
279
  if (result) {
280
  pptData = result.content;
281
+ break;
282
  }
283
  } catch (error) {
284
+ continue;
285
  }
286
  }
287
 
288
+ if (!pptData) {
289
+ return res.status(404).json({ error: 'PPT not found' });
290
+ }
291
+
292
+ const slideIdx = parseInt(slideIndex);
293
+ if (slideIdx >= pptData.slides.length || slideIdx < 0) {
294
+ return res.status(404).json({ error: 'Invalid slide index' });
295
+ }
296
+
297
+ // Frontend Export Strategy - Return HTML page for client-side screenshot generation
298
+ if (strategy === 'frontend-first' && returnHtml !== 'false') {
299
+ console.log('Using frontend export strategy - returning HTML page');
300
+
301
+ // 使用共享模块生成导出页面
302
+ const htmlPage = generateExportPage(pptData, slideIdx, {
303
+ format,
304
+ quality: parseInt(quality),
305
+ autoDownload: req.query.autoDownload === 'true'
306
+ });
307
+
308
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
309
+ res.setHeader('X-Screenshot-Strategy', 'frontend-export');
310
+ res.setHeader('X-Generation-Time', '< 100ms');
311
+ return res.send(htmlPage);
312
+ }
313
+
314
+ // Fallback: Backend screenshot (if specifically requested)
315
+ console.log('Using backend screenshot for direct image return...');
316
+
317
+ // 使用共享模块生成HTML用于后端截图
318
+ const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'screenshot' });
319
+
320
+ try {
321
+ const screenshot = await screenshotService.generateScreenshot(htmlContent, {
322
+ format,
323
+ quality: parseInt(quality),
324
+ width: pptData.viewportSize || 1000,
325
+ height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
326
+ });
327
+
328
+ res.setHeader('Content-Type', `image/${format}`);
329
+ res.setHeader('X-Screenshot-Type', 'backend-generated');
330
+ res.setHeader('X-Generation-Time', '2-5s');
331
+ res.send(screenshot);
332
+
333
+ } catch (error) {
334
+ console.error('Backend screenshot failed:', error);
335
+
336
+ // Generate fallback image
337
+ const fallbackImage = screenshotService.generateFallbackImage(
338
+ pptData.viewportSize || 1000,
339
+ Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
340
+ );
341
+
342
+ res.setHeader('Content-Type', `image/${format}`);
343
+ res.setHeader('X-Screenshot-Type', 'fallback-generated');
344
+ res.setHeader('X-Generation-Time', '< 50ms');
345
+ res.send(fallbackImage);
346
+ }
347
+
348
+ } catch (error) {
349
+ next(error);
350
+ }
351
+ });
352
+
353
+ // Image endpoint - Direct frontend export page
354
+ router.get('/image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
355
+ try {
356
+ const { userId, pptId, slideIndex = 0 } = req.params;
357
+ const { format = 'jpeg', quality = 90 } = req.query;
358
+
359
+ console.log(`Image export request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`);
360
+
361
+ // Get PPT data
362
+ const fileName = `${pptId}.json`;
363
+ let pptData = null;
364
+
365
+ for (let i = 0; i < githubService.repositories.length; i++) {
366
  try {
367
+ const result = await githubService.getFile(userId, fileName, i);
368
+ if (result) {
369
+ pptData = result.content;
370
+ break;
371
  }
372
+ } catch (error) {
373
+ continue;
374
  }
375
  }
376
 
377
  if (!pptData) {
378
+ const errorPage = generateErrorPage('PPT Not Found', `PPT ${pptId} not found for user ${userId}`);
379
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
380
+ return res.status(404).send(errorPage);
 
 
 
 
381
  }
382
 
383
+ const slideIdx = parseInt(slideIndex);
384
  if (slideIdx >= pptData.slides.length || slideIdx < 0) {
385
+ const errorPage = generateErrorPage('Invalid Slide', `Slide ${slideIndex} not found in PPT`);
386
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
387
+ return res.status(404).send(errorPage);
 
 
 
 
388
  }
389
 
390
+ // 使用共享模块生成导出页面
391
+ const htmlPage = generateExportPage(pptData, slideIdx, {
392
+ format,
393
+ quality: parseInt(quality),
394
+ autoDownload: true // Auto-generate and download
 
 
 
 
395
  });
396
 
397
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
398
+ res.setHeader('X-Screenshot-Strategy', 'frontend-export-page');
399
+ res.setHeader('X-Generation-Time', '< 50ms');
400
+ res.send(htmlPage);
401
 
402
+ } catch (error) {
403
+ next(error);
404
+ }
405
+ });
406
+
407
+ // Screenshot data endpoint - For frontend direct rendering
408
+ router.get('/screenshot-data/:userId/:pptId/:slideIndex?', async (req, res, next) => {
409
+ try {
410
+ const { userId, pptId, slideIndex = 0 } = req.params;
411
+ const { format = 'jpeg', quality = 90 } = req.query;
412
+
413
+ console.log(`Screenshot data request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`);
414
 
415
+ // Get PPT data
416
+ const fileName = `${pptId}.json`;
417
+ let pptData = null;
418
+
419
+ for (let i = 0; i < githubService.repositories.length; i++) {
420
+ try {
421
+ const result = await githubService.getFile(userId, fileName, i);
422
+ if (result) {
423
+ pptData = result.content;
424
+ break;
425
+ }
426
+ } catch (error) {
427
+ continue;
428
+ }
429
+ }
430
+
431
+ if (!pptData) {
432
+ return res.status(404).json({ error: 'PPT not found' });
433
+ }
434
+
435
+ const slideIdx = parseInt(slideIndex);
436
+ if (slideIdx >= pptData.slides.length || slideIdx < 0) {
437
+ return res.status(404).json({ error: 'Invalid slide index' });
438
  }
439
+
440
+ // Return PPT data for frontend rendering
441
+ const responseData = {
442
+ pptData: {
443
+ id: pptData.id,
444
+ title: pptData.title,
445
+ theme: pptData.theme,
446
+ viewportSize: pptData.viewportSize,
447
+ viewportRatio: pptData.viewportRatio,
448
+ slide: pptData.slides[slideIdx]
449
+ },
450
+ slideIndex: slideIdx,
451
+ totalSlides: pptData.slides.length,
452
+ exportConfig: {
453
+ format,
454
+ quality: parseInt(quality),
455
+ width: pptData.viewportSize || 1000,
456
+ height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
457
+ },
458
+ strategy: 'frontend-direct-rendering',
459
+ timestamp: new Date().toISOString()
460
+ };
461
 
462
+ res.setHeader('X-Screenshot-Strategy', 'frontend-data-api');
463
+ res.json(responseData);
464
 
465
  } catch (error) {
466
+ next(error);
467
+ }
468
+ });
469
+
470
+ // Screenshot health check
471
+ router.get('/screenshot-health', async (req, res, next) => {
472
+ try {
473
+ const status = {
474
+ status: 'healthy',
475
+ timestamp: new Date().toISOString(),
476
+ environment: {
477
+ nodeEnv: process.env.NODE_ENV,
478
+ isHuggingFace: process.env.SPACE_ID ? true : false,
479
+ platform: process.platform
480
+ },
481
+ screenshotService: {
482
+ available: true,
483
+ strategy: 'frontend-first',
484
+ backendFallback: false, // Disable backend browsers
485
+ screenshotSize: 0
486
+ }
487
+ };
488
+
489
+ // Test fallback image generation
490
  try {
491
+ const testImage = screenshotService.generateFallbackImage(100, 100);
492
+ status.screenshotService.screenshotSize = testImage.length;
493
+ status.screenshotService.fallbackAvailable = true;
494
+ } catch (error) {
495
+ status.screenshotService.fallbackAvailable = false;
496
+ status.screenshotService.fallbackError = error.message;
 
 
 
 
497
  }
498
+
499
+ res.json(status);
500
+ } catch (error) {
501
+ res.status(500).json({
502
+ status: 'error',
503
+ timestamp: new Date().toISOString(),
504
+ error: error.message
505
+ });
506
  }
507
  });
508
 
backend/src/services/githubService.js CHANGED
@@ -1,39 +1,205 @@
1
  import axios from 'axios';
2
- import { GITHUB_CONFIG } from '../config/users.js';
3
- import memoryStorageService from './memoryStorageService.js';
4
 
5
  class GitHubService {
6
  constructor() {
7
- this.apiUrl = GITHUB_CONFIG.apiUrl;
8
- this.token = GITHUB_CONFIG.token;
9
- this.repositories = GITHUB_CONFIG.repositories;
10
- this.useMemoryStorage = !this.token; // 如果没有token,使用内存存储
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  console.log('=== GitHub Service Configuration ===');
13
- console.log('Token configured:', !!this.token);
14
- console.log('Token preview:', this.token ? `${this.token.substring(0, 8)}...` : 'Not set');
15
- console.log('Repositories:', this.repositories);
16
- console.log('Using memory storage:', this.useMemoryStorage);
17
 
18
- if (!this.token) {
19
- console.warn('GitHub token not configured, using memory storage for development');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
 
 
 
 
 
 
 
21
  }
22
 
23
  // 验证GitHub连接
24
  async validateConnection() {
 
 
25
  if (this.useMemoryStorage) {
26
- return { valid: false, reason: 'No GitHub token configured' };
 
 
 
 
 
 
27
  }
28
 
29
  try {
30
  // 测试GitHub API连接
31
- const response = await axios.get(`${this.apiUrl}/user`, {
32
- headers: {
33
- 'Authorization': `token ${this.token}`,
34
- 'Accept': 'application/vnd.github.v3+json'
35
- }
36
- });
 
 
 
37
 
38
  console.log('GitHub API connection successful:', response.data.login);
39
 
@@ -44,12 +210,15 @@ class GitHubService {
44
  const { owner, repo } = this.parseRepoUrl(repoUrl);
45
 
46
  // 检查仓库基本信息
47
- const repoResponse = await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, {
48
- headers: {
49
- 'Authorization': `token ${this.token}`,
50
- 'Accept': 'application/vnd.github.v3+json'
51
- }
52
- });
 
 
 
53
 
54
  // 检查权限
55
  const permissions = repoResponse.data.permissions;
@@ -63,7 +232,8 @@ class GitHubService {
63
  headers: {
64
  'Authorization': `token ${this.token}`,
65
  'Accept': 'application/vnd.github.v3+json'
66
- }
 
67
  });
68
  canAccessContents = true;
69
  } catch (contentsError) {
@@ -108,403 +278,1140 @@ class GitHubService {
108
 
109
  // 初始化空仓库
110
  async initializeRepository(repoIndex = 0) {
 
 
111
  if (this.useMemoryStorage) {
112
- return { success: false, reason: 'Using memory storage' };
 
113
  }
114
 
 
 
 
115
  try {
116
- const repoUrl = this.repositories[repoIndex];
117
- const { owner, repo } = this.parseRepoUrl(repoUrl);
118
-
119
- console.log(`Initializing empty repository: ${owner}/${repo}`);
120
 
121
  // 创建初始README文件
122
- const readmeContent = Buffer.from(`# PPTist Data Repository
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- This repository stores PPT data for the PPTist application.
 
 
 
 
 
 
125
 
126
- ## Structure
 
 
 
 
127
 
128
- \`\`\`
129
- users/
130
- ├── PS01/
131
- │ ├── README.md
132
- │ └── *.json (PPT files)
133
- ├── PS02/
134
- │ ├── README.md
135
- │ └── *.json (PPT files)
136
- └── ...
137
- \`\`\`
 
 
 
 
 
138
 
139
- ## Auto-generated
 
 
 
 
 
 
 
 
140
 
141
- This repository is automatically managed by PPTist Huggingface Space.
142
- Do not manually edit files unless you know what you're doing.
143
- `).toString('base64');
 
 
 
 
144
 
145
- const response = await axios.put(
146
- `${this.apiUrl}/repos/${owner}/${repo}/contents/README.md`,
147
- {
148
- message: 'Initialize repository for PPTist data storage',
149
- content: readmeContent,
150
- branch: 'main'
151
- },
152
- {
153
- headers: {
154
- 'Authorization': `token ${this.token}`,
155
- 'Accept': 'application/vnd.github.v3+json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
- }
158
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- console.log(`Repository initialized successfully: ${response.data.commit.sha}`);
161
- return { success: true, commit: response.data.commit.sha };
162
  } catch (error) {
163
- console.error('Repository initialization failed:', error.response?.data || error.message);
164
- return { success: false, error: error.message };
 
 
 
 
165
  }
166
  }
167
 
168
- // 获取文件内容
169
- async getFile(userId, fileName, repoIndex = 0) {
170
- // 如果使用内存存储
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  if (this.useMemoryStorage) {
172
- return await memoryStorageService.getFile(userId, fileName);
173
  }
174
 
175
- // 原有的GitHub逻辑
176
  try {
 
 
177
  const repoUrl = this.repositories[repoIndex];
178
  const { owner, repo } = this.parseRepoUrl(repoUrl);
179
- const path = `users/${userId}/${fileName}`;
180
 
181
- const response = await axios.get(
182
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
183
- {
184
- headers: {
185
- 'Authorization': `token ${this.token}`,
186
- 'Accept': 'application/vnd.github.v3+json'
 
 
 
 
187
  }
188
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  );
190
 
191
- const content = Buffer.from(response.data.content, 'base64').toString('utf8');
192
- return {
193
- content: JSON.parse(content),
194
- sha: response.data.sha
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  };
 
 
 
 
 
 
 
 
 
196
  } catch (error) {
197
  if (error.response?.status === 404) {
 
198
  return null;
199
  }
200
- throw error;
 
201
  }
202
  }
203
 
204
- // 保存文件
205
- async saveFile(userId, fileName, data, repoIndex = 0) {
206
- // 如果使用内存存储
207
- if (this.useMemoryStorage) {
208
- return await memoryStorageService.saveFile(userId, fileName, data);
209
- }
210
-
211
- // 原有的GitHub逻辑
212
  const repoUrl = this.repositories[repoIndex];
213
  const { owner, repo } = this.parseRepoUrl(repoUrl);
214
- const path = `users/${userId}/${fileName}`;
215
-
216
- console.log(`Attempting to save file: ${path} to repo: ${owner}/${repo}`);
217
-
218
- // 先尝试获取现有文件的SHA
219
- let sha = null;
220
  try {
221
- const existing = await this.getFile(userId, fileName, repoIndex);
222
- if (existing) {
223
- sha = existing.sha;
224
- console.log(`Found existing file with SHA: ${sha}`);
225
- }
 
 
 
 
 
 
 
 
 
 
 
 
226
  } catch (error) {
227
- console.log(`No existing file found: ${error.message}`);
 
 
 
228
  }
 
229
 
230
- const content = Buffer.from(JSON.stringify(data, null, 2)).toString('base64');
231
-
232
- const payload = {
233
- message: `${sha ? 'Update' : 'Create'} ${fileName} for user ${userId}`,
234
- content: content,
235
- branch: 'main'
236
- };
237
-
238
- if (sha) {
239
- payload.sha = sha;
 
240
  }
 
 
 
 
 
241
 
242
- try {
243
- console.log(`Saving to GitHub: ${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`);
244
-
245
- const response = await axios.put(
246
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
247
- payload,
 
 
248
  {
249
  headers: {
250
  'Authorization': `token ${this.token}`,
251
  'Accept': 'application/vnd.github.v3+json'
252
- }
 
253
  }
254
  );
 
 
 
 
 
255
 
256
- console.log(`Successfully saved to GitHub: ${response.data.commit.sha}`);
257
- return response.data;
258
- } catch (error) {
259
- console.error(`GitHub save failed:`, error.response?.data || error.message);
 
 
 
260
 
261
- // 特殊处理空仓库的情况
262
- if (error.response?.status === 409 && error.response?.data?.message?.includes('empty')) {
263
- console.log('Repository is empty, initializing...');
264
- const initResult = await this.initializeRepository(repoIndex);
265
-
266
- if (initResult.success) {
267
- console.log('Repository initialized, retrying save...');
268
- // 等待一会儿让GitHub同步
269
- await new Promise(resolve => setTimeout(resolve, 2000));
270
-
271
- // 继续执行原来的目录创建逻辑
272
- return this.saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload);
273
- }
274
- }
275
 
276
- // 详细的错误处理
277
- if (error.response?.status === 404) {
278
- return this.saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload);
279
- } else if (error.response?.status === 403) {
280
- throw new Error(`GitHub permission denied. Check if the token has 'repo' permissions: ${error.response.data.message}`);
281
  } else {
282
- throw new Error(`GitHub API error (${error.response?.status}): ${error.response?.data?.message || error.message}`);
 
283
  }
 
 
 
 
 
 
284
  }
285
  }
286
 
287
- // 带目录创建的保存文件方法
288
- async saveFileWithDirectoryCreation(userId, fileName, data, repoIndex, payload) {
289
- const repoUrl = this.repositories[repoIndex];
290
- const { owner, repo } = this.parseRepoUrl(repoUrl);
291
- const path = `users/${userId}/${fileName}`;
292
 
293
- console.log('404 error - attempting to create directory structure...');
 
294
 
295
  try {
296
- // 方法1: 先检查仓库是否存在和可访问
297
- const repoCheckResponse = await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, {
298
- headers: {
299
- 'Authorization': `token ${this.token}`,
300
- 'Accept': 'application/vnd.github.v3+json'
301
- }
302
- });
303
- console.log(`Repository exists: ${repoCheckResponse.data.full_name}`);
304
 
305
- // 方法2: 检查users目录是否存在
306
- try {
307
- await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents/users`, {
308
- headers: {
309
- 'Authorization': `token ${this.token}`,
310
- 'Accept': 'application/vnd.github.v3+json'
311
- }
312
- });
313
- console.log('Users directory exists');
314
- } catch (usersDirError) {
315
- console.log('Users directory does not exist, creating...');
316
-
317
- // 创建users目录的README
318
- const usersReadmePath = 'users/README.md';
319
- const usersReadmeContent = Buffer.from('# Users Directory\n\nThis directory contains user-specific PPT files.\n').toString('base64');
320
-
321
- await axios.put(
322
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${usersReadmePath}`,
323
- {
324
- message: 'Create users directory',
325
- content: usersReadmeContent,
326
- branch: 'main'
327
- },
328
  {
329
  headers: {
330
  'Authorization': `token ${this.token}`,
331
  'Accept': 'application/vnd.github.v3+json'
332
- }
 
333
  }
334
  );
335
- console.log('Users directory created');
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
- // 方法3: 检查用户目录是否存在
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  try {
340
- await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents/users/${userId}`, {
341
- headers: {
342
- 'Authorization': `token ${this.token}`,
343
- 'Accept': 'application/vnd.github.v3+json'
344
- }
345
- });
346
- console.log(`User directory exists: users/${userId}`);
347
- } catch (userDirError) {
348
- console.log(`User directory does not exist, creating: users/${userId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- // 创建用户目录的README
351
- const userReadmePath = `users/${userId}/README.md`;
352
- const userReadmeContent = Buffer.from(`# PPT Files for User ${userId}\n\nThis directory contains PPT files for user ${userId}.\n`).toString('base64');
 
353
 
354
- await axios.put(
355
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${userReadmePath}`,
356
- {
357
- message: `Create user directory for ${userId}`,
358
- content: userReadmeContent,
359
- branch: 'main'
360
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  {
362
  headers: {
363
  'Authorization': `token ${this.token}`,
364
  'Accept': 'application/vnd.github.v3+json'
365
- }
 
366
  }
367
  );
368
- console.log(`User directory created: users/${userId}`);
 
 
369
  }
370
-
371
- // 等待一小会儿让GitHub同步
372
- await new Promise(resolve => setTimeout(resolve, 1000));
373
-
374
- // 重试保存PPT文件
375
- console.log('Retrying PPT file save...');
376
- const retryResponse = await axios.put(
377
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
378
- payload,
379
  {
380
  headers: {
381
  'Authorization': `token ${this.token}`,
382
  'Accept': 'application/vnd.github.v3+json'
383
- }
 
384
  }
385
  );
 
 
 
 
 
386
 
387
- console.log(`Successfully saved to GitHub after retry: ${retryResponse.data.commit.sha}`);
388
- return retryResponse.data;
 
 
 
 
 
 
389
 
390
- } catch (retryError) {
391
- console.error(`Comprehensive retry also failed:`, retryError.response?.data || retryError.message);
 
392
 
393
- // 如果GitHub彻底失败,fallback到内存存储
394
- console.log('GitHub save completely failed, falling back to memory storage...');
395
- try {
396
- const memoryResult = await memoryStorageService.saveFile(userId, fileName, data);
397
- console.log('Successfully saved to memory storage as fallback');
398
- return {
399
- ...memoryResult,
400
- warning: 'Saved to temporary memory storage due to GitHub issues'
401
- };
402
- } catch (memoryError) {
403
- console.error('Memory storage fallback also failed:', memoryError.message);
404
- throw new Error(`All storage methods failed. GitHub: ${retryError.message}, Memory: ${memoryError.message}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  }
406
  }
 
 
 
407
  }
408
 
409
- // 获取用户的所有PPT列表
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  async getUserPPTList(userId) {
411
- // 如果使用内存存储
 
412
  if (this.useMemoryStorage) {
413
- return await memoryStorageService.getUserPPTList(userId);
414
  }
415
 
416
- // 原有的GitHub逻辑
417
- const results = [];
418
 
419
- for (let i = 0; i < this.repositories.length; i++) {
 
420
  try {
421
- const repoUrl = this.repositories[i];
422
  const { owner, repo } = this.parseRepoUrl(repoUrl);
423
- const path = `users/${userId}`;
424
 
425
- const response = await axios.get(
426
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
427
- {
428
- headers: {
429
- 'Authorization': `token ${this.token}`,
430
- 'Accept': 'application/vnd.github.v3+json'
 
 
 
431
  }
432
- }
 
 
 
 
 
433
  );
434
 
435
- const files = response.data
436
- .filter(item => item.type === 'file' && item.name.endsWith('.json'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
- // 获取每个PPT文件的实际内容以读取标题
439
- for (const file of files) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  try {
441
  const pptId = file.name.replace('.json', '');
442
- const fileContent = await this.getFile(userId, file.name, i);
443
 
444
- if (fileContent && fileContent.content) {
445
- results.push({
446
- name: pptId,
447
- title: fileContent.content.title || '未命名演示文稿',
448
- lastModified: fileContent.content.updatedAt || fileContent.content.createdAt,
449
- repoIndex: i,
450
- repoUrl: repoUrl
451
- });
452
  }
453
- } catch (error) {
454
- // 如果读取单个文件失败,使用文件名作为标题
455
- console.warn(`Failed to read PPT content for ${file.name}:`, error.message);
456
- results.push({
457
- name: file.name.replace('.json', ''),
458
- title: file.name.replace('.json', ''),
459
- lastModified: new Date().toISOString(),
460
- repoIndex: i,
461
- repoUrl: repoUrl
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  });
 
 
463
  }
464
  }
465
  } catch (error) {
466
- if (error.response?.status !== 404) {
467
- console.error(`Error fetching files from repo ${i}:`, error.message);
468
- }
469
  }
470
  }
471
 
472
- return results;
 
473
  }
474
 
475
- // 删除文件
476
- async deleteFile(userId, fileName, repoIndex = 0) {
477
- // 如果使用内存存储
 
478
  if (this.useMemoryStorage) {
479
- return await memoryStorageService.deleteFile(userId, fileName);
480
- }
481
-
482
- // 原有的GitHub逻辑
483
- const existing = await this.getFile(userId, fileName, repoIndex);
484
- if (!existing) {
485
- throw new Error('File not found');
486
  }
487
 
488
  const repoUrl = this.repositories[repoIndex];
489
  const { owner, repo } = this.parseRepoUrl(repoUrl);
490
- const path = `users/${userId}/${fileName}`;
491
 
492
- const response = await axios.delete(
493
- `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
494
- {
495
- data: {
496
- message: `Delete ${fileName} for user ${userId}`,
497
- sha: existing.sha,
498
- branch: 'main'
499
- },
500
- headers: {
501
- 'Authorization': `token ${this.token}`,
502
- 'Accept': 'application/vnd.github.v3+json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  }
504
  }
505
- );
506
 
507
- return response.data;
 
 
 
 
 
 
 
 
 
 
 
508
  }
509
  }
510
 
 
1
  import axios from 'axios';
 
 
2
 
3
  class GitHubService {
4
  constructor() {
5
+ // 延迟初始化,动态读取环境变量
6
+ this.initialized = false;
7
+ this.useMemoryStorage = false;
8
+ this.memoryStorage = new Map();
9
+ this.apiUrl = 'https://api.github.com';
10
 
11
+ // 首次使用时再初始化
12
+ this.initPromise = null;
13
+
14
+ // 添加错误重试配置
15
+ this.retryConfig = {
16
+ maxRetries: 3,
17
+ retryDelay: 1000,
18
+ backoffMultiplier: 2
19
+ };
20
+
21
+ // API限制处理
22
+ this.apiRateLimit = {
23
+ requestQueue: [],
24
+ processing: false,
25
+ minDelay: 100 // 最小请求间隔
26
+ };
27
+ }
28
+
29
+ // 动态初始化方法
30
+ async initialize() {
31
+ if (this.initialized) {
32
+ return;
33
+ }
34
+
35
  console.log('=== GitHub Service Configuration ===');
 
 
 
 
36
 
37
+ try {
38
+ // 动态读取环境变量
39
+ this.token = process.env.GITHUB_TOKEN;
40
+ this.repositories = process.env.GITHUB_REPOS
41
+ ? process.env.GITHUB_REPOS.split(',').map(repo => repo.trim())
42
+ : [];
43
+
44
+ console.log('Token configured:', !!this.token);
45
+ console.log('Token preview:', this.token ? `${this.token.substring(0, 8)}...` : 'Not set');
46
+ console.log('Repositories:', this.repositories);
47
+ console.log('Repositories length:', this.repositories?.length || 0);
48
+
49
+ // Check if token is a placeholder
50
+ if (!this.token || this.token === 'your_github_token_here') {
51
+ console.warn('❌ GitHub token is missing or using placeholder value!');
52
+ console.warn('⚠️ Switching to memory storage mode for development...');
53
+ this.useMemoryStorage = true;
54
+ console.log('✅ Memory storage mode activated');
55
+ this.initialized = true;
56
+ return;
57
+ }
58
+
59
+ if (!this.repositories || this.repositories.length === 0) {
60
+ console.warn('❌ No GitHub repositories configured!');
61
+ console.warn('⚠️ Switching to memory storage mode...');
62
+ this.useMemoryStorage = true;
63
+ console.log('✅ Memory storage mode activated');
64
+ this.initialized = true;
65
+ return;
66
+ }
67
+
68
+ // Check for placeholder repository URLs
69
+ const validRepos = this.repositories.filter(repo => {
70
+ const isPlaceholder = repo.includes('your-username') || repo.includes('placeholder');
71
+ if (isPlaceholder) {
72
+ console.warn(`⚠️ Skipping placeholder repository: ${repo}`);
73
+ return false;
74
+ }
75
+ return this.isValidRepoUrl(repo);
76
+ });
77
+
78
+ if (validRepos.length === 0) {
79
+ console.warn('❌ No valid GitHub repositories found (all are placeholders)!');
80
+ console.warn('⚠️ Switching to memory storage mode...');
81
+ this.useMemoryStorage = true;
82
+ console.log('✅ Memory storage mode activated');
83
+ this.initialized = true;
84
+ return;
85
+ }
86
+
87
+ this.repositories = validRepos;
88
+
89
+ // 验证每个仓库URL的格式
90
+ this.repositories.forEach((repo, index) => {
91
+ if (!this.isValidRepoUrl(repo)) {
92
+ console.error(`❌ Invalid repository URL at index ${index}: ${repo}`);
93
+ throw new Error(`Invalid repository URL: ${repo}. Expected format: https://github.com/owner/repo`);
94
+ }
95
+ });
96
+
97
+ console.log('✅ GitHub Service initialized successfully');
98
+ this.initialized = true;
99
+ } catch (error) {
100
+ console.error('❌ GitHub Service initialization failed:', error);
101
+ console.warn('⚠️ Falling back to memory storage mode...');
102
+ this.useMemoryStorage = true;
103
+ this.initialized = true;
104
+ }
105
+ }
106
+
107
+ // 确保在所有方法中先初始化
108
+ async ensureInitialized() {
109
+ if (!this.initPromise) {
110
+ this.initPromise = this.initialize();
111
+ }
112
+ await this.initPromise;
113
+ }
114
+
115
+ // 验证仓库URL格式
116
+ isValidRepoUrl(repoUrl) {
117
+ const githubUrlPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
118
+ return githubUrlPattern.test(repoUrl.trim());
119
+ }
120
+
121
+ // 带重试的API请求方法
122
+ async makeGitHubRequest(requestFn, operation = 'GitHub API request', maxRetries = null) {
123
+ const retries = maxRetries || this.retryConfig.maxRetries;
124
+ let lastError = null;
125
+
126
+ for (let attempt = 0; attempt <= retries; attempt++) {
127
+ try {
128
+ if (attempt > 0) {
129
+ const delay = this.retryConfig.retryDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1);
130
+ console.log(`🔄 Retrying ${operation} (attempt ${attempt + 1}/${retries + 1}) after ${delay}ms...`);
131
+ await new Promise(resolve => setTimeout(resolve, delay));
132
+ }
133
+
134
+ const result = await requestFn();
135
+
136
+ if (attempt > 0) {
137
+ console.log(`✅ ${operation} succeeded on retry attempt ${attempt + 1}`);
138
+ }
139
+
140
+ return result;
141
+ } catch (error) {
142
+ lastError = error;
143
+
144
+ // 判断是否应该重试
145
+ if (!this.shouldRetry(error) || attempt === retries) {
146
+ break;
147
+ }
148
+
149
+ console.warn(`⚠️ ${operation} failed (attempt ${attempt + 1}):`, error.message);
150
+ }
151
+ }
152
+
153
+ console.error(`❌ ${operation} failed after ${retries + 1} attempts:`, lastError.message);
154
+ throw lastError;
155
+ }
156
+
157
+ // 判断错误是否应该重试
158
+ shouldRetry(error) {
159
+ if (!error.response) {
160
+ return true; // 网络错误,重试
161
+ }
162
+
163
+ const status = error.response.status;
164
+
165
+ // 这些状态码不应该重试
166
+ if ([400, 401, 403, 404, 422].includes(status)) {
167
+ return false;
168
  }
169
+
170
+ // 5xx错误和429限制错误应该重试
171
+ if (status >= 500 || status === 429) {
172
+ return true;
173
+ }
174
+
175
+ return false;
176
  }
177
 
178
  // 验证GitHub连接
179
  async validateConnection() {
180
+ await this.ensureInitialized();
181
+
182
  if (this.useMemoryStorage) {
183
+ console.log('📝 Memory storage mode active');
184
+ return {
185
+ valid: true,
186
+ useMemoryStorage: true,
187
+ repositories: [],
188
+ message: 'Using memory storage mode for development'
189
+ };
190
  }
191
 
192
  try {
193
  // 测试GitHub API连接
194
+ const response = await this.makeGitHubRequest(async () => {
195
+ return await axios.get(`${this.apiUrl}/user`, {
196
+ headers: {
197
+ 'Authorization': `token ${this.token}`,
198
+ 'Accept': 'application/vnd.github.v3+json'
199
+ },
200
+ timeout: 30000
201
+ });
202
+ }, 'GitHub user authentication');
203
 
204
  console.log('GitHub API connection successful:', response.data.login);
205
 
 
210
  const { owner, repo } = this.parseRepoUrl(repoUrl);
211
 
212
  // 检查仓库基本信息
213
+ const repoResponse = await this.makeGitHubRequest(async () => {
214
+ return await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, {
215
+ headers: {
216
+ 'Authorization': `token ${this.token}`,
217
+ 'Accept': 'application/vnd.github.v3+json'
218
+ },
219
+ timeout: 30000
220
+ });
221
+ }, `Repository access check for ${owner}/${repo}`);
222
 
223
  // 检查权限
224
  const permissions = repoResponse.data.permissions;
 
232
  headers: {
233
  'Authorization': `token ${this.token}`,
234
  'Accept': 'application/vnd.github.v3+json'
235
+ },
236
+ timeout: 15000
237
  });
238
  canAccessContents = true;
239
  } catch (contentsError) {
 
278
 
279
  // 初始化空仓库
280
  async initializeRepository(repoIndex = 0) {
281
+ await this.ensureInitialized();
282
+
283
  if (this.useMemoryStorage) {
284
+ console.log('📝 Memory storage mode - no repository initialization needed');
285
+ return { success: true, message: 'Memory storage initialized' };
286
  }
287
 
288
+ const repoUrl = this.repositories[repoIndex];
289
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
290
+
291
  try {
292
+ console.log(`🚀 Initializing repository: ${owner}/${repo}`);
 
 
 
293
 
294
  // 创建初始README文件
295
+ const readmeContent = `# PPT Storage Repository\n\nThis repository is used to store PPT data files.\n\nCreated: ${new Date().toISOString()}`;
296
+ const content = Buffer.from(readmeContent).toString('base64');
297
+
298
+ await this.makeGitHubRequest(async () => {
299
+ return await axios.put(
300
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/README.md`,
301
+ {
302
+ message: 'Initialize PPT storage repository',
303
+ content: content
304
+ },
305
+ {
306
+ headers: {
307
+ 'Authorization': `token ${this.token}`,
308
+ 'Accept': 'application/vnd.github.v3+json'
309
+ },
310
+ timeout: 30000
311
+ }
312
+ );
313
+ }, `Repository initialization for ${owner}/${repo}`);
314
+
315
+ console.log(`✅ Repository ${owner}/${repo} initialized successfully`);
316
+ return { success: true, message: 'Repository initialized' };
317
+ } catch (error) {
318
+ console.error(`❌ Repository initialization failed:`, error);
319
+ throw new Error(`Failed to initialize repository: ${error.message}`);
320
+ }
321
+ }
322
+
323
+ // 兼容性方法:旧的getFile方法重定向到新的getPPT
324
+ async getFile(userId, fileName, repoIndex = 0) {
325
+ await this.ensureInitialized();
326
+
327
+ const pptId = fileName.replace('.json', '');
328
+ return await this.getPPT(userId, pptId, repoIndex);
329
+ }
330
 
331
+ // 兼容性方法:旧的saveFile方法重定向到新的savePPT
332
+ async saveFile(userId, fileName, data, repoIndex = 0) {
333
+ await this.ensureInitialized();
334
+
335
+ const pptId = fileName.replace('.json', '');
336
+ return await this.savePPT(userId, pptId, data, repoIndex);
337
+ }
338
 
339
+ // 数据格式标准化
340
+ normalizeDataFormat(data) {
341
+ if (!data || typeof data !== 'object') {
342
+ throw new Error('Invalid data format provided');
343
+ }
344
 
345
+ const normalized = {
346
+ id: data.id || data.pptId || `ppt-${Date.now()}`,
347
+ title: data.title || '未命名演示文稿',
348
+ slides: Array.isArray(data.slides) ? data.slides : [],
349
+ theme: data.theme || {
350
+ backgroundColor: '#ffffff',
351
+ themeColor: '#d14424',
352
+ fontColor: '#333333',
353
+ fontName: 'Microsoft YaHei'
354
+ },
355
+ viewportSize: data.viewportSize || 1000,
356
+ viewportRatio: data.viewportRatio || 0.5625,
357
+ createdAt: data.createdAt || new Date().toISOString(),
358
+ updatedAt: new Date().toISOString()
359
+ };
360
 
361
+ // 标准化slides数据
362
+ normalized.slides = normalized.slides.map((slide, index) => {
363
+ if (!slide || typeof slide !== 'object') {
364
+ return {
365
+ id: `slide-${index}`,
366
+ elements: [],
367
+ background: { type: 'solid', color: '#ffffff' }
368
+ };
369
+ }
370
 
371
+ return {
372
+ id: slide.id || `slide-${index}`,
373
+ elements: Array.isArray(slide.elements) ? slide.elements : [],
374
+ background: slide.background || { type: 'solid', color: '#ffffff' },
375
+ ...slide
376
+ };
377
+ });
378
 
379
+ // 保留原始数据的其他字段
380
+ Object.keys(data).forEach(key => {
381
+ if (!normalized.hasOwnProperty(key) && key !== 'slides') {
382
+ normalized[key] = data[key];
383
+ }
384
+ });
385
+
386
+ return normalized;
387
+ }
388
+
389
+ // 兼容性:旧的deleteFile方法
390
+ async deleteFile(userId, fileName, repoIndex = 0) {
391
+ await this.ensureInitialized();
392
+
393
+ if (this.useMemoryStorage) {
394
+ const key = `users/${userId}/${fileName}`;
395
+ return this.memoryStorage.delete(key);
396
+ }
397
+
398
+ const repoUrl = this.repositories[repoIndex];
399
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
400
+ const path = `users/${userId}/${fileName}`;
401
+
402
+ try {
403
+ // 获取文件信息
404
+ const response = await this.makeGitHubRequest(async () => {
405
+ return await axios.get(
406
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
407
+ {
408
+ headers: {
409
+ 'Authorization': `token ${this.token}`,
410
+ 'Accept': 'application/vnd.github.v3+json'
411
+ },
412
+ timeout: 30000
413
  }
414
+ );
415
+ }, `Get file info for deletion: ${fileName}`);
416
+
417
+ // 删除主文件
418
+ await this.makeGitHubRequest(async () => {
419
+ return await axios.delete(
420
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
421
+ {
422
+ data: {
423
+ message: `Delete legacy PPT: ${fileName}`,
424
+ sha: response.data.sha
425
+ },
426
+ headers: {
427
+ 'Authorization': `token ${this.token}`,
428
+ 'Accept': 'application/vnd.github.v3+json'
429
+ },
430
+ timeout: 30000
431
+ }
432
+ );
433
+ }, `Delete legacy file: ${fileName}`);
434
 
435
+ console.log(`✅ Legacy file deleted: ${fileName}`);
436
+ return true;
437
  } catch (error) {
438
+ if (error.response?.status === 404) {
439
+ console.log(`📄 File not found: ${fileName}`);
440
+ return false;
441
+ }
442
+ console.error(`❌ Delete failed for ${fileName}:`, error);
443
+ throw new Error(`Failed to delete file: ${error.message}`);
444
  }
445
  }
446
 
447
+ // Memory storage methods - 兼容性方法
448
+ async saveToMemory(userId, fileName, data) {
449
+ const pptId = fileName.replace('.json', '');
450
+ return await this.savePPTToMemory(userId, pptId, data);
451
+ }
452
+
453
+ async getFromMemory(userId, fileName) {
454
+ const pptId = fileName.replace('.json', '');
455
+ const result = await this.getPPTFromMemory(userId, pptId);
456
+ return result ? result.content : null;
457
+ }
458
+
459
+ // 新架构:获取PPT(文件夹模式) - 简化版本
460
+ async getPPT(userId, pptId, repoIndex = 0) {
461
+ await this.ensureInitialized();
462
+
463
  if (this.useMemoryStorage) {
464
+ return await this.getPPTFromMemory(userId, pptId);
465
  }
466
 
 
467
  try {
468
+ console.log(`📂 Getting PPT folder: ${pptId} for user: ${userId}`);
469
+
470
  const repoUrl = this.repositories[repoIndex];
471
  const { owner, repo } = this.parseRepoUrl(repoUrl);
472
+ const pptFolderPath = `users/${userId}/${pptId}`;
473
 
474
+ // 1. 获取PPT文件夹内容 - 带重试
475
+ const folderResponse = await this.makeGitHubRequest(async () => {
476
+ return await axios.get(
477
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`,
478
+ {
479
+ headers: {
480
+ 'Authorization': `token ${this.token}`,
481
+ 'Accept': 'application/vnd.github.v3+json'
482
+ },
483
+ timeout: 30000
484
  }
485
+ );
486
+ }, `Get PPT folder contents for ${pptId}`);
487
+
488
+ // 2. 读取元数据文件
489
+ const metaFile = folderResponse.data.find(file => file.name === 'meta.json');
490
+ if (!metaFile) {
491
+ throw new Error('PPT metadata file not found');
492
+ }
493
+
494
+ const metaResponse = await this.makeGitHubRequest(async () => {
495
+ return await axios.get(
496
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${metaFile.path}`,
497
+ {
498
+ headers: {
499
+ 'Authorization': `token ${this.token}`,
500
+ 'Accept': 'application/vnd.github.v3+json'
501
+ },
502
+ timeout: 15000
503
+ }
504
+ );
505
+ }, `Get PPT metadata for ${pptId}`);
506
+
507
+ let metadata;
508
+ try {
509
+ const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8');
510
+ metadata = JSON.parse(metaContent);
511
+ } catch (parseError) {
512
+ console.error('❌ Failed to parse metadata:', parseError);
513
+ throw new Error('Invalid PPT metadata format');
514
+ }
515
+
516
+ // 3. 加载所有slide文件
517
+ const allFiles = folderResponse.data;
518
+ const slides = [];
519
+ const failedSlides = [];
520
+
521
+ // 查找slide文件(只处理普通文件,忽略拆分文件)
522
+ const slideFiles = allFiles.filter(file =>
523
+ file.name.startsWith('slide_') &&
524
+ file.name.endsWith('.json') &&
525
+ !file.name.includes('_main') &&
526
+ !file.name.includes('_part_')
527
  );
528
 
529
+ // 按序号排序
530
+ const sortedSlideFiles = slideFiles.sort((a, b) => {
531
+ const aIndex = parseInt(a.name.match(/slide_(\d+)\.json/)?.[1] || '0');
532
+ const bIndex = parseInt(b.name.match(/slide_(\d+)\.json/)?.[1] || '0');
533
+ return aIndex - bIndex;
534
+ });
535
+
536
+ for (const slideFile of sortedSlideFiles) {
537
+ try {
538
+ const slide = await this.loadSlideFile(slideFile, repoIndex);
539
+ const slideIndex = parseInt(slideFile.name.match(/slide_(\d+)\.json/)?.[1] || '0');
540
+ slides[slideIndex] = slide;
541
+ } catch (slideError) {
542
+ console.warn(`⚠️ Failed to load slide ${slideFile.name}:`, slideError.message);
543
+ failedSlides.push(slideFile.name);
544
+ }
545
+ }
546
+
547
+ // 4. 组装完整PPT数据
548
+ const finalSlides = slides.filter(slide => slide !== undefined);
549
+ const pptData = {
550
+ ...metadata,
551
+ slides: finalSlides,
552
+ storage: {
553
+ type: 'folder',
554
+ slidesCount: finalSlides.length,
555
+ folderPath: pptFolderPath,
556
+ loadedAt: new Date().toISOString(),
557
+ failedSlides: failedSlides.length > 0 ? failedSlides : undefined
558
+ }
559
  };
560
+
561
+ if (failedSlides.length > 0) {
562
+ console.warn(`⚠️ PPT loaded with ${failedSlides.length} failed slides`);
563
+ } else {
564
+ console.log(`✅ PPT loaded successfully: ${finalSlides.length} slides`);
565
+ }
566
+
567
+ return { content: pptData };
568
+
569
  } catch (error) {
570
  if (error.response?.status === 404) {
571
+ console.log(`📄 PPT folder not found: ${pptId}`);
572
  return null;
573
  }
574
+ console.error(`❌ Get PPT failed:`, error);
575
+ throw new Error(`Failed to get PPT: ${error.message}`);
576
  }
577
  }
578
 
579
+ // 新增:检查文件大小
580
+ async checkFileSize(filePath, repoIndex) {
 
 
 
 
 
 
581
  const repoUrl = this.repositories[repoIndex];
582
  const { owner, repo } = this.parseRepoUrl(repoUrl);
583
+
 
 
 
 
 
584
  try {
585
+ const response = await this.makeGitHubRequest(async () => {
586
+ return await axios.get(
587
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
588
+ {
589
+ headers: {
590
+ 'Authorization': `token ${this.token}`,
591
+ 'Accept': 'application/vnd.github.v3+json'
592
+ },
593
+ timeout: 15000
594
+ }
595
+ );
596
+ }, `Check file size for ${filePath}`);
597
+
598
+ return {
599
+ size: response.data.size,
600
+ sha: response.data.sha
601
+ };
602
  } catch (error) {
603
+ if (error.response?.status === 404) {
604
+ return null;
605
+ }
606
+ throw error;
607
  }
608
+ }
609
 
610
+ // 新增:智能读取文件内容(根据大小选择策略)
611
+ async readFileContent(filePath, repoIndex, maxDirectSize = 1024 * 1024) { // 1MB限制
612
+ const fileInfo = await this.checkFileSize(filePath, repoIndex);
613
+
614
+ if (!fileInfo) {
615
+ return null;
616
+ }
617
+
618
+ // 如果文件小于限制,直接读取
619
+ if (fileInfo.size <= maxDirectSize) {
620
+ return await this.readFileContentDirect(filePath, repoIndex);
621
  }
622
+
623
+ // 大文件:使用Git Blob API读取
624
+ console.log(`📊 Large file detected (${(fileInfo.size / 1024 / 1024).toFixed(2)} MB), using blob API`);
625
+ return await this.readFileContentViaBlob(filePath, fileInfo.sha, repoIndex);
626
+ }
627
 
628
+ // 直接读取文件(小文件)
629
+ async readFileContentDirect(filePath, repoIndex) {
630
+ const repoUrl = this.repositories[repoIndex];
631
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
632
+
633
+ const response = await this.makeGitHubRequest(async () => {
634
+ return await axios.get(
635
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
636
  {
637
  headers: {
638
  'Authorization': `token ${this.token}`,
639
  'Accept': 'application/vnd.github.v3+json'
640
+ },
641
+ timeout: 30000
642
  }
643
  );
644
+ }, `Read file content directly: ${filePath}`);
645
+
646
+ const content = Buffer.from(response.data.content, 'base64').toString('utf8');
647
+ return JSON.parse(content);
648
+ }
649
 
650
+ // 通过Git Blob API读取大文件
651
+ async readFileContentViaBlob(filePath, sha, repoIndex) {
652
+ const repoUrl = this.repositories[repoIndex];
653
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
654
+
655
+ try {
656
+ console.log(`🔍 Reading large file via blob API: ${filePath}`);
657
 
658
+ const response = await this.makeGitHubRequest(async () => {
659
+ return await axios.get(
660
+ `${this.apiUrl}/repos/${owner}/${repo}/git/blobs/${sha}`,
661
+ {
662
+ headers: {
663
+ 'Authorization': `token ${this.token}`,
664
+ 'Accept': 'application/vnd.github.v3+json'
665
+ },
666
+ timeout: 120000 // 2分钟超时
667
+ }
668
+ );
669
+ }, `Read blob for large file: ${filePath}`);
 
 
670
 
671
+ if (response.data.encoding === 'base64') {
672
+ const content = Buffer.from(response.data.content, 'base64').toString('utf8');
673
+ return JSON.parse(content);
 
 
674
  } else {
675
+ // 如果不是base64编码,直接解析
676
+ return JSON.parse(response.data.content);
677
  }
678
+ } catch (error) {
679
+ console.error(`❌ Blob API read failed for ${filePath}:`, error.message);
680
+
681
+ // 回退到分批读取策略
682
+ console.log(`🔄 Falling back to chunked reading...`);
683
+ return await this.readLargeFileInChunks(filePath, repoIndex);
684
  }
685
  }
686
 
687
+ // 分批读取大文件(最后的回退策略)
688
+ async readLargeFileInChunks(filePath, repoIndex) {
689
+ console.log(`📦 Attempting chunked read for: ${filePath}`);
 
 
690
 
691
+ // 检查是否存在分块文件(PPT文件夹结构)
692
+ const folderPath = filePath.replace(/\/[^\/]+$/, '');
693
 
694
  try {
695
+ const repoUrl = this.repositories[repoIndex];
696
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
 
 
 
 
 
 
697
 
698
+ // 尝试读取文件夹内容
699
+ const folderResponse = await this.makeGitHubRequest(async () => {
700
+ return await axios.get(
701
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${folderPath}`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  {
703
  headers: {
704
  'Authorization': `token ${this.token}`,
705
  'Accept': 'application/vnd.github.v3+json'
706
+ },
707
+ timeout: 30000
708
  }
709
  );
710
+ }, `Read folder for chunked file: ${folderPath}`);
711
+
712
+ // 查找相关的分块文件
713
+ const chunkFiles = folderResponse.data.filter(file =>
714
+ file.name.includes('_part_') || file.name.includes('_chunk_')
715
+ );
716
+
717
+ if (chunkFiles.length > 0) {
718
+ console.log(`📊 Found ${chunkFiles.length} chunk files, reassembling...`);
719
+ return await this.reassembleChunkedFiles(chunkFiles, repoIndex);
720
  }
721
 
722
+ throw new Error('No chunked files found, cannot read large file');
723
+ } catch (error) {
724
+ console.error(`❌ Chunked read failed:`, error.message);
725
+ throw new Error(`Unable to read large file: ${filePath}. File may be too large for current GitHub API limits.`);
726
+ }
727
+ }
728
+
729
+ // 重组分块文件
730
+ async reassembleChunkedFiles(chunkFiles, repoIndex) {
731
+ const chunks = [];
732
+
733
+ // 按顺序读取所有分块
734
+ const sortedChunks = chunkFiles.sort((a, b) => {
735
+ const aIndex = parseInt(a.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0');
736
+ const bIndex = parseInt(b.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0');
737
+ return aIndex - bIndex;
738
+ });
739
+
740
+ for (const chunkFile of sortedChunks) {
741
  try {
742
+ const chunkContent = await this.readFileContentDirect(chunkFile.path, repoIndex);
743
+ chunks.push(chunkContent);
744
+ } catch (error) {
745
+ console.error(`⚠️ Failed to read chunk ${chunkFile.name}:`, error.message);
746
+ }
747
+ }
748
+
749
+ // 重组数据
750
+ if (chunks.length === 0) {
751
+ throw new Error('No valid chunks found');
752
+ }
753
+
754
+ // 假设第一个chunk包含基础结构
755
+ const baseData = chunks[0];
756
+ const allSlides = [];
757
+
758
+ chunks.forEach(chunk => {
759
+ if (chunk.slides && Array.isArray(chunk.slides)) {
760
+ allSlides.push(...chunk.slides);
761
+ }
762
+ });
763
+
764
+ return {
765
+ ...baseData,
766
+ slides: allSlides,
767
+ storage: {
768
+ type: 'reassembled',
769
+ chunksCount: chunks.length,
770
+ reassembledAt: new Date().toISOString()
771
+ }
772
+ };
773
+ }
774
+
775
+ // 修改现有的loadSlideFile方法,使用新的智能读取
776
+ async loadSlideFile(slideFile, repoIndex) {
777
+ try {
778
+ // 使用智能读取策略
779
+ const slideContent = await this.readFileContent(slideFile.path, repoIndex);
780
+ return slideContent;
781
+ } catch (error) {
782
+ console.error(`❌ Failed to load slide file ${slideFile.name}:`, error.message);
783
+ throw error;
784
+ }
785
+ }
786
+
787
+ // slide压缩算法 - 保留轻量压缩
788
+ async compressSlide(slide) {
789
+ let compressedSlide = JSON.parse(JSON.stringify(slide)); // 深拷贝
790
+
791
+ // 压缩策略1:移除不必要的属性
792
+ if (compressedSlide.elements) {
793
+ compressedSlide.elements = compressedSlide.elements.map(element => {
794
+ // 移除编辑状态属性
795
+ element = this.removeUnnecessaryProps(element);
796
 
797
+ // 压缩文本内容(移除多余空白)
798
+ if (element.type === 'text' && element.content && typeof element.content === 'string') {
799
+ element.content = element.content.replace(/\s+/g, ' ').trim();
800
+ }
801
 
802
+ return element;
803
+ });
804
+ }
805
+
806
+ // 压缩策略2:精简数值���度
807
+ compressedSlide = this.roundNumericValues(compressedSlide);
808
+
809
+ const compressedSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
810
+ console.log(`🗜️ Light compression applied: ${(compressedSize / 1024).toFixed(2)} KB`);
811
+
812
+ return compressedSlide;
813
+ }
814
+
815
+ // 移除不必要的属性
816
+ removeUnnecessaryProps(element) {
817
+ const unnecessaryProps = [
818
+ 'selected', 'editing', 'dragData', 'resizeData',
819
+ 'tempData', 'cache', 'debug'
820
+ ];
821
+
822
+ unnecessaryProps.forEach(prop => {
823
+ if (element[prop] !== undefined) {
824
+ delete element[prop];
825
+ }
826
+ });
827
+
828
+ return element;
829
+ }
830
+
831
+ // 精简数值精度 - 保持高精度的关键属性
832
+ roundNumericValues(obj, precision = 2) {
833
+ if (typeof obj !== 'object' || obj === null) {
834
+ return obj;
835
+ }
836
+
837
+ if (Array.isArray(obj)) {
838
+ return obj.map(item => this.roundNumericValues(item, precision));
839
+ }
840
+
841
+ const rounded = {};
842
+ for (const [key, value] of Object.entries(obj)) {
843
+ if (typeof value === 'number') {
844
+ // 🎯 关键布局属性保持高精度
845
+ if (['left', 'top', 'width', 'height', 'x', 'y', 'viewportRatio', 'viewportSize'].includes(key)) {
846
+ rounded[key] = Math.round(value * 1000000000) / 1000000000; // 9位精度
847
+ }
848
+ // 🎯 变换和旋转属性保持高精度
849
+ else if (key.includes('transform') || key.includes('rotate') || key === 'rotate') {
850
+ rounded[key] = Math.round(value * 1000000000) / 1000000000;
851
+ }
852
+ // 📐 其他数值属性适度精简
853
+ else {
854
+ rounded[key] = typeof value === 'number' && !isNaN(value) && isFinite(value)
855
+ ? Math.round(value * 1000000) / 1000000 // 6位精度
856
+ : value;
857
+ }
858
+ } else if (typeof value === 'object') {
859
+ rounded[key] = this.roundNumericValues(value, precision);
860
+ } else {
861
+ rounded[key] = value;
862
+ }
863
+ }
864
+
865
+ return rounded;
866
+ }
867
+
868
+ // 通用文件保存到仓库 - 简化版本
869
+ async saveFileToRepo(filePath, data, commitMessage, repoIndex) {
870
+ const repoUrl = this.repositories[repoIndex];
871
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
872
+
873
+ try {
874
+ // 验证数据
875
+ if (!data || typeof data !== 'object') {
876
+ throw new Error('Invalid data provided for file save');
877
+ }
878
+
879
+ const content = Buffer.from(JSON.stringify(data)).toString('base64');
880
+ const fileSize = Buffer.byteLength(JSON.stringify(data), 'utf8');
881
+
882
+ // 检查文件大小(GitHub限制100MB)
883
+ if (fileSize > 100 * 1024 * 1024) {
884
+ throw new Error(`File too large: ${(fileSize / 1024 / 1024).toFixed(2)} MB exceeds GitHub's 100MB limit`);
885
+ }
886
+
887
+ console.log(`💾 Saving file: ${filePath} (${(fileSize / 1024).toFixed(2)} KB)`);
888
+
889
+ // 检查文件是否已存在
890
+ let sha = null;
891
+ try {
892
+ const existingResponse = await axios.get(
893
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
894
  {
895
  headers: {
896
  'Authorization': `token ${this.token}`,
897
  'Accept': 'application/vnd.github.v3+json'
898
+ },
899
+ timeout: 30000
900
  }
901
  );
902
+ sha = existingResponse.data.sha;
903
+ } catch (error) {
904
+ // 文件不存在,将创建新文件
905
  }
906
+
907
+ // 保存文件
908
+ const response = await axios.put(
909
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
910
+ {
911
+ message: commitMessage,
912
+ content: content,
913
+ ...(sha && { sha })
914
+ },
915
  {
916
  headers: {
917
  'Authorization': `token ${this.token}`,
918
  'Accept': 'application/vnd.github.v3+json'
919
+ },
920
+ timeout: 60000
921
  }
922
  );
923
+
924
+ console.log(`✅ File saved successfully: ${filePath}`);
925
+ return response.data;
926
+ } catch (error) {
927
+ console.error(`❌ File save failed for ${filePath}:`, error.message);
928
 
929
+ if (error.response?.status === 422) {
930
+ if (error.response?.data?.message?.includes('file is too large')) {
931
+ throw new Error(`File too large: ${filePath} exceeds GitHub size limits`);
932
+ }
933
+ if (error.response?.data?.message?.includes('Invalid request')) {
934
+ throw new Error(`Invalid file format for: ${filePath}`);
935
+ }
936
+ }
937
 
938
+ if (error.response?.status === 409) {
939
+ throw new Error(`File conflict for: ${filePath}. File may have been modified by another process.`);
940
+ }
941
 
942
+ throw new Error(`Failed to save file ${filePath}: ${error.message}`);
943
+ }
944
+ }
945
+
946
+ // 单个slide保存 - 简化版本
947
+ async saveSlideWithCompression(filePath, slide, slideIndex, repoIndex) {
948
+ try {
949
+ // 应用轻量压缩
950
+ const compressedSlide = await this.compressSlide(slide);
951
+
952
+ await this.makeGitHubRequest(async () => {
953
+ return await this.saveFileToRepo(
954
+ filePath,
955
+ compressedSlide,
956
+ `Save slide ${slideIndex}`,
957
+ repoIndex
958
+ );
959
+ }, `Save slide ${slideIndex}`);
960
+
961
+ const finalSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
962
+
963
+ return {
964
+ slideIndex,
965
+ finalSize,
966
+ compressed: true
967
+ };
968
+ } catch (error) {
969
+ console.error(`❌ Failed to save slide ${slideIndex}:`, error);
970
+ throw error;
971
+ }
972
+ }
973
+
974
+ // 新架构:保存PPT(文件夹模式) - 简化版本
975
+ async savePPT(userId, pptId, pptData, repoIndex = 0) {
976
+ await this.ensureInitialized();
977
+
978
+ if (this.useMemoryStorage) {
979
+ return await this.savePPTToMemory(userId, pptId, pptData);
980
+ }
981
+
982
+ try {
983
+ console.log(`📂 Saving PPT to folder: ${pptId} for user: ${userId}`);
984
+
985
+ const repoUrl = this.repositories[repoIndex];
986
+ const { owner, repo } = this.parseRepoUrl(repoUrl);
987
+ const pptFolderPath = `users/${userId}/${pptId}`;
988
+
989
+ // 验证输入数据
990
+ if (!pptData || typeof pptData !== 'object') {
991
+ throw new Error('Invalid PPT data provided');
992
+ }
993
+
994
+ // 1. 准备元数据(不包含slides)
995
+ const metadata = {
996
+ id: pptData.id || pptId,
997
+ title: pptData.title || '未命名演示文稿',
998
+ theme: pptData.theme || {
999
+ backgroundColor: '#ffffff',
1000
+ themeColor: '#d14424',
1001
+ fontColor: '#333333',
1002
+ fontName: 'Microsoft YaHei'
1003
+ },
1004
+ viewportSize: pptData.viewportSize || 1000,
1005
+ viewportRatio: pptData.viewportRatio || 0.5625,
1006
+ createdAt: pptData.createdAt || new Date().toISOString(),
1007
+ updatedAt: new Date().toISOString(),
1008
+ storage: {
1009
+ type: 'folder',
1010
+ version: '2.0',
1011
+ slidesCount: pptData.slides?.length || 0
1012
+ }
1013
+ };
1014
+
1015
+ // 2. 保存元数据文件
1016
+ await this.makeGitHubRequest(async () => {
1017
+ return await this.saveFileToRepo(
1018
+ `${pptFolderPath}/meta.json`,
1019
+ metadata,
1020
+ `Update PPT metadata: ${metadata.title}`,
1021
+ repoIndex
1022
+ );
1023
+ }, `Save metadata for PPT ${pptId}`);
1024
+
1025
+ console.log(`✅ Metadata saved for PPT: ${pptId}`);
1026
+
1027
+ // 3. 串行化保存每个slide文件
1028
+ const slides = Array.isArray(pptData.slides) ? pptData.slides : [];
1029
+ const saveResults = [];
1030
+
1031
+ console.log(`📊 Starting to save ${slides.length} slides...`);
1032
+
1033
+ for (let i = 0; i < slides.length; i++) {
1034
+ const slide = slides[i];
1035
+ const slideFileName = `slide_${String(i).padStart(3, '0')}.json`;
1036
+
1037
+ console.log(`💾 Saving slide ${i + 1}/${slides.length}: ${slideFileName}`);
1038
+
1039
+ try {
1040
+ // 验证slide数据
1041
+ if (!slide || typeof slide !== 'object') {
1042
+ throw new Error(`Invalid slide data at index ${i}`);
1043
+ }
1044
+
1045
+ const result = await this.saveSlideWithCompression(
1046
+ `${pptFolderPath}/${slideFileName}`,
1047
+ slide,
1048
+ i,
1049
+ repoIndex
1050
+ );
1051
+
1052
+ saveResults.push(result);
1053
+ console.log(`✅ Slide ${i} saved: ${(result.finalSize / 1024).toFixed(2)} KB`);
1054
+
1055
+ // 添加延迟避免GitHub API速率限制
1056
+ if (i < slides.length - 1) {
1057
+ await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay));
1058
+ }
1059
+ } catch (slideError) {
1060
+ console.error(`❌ Failed to save slide ${i}: ${slideError.message}`);
1061
+ throw slideError; // 任何slide保存失败都应该停止整个过程
1062
+ }
1063
+ }
1064
+
1065
+ // 4. 计算保存统计
1066
+ const totalSize = saveResults.reduce((sum, r) => sum + r.finalSize, 0);
1067
+
1068
+ console.log(`🎉 PPT saved successfully: ${slides.length} slides, total size: ${(totalSize / 1024).toFixed(2)} KB`);
1069
+
1070
+ return {
1071
+ success: true,
1072
+ pptId: pptId,
1073
+ storage: 'folder',
1074
+ slidesCount: slides.length,
1075
+ folderPath: pptFolderPath,
1076
+ size: totalSize
1077
+ };
1078
+
1079
+ } catch (error) {
1080
+ console.error(`❌ PPT save failed for ${pptId}: ${error.message}`);
1081
+ throw new Error(`Failed to save PPT: ${error.message}`);
1082
+ }
1083
+ }
1084
+
1085
+ // Memory storage methods - 简化版本
1086
+ async getPPTFromMemory(userId, pptId) {
1087
+ const metaKey = `users/${userId}/${pptId}/meta`;
1088
+ const metadata = this.memoryStorage.get(metaKey);
1089
+
1090
+ if (!metadata) {
1091
+ console.log(`📄 PPT not found in memory: ${pptId}`);
1092
+ return null;
1093
+ }
1094
+
1095
+ // 重组slides
1096
+ const slides = [];
1097
+ const slidesCount = metadata.storage?.slidesCount || 0;
1098
+
1099
+ for (let i = 0; i < slidesCount; i++) {
1100
+ const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`;
1101
+ const slide = this.memoryStorage.get(slideKey);
1102
+ if (slide) {
1103
+ slides.push(slide);
1104
+ }
1105
+ }
1106
+
1107
+ const pptData = {
1108
+ ...metadata,
1109
+ slides: slides,
1110
+ storage: {
1111
+ ...metadata.storage,
1112
+ loadedAt: new Date().toISOString()
1113
+ }
1114
+ };
1115
+
1116
+ console.log(`📖 Read PPT from memory: ${pptId} (${slides.length} slides)`);
1117
+ return { content: pptData };
1118
+ }
1119
+
1120
+ // 简化内存存储
1121
+ async savePPTToMemory(userId, pptId, pptData) {
1122
+ // 保存元数据
1123
+ const metadata = {
1124
+ id: pptData.id || pptId,
1125
+ title: pptData.title || '未命名演示文稿',
1126
+ theme: pptData.theme,
1127
+ viewportSize: pptData.viewportSize || 1000,
1128
+ viewportRatio: pptData.viewportRatio || 0.5625,
1129
+ createdAt: pptData.createdAt || new Date().toISOString(),
1130
+ updatedAt: new Date().toISOString(),
1131
+ storage: {
1132
+ type: 'folder',
1133
+ version: '2.0',
1134
+ slidesCount: pptData.slides?.length || 0
1135
+ }
1136
+ };
1137
+
1138
+ const metaKey = `users/${userId}/${pptId}/meta`;
1139
+ this.memoryStorage.set(metaKey, metadata);
1140
+
1141
+ // 保存slides
1142
+ const slides = pptData.slides || [];
1143
+ let totalSize = 0;
1144
+
1145
+ for (let i = 0; i < slides.length; i++) {
1146
+ const slide = slides[i];
1147
+ const compressedSlide = await this.compressSlide(slide);
1148
+ const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`;
1149
+ this.memoryStorage.set(slideKey, compressedSlide);
1150
+
1151
+ totalSize += Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
1152
+ }
1153
+
1154
+ console.log(`💾 Saved PPT to memory: ${pptId} (${slides.length} slides, ${(totalSize / 1024).toFixed(2)} KB)`);
1155
+
1156
+ return {
1157
+ success: true,
1158
+ storage: 'folder',
1159
+ slidesCount: slides.length,
1160
+ size: totalSize
1161
+ };
1162
+ }
1163
+
1164
+ // 删除PPT时清理所有文件
1165
+ async deletePPTFromMemory(userId, pptId) {
1166
+ let deleted = 0;
1167
+ const prefix = `users/${userId}/${pptId}/`;
1168
+
1169
+ // 删除所有相关键
1170
+ for (const key of this.memoryStorage.keys()) {
1171
+ if (key.startsWith(prefix)) {
1172
+ this.memoryStorage.delete(key);
1173
+ deleted++;
1174
  }
1175
  }
1176
+
1177
+ console.log(`📝 Memory storage: Deleted ${deleted} files for PPT ${pptId}`);
1178
+ return deleted > 0;
1179
  }
1180
 
1181
+ // 更新用户PPT列表
1182
+ async getUserPPTListFromMemory(userId) {
1183
+ const results = [];
1184
+ const userPrefix = `users/${userId}/`;
1185
+ const pptIds = new Set();
1186
+
1187
+ // 收集所有PPT ID
1188
+ for (const key of this.memoryStorage.keys()) {
1189
+ if (key.startsWith(userPrefix) && key.includes('/meta')) {
1190
+ const pptId = key.replace(userPrefix, '').replace('/meta', '');
1191
+ pptIds.add(pptId);
1192
+ }
1193
+ }
1194
+
1195
+ // 获取每个PPT的元数据
1196
+ for (const pptId of pptIds) {
1197
+ const metaKey = `${userPrefix}${pptId}/meta`;
1198
+ const metadata = this.memoryStorage.get(metaKey);
1199
+
1200
+ if (metadata) {
1201
+ results.push({
1202
+ name: pptId,
1203
+ title: metadata.title || '未命名演示文稿',
1204
+ updatedAt: metadata.updatedAt || new Date().toISOString(),
1205
+ slidesCount: metadata.storage?.slidesCount || 0,
1206
+ isChunked: false,
1207
+ storageType: 'folder',
1208
+ size: JSON.stringify(metadata).length,
1209
+ repoIndex: 0
1210
+ });
1211
+ }
1212
+ }
1213
+
1214
+ console.log(`📋 Found ${results.length} PPTs in memory for user ${userId}`);
1215
+ return results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
1216
+ }
1217
+
1218
+ // 获取用户PPT列表(新架构) - 简化版本
1219
  async getUserPPTList(userId) {
1220
+ await this.ensureInitialized();
1221
+
1222
  if (this.useMemoryStorage) {
1223
+ return await this.getUserPPTListFromMemory(userId);
1224
  }
1225
 
1226
+ const pptList = [];
 
1227
 
1228
+ // 检查所有仓库
1229
+ for (let repoIndex = 0; repoIndex < this.repositories.length; repoIndex++) {
1230
  try {
1231
+ const repoUrl = this.repositories[repoIndex];
1232
  const { owner, repo } = this.parseRepoUrl(repoUrl);
1233
+ const userDirPath = `users/${userId}`;
1234
 
1235
+ const response = await this.makeGitHubRequest(async () => {
1236
+ return await axios.get(
1237
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}`,
1238
+ {
1239
+ headers: {
1240
+ 'Authorization': `token ${this.token}`,
1241
+ 'Accept': 'application/vnd.github.v3+json'
1242
+ },
1243
+ timeout: 30000
1244
  }
1245
+ );
1246
+ }, `Get user directory for ${userId} in repo ${repoIndex}`);
1247
+
1248
+ // 查找PPT文件夹
1249
+ const pptFolders = response.data.filter(item =>
1250
+ item.type === 'dir' // PPT存储为文件夹
1251
  );
1252
 
1253
+ for (const folder of pptFolders) {
1254
+ try {
1255
+ const pptId = folder.name;
1256
+
1257
+ // 获取PPT元数据
1258
+ const metaResponse = await this.makeGitHubRequest(async () => {
1259
+ return await axios.get(
1260
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${pptId}/meta.json`,
1261
+ {
1262
+ headers: {
1263
+ 'Authorization': `token ${this.token}`,
1264
+ 'Accept': 'application/vnd.github.v3+json'
1265
+ },
1266
+ timeout: 15000
1267
+ }
1268
+ );
1269
+ }, `Get metadata for PPT ${pptId}`);
1270
+
1271
+ const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8');
1272
+ const metadata = JSON.parse(metaContent);
1273
 
1274
+ pptList.push({
1275
+ name: pptId,
1276
+ title: metadata.title || '未命名演示文稿',
1277
+ updatedAt: metadata.updatedAt || new Date().toISOString(),
1278
+ slidesCount: metadata.storage?.slidesCount || 0,
1279
+ isChunked: false,
1280
+ storageType: 'folder',
1281
+ size: metaResponse.data.size || 0,
1282
+ repoIndex: repoIndex
1283
+ });
1284
+ } catch (error) {
1285
+ console.warn(`跳过无效PPT文件夹 ${folder.name}:`, error.message);
1286
+ }
1287
+ }
1288
+
1289
+ // 兼容性:同时检查旧的单文件格式
1290
+ const jsonFiles = response.data.filter(file =>
1291
+ file.type === 'file' &&
1292
+ file.name.endsWith('.json') &&
1293
+ !file.name.includes('_chunk_')
1294
+ );
1295
+
1296
+ for (const file of jsonFiles) {
1297
  try {
1298
  const pptId = file.name.replace('.json', '');
 
1299
 
1300
+ // 避免重复添加(如果已经有文件夹版本)
1301
+ if (pptList.some(p => p.name === pptId)) {
1302
+ continue;
 
 
 
 
 
1303
  }
1304
+
1305
+ const fileResponse = await this.makeGitHubRequest(async () => {
1306
+ return await axios.get(
1307
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${file.name}`,
1308
+ {
1309
+ headers: {
1310
+ 'Authorization': `token ${this.token}`,
1311
+ 'Accept': 'application/vnd.github.v3+json'
1312
+ },
1313
+ timeout: 15000
1314
+ }
1315
+ );
1316
+ }, `Get legacy PPT file ${file.name}`);
1317
+
1318
+ const content = Buffer.from(fileResponse.data.content, 'base64').toString('utf8');
1319
+ const pptData = JSON.parse(content);
1320
+
1321
+ pptList.push({
1322
+ name: pptId,
1323
+ title: pptData.title || '未命名演示文稿',
1324
+ updatedAt: pptData.updatedAt || new Date().toISOString(),
1325
+ slidesCount: pptData.isChunked ? pptData.totalSlides : (pptData.slides?.length || 0),
1326
+ isChunked: pptData.isChunked || false,
1327
+ storageType: 'legacy',
1328
+ size: file.size,
1329
+ repoIndex: repoIndex
1330
  });
1331
+ } catch (error) {
1332
+ console.warn(`跳过无效文件 ${file.name}:`, error.message);
1333
  }
1334
  }
1335
  } catch (error) {
1336
+ console.warn(`仓库 ${repoIndex} 中没有找到用户目录或访问失败:`, error.message);
1337
+ continue;
 
1338
  }
1339
  }
1340
 
1341
+ // 按更新时间排序
1342
+ return pptList.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
1343
  }
1344
 
1345
+ // 删除PPT(新架构) - 简化版本
1346
+ async deletePPT(userId, pptId, repoIndex = 0) {
1347
+ await this.ensureInitialized();
1348
+
1349
  if (this.useMemoryStorage) {
1350
+ return await this.deletePPTFromMemory(userId, pptId);
 
 
 
 
 
 
1351
  }
1352
 
1353
  const repoUrl = this.repositories[repoIndex];
1354
  const { owner, repo } = this.parseRepoUrl(repoUrl);
1355
+ const pptFolderPath = `users/${userId}/${pptId}`;
1356
 
1357
+ try {
1358
+ console.log(`🗑️ Deleting PPT folder: ${pptFolderPath}`);
1359
+
1360
+ // 1. 获取文件夹内容
1361
+ const folderResponse = await this.makeGitHubRequest(async () => {
1362
+ return await axios.get(
1363
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`,
1364
+ {
1365
+ headers: {
1366
+ 'Authorization': `token ${this.token}`,
1367
+ 'Accept': 'application/vnd.github.v3+json'
1368
+ },
1369
+ timeout: 30000
1370
+ }
1371
+ );
1372
+ }, `Get PPT folder contents for deletion: ${pptId}`);
1373
+
1374
+ // 2. 串行删除所有文件
1375
+ for (const file of folderResponse.data) {
1376
+ try {
1377
+ await this.makeGitHubRequest(async () => {
1378
+ return await axios.delete(
1379
+ `${this.apiUrl}/repos/${owner}/${repo}/contents/${file.path}`,
1380
+ {
1381
+ data: {
1382
+ message: `Delete PPT file: ${file.name}`,
1383
+ sha: file.sha
1384
+ },
1385
+ headers: {
1386
+ 'Authorization': `token ${this.token}`,
1387
+ 'Accept': 'application/vnd.github.v3+json'
1388
+ },
1389
+ timeout: 30000
1390
+ }
1391
+ );
1392
+ }, `Delete file ${file.name}`);
1393
+
1394
+ console.log(`✅ Deleted file: ${file.name}`);
1395
+
1396
+ // 添加延迟避免API限制
1397
+ await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay));
1398
+ } catch (error) {
1399
+ console.warn(`⚠️ Failed to delete file ${file.name}:`, error.message);
1400
  }
1401
  }
 
1402
 
1403
+ console.log(`✅ PPT folder deleted successfully: ${pptId}`);
1404
+ return true;
1405
+
1406
+ } catch (error) {
1407
+ if (error.response?.status === 404) {
1408
+ console.log(`📄 PPT folder not found, trying legacy format: ${pptId}`);
1409
+ return await this.deleteFile(userId, `${pptId}.json`, repoIndex);
1410
+ }
1411
+
1412
+ console.error(`❌ Delete PPT failed for ${pptId}:`, error);
1413
+ throw new Error(`Failed to delete PPT: ${error.message}`);
1414
+ }
1415
  }
1416
  }
1417
 
backend/src/services/screenshotService.js CHANGED
@@ -1,663 +1,341 @@
1
  import puppeteer from 'puppeteer';
 
2
 
3
  class ScreenshotService {
4
  constructor() {
5
- this.browser = null;
6
- this.playwrightBrowser = null; // 添加Playwright浏览器实例
7
- this.isInitialized = false;
8
- this.maxRetries = 2;
9
- this.browserLaunchRetries = 0;
10
- this.maxBrowserLaunchRetries = 2;
11
- this.isClosing = false;
12
- this.isHuggingFaceSpace = process.env.SPACE_ID || process.env.HF_SPACE_ID;
13
- this.preferredEngine = 'puppeteer'; // 优先使用的截图引擎
14
- this.isPlaywrightAvailable = false; // 初始为false,运行时检查
15
- this.chromium = null; // Playwright chromium实例
16
- }
17
-
18
- // 动态检查和加载Playwright
19
- async initPlaywright() {
20
- if (this.chromium) return this.chromium; // 已经加载过
21
 
22
- try {
23
- console.log('🎭 尝试动态加载Playwright...');
24
- const playwrightModule = await import('playwright');
25
- this.chromium = playwrightModule.chromium;
26
- this.isPlaywrightAvailable = true;
27
- console.log('✅ Playwright动态加载成功,可作为截图备用方案');
28
- return this.chromium;
29
- } catch (error) {
30
- console.warn('⚠️ Playwright动态加载失败,将仅使用Puppeteer进行截图:', error.message);
31
- this.isPlaywrightAvailable = false;
32
- this.chromium = null;
33
- return null;
34
- }
35
  }
36
 
37
- async initBrowser() {
38
- if (this.isClosing) {
39
- console.log('浏览器正在关闭中,等待重新初始化...');
40
- this.browser = null;
41
- this.isClosing = false;
42
- }
43
-
44
- if (!this.browser) {
45
- try {
46
- console.log('初始化Puppeteer浏览器...');
47
-
48
- const launchOptions = {
49
- headless: 'new',
50
- timeout: 60000,
51
- protocolTimeout: 60000,
52
- args: [
53
- '--no-sandbox',
54
- '--disable-setuid-sandbox',
55
- '--disable-dev-shm-usage',
56
- '--disable-accelerated-2d-canvas',
57
- '--no-first-run',
58
- '--disable-gpu',
59
- '--disable-background-timer-throttling',
60
- '--disable-backgrounding-occluded-windows',
61
- '--disable-renderer-backgrounding',
62
- '--disable-features=TranslateUI',
63
- '--disable-extensions',
64
- '--hide-scrollbars',
65
- '--mute-audio',
66
- '--no-default-browser-check',
67
- '--disable-default-apps',
68
- '--disable-background-networking',
69
- '--disable-sync',
70
- '--metrics-recording-only',
71
- '--disable-domain-reliability',
72
- '--force-device-scale-factor=1',
73
- '--disable-features=VizDisplayCompositor',
74
- '--run-all-compositor-stages-before-draw',
75
- '--disable-new-content-rendering-timeout',
76
- '--disable-ipc-flooding-protection',
77
- '--disable-hang-monitor',
78
- '--disable-prompt-on-repost',
79
- '--memory-pressure-off',
80
- '--max_old_space_size=1024',
81
- '--disable-background-media-suspend',
82
- '--disable-backgrounding-occluded-windows',
83
- '--disable-renderer-backgrounding',
84
- '--disable-field-trial-config',
85
- '--disable-component-extensions-with-background-pages',
86
- '--disable-permissions-api',
87
- '--disable-client-side-phishing-detection',
88
- '--no-zygote',
89
- '--disable-web-security',
90
- '--allow-running-insecure-content',
91
- '--disable-features=VizDisplayCompositor,AudioServiceOutOfProcess',
92
- '--disable-software-rasterizer',
93
- '--disable-canvas-aa',
94
- '--disable-2d-canvas-clip-aa',
95
- '--disable-gl-drawing-for-tests',
96
- '--use-gl=swiftshader'
97
- ]
98
- };
99
-
100
- if (this.isHuggingFaceSpace) {
101
- console.log('检测到Hugging Face Space环境,使用单进程模式');
102
- launchOptions.args.push(
103
- '--single-process',
104
- '--disable-features=site-per-process',
105
- '--disable-site-isolation-trials',
106
- '--disable-features=BlockInsecurePrivateNetworkRequests'
107
- );
108
- }
109
-
110
- try {
111
- const chromePath = process.env.CHROME_BIN ||
112
- process.env.GOOGLE_CHROME_BIN ||
113
- '/usr/bin/google-chrome-stable' ||
114
- '/usr/bin/chromium-browser';
115
-
116
- if (chromePath && require('fs').existsSync(chromePath)) {
117
- launchOptions.executablePath = chromePath;
118
- console.log(`使用Chrome路径: ${chromePath}`);
119
- }
120
- } catch (pathError) {
121
- console.log('未找到自定义Chrome路径,使用默认配置');
122
- }
123
-
124
- this.browser = await puppeteer.launch(launchOptions);
125
- console.log('✅ Puppeteer浏览器初始化成功');
126
-
127
- this.browser.on('disconnected', () => {
128
- console.log('Puppeteer浏览器连接断开');
129
- this.browser = null;
130
- this.isClosing = false;
131
- });
132
-
133
- this.browserLaunchRetries = 0;
134
- } catch (error) {
135
- console.error('❌ Puppeteer浏览器初始化失败:', error.message);
136
- this.browserLaunchRetries++;
137
-
138
- if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
139
- console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
140
- await this.delay(3000);
141
- return this.initBrowser();
142
- } else {
143
- console.warn('⚠️ Puppeteer初始化完全失败,将使用fallback方法');
144
- return null;
145
- }
146
- }
147
- }
148
-
149
- if (this.browser) {
150
- try {
151
- await this.browser.version();
152
- console.log('✅ 浏览器连接正常');
153
- } catch (error) {
154
- console.warn('浏览器连接已断开,重新初始化:', error.message);
155
- this.browser = null;
156
- return this.initBrowser();
157
- }
158
- }
159
-
160
- return this.browser;
161
- }
162
-
163
- async initPlaywrightBrowser() {
164
- // 先尝试加载Playwright
165
- const chromium = await this.initPlaywright();
166
- if (!chromium) {
167
- console.warn('Playwright不可用,跳过初始化');
168
- return null;
169
- }
170
-
171
- if (this.playwrightBrowser) {
172
- try {
173
- const context = await this.playwrightBrowser.newContext();
174
- await context.close();
175
- return this.playwrightBrowser;
176
- } catch (error) {
177
- console.warn('Playwright浏览器连接已断开,重新初始化');
178
- this.playwrightBrowser = null;
179
- }
180
- }
181
-
182
  try {
183
- console.log('初始化Playwright浏览器...');
184
 
185
  const launchOptions = {
186
- headless: true,
187
- timeout: 30000,
188
  args: [
189
  '--no-sandbox',
190
  '--disable-setuid-sandbox',
191
  '--disable-dev-shm-usage',
192
  '--disable-gpu',
193
  '--disable-extensions',
194
- '--no-first-run',
195
  '--disable-background-timer-throttling',
 
196
  '--disable-renderer-backgrounding',
197
- '--disable-backgrounding-occluded-windows'
198
- ]
 
 
 
 
 
 
 
199
  };
200
 
201
- if (this.isHuggingFaceSpace) {
202
- console.log('检测到Hugging Face Space环境,使用Playwright优化配置');
203
  launchOptions.args.push(
204
  '--single-process',
205
- '--no-zygote',
206
- '--disable-web-security'
207
  );
208
  }
209
 
210
- this.playwrightBrowser = await chromium.launch(launchOptions);
211
- console.log('✅ Playwright浏览器初始化成功');
 
 
 
 
 
 
212
 
213
- return this.playwrightBrowser;
214
  } catch (error) {
215
- console.error('❌ Playwright浏览器初始化失败:', error.message);
216
- return null;
 
 
217
  }
218
  }
219
 
220
- async closeBrowser() {
221
- if (this.browser && !this.isClosing) {
222
- try {
223
- this.isClosing = true;
224
- console.log('正在关闭Puppeteer浏览器...');
225
- await this.browser.close();
226
- console.log('Puppeteer浏览器已关闭');
227
- } catch (error) {
228
- console.warn('关闭浏览器时出错:', error.message);
229
- } finally {
230
- this.browser = null;
231
- this.isClosing = false;
232
- }
 
 
 
 
 
 
 
 
 
233
  }
234
  }
235
 
236
- async closePlaywrightBrowser() {
237
- if (this.playwrightBrowser) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  try {
239
- console.log('正在关闭Playwright浏览器...');
240
- await this.playwrightBrowser.close();
241
- console.log('Playwright浏览器已关闭');
242
  } catch (error) {
243
- console.warn('关闭Playwright浏览器时出错:', error.message);
244
- } finally {
245
- this.playwrightBrowser = null;
246
  }
247
  }
248
- }
249
-
250
- delay(ms) {
251
- return new Promise(resolve => setTimeout(resolve, ms));
252
- }
253
 
254
- extractPPTDimensions(htmlContent) {
255
- try {
256
- const dimensionMatch = htmlContent.match(/window\.PPT_DIMENSIONS\s*=\s*{\s*width:\s*(\d+),\s*height:\s*(\d+)\s*}/);
257
- if (dimensionMatch) {
258
- return {
259
- width: parseInt(dimensionMatch[1]),
260
- height: parseInt(dimensionMatch[2])
261
- };
262
- }
263
 
264
- const viewportMatch = htmlContent.match(/width=(\d+),\s*height=(\d+)/);
265
- if (viewportMatch) {
266
- return {
267
- width: parseInt(viewportMatch[1]),
268
- height: parseInt(viewportMatch[2])
269
- };
270
  }
271
-
272
- return { width: 960, height: 720 };
273
- } catch (error) {
274
- console.warn('提取PPT尺寸失败,使用默认尺寸:', error.message);
275
- return { width: 960, height: 720 };
276
  }
277
- }
278
 
279
- generateFallbackImage(width, height, message = 'Screenshot not available') {
280
- console.log(`🔄 生成fallback图片: ${width}x${height}`);
281
-
282
- const svg = `
283
- <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
284
- <rect width="100%" height="100%" fill="#f8f9fa"/>
285
- <rect x="10" y="10" width="${width-20}" height="${height-20}"
286
- fill="none" stroke="#dee2e6" stroke-width="2" stroke-dasharray="10,5"/>
287
- <text x="50%" y="40%" text-anchor="middle" dominant-baseline="middle"
288
- font-family="Arial, sans-serif" font-size="28" fill="#495057" font-weight="bold">
289
- PPT 预览图
290
- </text>
291
- <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
292
- font-family="Arial, sans-serif" font-size="16" fill="#6c757d">
293
- ${message}
294
- </text>
295
- <text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
296
- font-family="Arial, sans-serif" font-size="14" fill="#adb5bd">
297
- 尺寸: ${width} × ${height}
298
- </text>
299
- <circle cx="50%" cy="70%" r="20" fill="none" stroke="#28a745" stroke-width="3">
300
- <animate attributeName="stroke-dasharray" values="0,126;126,126" dur="2s" repeatCount="indefinite"/>
301
- </circle>
302
- </svg>
303
- `;
304
-
305
- return Buffer.from(svg, 'utf8');
306
  }
307
 
308
- optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
309
- console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
310
-
311
- const optimizedHtml = htmlContent.replace(
312
- /(<head[^>]*>)/i,
313
- `$1
314
- <meta name="screenshot-mode" content="true">
315
- <style id="screenshot-precise-control">
316
- *, *::before, *::after {
317
- margin: 0 !important;
318
- padding: 0 !important;
319
- box-sizing: border-box !important;
320
- border: none !important;
321
- outline: none !important;
322
- }
323
-
324
- html {
325
- width: ${targetWidth}px !important;
326
- height: ${targetHeight}px !important;
327
- min-width: ${targetWidth}px !important;
328
- min-height: ${targetHeight}px !important;
329
- max-width: ${targetWidth}px !important;
330
- max-height: ${targetHeight}px !important;
331
- overflow: hidden !important;
332
- position: fixed !important;
333
- top: 0 !important;
334
- left: 0 !important;
335
- margin: 0 !important;
336
- padding: 0 !important;
337
- transform: none !important;
338
- transform-origin: top left !important;
339
- }
340
-
341
- body {
342
- width: ${targetWidth}px !important;
343
- height: ${targetHeight}px !important;
344
- min-width: ${targetWidth}px !important;
345
- min-height: ${targetHeight}px !important;
346
- max-width: ${targetWidth}px !important;
347
- max-height: ${targetHeight}px !important;
348
- overflow: hidden !important;
349
- position: fixed !important;
350
- top: 0 !important;
351
- left: 0 !important;
352
- margin: 0 !important;
353
- padding: 0 !important;
354
- transform: none !important;
355
- transform-origin: top left !important;
356
- }
357
-
358
- .slide-container {
359
- width: ${targetWidth}px !important;
360
- height: ${targetHeight}px !important;
361
- min-width: ${targetWidth}px !important;
362
- min-height: ${targetHeight}px !important;
363
- max-width: ${targetWidth}px !important;
364
- max-height: ${targetHeight}px !important;
365
- position: fixed !important;
366
- top: 0 !important;
367
- left: 0 !important;
368
- overflow: hidden !important;
369
- transform: none !important;
370
- transform-origin: top left !important;
371
- margin: 0 !important;
372
- padding: 0 !important;
373
- box-shadow: none !important;
374
- z-index: 1 !important;
375
- }
376
-
377
- html::-webkit-scrollbar,
378
- body::-webkit-scrollbar,
379
- *::-webkit-scrollbar {
380
- display: none !important;
381
- width: 0 !important;
382
- height: 0 !important;
383
- }
384
-
385
- html { scrollbar-width: none !important; }
386
-
387
- * {
388
- -webkit-user-select: none !important;
389
- -moz-user-select: none !important;
390
- -ms-user-select: none !important;
391
- user-select: none !important;
392
- pointer-events: none !important;
393
- }
394
- </style>`
395
- );
396
 
397
- return optimizedHtml;
398
- }
399
-
400
- async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) {
401
  try {
402
- console.log(`🎯 开始Puppeteer截图生成... (尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
403
-
404
- const browser = await this.initBrowser();
405
- if (!browser) {
406
- throw new Error('浏览器初始化失败');
407
- }
408
-
409
- const dimensions = this.extractPPTDimensions(htmlContent);
410
- console.log(`📐 检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`);
411
 
412
- const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
 
413
 
414
- let page = null;
415
- try {
416
- page = await browser.newPage();
417
- console.log('📄 创建新页面成功');
418
-
419
- page.setDefaultTimeout(20000);
420
- page.setDefaultNavigationTimeout(20000);
421
-
422
- await page.setViewport({
423
- width: dimensions.width,
424
- height: dimensions.height,
425
- deviceScaleFactor: 1,
426
- });
427
- console.log(`🖥️ 设置viewport: ${dimensions.width}x${dimensions.height}`);
428
-
429
- await page.setContent(optimizedHtml, {
430
- waitUntil: ['load', 'domcontentloaded'],
431
- timeout: 20000
432
- });
433
- console.log('📝 页面内容设置完成');
434
-
435
- await page.waitForTimeout(1000);
436
- console.log('⏱️ 等待渲染完成');
437
-
438
- console.log('📸 开始执行截图...');
439
- const screenshot = await page.screenshot({
440
- type: 'jpeg',
441
- quality: 85,
442
- clip: {
443
- x: 0,
444
- y: 0,
445
- width: dimensions.width,
446
- height: dimensions.height
447
- },
448
- omitBackground: false,
449
- captureBeyondViewport: false,
450
- });
451
-
452
- console.log(`✅ Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
453
- return screenshot;
454
-
455
- } finally {
456
- if (page) {
457
- try {
458
- await page.close();
459
- console.log('📄 页面已关闭');
460
- } catch (error) {
461
- console.warn('关闭页面时出错:', error.message);
462
- }
463
- }
464
- }
465
-
466
- } catch (error) {
467
- console.error(`❌ Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
468
-
469
- if (error.message.includes('Target closed') ||
470
- error.message.includes('Connection closed') ||
471
- error.message.includes('Protocol error')) {
472
- console.log('🔄 检测到浏览器连接问题,重置浏览器实例');
473
- this.browser = null;
474
- this.isClosing = false;
475
- }
476
 
477
- if (retryCount < this.maxRetries) {
478
- const waitTime = (retryCount + 1) * 2;
479
- console.log(`⏳ 等待 ${waitTime} 秒后重试...`);
480
- await this.delay(waitTime * 1000);
481
- return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
 
482
  }
483
-
484
- throw error;
485
  }
486
  }
487
 
488
- async generateScreenshotWithPlaywright(htmlContent, options = {}) {
489
- // 先尝试加载Playwright
490
- const chromium = await this.initPlaywright();
491
- if (!chromium) {
492
- throw new Error('Playwright不可用');
493
- }
494
 
495
  try {
496
- console.log('🎭 开始Playwright截图生成...');
497
 
498
- const browser = await this.initPlaywrightBrowser();
499
- if (!browser) {
500
- throw new Error('Playwright浏览器初始化失败');
501
- }
502
-
503
- const dimensions = this.extractPPTDimensions(htmlContent);
504
- console.log(`📐 Playwright检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`);
505
-
506
- const context = await browser.newContext({
507
- viewport: {
508
- width: dimensions.width,
509
- height: dimensions.height
510
- },
511
- deviceScaleFactor: 1,
512
- hasTouch: false,
513
- isMobile: false
514
- });
515
-
516
- const page = await context.newPage();
517
 
518
- await page.setContent(htmlContent, {
519
- waitUntil: 'domcontentloaded',
520
- timeout: 15000
 
521
  });
522
 
523
- await page.waitForTimeout(800);
524
-
525
- const screenshot = await page.screenshot({
526
- type: 'jpeg',
527
- quality: 90,
528
- clip: {
529
- x: 0,
530
- y: 0,
531
- width: dimensions.width,
532
- height: dimensions.height
533
- },
534
- animations: 'disabled'
535
  });
536
 
537
- await context.close();
 
 
 
 
 
 
 
 
538
 
539
- console.log(`✅ Playwright截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
540
  return screenshot;
541
-
542
- } catch (error) {
543
- console.error('❌ Playwright截图生成失败:', error.message);
544
- throw error;
545
- }
546
- }
547
 
548
- async generateScreenshot(htmlContent, options = {}) {
549
- console.log('🎯 开始生成截图...');
550
-
551
- if (this.preferredEngine === 'puppeteer') {
552
- try {
553
- console.log('🚀 尝试使用Puppeteer生成截图');
554
- const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options);
555
- console.log('✅ Puppeteer截图生成成功');
556
- return screenshot;
557
- } catch (puppeteerError) {
558
- console.warn('⚠️ Puppeteer截图失败:', puppeteerError.message);
559
-
560
- // 动态检查Playwright是否可用
561
- if (await this.initPlaywright()) {
562
- console.log('🎭 尝试使用Playwright作为备用方案...');
563
- try {
564
- const screenshot = await this.generateScreenshotWithPlaywright(htmlContent, options);
565
- console.log('✅ Playwright截图生成成功');
566
- this.preferredEngine = 'playwright';
567
- return screenshot;
568
- } catch (playwrightError) {
569
- console.warn('⚠️ Playwright截图也失败,使用fallback方法:', playwrightError.message);
570
- }
571
- } else {
572
- console.warn('⚠️ Playwright不可用,直接使用fallback方法');
573
- }
574
- }
575
- } else {
576
- // 如果偏好Playwright,先动态检查是否可用
577
- if (await this.initPlaywright()) {
578
- try {
579
- console.log('🎭 尝试使用Playwright生成截图');
580
- const screenshot = await this.generateScreenshotWithPlaywright(htmlContent, options);
581
- console.log('✅ Playwright截图生成成功');
582
- return screenshot;
583
- } catch (playwrightError) {
584
- console.warn('⚠️ Playwright截图失败,尝试Puppeteer:', playwrightError.message);
585
-
586
- try {
587
- const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options);
588
- console.log('✅ Puppeteer截图生成成功');
589
- return screenshot;
590
- } catch (puppeteerError) {
591
- console.warn('⚠️ Puppeteer截图也失败,使用fallback方法:', puppeteerError.message);
592
- }
593
- }
594
- } else {
595
- console.warn('⚠️ Playwright不可用,直接使用Puppeteer');
596
- try {
597
- const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options);
598
- console.log('✅ Puppeteer截图生成成功');
599
- return screenshot;
600
- } catch (puppeteerError) {
601
- console.warn('⚠️ Puppeteer截图也失败,使用fallback方法:', puppeteerError.message);
602
- }
603
  }
604
  }
605
-
606
- // 如果所有截图引擎都失败,生成fallback图片
607
- const dimensions = this.extractPPTDimensions(htmlContent);
608
- const fallbackImage = this.generateFallbackImage(
609
- dimensions.width,
610
- dimensions.height,
611
- '截图引擎不可用,显示占位图'
612
- );
613
-
614
- console.log(`📋 生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`);
615
- return fallbackImage;
616
  }
617
 
618
- async setPreferredEngine(engine) {
619
- if (['puppeteer', 'playwright'].includes(engine)) {
620
- // 检查所选引擎是否可用
621
- if (engine === 'playwright') {
622
- const chromium = await this.initPlaywright();
623
- if (!chromium) {
624
- console.warn('Playwright不可用,保持使用Puppeteer');
625
- return;
626
- }
627
- }
628
-
629
- this.preferredEngine = engine;
630
- console.log(`截图引擎偏好设置为: ${engine}`);
631
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  }
633
 
634
- async captureScreenshot(htmlContent, width, height, options = {}) {
635
- console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot');
636
- return this.generateScreenshot(htmlContent, options);
 
 
 
 
 
637
  }
638
 
 
639
  async cleanup() {
640
- await Promise.all([
641
- this.closeBrowser(),
642
- this.closePlaywrightBrowser()
643
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  }
645
  }
646
 
647
- const screenshotService = new ScreenshotService();
648
-
649
- process.on('exit', async () => {
650
- await screenshotService.cleanup();
651
- });
652
-
653
- process.on('SIGINT', async () => {
654
- await screenshotService.cleanup();
655
- process.exit(0);
656
- });
657
-
658
- process.on('SIGTERM', async () => {
659
- await screenshotService.cleanup();
660
- process.exit(0);
661
- });
662
-
663
- export default screenshotService;
 
1
  import puppeteer from 'puppeteer';
2
+ import playwright from 'playwright';
3
 
4
  class ScreenshotService {
5
  constructor() {
6
+ this.puppeteerBrowser = null;
7
+ this.playwrightBrowser = null;
8
+ this.isInitializing = false;
9
+ this.puppeteerReady = false;
10
+ this.playwrightReady = false;
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ console.log('Screenshot service initialized');
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
+ // Initialize Puppeteer browser
16
+ async initPuppeteer() {
17
+ if (this.puppeteerBrowser || this.isInitializing) return;
18
+
19
+ this.isInitializing = true;
20
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try {
22
+ console.log('🚀 Starting Puppeteer browser...');
23
 
24
  const launchOptions = {
25
+ headless: 'new',
 
26
  args: [
27
  '--no-sandbox',
28
  '--disable-setuid-sandbox',
29
  '--disable-dev-shm-usage',
30
  '--disable-gpu',
31
  '--disable-extensions',
 
32
  '--disable-background-timer-throttling',
33
+ '--disable-backgrounding-occluded-windows',
34
  '--disable-renderer-backgrounding',
35
+ '--no-first-run',
36
+ '--no-default-browser-check',
37
+ '--disable-default-apps',
38
+ '--disable-features=TranslateUI',
39
+ '--disable-ipc-flooding-protection',
40
+ '--memory-pressure-off',
41
+ '--max_old_space_size=4096'
42
+ ],
43
+ timeout: 30000
44
  };
45
 
46
+ // Hugging Face Spaces optimizations
47
+ if (process.env.SPACE_ID) {
48
  launchOptions.args.push(
49
  '--single-process',
50
+ '--disable-background-networking',
51
+ '--disable-background-mode'
52
  );
53
  }
54
 
55
+ this.puppeteerBrowser = await puppeteer.launch(launchOptions);
56
+ this.puppeteerReady = true;
57
+ console.log('✅ Puppeteer browser started successfully');
58
+
59
+ // Cleanup when process exits
60
+ process.on('exit', () => this.cleanup());
61
+ process.on('SIGINT', () => this.cleanup());
62
+ process.on('SIGTERM', () => this.cleanup());
63
 
 
64
  } catch (error) {
65
+ console.error('❌ Puppeteer initialization failed:', error.message);
66
+ this.puppeteerReady = false;
67
+ } finally {
68
+ this.isInitializing = false;
69
  }
70
  }
71
 
72
+ // Initialize Playwright browser (fallback)
73
+ async initPlaywright() {
74
+ if (this.playwrightBrowser) return;
75
+
76
+ try {
77
+ console.log('🎭 Starting Playwright browser...');
78
+
79
+ this.playwrightBrowser = await playwright.chromium.launch({
80
+ headless: true,
81
+ args: [
82
+ '--no-sandbox',
83
+ '--disable-setuid-sandbox',
84
+ '--disable-dev-shm-usage'
85
+ ]
86
+ });
87
+
88
+ this.playwrightReady = true;
89
+ console.log('✅ Playwright browser started successfully');
90
+
91
+ } catch (error) {
92
+ console.error('❌ Playwright initialization failed:', error.message);
93
+ this.playwrightReady = false;
94
  }
95
  }
96
 
97
+ // Generate screenshot
98
+ async generateScreenshot(htmlContent, options = {}) {
99
+ const {
100
+ format = 'jpeg',
101
+ quality = 90,
102
+ width = 1000,
103
+ height = 562,
104
+ timeout = 15000
105
+ } = options;
106
+
107
+ console.log(`📸 Generating screenshot: ${width}x${height}, ${format}@${quality}%`);
108
+
109
+ // Try Puppeteer first
110
+ if (!this.puppeteerReady) {
111
+ await this.initPuppeteer();
112
+ }
113
+
114
+ if (this.puppeteerReady) {
115
  try {
116
+ return await this.generateWithPuppeteer(htmlContent, { format, quality, width, height, timeout });
 
 
117
  } catch (error) {
118
+ console.warn('Puppeteer screenshot failed, trying Playwright:', error.message);
 
 
119
  }
120
  }
 
 
 
 
 
121
 
122
+ // Fallback to Playwright
123
+ if (!this.playwrightReady) {
124
+ await this.initPlaywright();
125
+ }
 
 
 
 
 
126
 
127
+ if (this.playwrightReady) {
128
+ try {
129
+ return await this.generateWithPlaywright(htmlContent, { format, quality, width, height, timeout });
130
+ } catch (error) {
131
+ console.warn('Playwright screenshot failed:', error.message);
 
132
  }
 
 
 
 
 
133
  }
 
134
 
135
+ // Final fallback: generate SVG
136
+ console.warn('All screenshot methods failed, generating fallback SVG');
137
+ return this.generateFallbackImage(width, height, 'Screenshot Service', 'Browser unavailable');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
 
140
+ // Generate screenshot using Puppeteer
141
+ async generateWithPuppeteer(htmlContent, options) {
142
+ const { format, quality, width, height, timeout } = options;
143
+ let page = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
 
 
 
 
145
  try {
146
+ page = await this.puppeteerBrowser.newPage();
 
 
 
 
 
 
 
 
147
 
148
+ // Set viewport
149
+ await page.setViewport({ width, height });
150
 
151
+ // Set content
152
+ await page.setContent(htmlContent, {
153
+ waitUntil: 'networkidle0',
154
+ timeout: timeout
155
+ });
156
+
157
+ // Wait for fonts
158
+ await page.evaluate(() => {
159
+ return document.fonts ? document.fonts.ready : Promise.resolve();
160
+ });
161
+
162
+ // Additional wait for rendering
163
+ await page.waitForTimeout(300);
164
+
165
+ // Take screenshot
166
+ const screenshotOptions = {
167
+ type: format,
168
+ quality: format === 'jpeg' ? quality : undefined,
169
+ fullPage: false,
170
+ clip: { x: 0, y: 0, width, height }
171
+ };
172
+
173
+ const screenshot = await page.screenshot(screenshotOptions);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ console.log(`✅ Puppeteer screenshot generated: ${screenshot.length} bytes`);
176
+ return screenshot;
177
+
178
+ } finally {
179
+ if (page) {
180
+ await page.close().catch(console.error);
181
  }
 
 
182
  }
183
  }
184
 
185
+ // Generate screenshot using Playwright
186
+ async generateWithPlaywright(htmlContent, options) {
187
+ const { format, quality, width, height, timeout } = options;
188
+ let page = null;
 
 
189
 
190
  try {
191
+ page = await this.playwrightBrowser.newPage();
192
 
193
+ // Set viewport
194
+ await page.setViewportSize({ width, height });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
+ // Set content
197
+ await page.setContent(htmlContent, {
198
+ waitUntil: 'networkidle',
199
+ timeout: timeout
200
  });
201
 
202
+ // Wait for fonts
203
+ await page.evaluate(() => {
204
+ return document.fonts ? document.fonts.ready : Promise.resolve();
 
 
 
 
 
 
 
 
 
205
  });
206
 
207
+ // Take screenshot
208
+ const screenshotOptions = {
209
+ type: format,
210
+ quality: format === 'jpeg' ? quality : undefined,
211
+ fullPage: false,
212
+ clip: { x: 0, y: 0, width, height }
213
+ };
214
+
215
+ const screenshot = await page.screenshot(screenshotOptions);
216
 
217
+ console.log(`✅ Playwright screenshot generated: ${screenshot.length} bytes`);
218
  return screenshot;
 
 
 
 
 
 
219
 
220
+ } finally {
221
+ if (page) {
222
+ await page.close().catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
  }
 
 
 
 
 
 
 
 
 
 
 
225
  }
226
 
227
+ // Generate fallback SVG image
228
+ generateFallbackImage(width = 1000, height = 562, title = 'PPT Screenshot', subtitle = '', message = '') {
229
+ const svg = `<?xml version="1.0" encoding="UTF-8"?>
230
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
231
+ <defs>
232
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
233
+ <stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
234
+ <stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
235
+ </linearGradient>
236
+ <pattern id="dots" patternUnits="userSpaceOnUse" width="20" height="20">
237
+ <circle cx="2" cy="2" r="1" fill="#dee2e6" opacity="0.5"/>
238
+ </pattern>
239
+ </defs>
240
+
241
+ <!-- Background -->
242
+ <rect width="100%" height="100%" fill="url(#bg)"/>
243
+ <rect width="100%" height="100%" fill="url(#dots)"/>
244
+
245
+ <!-- Border -->
246
+ <rect x="20" y="20" width="${width-40}" height="${height-40}"
247
+ fill="none" stroke="#dee2e6" stroke-width="3"
248
+ stroke-dasharray="15,10" rx="10"/>
249
+
250
+ <!-- Icon -->
251
+ <g transform="translate(${width/2}, ${height*0.25})">
252
+ <circle cx="0" cy="0" r="30" fill="#6c757d" opacity="0.3"/>
253
+ <rect x="-15" y="-10" width="30" height="20" rx="3" fill="#6c757d"/>
254
+ <rect x="-12" y="-7" width="24" height="3" fill="white"/>
255
+ <rect x="-12" y="-2" width="24" height="3" fill="white"/>
256
+ <rect x="-12" y="3" width="16" height="3" fill="white"/>
257
+ </g>
258
+
259
+ <!-- Title -->
260
+ <text x="${width/2}" y="${height*0.45}"
261
+ text-anchor="middle"
262
+ font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
263
+ font-size="28"
264
+ font-weight="bold"
265
+ fill="#495057">${title}</text>
266
+
267
+ ${subtitle ? `
268
+ <!-- Subtitle -->
269
+ <text x="${width/2}" y="${height*0.55}"
270
+ text-anchor="middle"
271
+ font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
272
+ font-size="18"
273
+ fill="#6c757d">${subtitle}</text>
274
+ ` : ''}
275
+
276
+ ${message ? `
277
+ <!-- Message -->
278
+ <text x="${width/2}" y="${height*0.65}"
279
+ text-anchor="middle"
280
+ font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
281
+ font-size="14"
282
+ fill="#adb5bd">${message}</text>
283
+ ` : ''}
284
+
285
+ <!-- Footer -->
286
+ <text x="${width/2}" y="${height*0.85}"
287
+ text-anchor="middle"
288
+ font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
289
+ font-size="12"
290
+ fill="#ced4da">PPT Screenshot Service • ${new Date().toLocaleString('zh-CN')}</text>
291
+
292
+ <!-- Dimensions info -->
293
+ <text x="${width/2}" y="${height*0.92}"
294
+ text-anchor="middle"
295
+ font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
296
+ font-size="10"
297
+ fill="#ced4da">Size: ${width} × ${height}</text>
298
+ </svg>`;
299
+
300
+ return Buffer.from(svg, 'utf-8');
301
  }
302
 
303
+ // Get service status
304
+ getStatus() {
305
+ return {
306
+ puppeteerReady: this.puppeteerReady,
307
+ playwrightReady: this.playwrightReady,
308
+ environment: process.env.NODE_ENV || 'development',
309
+ isHuggingFace: !!process.env.SPACE_ID
310
+ };
311
  }
312
 
313
+ // Cleanup resources
314
  async cleanup() {
315
+ console.log('🧹 Cleaning up screenshot service...');
316
+
317
+ if (this.puppeteerBrowser) {
318
+ try {
319
+ await this.puppeteerBrowser.close();
320
+ this.puppeteerBrowser = null;
321
+ this.puppeteerReady = false;
322
+ console.log('✅ Puppeteer browser closed');
323
+ } catch (error) {
324
+ console.error('Error closing Puppeteer browser:', error);
325
+ }
326
+ }
327
+
328
+ if (this.playwrightBrowser) {
329
+ try {
330
+ await this.playwrightBrowser.close();
331
+ this.playwrightBrowser = null;
332
+ this.playwrightReady = false;
333
+ console.log('✅ Playwright browser closed');
334
+ } catch (error) {
335
+ console.error('Error closing Playwright browser:', error);
336
+ }
337
+ }
338
  }
339
  }
340
 
341
+ export default new ScreenshotService();