comate-ui-inspector 0.1.5-alpha.4 → 0.1.5-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/enhancements/recorder/recorder-main.js +102 -6
- package/extension/background.js +7 -1
- package/extension/inspector-main.js +309 -17
- package/hooks/inject-ui-inspector.mjs +47 -46
- package/package.json +1 -1
- package/extension/enhancements/recorder/pane.html +0 -28
- package/extension/enhancements/recorder/pane.js +0 -38
- package/extension/enhancements/recorder/plugin.json +0 -7
- package/extension/enhancements/recorder/recorder-main.js +0 -224
- package/extension/enhancements.json +0 -11
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* MAIN world
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
2
|
+
* MAIN world 录制脚本(增强包):录制期间抓取并**逐条上报**常见用户事件
|
|
3
|
+
* - 点击 click(含 Vue 组件源码链,和选择模式同源)
|
|
4
|
+
* - 滚动 scroll(trailing 防抖,记一段滚动的落点)
|
|
5
|
+
* - 表单变更 change(密码脱敏)、表单提交 submit、操作键 keydown(仅 Enter/Escape)
|
|
6
|
+
* - console.log/info/warn/error(可选)
|
|
7
|
+
* - 同域名下的 fetch / XMLHttpRequest(可选,含发起与完成时间)
|
|
6
8
|
* 每个事件即时 postMessage 出去,经 bridge→background→host/bridge 追加进 recording.json。
|
|
7
9
|
* 这样页面刷新/跳转也不丢数据:缓冲在后端,不在页面内存,录制可跨页面连续进行。
|
|
8
10
|
* 本脚本自身无状态缓冲;未开始录制时完全惰性,不打补丁、无开销。
|
|
@@ -92,6 +94,73 @@
|
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// 滚动:高频事件,trailing 防抖 300ms,只记一段滚动结束时的落点。
|
|
98
|
+
var scrollTimer = null;
|
|
99
|
+
var lastScroll = null;
|
|
100
|
+
function onScroll(e) {
|
|
101
|
+
if (!on) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
var t = e.target;
|
|
105
|
+
var winLevel = t === document || t === window || t === document.documentElement || t === document.body;
|
|
106
|
+
var x = winLevel ? window.scrollX || window.pageXOffset || 0 : t.scrollLeft;
|
|
107
|
+
var y = winLevel ? window.scrollY || window.pageYOffset || 0 : t.scrollTop;
|
|
108
|
+
lastScroll = {
|
|
109
|
+
selector: winLevel ? '(window)' : cssPath(t),
|
|
110
|
+
x: Math.round(x),
|
|
111
|
+
y: Math.round(y),
|
|
112
|
+
ts: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
if (scrollTimer) {
|
|
115
|
+
clearTimeout(scrollTimer);
|
|
116
|
+
}
|
|
117
|
+
scrollTimer = setTimeout(function () {
|
|
118
|
+
scrollTimer = null;
|
|
119
|
+
if (on && lastScroll) {
|
|
120
|
+
emit('scroll', lastScroll);
|
|
121
|
+
lastScroll = null;
|
|
122
|
+
}
|
|
123
|
+
}, 300);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 表单值变更(提交态:blur / 回车 / 选择后触发,天然去抖);密码脱敏,其余截断。
|
|
127
|
+
function onChange(e) {
|
|
128
|
+
if (!on) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
var el = e.target;
|
|
132
|
+
if (!el || el.nodeType !== 1) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
var type = (el.getAttribute && (el.getAttribute('type') || '')).toLowerCase();
|
|
136
|
+
var val;
|
|
137
|
+
if (type === 'password') {
|
|
138
|
+
val = '••••••';
|
|
139
|
+
} else if (type === 'checkbox' || type === 'radio') {
|
|
140
|
+
val = el.checked ? 'checked' : 'unchecked';
|
|
141
|
+
} else {
|
|
142
|
+
val = String(el.value == null ? '' : el.value).slice(0, 100);
|
|
143
|
+
}
|
|
144
|
+
emit('change', {selector: cssPath(el), tag: el.nodeName.toLowerCase(), type: type || undefined, value: val, ts: Date.now()});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onSubmit(e) {
|
|
148
|
+
if (!on) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
var el = e.target;
|
|
152
|
+
emit('submit', {selector: el && el.nodeType === 1 ? cssPath(el) : '(form)', ts: Date.now()});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 只记有意义的操作键(Enter / Escape),避免逐字记录输入内容(噪声 + 隐私)。
|
|
156
|
+
function onKeydown(e) {
|
|
157
|
+
if (!on || (e.key !== 'Enter' && e.key !== 'Escape')) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
var el = e.target;
|
|
161
|
+
emit('keydown', {key: e.key, selector: el && el.nodeType === 1 ? cssPath(el) : '', ts: Date.now()});
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
function startConsole() {
|
|
96
165
|
['log', 'info', 'warn', 'error'].forEach(function (level) {
|
|
97
166
|
saved['c_' + level] = console[level];
|
|
@@ -133,11 +202,26 @@
|
|
|
133
202
|
p.then(
|
|
134
203
|
function (r) {
|
|
135
204
|
var t1 = Date.now();
|
|
136
|
-
emit('network', {
|
|
205
|
+
emit('network', {
|
|
206
|
+
method: method,
|
|
207
|
+
url: url,
|
|
208
|
+
status: r.status,
|
|
209
|
+
startTs: t0,
|
|
210
|
+
endTs: t1,
|
|
211
|
+
ms: t1 - t0,
|
|
212
|
+
});
|
|
137
213
|
},
|
|
138
214
|
function () {
|
|
139
215
|
var t1 = Date.now();
|
|
140
|
-
emit('network', {
|
|
216
|
+
emit('network', {
|
|
217
|
+
method: method,
|
|
218
|
+
url: url,
|
|
219
|
+
status: 0,
|
|
220
|
+
startTs: t0,
|
|
221
|
+
endTs: t1,
|
|
222
|
+
ms: t1 - t0,
|
|
223
|
+
error: true,
|
|
224
|
+
});
|
|
141
225
|
}
|
|
142
226
|
);
|
|
143
227
|
}
|
|
@@ -193,6 +277,10 @@
|
|
|
193
277
|
}
|
|
194
278
|
on = {console: !!opts.console, network: !!opts.network};
|
|
195
279
|
document.addEventListener('click', onClick, true);
|
|
280
|
+
document.addEventListener('scroll', onScroll, true);
|
|
281
|
+
document.addEventListener('change', onChange, true);
|
|
282
|
+
document.addEventListener('submit', onSubmit, true);
|
|
283
|
+
document.addEventListener('keydown', onKeydown, true);
|
|
196
284
|
if (on.console) {
|
|
197
285
|
startConsole();
|
|
198
286
|
}
|
|
@@ -206,6 +294,14 @@
|
|
|
206
294
|
return;
|
|
207
295
|
}
|
|
208
296
|
document.removeEventListener('click', onClick, true);
|
|
297
|
+
document.removeEventListener('scroll', onScroll, true);
|
|
298
|
+
document.removeEventListener('change', onChange, true);
|
|
299
|
+
document.removeEventListener('submit', onSubmit, true);
|
|
300
|
+
document.removeEventListener('keydown', onKeydown, true);
|
|
301
|
+
if (scrollTimer) {
|
|
302
|
+
clearTimeout(scrollTimer);
|
|
303
|
+
scrollTimer = null;
|
|
304
|
+
}
|
|
209
305
|
stopConsole();
|
|
210
306
|
stopNetwork();
|
|
211
307
|
on = null;
|
package/extension/background.js
CHANGED
|
@@ -99,7 +99,13 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
99
99
|
if (!msg || typeof msg.type !== 'string') {
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
msg.type === 'ingest' ||
|
|
104
|
+
msg.type === 'state' ||
|
|
105
|
+
msg.type === 'rec-start' ||
|
|
106
|
+
msg.type === 'rec-event' ||
|
|
107
|
+
msg.type === 'rec-stop'
|
|
108
|
+
) {
|
|
103
109
|
dispatch(msg).then(sendResponse);
|
|
104
110
|
return true; // 异步 sendResponse
|
|
105
111
|
}
|
|
@@ -27,6 +27,202 @@
|
|
|
27
27
|
'opacity:0;transition:opacity 0.15s;pointer-events:none;';
|
|
28
28
|
document.body.appendChild(hint);
|
|
29
29
|
|
|
30
|
+
// 右侧列表弹窗:有选中元素时列出全部已选,支持逐条删除。pointer-events 开(要点删除按钮),
|
|
31
|
+
// 但需在选取逻辑里把它自身排除,避免 Alt 点面板又被当成选元素。
|
|
32
|
+
var panel = document.createElement('div');
|
|
33
|
+
panel.id = 'oreate-ui-inspector-panel';
|
|
34
|
+
panel.style.cssText =
|
|
35
|
+
'position:fixed;top:16px;right:16px;width:260px;max-height:60vh;overflow:auto;z-index:2147483646;' +
|
|
36
|
+
'display:none;background:#111;color:#fff;border-radius:8px;padding:10px 10px 8px;' +
|
|
37
|
+
'font:12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 2px 12px rgba(0,0,0,0.3);';
|
|
38
|
+
var panelHead = document.createElement('div');
|
|
39
|
+
panelHead.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;';
|
|
40
|
+
var panelCount = document.createElement('span');
|
|
41
|
+
panelCount.style.cssText = 'font-weight:600;';
|
|
42
|
+
var panelClear = document.createElement('button');
|
|
43
|
+
panelClear.textContent = '清空';
|
|
44
|
+
panelClear.style.cssText =
|
|
45
|
+
'border:none;background:#2f2f2f;color:#fff;border-radius:4px;padding:2px 8px;cursor:pointer;font:inherit;';
|
|
46
|
+
panelClear.onclick = function () {
|
|
47
|
+
clearSelection();
|
|
48
|
+
postSelection();
|
|
49
|
+
renderPanel();
|
|
50
|
+
showHint('已清空全部选中');
|
|
51
|
+
};
|
|
52
|
+
panelHead.appendChild(panelCount);
|
|
53
|
+
panelHead.appendChild(panelClear);
|
|
54
|
+
var panelList = document.createElement('div');
|
|
55
|
+
panel.appendChild(panelHead);
|
|
56
|
+
panel.appendChild(panelList);
|
|
57
|
+
document.body.appendChild(panel);
|
|
58
|
+
|
|
59
|
+
// 顶部左侧「层级选择」面包屑:悬停元素时列出 标签 + 其 Vue 组件祖先(含 __file),点一级即选中那一层。
|
|
60
|
+
var crumbBar = document.createElement('div');
|
|
61
|
+
crumbBar.id = 'oreate-ui-inspector-crumbs';
|
|
62
|
+
crumbBar.style.cssText =
|
|
63
|
+
'position:fixed;top:16px;left:16px;max-width:60vw;z-index:2147483646;display:none;flex-wrap:wrap;' +
|
|
64
|
+
'align-items:center;gap:4px;background:#111;color:#fff;border-radius:8px;padding:6px 8px;' +
|
|
65
|
+
'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 2px 12px rgba(0,0,0,0.3);';
|
|
66
|
+
document.body.appendChild(crumbBar);
|
|
67
|
+
|
|
68
|
+
// 该元素是否属于本插件自身 UI(面板/面包屑/overlay/hint),选取时要跳过。
|
|
69
|
+
function isOwnUi(el) {
|
|
70
|
+
return (
|
|
71
|
+
!!el &&
|
|
72
|
+
(el === overlay ||
|
|
73
|
+
el === hint ||
|
|
74
|
+
el === panel ||
|
|
75
|
+
panel.contains(el) ||
|
|
76
|
+
el === crumbBar ||
|
|
77
|
+
crumbBar.contains(el))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// DOM 树上下级导航状态:navPath 从叶子往上累积,navIdx 当前停在哪一层(0 = 叶子)。
|
|
82
|
+
var navPath = [];
|
|
83
|
+
var navIdx = 0;
|
|
84
|
+
|
|
85
|
+
function renderPanel() {
|
|
86
|
+
if (!selected.length) {
|
|
87
|
+
panel.style.display = 'none';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
panel.style.display = 'block';
|
|
91
|
+
panelCount.textContent = '已选中 ' + selected.length + ' 个';
|
|
92
|
+
panelList.textContent = '';
|
|
93
|
+
renumber();
|
|
94
|
+
selected.forEach(function (item, i) {
|
|
95
|
+
var info = item.info || {};
|
|
96
|
+
var files = (info.componentChain || []).map(function (c) {
|
|
97
|
+
return c && c.file;
|
|
98
|
+
}).filter(Boolean);
|
|
99
|
+
var row = document.createElement('div');
|
|
100
|
+
row.style.cssText =
|
|
101
|
+
'display:flex;align-items:flex-start;gap:6px;padding:6px 0;border-top:1px solid #2a2a2a;';
|
|
102
|
+
var meta = document.createElement('div');
|
|
103
|
+
meta.style.cssText = 'flex:1;min-width:0;';
|
|
104
|
+
var title = document.createElement('div');
|
|
105
|
+
title.style.cssText = 'font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
106
|
+
title.textContent =
|
|
107
|
+
i + 1 + '. <' + (info.tag || '?') + '>' + (info.text ? ' ' + info.text.slice(0, 20) : '');
|
|
108
|
+
var sub = document.createElement('div');
|
|
109
|
+
sub.style.cssText = 'color:#9aa0a6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
110
|
+
sub.textContent = files.length ? files[0].split('/').pop() : info.selector || '';
|
|
111
|
+
sub.title = files.length ? files.join(' <- ') : info.selector || '';
|
|
112
|
+
meta.appendChild(title);
|
|
113
|
+
meta.appendChild(sub);
|
|
114
|
+
var del = document.createElement('button');
|
|
115
|
+
del.textContent = '删除';
|
|
116
|
+
del.style.cssText =
|
|
117
|
+
'flex:none;border:none;background:none;color:#ff6b6b;cursor:pointer;padding:0 2px;font:inherit;';
|
|
118
|
+
del.onclick = function () {
|
|
119
|
+
removeAt(i);
|
|
120
|
+
};
|
|
121
|
+
row.appendChild(meta);
|
|
122
|
+
row.appendChild(del);
|
|
123
|
+
panelList.appendChild(row);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function removeAt(i) {
|
|
128
|
+
if (i < 0 || i >= selected.length) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
selected[i].box.remove();
|
|
132
|
+
selected.splice(i, 1);
|
|
133
|
+
postSelection();
|
|
134
|
+
renderPanel();
|
|
135
|
+
showHint(selected.length ? '已删除(当前 ' + selected.length + ' 个)' : '已删除全部选中');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 取元素的 Vue 组件祖先链,并带上每层组件的根 DOM 元素(可能没有,fragment 时为 null)。
|
|
139
|
+
function componentChainEls(el) {
|
|
140
|
+
var out = [];
|
|
141
|
+
var inst = el && el.__vueParentComponent;
|
|
142
|
+
while (inst && out.length < 8) {
|
|
143
|
+
var type = inst.type || {};
|
|
144
|
+
var file = type.__file || null;
|
|
145
|
+
var name = type.__name || type.name || null;
|
|
146
|
+
var node = (inst.vnode && inst.vnode.el) || (inst.subTree && inst.subTree.el) || null;
|
|
147
|
+
if (file || name) {
|
|
148
|
+
out.push({name: name, file: file, el: node && node.nodeType === 1 ? node : null});
|
|
149
|
+
}
|
|
150
|
+
inst = inst.parent;
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 渲染「层级选择」面包屑:叶子标签 + 组件祖先;带 DOM 元素的可点击,点击即选中那一层。
|
|
156
|
+
function renderBreadcrumb(el) {
|
|
157
|
+
if (!active || !el) {
|
|
158
|
+
crumbBar.style.display = 'none';
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
var crumbs = [{label: '<' + el.nodeName.toLowerCase() + '>', el: el}];
|
|
162
|
+
componentChainEls(el).forEach(function (c) {
|
|
163
|
+
crumbs.push({label: c.name || (c.file ? c.file.split('/').pop() : '组件'), el: c.el, file: c.file});
|
|
164
|
+
});
|
|
165
|
+
crumbBar.textContent = '';
|
|
166
|
+
var tip = document.createElement('span');
|
|
167
|
+
tip.textContent = '层级:';
|
|
168
|
+
tip.style.cssText = 'color:#9aa0a6;';
|
|
169
|
+
crumbBar.appendChild(tip);
|
|
170
|
+
crumbs.forEach(function (c, i) {
|
|
171
|
+
if (i) {
|
|
172
|
+
var sep = document.createElement('span');
|
|
173
|
+
sep.textContent = '›';
|
|
174
|
+
sep.style.cssText = 'color:#555;';
|
|
175
|
+
crumbBar.appendChild(sep);
|
|
176
|
+
}
|
|
177
|
+
var btn = document.createElement(c.el ? 'button' : 'span');
|
|
178
|
+
btn.textContent = c.label;
|
|
179
|
+
btn.title = c.file || '';
|
|
180
|
+
btn.style.cssText = c.el
|
|
181
|
+
? 'border:none;background:#2f2f2f;color:#fff;border-radius:4px;padding:2px 6px;cursor:pointer;font:inherit;'
|
|
182
|
+
: 'color:#9aa0a6;';
|
|
183
|
+
if (c.el) {
|
|
184
|
+
btn.onclick = function () {
|
|
185
|
+
toggleSelect(c.el);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
crumbBar.appendChild(btn);
|
|
189
|
+
});
|
|
190
|
+
crumbBar.style.display = 'flex';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 悬停到页面元素:重置导航路径并高亮,刷新面包屑。
|
|
194
|
+
function setHover(el) {
|
|
195
|
+
navPath = [el];
|
|
196
|
+
navIdx = 0;
|
|
197
|
+
current = el;
|
|
198
|
+
moveOverlay(el);
|
|
199
|
+
renderBreadcrumb(el);
|
|
200
|
+
}
|
|
201
|
+
// ↑ 选父级:按需把父节点压入 navPath,再上移一层。
|
|
202
|
+
function navUp() {
|
|
203
|
+
if (navIdx === navPath.length - 1) {
|
|
204
|
+
var p = navPath[navIdx] && navPath[navIdx].parentElement;
|
|
205
|
+
if (!p || p === document.body || p === document.documentElement || isOwnUi(p)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
navPath.push(p);
|
|
209
|
+
}
|
|
210
|
+
navIdx++;
|
|
211
|
+
current = navPath[navIdx];
|
|
212
|
+
moveOverlay(current);
|
|
213
|
+
renderBreadcrumb(current);
|
|
214
|
+
}
|
|
215
|
+
// ↓ 回子级:沿来时路径下移一层。
|
|
216
|
+
function navDown() {
|
|
217
|
+
if (navIdx === 0) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
navIdx--;
|
|
221
|
+
current = navPath[navIdx];
|
|
222
|
+
moveOverlay(current);
|
|
223
|
+
renderBreadcrumb(current);
|
|
224
|
+
}
|
|
225
|
+
|
|
30
226
|
function showHint(text) {
|
|
31
227
|
window.clearTimeout(showStatus._t);
|
|
32
228
|
hint.style.background = '#111';
|
|
@@ -113,10 +309,26 @@
|
|
|
113
309
|
box.style.cssText =
|
|
114
310
|
'position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #16a34a;' +
|
|
115
311
|
'background:rgba(22,163,74,0.10);border-radius:4px;';
|
|
312
|
+
var badge = document.createElement('div');
|
|
313
|
+
badge.className = 'oreate-sel-badge';
|
|
314
|
+
badge.style.cssText =
|
|
315
|
+
'position:absolute;left:-2px;top:-2px;transform:translateY(-100%);background:#16a34a;color:#fff;' +
|
|
316
|
+
'font:11px/1.5 sans-serif;padding:0 6px;border-radius:4px 4px 4px 0;white-space:nowrap;';
|
|
317
|
+
box.appendChild(badge);
|
|
116
318
|
document.body.appendChild(box);
|
|
117
319
|
return box;
|
|
118
320
|
}
|
|
119
321
|
|
|
322
|
+
// 按当前选中顺序刷新每个高亮框上的数字角标(删除中间项后序号会重排)。
|
|
323
|
+
function renumber() {
|
|
324
|
+
for (var i = 0; i < selected.length; i++) {
|
|
325
|
+
var b = selected[i].box.querySelector('.oreate-sel-badge');
|
|
326
|
+
if (b) {
|
|
327
|
+
b.textContent = String(i + 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
120
332
|
function placeBox(box, el) {
|
|
121
333
|
var rect = el.getBoundingClientRect();
|
|
122
334
|
box.style.left = rect.left + 'px';
|
|
@@ -130,12 +342,34 @@
|
|
|
130
342
|
return el && el.isConnected && el.getClientRects().length > 0;
|
|
131
343
|
}
|
|
132
344
|
|
|
133
|
-
//
|
|
345
|
+
// 解析选中项当前对应的真实元素:优先用已记引用;若框架重渲染把节点换掉了(引用失效),
|
|
346
|
+
// 用当初记录的 CSS 选择器回查找回,避免「还在页面上却被当成消失」而误删。
|
|
347
|
+
function resolveEl(item) {
|
|
348
|
+
if (item.el && item.el.isConnected) {
|
|
349
|
+
return item.el;
|
|
350
|
+
}
|
|
351
|
+
var sel = item.info && item.info.selector;
|
|
352
|
+
if (sel) {
|
|
353
|
+
try {
|
|
354
|
+
var found = document.querySelector(sel);
|
|
355
|
+
if (found) {
|
|
356
|
+
item.el = found;
|
|
357
|
+
return found;
|
|
358
|
+
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
// 选择器非法,忽略
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 跟随元素位置;剔除真正消失(引用失效且选择器也找不回)的选中项,连同高亮框一起移除。
|
|
134
367
|
function repositionBoxes() {
|
|
135
368
|
var removed = 0;
|
|
136
369
|
for (var i = selected.length - 1; i >= 0; i--) {
|
|
137
|
-
|
|
138
|
-
|
|
370
|
+
var el = resolveEl(selected[i]);
|
|
371
|
+
if (el && isRendered(el)) {
|
|
372
|
+
placeBox(selected[i].box, el);
|
|
139
373
|
} else {
|
|
140
374
|
selected[i].box.remove();
|
|
141
375
|
selected.splice(i, 1);
|
|
@@ -144,6 +378,7 @@
|
|
|
144
378
|
}
|
|
145
379
|
if (removed) {
|
|
146
380
|
postSelection();
|
|
381
|
+
renderPanel();
|
|
147
382
|
showHint(
|
|
148
383
|
selected.length
|
|
149
384
|
? '有选中元素已消失,已移除(当前 ' + selected.length + ' 个)'
|
|
@@ -152,11 +387,45 @@
|
|
|
152
387
|
}
|
|
153
388
|
}
|
|
154
389
|
|
|
390
|
+
// 事件驱动重定位:scroll/resize/ResizeObserver/MutationObserver 触发,用 rAF 合并成每帧一次,
|
|
391
|
+
// 再加一个 500ms 轮询兜底(纯 CSS 动画位移这类不触发上述事件的情况)。替代原来每帧常开的 rAF。
|
|
392
|
+
var repoScheduled = false;
|
|
393
|
+
function scheduleReposition() {
|
|
394
|
+
if (repoScheduled || !selected.length) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
repoScheduled = true;
|
|
398
|
+
window.requestAnimationFrame(function () {
|
|
399
|
+
repoScheduled = false;
|
|
400
|
+
if (selected.length) {
|
|
401
|
+
repositionBoxes();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
var sizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleReposition) : null;
|
|
406
|
+
function observeSelected() {
|
|
407
|
+
if (!sizeObserver) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
sizeObserver.disconnect();
|
|
411
|
+
for (var i = 0; i < selected.length; i++) {
|
|
412
|
+
if (selected[i].el) {
|
|
413
|
+
try {
|
|
414
|
+
sizeObserver.observe(selected[i].el);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
// 已脱离文档,忽略
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
155
422
|
function clearSelection() {
|
|
156
423
|
for (var i = 0; i < selected.length; i++) {
|
|
157
424
|
selected[i].box.remove();
|
|
158
425
|
}
|
|
159
426
|
selected = [];
|
|
427
|
+
observeSelected();
|
|
428
|
+
renderPanel();
|
|
160
429
|
}
|
|
161
430
|
|
|
162
431
|
function indexOfEl(el) {
|
|
@@ -192,6 +461,8 @@
|
|
|
192
461
|
selected.push({el: el, info: collect(el), box: box});
|
|
193
462
|
showHint('已选中 ' + selected.length + ' 个,可继续选,或回对话写意见');
|
|
194
463
|
}
|
|
464
|
+
observeSelected();
|
|
465
|
+
renderPanel();
|
|
195
466
|
postSelection();
|
|
196
467
|
}
|
|
197
468
|
window.addEventListener(
|
|
@@ -199,14 +470,24 @@
|
|
|
199
470
|
function (e) {
|
|
200
471
|
if (e.key === 'Alt' && !active) {
|
|
201
472
|
active = true;
|
|
202
|
-
showHint('
|
|
473
|
+
showHint('选择模式:Alt 点选可多选;↑/↓ 选上一级/下一级;Enter 选中当前悬停;松开 Alt 退出');
|
|
203
474
|
}
|
|
204
475
|
// 有些元素会吞掉 click(preventDefault/覆盖层),改用 Enter 选中当前 hover 的元素
|
|
205
|
-
if (e.key === 'Enter' && active && current && current
|
|
476
|
+
if (e.key === 'Enter' && active && current && !isOwnUi(current)) {
|
|
206
477
|
e.preventDefault();
|
|
207
478
|
e.stopPropagation();
|
|
208
479
|
toggleSelect(current);
|
|
209
480
|
}
|
|
481
|
+
// ↑/↓ 在 DOM 树上下级之间切换当前高亮元素(只在按住 Alt 的选择模式下拦截)
|
|
482
|
+
if (active && current && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
483
|
+
e.preventDefault();
|
|
484
|
+
e.stopPropagation();
|
|
485
|
+
if (e.key === 'ArrowUp') {
|
|
486
|
+
navUp();
|
|
487
|
+
} else {
|
|
488
|
+
navDown();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
210
491
|
},
|
|
211
492
|
true
|
|
212
493
|
);
|
|
@@ -216,6 +497,7 @@
|
|
|
216
497
|
if (e.key === 'Alt') {
|
|
217
498
|
active = false;
|
|
218
499
|
overlay.style.display = 'none';
|
|
500
|
+
crumbBar.style.display = 'none';
|
|
219
501
|
}
|
|
220
502
|
},
|
|
221
503
|
true
|
|
@@ -227,9 +509,13 @@
|
|
|
227
509
|
return;
|
|
228
510
|
}
|
|
229
511
|
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
512
|
+
// 悬停在插件自身 UI(面板/面包屑)上:不高亮,也不清空当前,便于点面包屑或删除按钮。
|
|
513
|
+
if (isOwnUi(el)) {
|
|
514
|
+
overlay.style.display = 'none';
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (el && el !== current) {
|
|
518
|
+
setHover(el);
|
|
233
519
|
}
|
|
234
520
|
},
|
|
235
521
|
true
|
|
@@ -240,26 +526,32 @@
|
|
|
240
526
|
if (!active || !e.altKey) {
|
|
241
527
|
return;
|
|
242
528
|
}
|
|
529
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
530
|
+
// 点到面板(如「删除」按钮):放行默认行为,别 preventDefault/选取。
|
|
531
|
+
if (isOwnUi(el)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
243
534
|
e.preventDefault();
|
|
244
535
|
e.stopPropagation();
|
|
245
|
-
|
|
246
|
-
if (el && el !== overlay && el !== hint) {
|
|
536
|
+
if (el) {
|
|
247
537
|
toggleSelect(el);
|
|
248
538
|
}
|
|
249
539
|
},
|
|
250
540
|
true
|
|
251
541
|
);
|
|
252
542
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
(
|
|
543
|
+
// 事件驱动的位置跟随(替代每帧常开 rAF):滚动/缩放 + ResizeObserver + DOM 变更,合并到每帧一次;
|
|
544
|
+
// 再加 500ms 轮询兜底,覆盖纯 CSS 位移这类不触发上述事件的情况。
|
|
545
|
+
window.addEventListener('scroll', scheduleReposition, true);
|
|
546
|
+
window.addEventListener('resize', scheduleReposition, true);
|
|
547
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
548
|
+
new MutationObserver(scheduleReposition).observe(document.documentElement, {childList: true, subtree: true});
|
|
549
|
+
}
|
|
550
|
+
window.setInterval(function () {
|
|
258
551
|
if (selected.length) {
|
|
259
552
|
repositionBoxes();
|
|
260
553
|
}
|
|
261
|
-
|
|
262
|
-
})();
|
|
554
|
+
}, 500);
|
|
263
555
|
|
|
264
556
|
// bridge-content 轮询回发的消息:cleared(撤高亮)/ status(toast 提示)
|
|
265
557
|
window.addEventListener('message', function (event) {
|
|
@@ -33,16 +33,14 @@ function readJson(file, fallback) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// 读 append-only
|
|
36
|
+
// 读 append-only 事件流,返回有序的 {kind, item} 列表(各类用户事件混在一起,按写入顺序)。
|
|
37
37
|
function readEvents() {
|
|
38
|
-
const
|
|
39
|
-
const con = [];
|
|
40
|
-
const network = [];
|
|
38
|
+
const out = [];
|
|
41
39
|
let text = '';
|
|
42
40
|
try {
|
|
43
41
|
text = readFileSync(eventsFile, 'utf8');
|
|
44
42
|
} catch {
|
|
45
|
-
return
|
|
43
|
+
return out;
|
|
46
44
|
}
|
|
47
45
|
for (const line of text.split('\n')) {
|
|
48
46
|
if (!line.trim()) {
|
|
@@ -54,18 +52,11 @@ function readEvents() {
|
|
|
54
52
|
} catch {
|
|
55
53
|
continue;
|
|
56
54
|
}
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (ev.kind === 'click') {
|
|
61
|
-
clicks.push(ev.item);
|
|
62
|
-
} else if (ev.kind === 'console') {
|
|
63
|
-
con.push(ev.item);
|
|
64
|
-
} else if (ev.kind === 'network') {
|
|
65
|
-
network.push(ev.item);
|
|
55
|
+
if (ev && ev.kind && ev.item) {
|
|
56
|
+
out.push(ev);
|
|
66
57
|
}
|
|
67
58
|
}
|
|
68
|
-
return
|
|
59
|
+
return out;
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
const parsed = readJson(storeFile, []);
|
|
@@ -74,9 +65,8 @@ const meta = readJson(recordingFile, null);
|
|
|
74
65
|
// 只注入**已停止**的录制(active === false)。录制进行中(跨刷新/跨页面累积)时不消费、不清空,
|
|
75
66
|
// 以免中途给 Comate 发消息把整段会话吞掉。
|
|
76
67
|
const recDone = meta && meta.active === false;
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const hasRec = recDone && (events.clicks.length || events.console.length || events.network.length);
|
|
68
|
+
const recEvents = recDone ? readEvents() : [];
|
|
69
|
+
const hasRec = recDone && recEvents.length > 0;
|
|
80
70
|
|
|
81
71
|
if (!list.length && !hasRec) {
|
|
82
72
|
emit({decision: 'allow'});
|
|
@@ -95,38 +85,49 @@ function describe(target, index, total) {
|
|
|
95
85
|
.join('\n');
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
88
|
+
// 把各类用户事件(点击/滚动/输入/提交/按键/控制台/网络)归并成一条按时间排序的时间线。
|
|
89
|
+
function describeRecording(meta, evs) {
|
|
90
|
+
const base = meta.startedAt || 0;
|
|
91
|
+
const secs = Math.round(((meta.endedAt || 0) - base || 0) / 1000);
|
|
101
92
|
const rel = t => (typeof t === 'number' ? `+${((t - base) / 1000).toFixed(2)}s` : '+?');
|
|
102
93
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (const ev of evs) {
|
|
96
|
+
const it = ev.item || {};
|
|
97
|
+
if (ev.kind === 'click') {
|
|
98
|
+
const files = (it.componentChain || []).map(x => x && x.file).filter(Boolean);
|
|
99
|
+
lines.push({
|
|
100
|
+
ts: it.ts,
|
|
101
|
+
text:
|
|
102
|
+
`点击 <${it.tag}> ${JSON.stringify((it.text || '').slice(0, 40))} @ ${it.selector}` +
|
|
103
|
+
(files.length ? ` | 源码 ${files.join(' <- ')}` : ''),
|
|
104
|
+
});
|
|
105
|
+
} else if (ev.kind === 'scroll') {
|
|
106
|
+
lines.push({ts: it.ts, text: `滚动 ${it.selector} → (${it.x}, ${it.y})`});
|
|
107
|
+
} else if (ev.kind === 'change') {
|
|
108
|
+
lines.push({ts: it.ts, text: `输入/变更 <${it.tag}> @ ${it.selector} = ${JSON.stringify(String(it.value ?? '').slice(0, 60))}`});
|
|
109
|
+
} else if (ev.kind === 'submit') {
|
|
110
|
+
lines.push({ts: it.ts, text: `提交表单 @ ${it.selector}`});
|
|
111
|
+
} else if (ev.kind === 'keydown') {
|
|
112
|
+
lines.push({ts: it.ts, text: `按键 ${it.key}${it.selector ? ` @ ${it.selector}` : ''}`});
|
|
113
|
+
} else if (ev.kind === 'console') {
|
|
114
|
+
lines.push({ts: it.ts, text: `控制台[${it.level}] ${String(it.text).slice(0, 300)}`});
|
|
115
|
+
} else if (ev.kind === 'network') {
|
|
116
|
+
lines.push({ts: it.startTs, text: `网络→发起 ${it.method} ${it.url}`});
|
|
117
|
+
lines.push({ts: it.endTs, text: `网络←完成 ${it.method} ${it.url} → ${it.status} (${it.ms}ms)`});
|
|
118
|
+
} else {
|
|
119
|
+
lines.push({ts: it.ts, text: `${ev.kind} ${JSON.stringify(it).slice(0, 120)}`});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// V8 的 sort 稳定:同一时刻保持插入序(如网络发起在完成之前)。
|
|
123
|
+
lines.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
123
124
|
|
|
124
125
|
const parts = [
|
|
125
|
-
`【ui-inspector 录制】用户录制了一段操作(页面 ${
|
|
126
|
+
`【ui-inspector 录制】用户录制了一段操作(页面 ${meta.pageUrl || ''},约 ${secs}s,共 ${lines.length} 个事件,时间为相对录制起点的偏移):`,
|
|
126
127
|
];
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
parts.push(` …(其余 ${
|
|
128
|
+
lines.slice(0, 160).forEach(e => parts.push(` ${rel(e.ts)} ${e.text}`));
|
|
129
|
+
if (lines.length > 160) {
|
|
130
|
+
parts.push(` …(其余 ${lines.length - 160} 个事件省略)`);
|
|
130
131
|
}
|
|
131
132
|
return parts.join('\n');
|
|
132
133
|
}
|
|
@@ -142,7 +143,7 @@ if (list.length) {
|
|
|
142
143
|
);
|
|
143
144
|
}
|
|
144
145
|
if (hasRec) {
|
|
145
|
-
sections.push(describeRecording(
|
|
146
|
+
sections.push(describeRecording(meta, recEvents));
|
|
146
147
|
}
|
|
147
148
|
sections.push(
|
|
148
149
|
'请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。'
|
package/package.json
CHANGED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<label class="radio">
|
|
2
|
-
<input
|
|
3
|
-
type="checkbox"
|
|
4
|
-
id="rec-console"
|
|
5
|
-
checked
|
|
6
|
-
/>
|
|
7
|
-
录制控制台输出
|
|
8
|
-
</label>
|
|
9
|
-
<label class="radio">
|
|
10
|
-
<input
|
|
11
|
-
type="checkbox"
|
|
12
|
-
id="rec-network"
|
|
13
|
-
checked
|
|
14
|
-
/>
|
|
15
|
-
录制网络请求
|
|
16
|
-
</label>
|
|
17
|
-
<button
|
|
18
|
-
id="rec-toggle"
|
|
19
|
-
class="primary"
|
|
20
|
-
style="margin-top: 6px"
|
|
21
|
-
>
|
|
22
|
-
开始录制
|
|
23
|
-
</button>
|
|
24
|
-
<div
|
|
25
|
-
id="rec-status"
|
|
26
|
-
class="hint"
|
|
27
|
-
></div>
|
|
28
|
-
<div class="hint">录制点击的元素(含组件源码)+ 勾选项;停止后在下次发消息时自动带给 Agent。</div>
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* 录制增强包的弹窗面板逻辑(随 pane.html 一起由 popup.js 注入后加载)。
|
|
3
|
-
* 自包含:读写 chrome.storage.local.recording {active,console,network,startedAt},
|
|
4
|
-
* bridge-content 监听该键的变化,驱动 MAIN 世界的 recorder-main.js 起停。
|
|
5
|
-
*/
|
|
6
|
-
(function () {
|
|
7
|
-
const byId = id => document.getElementById(id);
|
|
8
|
-
|
|
9
|
-
async function refresh() {
|
|
10
|
-
const {recording} = await chrome.storage.local.get({recording: null});
|
|
11
|
-
const active = !!(recording && recording.active);
|
|
12
|
-
byId('rec-toggle').textContent = active ? '停止录制' : '开始录制';
|
|
13
|
-
byId('rec-status').textContent = active ? '录制中…(操作页面后点停止)' : '';
|
|
14
|
-
byId('rec-console').disabled = active;
|
|
15
|
-
byId('rec-network').disabled = active;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
byId('rec-toggle').onclick = async () => {
|
|
19
|
-
const {recording} = await chrome.storage.local.get({recording: null});
|
|
20
|
-
if (recording && recording.active) {
|
|
21
|
-
await chrome.storage.local.set({recording: {active: false}});
|
|
22
|
-
refresh();
|
|
23
|
-
} else {
|
|
24
|
-
await chrome.storage.local.set({
|
|
25
|
-
recording: {
|
|
26
|
-
active: true,
|
|
27
|
-
console: byId('rec-console').checked,
|
|
28
|
-
network: byId('rec-network').checked,
|
|
29
|
-
startedAt: Date.now(),
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
// 开始录制后关闭 popup,让用户直接去页面操作;页面顶部会出现「正在录制」提示条。
|
|
33
|
-
window.close();
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
refresh();
|
|
38
|
-
})();
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* MAIN world 录制脚本(增强包):录制期间抓取并**逐条上报**
|
|
3
|
-
* - 点击的元素(含 Vue 组件源码链,和选择模式同源)
|
|
4
|
-
* - console.log/info/warn/error
|
|
5
|
-
* - 同域名下的 fetch / XMLHttpRequest(方法/URL/状态/发起与完成时间)
|
|
6
|
-
* 每个事件即时 postMessage 出去,经 bridge→background→host/bridge 追加进 recording.json。
|
|
7
|
-
* 这样页面刷新/跳转也不丢数据:缓冲在后端,不在页面内存,录制可跨页面连续进行。
|
|
8
|
-
* 本脚本自身无状态缓冲;未开始录制时完全惰性,不打补丁、无开销。
|
|
9
|
-
*/
|
|
10
|
-
(function () {
|
|
11
|
-
if (window.__oreateRecorder) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
window.__oreateRecorder = true;
|
|
15
|
-
|
|
16
|
-
var on = null; // 录制中:{console, network};未录制:null
|
|
17
|
-
var saved = {}; // 被打补丁的原函数
|
|
18
|
-
|
|
19
|
-
function cssPath(el) {
|
|
20
|
-
var parts = [];
|
|
21
|
-
var node = el;
|
|
22
|
-
while (node && node.nodeType === 1 && parts.length < 6) {
|
|
23
|
-
var seg = node.nodeName.toLowerCase();
|
|
24
|
-
if (node.id) {
|
|
25
|
-
parts.unshift(seg + '#' + node.id);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
var cls = (node.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean).slice(0, 2);
|
|
29
|
-
if (cls.length) {
|
|
30
|
-
seg += '.' + cls.join('.');
|
|
31
|
-
}
|
|
32
|
-
parts.unshift(seg);
|
|
33
|
-
node = node.parentElement;
|
|
34
|
-
}
|
|
35
|
-
return parts.join(' > ');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function componentChain(el) {
|
|
39
|
-
var chain = [];
|
|
40
|
-
var instance = el.__vueParentComponent;
|
|
41
|
-
while (instance && chain.length < 6) {
|
|
42
|
-
var type = instance.type || {};
|
|
43
|
-
var file = type.__file || null;
|
|
44
|
-
var name = type.__name || type.name || null;
|
|
45
|
-
if (file || name) {
|
|
46
|
-
chain.push({name: name, file: file});
|
|
47
|
-
}
|
|
48
|
-
instance = instance.parent;
|
|
49
|
-
}
|
|
50
|
-
return chain;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function fmt(a) {
|
|
54
|
-
if (typeof a === 'string') {
|
|
55
|
-
return a;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
return JSON.stringify(a);
|
|
59
|
-
} catch (e) {
|
|
60
|
-
return String(a);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function sameHost(url) {
|
|
65
|
-
try {
|
|
66
|
-
return new URL(url, location.href).host === location.host;
|
|
67
|
-
} catch (e) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 逐条上报:kind = click | console | network
|
|
73
|
-
function emit(kind, item) {
|
|
74
|
-
window.postMessage({__uiInspector: 'rec-event', kind: kind, item: item}, '*');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function onClick(e) {
|
|
78
|
-
if (!on) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
var el = e.target;
|
|
82
|
-
if (!el || el.nodeType !== 1) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
emit('click', {
|
|
86
|
-
selector: cssPath(el),
|
|
87
|
-
tag: el.nodeName.toLowerCase(),
|
|
88
|
-
text: (el.textContent || '').trim().slice(0, 80),
|
|
89
|
-
componentChain: componentChain(el),
|
|
90
|
-
pageUrl: location.href,
|
|
91
|
-
ts: Date.now(),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function startConsole() {
|
|
96
|
-
['log', 'info', 'warn', 'error'].forEach(function (level) {
|
|
97
|
-
saved['c_' + level] = console[level];
|
|
98
|
-
console[level] = function () {
|
|
99
|
-
try {
|
|
100
|
-
if (on) {
|
|
101
|
-
emit('console', {
|
|
102
|
-
level: level,
|
|
103
|
-
text: Array.prototype.map.call(arguments, fmt).join(' ').slice(0, 2000),
|
|
104
|
-
ts: Date.now(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
} catch (e) {
|
|
108
|
-
// 记录失败不影响原 console
|
|
109
|
-
}
|
|
110
|
-
return saved['c_' + level].apply(console, arguments);
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function stopConsole() {
|
|
116
|
-
['log', 'info', 'warn', 'error'].forEach(function (level) {
|
|
117
|
-
if (saved['c_' + level]) {
|
|
118
|
-
console[level] = saved['c_' + level];
|
|
119
|
-
delete saved['c_' + level];
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function startNetwork() {
|
|
125
|
-
if (window.fetch) {
|
|
126
|
-
saved.fetch = window.fetch;
|
|
127
|
-
window.fetch = function (input, init) {
|
|
128
|
-
var url = typeof input === 'string' ? input : (input && input.url) || '';
|
|
129
|
-
var method = (init && init.method) || (input && input.method) || 'GET';
|
|
130
|
-
var t0 = Date.now();
|
|
131
|
-
var p = saved.fetch.apply(this, arguments);
|
|
132
|
-
if (on && sameHost(url)) {
|
|
133
|
-
p.then(
|
|
134
|
-
function (r) {
|
|
135
|
-
var t1 = Date.now();
|
|
136
|
-
emit('network', {method: method, url: url, status: r.status, startTs: t0, endTs: t1, ms: t1 - t0});
|
|
137
|
-
},
|
|
138
|
-
function () {
|
|
139
|
-
var t1 = Date.now();
|
|
140
|
-
emit('network', {method: method, url: url, status: 0, startTs: t0, endTs: t1, ms: t1 - t0, error: true});
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return p;
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
saved.xhrOpen = XMLHttpRequest.prototype.open;
|
|
148
|
-
saved.xhrSend = XMLHttpRequest.prototype.send;
|
|
149
|
-
XMLHttpRequest.prototype.open = function (method, url) {
|
|
150
|
-
this.__rec = {method: method, url: url, t0: Date.now()};
|
|
151
|
-
return saved.xhrOpen.apply(this, arguments);
|
|
152
|
-
};
|
|
153
|
-
XMLHttpRequest.prototype.send = function () {
|
|
154
|
-
var xhr = this;
|
|
155
|
-
var info = xhr.__rec;
|
|
156
|
-
if (info) {
|
|
157
|
-
xhr.addEventListener('loadend', function () {
|
|
158
|
-
if (on && sameHost(info.url)) {
|
|
159
|
-
var t1 = Date.now();
|
|
160
|
-
emit('network', {
|
|
161
|
-
method: info.method,
|
|
162
|
-
url: info.url,
|
|
163
|
-
status: xhr.status,
|
|
164
|
-
startTs: info.t0,
|
|
165
|
-
endTs: t1,
|
|
166
|
-
ms: t1 - info.t0,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return saved.xhrSend.apply(this, arguments);
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function stopNetwork() {
|
|
176
|
-
if (saved.fetch) {
|
|
177
|
-
window.fetch = saved.fetch;
|
|
178
|
-
delete saved.fetch;
|
|
179
|
-
}
|
|
180
|
-
if (saved.xhrOpen) {
|
|
181
|
-
XMLHttpRequest.prototype.open = saved.xhrOpen;
|
|
182
|
-
delete saved.xhrOpen;
|
|
183
|
-
}
|
|
184
|
-
if (saved.xhrSend) {
|
|
185
|
-
XMLHttpRequest.prototype.send = saved.xhrSend;
|
|
186
|
-
delete saved.xhrSend;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function start(opts) {
|
|
191
|
-
if (on) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
on = {console: !!opts.console, network: !!opts.network};
|
|
195
|
-
document.addEventListener('click', onClick, true);
|
|
196
|
-
if (on.console) {
|
|
197
|
-
startConsole();
|
|
198
|
-
}
|
|
199
|
-
if (on.network) {
|
|
200
|
-
startNetwork();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function stop() {
|
|
205
|
-
if (!on) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
document.removeEventListener('click', onClick, true);
|
|
209
|
-
stopConsole();
|
|
210
|
-
stopNetwork();
|
|
211
|
-
on = null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
window.addEventListener('message', function (ev) {
|
|
215
|
-
if (ev.source !== window || !ev.data || ev.data.__uiInspectorBridge !== 'rec-control') {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (ev.data.active) {
|
|
219
|
-
start({console: !!ev.data.console, network: !!ev.data.network});
|
|
220
|
-
} else {
|
|
221
|
-
stop();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
})();
|