@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.
Files changed (55) hide show
  1. package/README.md +858 -0
  2. package/dist/admin-6UTU2RZ2.js +281 -0
  3. package/dist/admin-MDGF4CET.js +285 -0
  4. package/dist/admin-RPJJ5CAF.js +282 -0
  5. package/dist/browser-GWBH6OJK.js +46 -0
  6. package/dist/browser-I2HJZ7IP.js +48 -0
  7. package/dist/browser-R7B255ML.js +46 -0
  8. package/dist/chunk-2ONMTDLK.js +2050 -0
  9. package/dist/chunk-3RG5ZIWI.js +10 -0
  10. package/dist/chunk-43VX3TYN.js +83 -0
  11. package/dist/chunk-ATFTAKMN.js +267 -0
  12. package/dist/chunk-DESA2KMG.js +77 -0
  13. package/dist/chunk-DTJRVA76.js +206 -0
  14. package/dist/chunk-F3ZWFCJJ.js +2051 -0
  15. package/dist/chunk-FF5WHQHN.js +135 -0
  16. package/dist/chunk-HINTG75P.js +77 -0
  17. package/dist/chunk-KDYXFLAC.js +1503 -0
  18. package/dist/chunk-KTSQU4QT.js +29 -0
  19. package/dist/chunk-L53IDAWK.js +68 -0
  20. package/dist/chunk-M7CMBPCA.js +100 -0
  21. package/dist/chunk-NFGO7J2I.js +29 -0
  22. package/dist/chunk-OLB6UJ25.js +438 -0
  23. package/dist/chunk-OPRXFZVE.js +52 -0
  24. package/dist/chunk-RS6YYWTK.js +685 -0
  25. package/dist/chunk-VEDJ5XSQ.js +196 -0
  26. package/dist/chunk-VEKPHQBR.js +47 -0
  27. package/dist/chunk-VUJDJCIN.js +437 -0
  28. package/dist/chunk-YEN2ODUI.js +14 -0
  29. package/dist/chunk-ZZ2TFWIV.js +1382 -0
  30. package/dist/cli.js +11012 -0
  31. package/dist/convert-4DUWZIKH.js +205 -0
  32. package/dist/convert-EKQVHKB4.js +11 -0
  33. package/dist/daemon-client-3IJD6X4B.js +59 -0
  34. package/dist/daemon-client-GX2UYIW4.js +241 -0
  35. package/dist/daemon-client-XWSSQBEA.js +58 -0
  36. package/dist/daemon-main.js +9910 -0
  37. package/dist/extract-EGRXZSSK.js +67 -0
  38. package/dist/extract-JUOQQX4V.js +11 -0
  39. package/dist/filter-OLAE26HN.js +51 -0
  40. package/dist/filter-VID2GGZ7.js +9 -0
  41. package/dist/human-interaction-QPHNDD76.js +8 -0
  42. package/dist/index.d.ts +2313 -0
  43. package/dist/index.js +13839 -0
  44. package/dist/marketplace-FCVN5OTZ.js +706 -0
  45. package/dist/marketplace-FPT5YLKB.js +351 -0
  46. package/dist/marketplace-W545W4FR.js +706 -0
  47. package/dist/network-store-2S5HATEV.js +194 -0
  48. package/dist/network-store-BN6QEZ7R.js +196 -0
  49. package/dist/network-store-YAF5OIBH.js +12 -0
  50. package/dist/parse-action-dsl-DRSPBALP.js +72 -0
  51. package/dist/parse-action-dsl-T3DYC33D.js +74 -0
  52. package/dist/proxy-WKGUCH2C.js +7 -0
  53. package/dist/session-recorder-ILSSV2UC.js +6 -0
  54. package/dist/session-recorder-XET3DNML.js +7 -0
  55. 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, '&lt;') + '</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
+ };