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.
410 lines
17 KiB
410 lines
17 KiB
|
4 days ago
|
(function (global) {
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
var WorkbenchAgent = global.PWAgent;
|
||
|
|
if (!WorkbenchAgent) {
|
||
|
|
console.error('[调试工作台] agent-console 需先加载 agent-core');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var CONSOLE_FLUSH_MS = 100;
|
||
|
|
|
||
|
|
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(-\w+)?\.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);
|
||
|
|
}
|
||
|
|
|
||
|
|
})(window);
|