'use strict';
// Cocos MCP 功能面板
// MCP 状态 / 编辑器状态 / 快捷动作 / Debug 注入 / 命令日志 / 同机 worktree
exports.template = /* html */ `
-
-
-
0
复制端点
复制 CLI 命令
重启
-
-
-
-
-
-
-
刷新(资源+场景+预览)
仅软重载场景
打开预览浏览器
查询预览地址
截图 → 复制路径
打开 .dev 目录
清理 .dev 临时文件
刷新状态
导入
执行
自定义按钮配置:.dev/cc-mcp-panel.json(或旧名 .dev/dev-reload-panel.json)→ { "buttons": [{ "label": "...", "code": "..." }] }
`;
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; }
};