comate-ui-inspector 0.1.5-alpha.4 → 0.1.5-alpha.5

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.
@@ -133,11 +133,26 @@
133
133
  p.then(
134
134
  function (r) {
135
135
  var t1 = Date.now();
136
- emit('network', {method: method, url: url, status: r.status, startTs: t0, endTs: t1, ms: t1 - t0});
136
+ emit('network', {
137
+ method: method,
138
+ url: url,
139
+ status: r.status,
140
+ startTs: t0,
141
+ endTs: t1,
142
+ ms: t1 - t0,
143
+ });
137
144
  },
138
145
  function () {
139
146
  var t1 = Date.now();
140
- emit('network', {method: method, url: url, status: 0, startTs: t0, endTs: t1, ms: t1 - t0, error: true});
147
+ emit('network', {
148
+ method: method,
149
+ url: url,
150
+ status: 0,
151
+ startTs: t0,
152
+ endTs: t1,
153
+ ms: t1 - t0,
154
+ error: true,
155
+ });
141
156
  }
142
157
  );
143
158
  }
@@ -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 (msg.type === 'ingest' || msg.type === 'state' || msg.type === 'rec-start' || msg.type === 'rec-event' || msg.type === 'rec-stop') {
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,200 @@
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
+ selected.forEach(function (item, i) {
94
+ var info = item.info || {};
95
+ var files = (info.componentChain || []).map(function (c) {
96
+ return c && c.file;
97
+ }).filter(Boolean);
98
+ var row = document.createElement('div');
99
+ row.style.cssText =
100
+ 'display:flex;align-items:flex-start;gap:6px;padding:6px 0;border-top:1px solid #2a2a2a;';
101
+ var meta = document.createElement('div');
102
+ meta.style.cssText = 'flex:1;min-width:0;';
103
+ var title = document.createElement('div');
104
+ title.style.cssText = 'font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
105
+ title.textContent = i + 1 + '. <' + (info.tag || '?') + '>' + (info.text ? ' ' + info.text.slice(0, 20) : '');
106
+ var sub = document.createElement('div');
107
+ sub.style.cssText = 'color:#9aa0a6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
108
+ sub.textContent = files.length ? files[0].split('/').pop() : info.selector || '';
109
+ sub.title = files.length ? files.join(' <- ') : info.selector || '';
110
+ meta.appendChild(title);
111
+ meta.appendChild(sub);
112
+ var del = document.createElement('button');
113
+ del.textContent = '删除';
114
+ del.style.cssText =
115
+ 'flex:none;border:none;background:none;color:#ff6b6b;cursor:pointer;padding:0 2px;font:inherit;';
116
+ del.onclick = function () {
117
+ removeAt(i);
118
+ };
119
+ row.appendChild(meta);
120
+ row.appendChild(del);
121
+ panelList.appendChild(row);
122
+ });
123
+ }
124
+
125
+ function removeAt(i) {
126
+ if (i < 0 || i >= selected.length) {
127
+ return;
128
+ }
129
+ selected[i].box.remove();
130
+ selected.splice(i, 1);
131
+ postSelection();
132
+ renderPanel();
133
+ showHint(selected.length ? '已删除(当前 ' + selected.length + ' 个)' : '已删除全部选中');
134
+ }
135
+
136
+ // 取元素的 Vue 组件祖先链,并带上每层组件的根 DOM 元素(可能没有,fragment 时为 null)。
137
+ function componentChainEls(el) {
138
+ var out = [];
139
+ var inst = el && el.__vueParentComponent;
140
+ while (inst && out.length < 8) {
141
+ var type = inst.type || {};
142
+ var file = type.__file || null;
143
+ var name = type.__name || type.name || null;
144
+ var node = (inst.vnode && inst.vnode.el) || (inst.subTree && inst.subTree.el) || null;
145
+ if (file || name) {
146
+ out.push({name: name, file: file, el: node && node.nodeType === 1 ? node : null});
147
+ }
148
+ inst = inst.parent;
149
+ }
150
+ return out;
151
+ }
152
+
153
+ // 渲染「层级选择」面包屑:叶子标签 + 组件祖先;带 DOM 元素的可点击,点击即选中那一层。
154
+ function renderBreadcrumb(el) {
155
+ if (!active || !el) {
156
+ crumbBar.style.display = 'none';
157
+ return;
158
+ }
159
+ var crumbs = [{label: '<' + el.nodeName.toLowerCase() + '>', el: el}];
160
+ componentChainEls(el).forEach(function (c) {
161
+ crumbs.push({label: c.name || (c.file ? c.file.split('/').pop() : '组件'), el: c.el, file: c.file});
162
+ });
163
+ crumbBar.textContent = '';
164
+ var tip = document.createElement('span');
165
+ tip.textContent = '层级:';
166
+ tip.style.cssText = 'color:#9aa0a6;';
167
+ crumbBar.appendChild(tip);
168
+ crumbs.forEach(function (c, i) {
169
+ if (i) {
170
+ var sep = document.createElement('span');
171
+ sep.textContent = '›';
172
+ sep.style.cssText = 'color:#555;';
173
+ crumbBar.appendChild(sep);
174
+ }
175
+ var btn = document.createElement(c.el ? 'button' : 'span');
176
+ btn.textContent = c.label;
177
+ btn.title = c.file || '';
178
+ btn.style.cssText = c.el
179
+ ? 'border:none;background:#2f2f2f;color:#fff;border-radius:4px;padding:2px 6px;cursor:pointer;font:inherit;'
180
+ : 'color:#9aa0a6;';
181
+ if (c.el) {
182
+ btn.onclick = function () {
183
+ toggleSelect(c.el);
184
+ };
185
+ }
186
+ crumbBar.appendChild(btn);
187
+ });
188
+ crumbBar.style.display = 'flex';
189
+ }
190
+
191
+ // 悬停到页面元素:重置导航路径并高亮,刷新面包屑。
192
+ function setHover(el) {
193
+ navPath = [el];
194
+ navIdx = 0;
195
+ current = el;
196
+ moveOverlay(el);
197
+ renderBreadcrumb(el);
198
+ }
199
+ // ↑ 选父级:按需把父节点压入 navPath,再上移一层。
200
+ function navUp() {
201
+ if (navIdx === navPath.length - 1) {
202
+ var p = navPath[navIdx] && navPath[navIdx].parentElement;
203
+ if (!p || p === document.body || p === document.documentElement || isOwnUi(p)) {
204
+ return;
205
+ }
206
+ navPath.push(p);
207
+ }
208
+ navIdx++;
209
+ current = navPath[navIdx];
210
+ moveOverlay(current);
211
+ renderBreadcrumb(current);
212
+ }
213
+ // ↓ 回子级:沿来时路径下移一层。
214
+ function navDown() {
215
+ if (navIdx === 0) {
216
+ return;
217
+ }
218
+ navIdx--;
219
+ current = navPath[navIdx];
220
+ moveOverlay(current);
221
+ renderBreadcrumb(current);
222
+ }
223
+
30
224
  function showHint(text) {
31
225
  window.clearTimeout(showStatus._t);
32
226
  hint.style.background = '#111';
@@ -130,12 +324,34 @@
130
324
  return el && el.isConnected && el.getClientRects().length > 0;
131
325
  }
