Toy文件容器存在文件后缀白名单,开发者使用cocos发布内容时部分文件会被容器安全控件过滤。导致预览和正式页面因文件缺失报错。
后缀白名单参见:常见问题

该问题可以通过js脚本实现全局替换来解决。
这里是我自己写的脚本,请将该js文件放在build文件夹下面。在cocos编辑器完成发布后,通过运行 node toy.js实现全局文件后缀替换。
脚本内容如下:
const fs = require('fs');
const path = require('path');
// 目标文件夹路径
const targetDir = './web-mobile';
// 定义扩展名映射
const extensionMap = {
'.cconb': '.unityweb',
'.bin': '.part'
};
// 需要处理的文件类型
const targetFileTypes = ['.js', '.json'];
// 递归遍历文件夹
function walkDir(dir, callback) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
walkDir(filePath, callback);
} else {
callback(filePath);
}
});
}
// 精确替换双引号中的完整扩展名
function replaceExactExtension(content, oldExt, newExt) {
// 转义特殊字符
const escapedOldExt = oldExt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// 匹配双引号中的内容,且扩展名前后必须是引号或路径分隔符
// 匹配模式: "xxx.cconb" 或 "xxx/yyy.cconb"
const pattern = new RegExp(`"([^"]*)${escapedOldExt}([^"]*)"`, 'g');
let modified = false;
let replacements = 0;
let result = content;
// 找到所有匹配
let match;
while ((match = pattern.exec(content)) !== null) {
const fullMatch = match[0];
const prefix = match[1];
const suffix = match[2];
// 检查扩展名是否完整(后面没有多余的字母或数字)
// 如果后缀为空或以引号结束,说明扩展名是完整的
if (suffix === '' || suffix.startsWith('"')) {
// 检查扩展名是否正好在字符串末尾(引号前)
const stringEnd = fullMatch.lastIndexOf('"');
const extStart = fullMatch.indexOf(oldExt);
// 确保扩展名后面紧跟着引号
if (extStart + oldExt.length === fullMatch.length - 1) {
// 完全匹配,替换
const newStr = fullMatch.replace(oldExt, newExt);
result = result.replace(fullMatch, newStr);
replacements++;
modified = true;
}
}
}
return { content: result, modified, replacements };
}
// 替换文件内容中的字符串
function replaceFileContent(filePath) {
try {
// 读取文件内容
let content = fs.readFileSync(filePath, 'utf8');
let totalModified = false;
let totalReplacements = 0;
const fileName = path.basename(filePath);
const ext = path.extname(filePath);
for (const [oldExt, newExt] of Object.entries(extensionMap)) {
let result;
if (ext === '.js' || ext === '.json') {
// 精确替换
result = replaceExactExtension(content, oldExt, newExt);
} else {
continue;
}
if (result.modified) {
content = result.content;
totalModified = true;
totalReplacements += result.replacements;
}
}
// 如果有修改,写回文件
if (totalModified) {
fs.writeFileSync(filePath, content, 'utf8');
console.log(`📝 内容替换: ${fileName} (替换了 ${totalReplacements} 处)`);
return { fileName, replacementCount: totalReplacements };
}
return null;
} catch (err) {
console.error(`❌ 读取/写入文件失败: ${filePath}`, err.message);
return null;
}
}
// 主函数
function processFiles() {
// 检查目标文件夹是否存在
if (!fs.existsSync(targetDir)) {
console.error(`错误: 文件夹 "${targetDir}" 不存在`);
return;
}
console.log('🚀 开始处理文件...\n');
let renameCount = 0;
let errorCount = 0;
let replaceCount = 0;
const renameLog = [];
const replaceLog = [];
// 第一步:重命名文件
console.log('📁 第一步:重命名文件');
console.log('─'.repeat(50));
walkDir(targetDir, (filePath) => {
// 检查文件扩展名是否需要替换
let shouldRename = false;
let newPath = filePath;
for (const [oldExt, newExt] of Object.entries(extensionMap)) {
if (filePath.endsWith(oldExt)) {
newPath = filePath.replace(new RegExp(`${oldExt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`), newExt);
shouldRename = true;
break;
}
}
if (shouldRename) {
try {
fs.renameSync(filePath, newPath);
const oldName = path.basename(filePath);
const newName = path.basename(newPath);
console.log(`✅ 重命名: ${oldName} -> ${newName}`);
renameCount++;
renameLog.push({ old: filePath, new: newPath });
} catch (err) {
console.error(`❌ 重命名失败: ${filePath}`, err.message);
errorCount++;
}
}
});
console.log(`\n📊 重命名完成: ${renameCount} 个文件\n`);
// 第二步:替换文件内容
console.log('📝 第二步:替换文件内容(精确匹配)');
console.log('─'.repeat(50));
walkDir(targetDir, (filePath) => {
const ext = path.extname(filePath);
// 只处理 .js 和 .json 文件
if (targetFileTypes.includes(ext)) {
const result = replaceFileContent(filePath);
if (result) {
replaceCount++;
replaceLog.push(result);
}
}
});
// 显示汇总信息
console.log('\n' + '='.repeat(60));
console.log('📊 处理完成汇总');
console.log('='.repeat(60));
console.log(`📁 重命名文件: ${renameCount} 个`);
console.log(`📝 内容替换文件: ${replaceCount} 个`);
if (renameLog.length > 0) {
console.log('\n📋 重命名详情:');
const cconbCount = renameLog.filter(item => item.old.endsWith('.cconb')).length;
const binCount = renameLog.filter(item => item.old.endsWith('.bin')).length;
console.log(` - .cconb -> .unityweb: ${cconbCount} 个`);
console.log(` - .bin -> .part: ${binCount} 个`);
}
if (replaceLog.length > 0) {
console.log('\n📋 内容替换详情:');
let totalReplacements = 0;
replaceLog.forEach(item => {
console.log(` - ${item.fileName}: ${item.replacementCount} 处`);
totalReplacements += item.replacementCount;
});
console.log(` 📈 总计替换: ${totalReplacements} 处`);
}
console.log('\n✅ 所有处理完成!');
}
// 执行主函数
processFiles();