You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1399 lines
55 KiB

(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 = [];
3 days ago
this._consoleHist = [];
this._consoleFlushTimer = 0;
this._consoleObjs = {};
this._consoleObjSeq = 1;
3 days ago
this._consoleErrBound = false;
this._consolePost = false;
this._hierSnap = null;
this._boundMessage = this._onMessage.bind(this);
3 days ago
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);
3 days ago
// 尽早静默缓冲,订阅控制台后再推送到壳(避免打开面板前日志丢失)
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;
3 days ago
this._consolePost = false;
for (var i = 0; i < CHANNELS.length; i++) this._onChannelOff(CHANNELS[i]);
3 days ago
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;
3 days ago
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);
3 days ago
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') {
3 days ago
// 保持静默缓冲,仅停止推送;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 };
};
3 days ago
/** 组件内部属性,不在 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;
3 days ago
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) {
3 days ago
return this._setCompProp(node, fid, value);
} else {
return false;
}
return true;
3 days ago
} 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;
}
};
3 days ago
/**
* @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);
3 days ago
this._bindConsoleErrors(true);
} else if (this._consoleOrig) {
3 days ago
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;
3 days ago
this._consolePost = false;
if (this._consoleFlushTimer) {
clearTimeout(this._consoleFlushTimer);
this._consoleFlushTimer = 0;
}
3 days ago
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)];
}
3 days ago
this._pushConsole('error', args);
};
WorkbenchAgent.prototype._pushConsole = function (level, args) {
3 days ago
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;
}
3 days ago
// 未订阅时静默环缓冲,打开面板时再推送
this._consoleHist.push(entry);
if (this._consoleHist.length > 200) this._consoleHist.splice(0, this._consoleHist.length - 200);
};
WorkbenchAgent.prototype._flushConsole = function () {
3 days ago
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 } });
};
3 days ago
/** 订阅时把未推送历史一次性推给壳 */
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])/;
3 days ago
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;
3 days ago
// 对齐 Chrome:%c 只作用于格式串本身;其后附加参数用默认色、不加引号
for (var r = 0; r < formatted.rest.length; r++) {
3 days ago
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++) {
3 days ago
// 顶层消息字符串不加引号(属性展开时仍用 string 带引号)
var p = this._valuePart(args[i], typeof args[i] === 'string');
parts.push(p);
plain += (i ? ' ' : '') + (p.plain || '');
}
}
3 days ago
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,
3 days ago
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;
}
}
3 days ago
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);