comate-ui-inspector 0.1.5-alpha.5 → 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.
@@ -1,8 +1,10 @@
1
1
  /*
2
- * MAIN world 录制脚本(增强包):录制期间抓取并**逐条上报**
3
- * - 点击的元素(含 Vue 组件源码链,和选择模式同源)
4
- * - console.log/info/warn/error
5
- * - 同域名下的 fetch / XMLHttpRequest(方法/URL/状态/发起与完成时间)
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];
@@ -208,6 +277,10 @@
208
277
  }
209
278
  on = {console: !!opts.console, network: !!opts.network};
210
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);
211
284
  if (on.console) {
212
285
  startConsole();
213
286
  }
@@ -221,6 +294,14 @@
221
294
  return;
222
295
  }
223
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
+ }
224
305
  stopConsole();
225
306
  stopNetwork();
226
307
  on = null;
@@ -90,6 +90,7 @@
90
90
  panel.style.display = 'block';
91
91
  panelCount.textContent = '已选中 ' + selected.length + ' 个';
92
92
  panelList.textContent = '';
93
+ renumber();
93
94
  selected.forEach(function (item, i) {
94
95
  var info = item.info || {};
95
96
  var files = (info.componentChain || []).map(function (c) {
@@ -102,7 +103,8 @@
102
103
  meta.style.cssText = 'flex:1;min-width:0;';
103
104
  var title = document.createElement('div');
104
105
  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
+ title.textContent =
107
+ i + 1 + '. <' + (info.tag || '?') + '>' + (info.text ? ' ' + info.text.slice(0, 20) : '');
106
108
  var sub = document.createElement('div');
107
109
  sub.style.cssText = 'color:#9aa0a6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
108
110
  sub.textContent = files.length ? files[0].split('/').pop() : info.selector || '';
@@ -307,10 +309,26 @@
307
309
  box.style.cssText =
308
310
  'position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #16a34a;' +
309
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);
310
318
  document.body.appendChild(box);
311
319
  return box;
312
320
  }
313
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
+
314
332
  function placeBox(box, el) {
315
333
  var rect = el.getBoundingClientRect();
316
334
  box.style.left = rect.left + 'px';
@@ -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 clicks = [];
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 {clicks, console: con, network};
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 (!ev || !ev.item) {
58
- continue;
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 {clicks, console: con, network};
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 events = recDone ? readEvents() : {clicks: [], console: [], network: []};
78
- const recording = recDone ? {...meta, ...events} : null;
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
- function describeRecording(r) {
99
- const base = r.startedAt || 0;
100
- const secs = Math.round(((r.endedAt || 0) - base || 0) / 1000);
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 events = [];
105
- (r.clicks || []).forEach(c => {
106
- const files = (c.componentChain || []).map(x => x && x.file).filter(Boolean);
107
- events.push({
108
- ts: c.ts,
109
- text:
110
- `点击 <${c.tag}> ${JSON.stringify((c.text || '').slice(0, 40))} @ ${c.selector}` +
111
- (files.length ? ` | 源码 ${files.join(' <- ')}` : ''),
112
- });
113
- });
114
- (r.console || []).forEach(l => {
115
- events.push({ts: l.ts, text: `控制台[${l.level}] ${String(l.text).slice(0, 300)}`});
116
- });
117
- (r.network || []).forEach(n => {
118
- events.push({ts: n.startTs, text: `网络→发起 ${n.method} ${n.url}`});
119
- events.push({ts: n.endTs, text: `网络←完成 ${n.method} ${n.url} → ${n.status} (${n.ms}ms)`});
120
- });
121
- // V8 的 sort 稳定:同一时刻保持插入序(发起在完成之前)。
122
- events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
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 录制】用户录制了一段操作(页面 ${r.pageUrl || ''},约 ${secs}s,共 ${events.length} 个事件,时间为相对录制起点的偏移):`,
126
+ `【ui-inspector 录制】用户录制了一段操作(页面 ${meta.pageUrl || ''},约 ${secs}s,共 ${lines.length} 个事件,时间为相对录制起点的偏移):`,
126
127
  ];
127
- events.slice(0, 120).forEach(e => parts.push(` ${rel(e.ts)} ${e.text}`));
128
- if (events.length > 120) {
129
- parts.push(` …(其余 ${events.length - 120} 个事件省略)`);
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(recording));
146
+ sections.push(describeRecording(meta, recEvents));
146
147
  }
147
148
  sections.push(
148
149
  '请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.5-alpha.5",
3
+ "version": "0.1.5-alpha.6",
4
4
  "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
5
  "type": "module",
6
6
  "license": "MIT",