'use strict'; // Cocos MCP 功能面板 // MCP 状态 / 编辑器状态 / 快捷动作 / Debug 注入 / 命令日志 / 同机 worktree exports.template = /* html */ `
MCP Server
-
-
-
0
复制端点 复制 CLI 命令 重启
编辑器
-
-
-
-
-
-
-
快捷动作
刷新(资源+场景+预览) 仅软重载场景 打开预览浏览器 查询预览地址 截图 → 复制路径 打开 .dev 目录 清理 .dev 临时文件 刷新状态
导入
Debug 注入
执行

    
自定义按钮配置:.dev/cc-mcp-panel.json(或旧名 .dev/dev-reload-panel.json)→ { "buttons": [{ "label": "...", "code": "..." }] }
同机 Worktree
-
最近命令日志
-
`; exports.style = /* css */ ` :host { display: flex; flex: 1; } .wrap { display: flex; flex-direction: column; padding: 12px; gap: 14px; font-size: 12px; flex: 1; overflow: auto; position: relative; } section header { font-weight: bold; margin-bottom: 6px; opacity: 0.75; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; display: flex; align-items: center; gap: 6px; } .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .dot.gray { background: #888; } .dot.green { background: #3ddc84; } .dot.red { background: #e45; } .status .row { display: flex; justify-content: space-between; padding: 3px 0; border-bottom: 1px dashed rgba(255,255,255,0.08); } .status .row label { opacity: 0.6; } .status .row span { font-family: monospace; max-width: 65%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } .btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .btn-grid ui-button { width: 100%; } .btn-grid .secondary { opacity: 0.7; grid-column: span 2; } .mcp-actions { display: flex; gap: 6px; margin-top: 8px; } .mcp-actions ui-button { flex: 1; } .mcp-actions .secondary { opacity: 0.75; } .reimport-row, .eval-row { display: flex; gap: 6px; margin-top: 6px; } .grow { flex: 1; } .eval-result { max-height: 120px; overflow: auto; background: rgba(0,0,0,0.3); padding: 6px 8px; border-radius: 3px; font-family: monospace; font-size: 11px; white-space: pre-wrap; word-break: break-all; margin: 6px 0 0; min-height: 0; } .eval-result:empty { display: none; } .debug-buttons { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } .debug-buttons ui-button { font-size: 11px; } .hint-small { opacity: 0.55; font-size: 10px; margin-top: 4px; } .hint-small code { background: rgba(255,255,255,0.08); padding: 1px 4px; border-radius: 3px; font-family: monospace; } .wt-list, .log-list { font-family: monospace; font-size: 11px; max-height: 140px; overflow: auto; background: rgba(0,0,0,0.2); padding: 6px 8px; border-radius: 3px; line-height: 1.5; } .wt-row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; } .wt-row.self { color: #3ddc84; } .wt-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wt-row .port { opacity: 0.8; flex-shrink: 0; } .log-row { display: flex; gap: 8px; padding: 1px 0; } .log-row .time { opacity: 0.5; flex-shrink: 0; } .log-row .cmd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .toast { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.85); color: #fff; padding: 6px 12px; border-radius: 4px; font-size: 11px; opacity: 0; transition: opacity 0.2s; pointer-events: none; max-width: 80%; text-align: center; } .toast.show { opacity: 1; } `; exports.$ = { mcpDot: '#mcpDot', mcpRunning: '#mcpRunning', mcpUrl: '#mcpUrl', mcpTools: '#mcpTools', mcpReqCount: '#mcpReqCount', btnCopyMcpUrl: '#btnCopyMcpUrl', btnCopyCli: '#btnCopyCli', btnRestartMcp: '#btnRestartMcp', previewUrl: '#previewUrl', previewPort: '#previewPort', editorPid: '#editorPid', watchers: '#watchers', updatedAt: '#updatedAt', gitBranch: '#gitBranch', gitHead: '#gitHead', 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', }; let toastTimer = null; function fmtTime(iso) { if (!iso) return '-'; try { return iso.replace('T', ' ').replace(/\..+$/, '').split(' ')[1] || iso; } catch (e) { return iso; } } exports.methods = { showToast(msg) { this.$.toast.textContent = msg; this.$.toast.classList.add('show'); if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { this.$.toast.classList.remove('show'); }, 2200); }, async refreshStatus() { try { const s = await Editor.Message.request('cc-3-8-x-mcp', 'get-status'); if (!s) return; this.$.gitBranch.textContent = s.gitBranch || '-'; this.$.gitHead.textContent = s.gitHead || '-'; this.$.previewUrl.textContent = s.previewUrl || '-'; this.$.previewPort.textContent = s.previewPort != null ? String(s.previewPort) : '-'; this.$.editorPid.textContent = String(s.editorPid || '-'); const w = s.watchers || {}; this.$.watchers.textContent = (w.refresh ? '●refresh ' : '○refresh ') + (w.infoInterval ? '●info' : '○info'); this.$.updatedAt.textContent = s.updatedAt ? s.updatedAt.replace('T', ' ').replace(/\..+$/, '') : '-'; // MCP 区 const mcp = s.mcpServer || {}; if (mcp.running) { this.$.mcpDot.className = 'dot green'; this.$.mcpRunning.textContent = 'running'; this.$.mcpUrl.textContent = mcp.url || '-'; this.$.mcpTools.textContent = (mcp.toolCount || 0) + ' tools / ' + (mcp.resourceCount || 0) + ' res'; this.$.mcpReqCount.textContent = String((mcp.stats && mcp.stats.requestCount) || 0); } else { this.$.mcpDot.className = 'dot red'; this.$.mcpRunning.textContent = 'stopped'; this.$.mcpUrl.textContent = '-'; } // 命令日志 if (Array.isArray(s.commandLog)) { this.$.logList.innerHTML = s.commandLog.length ? s.commandLog.map(e => `
${fmtTime(e.t)}[${e.source}] ${escapeHtml(e.cmd)}
`).join('') : '
(暂无)
'; } // 预览连通性探测 this.probePreview(s.previewUrl); } catch (e) { this.showToast('状态获取失败: ' + (e.message || e)); } this.refreshWorktrees(); this.refreshDebugButtons(); }, async probePreview(url) { if (!url) { this.$.probeDot.className = 'dot gray'; return; } try { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 1500); const resp = await fetch(url, { method: 'HEAD', signal: ctrl.signal }); clearTimeout(timer); this.$.probeDot.className = resp.ok ? 'dot green' : 'dot red'; this.$.probeDot.title = '预览: HTTP ' + resp.status; } catch (e) { this.$.probeDot.className = 'dot red'; this.$.probeDot.title = '预览: ' + (e.message || 'unreachable'); } }, async refreshWorktrees() { try { const list = await Editor.Message.request('cc-3-8-x-mcp', 'list-worktrees'); if (!Array.isArray(list) || !list.length) { this.$.worktreeList.innerHTML = '
(未发现其他 worktree)
'; return; } this.$.worktreeList.innerHTML = list.map(w => { const name = (w.projectName || w.projectPath || '').split('/').slice(-2).join('/'); const stale = w.staleSec > 90 ? ` ⚠${w.staleSec}s` : ''; const selfCls = w.self ? 'wt-row self' : 'wt-row'; return `
${escapeHtml(name)}${w.self ? ' (本)' : ''}:${w.previewPort || '?'} pid${w.editorPid}${stale}
`; }).join(''); } 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.refreshStatus(); } catch (e) { this.showToast('失败: ' + (e.message || e)); } }, async onSoftReloadClick() { 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'); this.showToast('预览: ' + url); 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)); } }, async onCleanClick() { try { const removed = await Editor.Message.request('cc-3-8-x-mcp', 'clean-dev-dir'); this.showToast('已清理 ' + (removed ? removed.length : 0) + ' 个文件'); } catch (e) { this.showToast('失败: ' + (e.message || e)); } }, async onReimportClick() { const url = (this.$.reimportInput.value || '').trim(); if (!url) { this.showToast('请输入 assetUrl'); return; } try { await Editor.Message.request('cc-3-8-x-mcp', 'trigger-reimport', url); this.showToast('已重新导入: ' + url); } catch (e) { this.showToast('失败: ' + (e.message || e)); } }, async onCopyMcpUrl() { try { const cfg = await Editor.Message.request('cc-3-8-x-mcp', 'get-mcp-config'); if (!cfg || !cfg.url) { this.showToast('MCP 未运行'); return; } await navigator.clipboard.writeText(cfg.url); this.showToast('已复制: ' + cfg.url); } catch (e) { this.showToast('失败: ' + (e.message || e)); } }, async onCopyCli() { try { const cfg = await Editor.Message.request('cc-3-8-x-mcp', 'get-mcp-config'); if (!cfg || !cfg.cliAddCommand) { this.showToast('MCP 未运行'); return; } await navigator.clipboard.writeText(cfg.cliAddCommand); this.showToast('已复制 CLI 命令'); } catch (e) { this.showToast('失败: ' + (e.message || e)); } }, async onRestartMcp() { this.showToast('重启 MCP…'); try { await Editor.Message.request('cc-3-8-x-mcp', 'restart-server'); this.showToast('MCP 已重启'); 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) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } 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()); this.refreshStatus(); // 每 10s 自动刷状态 this._statusTimer = setInterval(() => this.refreshStatus(), 10000); }; exports.close = function () { if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } if (this._statusTimer) { clearInterval(this._statusTimer); this._statusTimer = null; } };