(function (global) { 'use strict'; var U = global.PWAgentUtil; var serializeNode = U.serializeNode; var copySeries = U.copySeries; var hierFingerprint = U.hierFingerprint; var findNodeByUuid = U.findNodeByUuid; var CHANNELS = ['perf', 'hierarchy', 'inspector', 'assets', 'console', 'markers', 'pick']; var PERF_DEFAULT_MS = 500; var HIER_DEFAULT_MS = 1500; var ASSETS_DEFAULT_MS = 2500; var PICK_RECT_MS = 100; /** * 游戏页探针:默认休眠,按订阅激活;经 postMessage 与壳通信。 * @constructor */ function WorkbenchAgent() { this.cc = null; this._subs = {}; this._hibernating = false; this._parentOrigin = '*'; this._reqId = 0; this._pending = {}; this._timers = {}; this._selectedUuid = ''; this._pickMode = 'game'; this._overlay = null; this._pickBoundEls = []; this._consoleOrig = null; this._consoleBuf = []; this._consoleHist = []; this._consoleFlushTimer = 0; this._consoleObjs = {}; this._consoleObjSeq = 1; this._consoleErrBound = false; this._consolePost = false; this._hierSnap = null; this._boundMessage = this._onMessage.bind(this); this._boundConsoleError = this._onConsoleError.bind(this); this._boundConsoleRejection = this._onConsoleRejection.bind(this); this._sampler = null; this._markers = null; this._device = null; this._paused = false; this._fpsOn = false; for (var i = 0; i < CHANNELS.length; i++) this._subs[CHANNELS[i]] = 0; } WorkbenchAgent.prototype.boot = function () { var self = this; if (global.PWDevicePreview && typeof global.PWDevicePreview.hideOfficialToolbar === 'function') { global.PWDevicePreview.hideOfficialToolbar(); } window.addEventListener('message', this._boundMessage); // 尽早静默缓冲,订阅控制台后再推送到壳(避免打开面板前日志丢失) this._hookConsole(true); this._ensureOverlay(); this._waitForEngine(function (cc) { self.cc = cc; self._sampler = new global.PWPerfSampler(); self._sampler.bindEngine(cc); self._sampler.intervalMs = PERF_DEFAULT_MS; self._sampler.onSample(function () { if (!self._isActive('perf')) return; self._post({ type: 'event', name: 'perf.sample', data: self._perfPayload() }); }); self._markers = new global.PWAutoMarkers(self._sampler); self._device = new global.PWDevicePreview(); self._device.restore(); var center = document.getElementById('pw-center'); var content = document.getElementById('content'); if (center && content) self._device.mount(center, content); self._device.apply(false); self._post({ type: 'event', name: 'agent.ready', data: { ok: true } }); setTimeout(function () { self._device.apply(false); }, 100); }); }; WorkbenchAgent.prototype._waitForEngine = function (done) { var self = this; var tries = 0; var timer = setInterval(function () { tries++; var cc = global.cc && global.cc.director ? global.cc : (global.CocosEngine && global.CocosEngine.director ? global.CocosEngine : null); if (cc && cc.director && cc.game) { var scene = null; try { scene = cc.director.getScene(); } catch (e) { scene = null; } if (scene || tries > 40) { clearInterval(timer); done(cc); return; } } if (tries >= 300) { clearInterval(timer); self._post({ type: 'event', name: 'agent.ready', data: { ok: false } }); } }, 100); }; WorkbenchAgent.prototype._post = function (msg) { msg.source = 'pw-agent'; try { if (window.parent && window.parent !== window) { window.parent.postMessage(msg, this._parentOrigin); } } catch (e) { /* */ } }; WorkbenchAgent.prototype._onMessage = function (ev) { var msg = ev.data; if (!msg || msg.source !== 'pw-bridge') return; if (ev.origin && ev.origin !== 'null') this._parentOrigin = ev.origin; var self = this; var reply = function (ok, result, error) { if (msg.id == null) return; self._post({ type: 'rpc-result', id: msg.id, ok: ok, result: result, error: error || '' }); }; try { var result = this._dispatch(msg.method, msg.params || {}); if (result && typeof result.then === 'function') { result.then(function (r) { reply(true, r); }, function (err) { reply(false, null, String(err && err.message ? err.message : err)); }); } else { reply(true, result); } } catch (e) { reply(false, null, String(e && e.message ? e.message : e)); } }; WorkbenchAgent.prototype._dispatch = function (method, params) { switch (method) { case 'ping': return { pong: true, hibernating: this._hibernating }; case 'hibernate': return this.hibernate(); case 'wake': return this.wake(); case 'subscribe': return this.subscribe(params.channel); case 'unsubscribe': return this.unsubscribe(params.channel); case 'perf.get': return this._perfPayload(); case 'stats.get': return this._statsPayload(); case 'perf.clear': if (this._sampler) this._sampler.clear(); return true; case 'perf.mark': if (this._sampler) this._sampler.userMark(params.text || ''); return true; case 'perf.setInterval': if (this._sampler) this._sampler.setIntervalMs(params.ms || PERF_DEFAULT_MS); return true; case 'perf.setCapacity': if (this._sampler) this._sampler.setCapacity(params.n || 300); return true; case 'perf.setPaused': if (this._sampler) this._sampler.paused = !!params.paused; return true; case 'hier.getTree': return this._getHierTree(); case 'hier.select': return this._selectNode(params.uuid); case 'node.get': return this._getNodeProps(params.uuid); case 'node.set': return this._setNodeProp(params.uuid, params.fid, params.value); case 'assets.list': return this._listAssets(); case 'assets.preview': return this._previewAsset(params.uuid); case 'pick.setMode': return this._setPickMode(params.mode); case 'pick.select': return this._selectNode(params.uuid); case 'pick.at': return this._pickAt(params); case 'pick.getRect': return this._getOverlayRect(); case 'game.pause': return this._setPaused(true); case 'game.resume': return this._setPaused(false); case 'game.step': return this._stepFrame(); case 'game.fps': return this._setFps(!!params.on); case 'device.list': return this._device ? this._device.list() : []; case 'device.set': if (this._device) this._device.setDevice(params.id); return true; case 'device.rotate': return this._device ? this._device.toggleRotate() : false; case 'device.apply': if (this._device) this._device.apply(!!params.force); return true; case 'device.getState': return this._device ? { id: this._device.deviceId, rotated: this._device.rotated, } : null; case 'console.inspect': return this._inspectConsoleObj(params.oid, params.path || ''); default: throw new Error('unknown method: ' + method); } }; WorkbenchAgent.prototype._isActive = function (ch) { return !this._hibernating && this._subs[ch] > 0; }; WorkbenchAgent.prototype.subscribe = function (channel) { if (!this._subs.hasOwnProperty(channel)) return { active: false }; this._subs[channel] += 1; if (this._subs[channel] === 1) this._onChannelOn(channel); return { active: this._isActive(channel), count: this._subs[channel] }; }; WorkbenchAgent.prototype.unsubscribe = function (channel) { if (!this._subs.hasOwnProperty(channel)) return { active: false }; this._subs[channel] = Math.max(0, this._subs[channel] - 1); if (this._subs[channel] === 0) this._onChannelOff(channel); return { active: this._isActive(channel), count: this._subs[channel] }; }; WorkbenchAgent.prototype.hibernate = function () { this._hibernating = true; this._consolePost = false; for (var i = 0; i < CHANNELS.length; i++) this._onChannelOff(CHANNELS[i]); this._hookConsole(false); this._consoleHist.length = 0; this._post({ type: 'event', name: 'agent.hibernate', data: { on: true } }); return true; }; WorkbenchAgent.prototype.wake = function () { this._hibernating = false; this._hookConsole(true); for (var i = 0; i < CHANNELS.length; i++) { if (this._subs[CHANNELS[i]] > 0) this._onChannelOn(CHANNELS[i]); } this._post({ type: 'event', name: 'agent.hibernate', data: { on: false } }); return true; }; WorkbenchAgent.prototype._onChannelOn = function (ch) { if (this._hibernating) return; var self = this; if (ch === 'perf') { if (this._sampler) this._sampler.start(); } else if (ch === 'markers') { if (this._markers && this.cc && !this._markers._pollTimer) this._markers.bindEngine(this.cc); } else if (ch === 'hierarchy') { this._hierFp = ''; this._clearTimer('hier'); this._timers.hier = setInterval(function () { if (!self._isActive('hierarchy')) return; self._pushHierIfChanged(); }, HIER_DEFAULT_MS); this._pushHierIfChanged(true); } else if (ch === 'assets') { this._clearTimer('assets'); this._timers.assets = setInterval(function () { if (!self._isActive('assets')) return; self._listAssetsAsync(function (data) { self._post({ type: 'event', name: 'assets.update', data: data }); }); }, ASSETS_DEFAULT_MS); } else if (ch === 'console') { this._hookConsole(true); this._consolePost = true; this._flushConsoleHist(); } else if (ch === 'pick') { // 点选输入跟随检视模式;本通道只负责高亮框跟随 if (this._pickMode === 'inspect') this._bindCanvasPick(true); this._clearTimer('pickRect'); this._timers.pickRect = setInterval(function () { if (!self._isActive('pick') || !self._selectedUuid) return; self._updateOverlay(); var rect = self._getOverlayRect(); if (rect) self._post({ type: 'event', name: 'pick.rect', data: rect }); }, PICK_RECT_MS); } }; WorkbenchAgent.prototype._onChannelOff = function (ch) { if (ch === 'perf') { if (this._sampler) this._sampler.stop(); } else if (ch === 'markers') { if (this._markers) this._markers.dispose(); } else if (ch === 'hierarchy') { this._clearTimer('hier'); } else if (ch === 'assets') { this._clearTimer('assets'); } else if (ch === 'console') { // 保持静默缓冲,仅停止推送;hibernate 时才真正卸 hook this._consolePost = false; if (this._consoleFlushTimer) { clearTimeout(this._consoleFlushTimer); this._consoleFlushTimer = 0; } this._consoleBuf.length = 0; } else if (ch === 'pick') { this._clearTimer('pickRect'); // 检视模式下仍保留点选,只停跟随框 if (this._pickMode !== 'inspect') { this._bindCanvasPick(false); if (this._overlay) this._overlay.hidden = true; } else { this._bindCanvasPick(true); } } }; WorkbenchAgent.prototype._clearTimer = function (key) { if (this._timers[key]) { clearInterval(this._timers[key]); this._timers[key] = 0; } }; WorkbenchAgent.prototype._perfPayload = function () { if (!this._sampler) return null; return { times: this._sampler.times.slice(), series: copySeries(this._sampler.series), markers: this._sampler.markers.slice(), latest: this._sampler.latest(), }; }; /** * 轻量实时状态(壳侧工具栏):按需读取,不启定时器。 * fps 优先 profiler 计数器,否则用 director 帧数差估算。 * totalMemory ≈ 纹理 + 缓冲 + JS 堆。 */ WorkbenchAgent.prototype._statsPayload = function () { var cc = this.cc; var fps = 0; var tex = 0; var buf = 0; if (cc && cc.profiler && cc.profiler.stats) { var stats = cc.profiler.stats; var read = function (key) { if (!stats[key] || !stats[key].counter) return 0; var v = stats[key].counter.value; return typeof v === 'number' ? v : 0; }; fps = read('fps'); tex = read('textureMemory'); buf = read('bufferMemory'); } if (!fps && cc && cc.director && typeof cc.director.getTotalFrames === 'function') { var now = performance.now(); var frames = cc.director.getTotalFrames(); if (this._statsLastFrames != null && this._statsLastTime) { var dt = now - this._statsLastTime; if (dt > 0) fps = ((frames - this._statsLastFrames) * 1000) / dt; } this._statsLastFrames = frames; this._statsLastTime = now; } var heap = 0; if (performance.memory && performance.memory.usedJSHeapSize) { heap = performance.memory.usedJSHeapSize / (1024 * 1024); } return { fps: fps, textureMemory: tex, bufferMemory: buf, jsHeap: heap, totalMemory: tex + buf + heap, }; }; WorkbenchAgent.prototype._getHierTree = function () { var cc = this.cc; if (!cc || !cc.director) return null; var scene = cc.director.getScene(); if (!scene) return null; var selected = this._selectedUuid; return { selected: selected, root: serializeNode(scene, 0, 48), }; }; /** 层级指纹变化才推送,避免无改动全量刷壳 */ WorkbenchAgent.prototype._pushHierIfChanged = function (force) { var tree = this._getHierTree(); var fp = hierFingerprint(tree && tree.root) + '|' + (tree && tree.selected || ''); if (!force && fp === this._hierFp) return; this._hierFp = fp; this._post({ type: 'event', name: 'hier.diff', data: tree }); }; /** 空闲切片枚举资源,降低峰值卡顿 */ WorkbenchAgent.prototype._listAssetsAsync = function (done) { var cc = this.cc; if (!cc || !cc.assetManager) { done({ rows: [], bundles: [] }); return; } var bundleNames = []; var entries = []; try { if (cc.assetManager.bundles) { cc.assetManager.bundles.forEach(function (bundle, name) { bundleNames.push(name || (bundle && bundle.name) || ''); }); } if (cc.assetManager.assets) { cc.assetManager.assets.forEach(function (asset, uuid) { entries.push({ asset: asset, uuid: uuid }); }); } } catch (e) { done({ rows: [], bundles: bundleNames }); return; } var rows = []; var idle = global.PWIdle; var visit = function (entry) { var asset = entry.asset; if (!asset) return; var type = (asset.constructor && asset.constructor.name) || 'Other'; var mem = 0; if (asset.width && asset.height) mem = (asset.width * asset.height * 4) / (1024 * 1024); rows.push({ uuid: entry.uuid, name: asset.name || entry.uuid, type: type, bundle: '', mem: mem, ref: asset.refCount != null ? asset.refCount : 0, }); }; if (idle && idle.forEachChunk) { idle.forEachChunk(entries, visit, function () { done({ rows: rows, bundles: bundleNames }); }, 2); } else { for (var i = 0; i < entries.length; i++) visit(entries[i]); done({ rows: rows, bundles: bundleNames }); } }; WorkbenchAgent.prototype._findNode = function (uuid) { var cc = this.cc; if (!cc || !cc.director || !uuid) return null; var scene = cc.director.getScene(); if (!scene) return null; return findNodeByUuid(scene, uuid); }; WorkbenchAgent.prototype._selectNode = function (uuid) { this._selectedUuid = uuid || ''; var node = this._findNode(uuid); this._updateOverlay(); this._post({ type: 'event', name: 'pick.selected', data: { uuid: this._selectedUuid, ok: !!node } }); this._post({ type: 'event', name: 'hier.diff', data: this._getHierTree() }); // 立刻推高亮框,壳层画绿色遮罩 var rect = this._getOverlayRect(); if (rect) this._post({ type: 'event', name: 'pick.rect', data: rect }); return { uuid: this._selectedUuid, ok: !!node, rect: rect || null }; }; /** 组件内部属性,不在 Inspector 展示 */ WorkbenchAgent.prototype._listAssets = function () { var cc = this.cc; if (!cc || !cc.assetManager) return { rows: [], bundles: [] }; var rows = []; var bundleNames = []; try { if (cc.assetManager.bundles) { cc.assetManager.bundles.forEach(function (bundle, name) { bundleNames.push(name || (bundle && bundle.name) || ''); }); } if (cc.assetManager.assets) { cc.assetManager.assets.forEach(function (asset, uuid) { if (!asset) return; var type = (asset.constructor && asset.constructor.name) || 'Other'; var mem = 0; if (asset.width && asset.height) mem = (asset.width * asset.height * 4) / (1024 * 1024); rows.push({ uuid: uuid, name: asset.name || uuid, type: type, bundle: '', mem: mem, ref: asset.refCount != null ? asset.refCount : 0, }); }); } } catch (e) { console.warn('[Agent] assets', e); } return { rows: rows, bundles: bundleNames }; }; WorkbenchAgent.prototype._setPaused = function (paused) { var cc = this.cc; if (!cc || !cc.director) return false; this._paused = !!paused; try { if (this._paused) cc.director.pause(); else cc.director.resume(); } catch (e) { return false; } return true; }; WorkbenchAgent.prototype._stepFrame = function () { var cc = this.cc; if (!cc) return false; try { if (!this._paused && cc.director) { cc.director.pause(); this._paused = true; } if (cc.game && typeof cc.game.step === 'function') { cc.game.step(); return true; } if (cc.director && typeof cc.director.tick === 'function') { var fps = (cc.game && cc.game.frameRate) ? cc.game.frameRate : 60; cc.director.tick(1 / Math.max(1, fps)); return true; } } catch (e) { console.warn('[Agent] step', e); } return false; }; WorkbenchAgent.prototype._setFps = function (on) { var cc = this.cc; this._fpsOn = !!on; if (!cc || !cc.profiler) return false; try { if (this._fpsOn) cc.profiler.showStats(); else cc.profiler.hideStats(); return true; } catch (e) { return false; } }; global.PWAgent = WorkbenchAgent; })(window);