132
326
 
133
- // 每帧跟随元素位置;同时剔除已消失(被删除/隐藏)的选中项,连同高亮框一起移除。
327
+ // 解析选中项当前对应的真实元素:优先用已记引用;若框架重渲染把节点换掉了(引用失效),
328
+ // 用当初记录的 CSS 选择器回查找回,避免「还在页面上却被当成消失」而误删。
329
+ function resolveEl(item) {
330
+ if (item.el && item.el.isConnected) {
331
+ return item.el;
332
+ }
333
+ var sel = item.info && item.info.selector;
334
+ if (sel) {
335
+ try {
336
+ var found = document.querySelector(sel);
337
+ if (found) {
338
+ item.el = found;
339
+ return found;
340
+ }
341
+ } catch (e) {
342
+ // 选择器非法,忽略
343
+ }
344
+ }
345
+ return null;
346
+ }
347
+
348
+ // 跟随元素位置;剔除真正消失(引用失效且选择器也找不回)的选中项,连同高亮框一起移除。
134
349
  function repositionBoxes() {
135
350
  var removed = 0;
136
351
  for (var i = selected.length - 1; i >= 0; i--) {
137
- if (isRendered(selected[i].el)) {
138
- placeBox(selected[i].box, selected[i].el);
352
+ var el = resolveEl(selected[i]);
353
+ if (el && isRendered(el)) {
354
+ placeBox(selected[i].box, el);
139
355
  } else {
140
356
  selected[i].box.remove();
141
357
  selected.splice(i, 1);
@@ -144,6 +360,7 @@
144
360
  }
145
361
  if (removed) {
146
362
  postSelection();
363
+ renderPanel();
147
364
  showHint(
148
365
  selected.length
149
366
  ? '有选中元素已消失,已移除(当前 ' + selected.length + ' 个)'
@@ -152,11 +369,45 @@
152
369
  }
153
370
  }
154
371
 
372
+ // 事件驱动重定位:scroll/resize/ResizeObserver/MutationObserver 触发,用 rAF 合并成每帧一次,
373
+ // 再加一个 500ms 轮询兜底(纯 CSS 动画位移这类不触发上述事件的情况)。替代原来每帧常开的 rAF。
374
+ var repoScheduled = false;
375
+ function scheduleReposition() {
376
+ if (repoScheduled || !selected.length) {
377
+ return;
378
+ }
379
+ repoScheduled = true;
380
+ window.requestAnimationFrame(function () {
381
+ repoScheduled = false;
382
+ if (selected.length) {
383
+ repositionBoxes();
384
+ }
385
+ });
386
+ }
387
+ var sizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleReposition) : null;
388
+ function observeSelected() {
389
+ if (!sizeObserver) {
390
+ return;
391
+ }
392
+ sizeObserver.disconnect();
393
+ for (var i = 0; i < selected.length; i++) {
394
+ if (selected[i].el) {
395
+ try {
396
+ sizeObserver.observe(selected[i].el);
397
+ } catch (e) {
398
+ // 已脱离文档,忽略
399
+ }
400
+ }
401
+ }
402
+ }
403
+
155
404
  function clearSelection() {
156
405
  for (var i = 0; i < selected.length; i++) {
157
406
  selected[i].box.remove();
158
407
  }
159
408
  selected = [];
409
+ observeSelected();
410
+ renderPanel();
160
411
  }
161
412
 
162
413
  function indexOfEl(el) {
@@ -192,6 +443,8 @@
192
443
  selected.push({el: el, info: collect(el), box: box});
193
444
  showHint('已选中 ' + selected.length + ' 个,可继续选,或回对话写意见');
194
445
  }
446
+ observeSelected();
447
+ renderPanel();
195
448
  postSelection();
196
449
  }
197
450
  window.addEventListener(
@@ -199,14 +452,24 @@
199
452
  function (e) {
200
453
  if (e.key === 'Alt' && !active) {
201
454
  active = true;
202
- showHint('选择模式:点击元素可多选,再点取消;松开 Alt 退出。点不动时按 Enter 选中当前悬停元素');
455
+ showHint('选择模式:Alt 点选可多选;↑/↓ 选上一级/下一级;Enter 选中当前悬停;松开 Alt 退出');
203
456
  }
204
457
  // 有些元素会吞掉 click(preventDefault/覆盖层),改用 Enter 选中当前 hover 的元素
205
- if (e.key === 'Enter' && active && current && current !== overlay && current !== hint) {
458
+ if (e.key === 'Enter' && active && current && !isOwnUi(current)) {
206
459
  e.preventDefault();
207
460
  e.stopPropagation();
208
461
  toggleSelect(current);
209
462
  }
463
+ // ↑/↓ 在 DOM 树上下级之间切换当前高亮元素(只在按住 Alt 的选择模式下拦截)
464
+ if (active && current && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
465
+ e.preventDefault();
466
+ e.stopPropagation();
467
+ if (e.key === 'ArrowUp') {
468
+ navUp();
469
+ } else {
470
+ navDown();
471
+ }
472
+ }
210
473
  },
211
474
  true
212
475
  );
@@ -216,6 +479,7 @@
216
479
  if (e.key === 'Alt') {
217
480
  active = false;
218
481
  overlay.style.display = 'none';
482
+ crumbBar.style.display = 'none';
219
483
  }
220
484
  },
221
485
  true
@@ -227,9 +491,13 @@
227
491
  return;
228
492
  }
