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();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|