comate-ui-inspector 0.1.5-alpha.5 → 0.1.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.
|
@@ -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,79 @@
|
|
|
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', {
|
|
145
|
+
selector: cssPath(el),
|
|
146
|
+
tag: el.nodeName.toLowerCase(),
|
|
147
|
+
type: type || undefined,
|
|
148
|
+
value: val,
|
|
149
|
+
ts: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onSubmit(e) {
|
|
154
|
+
if (!on) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
var el = e.target;
|
|
158
|
+
emit('submit', {selector: el && el.nodeType === 1 ? cssPath(el) : '(form)', ts: Date.now()});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 只记有意义的操作键(Enter / Escape),避免逐字记录输入内容(噪声 + 隐私)。
|
|
162
|
+
function onKeydown(e) {
|
|
163
|
+
if (!on || (e.key !== 'Enter' && e.key !== 'Escape')) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
var el = e.target;
|
|
167
|
+
emit('keydown', {key: e.key, selector: el && el.nodeType === 1 ? cssPath(el) : '', ts: Date.now()});
|
|
168
|
+
}
|
|
169
|
+
|
|
95
170
|
function startConsole() {
|
|
96
171
|
['log', 'info', 'warn', 'error'].forEach(function (level) {
|
|
97
172
|
saved['c_' + level] = console[level];
|
|
@@ -208,6 +283,10 @@
|
|
|
208
283
|
}
|
|
209
284
|
on = {console: !!opts.console, network: !!opts.network};
|
|
210
285
|
document.addEventListener('click', onClick, true);
|
|
286
|
+
document.addEventListener('scroll', onScroll, true);
|
|
287
|
+
document.addEventListener('change', onChange, true);
|
|
288
|
+
document.addEventListener('submit', onSubmit, true);
|
|
289
|
+
document.addEventListener('keydown', onKeydown, true);
|
|
211
290
|
if (on.console) {
|
|
212
291
|
startConsole();
|
|
213
292
|
}
|
|
@@ -221,6 +300,14 @@
|
|
|
221
300
|
return;
|
|
222
301
|
}
|
|
223
302
|
document.removeEventListener('click', onClick, true);
|
|
303
|
+
document.removeEventListener('scroll', onScroll, true);
|
|
304
|
+
document.removeEventListener('change', onChange, true);
|
|
305
|
+
document.removeEventListener('submit', onSubmit, true);
|
|
306
|
+
document.removeEventListener('keydown', onKeydown, true);
|
|
307
|
+
if (scrollTimer) {
|
|
308
|
+
clearTimeout(scrollTimer);
|
|
309
|
+
scrollTimer = null;
|
|
310
|
+
}
|
|
224
311
|
stopConsole();
|
|
225
312
|
stopNetwork();
|
|
226
313
|
on = null;
|
|
@@ -90,11 +90,14 @@
|
|
|
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
|
-
var files = (info.componentChain || [])
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
var files = (info.componentChain || [])
|
|
97
|
+
.map(function (c) {
|
|
98
|
+
return c && c.file;
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
98
101
|
var row = document.createElement('div');
|
|
99
102
|
row.style.cssText =
|
|
100
103
|
'display:flex;align-items:flex-start;gap:6px;padding:6px 0;border-top:1px solid #2a2a2a;';
|
|
@@ -102,7 +105,8 @@
|
|
|
102
105
|
meta.style.cssText = 'flex:1;min-width:0;';
|
|
103
106
|
var title = document.createElement('div');
|
|
104
107
|
title.style.cssText = 'font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
105
|
-
title.textContent =
|
|
108
|
+
title.textContent =
|
|
109
|
+
i + 1 + '. <' + (info.tag || '?') + '>' + (info.text ? ' ' + info.text.slice(0, 20) : '');
|
|
106
110
|
var sub = document.createElement('div');
|
|
107
111
|
sub.style.cssText = 'color:#9aa0a6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
108
112
|
sub.textContent = files.length ? files[0].split('/').pop() : info.selector || '';
|
|
@@ -307,10 +311,26 @@
|
|
|
307
311
|
box.style.cssText =
|
|
308
312
|
'position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #16a34a;' +
|
|
309
313
|
'background:rgba(22,163,74,0.10);border-radius:4px;';
|
|
314
|
+
var badge = document.createElement('div');
|
|
315
|
+
badge.className = 'oreate-sel-badge';
|
|
316
|
+
badge.style.cssText =
|
|
317
|
+
'position:absolute;left:-2px;top:-2px;transform:translateY(-100%);background:#16a34a;color:#fff;' +
|
|
318
|
+
'font:11px/1.5 sans-serif;padding:0 6px;border-radius:4px 4px 4px 0;white-space:nowrap;';
|
|
319
|
+
box.appendChild(badge);
|
|
310
320
|
document.body.appendChild(box);
|
|
311
321
|
return box;
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
// 按当前选中顺序刷新每个高亮框上的数字角标(删除中间项后序号会重排)。
|
|
325
|
+
function renumber() {
|
|
326
|
+
for (var i = 0; i < selected.length; i++) {
|
|
327
|
+
var b = selected[i].box.querySelector('.oreate-sel-badge');
|
|
328
|
+
if (b) {
|
|
329
|
+
b.textContent = String(i + 1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
314
334
|
function placeBox(box, el) {
|
|
315
335
|
var rect = el.getBoundingClientRect();
|
|
316
336
|
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
|
|
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,52 @@ 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({
|
|
109
|
+
ts: it.ts,
|
|
110
|
+
text: `输入/变更 <${it.tag}> @ ${it.selector} = ${JSON.stringify(String(it.value ?? '').slice(0, 60))}`,
|
|
111
|
+
});
|
|
112
|
+
} else if (ev.kind === 'submit') {
|
|
113
|
+
lines.push({ts: it.ts, text: `提交表单 @ ${it.selector}`});
|
|
114
|
+
} else if (ev.kind === 'keydown') {
|
|
115
|
+
lines.push({ts: it.ts, text: `按键 ${it.key}${it.selector ? ` @ ${it.selector}` : ''}`});
|
|
116
|
+
} else if (ev.kind === 'console') {
|
|
117
|
+
lines.push({ts: it.ts, text: `控制台[${it.level}] ${String(it.text).slice(0, 300)}`});
|
|
118
|
+
} else if (ev.kind === 'network') {
|
|
119
|
+
lines.push({ts: it.startTs, text: `网络→发起 ${it.method} ${it.url}`});
|
|
120
|
+
lines.push({ts: it.endTs, text: `网络←完成 ${it.method} ${it.url} → ${it.status} (${it.ms}ms)`});
|
|
121
|
+
} else {
|
|
122
|
+
lines.push({ts: it.ts, text: `${ev.kind} ${JSON.stringify(it).slice(0, 120)}`});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// V8 的 sort 稳定:同一时刻保持插入序(如网络发起在完成之前)。
|
|
126
|
+
lines.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
123
127
|
|
|
124
128
|
const parts = [
|
|
125
|
-
`【ui-inspector 录制】用户录制了一段操作(页面 ${
|
|
129
|
+
`【ui-inspector 录制】用户录制了一段操作(页面 ${meta.pageUrl || ''},约 ${secs}s,共 ${lines.length} 个事件,时间为相对录制起点的偏移):`,
|
|
126
130
|
];
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
parts.push(` …(其余 ${
|
|
131
|
+
lines.slice(0, 160).forEach(e => parts.push(` ${rel(e.ts)} ${e.text}`));
|
|
132
|
+
if (lines.length > 160) {
|
|
133
|
+
parts.push(` …(其余 ${lines.length - 160} 个事件省略)`);
|
|
130
134
|
}
|
|
131
135
|
return parts.join('\n');
|
|
132
136
|
}
|
|
@@ -142,7 +146,7 @@ if (list.length) {
|
|
|
142
146
|
);
|
|
143
147
|
}
|
|
144
148
|
if (hasRec) {
|
|
145
|
-
sections.push(describeRecording(
|
|
149
|
+
sections.push(describeRecording(meta, recEvents));
|
|
146
150
|
}
|
|
147
151
|
sections.push(
|
|
148
152
|
'请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。'
|
package/package.json
CHANGED