From bee9f89c989cea68ad3046475e5098c436ca4554 Mon Sep 17 00:00:00 2001 From: furao Date: Sun, 7 Jun 2026 17:54:05 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E5=B9=B3=E5=8F=B0=E5=8C=96=EF=BC=9A?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20AppleScript=20+=20=E5=8E=BB=20macOS=20?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除浏览器交互的 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) --- cli/src/cli/build-cmd.js | 18 ++- main.js | 258 +++-------------------------------- package.json | 5 - panel/index.js | 75 +--------- router/src/editor-control.js | 58 +++----- server/tools.js | 60 ++------ 6 files changed, 67 insertions(+), 407 deletions(-) diff --git a/cli/src/cli/build-cmd.js b/cli/src/cli/build-cmd.js index 506ffd0..db86ea3 100644 --- a/cli/src/cli/build-cmd.js +++ b/cli/src/cli/build-cmd.js @@ -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 或 --version <如 3.8.8>'); diff --git a/main.js b/main.js index ff95e22..acac90a 100644 --- a/main.js +++ b/main.js @@ -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 tool(preview_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(写入命令 → 读取 → 执行 → 清空) */ diff --git a/package.json b/package.json index 6f4866e..6c574bc 100644 --- a/package.json +++ b/package.json @@ -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"] } } diff --git a/panel/index.js b/panel/index.js index 5e92ab1..18a9d20 100644 --- a/panel/index.js +++ b/panel/index.js @@ -32,11 +32,9 @@ exports.template = /* html */ `
快捷动作
- 刷新(资源+场景+预览) + 刷新(资源+场景) 仅软重载场景 - 打开预览浏览器 查询预览地址 - 截图 → 复制路径 打开 .dev 目录 清理 .dev 临时文件 刷新状态 @@ -48,17 +46,6 @@ exports.template = /* html */ `
-
-
Debug 注入
-
- - 执行 -
-

-    
-
自定义按钮配置:.dev/cc-mcp-panel.json(或旧名 .dev/dev-reload-panel.json)→ { "buttons": [{ "label": "...", "code": "..." }] }
-
-
同机 Worktree
-
@@ -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()); diff --git a/router/src/editor-control.js b/router/src/editor-control.js index ecca4df..0acc1b5 100644 --- a/router/src/editor-control.js +++ b/router/src/editor-control.js @@ -15,7 +15,7 @@ * * [editor] tool 命名不加 shortName 前缀(router 全局工具)。 * - * 仅支持 macOS(execPath 解析按 /Applications/Cocos/Creator// 规律)。 + * 跨平台: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//,' + - '或重启编辑器让扩展写入 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: { diff --git a/server/tools.js b/server/tools.js index 175eb41..706d2e4 100644 --- a/server/tools.js +++ b/server/tools.js @@ -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: 'reload(disable→enable)指定编辑器扩展,让其 JS 代码改动生效,无需重启编辑器。本质 Editor.Package.disable→enable,fire-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、预览、命令日志)',