|
|
|
|
@ -25,11 +25,16 @@ |
|
|
|
|
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; |
|
|
|
|
@ -44,6 +49,8 @@ |
|
|
|
|
global.PWDevicePreview.hideOfficialToolbar(); |
|
|
|
|
} |
|
|
|
|
window.addEventListener('message', this._boundMessage); |
|
|
|
|
// 尽早静默缓冲,订阅控制台后再推送到壳(避免打开面板前日志丢失)
|
|
|
|
|
this._hookConsole(true); |
|
|
|
|
this._ensureOverlay(); |
|
|
|
|
this._waitForEngine(function (cc) { |
|
|
|
|
self.cc = cc; |
|
|
|
|
@ -178,13 +185,17 @@ |
|
|
|
|
|
|
|
|
|
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]); |
|
|
|
|
} |
|
|
|
|
@ -217,6 +228,8 @@ |
|
|
|
|
}, 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'); |
|
|
|
|
@ -239,7 +252,13 @@ |
|
|
|
|
} else if (ch === 'assets') { |
|
|
|
|
this._clearTimer('assets'); |
|
|
|
|
} else if (ch === 'console') { |
|
|
|
|
this._hookConsole(false); |
|
|
|
|
// 保持静默缓冲,仅停止推送;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'); |
|
|
|
|
@ -357,6 +376,170 @@ |
|
|
|
|
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; |
|
|
|
|
@ -364,28 +547,12 @@ |
|
|
|
|
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 uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null; |
|
|
|
|
var comps = []; |
|
|
|
|
if (node.components) { |
|
|
|
|
for (var i = 0; i < node.components.length; i++) { |
|
|
|
|
var comp = node.components[i]; |
|
|
|
|
if (!comp || !comp.constructor) continue; |
|
|
|
|
var cname = comp.constructor.name; |
|
|
|
|
if (cname === 'UITransform') continue; |
|
|
|
|
var fields = []; |
|
|
|
|
var keys = Object.keys(comp); |
|
|
|
|
var shown = 0; |
|
|
|
|
for (var k = 0; k < keys.length && shown < 24; k++) { |
|
|
|
|
var key = keys[k]; |
|
|
|
|
if (!key || key.charAt(0) === '_' || key === 'node' || key === 'uuid') continue; |
|
|
|
|
var val = comp[key]; |
|
|
|
|
var t = typeof val; |
|
|
|
|
if (t === 'number' || t === 'boolean' || t === 'string') { |
|
|
|
|
fields.push({ key: key, type: t, value: val }); |
|
|
|
|
shown++; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
comps.push({ name: cname, index: i, fields: fields }); |
|
|
|
|
comps.push(extractCompFields(comp, i, cc)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
@ -396,10 +563,6 @@ |
|
|
|
|
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 }, |
|
|
|
|
ui: uit ? { |
|
|
|
|
width: uit.width, height: uit.height, |
|
|
|
|
anchorX: uit.anchorX, anchorY: uit.anchorY, |
|
|
|
|
} : null, |
|
|
|
|
comps: comps, |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
@ -408,7 +571,6 @@ |
|
|
|
|
var node = this._findNode(uuid); |
|
|
|
|
if (!node || !fid) return false; |
|
|
|
|
var cc = this.cc; |
|
|
|
|
var uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null; |
|
|
|
|
try { |
|
|
|
|
if (fid === 'name') node.name = String(value); |
|
|
|
|
else if (fid === 'active') node.active = !!value; |
|
|
|
|
@ -434,24 +596,132 @@ |
|
|
|
|
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 (uit && (fid === 'uiw' || fid === 'uih' || fid === 'uax' || fid === 'uay')) { |
|
|
|
|
if (fid === 'uiw') uit.width = Number(value); |
|
|
|
|
if (fid === 'uih') uit.height = Number(value); |
|
|
|
|
if (fid === 'uax') uit.anchorX = Number(value); |
|
|
|
|
if (fid === 'uay') uit.anchorY = Number(value); |
|
|
|
|
} else if (fid.indexOf('c') === 0 && fid.indexOf('_') > 0) { |
|
|
|
|
var parts = fid.split('_'); |
|
|
|
|
var ci = parseInt(parts[0].slice(1), 10); |
|
|
|
|
var key = parts.slice(1).join('_'); |
|
|
|
|
if (node.components && node.components[ci]) node.components[ci][key] = value; |
|
|
|
|
return this._setCompProp(node, fid, value); |
|
|
|
|
} else { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} catch (e) { |
|
|
|
|
console.warn('[Agent] set prop', fid, e); |
|
|
|
|
} 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: [] }; |
|
|
|
|
@ -634,40 +904,100 @@ |
|
|
|
|
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._flushConsole(); |
|
|
|
|
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) { |
|
|
|
|
var entry = this._serializeConsoleArgs(level, args); |
|
|
|
|
this._consoleBuf.push(entry); |
|
|
|
|
if (this._consoleBuf.length > 200) this._consoleBuf.splice(0, this._consoleBuf.length - 200); |
|
|
|
|
var self = this; |
|
|
|
|
if (!this._consoleFlushTimer) { |
|
|
|
|
this._consoleFlushTimer = setTimeout(function () { |
|
|
|
|
self._consoleFlushTimer = 0; |
|
|
|
|
self._flushConsole(); |
|
|
|
|
}, CONSOLE_FLUSH_MS); |
|
|
|
|
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._consoleBuf.length) return; |
|
|
|
|
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 打印不受影响。 |
|
|
|
|
@ -676,28 +1006,124 @@ |
|
|
|
|
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 rp = this._valuePart(formatted.rest[r]); |
|
|
|
|
parts.push(rp); |
|
|
|
|
plain += (plain ? ' ' : '') + (rp.plain || ''); |
|
|
|
|
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++) { |
|
|
|
|
var p = this._valuePart(args[i], i === 0 && typeof args[i] === 'string'); |
|
|
|
|
// 顶层消息字符串不加引号(属性展开时仍用 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, |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
@ -751,7 +1177,7 @@ |
|
|
|
|
plain += lit; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return { parts: parts, plain: plain, rest: args.slice(ai) }; |
|
|
|
|
return { parts: parts, plain: plain, rest: args.slice(ai), style: style }; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
WorkbenchAgent.prototype._valuePart = function (value, bareString) { |
|
|
|
|
|