@xbrowser/cli 0.14.0
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/README.md +858 -0
- package/dist/admin-6UTU2RZ2.js +281 -0
- package/dist/admin-MDGF4CET.js +285 -0
- package/dist/admin-RPJJ5CAF.js +282 -0
- package/dist/browser-GWBH6OJK.js +46 -0
- package/dist/browser-I2HJZ7IP.js +48 -0
- package/dist/browser-R7B255ML.js +46 -0
- package/dist/chunk-2ONMTDLK.js +2050 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-43VX3TYN.js +83 -0
- package/dist/chunk-ATFTAKMN.js +267 -0
- package/dist/chunk-DESA2KMG.js +77 -0
- package/dist/chunk-DTJRVA76.js +206 -0
- package/dist/chunk-F3ZWFCJJ.js +2051 -0
- package/dist/chunk-FF5WHQHN.js +135 -0
- package/dist/chunk-HINTG75P.js +77 -0
- package/dist/chunk-KDYXFLAC.js +1503 -0
- package/dist/chunk-KTSQU4QT.js +29 -0
- package/dist/chunk-L53IDAWK.js +68 -0
- package/dist/chunk-M7CMBPCA.js +100 -0
- package/dist/chunk-NFGO7J2I.js +29 -0
- package/dist/chunk-OLB6UJ25.js +438 -0
- package/dist/chunk-OPRXFZVE.js +52 -0
- package/dist/chunk-RS6YYWTK.js +685 -0
- package/dist/chunk-VEDJ5XSQ.js +196 -0
- package/dist/chunk-VEKPHQBR.js +47 -0
- package/dist/chunk-VUJDJCIN.js +437 -0
- package/dist/chunk-YEN2ODUI.js +14 -0
- package/dist/chunk-ZZ2TFWIV.js +1382 -0
- package/dist/cli.js +11012 -0
- package/dist/convert-4DUWZIKH.js +205 -0
- package/dist/convert-EKQVHKB4.js +11 -0
- package/dist/daemon-client-3IJD6X4B.js +59 -0
- package/dist/daemon-client-GX2UYIW4.js +241 -0
- package/dist/daemon-client-XWSSQBEA.js +58 -0
- package/dist/daemon-main.js +9910 -0
- package/dist/extract-EGRXZSSK.js +67 -0
- package/dist/extract-JUOQQX4V.js +11 -0
- package/dist/filter-OLAE26HN.js +51 -0
- package/dist/filter-VID2GGZ7.js +9 -0
- package/dist/human-interaction-QPHNDD76.js +8 -0
- package/dist/index.d.ts +2313 -0
- package/dist/index.js +13839 -0
- package/dist/marketplace-FCVN5OTZ.js +706 -0
- package/dist/marketplace-FPT5YLKB.js +351 -0
- package/dist/marketplace-W545W4FR.js +706 -0
- package/dist/network-store-2S5HATEV.js +194 -0
- package/dist/network-store-BN6QEZ7R.js +196 -0
- package/dist/network-store-YAF5OIBH.js +12 -0
- package/dist/parse-action-dsl-DRSPBALP.js +72 -0
- package/dist/parse-action-dsl-T3DYC33D.js +74 -0
- package/dist/proxy-WKGUCH2C.js +7 -0
- package/dist/session-recorder-ILSSV2UC.js +6 -0
- package/dist/session-recorder-XET3DNML.js +7 -0
- package/package.json +111 -0
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
// src/recorder/session-recorder.ts
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var ACTION_SIGNAL_SCRIPT = `
|
|
6
|
+
(function() {
|
|
7
|
+
if (window.__xb_action_signal) return;
|
|
8
|
+
window.__xb_action_signal = true;
|
|
9
|
+
window.__xb_pending_actions = [];
|
|
10
|
+
|
|
11
|
+
// --- Unique short selector generator ---
|
|
12
|
+
function uniqueSelector(el) {
|
|
13
|
+
if (!el || !el.tagName) return null;
|
|
14
|
+
var doc = el.ownerDocument || document;
|
|
15
|
+
|
|
16
|
+
function isUnique(sel) {
|
|
17
|
+
try { return doc.querySelectorAll(sel).length === 1; } catch(e) { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. #id (shortest, globally unique)
|
|
21
|
+
if (el.id) {
|
|
22
|
+
var idSel = '#' + CSS.escape(el.id);
|
|
23
|
+
if (isUnique(idSel)) return idSel;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. [data-testid="..."]
|
|
27
|
+
var testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
28
|
+
if (testId) {
|
|
29
|
+
var sel = '[data-testid="' + testId + '"]';
|
|
30
|
+
if (isUnique(sel)) return sel;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. [name="..."]
|
|
34
|
+
var name = el.getAttribute('name');
|
|
35
|
+
if (name) {
|
|
36
|
+
var sel = el.tagName.toLowerCase() + '[name="' + name + '"]';
|
|
37
|
+
if (isUnique(sel)) return sel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 4. [aria-label="..."]
|
|
41
|
+
var aria = el.getAttribute('aria-label');
|
|
42
|
+
if (aria) {
|
|
43
|
+
var sel = '[aria-label="' + aria.substring(0, 50) + '"]';
|
|
44
|
+
if (isUnique(sel)) return sel;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 5. [placeholder="..."]
|
|
48
|
+
var ph = el.getAttribute('placeholder');
|
|
49
|
+
if (ph) {
|
|
50
|
+
var sel = el.tagName.toLowerCase() + '[placeholder="' + ph.substring(0, 50) + '"]';
|
|
51
|
+
if (isUnique(sel)) return sel;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 6. tag.class \u2014 pick shortest combo that's unique
|
|
55
|
+
var tag = el.tagName.toLowerCase();
|
|
56
|
+
if (typeof el.className === 'string' && el.className.trim()) {
|
|
57
|
+
var classes = el.className.trim().split(/\\s+/).filter(function(c) {
|
|
58
|
+
return c && !/^(ng-|_|css-|sc-|styled-|emotion-)/.test(c);
|
|
59
|
+
});
|
|
60
|
+
// Sort by rarity (less common class first)
|
|
61
|
+
classes.sort(function(a, b) {
|
|
62
|
+
return doc.querySelectorAll('.' + a).length - doc.querySelectorAll('.' + b).length;
|
|
63
|
+
});
|
|
64
|
+
// Try tag + single class
|
|
65
|
+
for (var i = 0; i < classes.length; i++) {
|
|
66
|
+
var sel = tag + '.' + CSS.escape(classes[i]);
|
|
67
|
+
if (isUnique(sel)) return sel;
|
|
68
|
+
}
|
|
69
|
+
// Try tag + two classes
|
|
70
|
+
if (classes.length >= 2) {
|
|
71
|
+
var sel = tag + '.' + CSS.escape(classes[0]) + '.' + CSS.escape(classes[1]);
|
|
72
|
+
if (isUnique(sel)) return sel;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 7. parent > tag (one level up)
|
|
77
|
+
var parent = el.parentElement;
|
|
78
|
+
if (parent) {
|
|
79
|
+
var parentSel = parent.id ? '#' + CSS.escape(parent.id) : parent.tagName.toLowerCase();
|
|
80
|
+
var sel = parentSel + ' > ' + tag;
|
|
81
|
+
if (isUnique(sel)) return sel;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 8. :nth-child fallback (tag:nth-child(n) under parent)
|
|
85
|
+
if (parent) {
|
|
86
|
+
var siblings = Array.from(parent.children);
|
|
87
|
+
var idx = siblings.indexOf(el) + 1;
|
|
88
|
+
var parentSel = parent.id ? '#' + CSS.escape(parent.id) : parent.tagName.toLowerCase();
|
|
89
|
+
var sel = parentSel + ' > ' + tag + ':nth-child(' + idx + ')';
|
|
90
|
+
if (isUnique(sel)) return sel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 9. Last resort: full tag
|
|
94
|
+
return tag;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Element descriptor ---
|
|
98
|
+
function describe(el) {
|
|
99
|
+
if (!el || !el.tagName) return null;
|
|
100
|
+
var tag = el.tagName.toLowerCase();
|
|
101
|
+
var isInputLike = (tag === 'input' || tag === 'textarea' || tag === 'select');
|
|
102
|
+
var displayText = isInputLike
|
|
103
|
+
? (el.value || el.getAttribute('placeholder') || '').trim().substring(0, 40)
|
|
104
|
+
: (el.textContent || '').trim().substring(0, 40);
|
|
105
|
+
if (tag === 'a' && el.getAttribute('href')) displayText = el.textContent.trim().substring(0, 40);
|
|
106
|
+
return {
|
|
107
|
+
tag: tag,
|
|
108
|
+
selector: uniqueSelector(el),
|
|
109
|
+
text: displayText,
|
|
110
|
+
role: el.getAttribute('role') || undefined,
|
|
111
|
+
type: el.getAttribute('type') || undefined,
|
|
112
|
+
placeholder: el.getAttribute('placeholder') || undefined,
|
|
113
|
+
ariaLabel: el.getAttribute('aria-label') || undefined,
|
|
114
|
+
href: el.getAttribute('href') ? el.getAttribute('href').substring(0, 80) : undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isMeaningful(el) {
|
|
119
|
+
if (!el || !el.tagName) return false;
|
|
120
|
+
var tag = el.tagName.toLowerCase();
|
|
121
|
+
if (tag === 'a' || tag === 'button' || tag === 'input' || tag === 'textarea' || tag === 'select') return true;
|
|
122
|
+
if (el.getAttribute('role')) return true;
|
|
123
|
+
if (el.getAttribute('aria-label')) return true;
|
|
124
|
+
var text = (el.textContent || '').trim();
|
|
125
|
+
if (text.length > 0 && text.length <= 80) return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveMeaningful(e) {
|
|
130
|
+
var path = e.composedPath ? e.composedPath() : [e.target];
|
|
131
|
+
for (var i = 0; i < Math.min(path.length, 8); i++) {
|
|
132
|
+
var el = path[i];
|
|
133
|
+
if (isMeaningful(el)) return el;
|
|
134
|
+
}
|
|
135
|
+
return path[0] || e.target;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function actualTarget(e) {
|
|
139
|
+
var path = e.composedPath && e.composedPath();
|
|
140
|
+
return (path && path.length > 0) ? path[0] : e.target;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Input debounce: coalesce rapid keystrokes on same element ---
|
|
144
|
+
var __xb_input_timer = null;
|
|
145
|
+
var __xb_input_pending = null;
|
|
146
|
+
|
|
147
|
+
function flushInputAction() {
|
|
148
|
+
if (__xb_input_pending) {
|
|
149
|
+
window.__xb_pending_actions.push(__xb_input_pending);
|
|
150
|
+
__xb_input_pending = null;
|
|
151
|
+
}
|
|
152
|
+
__xb_input_timer = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pushAction(type, detail) {
|
|
156
|
+
if (type === 'input') {
|
|
157
|
+
if (__xb_input_timer) clearTimeout(__xb_input_timer);
|
|
158
|
+
__xb_input_pending = {
|
|
159
|
+
type: type,
|
|
160
|
+
ts: Date.now(),
|
|
161
|
+
url: location.href,
|
|
162
|
+
title: document.title,
|
|
163
|
+
...detail,
|
|
164
|
+
};
|
|
165
|
+
__xb_input_timer = setTimeout(flushInputAction, 800);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (type === 'click' || type === 'submit' || type === 'keydown') {
|
|
169
|
+
if (__xb_input_timer) { clearTimeout(__xb_input_timer); flushInputAction(); }
|
|
170
|
+
}
|
|
171
|
+
window.__xb_pending_actions.push({
|
|
172
|
+
type: type,
|
|
173
|
+
ts: Date.now(),
|
|
174
|
+
url: location.href,
|
|
175
|
+
title: document.title,
|
|
176
|
+
...detail,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Click context: capture popover/dropdown/menu/state changes after click ---
|
|
181
|
+
var POPOVER_SELECTORS = [
|
|
182
|
+
'[role="menu"]','[role="listbox"]','[role="dialog"]','[role="tooltip"]','[role="popover"]',
|
|
183
|
+
'[role="combobox"]','[role="tree"]','[role="grid"]',
|
|
184
|
+
'.popover','.popup','.dropdown','.menu','.modal','.tooltip','.panel',
|
|
185
|
+
'[class*="popover"]','[class*="popup"]','[class*="dropdown"]','[class*="menu"]','[class*="tooltip"]',
|
|
186
|
+
'[class*="modal"]','[class*="panel"]','[class*="overlay"]','[class*="sheet"]',
|
|
187
|
+
'[data-popup]','[data-dropdown]','[data-menu]','[data-popover]',
|
|
188
|
+
'.semi-dropdown','.semi-popover','.semi-modal','.semi-select-option',
|
|
189
|
+
'.ant-dropdown','.ant-popover','.ant-modal','.ant-select-dropdown',
|
|
190
|
+
'.el-dropdown','.el-popover','.el-dialog','.el-select-dropdown',
|
|
191
|
+
'.t-dropdown','.t-popup','.t-dialog'
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function isNearClick(el, cx, cy, range) {
|
|
195
|
+
try {
|
|
196
|
+
var r = el.getBoundingClientRect();
|
|
197
|
+
if (!r || r.width === 0 || r.height === 0) return false;
|
|
198
|
+
// Element overlaps with or is near the click area
|
|
199
|
+
var margin = range || 300;
|
|
200
|
+
return !(r.left > cx + margin || r.right < cx - margin || r.top > cy + margin || r.bottom < cy - margin);
|
|
201
|
+
} catch(e) { return false; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function captureVisibleContext(cx, cy) {
|
|
205
|
+
var result = { appeared: [], disappeared: [], stateChanges: [] };
|
|
206
|
+
try {
|
|
207
|
+
// 1. Find popover/dropdown/menu elements near the click
|
|
208
|
+
for (var i = 0; i < POPOVER_SELECTORS.length; i++) {
|
|
209
|
+
try {
|
|
210
|
+
var els = document.querySelectorAll(POPOVER_SELECTORS[i]);
|
|
211
|
+
for (var j = 0; j < els.length; j++) {
|
|
212
|
+
var el = els[j];
|
|
213
|
+
if (!isNearClick(el, cx, cy, 500)) continue;
|
|
214
|
+
var rect = el.getBoundingClientRect();
|
|
215
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
216
|
+
var items = [];
|
|
217
|
+
// Capture child items (up to 20)
|
|
218
|
+
var children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
|
|
219
|
+
for (var k = 0; k < Math.min(children.length, 20); k++) {
|
|
220
|
+
var child = children[k];
|
|
221
|
+
var childText = (child.textContent || '').trim().substring(0, 60);
|
|
222
|
+
if (!childText) continue;
|
|
223
|
+
var childInfo = { text: childText };
|
|
224
|
+
if (child.disabled) childInfo.disabled = true;
|
|
225
|
+
if (child.getAttribute('aria-disabled') === 'true') childInfo.disabled = true;
|
|
226
|
+
if (child.tagName) childInfo.tag = child.tagName.toLowerCase();
|
|
227
|
+
if (child.href) childInfo.href = child.href.substring(0, 80);
|
|
228
|
+
items.push(childInfo);
|
|
229
|
+
}
|
|
230
|
+
result.appeared.push({
|
|
231
|
+
tag: el.tagName.toLowerCase(),
|
|
232
|
+
selector: uniqueSelector(el),
|
|
233
|
+
role: el.getAttribute('role'),
|
|
234
|
+
text: (el.textContent || '').trim().substring(0, 100),
|
|
235
|
+
rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
|
|
236
|
+
items: items,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
} catch(e) {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. Find elements that changed aria-expanded or disabled state near click
|
|
243
|
+
var nearbyEls = document.elementsFromPoint ? document.elementsFromPoint(cx, cy) : [];
|
|
244
|
+
// Also check elements in a wider area
|
|
245
|
+
var area = document.querySelector('body');
|
|
246
|
+
if (area) {
|
|
247
|
+
var allInteractive = area.querySelectorAll('[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]');
|
|
248
|
+
for (var i = 0; i < allInteractive.length; i++) {
|
|
249
|
+
var el = allInteractive[i];
|
|
250
|
+
if (!isNearClick(el, cx, cy, 400)) continue;
|
|
251
|
+
var info = { tag: el.tagName.toLowerCase(), text: (el.textContent || '').trim().substring(0, 60) };
|
|
252
|
+
if (el.id) info.id = el.id;
|
|
253
|
+
if (el.getAttribute('aria-expanded')) info.ariaExpanded = el.getAttribute('aria-expanded');
|
|
254
|
+
if (el.disabled) info.disabled = true;
|
|
255
|
+
if (el.getAttribute('aria-disabled') === 'true') info.disabled = true;
|
|
256
|
+
if (el.getAttribute('aria-selected')) info.ariaSelected = el.getAttribute('aria-selected');
|
|
257
|
+
if (el.getAttribute('data-state')) info.dataState = el.getAttribute('data-state');
|
|
258
|
+
result.stateChanges.push(info);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch(e) {}
|
|
262
|
+
// Deduplicate appeared by selector
|
|
263
|
+
var seen = {};
|
|
264
|
+
result.appeared = result.appeared.filter(function(item) {
|
|
265
|
+
if (!item.selector) return true;
|
|
266
|
+
if (seen[item.selector]) return false;
|
|
267
|
+
seen[item.selector] = true;
|
|
268
|
+
return true;
|
|
269
|
+
});
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
document.addEventListener('click', function(e) {
|
|
274
|
+
var cx = e.clientX, cy = e.clientY;
|
|
275
|
+
// Snapshot before (for diff)
|
|
276
|
+
var beforeExpanded = {};
|
|
277
|
+
try {
|
|
278
|
+
var expandedEls = document.querySelectorAll('[aria-expanded]');
|
|
279
|
+
for (var i = 0; i < expandedEls.length; i++) {
|
|
280
|
+
var el = expandedEls[i];
|
|
281
|
+
if (isNearClick(el, cx, cy, 400)) {
|
|
282
|
+
beforeExpanded[el.id || uniqueSelector(el)] = el.getAttribute('aria-expanded');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch(e) {}
|
|
286
|
+
|
|
287
|
+
pushAction('click', { element: describe(resolveMeaningful(e)), x: cx, y: cy });
|
|
288
|
+
|
|
289
|
+
// After 200ms, capture what changed
|
|
290
|
+
setTimeout(function() {
|
|
291
|
+
try {
|
|
292
|
+
var ctx = captureVisibleContext(cx, cy);
|
|
293
|
+
// Check aria-expanded changes
|
|
294
|
+
try {
|
|
295
|
+
var expandedEls = document.querySelectorAll('[aria-expanded]');
|
|
296
|
+
for (var i = 0; i < expandedEls.length; i++) {
|
|
297
|
+
var el = expandedEls[i];
|
|
298
|
+
var key = el.id || uniqueSelector(el);
|
|
299
|
+
var now = el.getAttribute('aria-expanded');
|
|
300
|
+
if (beforeExpanded[key] !== undefined && beforeExpanded[key] !== now) {
|
|
301
|
+
ctx.stateChanges.push({
|
|
302
|
+
tag: el.tagName.toLowerCase(),
|
|
303
|
+
text: (el.textContent || '').trim().substring(0, 60),
|
|
304
|
+
id: el.id || undefined,
|
|
305
|
+
ariaExpanded: now,
|
|
306
|
+
changed: true,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch(e) {}
|
|
311
|
+
if (ctx.appeared.length > 0 || ctx.stateChanges.length > 0) {
|
|
312
|
+
var lastAction = window.__xb_pending_actions[window.__xb_pending_actions.length - 1];
|
|
313
|
+
if (lastAction && lastAction.type === 'click') {
|
|
314
|
+
lastAction.clickContext = ctx;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch(e) {}
|
|
318
|
+
}, 200);
|
|
319
|
+
}, true);
|
|
320
|
+
|
|
321
|
+
document.addEventListener('input', function(e) {
|
|
322
|
+
var target = actualTarget(e);
|
|
323
|
+
pushAction('input', {
|
|
324
|
+
element: describe(target),
|
|
325
|
+
value: (target.value || target.textContent || '').substring(0, 200),
|
|
326
|
+
});
|
|
327
|
+
}, true);
|
|
328
|
+
|
|
329
|
+
document.addEventListener('change', function(e) {
|
|
330
|
+
var target = actualTarget(e);
|
|
331
|
+
var tag = target.tagName && target.tagName.toLowerCase();
|
|
332
|
+
if (tag === 'select') {
|
|
333
|
+
pushAction('change', { element: describe(target), value: (target.value || '').substring(0, 100) });
|
|
334
|
+
}
|
|
335
|
+
}, true);
|
|
336
|
+
|
|
337
|
+
document.addEventListener('keydown', function(e) {
|
|
338
|
+
if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape' || e.key.startsWith('Arrow')) {
|
|
339
|
+
pushAction('keydown', { key: e.key, element: describe(actualTarget(e)) });
|
|
340
|
+
}
|
|
341
|
+
}, true);
|
|
342
|
+
|
|
343
|
+
document.addEventListener('submit', function(e) {
|
|
344
|
+
pushAction('submit', { element: describe(actualTarget(e)) });
|
|
345
|
+
}, true);
|
|
346
|
+
|
|
347
|
+
document.addEventListener('scroll', function() {
|
|
348
|
+
if (!window.__xb_last_scroll || Date.now() - window.__xb_last_scroll > 500) {
|
|
349
|
+
window.__xb_last_scroll = Date.now();
|
|
350
|
+
pushAction('scroll', { scrollX: window.scrollX, scrollY: window.scrollY });
|
|
351
|
+
}
|
|
352
|
+
}, true);
|
|
353
|
+
})();
|
|
354
|
+
`;
|
|
355
|
+
var CHECKPOINT_OVERLAY_SCRIPT = `
|
|
356
|
+
(function() {
|
|
357
|
+
if (window.__xb_checkpoint_overlay) return;
|
|
358
|
+
window.__xb_checkpoint_overlay = true;
|
|
359
|
+
|
|
360
|
+
var __xb_overlay = null; // main overlay container
|
|
361
|
+
var __xb_highlight = null; // highlight box around hovered element
|
|
362
|
+
var __xb_preview = null; // content preview panel
|
|
363
|
+
var __xb_hint = null; // top hint bar
|
|
364
|
+
var __xb_active = false; // is marking mode active
|
|
365
|
+
var __xb_hovered_el = null; // currently hovered element
|
|
366
|
+
var __xb_flash_timer = null;
|
|
367
|
+
|
|
368
|
+
// \u2500\u2500 helper: get element content for preview \u2500\u2500
|
|
369
|
+
function getElementContent(el) {
|
|
370
|
+
var texts = [];
|
|
371
|
+
// Collect child items (li, option, button, menu items)
|
|
372
|
+
var children = el.querySelectorAll('li, option, button, [role="option"], [role="menuitem"], [class*="item"], [class*="option"], a[href]');
|
|
373
|
+
if (children.length > 0) {
|
|
374
|
+
for (var i = 0; i < Math.min(children.length, 20); i++) {
|
|
375
|
+
var t = children[i].textContent.trim();
|
|
376
|
+
if (t && t.length < 100) texts.push(t);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (texts.length > 0) return texts;
|
|
380
|
+
// Fallback: element's own text (truncated)
|
|
381
|
+
var own = el.textContent.trim();
|
|
382
|
+
if (own.length > 0) return [own.substring(0, 300)];
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// \u2500\u2500 helper: get short selector for element \u2500\u2500
|
|
387
|
+
function shortSelector(el) {
|
|
388
|
+
if (!el || !el.tagName) return '';
|
|
389
|
+
if (el.id) return '#' + el.id;
|
|
390
|
+
if (el.getAttribute('data-testid')) return '[data-testid="' + el.getAttribute('data-testid') + '"]';
|
|
391
|
+
if (el.getAttribute('aria-label')) return '[aria-label="' + el.getAttribute('aria-label').substring(0, 30) + '"]';
|
|
392
|
+
if (el.className && typeof el.className === 'string') {
|
|
393
|
+
var cls = el.className.trim().split(/\\s+/)[0];
|
|
394
|
+
if (cls) return el.tagName.toLowerCase() + '.' + cls;
|
|
395
|
+
}
|
|
396
|
+
return el.tagName.toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// \u2500\u2500 helper: tag label \u2500\u2500
|
|
400
|
+
function tagLabel(el) {
|
|
401
|
+
var tag = el.tagName.toLowerCase();
|
|
402
|
+
var type = el.getAttribute('type');
|
|
403
|
+
var role = el.getAttribute('role');
|
|
404
|
+
var placeholder = el.getAttribute('placeholder');
|
|
405
|
+
var text = (el.textContent || '').trim().substring(0, 40);
|
|
406
|
+
var parts = [tag];
|
|
407
|
+
if (type) parts.push('type=' + type);
|
|
408
|
+
if (role) parts.push('role=' + role);
|
|
409
|
+
if (placeholder) parts.push('"' + placeholder.substring(0, 20) + '"');
|
|
410
|
+
if (text && parts.length < 3) parts.push('"' + text + '"');
|
|
411
|
+
return parts.join(' ');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// \u2500\u2500 create overlay elements \u2500\u2500
|
|
415
|
+
function createOverlay() {
|
|
416
|
+
__xb_overlay = document.createElement('div');
|
|
417
|
+
__xb_overlay.id = '__xb_mark_overlay';
|
|
418
|
+
__xb_overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:999999;';
|
|
419
|
+
|
|
420
|
+
// Highlight box
|
|
421
|
+
__xb_highlight = document.createElement('div');
|
|
422
|
+
__xb_highlight.style.cssText = 'position:fixed;border:3px solid #3b82f6;border-radius:4px;pointer-events:none;transition:all 0.1s ease;display:none;box-shadow:0 0 12px rgba(59,130,246,0.5);';
|
|
423
|
+
__xb_overlay.appendChild(__xb_highlight);
|
|
424
|
+
|
|
425
|
+
// Content preview panel (shows on hover)
|
|
426
|
+
__xb_preview = document.createElement('div');
|
|
427
|
+
__xb_preview.style.cssText = 'position:fixed;right:16px;top:50px;width:340px;max-height:60vh;overflow-y:auto;background:rgba(15,15,15,0.95);color:#e5e5e5;font:12px/1.5 system-ui,sans-serif;padding:12px;border-radius:8px;pointer-events:none;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:none;';
|
|
428
|
+
__xb_overlay.appendChild(__xb_preview);
|
|
429
|
+
|
|
430
|
+
// Top hint bar
|
|
431
|
+
__xb_hint = document.createElement('div');
|
|
432
|
+
__xb_hint.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.88);color:white;font:13px/1.4 system-ui,sans-serif;padding:8px 16px;border-radius:8px;pointer-events:none;white-space:nowrap;box-shadow:0 4px 16px rgba(0,0,0,0.3);';
|
|
433
|
+
__xb_hint.textContent = '\\u00b7 \\u00b7 \\u00b7';
|
|
434
|
+
__xb_overlay.appendChild(__xb_hint);
|
|
435
|
+
|
|
436
|
+
document.body.appendChild(__xb_overlay);
|
|
437
|
+
document.body.style.cursor = 'crosshair';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// \u2500\u2500 destroy overlay \u2500\u2500
|
|
441
|
+
function destroyOverlay() {
|
|
442
|
+
if (__xb_overlay && __xb_overlay.parentNode) {
|
|
443
|
+
__xb_overlay.parentNode.removeChild(__xb_overlay);
|
|
444
|
+
}
|
|
445
|
+
__xb_overlay = null;
|
|
446
|
+
__xb_highlight = null;
|
|
447
|
+
__xb_preview = null;
|
|
448
|
+
__xb_hint = null;
|
|
449
|
+
document.body.style.cursor = '';
|
|
450
|
+
__xb_hovered_el = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// \u2500\u2500 update highlight on mouse move \u2500\u2500
|
|
454
|
+
function updateHighlight(e) {
|
|
455
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
456
|
+
// Skip overlay elements
|
|
457
|
+
if (!el || (el.id && el.id.indexOf('__xb_') === 0) || (__xb_overlay && __xb_overlay.contains(el))) {
|
|
458
|
+
__xb_highlight.style.display = 'none';
|
|
459
|
+
__xb_preview.style.display = 'none';
|
|
460
|
+
__xb_hovered_el = null;
|
|
461
|
+
__xb_hint.textContent = '\\u5c06 \\u9f20\\u6807 \\u79fb\\u5230\\u60f3\\u6807\\u8bb0\\u7684\\u5143\\u7d20\\u4e0a';
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
__xb_hovered_el = el;
|
|
465
|
+
var rect = el.getBoundingClientRect();
|
|
466
|
+
|
|
467
|
+
// Update highlight box
|
|
468
|
+
__xb_highlight.style.display = 'block';
|
|
469
|
+
__xb_highlight.style.left = (rect.left - 3) + 'px';
|
|
470
|
+
__xb_highlight.style.top = (rect.top - 3) + 'px';
|
|
471
|
+
__xb_highlight.style.width = (rect.width + 6) + 'px';
|
|
472
|
+
__xb_highlight.style.height = (rect.height + 6) + 'px';
|
|
473
|
+
|
|
474
|
+
// Build content preview
|
|
475
|
+
var content = getElementContent(el);
|
|
476
|
+
var sel = shortSelector(el);
|
|
477
|
+
var tag = tagLabel(el);
|
|
478
|
+
var html = '<div style="color:#888;margin-bottom:6px;font-size:11px;">' + sel + '</div>';
|
|
479
|
+
html += '<div style="color:#fff;font-weight:bold;margin-bottom:8px;">' + tag + '</div>';
|
|
480
|
+
if (content.length > 0) {
|
|
481
|
+
html += '<div style="border-top:1px solid #333;padding-top:8px;">';
|
|
482
|
+
for (var i = 0; i < Math.min(content.length, 15); i++) {
|
|
483
|
+
html += '<div style="padding:2px 0;color:#ccc;">\\u2022 ' + content[i].replace(/</g, '<') + '</div>';
|
|
484
|
+
}
|
|
485
|
+
if (content.length > 15) html += '<div style="color:#666;">... +' + (content.length - 15) + ' more</div>';
|
|
486
|
+
html += '</div>';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
__xb_preview.innerHTML = html;
|
|
490
|
+
__xb_preview.style.display = 'block';
|
|
491
|
+
|
|
492
|
+
// Update hint
|
|
493
|
+
__xb_hint.textContent = '\\u2461 \\u91c7\\u96c6\\u6b64\\u5143\\u7d20 \\u2462 \\u5361\\u70b9\\uff08\\u4eba\\u5de5\\u4ecb\\u5165\\uff09 \\u2502 \\u677e\\u5f00 Option \\u9000\\u51fa';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// \u2500\u2500 flash feedback \u2500\u2500
|
|
497
|
+
function flash(color) {
|
|
498
|
+
if (!__xb_highlight) return;
|
|
499
|
+
__xb_highlight.style.borderColor = color;
|
|
500
|
+
__xb_highlight.style.boxShadow = '0 0 24px ' + color + '80';
|
|
501
|
+
clearTimeout(__xb_flash_timer);
|
|
502
|
+
__xb_flash_timer = setTimeout(function() {
|
|
503
|
+
if (__xb_highlight) {
|
|
504
|
+
__xb_highlight.style.borderColor = '#3b82f6';
|
|
505
|
+
__xb_highlight.style.boxShadow = '0 0 12px rgba(59,130,246,0.5)';
|
|
506
|
+
}
|
|
507
|
+
}, 600);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// \u2500\u2500 push checkpoint to pending actions \u2500\u2500
|
|
511
|
+
function pushCheckpoint(mode) {
|
|
512
|
+
if (!__xb_hovered_el) return;
|
|
513
|
+
var el = __xb_hovered_el;
|
|
514
|
+
var content = getElementContent(el);
|
|
515
|
+
var sel = shortSelector(el);
|
|
516
|
+
var rect = el.getBoundingClientRect();
|
|
517
|
+
|
|
518
|
+
window.__xb_pending_actions = window.__xb_pending_actions || [];
|
|
519
|
+
window.__xb_pending_actions.push({
|
|
520
|
+
type: 'checkpoint',
|
|
521
|
+
ts: Date.now(),
|
|
522
|
+
url: location.href,
|
|
523
|
+
title: document.title,
|
|
524
|
+
checkpointType: mode === 'collect' ? 'collect' : 'blocker',
|
|
525
|
+
hint: mode === 'collect' ? '\\u91c7\\u96c6: ' + tagLabel(el) : '\\u5361\\u70b9: \\u9700\\u8981\\u4eba\\u5de5\\u4ecb\\u5165',
|
|
526
|
+
selector: sel,
|
|
527
|
+
source: 'manual',
|
|
528
|
+
category: mode,
|
|
529
|
+
content: content.join(' | '),
|
|
530
|
+
elementTag: el.tagName.toLowerCase(),
|
|
531
|
+
elementText: (el.textContent || '').trim().substring(0, 200),
|
|
532
|
+
rect: { x: Math.round(rect.left), y: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height) },
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// \u2500\u2500 event listeners \u2500\u2500
|
|
537
|
+
document.addEventListener('keydown', function(e) {
|
|
538
|
+
// Option/Alt pressed \u2192 enter marking mode
|
|
539
|
+
if (e.key === 'Alt' && !e.repeat && !__xb_active) {
|
|
540
|
+
__xb_active = true;
|
|
541
|
+
createOverlay();
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Only process keys while marking mode is active
|
|
547
|
+
if (!__xb_active) return;
|
|
548
|
+
|
|
549
|
+
// Press 1 \u2192 collect (capture element)
|
|
550
|
+
if (e.code === 'Digit1' || e.key === '1') {
|
|
551
|
+
pushCheckpoint('collect');
|
|
552
|
+
flash('#22c55e'); // green
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
e.stopPropagation();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Press 2 \u2192 blocker (human intervention needed)
|
|
559
|
+
if (e.code === 'Digit2' || e.key === '2') {
|
|
560
|
+
pushCheckpoint('blocker');
|
|
561
|
+
flash('#ef4444'); // red
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
e.stopPropagation();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}, true);
|
|
567
|
+
|
|
568
|
+
document.addEventListener('keyup', function(e) {
|
|
569
|
+
if (e.key === 'Alt') {
|
|
570
|
+
__xb_active = false;
|
|
571
|
+
destroyOverlay();
|
|
572
|
+
}
|
|
573
|
+
}, true);
|
|
574
|
+
|
|
575
|
+
// Track mouse move (only when active)
|
|
576
|
+
document.addEventListener('mousemove', function(e) {
|
|
577
|
+
if (!__xb_active) return;
|
|
578
|
+
updateHighlight(e);
|
|
579
|
+
}, true);
|
|
580
|
+
|
|
581
|
+
// Prevent click while in marking mode
|
|
582
|
+
document.addEventListener('click', function(e) {
|
|
583
|
+
if (__xb_active) {
|
|
584
|
+
e.preventDefault();
|
|
585
|
+
e.stopPropagation();
|
|
586
|
+
}
|
|
587
|
+
}, true);
|
|
588
|
+
})();
|
|
589
|
+
`;
|
|
590
|
+
var SessionRecorder = class _SessionRecorder {
|
|
591
|
+
context;
|
|
592
|
+
page;
|
|
593
|
+
sessionName;
|
|
594
|
+
startUrl = "";
|
|
595
|
+
startedAt = 0;
|
|
596
|
+
actions = [];
|
|
597
|
+
network = [];
|
|
598
|
+
contextChanges = [];
|
|
599
|
+
checkpoints = [];
|
|
600
|
+
actionCounter = 0;
|
|
601
|
+
networkCounter = 0;
|
|
602
|
+
contextCounter = 0;
|
|
603
|
+
checkpointCounter = 0;
|
|
604
|
+
pollTimer = null;
|
|
605
|
+
flushTimer = null;
|
|
606
|
+
lastActionTs = 0;
|
|
607
|
+
activePages = /* @__PURE__ */ new Set();
|
|
608
|
+
_isRecording = false;
|
|
609
|
+
constructor(context, page, sessionName) {
|
|
610
|
+
this.context = context;
|
|
611
|
+
this.page = page;
|
|
612
|
+
this.sessionName = sessionName;
|
|
613
|
+
}
|
|
614
|
+
get isRecording() {
|
|
615
|
+
return this._isRecording;
|
|
616
|
+
}
|
|
617
|
+
get actionCount() {
|
|
618
|
+
return this.actions.length;
|
|
619
|
+
}
|
|
620
|
+
get networkCount() {
|
|
621
|
+
return this.network.length;
|
|
622
|
+
}
|
|
623
|
+
getLiveData() {
|
|
624
|
+
return this.buildData();
|
|
625
|
+
}
|
|
626
|
+
addManualCheckpoint(type, hint, selector) {
|
|
627
|
+
this.checkpointCounter++;
|
|
628
|
+
const cp = {
|
|
629
|
+
id: this.checkpointCounter,
|
|
630
|
+
type,
|
|
631
|
+
timestamp: Date.now(),
|
|
632
|
+
url: this.page.url(),
|
|
633
|
+
pageTitle: "",
|
|
634
|
+
hint,
|
|
635
|
+
selector,
|
|
636
|
+
source: "manual"
|
|
637
|
+
};
|
|
638
|
+
this.checkpoints.push(cp);
|
|
639
|
+
return cp;
|
|
640
|
+
}
|
|
641
|
+
/** Directory for this session's recordings. */
|
|
642
|
+
get recordingsDir() {
|
|
643
|
+
return _SessionRecorder.getRecordingsDir(this.sessionName);
|
|
644
|
+
}
|
|
645
|
+
static getRecordingsDir(sessionName) {
|
|
646
|
+
return join(homedir(), ".xbrowser", "sessions", sessionName, "recordings");
|
|
647
|
+
}
|
|
648
|
+
/** Path to the control file (used by record stop to signal this process). */
|
|
649
|
+
get controlFilePath() {
|
|
650
|
+
return join(this.recordingsDir, ".control.json");
|
|
651
|
+
}
|
|
652
|
+
/** Path to the stop signal file (written by `record stop`). */
|
|
653
|
+
get stopSignalPath() {
|
|
654
|
+
return join(this.recordingsDir, ".stop");
|
|
655
|
+
}
|
|
656
|
+
// ─── Start ──────────────────────────────────────────────────────
|
|
657
|
+
async start(url) {
|
|
658
|
+
if (this._isRecording) throw new Error("Already recording");
|
|
659
|
+
this._isRecording = true;
|
|
660
|
+
this.startedAt = Date.now();
|
|
661
|
+
this.actions = [];
|
|
662
|
+
this.network = [];
|
|
663
|
+
this.contextChanges = [];
|
|
664
|
+
this.checkpoints = [];
|
|
665
|
+
this.checkpointCounter = 0;
|
|
666
|
+
if (url) {
|
|
667
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
668
|
+
this.startUrl = url;
|
|
669
|
+
} else {
|
|
670
|
+
this.startUrl = this.page.url();
|
|
671
|
+
}
|
|
672
|
+
mkdirSync(this.recordingsDir, { recursive: true });
|
|
673
|
+
const control = {
|
|
674
|
+
pid: process.pid,
|
|
675
|
+
startedAt: new Date(this.startedAt).toISOString(),
|
|
676
|
+
startUrl: this.startUrl,
|
|
677
|
+
sessionName: this.sessionName
|
|
678
|
+
};
|
|
679
|
+
writeFileSync(this.controlFilePath, JSON.stringify(control, null, 2), "utf-8");
|
|
680
|
+
await this.injectActionScript(this.page);
|
|
681
|
+
await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
|
|
682
|
+
await this.page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
|
|
683
|
+
this.context.on("request", this.handleRequest);
|
|
684
|
+
this.context.on("response", this.handleResponse);
|
|
685
|
+
this.context.on("page", this.handleNewPage);
|
|
686
|
+
this.page.on("framenavigated", this.handleFrameNavigated);
|
|
687
|
+
this.page.on("dialog", this.handleDialog);
|
|
688
|
+
this.pollTimer = setInterval(() => void this.pollActions(), 200);
|
|
689
|
+
this.flushTimer = setInterval(() => this.flushToDisk(), 5e3);
|
|
690
|
+
}
|
|
691
|
+
// ─── Stop ───────────────────────────────────────────────────────
|
|
692
|
+
async stop() {
|
|
693
|
+
if (!this._isRecording) throw new Error("Not recording");
|
|
694
|
+
this._isRecording = false;
|
|
695
|
+
if (this.pollTimer) {
|
|
696
|
+
clearInterval(this.pollTimer);
|
|
697
|
+
this.pollTimer = null;
|
|
698
|
+
}
|
|
699
|
+
if (this.flushTimer) {
|
|
700
|
+
clearInterval(this.flushTimer);
|
|
701
|
+
this.flushTimer = null;
|
|
702
|
+
}
|
|
703
|
+
this.context.off("request", this.handleRequest);
|
|
704
|
+
this.context.off("response", this.handleResponse);
|
|
705
|
+
this.context.off("page", this.handleNewPage);
|
|
706
|
+
this.page.off("framenavigated", this.handleFrameNavigated);
|
|
707
|
+
this.page.off("dialog", this.handleDialog);
|
|
708
|
+
for (const p of this.activePages) {
|
|
709
|
+
try {
|
|
710
|
+
p.off("framenavigated", this.handleFrameNavigated);
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
await this.flushPendingActions(this.page);
|
|
715
|
+
for (const p of this.activePages) {
|
|
716
|
+
await this.flushPendingActions(p).catch(() => {
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const data = this.buildData();
|
|
720
|
+
const summary = this.buildSummary(data);
|
|
721
|
+
this.writeFinalOutput(data, summary);
|
|
722
|
+
try {
|
|
723
|
+
rmSync(this.controlFilePath);
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
rmSync(this.stopSignalPath);
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
return { data, summary };
|
|
731
|
+
}
|
|
732
|
+
// ─── Cleanup (called on session close) ──────────────────────────
|
|
733
|
+
static cleanup(sessionName) {
|
|
734
|
+
const dir = _SessionRecorder.getRecordingsDir(sessionName);
|
|
735
|
+
if (existsSync(dir)) {
|
|
736
|
+
rmSync(dir, { recursive: true, force: true });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// ─── Wait for stop signal (blocks the process) ──────────────────
|
|
740
|
+
waitForStopSignal() {
|
|
741
|
+
return new Promise((resolve) => {
|
|
742
|
+
const check = () => {
|
|
743
|
+
if (existsSync(this.stopSignalPath)) {
|
|
744
|
+
resolve();
|
|
745
|
+
} else {
|
|
746
|
+
setTimeout(check, 300);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
check();
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
// ─── Static: send stop signal to a running recorder ─────────────
|
|
753
|
+
static async sendStopSignal(sessionName) {
|
|
754
|
+
const dir = _SessionRecorder.getRecordingsDir(sessionName);
|
|
755
|
+
const controlPath = join(dir, ".control.json");
|
|
756
|
+
const stopPath = join(dir, ".stop");
|
|
757
|
+
if (!existsSync(controlPath)) return null;
|
|
758
|
+
const control = JSON.parse(readFileSync(controlPath, "utf-8"));
|
|
759
|
+
let alive = false;
|
|
760
|
+
try {
|
|
761
|
+
process.kill(control.pid, 0);
|
|
762
|
+
alive = true;
|
|
763
|
+
} catch {
|
|
764
|
+
alive = false;
|
|
765
|
+
}
|
|
766
|
+
if (!alive) {
|
|
767
|
+
try {
|
|
768
|
+
rmSync(controlPath);
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
return control;
|
|
772
|
+
}
|
|
773
|
+
mkdirSync(dir, { recursive: true });
|
|
774
|
+
writeFileSync(stopPath, JSON.stringify({ stoppedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
775
|
+
for (let i = 0; i < 50; i++) {
|
|
776
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
777
|
+
if (!existsSync(controlPath)) return control;
|
|
778
|
+
try {
|
|
779
|
+
process.kill(control.pid, 0);
|
|
780
|
+
} catch {
|
|
781
|
+
try {
|
|
782
|
+
rmSync(controlPath);
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
return control;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
rmSync(controlPath);
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
return control;
|
|
793
|
+
}
|
|
794
|
+
// ─── Static: read recording from disk ───────────────────────────
|
|
795
|
+
static readSummary(sessionName) {
|
|
796
|
+
const path = join(_SessionRecorder.getRecordingsDir(sessionName), "summary.json");
|
|
797
|
+
try {
|
|
798
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
static readData(sessionName) {
|
|
804
|
+
const path = join(_SessionRecorder.getRecordingsDir(sessionName), "recording.json");
|
|
805
|
+
try {
|
|
806
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// ==================== Private ====================
|
|
812
|
+
async injectActionScript(page) {
|
|
813
|
+
try {
|
|
814
|
+
await page.evaluate(ACTION_SIGNAL_SCRIPT);
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
await page.evaluate(CHECKPOINT_OVERLAY_SCRIPT);
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// ─── Network capture ────────────────────────────────────────────
|
|
823
|
+
handleRequest = (request) => {
|
|
824
|
+
const resourceType = request.resourceType();
|
|
825
|
+
if (["image", "stylesheet", "font", "manifest", "other"].includes(resourceType)) return;
|
|
826
|
+
const url = request.url();
|
|
827
|
+
if (url.startsWith("data:") || url.startsWith("chrome-extension://") || url.startsWith("blob:")) return;
|
|
828
|
+
this.networkCounter++;
|
|
829
|
+
const entry = {
|
|
830
|
+
id: this.networkCounter,
|
|
831
|
+
timestamp: Date.now(),
|
|
832
|
+
method: request.method(),
|
|
833
|
+
url,
|
|
834
|
+
path: new URL(url).pathname,
|
|
835
|
+
status: 0,
|
|
836
|
+
resourceType,
|
|
837
|
+
contentType: "",
|
|
838
|
+
responseSize: 0
|
|
839
|
+
};
|
|
840
|
+
if (["POST", "PATCH", "PUT"].includes(request.method())) {
|
|
841
|
+
try {
|
|
842
|
+
const postData = request.postData();
|
|
843
|
+
if (postData) {
|
|
844
|
+
try {
|
|
845
|
+
entry.requestBody = JSON.parse(postData);
|
|
846
|
+
} catch {
|
|
847
|
+
entry.requestBody = postData.substring(0, 500);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
this.network.push(entry);
|
|
854
|
+
};
|
|
855
|
+
handleResponse = async (response) => {
|
|
856
|
+
const url = response.url();
|
|
857
|
+
if (url.startsWith("data:") || url.startsWith("chrome-extension://") || url.startsWith("blob:")) return;
|
|
858
|
+
const entry = [...this.network].reverse().find((e) => e.url === url && e.status === 0);
|
|
859
|
+
if (!entry) return;
|
|
860
|
+
entry.status = response.status();
|
|
861
|
+
entry.contentType = response.headers()["content-type"] || "";
|
|
862
|
+
const resourceType = response.request().resourceType();
|
|
863
|
+
const isApi = ["fetch", "xhr"].includes(resourceType) || entry.contentType.includes("json") || entry.contentType.includes("text/");
|
|
864
|
+
if (isApi) {
|
|
865
|
+
try {
|
|
866
|
+
const text = await response.text();
|
|
867
|
+
entry.responseSize = text.length;
|
|
868
|
+
if (text.length <= 20480) {
|
|
869
|
+
try {
|
|
870
|
+
entry.responseBody = JSON.parse(text);
|
|
871
|
+
} catch {
|
|
872
|
+
entry.responseBody = text.substring(0, 500);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
try {
|
|
879
|
+
entry.responseSize = parseInt(response.headers()["content-length"] || "0", 10);
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
// ─── Page tracking ──────────────────────────────────────────────
|
|
885
|
+
handleNewPage = async (page) => {
|
|
886
|
+
this.activePages.add(page);
|
|
887
|
+
this.contextCounter++;
|
|
888
|
+
this.contextChanges.push({
|
|
889
|
+
id: this.contextCounter,
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
type: "new_tab",
|
|
892
|
+
url: page.url(),
|
|
893
|
+
detail: "New tab/popup opened"
|
|
894
|
+
});
|
|
895
|
+
await page.addInitScript(ACTION_SIGNAL_SCRIPT);
|
|
896
|
+
await page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
|
|
897
|
+
await this.injectActionScript(page).catch(() => {
|
|
898
|
+
});
|
|
899
|
+
page.on("framenavigated", this.handleFrameNavigated);
|
|
900
|
+
page.on("close", () => {
|
|
901
|
+
this.activePages.delete(page);
|
|
902
|
+
});
|
|
903
|
+
};
|
|
904
|
+
handleFrameNavigated = (frame) => {
|
|
905
|
+
if (frame !== frame.page().mainFrame()) return;
|
|
906
|
+
this.contextCounter++;
|
|
907
|
+
this.contextChanges.push({
|
|
908
|
+
id: this.contextCounter,
|
|
909
|
+
timestamp: Date.now(),
|
|
910
|
+
type: "navigate",
|
|
911
|
+
url: frame.url()
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
handleDialog = async (dialog) => {
|
|
915
|
+
this.checkpointCounter++;
|
|
916
|
+
this.checkpoints.push({
|
|
917
|
+
id: this.checkpointCounter,
|
|
918
|
+
type: "dialog",
|
|
919
|
+
timestamp: Date.now(),
|
|
920
|
+
url: this.page.url(),
|
|
921
|
+
pageTitle: await this.page.title().catch(() => ""),
|
|
922
|
+
hint: `Dialog [${dialog.type()}]: "${dialog.message()}"`,
|
|
923
|
+
source: "auto",
|
|
924
|
+
context: { dialogType: dialog.type(), message: dialog.message() }
|
|
925
|
+
});
|
|
926
|
+
await dialog.dismiss().catch(() => {
|
|
927
|
+
});
|
|
928
|
+
};
|
|
929
|
+
// ─── Action polling ─────────────────────────────────────────────
|
|
930
|
+
async pollActions() {
|
|
931
|
+
const pages = [this.page, ...this.activePages];
|
|
932
|
+
for (const page of pages) {
|
|
933
|
+
try {
|
|
934
|
+
if (page.isClosed()) continue;
|
|
935
|
+
await this.flushPendingActions(page);
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async flushPendingActions(page) {
|
|
941
|
+
let pending = [];
|
|
942
|
+
try {
|
|
943
|
+
pending = await page.evaluate(() => {
|
|
944
|
+
const w = window;
|
|
945
|
+
const actions = w.__xb_pending_actions || [];
|
|
946
|
+
w.__xb_pending_actions = [];
|
|
947
|
+
return actions;
|
|
948
|
+
});
|
|
949
|
+
} catch {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const raw of pending) {
|
|
953
|
+
if (raw.ts <= this.lastActionTs) continue;
|
|
954
|
+
this.actionCounter++;
|
|
955
|
+
let clickContext;
|
|
956
|
+
if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
|
|
957
|
+
clickContext = await this.captureClickContext(page, raw.x, raw.y);
|
|
958
|
+
}
|
|
959
|
+
this.actions.push({
|
|
960
|
+
id: this.actionCounter,
|
|
961
|
+
type: raw.type,
|
|
962
|
+
timestamp: raw.ts,
|
|
963
|
+
url: raw.url || page.url(),
|
|
964
|
+
pageTitle: raw.title || "",
|
|
965
|
+
element: raw.element,
|
|
966
|
+
value: raw.value,
|
|
967
|
+
key: raw.key,
|
|
968
|
+
x: raw.x,
|
|
969
|
+
y: raw.y,
|
|
970
|
+
scrollX: raw.scrollX,
|
|
971
|
+
scrollY: raw.scrollY,
|
|
972
|
+
clickContext
|
|
973
|
+
});
|
|
974
|
+
this.lastActionTs = raw.ts;
|
|
975
|
+
if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
|
|
976
|
+
const detected = await this.detectCheckpoints(page);
|
|
977
|
+
for (const cp of detected) {
|
|
978
|
+
cp.relatedActionId = this.actionCounter;
|
|
979
|
+
this.checkpoints.push(cp);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* After a click, wait 300ms then scan for popover/dropdown/menu elements
|
|
986
|
+
* near the click position. This runs server-side to avoid race conditions
|
|
987
|
+
* with the client-side poll interval.
|
|
988
|
+
*/
|
|
989
|
+
async captureClickContext(page, cx, cy) {
|
|
990
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
991
|
+
try {
|
|
992
|
+
const ctx = await page.evaluate(([cx2, cy2]) => {
|
|
993
|
+
const POPOVER_SELECTORS = [
|
|
994
|
+
'[role="menu"]',
|
|
995
|
+
'[role="listbox"]',
|
|
996
|
+
'[role="dialog"]',
|
|
997
|
+
'[role="tooltip"]',
|
|
998
|
+
'[role="popover"]',
|
|
999
|
+
'[role="combobox"]',
|
|
1000
|
+
'[role="tree"]',
|
|
1001
|
+
'[role="grid"]',
|
|
1002
|
+
".popover",
|
|
1003
|
+
".popup",
|
|
1004
|
+
".dropdown",
|
|
1005
|
+
".menu",
|
|
1006
|
+
".modal",
|
|
1007
|
+
".tooltip",
|
|
1008
|
+
".panel",
|
|
1009
|
+
'[class*="popover"]',
|
|
1010
|
+
'[class*="popup"]',
|
|
1011
|
+
'[class*="dropdown"]',
|
|
1012
|
+
'[class*="menu"]',
|
|
1013
|
+
'[class*="tooltip"]',
|
|
1014
|
+
'[class*="modal"]',
|
|
1015
|
+
'[class*="panel"]',
|
|
1016
|
+
'[class*="overlay"]',
|
|
1017
|
+
'[class*="sheet"]',
|
|
1018
|
+
"[data-popup]",
|
|
1019
|
+
"[data-dropdown]",
|
|
1020
|
+
"[data-menu]",
|
|
1021
|
+
'[data-popover"]',
|
|
1022
|
+
".semi-dropdown",
|
|
1023
|
+
".semi-popover",
|
|
1024
|
+
".semi-modal",
|
|
1025
|
+
".ant-dropdown",
|
|
1026
|
+
".ant-popover",
|
|
1027
|
+
".ant-modal",
|
|
1028
|
+
".el-dropdown",
|
|
1029
|
+
".el-popover",
|
|
1030
|
+
".el-dialog",
|
|
1031
|
+
".t-dropdown",
|
|
1032
|
+
".t-popup",
|
|
1033
|
+
".t-dialog"
|
|
1034
|
+
];
|
|
1035
|
+
function isNear(el, x, y, range) {
|
|
1036
|
+
try {
|
|
1037
|
+
const r = el.getBoundingClientRect();
|
|
1038
|
+
if (!r || r.width === 0 || r.height === 0) return false;
|
|
1039
|
+
return !(r.left > x + range || r.right < x - range || r.top > y + range || r.bottom < y - range);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const result = {
|
|
1045
|
+
appeared: [],
|
|
1046
|
+
disappeared: [],
|
|
1047
|
+
stateChanges: []
|
|
1048
|
+
};
|
|
1049
|
+
const seenSelectors = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const sel of POPOVER_SELECTORS) {
|
|
1051
|
+
try {
|
|
1052
|
+
const els = document.querySelectorAll(sel);
|
|
1053
|
+
for (let j = 0; j < els.length; j++) {
|
|
1054
|
+
const el = els[j];
|
|
1055
|
+
if (!isNear(el, cx2, cy2, 500)) continue;
|
|
1056
|
+
const rect = el.getBoundingClientRect();
|
|
1057
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1058
|
+
const elSel = el.id ? "#" + el.id : sel;
|
|
1059
|
+
if (seenSelectors.has(elSel + rect.x + rect.y)) continue;
|
|
1060
|
+
seenSelectors.add(elSel + rect.x + rect.y);
|
|
1061
|
+
const items = [];
|
|
1062
|
+
const children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
|
|
1063
|
+
for (let k = 0; k < Math.min(children.length, 20); k++) {
|
|
1064
|
+
const child = children[k];
|
|
1065
|
+
const childText = (child.textContent || "").trim().substring(0, 60);
|
|
1066
|
+
if (!childText) continue;
|
|
1067
|
+
const ci = { text: childText };
|
|
1068
|
+
if (child.disabled || child.getAttribute("aria-disabled") === "true") ci.disabled = true;
|
|
1069
|
+
if (child.tagName) ci.tag = child.tagName.toLowerCase();
|
|
1070
|
+
if (child.href) ci.href = child.href.substring(0, 80);
|
|
1071
|
+
items.push(ci);
|
|
1072
|
+
}
|
|
1073
|
+
result.appeared.push({
|
|
1074
|
+
tag: el.tagName.toLowerCase(),
|
|
1075
|
+
selector: el.id ? "#" + el.id : void 0,
|
|
1076
|
+
role: el.getAttribute("role"),
|
|
1077
|
+
text: (el.textContent || "").trim().substring(0, 100),
|
|
1078
|
+
rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
|
|
1079
|
+
items
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const allInteractive = document.querySelectorAll("[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]");
|
|
1086
|
+
for (let i = 0; i < allInteractive.length; i++) {
|
|
1087
|
+
const el = allInteractive[i];
|
|
1088
|
+
if (!isNear(el, cx2, cy2, 400)) continue;
|
|
1089
|
+
const info = {
|
|
1090
|
+
tag: el.tagName.toLowerCase(),
|
|
1091
|
+
text: (el.textContent || "").trim().substring(0, 60)
|
|
1092
|
+
};
|
|
1093
|
+
if (el.id) info.id = el.id;
|
|
1094
|
+
if (el.getAttribute("aria-expanded")) info.ariaExpanded = el.getAttribute("aria-expanded");
|
|
1095
|
+
if (el.disabled || el.getAttribute("aria-disabled") === "true") info.disabled = true;
|
|
1096
|
+
if (el.getAttribute("aria-selected")) info.ariaSelected = el.getAttribute("aria-selected");
|
|
1097
|
+
if (el.getAttribute("data-state")) info.dataState = el.getAttribute("data-state");
|
|
1098
|
+
result.stateChanges.push(info);
|
|
1099
|
+
}
|
|
1100
|
+
return result;
|
|
1101
|
+
}, [cx, cy]);
|
|
1102
|
+
if (ctx.appeared.length > 0 || ctx.stateChanges.length > 0) {
|
|
1103
|
+
return ctx;
|
|
1104
|
+
}
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
return void 0;
|
|
1108
|
+
}
|
|
1109
|
+
async detectCheckpoints(page) {
|
|
1110
|
+
const CHECKPOINT_RULES = [
|
|
1111
|
+
{ type: "captcha", selectors: ['img[src*="captcha"]', 'img[src*="verify"]', '[class*="captcha"]', '[id*="captcha"]', "#captcha", ".captcha"] },
|
|
1112
|
+
{ type: "slider", selectors: ['[class*="slider"]', '[class*="drag-verify"]', '[class*="slide-verify"]'] },
|
|
1113
|
+
{ type: "login", selectors: ['input[type="password"]'] },
|
|
1114
|
+
{ type: "iframe", selectors: ['iframe[src*="captcha"]', 'iframe[src*="verify"]', 'iframe[src*="recaptcha"]', 'iframe[title*="captcha"]'] }
|
|
1115
|
+
];
|
|
1116
|
+
try {
|
|
1117
|
+
const found = await page.evaluate((rules) => {
|
|
1118
|
+
const results = [];
|
|
1119
|
+
for (const rule of rules) {
|
|
1120
|
+
for (const sel of rule.selectors) {
|
|
1121
|
+
try {
|
|
1122
|
+
const el = document.querySelector(sel);
|
|
1123
|
+
if (el) {
|
|
1124
|
+
const rect = el.getBoundingClientRect();
|
|
1125
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
1126
|
+
results.push({
|
|
1127
|
+
type: rule.type,
|
|
1128
|
+
selector: sel,
|
|
1129
|
+
text: (el.textContent || "").substring(0, 60)
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return results;
|
|
1138
|
+
}, CHECKPOINT_RULES);
|
|
1139
|
+
const entries = [];
|
|
1140
|
+
for (const item of found) {
|
|
1141
|
+
this.checkpointCounter++;
|
|
1142
|
+
const hints = {
|
|
1143
|
+
captcha: "Captcha verification detected",
|
|
1144
|
+
slider: "Slider verification detected",
|
|
1145
|
+
login: "Login form detected (password field)",
|
|
1146
|
+
iframe: "Verification iframe detected"
|
|
1147
|
+
};
|
|
1148
|
+
entries.push({
|
|
1149
|
+
id: this.checkpointCounter,
|
|
1150
|
+
type: item.type,
|
|
1151
|
+
timestamp: Date.now(),
|
|
1152
|
+
url: page.url(),
|
|
1153
|
+
pageTitle: await page.title().catch(() => ""),
|
|
1154
|
+
hint: hints[item.type] || item.type,
|
|
1155
|
+
selector: item.selector,
|
|
1156
|
+
source: "auto",
|
|
1157
|
+
context: { matchedSelector: item.selector, elementText: item.text }
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return entries;
|
|
1161
|
+
} catch {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// ─── Periodic disk flush ────────────────────────────────────────
|
|
1166
|
+
flushToDisk() {
|
|
1167
|
+
const data = this.buildData();
|
|
1168
|
+
try {
|
|
1169
|
+
writeFileSync(
|
|
1170
|
+
join(this.recordingsDir, "recording.json"),
|
|
1171
|
+
JSON.stringify(data, null, 2),
|
|
1172
|
+
"utf-8"
|
|
1173
|
+
);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
writeFinalOutput(data, summary) {
|
|
1178
|
+
mkdirSync(this.recordingsDir, { recursive: true });
|
|
1179
|
+
writeFileSync(join(this.recordingsDir, "recording.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
1180
|
+
writeFileSync(join(this.recordingsDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
1181
|
+
writeFileSync(join(this.recordingsDir, "summary.md"), this.buildMarkdownSummary(data, summary), "utf-8");
|
|
1182
|
+
}
|
|
1183
|
+
buildData() {
|
|
1184
|
+
return {
|
|
1185
|
+
startUrl: this.startUrl,
|
|
1186
|
+
sessionName: this.sessionName,
|
|
1187
|
+
startedAt: new Date(this.startedAt).toISOString(),
|
|
1188
|
+
actions: [...this.actions],
|
|
1189
|
+
network: [...this.network],
|
|
1190
|
+
contextChanges: [...this.contextChanges],
|
|
1191
|
+
checkpoints: [...this.checkpoints]
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
// ─── Summary builder with ref compression + input→network matching ──
|
|
1195
|
+
buildSummary(data) {
|
|
1196
|
+
const POST_WINDOW = 5e3;
|
|
1197
|
+
const MERGE_WINDOW = 2e3;
|
|
1198
|
+
const steps = [];
|
|
1199
|
+
const selectorToRef = /* @__PURE__ */ new Map();
|
|
1200
|
+
const elements = {};
|
|
1201
|
+
let refCounter = 0;
|
|
1202
|
+
function getRef(action) {
|
|
1203
|
+
const sel = action.element?.selector || action.element?.tag || "_none";
|
|
1204
|
+
if (selectorToRef.has(sel)) return selectorToRef.get(sel);
|
|
1205
|
+
refCounter++;
|
|
1206
|
+
const ref = "e" + refCounter;
|
|
1207
|
+
selectorToRef.set(sel, ref);
|
|
1208
|
+
if (action.element) {
|
|
1209
|
+
elements[ref] = {
|
|
1210
|
+
selector: action.element.selector || action.element.tag,
|
|
1211
|
+
tag: action.element.tag,
|
|
1212
|
+
text: action.element.text,
|
|
1213
|
+
role: action.element.role,
|
|
1214
|
+
type: action.element.type,
|
|
1215
|
+
placeholder: action.element.placeholder,
|
|
1216
|
+
ariaLabel: action.element.ariaLabel,
|
|
1217
|
+
href: action.element.href
|
|
1218
|
+
};
|
|
1219
|
+
} else {
|
|
1220
|
+
elements[ref] = { selector: "_none", tag: "_", text: "" };
|
|
1221
|
+
}
|
|
1222
|
+
return ref;
|
|
1223
|
+
}
|
|
1224
|
+
const isNoiseNetwork = (n) => {
|
|
1225
|
+
const url = n.url || "";
|
|
1226
|
+
const path = n.path || "";
|
|
1227
|
+
const rt = n.resourceType || "";
|
|
1228
|
+
if (["image", "stylesheet", "font", "manifest", "other"].includes(rt)) return true;
|
|
1229
|
+
if (n.status === 0) return true;
|
|
1230
|
+
if (/\/ztbox|\/mwb2\.gif|\/hmslog|\/log\.gif|\/tongji|hm\.baidu|clickstream|\/actionlog|\/collect\?|\/track|\/beacon/i.test(url)) return true;
|
|
1231
|
+
if (/\/favicon\.ico|\/robots\.txt/i.test(path)) return true;
|
|
1232
|
+
return false;
|
|
1233
|
+
};
|
|
1234
|
+
const meaningfulNetwork = data.network.filter((n) => !isNoiseNetwork(n));
|
|
1235
|
+
const filtered = data.actions.filter((a) => a.type !== "scroll");
|
|
1236
|
+
const groups = [];
|
|
1237
|
+
let current = null;
|
|
1238
|
+
for (const action of filtered) {
|
|
1239
|
+
const sameElement = current && current.primary.element?.selector && current.primary.element.selector === action.element?.selector && action.timestamp - current.primary.timestamp < MERGE_WINDOW;
|
|
1240
|
+
const isInputLike = action.type === "input" || action.type === "keydown" || action.type === "change";
|
|
1241
|
+
if (current && (sameElement || isInputLike && current.actions.some((a) => a.type === "input" || a.type === "click") && action.timestamp - current.primary.timestamp < MERGE_WINDOW)) {
|
|
1242
|
+
current.actions.push(action);
|
|
1243
|
+
if (action.type === "input") current.primary = action;
|
|
1244
|
+
} else {
|
|
1245
|
+
current = { actions: [action], primary: action };
|
|
1246
|
+
groups.push(current);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
for (const group of groups) {
|
|
1250
|
+
const primary = group.primary;
|
|
1251
|
+
const tsStart = Math.min(...group.actions.map((a) => a.timestamp));
|
|
1252
|
+
const tsEnd = Math.max(...group.actions.map((a) => a.timestamp));
|
|
1253
|
+
const inputAction = group.actions.find((a) => a.type === "input");
|
|
1254
|
+
const nearbyNetwork = meaningfulNetwork.filter(
|
|
1255
|
+
(n) => n.timestamp >= tsStart - 500 && n.timestamp <= tsEnd + POST_WINDOW
|
|
1256
|
+
);
|
|
1257
|
+
const nearbyContext = data.contextChanges.filter(
|
|
1258
|
+
(c) => c.timestamp >= tsStart - 500 && c.timestamp <= tsEnd + POST_WINDOW
|
|
1259
|
+
);
|
|
1260
|
+
const matchedInputs = inputAction ? this.matchActionToNetwork(inputAction, nearbyNetwork) : [];
|
|
1261
|
+
const clickMatches = primary.type === "click" && primary.element?.text ? this.matchActionToNetwork(primary, nearbyNetwork) : [];
|
|
1262
|
+
steps.push({
|
|
1263
|
+
step: steps.length + 1,
|
|
1264
|
+
ref: getRef(primary),
|
|
1265
|
+
action: primary,
|
|
1266
|
+
network: nearbyNetwork.map((n) => ({
|
|
1267
|
+
...n,
|
|
1268
|
+
responseBody: n.responseBody && JSON.stringify(n.responseBody).length > 1e3 ? "[truncated, " + JSON.stringify(n.responseBody).length + " bytes]" : n.responseBody
|
|
1269
|
+
})),
|
|
1270
|
+
contextChanges: nearbyContext,
|
|
1271
|
+
matchedInputs: [...matchedInputs, ...clickMatches]
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
startUrl: data.startUrl,
|
|
1276
|
+
recordedAt: new Date(this.startedAt).toISOString(),
|
|
1277
|
+
durationMs: Date.now() - this.startedAt,
|
|
1278
|
+
totalActions: data.actions.length,
|
|
1279
|
+
totalNetworkRequests: meaningfulNetwork.length,
|
|
1280
|
+
steps,
|
|
1281
|
+
elements,
|
|
1282
|
+
checkpoints: data.checkpoints
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
matchActionToNetwork(action, nearbyNetwork) {
|
|
1286
|
+
const matches = [];
|
|
1287
|
+
const searchValue = (action.value || action.element?.text || "").trim();
|
|
1288
|
+
if (!searchValue || searchValue.length < 2) return matches;
|
|
1289
|
+
for (const netEntry of nearbyNetwork) {
|
|
1290
|
+
if (netEntry.url.includes(encodeURIComponent(searchValue)) || netEntry.url.includes(searchValue)) {
|
|
1291
|
+
matches.push({ inputValue: searchValue, networkId: netEntry.id, paramName: "url" });
|
|
1292
|
+
}
|
|
1293
|
+
if (netEntry.requestBody && typeof netEntry.requestBody === "object") {
|
|
1294
|
+
this.searchObjectForValue(
|
|
1295
|
+
netEntry.requestBody,
|
|
1296
|
+
searchValue,
|
|
1297
|
+
netEntry.id,
|
|
1298
|
+
"",
|
|
1299
|
+
matches
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return matches;
|
|
1304
|
+
}
|
|
1305
|
+
searchObjectForValue(obj, targetValue, networkId, prefix, results) {
|
|
1306
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1307
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1308
|
+
if (typeof value === "string" && value.includes(targetValue)) {
|
|
1309
|
+
results.push({ inputValue: targetValue, networkId, paramName: fullKey });
|
|
1310
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1311
|
+
this.searchObjectForValue(value, targetValue, networkId, fullKey, results);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
buildMarkdownSummary(_data, summary) {
|
|
1316
|
+
const lines = [];
|
|
1317
|
+
const durSec = Math.round(summary.durationMs / 1e3);
|
|
1318
|
+
lines.push("# Recording Summary");
|
|
1319
|
+
lines.push("");
|
|
1320
|
+
lines.push(`- **URL**: ${summary.startUrl}`);
|
|
1321
|
+
lines.push(`- **Recorded**: ${summary.recordedAt}`);
|
|
1322
|
+
lines.push(`- **Duration**: ${durSec}s`);
|
|
1323
|
+
lines.push(`- **Steps**: ${summary.totalActions} actions, ${summary.totalNetworkRequests} network requests`);
|
|
1324
|
+
if (summary.checkpoints.length > 0) {
|
|
1325
|
+
const cpTypes = summary.checkpoints.map((c) => c.type);
|
|
1326
|
+
lines.push(`- **Checkpoints**: ${summary.checkpoints.length} (${[...new Set(cpTypes)].join(", ")})`);
|
|
1327
|
+
} else {
|
|
1328
|
+
lines.push("- **Checkpoints**: 0");
|
|
1329
|
+
}
|
|
1330
|
+
const checkpointSteps = /* @__PURE__ */ new Map();
|
|
1331
|
+
for (const cp of summary.checkpoints) {
|
|
1332
|
+
if (cp.relatedActionId != null) {
|
|
1333
|
+
for (const step of summary.steps) {
|
|
1334
|
+
if (step.action.id === cp.relatedActionId) {
|
|
1335
|
+
checkpointSteps.set(step.step, cp);
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
lines.push("");
|
|
1342
|
+
lines.push("## Steps");
|
|
1343
|
+
lines.push("");
|
|
1344
|
+
for (const step of summary.steps) {
|
|
1345
|
+
const a = step.action;
|
|
1346
|
+
const el = a.element;
|
|
1347
|
+
const cp = checkpointSteps.get(step.step);
|
|
1348
|
+
if (cp) {
|
|
1349
|
+
lines.push(`### Step ${step.step}: \u26A0\uFE0F CHECKPOINT \u2014 ${cp.hint}`);
|
|
1350
|
+
lines.push(`- **Type**: ${cp.type} (${cp.source})`);
|
|
1351
|
+
lines.push(`- **Hint**: ${cp.hint}`);
|
|
1352
|
+
if (cp.selector) lines.push(`- **Selector**: \`${cp.selector}\``);
|
|
1353
|
+
lines.push(`- **Action needed**: Human intervention required before continuing`);
|
|
1354
|
+
} else {
|
|
1355
|
+
const title = describeActionTitle(a);
|
|
1356
|
+
lines.push(`### Step ${step.step}: ${title}`);
|
|
1357
|
+
}
|
|
1358
|
+
if (el) {
|
|
1359
|
+
const parts = [`\`${el.selector || el.tag}\``];
|
|
1360
|
+
if (el.text) parts.push(`"${el.text.substring(0, 60)}"`);
|
|
1361
|
+
parts.push(`(${el.tag})`);
|
|
1362
|
+
if (el.type) parts.push(`type=${el.type}`);
|
|
1363
|
+
lines.push(`- **Element**: ${parts.join(" ")}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (a.value != null && a.type === "input") {
|
|
1366
|
+
lines.push(`- **Value**: "${a.value.substring(0, 100)}"`);
|
|
1367
|
+
}
|
|
1368
|
+
if (step.network.length > 0) {
|
|
1369
|
+
const netDescs = step.network.map((n) => {
|
|
1370
|
+
let desc = `${n.method} ${n.path}`;
|
|
1371
|
+
if (n.status) desc += ` \u2192 ${n.status}`;
|
|
1372
|
+
if (n.responseSize > 0) desc += ` (${formatBytes(n.responseSize)})`;
|
|
1373
|
+
return desc;
|
|
1374
|
+
});
|
|
1375
|
+
lines.push(`- **Network**: ${netDescs.join(", ")}`);
|
|
1376
|
+
for (const n of step.network) {
|
|
1377
|
+
if (n.requestBody && typeof n.requestBody === "object") {
|
|
1378
|
+
const bodyStr = JSON.stringify(n.requestBody);
|
|
1379
|
+
if (bodyStr.length <= 300) {
|
|
1380
|
+
lines.push(` - \`${n.method} ${n.path}\` body: \`${bodyStr}\``);
|
|
1381
|
+
} else {
|
|
1382
|
+
lines.push(` - \`${n.method} ${n.path}\` body: \`${bodyStr.substring(0, 300)}...\` (${bodyStr.length} bytes)`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
lines.push("- **Network**: none");
|
|
1388
|
+
}
|
|
1389
|
+
if (step.matchedInputs.length > 0) {
|
|
1390
|
+
for (const m of step.matchedInputs) {
|
|
1391
|
+
lines.push(`- **Input matched**: "${m.inputValue}" \u2192 ${m.paramName} (network #${m.networkId})`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
for (const ctx of step.contextChanges) {
|
|
1395
|
+
if (ctx.type === "navigate") {
|
|
1396
|
+
lines.push(`- **Navigate**: \u2192 ${ctx.url}`);
|
|
1397
|
+
} else if (ctx.type === "new_tab") {
|
|
1398
|
+
lines.push(`- **New tab**: ${ctx.url}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (a.clickContext) {
|
|
1402
|
+
if (a.clickContext.appeared?.length > 0) {
|
|
1403
|
+
for (const popup of a.clickContext.appeared) {
|
|
1404
|
+
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
1405
|
+
lines.push(`- **Popup**: <${popup.tag}${roleStr}> "${(popup.text || "").substring(0, 60)}"`);
|
|
1406
|
+
if (popup.items?.length > 0) {
|
|
1407
|
+
const itemStrs = popup.items.slice(0, 8).map((i) => {
|
|
1408
|
+
const dis = i.disabled ? " [disabled]" : "";
|
|
1409
|
+
return `"${i.text}"${dis}`;
|
|
1410
|
+
});
|
|
1411
|
+
let itemLine = ` - Items: ${itemStrs.join(", ")}`;
|
|
1412
|
+
if (popup.items.length > 8) itemLine += ` ... +${popup.items.length - 8} more`;
|
|
1413
|
+
lines.push(itemLine);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (a.clickContext.stateChanges?.length > 0) {
|
|
1418
|
+
for (const sc of a.clickContext.stateChanges) {
|
|
1419
|
+
const parts = [];
|
|
1420
|
+
if (sc.ariaExpanded !== void 0) parts.push(`expanded=${sc.ariaExpanded}`);
|
|
1421
|
+
if (sc.disabled) parts.push("disabled");
|
|
1422
|
+
if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
|
|
1423
|
+
if (sc.dataState) parts.push(`state=${sc.dataState}`);
|
|
1424
|
+
if (parts.length > 0) {
|
|
1425
|
+
lines.push(`- **State**: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
lines.push("");
|
|
1431
|
+
}
|
|
1432
|
+
const allNetwork = summary.steps.flatMap((s) => s.network);
|
|
1433
|
+
if (allNetwork.length > 0) {
|
|
1434
|
+
lines.push("## Network Timeline");
|
|
1435
|
+
lines.push("");
|
|
1436
|
+
allNetwork.forEach((n, i) => {
|
|
1437
|
+
let line = `${i + 1}. ${n.method} ${n.path}`;
|
|
1438
|
+
if (n.status) line += ` \u2192 ${n.status}`;
|
|
1439
|
+
if (n.requestBody && typeof n.requestBody === "object") {
|
|
1440
|
+
const bodyStr = JSON.stringify(n.requestBody);
|
|
1441
|
+
if (bodyStr.length <= 150) {
|
|
1442
|
+
line += ` ${bodyStr}`;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (n.responseSize > 0) line += ` (${formatBytes(n.responseSize)})`;
|
|
1446
|
+
lines.push(line);
|
|
1447
|
+
});
|
|
1448
|
+
lines.push("");
|
|
1449
|
+
}
|
|
1450
|
+
const orphanCheckpoints = summary.checkpoints.filter(
|
|
1451
|
+
(cp) => cp.relatedActionId == null || !checkpointSteps.has(
|
|
1452
|
+
summary.steps.find((s) => s.action.id === cp.relatedActionId)?.step ?? -1
|
|
1453
|
+
)
|
|
1454
|
+
);
|
|
1455
|
+
if (orphanCheckpoints.length > 0) {
|
|
1456
|
+
lines.push("## Unresolved Checkpoints");
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
for (const cp of orphanCheckpoints) {
|
|
1459
|
+
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
1460
|
+
lines.push(`- ${src} **${cp.type}**: ${cp.hint}`);
|
|
1461
|
+
if (cp.selector) lines.push(` - Selector: \`${cp.selector}\``);
|
|
1462
|
+
}
|
|
1463
|
+
lines.push("");
|
|
1464
|
+
}
|
|
1465
|
+
return lines.join("\n");
|
|
1466
|
+
}
|
|
1467
|
+
static readMarkdownSummary(sessionName) {
|
|
1468
|
+
const path = join(_SessionRecorder.getRecordingsDir(sessionName), "summary.md");
|
|
1469
|
+
try {
|
|
1470
|
+
return readFileSync(path, "utf-8");
|
|
1471
|
+
} catch {
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
function describeActionTitle(a) {
|
|
1477
|
+
const el = a.element;
|
|
1478
|
+
const elText = el?.text ? `"${el.text.substring(0, 40)}"` : "";
|
|
1479
|
+
const elTag = el?.tag ? `<${el.tag}>` : "";
|
|
1480
|
+
switch (a.type) {
|
|
1481
|
+
case "click":
|
|
1482
|
+
return `Click ${elText || elTag} button`.replace(/ +/g, " ").trim();
|
|
1483
|
+
case "input":
|
|
1484
|
+
return `Input "${(a.value || "").substring(0, 50)}" in ${elText || elTag || "field"}`.replace(/ +/g, " ").trim();
|
|
1485
|
+
case "change":
|
|
1486
|
+
return `Change ${elText || elTag} to "${(a.value || "").substring(0, 30)}"`;
|
|
1487
|
+
case "keydown":
|
|
1488
|
+
return `Press ${a.key || "key"} on ${elText || elTag || "element"}`;
|
|
1489
|
+
case "submit":
|
|
1490
|
+
return `Submit ${elText || elTag || "form"}`;
|
|
1491
|
+
default:
|
|
1492
|
+
return `${a.type} ${elText || elTag}`.trim() || a.type;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function formatBytes(bytes) {
|
|
1496
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1497
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1498
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
export {
|
|
1502
|
+
SessionRecorder
|
|
1503
|
+
};
|