Upload 11 files
Browse files- backend/package.json +2 -1
- backend/src/app.js +627 -11
- backend/src/middleware/errorHandler.js +72 -6
- backend/src/routes/ppt.js +404 -91
- backend/src/routes/public.js +320 -715
- backend/src/services/githubService.js +1203 -296
- backend/src/services/screenshotService.js +265 -587
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 |
-
|
|
|
|
|
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 |
-
// 前端路由处理 -
|
543 |
app.get('*', (req, res) => {
|
544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
if (status === 404) {
|
23 |
-
return res.status(404).json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
|
26 |
-
return res.status(500).json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
}
|
28 |
|
29 |
// 默认错误
|
|
|
|
|
30 |
res.status(500).json({
|
31 |
-
error:
|
32 |
-
|
33 |
-
|
|
|
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
|
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 |
-
|
41 |
-
|
42 |
|
43 |
let pptData = null;
|
|
|
44 |
|
45 |
-
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
try {
|
49 |
-
|
50 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
const pptData = {
|
88 |
id: pptId,
|
89 |
title: title || '未命名演示文稿',
|
90 |
-
slides: slides,
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
createdAt: new Date().toISOString(),
|
96 |
updatedAt: new Date().toISOString()
|
97 |
};
|
98 |
|
99 |
-
|
|
|
|
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
170 |
|
171 |
-
|
172 |
-
await
|
173 |
-
|
174 |
-
|
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 |
-
|
213 |
-
|
214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
try {
|
216 |
-
await
|
|
|
|
|
217 |
} catch (error) {
|
218 |
// 继续尝试其他仓库
|
219 |
continue;
|
220 |
}
|
221 |
}
|
222 |
-
|
223 |
-
|
224 |
-
|
|
|
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 |
-
|
245 |
-
|
|
|
|
|
|
|
|
|
|
|
246 |
try {
|
247 |
-
const result = await
|
248 |
if (result) {
|
249 |
-
sourcePPT = result
|
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 (
|
280 |
-
await
|
281 |
} else {
|
282 |
-
await
|
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 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
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 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
}
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
|
|
|
|
|
|
|
|
|
|
167 |
}
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
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 |
-
|
350 |
-
|
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 |
-
//
|
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 |
-
//
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
break;
|
502 |
-
}
|
503 |
-
} catch (error) {
|
504 |
-
continue;
|
505 |
}
|
506 |
-
}
|
507 |
-
|
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 |
-
//
|
563 |
-
const htmlContent = generateSlideHTML(pptData, slideIdx,
|
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
|
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 |
-
//
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
break;
|
588 |
-
}
|
589 |
-
} catch (error) {
|
590 |
-
continue;
|
591 |
}
|
592 |
-
}
|
593 |
-
|
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 |
-
//
|
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 |
-
//
|
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 |
-
//
|
635 |
-
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
break;
|
642 |
-
}
|
643 |
-
} catch (error) {
|
644 |
-
continue;
|
645 |
}
|
646 |
-
}
|
647 |
-
|
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 |
-
//
|
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 |
-
//
|
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 |
-
//
|
682 |
const fileName = `${pptId}.json`;
|
683 |
-
const storageService = getStorageService();
|
684 |
let pptExists = false;
|
685 |
let pptData = null;
|
686 |
|
687 |
-
console.log(`
|
688 |
|
689 |
-
|
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 |
-
|
|
|
713 |
if (result) {
|
714 |
pptExists = true;
|
715 |
pptData = result.content;
|
716 |
-
console.log(
|
|
|
717 |
}
|
718 |
} catch (error) {
|
719 |
-
console.log(`PPT not found in
|
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:
|
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 |
-
//
|
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 |
-
//
|
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 |
-
//
|
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
|
|
|
|
|
|
|
|
|
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 |
-
//
|
793 |
-
|
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
|
812 |
if (result) {
|
813 |
pptData = result.content;
|
814 |
-
|
815 |
}
|
816 |
} catch (error) {
|
817 |
-
|
818 |
}
|
819 |
}
|
820 |
|
821 |
-
|
822 |
-
|
823 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
824 |
try {
|
825 |
-
const
|
826 |
-
if (
|
827 |
-
pptData =
|
828 |
-
|
829 |
}
|
830 |
-
} catch (
|
831 |
-
|
832 |
}
|
833 |
}
|
834 |
|
835 |
if (!pptData) {
|
836 |
-
|
837 |
-
|
838 |
-
|
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 |
-
|
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 |
-
|
856 |
-
|
857 |
-
|
858 |
-
|
859 |
-
|
860 |
-
// 生成截图
|
861 |
-
const screenshot = await screenshotService.generateScreenshot(htmlContent, {
|
862 |
-
format: 'jpeg',
|
863 |
-
quality: 90
|
864 |
});
|
865 |
|
866 |
-
|
|
|
|
|
|
|
867 |
|
868 |
-
|
869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
870 |
|
871 |
-
|
872 |
-
|
873 |
-
|
874 |
-
|
875 |
-
|
876 |
-
|
877 |
-
|
878 |
-
|
879 |
-
|
880 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
881 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
882 |
|
883 |
-
res.
|
|
|
884 |
|
885 |
} catch (error) {
|
886 |
-
|
887 |
-
|
888 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
889 |
try {
|
890 |
-
|
891 |
-
|
892 |
-
|
893 |
-
|
894 |
-
|
895 |
-
|
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 |
-
|
8 |
-
this.
|
9 |
-
this.
|
10 |
-
this.
|
|
|
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 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
}
|
22 |
|
23 |
// 验证GitHub连接
|
24 |
async validateConnection() {
|
|
|
|
|
25 |
if (this.useMemoryStorage) {
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
}
|
28 |
|
29 |
try {
|
30 |
// 测试GitHub API连接
|
31 |
-
const response = await
|
32 |
-
|
33 |
-
|
34 |
-
|
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
|
48 |
-
|
49 |
-
|
50 |
-
|
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 |
-
|
|
|
113 |
}
|
114 |
|
|
|
|
|
|
|
115 |
try {
|
116 |
-
|
117 |
-
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
118 |
-
|
119 |
-
console.log(`Initializing empty repository: ${owner}/${repo}`);
|
120 |
|
121 |
// 创建初始README文件
|
122 |
-
const readmeContent =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
|
|
|
|
|
|
|
|
|
127 |
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
|
141 |
-
|
142 |
-
|
143 |
-
|
|
|
|
|
|
|
|
|
144 |
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
}
|
157 |
-
|
158 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
|
160 |
-
console.log(
|
161 |
-
return
|
162 |
} catch (error) {
|
163 |
-
|
164 |
-
|
|
|
|
|
|
|
|
|
165 |
}
|
166 |
}
|
167 |
|
168 |
-
//
|
169 |
-
async
|
170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
if (this.useMemoryStorage) {
|
172 |
-
return await
|
173 |
}
|
174 |
|
175 |
-
// 原有的GitHub逻辑
|
176 |
try {
|
|
|
|
|
177 |
const repoUrl = this.repositories[repoIndex];
|
178 |
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
179 |
-
const
|
180 |
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
|
|
187 |
}
|
188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
189 |
);
|
190 |
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
} catch (error) {
|
197 |
if (error.response?.status === 404) {
|
|
|
198 |
return null;
|
199 |
}
|
200 |
-
|
|
|
201 |
}
|
202 |
}
|
203 |
|
204 |
-
//
|
205 |
-
async
|
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 |
-
|
215 |
-
|
216 |
-
console.log(`Attempting to save file: ${path} to repo: ${owner}/${repo}`);
|
217 |
-
|
218 |
-
// 先尝试获取现有文件的SHA
|
219 |
-
let sha = null;
|
220 |
try {
|
221 |
-
const
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
226 |
} catch (error) {
|
227 |
-
|
|
|
|
|
|
|
228 |
}
|
|
|
229 |
|
230 |
-
|
231 |
-
|
232 |
-
const
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
}
|
237 |
-
|
238 |
-
|
239 |
-
|
|
|
240 |
}
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
|
|
|
|
248 |
{
|
249 |
headers: {
|
250 |
'Authorization': `token ${this.token}`,
|
251 |
'Accept': 'application/vnd.github.v3+json'
|
252 |
-
}
|
|
|
253 |
}
|
254 |
);
|
|
|
|
|
|
|
|
|
|
|
255 |
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
|
|
|
|
|
|
260 |
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
}
|
274 |
-
}
|
275 |
|
276 |
-
|
277 |
-
|
278 |
-
return
|
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 |
-
|
|
|
283 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
}
|
285 |
}
|
286 |
|
287 |
-
//
|
288 |
-
async
|
289 |
-
|
290 |
-
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
291 |
-
const path = `users/${userId}/${fileName}`;
|
292 |
|
293 |
-
|
|
|
294 |
|
295 |
try {
|
296 |
-
|
297 |
-
const
|
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 |
-
//
|
306 |
-
|
307 |
-
await axios.get(
|
308 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
}
|
337 |
|
338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
339 |
try {
|
340 |
-
await
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
|
350 |
-
//
|
351 |
-
|
352 |
-
|
|
|
353 |
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
{
|
362 |
headers: {
|
363 |
'Authorization': `token ${this.token}`,
|
364 |
'Accept': 'application/vnd.github.v3+json'
|
365 |
-
}
|
|
|
366 |
}
|
367 |
);
|
368 |
-
|
|
|
|
|
369 |
}
|
370 |
-
|
371 |
-
//
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
{
|
380 |
headers: {
|
381 |
'Authorization': `token ${this.token}`,
|
382 |
'Accept': 'application/vnd.github.v3+json'
|
383 |
-
}
|
|
|
384 |
}
|
385 |
);
|
|
|
|
|
|
|
|
|
|
|
386 |
|
387 |
-
|
388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
|
390 |
-
|
391 |
-
|
|
|
392 |
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
405 |
}
|
406 |
}
|
|
|
|
|
|
|
407 |
}
|
408 |
|
409 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
async getUserPPTList(userId) {
|
411 |
-
|
|
|
412 |
if (this.useMemoryStorage) {
|
413 |
-
return await
|
414 |
}
|
415 |
|
416 |
-
|
417 |
-
const results = [];
|
418 |
|
419 |
-
|
|
|
420 |
try {
|
421 |
-
const repoUrl = this.repositories[
|
422 |
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
423 |
-
const
|
424 |
|
425 |
-
const response = await
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
|
|
|
|
|
|
431 |
}
|
432 |
-
|
|
|
|
|
|
|
|
|
|
|
433 |
);
|
434 |
|
435 |
-
const
|
436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
437 |
|
438 |
-
|
439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
440 |
try {
|
441 |
const pptId = file.name.replace('.json', '');
|
442 |
-
const fileContent = await this.getFile(userId, file.name, i);
|
443 |
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
title: fileContent.content.title || '未命名演示文稿',
|
448 |
-
lastModified: fileContent.content.updatedAt || fileContent.content.createdAt,
|
449 |
-
repoIndex: i,
|
450 |
-
repoUrl: repoUrl
|
451 |
-
});
|
452 |
}
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
462 |
});
|
|
|
|
|
463 |
}
|
464 |
}
|
465 |
} catch (error) {
|
466 |
-
|
467 |
-
|
468 |
-
}
|
469 |
}
|
470 |
}
|
471 |
|
472 |
-
|
|
|
473 |
}
|
474 |
|
475 |
-
//
|
476 |
-
async
|
477 |
-
|
|
|
478 |
if (this.useMemoryStorage) {
|
479 |
-
return await
|
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
|
491 |
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
503 |
}
|
504 |
}
|
505 |
-
);
|
506 |
|
507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
6 |
-
this.playwrightBrowser = null;
|
7 |
-
this.
|
8 |
-
this.
|
9 |
-
this.
|
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 |
-
|
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 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
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('
|
184 |
|
185 |
const launchOptions = {
|
186 |
-
headless:
|
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 |
-
'--
|
198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
};
|
200 |
|
201 |
-
|
202 |
-
|
203 |
launchOptions.args.push(
|
204 |
'--single-process',
|
205 |
-
'--
|
206 |
-
'--disable-
|
207 |
);
|
208 |
}
|
209 |
|
210 |
-
this.
|
211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
|
213 |
-
return this.playwrightBrowser;
|
214 |
} catch (error) {
|
215 |
-
console.error('❌
|
216 |
-
|
|
|
|
|
217 |
}
|
218 |
}
|
219 |
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
}
|
234 |
}
|
235 |
|
236 |
-
|
237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
238 |
try {
|
239 |
-
|
240 |
-
await this.playwrightBrowser.close();
|
241 |
-
console.log('Playwright浏览器已关闭');
|
242 |
} catch (error) {
|
243 |
-
console.warn('
|
244 |
-
} finally {
|
245 |
-
this.playwrightBrowser = null;
|
246 |
}
|
247 |
}
|
248 |
-
}
|
249 |
-
|
250 |
-
delay(ms) {
|
251 |
-
return new Promise(resolve => setTimeout(resolve, ms));
|
252 |
-
}
|
253 |
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
return {
|
259 |
-
width: parseInt(dimensionMatch[1]),
|
260 |
-
height: parseInt(dimensionMatch[2])
|
261 |
-
};
|
262 |
-
}
|
263 |
|
264 |
-
|
265 |
-
|
266 |
-
return {
|
267 |
-
|
268 |
-
|
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 |
-
|
280 |
-
console.
|
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 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
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 |
-
|
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 |
-
|
|
|
413 |
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
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 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
|
|
482 |
}
|
483 |
-
|
484 |
-
throw error;
|
485 |
}
|
486 |
}
|
487 |
|
488 |
-
|
489 |
-
|
490 |
-
const
|
491 |
-
|
492 |
-
throw new Error('Playwright不可用');
|
493 |
-
}
|
494 |
|
495 |
try {
|
496 |
-
|
497 |
|
498 |
-
|
499 |
-
|
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 |
-
|
519 |
-
|
520 |
-
|
|
|
521 |
});
|
522 |
|
523 |
-
|
524 |
-
|
525 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
538 |
|
539 |
-
console.log(`✅ Playwright
|
540 |
return screenshot;
|
541 |
-
|
542 |
-
} catch (error) {
|
543 |
-
console.error('❌ Playwright截图生成失败:', error.message);
|
544 |
-
throw error;
|
545 |
-
}
|
546 |
-
}
|
547 |
|
548 |
-
|
549 |
-
|
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 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
|
625 |
-
|
626 |
-
|
627 |
-
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
632 |
}
|
633 |
|
634 |
-
|
635 |
-
|
636 |
-
return
|
|
|
|
|
|
|
|
|
|
|
637 |
}
|
638 |
|
|
|
639 |
async cleanup() {
|
640 |
-
|
641 |
-
|
642 |
-
|
643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
}
|
645 |
}
|
646 |
|
647 |
-
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|