wedecode
Version:
微信小程序源代码还原工具, 线上代码安全审计
1,415 lines (1,162 loc) • 56.2 kB
JavaScript
class WedecodeApp {
constructor() {
this.currentWorkspaceId = null;
this.selectedFiles = [];
this.socket = null;
this.executionId = null;
this.isProcessing = false;
this.fullLogHistory = []; // 存储完整日志历史
this.processOutputHistory = []; // 存储反编译进程的详细输出
this.initializeElements();
this.setupEventListeners();
this.connectWebSocket();
}
initializeElements() {
this.uploadArea = document.getElementById('uploadArea');
this.multiFileInput = document.getElementById('multiFileInput');
this.folderInput = document.getElementById('folderInput');
this.uploadText = document.getElementById('uploadText');
this.uploadHint = document.getElementById('uploadHint');
this.uploadBtn = document.getElementById('uploadBtn');
this.downloadBtn = document.getElementById('downloadBtn');
this.viewFullLogBtn = document.getElementById('viewFullLogBtn');
this.status = document.getElementById('status');
this.logContent = document.getElementById('logContent');
this.clearLogBtn = document.getElementById('clearLogBtn');
// 配置选项元素
this.wxidInput = document.getElementById('wxidInput');
this.usePx = document.getElementById('usePx');
this.unpackOnly = document.getElementById('unpackOnly');
// 模态框元素
this.logModal = document.getElementById('logModal');
this.closeModal = document.getElementById('closeModal');
this.fullLogContent = document.getElementById('fullLogContent');
this.copyLogBtn = document.getElementById('copyLogBtn');
// 文件夹编译相关元素
this.folderCompileBtn = document.getElementById('folderCompileBtn');
this.folderCompileModal = document.getElementById('folderCompileModal');
this.closeFolderModal = document.getElementById('closeFolderModal');
this.folderPath = document.getElementById('folderPath');
this.folderUsePx = document.getElementById('folderUsePx');
this.folderUnpackOnly = document.getElementById('folderUnpackOnly');
this.startFolderCompile = document.getElementById('startFolderCompile');
this.cancelFolderCompile = document.getElementById('cancelFolderCompile');
// 进度显示元素
this.progressContainer = document.getElementById('progressContainer');
this.progressText = document.getElementById('progressText');
this.progressPercent = document.getElementById('progressPercent');
this.progressFill = document.getElementById('progressFill');
this.progressDetails = document.getElementById('progressDetails');
}
setupEventListeners() {
// 文件上传区域事件
this.uploadArea.addEventListener('click', () => {
if (!this.isProcessing) {
this.showFileSelectionOptions();
}
});
this.uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.isProcessing) {
this.uploadArea.classList.add('dragover');
}
});
this.uploadArea.addEventListener('dragleave', () => {
this.uploadArea.classList.remove('dragover');
});
this.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
this.uploadArea.classList.remove('dragover');
if (!this.isProcessing && e.dataTransfer.files.length > 0) {
// 传递 DataTransferItems 以支持文件夹拖拽
this.handleDroppedFiles(e.dataTransfer.files, e.dataTransfer.items);
}
});
// 多文件选择事件
this.multiFileInput.addEventListener('change', (e) => {
console.log('多文件选择事件触发,文件数量:', e.target.files ? e.target.files.length : 0);
if (e.target.files && e.target.files.length > 0 && !this.isProcessing) {
this.handleMultiFileSelect(e.target.files);
}
});
// 按钮事件
this.uploadBtn.addEventListener('click', () => {
this.uploadAndDecompile();
});
this.downloadBtn.addEventListener('click', () => {
this.downloadResults();
});
this.clearLogBtn.addEventListener('click', () => {
this.clearLogs();
});
// 查看完整日志按钮事件
this.viewFullLogBtn.addEventListener('click', () => {
this.showFullLogModal();
});
// 模态框关闭事件
this.closeModal.addEventListener('click', () => {
this.hideFullLogModal();
});
// 复制日志按钮事件
this.copyLogBtn.addEventListener('click', () => {
this.copyLogsToClipboard();
});
// 点击模态框背景关闭
this.logModal.addEventListener('click', (e) => {
if (e.target === this.logModal) {
this.hideFullLogModal();
}
});
// ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.logModal.style.display === 'block') {
this.hideFullLogModal();
}
});
// 文件夹选择事件
this.folderInput.addEventListener('change', (e) => {
console.log('文件夹选择事件触发,文件数量:', e.target.files ? e.target.files.length : 0);
if (e.target.files && e.target.files.length > 0 && !this.isProcessing) {
this.handleFolderSelect(e.target.files);
}
});
// 文件夹编译相关事件
this.folderCompileBtn.addEventListener('click', () => {
this.showFolderCompileModal();
});
this.closeFolderModal.addEventListener('click', () => {
this.hideFolderCompileModal();
});
this.cancelFolderCompile.addEventListener('click', () => {
this.hideFolderCompileModal();
});
this.startFolderCompile.addEventListener('click', () => {
this.startFolderCompilation();
});
// 点击文件夹编译模态框背景关闭
this.folderCompileModal.addEventListener('click', (e) => {
if (e.target === this.folderCompileModal) {
this.hideFolderCompileModal();
}
});
// ESC键关闭文件夹编译模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.folderCompileModal.style.display === 'block') {
this.hideFolderCompileModal();
}
});
}
connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
this.addLog('WebSocket 连接已建立', 'success');
// 订阅工作区事件 - 使用当前工作区ID或默认ID
const workspaceId = this.currentWorkspace?.id || 'default';
this.socket.send(JSON.stringify({
type: 'subscribe',
workspaceId: workspaceId
}));
console.log('已发送 subscribe 消息,workspaceId:', workspaceId);
};
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('收到 WebSocket 消息:', data); // 添加调试日志
this.handleWebSocketMessage(data);
} catch (error) {
console.error('WebSocket 消息解析错误:', error);
}
};
this.socket.onclose = () => {
this.addLog('WebSocket 连接已断开', 'warning');
// 尝试重连
setTimeout(() => {
this.connectWebSocket();
}, 3000);
};
this.socket.onerror = (error) => {
this.addLog('WebSocket 连接错误', 'error');
console.error('WebSocket error:', error);
};
}
handleWebSocketMessage(data) {
if (data.type === 'execution' && data.executionId === this.executionId) {
if (data.event === 'output') {
// 处理不同类型的输出
const output = data.data.trim();
if (output) {
// 添加到反编译进程输出历史(用于完整日志显示)
this.processOutputHistory.push({
timestamp: new Date().toLocaleString(),
content: output,
type: 'output'
});
// 根据输出内容判断日志类型
let logType = 'info';
// 只有明确的错误信息才标记为error,避免误判文件名中包含error的情况
if (output.includes('❌') || output.includes('错误:') || output.includes('Error:') ||
output.includes('失败') || output.includes('异常') || output.includes('Exception')) {
logType = 'error';
} else if (output.includes('⚠️') || output.includes('警告:') || output.includes('Warning:')) {
logType = 'warning';
} else if (output.includes('✅') || output.includes('🎉') || output.includes('成功') ||
output.includes('完成') || output.includes('Success') || output.includes('Completed')) {
logType = 'success';
}
this.addLog(output, logType);
}
} else if (data.event === 'error') {
// 添加到反编译进程输出历史
this.processOutputHistory.push({
timestamp: new Date().toLocaleString(),
content: data.data,
type: 'error'
});
this.addLog(data.data, 'error');
} else if (data.event === 'exit') {
console.log('收到 exit 事件,退出码:', data.code); // 添加调试日志
this.isProcessing = false;
this.executionId = null;
if (data.code === 0) {
this.addLog('🎉 反编译完成!', 'success');
this.showStatus('反编译成功完成', 'success');
this.downloadBtn.disabled = false;
} else {
this.addLog(`❌ 反编译失败,退出码: ${data.code}`, 'error');
this.showStatus('反编译失败', 'error');
}
this.uploadBtn.disabled = false;
this.uploadBtn.innerHTML = '<i class="fas fa-play"></i> 开始反编译';
}
}
}
handleMultiFileSelect(files) {
console.log('多文件选择开始,文件数量:', files ? files.length : 0);
// 清空之前的状态
this.clearLogs();
this.closeFileSelectionModal();
if (!files || files.length === 0) {
this.addLog('❌ 错误: 未选择任何文件', 'error');
return;
}
// 转换为数组并过滤 .wxapkg 文件
const fileArray = Array.from(files);
const wxapkgFiles = fileArray.filter(file =>
file.name.toLowerCase().endsWith('.wxapkg')
);
if (wxapkgFiles.length === 0) {
this.addLog('❌ 错误: 选择的文件中没有找到 .wxapkg 文件', 'error');
return;
}
// 更新选择的文件(先复制后再清空 input)
this.selectedFiles = wxapkgFiles;
// 现在可以安全地清空 input 值,避免影响 FileList
this.multiFileInput.value = '';
// 启用上传按钮
this.uploadBtn.disabled = false;
this.downloadBtn.disabled = true;
// 显示成功信息
this.addLog(`✅ 成功选择 ${wxapkgFiles.length} 个 .wxapkg 文件`, 'success');
wxapkgFiles.forEach((file, index) => {
this.addLog(`📄 文件 ${index + 1}: ${file.name} (${this.formatFileSize(file.size)})`, 'info');
});
console.log('多文件选择完成,有效文件数:', wxapkgFiles.length);
}
getDecompileOptions() {
// 返回对象格式,匹配后端期望的格式
const wxidValue = this.wxidInput ? this.wxidInput.value.trim() : '';
const options = {
clear: true, // 固定设置:清空旧产物为 true
px: this.usePx.checked,
unpackOnly: this.unpackOnly.checked,
wxid: wxidValue || null
};
// 控制台打印配置参数
console.log('🔧 传给 wedecode 的配置参数:', options);
console.log('📋 配置详情:');
console.log(' - 清空旧产物 (clear):', options.clear);
console.log(' - 使用px单位 (px):', options.px);
console.log(' - 仅解包模式 (unpackOnly):', options.unpackOnly);
console.log(' - WXID:', options.wxid || '(未设置)');
return options;
}
checkFileSizes() {
const maxFileSize = 500 * 1024 * 1024; // 500MB
const maxTotalSize = 2 * 1024 * 1024 * 1024; // 2GB
const maxFileCount = 200;
let totalSize = 0;
let fileCount = 0;
let largeFiles = [];
// 检查选中的文件
const filesToCheck = this.selectedFiles && this.selectedFiles.length > 0
? this.selectedFiles
: (this.selectedFile ? [this.selectedFile] : []);
for (const file of filesToCheck) {
fileCount++;
totalSize += file.size;
if (file.size > maxFileSize) {
largeFiles.push({
name: file.name,
size: this.formatFileSize(file.size)
});
}
}
// 检查文件数量
if (fileCount > maxFileCount) {
return {
valid: false,
message: `文件数量超过限制,最多只能上传 ${maxFileCount} 个文件,当前选择了 ${fileCount} 个文件`
};
}
// 检查单个文件大小
if (largeFiles.length > 0) {
const fileList = largeFiles.map(f => `${f.name} (${f.size})`).join(', ');
return {
valid: false,
message: `以下文件超过 500MB 限制: ${fileList}`
};
}
// 检查总大小
if (totalSize > maxTotalSize) {
return {
valid: false,
message: `文件总大小超过限制,最大允许 2GB,当前总大小: ${this.formatFileSize(totalSize)}`
};
}
return {
valid: true,
message: `文件检查通过,共 ${fileCount} 个文件,总大小: ${this.formatFileSize(totalSize)}`
};
}
async uploadAndDecompile() {
// 检查是否有选择的文件或文件夹
if (this.selectedFiles && this.selectedFiles.length > 0) {
// 文件夹模式:有多个文件
} else if (this.selectedFile) {
// 单文件模式
} else {
this.showStatus('请先选择 wxapkg 文件或包含 wxapkg 文件的文件夹', 'error');
return;
}
if (this.isProcessing) {
this.showStatus('正在处理中,请稍候...', 'warning');
return;
}
this.isProcessing = true;
this.uploadBtn.disabled = true;
this.downloadBtn.disabled = true;
this.uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
// 清空历史日志
this.clearLogs();
this.showStatus('正在上传并反编译...', 'processing');
this.addLog('🚀 开始反编译流程...', 'info');
try {
// 检查文件大小
const sizeCheck = this.checkFileSizes();
if (!sizeCheck.valid) {
throw new Error(sizeCheck.message);
}
// 创建工作区
const workspace = await this.createWorkspace();
this.currentWorkspaceId = workspace.id;
this.currentWorkspace = workspace;
this.addLog(`✅ 创建工作区: ${workspace.name}`, 'success');
// 重新订阅新的工作区ID
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'subscribe',
workspaceId: workspace.id
}));
console.log('重新订阅工作区:', workspace.id);
}
// 获取配置选项
const options = this.getDecompileOptions();
const optionsText = [];
if (options.clear) optionsText.push('清理输出目录');
if (options.px) optionsText.push('转换为px');
if (options.unpackOnly) optionsText.push('仅解包');
this.addLog(`⚙️ 反编译选项: ${optionsText.length > 0 ? optionsText.join(', ') : '默认配置'}`, 'info');
// 判断是否需要分批上传
if (this.selectedFiles && this.selectedFiles.length > 0) {
// 多文件模式:检查是否需要分批上传
await this.uploadMultipleFiles(this.selectedFiles, options);
} else {
// 单文件模式:直接上传,显示详细进度
await this.uploadSingleFile(this.selectedFile, options, true);
}
} catch (error) {
this.isProcessing = false;
this.addLog(`❌ 错误: ${error.message}`, 'error');
this.showStatus('上传失败', 'error');
this.uploadBtn.disabled = false;
this.uploadBtn.innerHTML = '<i class="fas fa-play"></i> 开始反编译';
}
}
async uploadFileOnly(file, options) {
const formData = new FormData();
formData.append('wxapkg', file);
formData.append('options', JSON.stringify(options));
this.addLog(`📤 正在上传文件 ${file.name} (${this.formatFileSize(file.size)})...`, 'info');
const response = await fetch(`/api/workspaces/${this.currentWorkspaceId}/decompile`, {
method: 'POST',
body: formData
});
if (!response.ok) {
if (response.status === 413) {
// 处理文件大小超限错误
try {
const errorData = await response.json();
throw new Error(errorData.message || '文件大小超过限制');
} catch (parseError) {
throw new Error('文件大小超过限制,请确保单个文件不超过500MB,总大小不超过2GB,文件数量不超过200个');
}
} else {
throw new Error(`上传失败: ${response.statusText}`);
}
}
const result = await response.json();
this.addLog(`✅ 文件 ${file.name} 上传成功`, 'success');
return {
file: file,
executionId: result.executionId,
result: result
};
}
async decompileFile(uploadResult) {
const { file, executionId } = uploadResult;
if (!executionId) {
throw new Error('未能获取执行ID');
}
this.addLog(`🔄 开始反编译 ${file.name}...`, 'info');
// 等待反编译完成的信号
return new Promise((resolve, reject) => {
const originalHandler = this.handleWebSocketMessage.bind(this);
this.handleWebSocketMessage = (data) => {
originalHandler(data);
if (data.type === 'execution' && data.executionId === executionId) {
if (data.event === 'exit') {
if (data.code === 0) {
this.addLog(`🎉 ${file.name} 反编译完成!`, 'success');
resolve(uploadResult);
} else {
reject(new Error(`${file.name} 反编译失败,退出码: ${data.code}`));
}
} else if (data.event === 'error') {
reject(new Error(`${file.name} 反编译错误: ${data.data}`));
}
}
};
});
}
async uploadSingleFile(file, options, showIndividualProgress = false) {
const formData = new FormData();
formData.append('wxapkg', file);
formData.append('options', JSON.stringify(options));
// 如果需要显示单个文件进度,则显示进度条
if (showIndividualProgress) {
this.showProgress(0, 100, '文件处理', `正在上传 ${file.name}...`);
}
this.addLog(`📤 正在上传文件 ${file.name} (${this.formatFileSize(file.size)})...`, 'info');
// 模拟上传进度
if (showIndividualProgress) {
this.showProgress(30, 100, '文件处理', `正在上传 ${file.name}...`);
}
const response = await fetch(`/api/workspaces/${this.currentWorkspaceId}/decompile`, {
method: 'POST',
body: formData
});
if (!response.ok) {
if (showIndividualProgress) {
this.hideProgress();
}
if (response.status === 413) {
// 处理文件大小超限错误
try {
const errorData = await response.json();
throw new Error(errorData.message || '文件大小超过限制');
} catch (parseError) {
throw new Error('文件大小超过限制,请确保单个文件不超过500MB,总大小不超过2GB,文件数量不超过200个');
}
} else {
throw new Error(`上传失败: ${response.statusText}`);
}
}
const result = await response.json();
if (showIndividualProgress) {
this.showProgress(60, 100, '文件处理', `${file.name} 上传完成,开始反编译...`);
}
this.addLog(`✅ 文件 ${file.name} 上传成功,开始反编译...`, 'success');
if (result.executionId) {
this.executionId = result.executionId;
if (showIndividualProgress) {
this.showProgress(80, 100, '文件处理', `${file.name} 反编译进程已启动...`);
}
this.addLog(`🔄 ${file.name} 反编译进程已启动,正在处理...`, 'info');
this.showStatus('正在反编译,请查看日志...', 'processing');
// 等待反编译完成的信号
return new Promise((resolve, reject) => {
const originalHandler = this.handleWebSocketMessage.bind(this);
this.handleWebSocketMessage = (data) => {
originalHandler(data);
if (data.type === 'execution' && data.executionId === result.executionId) {
if (data.event === 'exit') {
if (showIndividualProgress) {
this.showProgress(100, 100, '文件处理', `${file.name} 处理完成`);
setTimeout(() => this.hideProgress(), 1000);
}
if (data.code === 0) {
this.addLog(`🎉 ${file.name} 反编译完成!`, 'success');
resolve(result);
} else {
reject(new Error(`反编译失败,退出码: ${data.code}`));
}
} else if (data.event === 'error') {
if (showIndividualProgress) {
this.hideProgress();
}
reject(new Error(`反编译错误: ${data.data}`));
}
}
};
});
} else {
if (showIndividualProgress) {
this.hideProgress();
}
throw new Error('未能启动反编译进程');
}
}
async uploadMultipleFiles(files, options) {
this.showProgress(0, files.length, '文件上传', `准备上传 ${files.length} 个文件...`);
let uploadResults = [];
let uploadFailedFiles = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const currentProgress = i + 1;
this.showProgress(i, files.length, '文件上传', `正在上传 ${file.name} (${currentProgress}/${files.length})`);
try {
const uploadResult = await this.uploadFileOnly(file, options);
uploadResults.push(uploadResult);
this.addLog(`✅ ${file.name} 上传成功 (${currentProgress}/${files.length})`, 'success');
} catch (error) {
uploadFailedFiles.push({ name: file.name, error: error.message });
this.addLog(`❌ ${file.name} 上传失败: ${error.message}`, 'error');
}
// 文件之间稍微延迟,避免服务器压力过大
if (i < files.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
this.showProgress(files.length, files.length, '文件上传', `上传阶段完成 (${uploadResults.length}/${files.length})`);
// 上传阶段总结
if (uploadFailedFiles.length === 0) {
this.addLog(`🎉 所有 ${files.length} 个文件上传完成!`, 'success');
} else {
this.addLog(`⚠️ 上传完成:成功 ${uploadResults.length} 个,失败 ${uploadFailedFiles.length} 个`, 'warning');
uploadFailedFiles.forEach(failed => {
this.addLog(`❌ 上传失败 ${failed.name}: ${failed.error}`, 'error');
});
}
// 如果没有成功上传的文件,直接结束
if (uploadResults.length === 0) {
this.hideProgress();
this.showStatus('所有文件上传失败', 'error');
return;
}
// 第二阶段:批量反编译所有已上传的文件
this.addLog(`🔄 阶段2:开始批量反编译所有已上传的文件...`, 'info');
this.showProgress(0, 1, '批量反编译', `准备批量反编译 ${uploadResults.length} 个文件...`);
try {
// 调用批量反编译API
const result = await this.batchDecompileFiles(options);
// 设置执行状态,这样WebSocket消息才能被正确处理
this.executionId = result.executionId;
this.isProcessing = true;
// 更新按钮状态
this.uploadBtn.disabled = true;
this.uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 批量反编译进行中...';
this.hideProgress();
} catch (error) {
this.hideProgress();
this.addLog(`❌ 批量反编译启动失败: ${error.message}`, 'error');
this.showStatus(`批量反编译失败`, 'error');
// 如果批量反编译失败,显示失败的文件详情
const totalFailed = uploadFailedFiles.length;
if (totalFailed > 0) {
uploadFailedFiles.forEach(failed => {
this.addLog(`❌ 上传失败 ${failed.name}: ${failed.error}`, 'error');
});
}
}
}
async createWorkspace() {
const response = await fetch('/api/workspaces', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `反编译_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}`,
type: 'miniapp',
description: '微信小程序反编译工作区'
})
});
if (!response.ok) {
throw new Error('创建工作区失败');
}
return await response.json();
}
async downloadResults() {
if (!this.currentWorkspaceId) {
this.showStatus('没有可下载的结果', 'error');
return;
}
this.addLog('📦 正在打包下载文件...', 'info');
this.showStatus('正在准备下载...', 'processing');
try {
const response = await fetch(`/api/workspaces/${this.currentWorkspaceId}/download`, {
method: 'GET'
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('未找到反编译结果,请先完成反编译');
}
throw new Error('下载失败');
}
// 从响应头中获取文件名
let filename = `反编译结果_${new Date().toISOString().slice(0, 10)}.zip`; // 默认文件名
const contentDisposition = response.headers.get('Content-Disposition');
// 调试日志
console.log('🔍 [下载调试] Content-Disposition响应头:', contentDisposition);
console.log('🔍 [下载调试] 默认文件名:', filename);
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
console.log('🔍 [下载调试] 文件名匹配结果:', filenameMatch);
if (filenameMatch && filenameMatch[1]) {
// 移除引号并解码
const rawFilename = filenameMatch[1].replace(/['"]/g, '');
filename = decodeURIComponent(rawFilename);
console.log('🔍 [下载调试] 原始文件名:', rawFilename);
console.log('🔍 [下载调试] 解码后文件名:', filename);
}
} else {
console.log('� [下载调试] 未找到Content-Disposition响应头,使用默认文件名');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.addLog(`✅ 下载完成!文件名: ${filename}`, 'success');
this.showStatus('下载完成', 'success');
} catch (error) {
this.addLog(`❌ 下载失败: ${error.message}`, 'error');
this.showStatus('下载失败', 'error');
}
}
addLog(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const fullTimestamp = new Date().toLocaleString();
const logLine = document.createElement('div');
logLine.className = `log-line ${type}`;
logLine.textContent = `[${timestamp}] ${message}`;
// 添加到显示区域
this.logContent.appendChild(logLine);
this.logContent.scrollTop = this.logContent.scrollHeight;
// 添加到完整日志历史
this.fullLogHistory.push({
timestamp: fullTimestamp,
message: message,
type: type
});
// 启用查看完整日志按钮
if (this.viewFullLogBtn) {
this.viewFullLogBtn.disabled = false;
}
}
clearLogs() {
this.logContent.innerHTML = '';
this.fullLogHistory = [];
this.processOutputHistory = []; // 同时清空反编译进程输出历史
// 禁用查看完整日志按钮
if (this.viewFullLogBtn) {
this.viewFullLogBtn.disabled = true;
}
}
showStatus(message, type) {
this.status.textContent = message;
this.status.className = `status ${type}`;
this.status.classList.remove('hidden');
// 清除之前的 spinner
const existingSpinner = this.status.querySelector('.spinner');
if (existingSpinner) {
existingSpinner.remove();
}
if (type === 'processing') {
const spinner = document.createElement('span');
spinner.className = 'spinner';
spinner.style.marginRight = '10px';
this.status.insertBefore(spinner, this.status.firstChild);
}
}
showProgress(current, total, text = '上传进度', details = '') {
const percent = Math.round((current / total) * 100);
this.progressContainer.classList.remove('hidden');
this.progressText.textContent = text;
this.progressPercent.textContent = `${percent}%`;
this.progressFill.style.width = `${percent}%`;
if (details) {
this.progressDetails.textContent = details;
}
}
hideProgress() {
this.progressContainer.classList.add('hidden');
this.progressFill.style.width = '0%';
this.progressPercent.textContent = '0%';
this.progressDetails.textContent = '';
}
showFullLogModal() {
// 生成完整日志内容 - 显示反编译进程的详细输出
let logText = '';
if (this.processOutputHistory.length === 0) {
logText = '暂无反编译进程日志内容\n\n如果您看到此消息,说明反编译进程尚未开始或没有产生输出。';
} else {
logText = this.processOutputHistory.map(log => {
const typePrefix = log.type === 'error' ? '❌ [错误] ' : '';
return `${typePrefix}${log.content}`;
}).join('\n');
}
this.fullLogContent.textContent = logText;
this.logModal.style.display = 'block';
// 滚动到底部
setTimeout(() => {
this.fullLogContent.scrollTop = this.fullLogContent.scrollHeight;
}, 100);
}
hideFullLogModal() {
this.logModal.style.display = 'none';
}
async copyLogsToClipboard() {
try {
// 获取当前显示的日志内容
const logText = this.fullLogContent.textContent;
// 使用现代的 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(logText);
} else {
// 降级方案:使用传统的 document.execCommand
const textArea = document.createElement('textarea');
textArea.value = logText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
// 显示成功提示
this.showCopySuccess();
} catch (err) {
console.error('复制失败:', err);
this.showCopyError();
}
}
showCopySuccess() {
const originalText = this.copyLogBtn.innerHTML;
this.copyLogBtn.innerHTML = '<i class="fas fa-check"></i> 已复制';
this.copyLogBtn.style.background = '#28a745';
setTimeout(() => {
this.copyLogBtn.innerHTML = originalText;
this.copyLogBtn.style.background = '';
}, 2000);
}
showCopyError() {
const originalText = this.copyLogBtn.innerHTML;
this.copyLogBtn.innerHTML = '<i class="fas fa-times"></i> 复制失败';
this.copyLogBtn.style.background = '#dc3545';
setTimeout(() => {
this.copyLogBtn.innerHTML = originalText;
this.copyLogBtn.style.background = '';
}, 2000);
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 跨平台路径分割函数
*/
splitPath(filePath) {
if (!filePath) return [];
// 处理不同操作系统的路径分隔符
// Windows: \ 或 /
// macOS/Linux: /
// 同时处理混合路径分隔符的情况
return filePath.split(/[/\\]+/).filter(part => part.length > 0);
}
handleFolderSelect(files) {
console.log('文件夹选择开始,文件数量:', files ? files.length : 0);
// 清空之前的状态
this.clearLogs();
this.closeFileSelectionModal();
if (!files || files.length === 0) {
this.addLog('❌ 错误: 文件夹为空或无法访问', 'error');
return;
}
// 转换为数组并过滤 .wxapkg 文件
const fileArray = Array.from(files);
const wxapkgFiles = fileArray.filter(file =>
file.name.toLowerCase().endsWith('.wxapkg')
);
if (wxapkgFiles.length === 0) {
this.addLog('❌ 错误: 选择的文件夹中没有找到 .wxapkg 文件', 'error');
return;
}
// 更新选择的文件(先复制后再清空 input)
this.selectedFiles = wxapkgFiles;
// 现在可以安全地清空 input 值,避免影响 FileList
this.folderInput.value = '';
// 启用上传按钮
this.uploadBtn.disabled = false;
this.downloadBtn.disabled = true;
// 显示成功信息
const totalSize = wxapkgFiles.reduce((sum, file) => sum + file.size, 0);
this.addLog(`✅ 成功选择文件夹,找到 ${wxapkgFiles.length} 个 .wxapkg 文件 (总大小: ${this.formatFileSize(totalSize)})`, 'success');
this.showStatus(`已选择 ${wxapkgFiles.length} 个 .wxapkg 文件`, 'success');
console.log('文件夹选择完成,有效文件数:', wxapkgFiles.length);
}
showFileSelectionOptions() {
// 检查是否已经有弹窗存在,避免多层模态框
if (document.getElementById('fileSelectionModal')) {
return;
}
// 创建简单的选择界面,设置唯一ID
const overlay = document.createElement('div');
overlay.id = 'fileSelectionModal';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 400px;
width: 90%;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 20px 0; color: #333;">选择上传方式</h3>
<div style="display: flex; flex-direction: column; gap: 12px;">
<button id="selectFolder" style="
padding: 12px 20px;
border: 2px solid #17a2b8;
background: #17a2b8;
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
transition: all 0.3s;
">📁 选择文件夹</button>
<div style="margin: 10px 0; padding: 10px; background: #e8f4fd; border-radius: 6px; font-size: 13px; color: #0c5460; text-align: left;">
• 支持批量处理多个 .wxapkg 文件<br>
</div>
<div style="margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 6px; font-size: 13px; color: #666; text-align: left;">
<strong>使用提示:💡</strong><br>
• <strong>拖拽</strong>:直接将文件夹拖到上传区域<br>
</div>
<button id="cancelSelection" style="
padding: 8px 20px;
border: 1px solid #ccc;
background: #f8f9fa;
color: #666;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
">取消</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// 添加事件监听器
document.getElementById('selectFolder').addEventListener('click', () => {
this.folderInput.click();
});
document.getElementById('cancelSelection').addEventListener('click', () => {
this.closeFileSelectionModal();
});
// 点击背景关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.closeFileSelectionModal();
}
});
}
closeFileSelectionModal() {
const modal = document.getElementById('fileSelectionModal');
if (modal) {
document.body.removeChild(modal);
}
}
async handleDroppedFiles(files, dataTransferItems = null) {
// 清空之前的日志
this.clearLogs();
this.addLog(`📂 拖拽检测: 开始处理拖拽内容`, 'info');
// 如果有 DataTransferItems,尝试使用高级API处理文件夹
if (dataTransferItems && dataTransferItems.length > 0) {
try {
const allFiles = await this.processDataTransferItems(dataTransferItems);
if (allFiles.length > 0) {
this.processExtractedFiles(allFiles);
return;
}
} catch (error) {
this.addLog(`⚠️ 高级API处理失败: ${error.message}`, 'warning');
this.addLog(`🔄 回退到基础文件处理模式`, 'info');
}
}
// 回退到基础文件处理
this.processBasicFiles(files);
}
async processDataTransferItems(items) {
const allFiles = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
const entry = item.webkitGetAsEntry();
if (entry) {
if (entry.isDirectory) {
const folderFiles = await this.readDirectoryRecursively(entry, entry.name);
allFiles.push(...folderFiles);
} else if (entry.isFile) {
this.addLog(`📄 处理文件: ${entry.name}`, 'info');
const file = await this.getFileFromEntry(entry);
if (file) {
// 为单个文件也设置路径信息
try {
Object.defineProperty(file, 'webkitRelativePath', {
value: entry.name,
writable: false,
enumerable: true,
configurable: true
});
} catch (pathError) {
file.relativePath = entry.name;
}
allFiles.push(file);
}
}
}
}
}
return allFiles;
}
async readDirectoryRecursively(directoryEntry, path = '') {
const files = [];
return new Promise((resolve, reject) => {
const reader = directoryEntry.createReader();
const readEntries = () => {
reader.readEntries(async (entries) => {
if (entries.length === 0) {
resolve(files);
return;
}
try {
for (const entry of entries) {
const currentPath = path ? `${path}/${entry.name}` : entry.name;
if (entry.isFile) {
try {
const file = await this.getFileFromEntry(entry);
if (file) {
// 添加相对路径信息 - 使用Object.defineProperty因为webkitRelativePath是只读的
try {
Object.defineProperty(file, 'webkitRelativePath', {
value: currentPath,
writable: false,
enumerable: true,
configurable: true
});
} catch (pathError) {
// 如果无法设置webkitRelativePath,添加自定义属性
file.relativePath = currentPath;
}
files.push(file);
// 只记录 .wxapkg 文件
if (file.name.toLowerCase().endsWith('.wxapkg')) {
this.addLog(`📄 发现 .wxapkg 文件: ${currentPath} (${this.formatFileSize(file.size)})`, 'success');
}
}
} catch (error) {
this.addLog(`❌ 文件读取异常: ${currentPath} - ${error.message}`, 'error');
}
} else if (entry.isDirectory) {
try {
const subFiles = await this.readDirectoryRecursively(entry, currentPath);
files.push(...subFiles);
} catch (error) {
this.addLog(`❌ 子目录处理失败: ${currentPath} - ${error.message}`, 'error');
}
}
}
// 继续读取更多条目(某些浏览器分批返回)
readEntries();
} catch (error) {
this.addLog(`❌ 处理目录条目时出错: ${path || directoryEntry.name} - ${error.message}`, 'error');
reject(error);
}
}, (error) => {
this.addLog(`❌ 读取目录失败: ${path || directoryEntry.name} - ${error.message}`, 'error');
reject(error);
});
};
readEntries();
});
}
async getFileFromEntry(fileEntry) {
return new Promise((resolve, reject) => {
fileEntry.file(resolve, reject);
});
}
processExtractedFiles(allFiles) {
// 过滤 .wxapkg 文件
const wxapkgFiles = allFiles.filter(file => {
const fileName = file.name.toLowerCase();
return fileName.endsWith('.wxapkg');
});
if (wxapkgFiles.length === 0) {
this.addLog('❌ 错误: 拖拽的内容中没有找到 .wxapkg 文件', 'error');
this.showStatus('未找到 .wxapkg 文件', 'error');
return;
}
// 设置选择的文件
this.selectedFiles = wxapkgFiles;
this.uploadBtn.disabled = false;
this.downloadBtn.disabled = true;
// 显示成功信息
const totalSize = wxapkgFiles.reduce((sum, file) => sum + file.size, 0);
this.addLog(`✅ 成功选择文件夹,找到 ${wxapkgFiles.length} 个 .wxapkg 文件 (总大小: ${this.formatFileSize(totalSize)})`, 'success');
this.showStatus(`已选择 ${wxapkgFiles.length} 个 .wxapkg 文件`, 'success');
}
processBasicFiles(files) {
// 检查拖拽的文件类型
const fileArray = Array.from(files);
this.addLog(`📂 基础模式: 共接收到 ${fileArray.length} 个项目`, 'info');
// 检查是否有 .wxapkg 文件(大小写不敏感)
const wxapkgFiles = fileArray.filter(file => {
const fileName = file.name.toLowerCase();
const isWxapkg = fileName.endsWith('.wxapkg');
this.addLog(` 检查: "${file.name}" -> ${isWxapkg ? '✅ 是 .wxapkg 文件' : '❌ 不是 .wxapkg 文件'}`, 'info');
return isWxapkg;
});
this.addLog(`🔍 找到 ${wxapkgFiles.length} 个 .wxapkg 文件`, 'info');
if (wxapkgFiles.length === 1 && fileArray.length === 1) {
// 单个 .wxapkg 文件
this.addLog('📄 处理方式: 单个文件模式', 'info');
this.selectedFiles = [wxapkgFiles[0]];
this.uploadBtn.disabled = false;
this.downloadBtn.disabled = true;
this.addLog(`✅ 已选择文件: ${wxapkgFiles[0].name}`, 'success');
this.showStatus(`已选择文件: ${wxapkgFiles[0].name}`, 'success');
} else if (wxapkgFiles.length > 0) {
// 多个 .wxapkg 文件或混合文件
this.addLog('📁 处理方式: 多文件模式', 'info');
this.selectedFiles = wxapkgFiles;
this.uploadBtn.disabled = false;
this.downloadBtn.disabled = true;
const totalSize = wxapkgFiles.reduce((sum, file) => sum + file.size, 0);
this.addLog(`✅ 已选择 ${wxapkgFiles.length} 个 .wxapkg 文件 (总大小: ${this.formatFileSize(totalSize)})`, 'success');
this.showStatus(`已选择 ${wxapkgFiles.length} 个 .wxapkg 文件`, 'success');
// 列出所有选择的文件
wxapkgFiles.forEach(file => {
this.addLog(` 📄 ${file.name} (${this.formatFileSize(file.size)})`, 'info');
});
} else {
// 没有 .wxapkg 文件
this.showStatus('拖拽的文件中没有找到 .wxapkg 文件', 'error');
this.addLog('❌ 错误: 拖拽的文件中没有找到 .wxapkg 文件', 'error');
this.addLog('💡 提示: 请拖拽 .wxapkg 文件,或使用文件夹选择按钮选择包含 .wxapkg 文件的文件夹', 'info');
}
}
// 显示文件夹编译选项
showFolderCompileOption() {
this.folderCompileBtn.style.display = 'inline-block';
this.addLog('💡 提示: 您可以使用文件夹编译功能对上传的文件进行批量编译', 'info');
}
// 显示文件夹编译模态框
showFolderCompileModal() {
this.folderCompileModal.style.display = 'block';
this.folderPathInput.focus();
}
// 隐藏文件夹编译模态框
hideFolderCompileModal() {
this.folderCompileModal.style.display = 'none';
// 清空输入
this.folderPathInput.value = '';
this.folderPxOption.checked = false;
this.folderUnpackOnlyOption.checked = false;
}
// 开始文件夹编译
async startFolderCompilation() {
const folderPath = this.folderPathInput.value.trim();
if (!folderPath) {
this.addLog('❌ 请输入文件夹路径', 'error');
return;
}
// 构建编译选项
const options = [];
if (this.folderPxOption.checked) {
options.push('--px');
}
if (this.folderUnpackOnlyOption.checked) {
options.push('--unpack-only');
}
this.hideFolderCompileModal();
this.addLog(`🚀 开始文件夹编译: ${folderPath}`, 'info');
if (options.length > 0) {
this.addLog(`📋 编译选项: ${options.join(' ')}`, 'info');
}
try {
const response = await fetch(`/api/workspaces/${this.currentWorkspaceId}/compile-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
folderPath: folderPath,
options: options
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '文件夹编译请求失败');
}
const result = await response.json();
this.addLog(`✅ 文件夹编译已启动,执行ID: ${result.executionId}`, 'success');
} catch (error) {
this.addLog(`❌ 文件夹编译失败: ${error.message}`, 'error');
console.error('文件夹编译错误:', error);
}
}
async batchDecompileFiles(options) {
if (!this.currentWor