229
493
  var el = document.elementFromPoint(e.clientX, e.clientY);
230
- if (el && el !== current && el !== overlay && el !== hint) {
231
- current = el;
232
- moveOverlay(el);
494
+ // 悬停在插件自身 UI(面板/面包屑)上:不高亮,也不清空当前,便于点面包屑或删除按钮。
495
+ if (isOwnUi(el)) {
496
+ overlay.style.display = 'none';
497
+ return;
498
+ }
499
+ if (el && el !== current) {
500
+ setHover(el);
233
501
  }
234
502
  },
235
503
  true
@@ -240,26 +508,32 @@
240
508
  if (!active || !e.altKey) {
241
509
  return;
242
510
  }
511
+ var el = document.elementFromPoint(e.clientX, e.clientY);
512
+ // 点到面板(如「删除」按钮):放行默认行为,别 preventDefault/选取。
513
+ if (isOwnUi(el)) {
514
+ return;
515
+ }
243
516
  e.preventDefault();
244
517
  e.stopPropagation();
245
- var el = document.elementFromPoint(e.clientX, e.clientY);
246
- if (el && el !== overlay && el !== hint) {
518
+ if (el) {
247
519
  toggleSelect(el);
248
520
  }
249
521
  },
250
522
  true
251
523
  );
