mirror of
https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption.git
synced 2026-10-25 17:36:33 +08:00
Merge pull request #5 from Johnserf-Seed/fix/concurrent-decrypt-and-page-recovery
fix: 修复页面关闭错误并添加并发解密支持
This commit is contained in:
2 files changed
+526
-81
No files matched your search
@@ -16,6 +16,7 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- POOL_SIZE=3
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
+525
-81
@@ -34,8 +34,321 @@ app.use('/wechat_files', express.static(path.join(__dirname, 'wechat_files')));
|
||||
let browser = null;
|
||||
let page = null;
|
||||
let server = null;
|
||||
let isProcessing = false; // 请求锁
|
||||
let requestQueue = []; // 请求队列
|
||||
const getWorkerUrl = () => `http://localhost:${PORT}/worker.html`;
|
||||
|
||||
// 页面池配置
|
||||
const POOL_SIZE = parseInt(process.env.POOL_SIZE) || 3; // 默认3个并发页面
|
||||
let pagePool = null;
|
||||
|
||||
/**
|
||||
* 页面池管理器 - 支持并发处理
|
||||
*/
|
||||
class PagePool {
|
||||
constructor(browser, size) {
|
||||
this.browser = browser;
|
||||
this.size = size;
|
||||
this.pages = []; // 所有页面
|
||||
this.available = []; // 可用页面队列
|
||||
this.waiting = []; // 等待获取页面的请求
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化页面池
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.initializing) return;
|
||||
this.initializing = true;
|
||||
|
||||
console.log(`🏊 初始化页面池 (大小: ${this.size})...`);
|
||||
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
try {
|
||||
const pg = await this._createPage(i);
|
||||
this.pages.push(pg);
|
||||
this.available.push(pg);
|
||||
console.log(` ✅ 页面 ${i + 1}/${this.size} 已就绪`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ 页面 ${i + 1}/${this.size} 创建失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🏊 页面池初始化完成,可用页面: ${this.available.length}/${this.size}`);
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个页面
|
||||
*/
|
||||
async _createPage(index) {
|
||||
const pg = await this.browser.newPage();
|
||||
pg._poolIndex = index;
|
||||
|
||||
await pg.goto(getWorkerUrl());
|
||||
|
||||
// 等待 WASM 加载
|
||||
await pg.waitForFunction(
|
||||
() => typeof Module !== 'undefined' && typeof Module.WxIsaac64 !== 'undefined',
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
|
||||
return pg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个可用页面(如果没有则等待)
|
||||
*/
|
||||
async acquire(timeout = 300000) {
|
||||
// 如果有可用页面,直接返回
|
||||
if (this.available.length > 0) {
|
||||
const pg = this.available.shift();
|
||||
console.log(`🔓 获取页面 #${pg._poolIndex},剩余可用: ${this.available.length}`);
|
||||
return pg;
|
||||
}
|
||||
|
||||
// 没有可用页面,加入等待队列
|
||||
console.log(`⏳ 页面池已满,等待中... (等待队列: ${this.waiting.length + 1})`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
const index = this.waiting.findIndex(w => w.resolve === resolve);
|
||||
if (index > -1) {
|
||||
this.waiting.splice(index, 1);
|
||||
}
|
||||
reject(new Error(`获取页面超时 (${timeout}ms),请稍后重试`));
|
||||
}, timeout);
|
||||
|
||||
this.waiting.push({
|
||||
resolve: (pg) => {
|
||||
clearTimeout(timer);
|
||||
resolve(pg);
|
||||
},
|
||||
reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 归还页面到池中
|
||||
*/
|
||||
release(pg) {
|
||||
// 检查页面是否有效
|
||||
if (!pg || pg.isClosed()) {
|
||||
console.log(`⚠️ 页面 #${pg?._poolIndex} 已关闭,将重建`);
|
||||
this._rebuildPage(pg?._poolIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有等待的请求,直接给它
|
||||
if (this.waiting.length > 0) {
|
||||
const waiter = this.waiting.shift();
|
||||
console.log(`🔄 页面 #${pg._poolIndex} 直接分配给等待的请求`);
|
||||
waiter.resolve(pg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则放回可用队列
|
||||
this.available.push(pg);
|
||||
console.log(`🔙 归还页面 #${pg._poolIndex},可用: ${this.available.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建损坏的页面
|
||||
*/
|
||||
async _rebuildPage(index) {
|
||||
if (index === undefined) return;
|
||||
|
||||
try {
|
||||
console.log(`🔄 重建页面 #${index}...`);
|
||||
const pg = await this._createPage(index);
|
||||
this.pages[index] = pg;
|
||||
|
||||
// 如果有等待的请求,直接给它
|
||||
if (this.waiting.length > 0) {
|
||||
const waiter = this.waiting.shift();
|
||||
waiter.resolve(pg);
|
||||
} else {
|
||||
this.available.push(pg);
|
||||
}
|
||||
console.log(`✅ 页面 #${index} 重建完成`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 页面 #${index} 重建失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取池状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
total: this.size,
|
||||
available: this.available.length,
|
||||
inUse: this.size - this.available.length,
|
||||
waiting: this.waiting.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有页面
|
||||
*/
|
||||
async close() {
|
||||
for (const pg of this.pages) {
|
||||
try {
|
||||
if (pg && !pg.isClosed()) {
|
||||
await pg.close();
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
}
|
||||
this.pages = [];
|
||||
this.available = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用页面池执行任务
|
||||
*/
|
||||
async function withPoolPage(fn) {
|
||||
if (!pagePool) {
|
||||
throw new Error('页面池未初始化');
|
||||
}
|
||||
|
||||
const pg = await pagePool.acquire();
|
||||
try {
|
||||
return await fn(pg);
|
||||
} catch (error) {
|
||||
// 如果是页面关闭错误,标记页面需要重建
|
||||
if (error.message && error.message.includes('closed')) {
|
||||
console.log(`⚠️ 页面 #${pg._poolIndex} 执行出错,将重建`);
|
||||
pagePool._rebuildPage(pg._poolIndex);
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
pagePool.release(pg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时的 page.evaluate (用于页面池)
|
||||
*/
|
||||
async function evaluateWithTimeoutOnPage(pg, pageFunction, arg, timeout = 30000) {
|
||||
return Promise.race([
|
||||
pg.evaluate(pageFunction, arg),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`操作超时 (${timeout}ms)`)), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页面是否健康可用
|
||||
*/
|
||||
async function isPageHealthy() {
|
||||
if (!browser || !page) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 尝试执行简单操作来检查页面是否有效(带5秒超时)
|
||||
await Promise.race([
|
||||
page.evaluate(() => true),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('健康检查超时')), 5000)
|
||||
)
|
||||
]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('⚠️ 页面健康检查失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置浏览器和页面
|
||||
*/
|
||||
async function resetBrowser() {
|
||||
console.log('🔄 重置浏览器...');
|
||||
|
||||
// 先尝试关闭旧的浏览器
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
}
|
||||
|
||||
browser = null;
|
||||
page = null;
|
||||
|
||||
// 重新初始化
|
||||
await initBrowser();
|
||||
console.log('✅ 浏览器重置完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保页面可用,如果不可用则重置
|
||||
*/
|
||||
async function ensurePageReady() {
|
||||
if (!await isPageHealthy()) {
|
||||
await resetBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带锁的执行函数,确保同一时间只有一个请求在使用页面
|
||||
*/
|
||||
async function withPageLock(fn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const execute = async () => {
|
||||
isProcessing = true;
|
||||
try {
|
||||
await ensurePageReady();
|
||||
const result = await fn();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
// 如果是页面关闭相关错误,标记需要重置
|
||||
if (error.message && error.message.includes('closed')) {
|
||||
console.log('⚠️ 检测到页面已关闭,将在下次请求时重置');
|
||||
browser = null;
|
||||
page = null;
|
||||
}
|
||||
reject(error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
// 处理队列中的下一个请求
|
||||
if (requestQueue.length > 0) {
|
||||
const next = requestQueue.shift();
|
||||
next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isProcessing) {
|
||||
// 如果正在处理,加入队列等待
|
||||
console.log('⏳ 请求排队中,当前队列长度:', requestQueue.length + 1);
|
||||
requestQueue.push(execute);
|
||||
} else {
|
||||
execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时的 page.evaluate 包装函数
|
||||
*/
|
||||
async function evaluateWithTimeout(pageFunction, arg, timeout = 30000) {
|
||||
return Promise.race([
|
||||
page.evaluate(pageFunction, arg),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`操作超时 (${timeout}ms)`)), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Playwright 浏览器
|
||||
*/
|
||||
@@ -141,8 +454,10 @@ app.get('/api/info', (req, res) => {
|
||||
github: 'https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption',
|
||||
endpoints: {
|
||||
health: 'GET /health',
|
||||
decrypt: 'POST /api/decrypt',
|
||||
keystream: 'POST /api/keystream'
|
||||
poolStatus: 'GET /api/pool-status',
|
||||
keystream: 'POST /api/keystream',
|
||||
decrypt: 'POST /api/decrypt (串行)',
|
||||
decryptConcurrent: 'POST /api/decrypt-concurrent (并发)'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -166,43 +481,46 @@ app.post('/api/keystream', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 确保浏览器已初始化
|
||||
if (!page) {
|
||||
await initBrowser();
|
||||
}
|
||||
|
||||
console.log(`🔑 生成密钥流: decode_key=${decode_key}, format=${format}`);
|
||||
|
||||
// 调用浏览器中的 RPC 方法
|
||||
const startTime = Date.now();
|
||||
const keystreamBase64 = await page.evaluate(async (key) => {
|
||||
return await window.generateKeystream(key);
|
||||
}, decode_key);
|
||||
// 使用锁机制确保串行处理
|
||||
const result = await withPageLock(async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
// 调用浏览器中的 RPC 方法(带超时保护)
|
||||
const keystreamBase64 = await evaluateWithTimeout(
|
||||
async (key) => await window.generateKeystream(key),
|
||||
decode_key,
|
||||
30000
|
||||
);
|
||||
|
||||
// 格式转换
|
||||
let keystream;
|
||||
if (format === 'hex') {
|
||||
const binary = atob(keystreamBase64);
|
||||
keystream = Array.from(binary, c =>
|
||||
c.charCodeAt(0).toString(16).padStart(2, '0')
|
||||
).join('');
|
||||
} else {
|
||||
keystream = keystreamBase64;
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`✅ 密钥流生成成功,耗时 ${duration}ms`);
|
||||
// 格式转换
|
||||
let keystream;
|
||||
if (format === 'hex') {
|
||||
const binary = atob(keystreamBase64);
|
||||
keystream = Array.from(binary, c =>
|
||||
c.charCodeAt(0).toString(16).padStart(2, '0')
|
||||
).join('');
|
||||
} else {
|
||||
keystream = keystreamBase64;
|
||||
}
|
||||
|
||||
res.json({
|
||||
decode_key,
|
||||
keystream,
|
||||
format,
|
||||
size: 131072,
|
||||
duration_ms: duration,
|
||||
timestamp: new Date().toISOString()
|
||||
console.log(`✅ 密钥流生成成功,耗时 ${duration}ms`);
|
||||
|
||||
return {
|
||||
decode_key,
|
||||
keystream,
|
||||
format,
|
||||
size: 131072,
|
||||
duration_ms: duration,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 密钥流生成失败:', error.message);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -226,57 +544,62 @@ app.post('/api/decrypt', upload.single('video'), async (req, res) => {
|
||||
return res.status(400).json({ error: '缺少视频文件' });
|
||||
}
|
||||
|
||||
// 确保浏览器已初始化
|
||||
if (!page) {
|
||||
await initBrowser();
|
||||
}
|
||||
|
||||
console.log(`📹 解密请求:`);
|
||||
console.log(` decode_key: ${decode_key}`);
|
||||
console.log(` 文件: ${videoFile.originalname} (${(videoFile.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
|
||||
const startTime = Date.now();
|
||||
// 根据文件大小动态设置超时时间(基础30秒 + 每MB额外1秒)
|
||||
const fileSizeMB = videoFile.size / 1024 / 1024;
|
||||
const timeout = Math.max(60000, 30000 + fileSizeMB * 1000);
|
||||
|
||||
// 步骤 1: 生成密钥流
|
||||
console.log(' [1/3] 生成密钥流...');
|
||||
const keystreamBase64 = await page.evaluate(async (key) => {
|
||||
return await window.generateKeystream(key);
|
||||
}, decode_key);
|
||||
// 使用锁机制确保串行处理
|
||||
const result = await withPageLock(async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 步骤 2: 转换视频为 Base64
|
||||
console.log(' [2/3] 编码视频数据...');
|
||||
const encryptedBase64 = videoFile.buffer.toString('base64');
|
||||
// 步骤 1: 生成密钥流
|
||||
console.log(' [1/3] 生成密钥流...');
|
||||
const keystreamBase64 = await evaluateWithTimeout(
|
||||
async (key) => await window.generateKeystream(key),
|
||||
decode_key,
|
||||
30000
|
||||
);
|
||||
|
||||
// 步骤 3: 在浏览器中执行解密
|
||||
console.log(' [3/3] 执行 XOR 解密...');
|
||||
const decryptedBase64 = await page.evaluate(
|
||||
(params) => {
|
||||
return window.decryptVideo(params.encrypted, params.keystream);
|
||||
},
|
||||
{ encrypted: encryptedBase64, keystream: keystreamBase64 }
|
||||
);
|
||||
// 步骤 2: 转换视频为 Base64
|
||||
console.log(' [2/3] 编码视频数据...');
|
||||
const encryptedBase64 = videoFile.buffer.toString('base64');
|
||||
|
||||
// Base64 → Buffer
|
||||
const decrypted = Buffer.from(decryptedBase64, 'base64');
|
||||
// 步骤 3: 在浏览器中执行解密(大文件需要更长超时)
|
||||
console.log(' [3/3] 执行 XOR 解密...');
|
||||
const decryptedBase64 = await evaluateWithTimeout(
|
||||
(params) => window.decryptVideo(params.encrypted, params.keystream),
|
||||
{ encrypted: encryptedBase64, keystream: keystreamBase64 },
|
||||
timeout
|
||||
);
|
||||
|
||||
// 验证 MP4 签名
|
||||
const ftyp = decrypted.toString('utf8', 4, 8);
|
||||
if (ftyp !== 'ftyp') {
|
||||
throw new Error('解密失败:未找到 MP4 ftyp 签名,请检查 decode_key');
|
||||
}
|
||||
// Base64 → Buffer
|
||||
const decrypted = Buffer.from(decryptedBase64, 'base64');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ 解密成功,耗时 ${duration}ms`);
|
||||
// 验证 MP4 签名
|
||||
const ftyp = decrypted.toString('utf8', 4, 8);
|
||||
if (ftyp !== 'ftyp') {
|
||||
throw new Error('解密失败:未找到 MP4 ftyp 签名,请检查 decode_key');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ 解密成功,耗时 ${duration}ms`);
|
||||
|
||||
return { decrypted, duration };
|
||||
});
|
||||
|
||||
// 返回解密后的视频
|
||||
res.set({
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': decrypted.length,
|
||||
'Content-Length': result.decrypted.length,
|
||||
'Content-Disposition': `attachment; filename="decrypted_${Date.now()}.mp4"`,
|
||||
'X-Decrypt-Duration': duration
|
||||
'X-Decrypt-Duration': result.duration
|
||||
});
|
||||
|
||||
res.send(decrypted);
|
||||
res.send(result.decrypted);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 解密失败:', error.message);
|
||||
@@ -284,6 +607,112 @@ app.post('/api/decrypt', upload.single('video'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/decrypt-concurrent
|
||||
* 并发解密视频(使用页面池)
|
||||
*/
|
||||
app.post('/api/decrypt-concurrent', upload.single('video'), async (req, res) => {
|
||||
try {
|
||||
const { decode_key } = req.body;
|
||||
const videoFile = req.file;
|
||||
|
||||
if (!decode_key) {
|
||||
return res.status(400).json({ error: '缺少 decode_key 参数' });
|
||||
}
|
||||
|
||||
if (!videoFile) {
|
||||
return res.status(400).json({ error: '缺少视频文件' });
|
||||
}
|
||||
|
||||
if (!pagePool) {
|
||||
return res.status(503).json({ error: '页面池未初始化,请稍后重试' });
|
||||
}
|
||||
|
||||
console.log(`📹 [并发] 解密请求:`);
|
||||
console.log(` decode_key: ${decode_key}`);
|
||||
console.log(` 文件: ${videoFile.originalname} (${(videoFile.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
console.log(` 页面池状态: ${JSON.stringify(pagePool.getStatus())}`);
|
||||
|
||||
// 根据文件大小动态设置超时时间
|
||||
const fileSizeMB = videoFile.size / 1024 / 1024;
|
||||
const timeout = Math.max(60000, 30000 + fileSizeMB * 1000);
|
||||
|
||||
// 使用页面池执行(支持并发)
|
||||
const result = await withPoolPage(async (pg) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 步骤 1: 生成密钥流
|
||||
console.log(` [页面#${pg._poolIndex}] [1/3] 生成密钥流...`);
|
||||
const keystreamBase64 = await evaluateWithTimeoutOnPage(
|
||||
pg,
|
||||
async (key) => await window.generateKeystream(key),
|
||||
decode_key,
|
||||
30000
|
||||
);
|
||||
|
||||
// 步骤 2: 转换视频为 Base64
|
||||
console.log(` [页面#${pg._poolIndex}] [2/3] 编码视频数据...`);
|
||||
const encryptedBase64 = videoFile.buffer.toString('base64');
|
||||
|
||||
// 步骤 3: 在浏览器中执行解密
|
||||
console.log(` [页面#${pg._poolIndex}] [3/3] 执行 XOR 解密...`);
|
||||
const decryptedBase64 = await evaluateWithTimeoutOnPage(
|
||||
pg,
|
||||
(params) => window.decryptVideo(params.encrypted, params.keystream),
|
||||
{ encrypted: encryptedBase64, keystream: keystreamBase64 },
|
||||
timeout
|
||||
);
|
||||
|
||||
// Base64 → Buffer
|
||||
const decrypted = Buffer.from(decryptedBase64, 'base64');
|
||||
|
||||
// 验证 MP4 签名
|
||||
const ftyp = decrypted.toString('utf8', 4, 8);
|
||||
if (ftyp !== 'ftyp') {
|
||||
throw new Error('解密失败:未找到 MP4 ftyp 签名,请检查 decode_key');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ [页面#${pg._poolIndex}] 解密成功,耗时 ${duration}ms`);
|
||||
|
||||
return { decrypted, duration };
|
||||
});
|
||||
|
||||
// 返回解密后的视频
|
||||
res.set({
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': result.decrypted.length,
|
||||
'Content-Disposition': `attachment; filename="decrypted_${Date.now()}.mp4"`,
|
||||
'X-Decrypt-Duration': result.duration
|
||||
});
|
||||
|
||||
res.send(result.decrypted);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [并发] 解密失败:', error.message);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/pool-status
|
||||
* 获取页面池状态
|
||||
*/
|
||||
app.get('/api/pool-status', (req, res) => {
|
||||
if (!pagePool) {
|
||||
return res.status(503).json({
|
||||
error: '页面池未初始化',
|
||||
status: null
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
pool: pagePool.getStatus(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 404 处理
|
||||
*/
|
||||
@@ -294,8 +723,10 @@ app.use((req, res) => {
|
||||
available: [
|
||||
'GET /',
|
||||
'GET /health',
|
||||
'GET /api/pool-status',
|
||||
'POST /api/keystream',
|
||||
'POST /api/decrypt'
|
||||
'POST /api/decrypt',
|
||||
'POST /api/decrypt-concurrent'
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -333,16 +764,23 @@ app.use((err, req, res, next) => {
|
||||
console.log(`🌐 访问地址: http://localhost:${PORT}\n`);
|
||||
|
||||
try {
|
||||
// 初始化浏览器
|
||||
// 初始化浏览器(用于串行接口)
|
||||
await initBrowser();
|
||||
|
||||
// 初始化页面池(用于并发接口)
|
||||
pagePool = new PagePool(browser, POOL_SIZE);
|
||||
await pagePool.initialize();
|
||||
|
||||
console.log('\n✅ 服务完全就绪');
|
||||
console.log('\n📚 API 端点:');
|
||||
console.log(' GET / 服务信息');
|
||||
console.log(' GET /health 健康检查');
|
||||
console.log(' POST /api/keystream 生成密钥流');
|
||||
console.log(' POST /api/decrypt 解密视频');
|
||||
console.log('\n🎭 使用 Playwright 浏览器执行 WASM');
|
||||
console.log(' GET / 服务信息');
|
||||
console.log(' GET /health 健康检查');
|
||||
console.log(' GET /api/pool-status 页面池状态');
|
||||
console.log(' POST /api/keystream 生成密钥流');
|
||||
console.log(' POST /api/decrypt 解密视频 (串行)');
|
||||
console.log(' POST /api/decrypt-concurrent 解密视频 (并发)');
|
||||
console.log(`\n🏊 页面池大小: ${POOL_SIZE} (可通过 POOL_SIZE 环境变量配置)`);
|
||||
console.log('🎭 使用 Playwright 浏览器执行 WASM');
|
||||
console.log(' 100% 兼容微信官方模块\n');
|
||||
} catch (error) {
|
||||
console.error('❌ 浏览器初始化失败:', error);
|
||||
@@ -357,18 +795,24 @@ app.use((err, req, res, next) => {
|
||||
})();
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', async () => {
|
||||
async function gracefulShutdown() {
|
||||
console.log('\n👋 正在关闭服务...');
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n👋 正在关闭服务...');
|
||||
// 关闭页面池
|
||||
if (pagePool) {
|
||||
console.log(' 关闭页面池...');
|
||||
await pagePool.close();
|
||||
}
|
||||
|
||||
// 关闭浏览器
|
||||
if (browser) {
|
||||
console.log(' 关闭浏览器...');
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log('✅ 服务已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
Reference in new issue
Block a user