(function (global) { 'use strict'; 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; var CONSOLE_FLUSH_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._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 '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 'pick.setMode': return this._setPickMode(params.mode); case 'pick.select': return this._selectNode(params.uuid); 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') { 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._bindCanvasPick(false); this._clearTimer('pickRect'); if (this._overlay) this._overlay.hidden = 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(), }; }; 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; var found = null; walk(scene, function (n) { var id = nodeId(n); if (id === uuid) found = n; }); return found; }; 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 } }); return { uuid: this._selectedUuid, ok: !!node }; }; /** 组件内部属性,不在 Inspector 展示 */ var COMP_PROP_SKIP = { _name: 1, _objFlags: 1, __scriptAsset: 1, node: 1, _enabled: 1, __prefab: 1, __editorExtras__: 1, uuid: 1, name: 1, enabled: 1, }; /** * 读取 CCClass 属性元数据(displayName / type / enumList / visible)。 * @param {Function} ctor * @returns {object} */ function getCtorAttrs(ctor) { if (!ctor) return {}; if (ctor.__attrs__ && typeof ctor.__attrs__ === 'object') return ctor.__attrs__; return {}; } /** * @param {object} attrs * @param {string} key * @param {string} name * @returns {*} */ function attrOf(attrs, key, name) { return attrs[key + '$_$' + name]; } /** * 列出组件应展示的属性名(对齐编辑器:走 __props__,隐藏 _ 开头与 visible:false)。 * @param {Function} ctor * @returns {string[]} */ function listCompPropKeys(ctor) { var attrs = getCtorAttrs(ctor); var raw = (ctor && (ctor.__props__ || ctor.__values__)) || []; var out = []; for (var i = 0; i < raw.length; i++) { var key = raw[i]; if (!key || COMP_PROP_SKIP[key]) continue; if (key.charAt(0) === '_' || key.indexOf('__') === 0) continue; if (attrOf(attrs, key, 'visible') === false) continue; out.push(key); } return out; } /** * 将运行时值序列化为 Inspector DTO。 * @param {*} val * @param {object} attrs * @param {string} key * @param {object} cc * @returns {{type:string, value:*, enumOptions?:Array, readOnly?:boolean}} */ function serializeFieldValue(val, attrs, key, cc) { var enumList = attrOf(attrs, key, 'enumList'); var typeTag = attrOf(attrs, key, 'type'); if (enumList && enumList.length) { return { type: 'enum', value: val, enumOptions: enumList.map(function (it) { return { name: String(it.name), value: it.value }; }), }; } if (typeTag === 'Enum') { return { type: 'number', value: typeof val === 'number' ? val : Number(val) || 0 }; } var t = typeof val; if (t === 'boolean' || t === 'string') return { type: t, value: val }; if (t === 'number') return { type: 'number', value: isFinite(val) ? val : 0 }; if (val == null) { var ctorHint = attrOf(attrs, key, 'ctor'); var hintName = ctorHint && (ctorHint.name || ctorHint.prototype && ctorHint.prototype.__classname__); if (hintName || typeTag === 'Object') { return { type: 'ref', readOnly: true, value: { kind: hintName || 'Object', name: '(空)', uuid: '' }, }; } return { type: 'string', value: '' }; } if (cc && cc.Color && val instanceof cc.Color) { return { type: 'color', value: { r: val.r | 0, g: val.g | 0, b: val.b | 0, a: val.a != null ? (val.a | 0) : 255 }, }; } // Size if (typeof val.width === 'number' && typeof val.height === 'number' && val.x === undefined) { return { type: 'size', value: { width: val.width, height: val.height } }; } // Vec2 / Vec3 if (typeof val.x === 'number' && typeof val.y === 'number') { if (typeof val.z === 'number') { return { type: 'vec3', value: { x: val.x, y: val.y, z: val.z } }; } return { type: 'vec2', value: { x: val.x, y: val.y } }; } // Asset / Node / Component 引用 if (typeof val === 'object') { var kind = (val.constructor && val.constructor.name) || 'Object'; var name = ''; if (val.name != null) name = String(val.name); else if (val.node && val.node.name) name = String(val.node.name); var uuid = val._uuid || val.uuid || ''; return { type: 'ref', readOnly: true, value: { kind: kind, name: name || kind, uuid: uuid ? String(uuid) : '' }, }; } return { type: 'string', value: String(val) }; } /** * 提取单个组件的 Inspector 字段列表。 * @param {object} comp * @param {number} index * @param {object} cc * @returns {{name:string, index:number, fields:Array}} */ function extractCompFields(comp, index, cc) { var ctor = comp.constructor; var cname = (cc && cc.js && typeof cc.js.getClassName === 'function') ? (cc.js.getClassName(ctor) || ctor.name) : (ctor && ctor.name) || 'Component'; var attrs = getCtorAttrs(ctor); var keys = listCompPropKeys(ctor); // 无元数据时有限回退 if (!keys.length) { var own = Object.keys(comp); for (var o = 0; o < own.length && keys.length < 24; o++) { var ok = own[o]; if (!ok || ok.charAt(0) === '_' || COMP_PROP_SKIP[ok]) continue; var ot = typeof comp[ok]; if (ot === 'number' || ot === 'boolean' || ot === 'string') keys.push(ok); } if (!keys.length) { console.warn('[Agent] 组件无 CCClass 属性元数据', cname); } } var fields = []; var max = 80; for (var i = 0; i < keys.length && fields.length < max; i++) { var key = keys[i]; var label = attrOf(attrs, key, 'displayName') || key; var raw; try { raw = comp[key]; } catch (e) { continue; } var ser = serializeFieldValue(raw, attrs, key, cc); fields.push({ key: key, label: label, type: ser.type, value: ser.value, enumOptions: ser.enumOptions || null, readOnly: !!ser.readOnly, }); } return { name: cname, index: index, fields: fields }; } WorkbenchAgent.prototype._getNodeProps = function (uuid) { var node = this._findNode(uuid); if (!node) return null; var cc = this.cc; var p = node.getPosition ? node.getPosition() : node.position; var e = node.eulerAngles || { x: 0, y: 0, z: 0 }; var s = node.getScale ? node.getScale() : node.scale; var comps = []; if (node.components) { for (var i = 0; i < node.components.length; i++) { var comp = node.components[i]; if (!comp || !comp.constructor) continue; comps.push(extractCompFields(comp, i, cc)); } } return { uuid: nodeId(node), name: node.name || '', active: !!node.active, layer: node.layer, pos: { x: p.x, y: p.y, z: p.z }, euler: { x: e.x, y: e.y, z: e.z }, scale: { x: s.x, y: s.y, z: s.z }, comps: comps, }; }; WorkbenchAgent.prototype._setNodeProp = function (uuid, fid, value) { var node = this._findNode(uuid); if (!node || !fid) return false; var cc = this.cc; try { if (fid === 'name') node.name = String(value); else if (fid === 'active') node.active = !!value; else if (fid === 'layer') node.layer = Number(value) || 0; else if (fid === 'px' || fid === 'py' || fid === 'pz') { var p = node.getPosition ? node.getPosition() : node.position; var x = fid === 'px' ? Number(value) : p.x; var y = fid === 'py' ? Number(value) : p.y; var z = fid === 'pz' ? Number(value) : p.z; if (typeof node.setPosition === 'function') node.setPosition(x, y, z); else if (cc && cc.Vec3) node.position = new cc.Vec3(x, y, z); } else if (fid === 'rx' || fid === 'ry' || fid === 'rz') { var e = node.eulerAngles || { x: 0, y: 0, z: 0 }; var ex = fid === 'rx' ? Number(value) : e.x; var ey = fid === 'ry' ? Number(value) : e.y; var ez = fid === 'rz' ? Number(value) : e.z; if (typeof node.setRotationFromEuler === 'function') node.setRotationFromEuler(ex, ey, ez); else if (cc && cc.Vec3) node.eulerAngles = new cc.Vec3(ex, ey, ez); } else if (fid === 'sx' || fid === 'sy' || fid === 'sz') { var sc = node.getScale ? node.getScale() : node.scale; var sx = fid === 'sx' ? Number(value) : sc.x; var sy = fid === 'sy' ? Number(value) : sc.y; var sz = fid === 'sz' ? Number(value) : sc.z; if (typeof node.setScale === 'function') node.setScale(sx, sy, sz); else if (cc && cc.Vec3) node.scale = new cc.Vec3(sx, sy, sz); } else if (fid.indexOf('c') === 0 && fid.indexOf('_') > 0) { return this._setCompProp(node, fid, value); } else { return false; } return true; } catch (err) { console.warn('[Agent] set prop', fid, err); return false; } }; /** * 写回组件字段。fid: c{index}_{key}[_axis] * @param {object} node * @param {string} fid * @param {*} value * @returns {boolean} */ WorkbenchAgent.prototype._setCompProp = function (node, fid, value) { var m = /^c(\d+)_(.+)$/.exec(fid); if (!m) return false; var ci = parseInt(m[1], 10); var rest = m[2]; var comp = node.components && node.components[ci]; if (!comp) return false; var cc = this.cc; var axis = ''; var key = rest; var axisMatch = /^(.*)_(x|y|z|w|h|r|g|b|a|hex)$/.exec(rest); if (axisMatch) { // 优先整键命中(字段名本身含下划线时仍以整键为准) if (!(rest in comp) && (axisMatch[1] in comp || hasCompProp(comp, axisMatch[1]))) { key = axisMatch[1]; axis = axisMatch[2]; } } if (axis) { return setCompAxisValue(comp, key, axis, value, cc); } var attrs = getCtorAttrs(comp.constructor); if (attrOf(attrs, key, 'type') === 'Object' || attrOf(attrs, key, 'ctor')) { // 引用只读 return false; } var cur; try { cur = comp[key]; } catch (e1) { return false; } if (cur && typeof cur === 'object' && !(typeof cur === 'boolean')) { // 未带轴的对象写入拒绝(避免覆盖引用) if (typeof cur.x === 'number' || typeof cur.width === 'number' || (cc && cc.Color && cur instanceof cc.Color)) { return false; } } try { if (typeof cur === 'boolean') comp[key] = !!value; else if (typeof cur === 'number') comp[key] = Number(value); else if (typeof cur === 'string') comp[key] = String(value); else if (attrOf(attrs, key, 'enumList')) comp[key] = Number(value); else comp[key] = value; return true; } catch (e2) { console.warn('[Agent] set comp prop', key, e2); return false; } }; /** * @param {object} comp * @param {string} key * @returns {boolean} */ function hasCompProp(comp, key) { if (!comp || !key) return false; if (key in comp) return true; var ctor = comp.constructor; var props = (ctor && (ctor.__props__ || ctor.__values__)) || []; return props.indexOf(key) >= 0; } /** * @param {object} comp * @param {string} key * @param {string} axis * @param {*} value * @param {object} cc * @returns {boolean} */ function setCompAxisValue(comp, key, axis, value, cc) { var cur; try { cur = comp[key]; } catch (e) { return false; } if (axis === 'hex') { var hex = String(value).replace(/^#/, ''); if (hex.length < 6) return false; var r = parseInt(hex.slice(0, 2), 16); var g = parseInt(hex.slice(2, 4), 16); var b = parseInt(hex.slice(4, 6), 16); var a = hex.length >= 8 ? parseInt(hex.slice(6, 8), 16) : 255; if (cc && cc.Color) { comp[key] = new cc.Color(r, g, b, a); return true; } if (cur && typeof cur.r === 'number') { cur.r = r; cur.g = g; cur.b = b; cur.a = a; return true; } return false; } var num = Number(value); if (!isFinite(num)) return false; if (!cur || typeof cur !== 'object') return false; if (axis === 'w' && typeof cur.width === 'number') { cur.width = num; return true; } if (axis === 'h' && typeof cur.height === 'number') { cur.height = num; return true; } if ((axis === 'x' || axis === 'y' || axis === 'z') && typeof cur[axis] === 'number') { cur[axis] = num; // 触发 setter(部分引擎属性需整体赋值) try { comp[key] = cur; } catch (e2) { /* 就地改写已足够 */ } return true; } if ((axis === 'r' || axis === 'g' || axis === 'b' || axis === 'a') && typeof cur[axis] === 'number') { cur[axis] = num; try { comp[key] = cur; } catch (e3) { /* */ } return true; } return false; } 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._setPickMode = function (mode) { this._pickMode = mode === 'inspect' ? 'inspect' : 'game'; document.documentElement.classList.toggle('pw-inspect', this._pickMode === 'inspect'); this._bindCanvasPick(this._isActive('pick')); return this._pickMode; }; WorkbenchAgent.prototype._bindCanvasPick = function (on) { var canvas = document.getElementById('GameCanvas'); if (!canvas) return; canvas.removeEventListener('pointerdown', this._onPickDown, true); if (on && this._pickMode === 'inspect') { if (!this._onPickDown) { var self = this; this._onPickDown = function (ev) { if (self._pickMode !== 'inspect') return; ev.preventDefault(); ev.stopPropagation(); var uuid = self._hitTest(ev.clientX, ev.clientY); self._selectNode(uuid); if (self._isActive('inspector') || self._isActive('hierarchy')) { self._post({ type: 'event', name: 'hier.diff', data: self._getHierTree() }); } }; } canvas.addEventListener('pointerdown', this._onPickDown, true); } }; WorkbenchAgent.prototype._hitTest = function (clientX, clientY) { var cc = this.cc; var canvas = document.getElementById('GameCanvas'); if (!cc || !canvas) return ''; var scene = cc.director.getScene(); if (!scene) return ''; var stack = []; walk(scene, function (n) { stack.push(n); }); for (var i = stack.length - 1; i >= 0; i--) { var node = stack[i]; if (!node || !node.activeInHierarchy) continue; var uit = node.getComponent && node.getComponent(cc.UITransform); if (!uit) continue; var box = this._nodeClientRect(node); if (!box) continue; if (clientX >= box.left && clientX <= box.right && clientY >= box.top && clientY <= box.bottom) { return nodeId(node); } } return ''; }; WorkbenchAgent.prototype._ensureOverlay = function () { var el = document.getElementById('pw-pick-overlay'); if (!el) { el = document.createElement('div'); el.id = 'pw-pick-overlay'; el.className = 'pw-pick-overlay'; el.hidden = true; document.body.appendChild(el); } this._overlay = el; }; WorkbenchAgent.prototype._nodeClientRect = function (node) { var cc = this.cc; var canvas = document.getElementById('GameCanvas'); if (!cc || !canvas || !node || !node.isValid) return null; var uit = node.getComponent && node.getComponent(cc.UITransform); if (!uit || typeof uit.getBoundingBoxToWorld !== 'function') return null; var box = uit.getBoundingBoxToWorld(); if (!box) return null; var canvasRect = canvas.getBoundingClientRect(); var vs = cc.view && cc.view.getVisibleSize ? cc.view.getVisibleSize() : { width: canvasRect.width, height: canvasRect.height }; var vw = Math.max(1, vs.width); var vh = Math.max(1, vs.height); var corners = [ { x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }, { x: box.x + box.width, y: box.y + box.height }, { x: box.x, y: box.y + box.height }, ]; var xs = []; var ys = []; for (var i = 0; i < 4; i++) { xs.push(canvasRect.left + (corners[i].x / vw + 0.5) * canvasRect.width); ys.push(canvasRect.top + (0.5 - corners[i].y / vh) * canvasRect.height); } var left = Math.min.apply(null, xs); var right = Math.max.apply(null, xs); var top = Math.min.apply(null, ys); var bottom = Math.max.apply(null, ys); return { left: left, top: top, right: right, bottom: bottom, width: Math.max(2, right - left), height: Math.max(2, bottom - top) }; }; WorkbenchAgent.prototype._updateOverlay = function () { var node = this._findNode(this._selectedUuid); var overlay = this._overlay; if (!overlay) return; if (!node) { overlay.hidden = true; return; } var box = this._nodeClientRect(node); if (!box) { overlay.hidden = true; return; } overlay.hidden = false; overlay.style.position = 'fixed'; overlay.style.left = box.left + 'px'; overlay.style.top = box.top + 'px'; overlay.style.width = box.width + 'px'; overlay.style.height = box.height + 'px'; }; WorkbenchAgent.prototype._getOverlayRect = function () { var overlay = this._overlay; if (!overlay || overlay.hidden) return null; return { left: parseFloat(overlay.style.left) || 0, top: parseFloat(overlay.style.top) || 0, width: parseFloat(overlay.style.width) || 0, height: parseFloat(overlay.style.height) || 0, }; }; WorkbenchAgent.prototype._hookConsole = function (on) { var self = this; if (on) { if (this._consoleOrig) return; this._consoleOrig = { log: console.log.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), debug: console.debug.bind(console), }; var wrap = function (level, fn) { return function () { var args = Array.prototype.slice.call(arguments); self._pushConsole(level, args); return fn.apply(console, args); }; }; console.log = wrap('log', this._consoleOrig.log); console.info = wrap('info', this._consoleOrig.info); console.warn = wrap('warn', this._consoleOrig.warn); console.error = wrap('error', this._consoleOrig.error); console.debug = wrap('debug', this._consoleOrig.debug); this._bindConsoleErrors(true); } else if (this._consoleOrig) { this._bindConsoleErrors(false); console.log = this._consoleOrig.log; console.info = this._consoleOrig.info; console.warn = this._consoleOrig.warn; console.error = this._consoleOrig.error; console.debug = this._consoleOrig.debug; this._consoleOrig = null; this._consolePost = false; if (this._consoleFlushTimer) { clearTimeout(this._consoleFlushTimer); this._consoleFlushTimer = 0; } this._consoleBuf.length = 0; } }; /** 挂载/卸载 window 未捕获异常监听(随 console 通道启停) */ WorkbenchAgent.prototype._bindConsoleErrors = function (on) { if (on) { if (this._consoleErrBound) return; window.addEventListener('error', this._boundConsoleError); window.addEventListener('unhandledrejection', this._boundConsoleRejection); this._consoleErrBound = true; } else if (this._consoleErrBound) { window.removeEventListener('error', this._boundConsoleError); window.removeEventListener('unhandledrejection', this._boundConsoleRejection); this._consoleErrBound = false; } }; /** 脚本运行时错误 → 控制台 error */ WorkbenchAgent.prototype._onConsoleError = function (ev) { if (!ev) return; var args = ev.error instanceof Error ? [ev.error] : [ev.message || 'Script error']; var entry = this._serializeConsoleArgs('error', args); if (ev.filename) { entry.src = this._srcFromUrl(ev.filename, ev.lineno || 0, ev.colno || 0); } this._enqueueConsole(entry); }; /** 未处理 Promise 拒绝 → 对齐浏览器 Uncaught (in promise) */ WorkbenchAgent.prototype._onConsoleRejection = function (ev) { var reason = ev && ev.reason; var args; if (reason instanceof Error) { args = ['Uncaught (in promise)', reason]; } else { args = ['Uncaught (in promise) ' + String(reason)]; } this._pushConsole('error', args); }; WorkbenchAgent.prototype._pushConsole = function (level, args) { this._enqueueConsole(this._serializeConsoleArgs(level, args)); }; WorkbenchAgent.prototype._enqueueConsole = function (entry) { if (!entry) return; if (this._consolePost && !this._hibernating) { this._consoleBuf.push(entry); var self = this; if (!this._consoleFlushTimer) { this._consoleFlushTimer = setTimeout(function () { self._consoleFlushTimer = 0; self._flushConsole(); }, CONSOLE_FLUSH_MS); } return; } // 未订阅时静默环缓冲,打开面板时再推送 this._consoleHist.push(entry); if (this._consoleHist.length > 200) this._consoleHist.splice(0, this._consoleHist.length - 200); }; WorkbenchAgent.prototype._flushConsole = function () { if (!this._consolePost || !this._consoleBuf.length) { this._consoleBuf.length = 0; return; } var batch = this._consoleBuf.splice(0, this._consoleBuf.length); this._post({ type: 'event', name: 'console.batch', data: { items: batch } }); }; /** 订阅时把未推送历史一次性推给壳 */ WorkbenchAgent.prototype._flushConsoleHist = function () { if (!this._consolePost || !this._consoleHist.length) return; var batch = this._consoleHist.splice(0, this._consoleHist.length); this._post({ type: 'event', name: 'console.batch', data: { items: batch } }); this._consoleBuf.length = 0; }; /** * 将 console 参数序列化为可 postMessage 的富结构(保留 %c/%s 等,对象可展开)。 * 原始 console.* 仍会调用,浏览器 DevTools 打印不受影响。 */ WorkbenchAgent.prototype._serializeConsoleArgs = function (level, args) { var parts = []; var plain = ''; var FMT_RE = /%(?:%|[csdifoO])/; var rawStack = ''; try { rawStack = new Error().stack || ''; } catch (e0) { rawStack = ''; } if (args.length && typeof args[0] === 'string' && FMT_RE.test(args[0])) { var formatted = this._formatConsole(args); parts = formatted.parts; plain = formatted.plain; // 对齐 Chrome:%c 只作用于格式串本身;其后附加参数用默认色、不加引号 for (var r = 0; r < formatted.rest.length; r++) { var rv = formatted.rest[r]; if (typeof rv === 'string') { parts.push({ kind: 'text', text: rv, plain: rv }); plain += (plain ? ' ' : '') + rv; } else { var rp = this._valuePart(rv); parts.push(rp); plain += (plain ? ' ' : '') + (rp.plain || ''); } } } else { for (var i = 0; i < args.length; i++) { // 顶层消息字符串不加引号(属性展开时仍用 string 带引号) var p = this._valuePart(args[i], typeof args[i] === 'string'); parts.push(p); plain += (i ? ' ' : '') + (p.plain || ''); } } var src = this._srcFromStack(rawStack); var callStack = ''; for (var j = 0; j < parts.length; j++) { if (parts[j] && parts[j].kind === 'error' && parts[j].stack) { if (!src) src = this._srcFromStack(parts[j].stack); callStack = parts[j].stack; break; } } if (!callStack && (level === 'warn' || level === 'error')) { callStack = this._businessStack(rawStack); } return { id: 'c' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), level: level, t: Date.now(), parts: parts, plain: plain, src: src, stack: callStack || '', }; }; /** 去掉 agent 帧后的业务堆栈文本 */ WorkbenchAgent.prototype._businessStack = function (stack) { if (!stack) return ''; var lines = String(stack).split('\n'); var out = []; for (var i = 0; i < lines.length; i++) { var line = lines[i]; var t = line.trim(); if (!t) continue; if (t.indexOf('Error') === 0 && t.indexOf('at ') < 0) continue; if (this._isAgentStackLine(t)) continue; out.push(line); } return out.join('\n'); }; WorkbenchAgent.prototype._isAgentStackLine = function (line) { return /agent\.js|WorkbenchAgent|_pushConsole|_enqueueConsole|_serializeConsole|_captureConsoleSrc|_srcFromStack|_businessStack|_hookConsole|_onConsoleError|_onConsoleRejection|_valuePart|_formatConsole|_flushConsole/i.test(line); }; /** 解析 stack:优先可读源文件名,跳过 agent / UUID 哈希帧 */ WorkbenchAgent.prototype._srcFromStack = function (stack) { if (!stack) return null; var lines = String(stack).split('\n'); var best = null; var bestScore = -1; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line || /^error$/i.test(line) || (line.indexOf('Error') === 0 && line.indexOf('at ') < 0)) continue; if (this._isAgentStackLine(line)) continue; var m = line.match(/\((.+?):(\d+):(\d+)\)\s*$/) || line.match(/at\s+(\S+?):(\d+):(\d+)\s*$/); if (!m) continue; var cand = this._srcFromUrl(m[1], parseInt(m[2], 10) || 0, parseInt(m[3], 10) || 0); var score = this._scoreSrcFile(cand.file); if (score > bestScore) { bestScore = score; best = cand; } } return best; }; /** 来源文件可读性评分(越高越优先展示) */ WorkbenchAgent.prototype._scoreSrcFile = function (file) { var f = String(file || ''); if (!f || f === '(anonymous)') return 0; if (/^[a-f0-9]{16,}$/i.test(f) || /^[a-f0-9-]{32,}$/i.test(f)) return 1; if (/\.ts$/i.test(f)) return 50; if (/\.js$/i.test(f) && /[a-zA-Z]/.test(f) && !/^[a-f0-9.-]+$/i.test(f.replace(/\.js$/i, ''))) return 40; if (/\.(js|mjs|cjs)$/i.test(f)) return 20; if (/[a-zA-Z]/.test(f)) return 10; return 2; }; /** URL + 行列 → 简短来源 */ WorkbenchAgent.prototype._srcFromUrl = function (url, line, col) { var u = String(url || ''); var file = u.split('/').pop() || u; var q = file.indexOf('?'); if (q >= 0) file = file.slice(0, q); var h = file.indexOf('#'); if (h >= 0) file = file.slice(0, h); if (!file) file = '(anonymous)'; var ln = line || 0; return { file: file, line: ln, col: col || 0, text: ln ? (file + ':' + ln) : file, }; }; WorkbenchAgent.prototype._formatConsole = function (args) { var fmt = args[0]; var ai = 1; var parts = []; var plain = ''; var style = ''; var i = 0; while (i < fmt.length) { if (fmt.charAt(i) === '%' && i + 1 < fmt.length) { var spec = fmt.charAt(i + 1); if (spec === '%') { parts.push({ kind: 'styled', text: '%', css: style }); plain += '%'; i += 2; continue; } if (spec === 'c') { style = typeof args[ai] === 'string' ? args[ai] : ''; ai++; i += 2; continue; } var val = ai < args.length ? args[ai] : undefined; ai++; var text = ''; if (spec === 's') text = val == null ? 'null' : String(val); else if (spec === 'd' || spec === 'i') text = String(parseInt(val, 10) || 0); else if (spec === 'f') text = String(Number(val)); else if (spec === 'o' || spec === 'O') { var vp = this._valuePart(val); parts.push(vp); plain += vp.plain || ''; i += 2; continue; } else { text = '%' + spec; } parts.push({ kind: 'styled', text: text, css: style }); plain += text; i += 2; continue; } var start = i; while (i < fmt.length && fmt.charAt(i) !== '%') i++; var lit = fmt.slice(start, i); if (lit) { parts.push({ kind: 'styled', text: lit, css: style }); plain += lit; } } return { parts: parts, plain: plain, rest: args.slice(ai), style: style }; }; WorkbenchAgent.prototype._valuePart = function (value, bareString) { if (value === null) return { kind: 'null', plain: 'null' }; var t = typeof value; if (t === 'undefined') return { kind: 'undefined', plain: 'undefined' }; if (t === 'string') { return bareString ? { kind: 'text', text: value, plain: value } : { kind: 'string', text: value, plain: '"' + value + '"' }; } if (t === 'number' || t === 'boolean' || t === 'bigint') { return { kind: t, value: t === 'bigint' ? String(value) : value, plain: String(value) }; } if (t === 'symbol') return { kind: 'symbol', text: String(value), plain: String(value) }; if (t === 'function') { var fname = value.name || 'anonymous'; return { kind: 'function', name: fname, plain: 'ƒ ' + fname + '()' }; } if (value instanceof Error) { return { kind: 'error', name: value.name || 'Error', message: value.message || '', stack: value.stack || '', plain: (value.name || 'Error') + ': ' + (value.message || ''), }; } var oid = 'o' + (this._consoleObjSeq++); this._consoleObjs[oid] = value; var isArray = Array.isArray(value); var ctor = (value && value.constructor && value.constructor.name) || (isArray ? 'Array' : 'Object'); var preview = isArray ? ('Array(' + value.length + ')') : ctor; try { if (isArray) { var bits = []; for (var i = 0; i < Math.min(value.length, 5); i++) bits.push(briefVal(value[i])); if (value.length > 5) bits.push('…'); preview += ' [' + bits.join(', ') + ']'; } else { var keys = Object.keys(value); var bits2 = []; for (var j = 0; j < Math.min(keys.length, 4); j++) { bits2.push(keys[j] + ': ' + briefVal(value[keys[j]])); } if (keys.length > 4) bits2.push('…'); preview += ' {' + bits2.join(', ') + '}'; } } catch (e) { /* */ } return { kind: 'object', oid: oid, ctor: ctor, isArray: isArray, length: isArray ? value.length : undefined, preview: preview, plain: preview, }; }; WorkbenchAgent.prototype._inspectConsoleObj = function (oid, path) { var root = this._consoleObjs[oid]; if (!root) return { props: [] }; var target = root; var segs = String(path || '').split('.').filter(Boolean); for (var i = 0; i < segs.length; i++) { try { target = target[segs[i]]; } catch (e) { return { props: [] }; } } var props = []; var MAX = 40; try { if (Array.isArray(target)) { for (var a = 0; a < Math.min(target.length, MAX); a++) { props.push({ key: String(a), part: this._valuePart(target[a]) }); } if (target.length > MAX) props.push({ key: '…', part: { kind: 'text', text: '…', plain: '…' } }); } else if (target && typeof target === 'object') { var keys = Object.getOwnPropertyNames(target); for (var k = 0; k < Math.min(keys.length, MAX); k++) { var key = keys[k]; var child; try { child = target[key]; } catch (err) { child = '⟨access error⟩'; } props.push({ key: key, part: this._valuePart(child) }); } if (keys.length > MAX) props.push({ key: '…', part: { kind: 'text', text: '…', plain: '…' } }); } } catch (e2) { /* */ } return { props: props }; }; function briefVal(v) { if (v === null) return 'null'; var t = typeof v; if (t === 'string') return '"' + (v.length > 24 ? v.slice(0, 24) + '…' : v) + '"'; if (t === 'number' || t === 'boolean' || t === 'undefined') return String(v); if (t === 'function') return 'ƒ'; if (Array.isArray(v)) return 'Array(' + v.length + ')'; if (t === 'object') return '{…}'; return String(v); } 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; } }; function nodeId(n) { return String(n.uuid || n._id || n.name || ''); } function walk(node, fn) { if (!node) return; fn(node); var ch = node.children; if (!ch) return; for (var i = 0; i < ch.length; i++) walk(ch[i], fn); } function serializeNode(node, depth, maxDepth) { if (!node || depth > maxDepth) return null; var children = []; var ch = node.children || []; for (var i = 0; i < ch.length; i++) { var c = serializeNode(ch[i], depth + 1, maxDepth); if (c) children.push(c); } var comps = []; if (node.components) { for (var j = 0; j < node.components.length; j++) { var comp = node.components[j]; if (comp && comp.constructor) comps.push(comp.constructor.name || ''); } } return { uuid: nodeId(node), name: node.name || '(未命名)', active: node.active !== false, comps: comps, children: children, }; } function copySeries(series) { var out = {}; for (var k in series) { if (Object.prototype.hasOwnProperty.call(series, k)) out[k] = series[k].slice(); } return out; } function hierFingerprint(node) { if (!node) return ''; var parts = [node.uuid, node.name, node.active ? 1 : 0, (node.comps || []).length]; var ch = node.children || []; parts.push(ch.length); for (var i = 0; i < ch.length; i++) parts.push(hierFingerprint(ch[i])); return parts.join('/'); } global.PWAgent = WorkbenchAgent; function startAgent() { if (global.__PW_MODE && global.__PW_MODE !== 'game') return; var agent = new WorkbenchAgent(); agent.boot(); global.__PW_Agent = agent; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startAgent); } else { startAgent(); } })(window);