252
524
 
253
- window.addEventListener('scroll', repositionBoxes, true);
254
- window.addEventListener('resize', repositionBoxes, true);
255
-
256
- // 每帧跟随并清理:元素被删/隐藏后,repositionBoxes 会移除它的高亮框(scroll/resize 不一定触发)。
257
- (function raf() {
525
+ // 事件驱动的位置跟随(替代每帧常开 rAF):滚动/缩放 + ResizeObserver + DOM 变更,合并到每帧一次;
526
+ // 再加 500ms 轮询兜底,覆盖纯 CSS 位移这类不触发上述事件的情况。
527
+ window.addEventListener('scroll', scheduleReposition, true);
528
+ window.addEventListener('resize', scheduleReposition, true);
529
+ if (typeof MutationObserver !== 'undefined') {
530
+ new MutationObserver(scheduleReposition).observe(document.documentElement, {childList: true, subtree: true});
531
+ }
532
+ window.setInterval(function () {
258
533
  if (selected.length) {
259
534
  repositionBoxes();
260
535
  }
261
- window.requestAnimationFrame(raf);
262
- })();
536
+ }, 500);
263
537
 
264
538
  // bridge-content 轮询回发的消息:cleared(撤高亮)/ status(toast 提示)
265
539
  window.addEventListener('message', function (event) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.5-alpha.4",
3
+ "version": "0.1.5-alpha.5",
4
4
  "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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,7 +0,0 @@
1
- {
2
- "id": "recorder",
3
- "name": "录制",
4
- "main": ["recorder-main.js"],
5
- "pane": "pane.html",
6
- "paneJs": "pane.js"
7
- }
@@ -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
- })();
@@ -1,11 +0,0 @@
1
- [
2
- {
3
- "id": "recorder",
4
- "name": "录制",
5
- "main": [
6
- "recorder-main.js"
7
- ],
8
- "pane": "pane.html",
9
- "paneJs": "pane.js"
10
- }
11
- ]