diff --git a/cli/src/cli/fix-borders-cmd.js b/cli/src/cli/fix-borders-cmd.js index 6b9d87c..3c2e2c2 100644 --- a/cli/src/cli/fix-borders-cmd.js +++ b/cli/src/cli/fix-borders-cmd.js @@ -28,10 +28,12 @@ function cmdFixBorders(argv) { project = argv[++i]; } else if (a === '--help' || a === '-h') { process.stdout.write( - 'cocos-mcp-cli fix-borders — 修复 Cocos 重启把九宫格 border 重置成 0 的 meta\n\n' + + 'cocos-mcp-cli fix-borders — 清理 Cocos 重启造成的图片 meta 噪音\n\n' + ' --project, -p 项目路径(git 仓库或其子目录,默认当前目录)\n' + ' --dry-run 只预览将还原哪些,不写文件\n\n' + - '原理:扫 git 改动的 .meta,把「git 有值、工作区被清 0」的 border 还原成 git 的值。\n'); + '处理两类噪音(都靠 git 对比,正确处理中文路径):\n' + + ' 1) 纯 key 顺序/格式变化(值没变)→ 还原成 git 原文\n' + + ' 2) 九宫格 border 被重置成 0 → 用 git 的值精准还原\n'); return; } else { die('未知参数 "' + a + '"'); @@ -47,8 +49,8 @@ function cmdFixBorders(argv) { process.stdout.write( (dryRun ? '[dry-run] ' : '') + - '扫描 ' + r.scanned + ' 个改动 meta,还原 border:' + - r.fixedFiles + ' 个 meta / ' + r.fixedFrames + ' 个 sprite-frame\n'); + '扫描 ' + r.scanned + ' 个改动 meta:顺序噪音还原 ' + r.reorderFiles + + ' 个,border 还原 ' + r.borderFiles + ' 个 meta / ' + r.borderFrames + ' frame\n'); if (r.details.length) { process.stdout.write(r.details.slice(0, 50).join('\n') + '\n'); diff --git a/cli/src/editor/fix-borders.js b/cli/src/editor/fix-borders.js index 168b20c..61960ba 100644 --- a/cli/src/editor/fix-borders.js +++ b/cli/src/editor/fix-borders.js @@ -1,14 +1,19 @@ // ============================================================ -// editor/fix-borders.js — 修复 Cocos 重启把图片九宫格 border 重置成 0 的 meta +// editor/fix-borders.js — 清理 Cocos 重启/重新导入造成的图片 meta 噪音 // -// 现象:Cocos 重启/重新导入有时把已设九宫格的图 meta 里 -// subMetas..userData.{borderTop,borderBottom,borderLeft,borderRight} -// 重置成 0(九宫格丢失)。 +// 现象(Cocos 重启/重新导入有时发生): +// 1) 纯 key 顺序/格式变化:如 userData 里 trimType/atlasUuid 等字段被重排位置, +// 值没变,只产生 git diff 噪音。 +// 2) 九宫格 border 被重置:subMetas..userData.{borderTop,Bottom,Left,Right} +// 被清成 0(九宫格丢失,数据真丢)。 // -// 修复:扫 git 工作区改动的 .meta,找出「git 版本 border 非 0、工作区被重置成 0」的, -// 把 git 的 border 值还原回去。靠 git 对比拿正确值,只动 border 四个字段、 -// 保留 meta 其它内容;写回用 Cocos 标准格式(2 空格缩进 + 尾随换行)。 +// 修复:扫 git 工作区改动的 .meta,逐个对比 git 版本: +// - 值深度相等(只 key 顺序/格式不同)→ 直接还原成 git 原文,消噪音; +// - 值不等但只是九宫格 border 被重置成 0 → 用 git 的值精准还原 border; +// 还原后若整体已等于 git(差异仅 border + 顺序)也直接写 git 原文,否则只改 border +// 字段、保留 meta 其它真实改动。 // +// 靠 git 对比拿正确值;git 调用统一带 core.quotePath=false,正确处理中文路径。 // 纯 Node + git,跨平台。供 CLI(fix-borders-cmd)和 MCP tool 共用。 // ============================================================ @@ -20,39 +25,45 @@ const cp = require('child_process'); const BORDER_KEYS = ['borderTop', 'borderBottom', 'borderLeft', 'borderRight']; -function git(root, args) { - return cp.execFileSync('git', ['-C', root].concat(args), { - encoding: 'utf8', - maxBuffer: 64 * 1024 * 1024, - }); -} - -// 四个 border 都为 0 / 缺失(被重置的特征) function allZero(ud) { return BORDER_KEYS.every(function (b) { return !ud[b]; }); } -// 至少一个 border 非 0(git 里有正确九宫格值的特征) function anyNonZero(ud) { return BORDER_KEYS.some(function (b) { return !!ud[b]; }); } +// 深度规范化(递归排序对象 key),用于「忽略 key 顺序」的值比较 +function norm(o) { + if (Array.isArray(o)) return o.map(norm); + if (o && typeof o === 'object') { + var r = {}; + Object.keys(o).sort().forEach(function (k) { r[k] = norm(o[k]); }); + return r; + } + return o; +} +function valueEqual(a, b) { return JSON.stringify(norm(a)) === JSON.stringify(norm(b)); } + /** - * 扫 git 改动的 .meta,还原被 Cocos 重置成 0 的九宫格 border。 + * 清理被 Cocos 重启搞乱的 .meta(顺序噪音 + 九宫格 border 重置)。 * @param {string} projectOrRoot 项目路径(git 仓库或其子目录均可) * @param {{dryRun?:boolean}} [opts] - * @returns {{gitRoot:string, scanned:number, fixedFiles:number, fixedFrames:number, details:string[]}} + * @returns {{gitRoot, scanned, reorderFiles, borderFiles, borderFrames, details:string[]}} */ function fixResetBorders(projectOrRoot, opts) { opts = opts || {}; var dryRun = !!opts.dryRun; - var gitRoot; - try { - gitRoot = git(projectOrRoot, ['rev-parse', '--show-toplevel']).trim(); - } catch (e) { - throw new Error('不是 git 仓库(或 git 不可用): ' + projectOrRoot); + // 统一带 core.quotePath=false:否则 git 把中文等非 ASCII 路径加引号转义,path.join 会失配 + function git(root, args) { + return cp.execFileSync('git', ['-C', root, '-c', 'core.quotePath=false'].concat(args), + { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 }); } + var gitRoot; + try { gitRoot = git(projectOrRoot, ['rev-parse', '--show-toplevel']).trim(); } + catch (e) { throw new Error('不是 git 仓库(或 git 不可用): ' + projectOrRoot); } + var changed = git(gitRoot, ['diff', '--name-only', '--', '*.meta']) .split('\n').map(function (s) { return s.trim(); }).filter(Boolean); - var fixedFiles = 0, fixedFrames = 0; + var reorderFiles = 0, borderFiles = 0, borderFrames = 0; var details = []; for (var i = 0; i < changed.length; i++) { @@ -60,10 +71,19 @@ function fixResetBorders(projectOrRoot, opts) { var abs = path.join(gitRoot, rel); if (!fs.existsSync(abs)) continue; - var work, head; - try { work = JSON.parse(fs.readFileSync(abs, 'utf8')); } catch (e) { continue; } - try { head = JSON.parse(git(gitRoot, ['show', 'HEAD:' + rel])); } catch (e) { continue; } // git 里没有(新文件)跳过 + var workStr, headStr, work, head; + try { workStr = fs.readFileSync(abs, 'utf8'); work = JSON.parse(workStr); } catch (e) { continue; } + try { headStr = git(gitRoot, ['show', 'HEAD:' + rel]); head = JSON.parse(headStr); } catch (e) { continue; } // git 里没有(新文件)跳过 + // 1) 纯 key 顺序/格式噪音(值深度相等)→ 还原 git 原文 + if (valueEqual(work, head)) { + if (!dryRun) fs.writeFileSync(abs, headStr, 'utf8'); + reorderFiles++; + details.push('[顺序] ' + rel); + continue; + } + + // 2) 值不等 → 检查九宫格 border 是否被重置成 0,精准还原 var wSubs = work && work.subMetas; var hSubs = head && head.subMetas; if (!wSubs || !hSubs || typeof wSubs !== 'object' || typeof hSubs !== 'object') continue; @@ -76,24 +96,31 @@ function fixResetBorders(projectOrRoot, opts) { var hud = hSubs[key] && hSubs[key].userData; if (!wud || !hud) continue; if (!BORDER_KEYS.some(function (b) { return b in wud; })) continue; - - // 工作区 border 全 0、git 版本有非 0 → 被重置,还原 git 的值 if (allZero(wud) && anyNonZero(hud)) { for (var b = 0; b < BORDER_KEYS.length; b++) wud[BORDER_KEYS[b]] = hud[BORDER_KEYS[b]]; metaChanged = true; - fixedFrames++; - details.push(rel + ' [' + key + '] → T' + hud.borderTop + '/B' + hud.borderBottom + + borderFrames++; + details.push('[border] ' + rel + ' [' + key + '] → T' + hud.borderTop + '/B' + hud.borderBottom + '/L' + hud.borderLeft + '/R' + hud.borderRight); } } if (metaChanged) { - fixedFiles++; - if (!dryRun) fs.writeFileSync(abs, JSON.stringify(work, null, 2) + '\n', 'utf8'); + borderFiles++; + if (!dryRun) { + // 还原 border 后若整体已等于 git(差异仅 border + 顺序)→ 直接写 git 原文,最干净; + // 否则只改了 border 字段、保留其它真实改动 → 按 work 写回(Cocos 标准格式) + if (valueEqual(work, head)) fs.writeFileSync(abs, headStr, 'utf8'); + else fs.writeFileSync(abs, JSON.stringify(work, null, 2) + '\n', 'utf8'); + } } } - return { gitRoot: gitRoot, scanned: changed.length, fixedFiles: fixedFiles, fixedFrames: fixedFrames, details: details }; + return { + gitRoot: gitRoot, scanned: changed.length, + reorderFiles: reorderFiles, borderFiles: borderFiles, borderFrames: borderFrames, + details: details, + }; } module.exports = { fixResetBorders: fixResetBorders }; diff --git a/server/tools.js b/server/tools.js index e1987e5..02d34be 100644 --- a/server/tools.js +++ b/server/tools.js @@ -310,7 +310,7 @@ function defineTools(ctx) { }, { name: 'meta_fix_reset_border', - description: '修复 Cocos 重启把图片九宫格 border(subMetas.*.userData.border*)重置成 0 的 meta:扫 git 改动的 .meta,把「git 有值、工作区被清 0」的 border 还原成 git 的正确值,只动 border 字段、保留 meta 其它改动。dryRun=true 只预览不写。', + description: '清理 Cocos 重启造成的图片 meta 噪音:① 纯 key 顺序/格式变化(值没变只 git diff 噪音)还原成 git 原文;② 九宫格 border(subMetas.*.userData.border*)被重置成 0 的精准还原 git 的值。靠 git 对比、正确处理中文路径。dryRun=true 只预览不写。', inputSchema: { type: 'object', properties: { dryRun: { type: 'boolean', description: 'true=只预览不写文件,列出将还原的 meta' } },