跨平台化:移除 AppleScript + 去 macOS 硬编码

- 删除浏览器交互的 AppleScript(osascript 控 Chrome/Safari 做截图/eval/刷新/打开),
  改由外部 playwright MCP 承担;保留走 Editor.Message 的跨平台编辑器操作
- openDevDir 打开命令按平台分支(mac=open / win=explorer / linux=xdg-open)
- execPath 解析弱化靠注册表/进程查询,删掉 /Applications macOS 硬编码兜底
- editor-control 去 macOS-only 假设,ps 进程查询按平台分支(win 用 wmic)
- build-cmd Cocos 安装路径按平台拼

Win 特定逻辑(wmic、CocosDashboard 安装路径)标 TODO[win-verify],待 Windows 实测补全。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
furao
2026-06-07 17:54:05 +08:00
parent 33b90dab22
commit bee9f89c98
6 changed files with 67 additions and 407 deletions
+15 -3
View File
@@ -41,15 +41,27 @@ function parseArgs(rest) {
return a;
}
// 解析 CocosCreator 可执行:--cocos 显式优先,否则 --version 拼标准安装路径(macOS
// 按平台拼 Cocos Creator 标准安装路径。Win/Linux 路径规律待目标平台实测,建议优先用 --cocos 显式。
function cocosStdPath(ver) {
if (process.platform === 'darwin') {
return `/Applications/Cocos/Creator/${ver}/CocosCreator.app/Contents/MacOS/CocosCreator`;
}
if (process.platform === 'win32') {
// TODO[win-verify]: Win 上 CocosDashboard 安装路径待实测确认(下面是常见默认,未验证)
return `C:\\ProgramData\\cocos\\editors\\Creator\\${ver}\\CocosCreator.exe`;
}
return ''; // linux 等:无标准约定,要求 --cocos 显式
}
// 解析 CocosCreator 可执行:--cocos 显式优先,否则 --version 拼标准安装路径
function resolveCocos(a) {
if (a.cocos) {
if (!fs.existsSync(a.cocos)) die(`--cocos 路径不存在: ${a.cocos}`);
return a.cocos;
}
if (a.version) {
const p = `/Applications/Cocos/Creator/${a.version}/CocosCreator.app/Contents/MacOS/CocosCreator`;
if (!fs.existsSync(p)) die(`版本 ${a.version} 不在标准路径: ${p}\n 用 --cocos <可执行绝对路径> 显式指定`);
const p = cocosStdPath(a.version);
if (!p || !fs.existsSync(p)) die(`版本 ${a.version} 不在标准安装路径${p ? ': ' + p : ''}\n 用 --cocos <可执行绝对路径> 显式指定`);
return p;
}
die('需指定 CocosCreator 可执行:--cocos <path> 或 --version <如 3.8.8>');
+17 -241
View File
@@ -128,17 +128,6 @@ async function getPreviewUrl() {
return 'http://localhost:unknown-port';
}
/**
* 生成 AppleScript 匹配预览 tab 的条件:只按端口匹配、忽略 host。
* 同一预览实例在多网卡 / 切换网络时 host(IP) 会变,但端口稳定(per-project 编辑器分配)。
* 避免字面比完整 IP:port 在环境切换后失配(截图退化全屏、eval 报找不到 tab)。
*/
function tabMatchClause(url) {
var m = url && url.match(/:(\d+)(?:\/|$)/);
var port = m ? m[1] : '';
return port ? ('URL of t contains ":' + port + '/"') : ('URL of t starts with "' + url + '"');
}
function sleep(ms) {
return new Promise(function (resolve) { setTimeout(resolve, ms); });
}
@@ -161,140 +150,6 @@ async function doReloadScene() {
log('scene reloaded.');
}
/**
* 在浏览器中打开预览
*/
async function doOpenPreview() {
var url = await getPreviewUrl();
// 先查有没有该端口的 tab,有就不重开——避免在已有(带 tid 的)预览 tab 之外再冒一个裸地址 tab
var checkScript = [
'tell application "Google Chrome"',
' repeat with w in windows',
' repeat with t in tabs of w',
' if ' + tabMatchClause(url) + ' then return "FOUND"',
' end repeat',
' end repeat',
' return "NONE"',
'end tell'
].join('\n');
return new Promise(function (resolve) {
exec('osascript -e \'' + checkScript.replace(/'/g, "'\\''") + '\'', function (err, stdout) {
if (!err && (stdout || '').trim() === 'FOUND') {
log('preview tab already open (port matched), skip open');
resolve();
} else {
log('opening preview: ' + url);
exec('open "' + url + '"', function () { resolve(); });
}
});
});
}
/**
* 刷新已打开的预览浏览器页面
*/
async function doRefreshPreview() {
var url = await getPreviewUrl();
log('refreshing preview browser...');
// 用 AppleScript 找到预览页面并刷新
var script = [
'tell application "Google Chrome"',
' set found to false',
' repeat with w in windows',
' repeat with t in tabs of w',
' if ' + tabMatchClause(url) + ' then',
' tell t to reload',
' set found to true',
' end if',
' end repeat',
' end repeat',
' if not found then',
' open location "' + url + '"',
' end if',
'end tell'
].join('\n');
return new Promise(function (resolve) {
exec('osascript -e \'' + script.replace(/'/g, "'\\''") + '\'', function (err) {
if (err) {
log('Chrome refresh failed, trying Safari...');
var safariScript = [
'tell application "Safari"',
' set found to false',
' repeat with w in windows',
' repeat with t in tabs of w',
' if ' + tabMatchClause(url) + ' then',
' tell t to do JavaScript "location.reload()"',
' set found to true',
' end if',
' end repeat',
' end repeat',
' if not found then',
' open location "' + url + '"',
' end if',
'end tell'
].join('\n');
exec('osascript -e \'' + safariScript.replace(/'/g, "'\\''") + '\'', function () {
resolve();
});
} else {
log('preview refreshed.');
resolve();
}
});
});
}
/**
* 截取预览浏览器页面的截图
*/
async function doScreenshot(outputPath) {
var url = await getPreviewUrl();
log('taking screenshot → ' + outputPath);
return new Promise(function (resolve) {
// 等待渲染
setTimeout(function () {
// 优先用 Chrome DevTools Protocol 截图(更精准)
var chromeScript = [
'tell application "Google Chrome"',
' repeat with w in windows',
' repeat with t in tabs of w',
' if ' + tabMatchClause(url) + ' then',
' set index of w to 1',
' set active tab index of w to (index of t)',
' delay 0.5',
' return id of w',
' end if',
' end repeat',
' end repeat',
' return ""',
'end tell'
].join('\n');
exec('osascript -e \'' + chromeScript.replace(/'/g, "'\\''") + '\'', function (err, stdout) {
var windowId = stdout ? stdout.trim() : '';
if (windowId) {
// 截取 Chrome 窗口
exec('screencapture -o -l ' + windowId + ' "' + outputPath + '"', function (err2) {
if (err2) {
log('window capture failed, fallback to full screen');
exec('screencapture -o "' + outputPath + '"', function () { resolve(); });
} else {
log('screenshot saved (browser window).');
resolve();
}
});
} else {
// 降级:截取整个屏幕
log('preview tab not found, capturing full screen');
exec('screencapture -o "' + outputPath + '"', function () { resolve(); });
}
});
}, 1000);
});
}
// ── 消息处理(支持从其他扩展或命令行调用) ──
exports.methods = {
@@ -302,11 +157,6 @@ exports.methods = {
await doRefreshAssets();
await doReloadScene();
},
async screenshot() {
var outputPath = path.join(Editor.Project.path, '.dev', 'screenshot.png');
await doScreenshot(outputPath);
return outputPath;
},
async queryPreviewUrl() {
var url = await getPreviewUrl();
// 顺便刷新 dev-reload-info.json,让外部脚本拿到最新端口
@@ -342,11 +192,10 @@ exports.methods = {
},
};
},
/** Panel 使用:刷新资源 + 重载场景 + 刷新预览 */
/** Panel 使用:刷新资源 + 重载场景 */
async triggerRefresh() {
await doRefreshAssets();
await doReloadScene();
await doRefreshPreview();
return true;
},
/** Panel 使用:重新导入指定 assetUrl */
@@ -359,7 +208,9 @@ exports.methods = {
openDevDir() {
var devDir = path.join(Editor.Project.path, DEV_DIR);
if (!fs.existsSync(devDir)) fs.mkdirSync(devDir, { recursive: true });
exec('open "' + devDir + '"');
// 跨平台在系统文件管理器打开:mac=open / win=explorer / linux=xdg-open
var opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
exec(opener + ' "' + devDir + '"');
return devDir;
},
/** Panel 使用:只做场景软重载 */
@@ -368,23 +219,6 @@ exports.methods = {
await doReloadScene();
return true;
},
/** Panel 使用:在浏览器中打开预览 */
async openPreview() {
pushCommandLog('panel', 'open-preview');
await doOpenPreview();
return true;
},
/** Panel 使用:截图并把路径复制到剪贴板 */
async screenshotCopy() {
pushCommandLog('panel', 'screenshot');
var outputPath = path.join(Editor.Project.path, DEV_DIR, 'screenshot.png');
await doScreenshot(outputPath);
return new Promise(function (resolve) {
exec('printf %s "' + outputPath.replace(/"/g, '\\"') + '" | pbcopy', function () {
resolve(outputPath);
});
});
},
/** Panel 使用:清理 .dev 临时产物(保留 dev-reload-info.json / dev-reload-panel.json / cc-mcp-panel.json */
cleanDevDir() {
pushCommandLog('panel', 'clean-dev');
@@ -404,56 +238,6 @@ exports.methods = {
} catch (e) { /* ignore */ }
return removed;
},
/** Panel 使用:向预览 Chrome 页面注入 JS 代码并返回执行结果 */
async evalInPreview(code) {
if (!code) return { ok: false, error: 'empty code' };
pushCommandLog('panel', 'eval:' + code.slice(0, 40));
var url = await getPreviewUrl();
// AppleScript 需要把 JS 代码里的双引号转义
// 包一层 window.app 校验:连到的 tab 不是游戏(编辑器内嵌预览 / 错 tab)就明确报错,不默默 eval 错上下文
var guarded = '(function(){ if(typeof window==="undefined"||!window.app){return "__NO_GAME_CONTEXT__";} return (' + code + '); })()';
var escaped = guarded.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
var script = [
'tell application "Google Chrome"',
' repeat with w in windows',
' repeat with t in tabs of w',
' if ' + tabMatchClause(url) + ' then',
' return (execute t javascript "' + escaped + '")',
' end if',
' end repeat',
' end repeat',
' return "__NO_PREVIEW_TAB__"',
'end tell'
].join('\n');
return new Promise(function (resolve) {
exec('osascript -e \'' + script.replace(/'/g, "'\\''") + '\'', function (err, stdout, stderr) {
if (err) {
resolve({ ok: false, error: (stderr || err.message || '').trim() });
} else {
var out = (stdout || '').trim();
if (out === '__NO_PREVIEW_TAB__') {
resolve({ ok: false, error: '未找到预览标签页(先在 Chrome 打开 ' + url + '' });
} else if (out === '__NO_GAME_CONTEXT__') {
resolve({ ok: false, error: '连到的 tab 没有游戏上下文(window.app undefined)——多半连的是编辑器内嵌预览或别的 tab。游戏要在浏览器跑且 app.ts 已加载;必要时用 playwright 直连游戏 tab' });
} else {
resolve({ ok: true, result: out });
}
}
});
});
},
/** Panel 使用:读取用户自定义的 debug 按钮配置 */
getDebugButtons() {
var cfgPath = path.join(Editor.Project.path, PANEL_CONFIG_FILE);
if (!fs.existsSync(cfgPath)) return [];
try {
var cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
if (Array.isArray(cfg.buttons)) return cfg.buttons;
return [];
} catch (e) {
return [];
}
},
/** Panel 使用:扫同机其他 worktree 的 dev-reload-info.json */
listWorktrees() {
var results = [];
@@ -575,15 +359,13 @@ function getProjectShortName() {
/**
* 解析 Cocos 编辑器主进程可执行路径,写进注册文件供 router 的 editor_restart 拉起用。
* 优先 process.argv[0](Electron 主进程启动命令首段,即 .app 可执行),按 version 拼标准路径兜底
* 排除 Helper(渲染/GPU 子进程路径),要外层主可执行。解析不到返回空串,router 端还有 ps / version 两级 fallback
* process.argv[0] / process.execPath(编辑器主进程可执行,跨平台)
* 排除 Helper(渲染/GPU 子进程),解析不到返回空串
*/
function getEditorExecPath() {
var candidates = [];
try { if (process.argv && process.argv[0]) candidates.push(process.argv[0]); } catch (e) { /* ignore */ }
try { if (process.execPath) candidates.push(process.execPath); } catch (e) { /* ignore */ }
var ver = (Editor.App && Editor.App.version) ? Editor.App.version : '';
if (ver) candidates.push('/Applications/Cocos/Creator/' + ver + '/CocosCreator.app/Contents/MacOS/CocosCreator');
for (var i = 0; i < candidates.length; i++) {
var c = candidates[i];
if (c && /CocosCreator/.test(c) && c.indexOf('Helper') < 0) {
@@ -699,20 +481,13 @@ function buildToolCtx() {
local: {
getPreviewUrl: getPreviewUrl,
doReimport: doReimport,
doRefreshPreview: doRefreshPreview,
doOpenPreview: doOpenPreview,
doScreenshot: async function (outputPath) {
var p = outputPath || path.join(Editor.Project.path, DEV_DIR, 'screenshot.png');
await doScreenshot(p);
return p;
},
doRefreshAssets: doRefreshAssets,
doReloadScene: doReloadScene,
evalInPreview: function (code) { return exports.methods.evalInPreview(code); },
listWorktrees: function () { return exports.methods.listWorktrees(); },
openDevDir: function () { return exports.methods.openDevDir(); },
cleanDevDir: function () { return exports.methods.cleanDevDir(); },
getStatus: function () { return exports.methods.getStatus(); },
reloadPackage: doRestartSelf,
},
};
}
@@ -748,13 +523,13 @@ async function doOpenPrefab(urlOrPath) {
log('open-prefab: opened ' + dbUrl + ' (uuid=' + uuid + ')');
}
/** 重启整个插件(disable → enable)让 main.js / tools.js / server/* 的代码改动生效
* 注意:本函数自身处于即将被卸载的 main.js 上下文,必须 fire-and-forget。
/** 重启指定插件(disable → enable)让其 JS 代码改动生效;name 缺省为本插件 cc-3-8-x-mcp
* 注意:reload 本插件自身时,本函数处于即将被卸载的 main.js 上下文,必须 fire-and-forget。
* Editor.Package.disable 是 host 进程 API,扩展沙箱卸载后仍然有效;
* enable 在 disable 完成后用 setTimeout 触发,给 unload 收尾留窗口。
*/
function doRestartSelf() {
var name = 'cc-3-8-x-mcp';
function doRestartSelf(name) {
name = name || 'cc-3-8-x-mcp';
log('restart-package: scheduling disable → enable for ' + name);
setImmediate(function () {
Promise.resolve()
@@ -771,18 +546,19 @@ function doRestartSelf() {
/**
* 分发单条 refresh 命令。
*
* 协议精简:只支持 `restart-package`disable→enable 整个扩展让 JS 代码改动生效)。
* 协议`restart-package [name]`disable→enable 指定扩展让 JS 改动生效;缺省 name 重启本插件自身)。
* 资源刷新 / 场景重载 / 预览刷新 / 截图等走 MCP toolpreview_refresh_and_reload /
* asset_reimport / preview_screenshot 等)或面板按钮,不再通过文件协议触发。
* asset_reimport 等)或面板按钮,不再通过文件协议触发。
*/
async function handleRefreshCommand(cmd) {
if (!cmd) return;
pushCommandLog('refresh', cmd);
if (cmd === 'restart-package') {
doRestartSelf();
var parts = cmd.split(/\s+/);
if (parts[0] === 'restart-package') {
doRestartSelf(parts[1]); // parts[1] 可选:缺省重启自身,指定则 reload 该扩展
return;
}
log('refresh: unknown command — ' + cmd + '(仅支持 restart-package');
log('refresh: unknown command — ' + cmd + '(仅支持 restart-package [name]');
}
/** 启动 .dev/refresh 文件 watcher(写入命令 → 读取 → 执行 → 清空) */
-5
View File
@@ -40,16 +40,11 @@
"get-status": { "methods": ["getStatus"] },
"get-mcp-config": { "methods": ["getMcpConfig"] },
"refresh-assets": { "methods": ["refreshAssets"] },
"screenshot": { "methods": ["screenshot"] },
"query-preview-url": { "methods": ["queryPreviewUrl"] },
"trigger-refresh": { "methods": ["triggerRefresh"] },
"trigger-reimport": { "methods": ["triggerReimport"] },
"soft-reload-scene": { "methods": ["softReloadScene"] },
"open-preview": { "methods": ["openPreview"] },
"screenshot-copy": { "methods": ["screenshotCopy"] },
"clean-dev-dir": { "methods": ["cleanDevDir"] },
"eval-in-preview": { "methods": ["evalInPreview"] },
"get-debug-buttons": { "methods": ["getDebugButtons"] },
"list-worktrees": { "methods": ["listWorktrees"] },
"open-dev-dir": { "methods": ["openDevDir"] }
}
+2 -73
View File
@@ -32,11 +32,9 @@ exports.template = /* html */ `
<section class="actions">
<header>快捷动作</header>
<div class="btn-grid">
<ui-button id="btnRefresh">刷新(资源+场景+预览</ui-button>
<ui-button id="btnRefresh">刷新(资源+场景)</ui-button>
<ui-button id="btnSoftReload">仅软重载场景</ui-button>
<ui-button id="btnOpenPreview">打开预览浏览器</ui-button>
<ui-button id="btnQueryUrl">查询预览地址</ui-button>
<ui-button id="btnScreenshot">截图 → 复制路径</ui-button>
<ui-button id="btnOpenDev">打开 .dev 目录</ui-button>
<ui-button id="btnClean">清理 .dev 临时文件</ui-button>
<ui-button id="btnRefreshStatus" class="secondary">刷新状态</ui-button>
@@ -48,17 +46,6 @@ exports.template = /* html */ `
</div>
</section>
<section class="debug">
<header>Debug 注入</header>
<div class="eval-row">
<ui-input id="evalInput" placeholder='console.log(app.userMod.getUserValue(0))' class="grow"></ui-input>
<ui-button id="btnEval">执行</ui-button>
</div>
<pre id="evalResult" class="eval-result"></pre>
<div id="debugButtons" class="debug-buttons"></div>
<div class="hint-small">自定义按钮配置:<code>.dev/cc-mcp-panel.json</code>(或旧名 <code>.dev/dev-reload-panel.json</code>)→ <code>{ "buttons": [{ "label": "...", "code": "..." }] }</code></div>
</section>
<section class="worktrees">
<header>同机 Worktree</header>
<div id="worktreeList" class="wt-list">-</div>
@@ -129,18 +116,12 @@ exports.$ = {
probeDot: '#probeDot',
btnRefresh: '#btnRefresh',
btnSoftReload: '#btnSoftReload',
btnOpenPreview: '#btnOpenPreview',
btnQueryUrl: '#btnQueryUrl',
btnScreenshot: '#btnScreenshot',
btnOpenDev: '#btnOpenDev',
btnClean: '#btnClean',
btnRefreshStatus: '#btnRefreshStatus',
btnReimport: '#btnReimport',
reimportInput: '#reimportInput',
evalInput: '#evalInput',
btnEval: '#btnEval',
evalResult: '#evalResult',
debugButtons: '#debugButtons',
worktreeList: '#worktreeList',
logList: '#logList',
toast: '#toast',
@@ -203,7 +184,6 @@ exports.methods = {
this.showToast('状态获取失败: ' + (e.message || e));
}
this.refreshWorktrees();
this.refreshDebugButtons();
},
async probePreview(url) {
@@ -237,43 +217,11 @@ exports.methods = {
} catch (e) { /* ignore */ }
},
async refreshDebugButtons() {
try {
const btns = await Editor.Message.request('cc-3-8-x-mcp', 'get-debug-buttons');
if (!Array.isArray(btns) || !btns.length) {
this.$.debugButtons.innerHTML = '';
return;
}
this.$.debugButtons.innerHTML = '';
btns.forEach(cfg => {
if (!cfg || !cfg.label || !cfg.code) return;
const btn = document.createElement('ui-button');
btn.textContent = cfg.label;
btn.addEventListener('confirm', () => this.runEval(cfg.code, cfg.label));
this.$.debugButtons.appendChild(btn);
});
} catch (e) { /* ignore */ }
},
async runEval(code, label) {
this.showToast('执行: ' + (label || code.slice(0, 30)));
try {
const r = await Editor.Message.request('cc-3-8-x-mcp', 'eval-in-preview', code);
if (r && r.ok) {
this.$.evalResult.textContent = '✓ ' + (r.result || '(no return)');
} else {
this.$.evalResult.textContent = '✗ ' + ((r && r.error) || 'unknown');
}
} catch (e) {
this.$.evalResult.textContent = '✗ ' + (e.message || e);
}
},
async onRefreshClick() {
this.showToast('刷新中…');
try {
await Editor.Message.request('cc-3-8-x-mcp', 'trigger-refresh');
this.showToast('已刷新资源+场景+预览');
this.showToast('已刷新资源+场景');
this.refreshStatus();
} catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
@@ -281,10 +229,6 @@ exports.methods = {
try { await Editor.Message.request('cc-3-8-x-mcp', 'soft-reload-scene'); this.showToast('场景已软重载'); }
catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
async onOpenPreviewClick() {
try { await Editor.Message.request('cc-3-8-x-mcp', 'open-preview'); this.showToast('已在浏览器打开预览'); }
catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
async onQueryUrlClick() {
try {
const url = await Editor.Message.request('cc-3-8-x-mcp', 'query-preview-url');
@@ -292,13 +236,6 @@ exports.methods = {
this.refreshStatus();
} catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
async onScreenshotClick() {
this.showToast('截图中…');
try {
const p = await Editor.Message.request('cc-3-8-x-mcp', 'screenshot-copy');
this.showToast('截图路径已复制: ' + p);
} catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
async onOpenDevClick() {
try { await Editor.Message.request('cc-3-8-x-mcp', 'open-dev-dir'); this.showToast('已打开 .dev'); }
catch (e) { this.showToast('失败: ' + (e.message || e)); }
@@ -341,11 +278,6 @@ exports.methods = {
this.refreshStatus();
} catch (e) { this.showToast('失败: ' + (e.message || e)); }
},
async onEvalClick() {
const code = (this.$.evalInput.value || '').trim();
if (!code) { this.showToast('请输入 JS 代码'); return; }
await this.runEval(code);
},
};
function escapeHtml(s) {
@@ -357,14 +289,11 @@ function escapeHtml(s) {
exports.ready = function () {
this.$.btnRefresh.addEventListener('confirm', () => this.onRefreshClick());
this.$.btnSoftReload.addEventListener('confirm', () => this.onSoftReloadClick());
this.$.btnOpenPreview.addEventListener('confirm', () => this.onOpenPreviewClick());
this.$.btnQueryUrl.addEventListener('confirm', () => this.onQueryUrlClick());
this.$.btnScreenshot.addEventListener('confirm', () => this.onScreenshotClick());
this.$.btnOpenDev.addEventListener('confirm', () => this.onOpenDevClick());
this.$.btnClean.addEventListener('confirm', () => this.onCleanClick());
this.$.btnRefreshStatus.addEventListener('confirm', () => this.refreshStatus());
this.$.btnReimport.addEventListener('confirm', () => this.onReimportClick());
this.$.btnEval.addEventListener('confirm', () => this.onEvalClick());
this.$.btnCopyMcpUrl.addEventListener('confirm', () => this.onCopyMcpUrl());
this.$.btnCopyCli.addEventListener('confirm', () => this.onCopyCli());
this.$.btnRestartMcp.addEventListener('confirm', () => this.onRestartMcp());
+18 -40
View File
@@ -15,7 +15,7 @@
*
* [editor] tool 命名不加 shortName 前缀(router 全局工具)。
*
* 仅支持 macOSexecPath 解析按 /Applications/Cocos/Creator/<version>/ 规律)
* 跨平台:execPath 优先用注册表(编辑器写入)/ 运行进程查询,不硬编码平台安装路径
*/
var fs = require('fs');
@@ -141,21 +141,25 @@ function resolveTarget(args) {
/** 从运行中进程的命令行抓可执行路径(编辑器启动命令首段,--project 之前) */
function execPathFromPs(pid) {
try {
if (process.platform === 'win32') {
// TODO[win-verify]: Win 没有 ps。下面用 wmic 拿可执行路径,需在 Win 上实测确认(新版 Win 可能要改 PowerShell Get-CimInstance
var winOut = cp.execFileSync('wmic', ['process', 'where', 'processid=' + pid, 'get', 'ExecutablePath', '/value'], { encoding: 'utf-8' });
var wm = winOut.match(/ExecutablePath=(.+)/);
return wm ? wm[1].trim() : '';
}
// mac / linux: ps 抓命令行首段(--project 之前)
var out = cp.execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8' }).trim();
if (!out) return '';
// 形如:/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/MacOS/CocosCreator --project /x ...
// 可执行路径本身不含 " --",按它切分安全
var idx = out.indexOf(' --');
return (idx >= 0 ? out.slice(0, idx) : out.split(/\s+/)[0]).trim();
} catch (e) { return ''; }
}
/**
* 解析编辑器可执行路径,级 fallback
* 解析编辑器可执行路径,级 fallback
* 1. 注册文件 execPath 字段(main.js 写入,最准)
* 2. 从活进程 ps 命令行抓(编辑器还活着时)—— restart 会在 kill 前调用,此时旧进程还在
* 3. 按 editorVersion 拼标准安装路径
* 全部失败抛错,提示带上尝试过的路径。
* 2. 从活进程命令行抓(编辑器还活着时)—— restart 会在 kill 前调用,此时旧进程还在
* 全部失败抛错。
*/
function resolveExecPath(entry) {
if (entry.execPath && fs.existsSync(entry.execPath)) return entry.execPath;
@@ -165,18 +169,11 @@ function resolveExecPath(entry) {
if (fromPs && fs.existsSync(fromPs)) return fromPs;
}
if (entry.editorVersion) {
var guess = '/Applications/Cocos/Creator/' + entry.editorVersion +
'/CocosCreator.app/Contents/MacOS/CocosCreator';
if (fs.existsSync(guess)) return guess;
}
throw new Error(
'editor-control: 无法解析 Cocos 编辑器可执行路径。\n' +
' 注册文件 execPath: ' + (entry.execPath || '(无)') + '\n' +
' editorVersion: ' + (entry.editorVersion || '(无)') + '\n' +
'请确认 Cocos Creator 装在标准路径 /Applications/Cocos/Creator/<version>/' +
'或重启编辑器让扩展写入 execPath 字段后再试。'
'请重启编辑器让扩展写入 execPath 字段,或用 editor_spawn 显式传 execPath。'
);
}
@@ -223,38 +220,19 @@ async function killEditor(pid, opts) {
}
/**
* 冷启动场景解析 execPath:进程已不在,没有活进程可 ps 抓,靠级 fallback
* 冷启动场景解析 execPath:进程已不在,没有活进程可 ps 抓,靠级 fallback
* 1. args.execPath 显式
* 2. args.version 拼标准路径
* 3. 借任意一条注册 entry 的 execPath —— execPath 是机器级安装路径,跨项目通用,
* 2. 借任意一条注册 entry 的 execPath —— execPath 是机器级安装路径,跨项目通用、跨平台,
* 哪怕那条 entry 是别的项目 / 已 stale 也能用
* 4. 扫 /Applications/Cocos/Creator 下唯一安装版本
*/
function resolveExecPathForSpawn(args, projectPath) {
if (args.execPath && fs.existsSync(args.execPath)) return args.execPath;
if (args.version) {
var byVer = '/Applications/Cocos/Creator/' + args.version + '/CocosCreator.app/Contents/MacOS/CocosCreator';
if (fs.existsSync(byVer)) return byVer;
}
// 借任意一条注册 entry 的 execPath(机器级安装路径,跨项目通用、跨平台)
var borrowed = readRegistryEntries().filter(function (e) { return e.execPath && fs.existsSync(e.execPath); })[0];
if (borrowed) return borrowed.execPath;
var base = '/Applications/Cocos/Creator';
try {
var vers = fs.readdirSync(base).filter(function (v) {
return fs.existsSync(base + '/' + v + '/CocosCreator.app/Contents/MacOS/CocosCreator');
});
if (vers.length === 1) return base + '/' + vers[0] + '/CocosCreator.app/Contents/MacOS/CocosCreator';
if (vers.length > 1) {
throw new Error('editor_spawn: ' + base + ' 下有多个版本 [' + vers.join(', ') + '],请用 version 指定要启动哪个。');
}
} catch (e) {
if (/多个版本/.test(e.message)) throw e;
}
throw new Error('editor_spawn: 无法解析 Cocos 可执行路径(execPath/version 都没给,注册表也无可借项)。请传 execPath 或 version。');
throw new Error('editor_spawn: 无法解析 Cocos 可执行路径。请传 execPath(注册表无可借项时无法推断安装路径)。');
}
/**
@@ -387,7 +365,7 @@ var EDITOR_TOOLS = [
{
name: 'editor_restart',
description: '[editor] 重启 Cocos 编辑器进程(kill 旧实例 → 重新拉起 → 等就绪)。' +
'不需要编辑器在运行也能调(挂 router 进程)。仅 macOS。' +
'不需要编辑器在运行也能调(挂 router 进程)。' +
'返回 oldPid / launchedPid / kill 结果 / ready 状态(含新 pid·port·url)。',
inputSchema: {
type: 'object',
@@ -433,7 +411,7 @@ var EDITOR_TOOLS = [
{
name: 'editor_spawn',
description: '[editor] 从零启动一个 Cocos 编辑器(进程完全不在时用,如崩溃后恢复)。' +
'同项目已有活跃实例则直接返回不重复开(Cocos 不支持同项目多开)。仅 macOS。',
'同项目已有活跃实例则直接返回不重复开(Cocos 不支持同项目多开)。',
inputSchema: {
type: 'object',
properties: {
+15 -45
View File
@@ -8,7 +8,7 @@
function defineTools(ctx) {
var msg = ctx.msg; // async (target, name, ...args) => result
var local = ctx.local; // { getPreviewUrl, doReimport, doRefreshPreview, doOpenPreview, doScreenshot, doRefreshAssets, doReloadScene, evalInPreview, listWorktrees, openDevDir, cleanDevDir, getStatus, getPanelConfig }
var local = ctx.local; // { getPreviewUrl, doReimport, doRefreshAssets, doReloadScene, listWorktrees, openDevDir, cleanDevDir, getStatus }
return [
// ── scene 域 ──
@@ -251,61 +251,31 @@ function defineTools(ctx) {
return { url: await local.getPreviewUrl() };
},
},
{
name: 'preview_open_browser',
description: '在系统默认浏览器打开预览',
inputSchema: { type: 'object', properties: {} },
handler: async function () {
await local.doOpenPreview();
return 'ok';
},
},
{
name: 'preview_refresh_browser',
description: '刷新已打开的预览浏览器页面(AppleScript 驱动 Chrome/Safari',
inputSchema: { type: 'object', properties: {} },
handler: async function () {
await local.doRefreshPreview();
return 'ok';
},
},
{
name: 'preview_screenshot',
description: '截图预览页面到指定路径(默认 .dev/screenshot.png),返回路径',
inputSchema: {
type: 'object',
properties: { outputPath: { type: 'string' } },
},
handler: async function (args) {
var p = args.outputPath || null;
return await local.doScreenshot(p);
},
},
{
name: 'preview_eval_js',
description: '向预览 Chrome 页面注入 JS 代码并返回执行结果',
inputSchema: {
type: 'object',
properties: { code: { type: 'string' } },
required: ['code'],
},
handler: async function (args) {
return await local.evalInPreview(args.code);
},
},
{
name: 'preview_refresh_and_reload',
description: '一键:刷新资源 + 软重载场景 + 刷新预览浏览器',
description: '一键:刷新资源 + 软重载场景',
inputSchema: { type: 'object', properties: {} },
handler: async function () {
await local.doRefreshAssets();
await local.doReloadScene();
await local.doRefreshPreview();
return 'ok';
},
},
// ── local 域 ──
{
name: 'local_reload_package',
description: 'reloaddisable→enable)指定编辑器扩展,让其 JS 代码改动生效,无需重启编辑器。本质 Editor.Package.disable→enablefire-and-forget 立即返回。',
inputSchema: {
type: 'object',
properties: { name: { type: 'string', description: '扩展名(package.json 的 name,如 state-ctrl-gen' } },
required: ['name'],
},
handler: async function (args) {
local.reloadPackage(args.name);
return { ok: true, reloaded: args.name };
},
},
{
name: 'local_get_status',
description: '获取插件本地状态(git 分支/HEAD、watchers、预览、命令日志)',