@xbrowser/cli 1.9.5 → 1.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browser-6WJHQDPV.js → browser-ALPLJNH2.js} +1 -1
- package/dist/{browser-4KRHALJ3.js → browser-JXIJF4VL.js} +1 -1
- package/dist/{browser-C63PFGPT.js → browser-ZPQP6VW7.js} +1 -1
- package/dist/{chunk-OOJ2H7IL.js → chunk-37SHZSUR.js} +1 -1
- package/dist/{chunk-MSQIPXKE.js → chunk-5JSOGINQ.js} +1 -1
- package/dist/{chunk-Z7WC4P2M.js → chunk-74S7UUI2.js} +1 -1
- package/dist/{chunk-TTZNR3QP.js → chunk-C4O73DQL.js} +165 -50
- package/dist/{chunk-YSCY52UJ.js → chunk-CH5ZQV64.js} +165 -50
- package/dist/{chunk-53LRNYPG.js → chunk-YAUG3NSI.js} +2 -2
- package/dist/cli.js +22 -13
- package/dist/{daemon-client-PZX2KOLQ.js → daemon-client-4HJENICO.js} +2 -2
- package/dist/{daemon-client-TOUDMIY5.js → daemon-client-CMRAWXTN.js} +1 -1
- package/dist/daemon-main.js +12 -8
- package/dist/index.d.ts +24 -3
- package/dist/index.js +21 -12
- package/dist/{session-recorder-UTULJFTE.js → session-recorder-7GXI42FO.js} +1 -1
- package/dist/{session-recorder-KPU4YQAE.js → session-recorder-RFOPX4SF.js} +1 -1
- package/dist/{session-replayer-RWFOWH3F.js → session-replayer-3HEIJBJ2.js} +65 -33
- package/package.json +1 -1
|
@@ -65,16 +65,28 @@ function getSelectorGeneratorScript() {
|
|
|
65
65
|
if (isUnique(root, s)) return { selector: s, strategy: 'id+tag', confidence: 'high' };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
// [data-testid]
|
|
69
|
+
for (var si = 0; si < STABLE_ATTRS.length; si++) {
|
|
70
|
+
var v = el.getAttribute(STABLE_ATTRS[si]);
|
|
71
|
+
if (v) {
|
|
72
|
+
var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
|
|
73
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
|
|
74
|
+
s = tag + s;
|
|
75
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// [data-*] \u2014 other data attributes beyond known testids
|
|
80
|
+
for (var ai = 0; ai < el.attributes.length; ai++) {
|
|
81
|
+
var a = el.attributes[ai];
|
|
82
|
+
if (!a.name.startsWith('data-')) continue;
|
|
83
|
+
if (STABLE_ATTRS.indexOf(a.name) !== -1) continue;
|
|
84
|
+
if (!a.value || a.value.length > 60 || a.value.indexOf('http') === 0) continue;
|
|
85
|
+
var s = '[' + a.name + '="' + esc(a.value) + '"]';
|
|
86
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'data-attr', confidence: 'high' };
|
|
87
|
+
s = tag + s;
|
|
88
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'data-attr+tag', confidence: 'high' };
|
|
89
|
+
}
|
|
78
90
|
|
|
79
91
|
// [name]
|
|
80
92
|
var name = el.getAttribute('name');
|
|
@@ -83,14 +95,35 @@ function getSelectorGeneratorScript() {
|
|
|
83
95
|
if (isUnique(root, s)) return { selector: s, strategy: 'name', confidence: 'high' };
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
// [aria-label]
|
|
99
|
+
var aria = el.getAttribute('aria-label');
|
|
100
|
+
if (aria) {
|
|
101
|
+
var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
|
|
102
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
|
|
103
|
+
s = tag + s;
|
|
104
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// [role][name] combo
|
|
108
|
+
var role = el.getAttribute('role');
|
|
109
|
+
if (role) {
|
|
110
|
+
var name2 = el.getAttribute('name');
|
|
111
|
+
if (name2) {
|
|
112
|
+
var s = '[role="' + role + '"][name="' + esc(name2) + '"]';
|
|
113
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'role+name', confidence: 'high' };
|
|
114
|
+
}
|
|
115
|
+
// [role][aria-label] combo
|
|
116
|
+
if (aria) {
|
|
117
|
+
var s = '[role="' + role + '"][aria-label="' + esc(aria.substring(0, 60)) + '"]';
|
|
118
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'role+aria-label', confidence: 'high' };
|
|
119
|
+
}
|
|
120
|
+
// [role] alone
|
|
121
|
+
var text2 = (el.textContent || '').trim();
|
|
122
|
+
if (text2) {
|
|
123
|
+
var s = '[role="' + role + '"]';
|
|
124
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'role', confidence: 'high' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
94
127
|
|
|
95
128
|
// [placeholder]
|
|
96
129
|
var ph = el.getAttribute('placeholder');
|
|
@@ -113,11 +146,12 @@ function getSelectorGeneratorScript() {
|
|
|
113
146
|
if (isUnique(root, s)) return { selector: s, strategy: 'title', confidence: 'high' };
|
|
114
147
|
}
|
|
115
148
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
149
|
+
// Unique attribute (skip URL-like, long, and common attrs handled above)
|
|
150
|
+
// Note: non-testid data-* attrs are already tried in [data-*] section above
|
|
151
|
+
var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
|
|
152
|
+
for (var ai = 0; ai < el.attributes.length; ai++) {
|
|
153
|
+
var a = el.attributes[ai];
|
|
154
|
+
if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
|
|
121
155
|
if (a.value && a.value.length > 2 && a.value.length <= 60) {
|
|
122
156
|
var s = tag + '[' + a.name + '="' + esc(a.value) + '"]';
|
|
123
157
|
if (isUnique(root, s)) return { selector: s, strategy: 'attribute', confidence: 'medium' };
|
|
@@ -154,10 +188,16 @@ function getSelectorGeneratorScript() {
|
|
|
154
188
|
var s = tag + '.' + esc(cls[i]) + '.' + esc(cls[j]);
|
|
155
189
|
if (isUnique(root, s)) return { selector: s, strategy: 'tag+class-combo', confidence: 'medium' };
|
|
156
190
|
}
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// tag[aria-label] fallback \u2014 aria-label exists but wasn't globally unique
|
|
195
|
+
if (aria) {
|
|
196
|
+
var s = tag + '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
|
|
197
|
+
if (isUnique(root, s)) return { selector: s, strategy: 'tag+aria-label', confidence: 'medium' };
|
|
198
|
+
}
|
|
159
199
|
|
|
160
|
-
|
|
200
|
+
// Parent scope
|
|
161
201
|
var parent = el.parentElement;
|
|
162
202
|
if (parent && parent !== root) {
|
|
163
203
|
if (parent.id) {
|
|
@@ -1224,12 +1264,28 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1224
1264
|
activePages = /* @__PURE__ */ new Set();
|
|
1225
1265
|
lastKnownUrl = "";
|
|
1226
1266
|
// Track URL to detect real navigation changes
|
|
1227
|
-
/** Dedup
|
|
1228
|
-
cdpActionDedup
|
|
1267
|
+
/** Dedup map: key = normalizedType|tag|value → expiration timestamp.
|
|
1268
|
+
* Replaces old single-entry cdpActionDedup for bidirectional dedup. */
|
|
1269
|
+
dedupMap = /* @__PURE__ */ new Map();
|
|
1270
|
+
dedupActionCount = 0;
|
|
1271
|
+
// counter for periodic cleanup
|
|
1229
1272
|
/** Network dedup: last request key for short-window dedup */
|
|
1230
1273
|
_lastRequestKey = "";
|
|
1231
1274
|
_lastRequestTs = 0;
|
|
1232
1275
|
_isRecording = false;
|
|
1276
|
+
streamMode = "clean";
|
|
1277
|
+
/** Ambient action types: filtered out in clean mode */
|
|
1278
|
+
static AMBIENT_TYPES = /* @__PURE__ */ new Set([
|
|
1279
|
+
"hover",
|
|
1280
|
+
"focus",
|
|
1281
|
+
"scroll",
|
|
1282
|
+
"visibility",
|
|
1283
|
+
"resize",
|
|
1284
|
+
"clipboard",
|
|
1285
|
+
"touch",
|
|
1286
|
+
"contextmenu",
|
|
1287
|
+
"drag"
|
|
1288
|
+
]);
|
|
1233
1289
|
constructor(context, page, sessionName) {
|
|
1234
1290
|
this.context = context;
|
|
1235
1291
|
this.page = page;
|
|
@@ -1243,13 +1299,10 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1243
1299
|
}
|
|
1244
1300
|
/** Record an action triggered by a CDP command (e.g. xbrowser fill/click/goto) */
|
|
1245
1301
|
async recordCommandAction(action) {
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const valueMatch = !action.value || recent.value === action.value;
|
|
1251
|
-
const selectorMatch = !action.selector || recent.element?.selector && (recent.element.selector === action.selector || recent.element.selector.endsWith(" " + action.selector) || action.selector.endsWith(" " + recent.element.selector));
|
|
1252
|
-
if (typeMatch && valueMatch && selectorMatch) {
|
|
1302
|
+
const dedupFromAction = this.dedupKey(action.type, action.selector, action.element?.tag, action.value);
|
|
1303
|
+
if (dedupFromAction) {
|
|
1304
|
+
const expires = this.dedupMap.get(dedupFromAction);
|
|
1305
|
+
if (expires && Date.now() < expires) {
|
|
1253
1306
|
return;
|
|
1254
1307
|
}
|
|
1255
1308
|
}
|
|
@@ -1271,18 +1324,26 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1271
1324
|
element: actionToPush.element
|
|
1272
1325
|
});
|
|
1273
1326
|
this.lastActionTs = ts;
|
|
1274
|
-
this.
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1327
|
+
this.dedupMap.set(dedupFromAction, Date.now() + 2e3);
|
|
1328
|
+
this.dedupActionCount++;
|
|
1329
|
+
if (this.dedupActionCount % 200 === 0) {
|
|
1330
|
+
const now = Date.now();
|
|
1331
|
+
for (const [k, v] of this.dedupMap) {
|
|
1332
|
+
if (now >= v) this.dedupMap.delete(k);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1280
1335
|
if (action.url && action.url !== "about:blank") {
|
|
1281
1336
|
this.lastKnownUrl = action.url;
|
|
1282
1337
|
} else if (action.type === "goto" && action.value && action.value !== "about:blank") {
|
|
1283
1338
|
this.lastKnownUrl = action.value;
|
|
1284
1339
|
}
|
|
1285
1340
|
}
|
|
1341
|
+
/** Generate dedup key for an action (normalized type + selector/tag + value). */
|
|
1342
|
+
dedupKey(type, selector, tag, value) {
|
|
1343
|
+
const normType = type === "cdp-click" ? "click" : type === "cdp-fill" ? "input" : type === "cdp-eval" ? "eval" : type;
|
|
1344
|
+
const selOrTag = selector || tag || "";
|
|
1345
|
+
return `${normType}|${selOrTag}|${value || ""}`;
|
|
1346
|
+
}
|
|
1286
1347
|
get networkCount() {
|
|
1287
1348
|
return this.network.length;
|
|
1288
1349
|
}
|
|
@@ -1320,7 +1381,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1320
1381
|
return join(this.recordingsDir, ".stop");
|
|
1321
1382
|
}
|
|
1322
1383
|
// ─── Start ──────────────────────────────────────────────────────
|
|
1323
|
-
async start(url) {
|
|
1384
|
+
async start(url, options) {
|
|
1324
1385
|
if (this._isRecording) throw new Error("Already recording");
|
|
1325
1386
|
this._isRecording = true;
|
|
1326
1387
|
this.startedAt = Date.now();
|
|
@@ -1329,6 +1390,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1329
1390
|
this.contextChanges = [];
|
|
1330
1391
|
this.checkpoints = [];
|
|
1331
1392
|
this.checkpointCounter = 0;
|
|
1393
|
+
this.streamMode = options?.stream ?? "clean";
|
|
1332
1394
|
this.lastKnownUrl = this.page.url();
|
|
1333
1395
|
await this.page.addInitScript(getSelectorGeneratorScript());
|
|
1334
1396
|
await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
|
|
@@ -1897,15 +1959,16 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1897
1959
|
}
|
|
1898
1960
|
for (const raw of pending) {
|
|
1899
1961
|
if (raw.ts <= this.lastActionTs) continue;
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
const selectorMatch = !dedup.selector || raw.element?.selector && (raw.element.selector === dedup.selector || raw.element.selector.endsWith(" " + dedup.selector) || dedup.selector.endsWith(" " + raw.element.selector));
|
|
1905
|
-
if (typeMatch && valueMatch && selectorMatch) {
|
|
1962
|
+
const dedupFromSignal = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
|
|
1963
|
+
if (dedupFromSignal) {
|
|
1964
|
+
const expires = this.dedupMap.get(dedupFromSignal);
|
|
1965
|
+
if (expires && Date.now() < expires) {
|
|
1906
1966
|
continue;
|
|
1907
1967
|
}
|
|
1908
1968
|
}
|
|
1969
|
+
if (this.streamMode === "clean" && _SessionRecorder.AMBIENT_TYPES.has(raw.type)) {
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1909
1972
|
this.actionCounter++;
|
|
1910
1973
|
let clickContext;
|
|
1911
1974
|
if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
|
|
@@ -1930,6 +1993,18 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
1930
1993
|
const pushed = this.actions[this.actions.length - 1];
|
|
1931
1994
|
pushed.elementScreenshot = await this.captureElementScreenshot(page, raw);
|
|
1932
1995
|
this.lastActionTs = raw.ts;
|
|
1996
|
+
const dedupKey = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
|
|
1997
|
+
if (dedupKey) {
|
|
1998
|
+
this.dedupMap.set(dedupKey, Date.now() + 2e3);
|
|
1999
|
+
this.dedupActionCount++;
|
|
2000
|
+
}
|
|
2001
|
+
const verifyTypes = /* @__PURE__ */ new Set(["click", "submit", "input", "change", "cdp-click", "cdp-fill"]);
|
|
2002
|
+
if (verifyTypes.has(raw.type)) {
|
|
2003
|
+
const signals = await this.verifyAction(page, pushed).catch(() => []);
|
|
2004
|
+
if (signals.length > 0) {
|
|
2005
|
+
pushed.__signals = signals;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1933
2008
|
if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
|
|
1934
2009
|
const detected = await this.detectCheckpoints(page);
|
|
1935
2010
|
for (const cp of detected) {
|
|
@@ -2112,6 +2187,44 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2112
2187
|
return [];
|
|
2113
2188
|
}
|
|
2114
2189
|
}
|
|
2190
|
+
// ─── Verification: collect success signals after key actions ──────
|
|
2191
|
+
/**
|
|
2192
|
+
* After a key action (click/submit/input), check for success signals:
|
|
2193
|
+
* URL changes, recent successful network responses, dialog appearances.
|
|
2194
|
+
*/
|
|
2195
|
+
async verifyAction(page, action) {
|
|
2196
|
+
const signals = [];
|
|
2197
|
+
try {
|
|
2198
|
+
const currentUrl = page.url();
|
|
2199
|
+
if (currentUrl && currentUrl !== "about:blank" && currentUrl !== action.url) {
|
|
2200
|
+
signals.push({ type: "url_change", value: currentUrl, label: `URL changed to ${currentUrl}` });
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const recentNetwork = this.network.filter(
|
|
2206
|
+
(n) => n.timestamp > action.timestamp - 100 && n.timestamp < action.timestamp + 5e3 && n.status >= 200 && n.status < 400
|
|
2207
|
+
);
|
|
2208
|
+
for (const net of recentNetwork.slice(0, 3)) {
|
|
2209
|
+
signals.push({
|
|
2210
|
+
type: "network_success",
|
|
2211
|
+
value: `${net.status}`,
|
|
2212
|
+
label: `${net.method} ${net.path} \u2192 ${net.status}`
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
const recentDialog = [...this.checkpoints].reverse().find(
|
|
2219
|
+
(cp) => cp.type === "dialog" && Math.abs(cp.timestamp - action.timestamp) < 3e3
|
|
2220
|
+
);
|
|
2221
|
+
if (recentDialog) {
|
|
2222
|
+
signals.push({ type: "dialog", value: recentDialog.type, label: recentDialog.hint });
|
|
2223
|
+
}
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
2226
|
+
return signals;
|
|
2227
|
+
}
|
|
2115
2228
|
// ─── Periodic disk flush ────────────────────────────────────────
|
|
2116
2229
|
flushToDisk() {
|
|
2117
2230
|
const data = this.buildData();
|
|
@@ -2182,7 +2295,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2182
2295
|
return false;
|
|
2183
2296
|
};
|
|
2184
2297
|
const meaningfulNetwork = data.network.filter((n) => !isNoiseNetwork(n));
|
|
2185
|
-
const filtered = data.actions
|
|
2298
|
+
const filtered = data.actions;
|
|
2186
2299
|
const groups = [];
|
|
2187
2300
|
let current = null;
|
|
2188
2301
|
for (const action of filtered) {
|
|
@@ -2209,6 +2322,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2209
2322
|
);
|
|
2210
2323
|
const matchedInputs = inputAction ? this.matchActionToNetwork(inputAction, nearbyNetwork) : [];
|
|
2211
2324
|
const clickMatches = primary.type === "click" && primary.element?.text ? this.matchActionToNetwork(primary, nearbyNetwork) : [];
|
|
2325
|
+
const signals = primary.__signals || [];
|
|
2212
2326
|
steps.push({
|
|
2213
2327
|
step: steps.length + 1,
|
|
2214
2328
|
ref: getRef(primary),
|
|
@@ -2218,7 +2332,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2218
2332
|
responseBody: n.responseBody && JSON.stringify(n.responseBody).length > 1e3 ? "[truncated, " + JSON.stringify(n.responseBody).length + " bytes]" : n.responseBody
|
|
2219
2333
|
})),
|
|
2220
2334
|
contextChanges: nearbyContext,
|
|
2221
|
-
matchedInputs: [...matchedInputs, ...clickMatches]
|
|
2335
|
+
matchedInputs: [...matchedInputs, ...clickMatches],
|
|
2336
|
+
signals
|
|
2222
2337
|
});
|
|
2223
2338
|
}
|
|
2224
2339
|
return {
|
|
@@ -300,8 +300,8 @@ async function forwardNetworkExport(sessionName, id, lang) {
|
|
|
300
300
|
async function forwardNetworkInspect(sessionName, id) {
|
|
301
301
|
return rpcCall("network:inspect", { session: sessionName, id }, 1e4);
|
|
302
302
|
}
|
|
303
|
-
async function forwardRecordStart(session, url, cdpEndpoint) {
|
|
304
|
-
return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
|
|
303
|
+
async function forwardRecordStart(session, url, cdpEndpoint, stream) {
|
|
304
|
+
return rpcCall("record:start", { session, url, cdpEndpoint, stream }, 15e3);
|
|
305
305
|
}
|
|
306
306
|
async function forwardRecordStop(session, output) {
|
|
307
307
|
const params = { session };
|
package/dist/cli.js
CHANGED
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
startDaemonProcess,
|
|
28
28
|
stopDaemonProcess,
|
|
29
29
|
version
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-YAUG3NSI.js";
|
|
31
31
|
import {
|
|
32
32
|
SessionRecorder
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-CH5ZQV64.js";
|
|
34
34
|
import {
|
|
35
35
|
addKnownIssue,
|
|
36
36
|
getKnowledgePath,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
resolveLaunchOpts,
|
|
55
55
|
saveSessionDiskMeta,
|
|
56
56
|
setActivePage
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-74S7UUI2.js";
|
|
58
58
|
import "./chunk-TNEN6VQ2.js";
|
|
59
59
|
import {
|
|
60
60
|
errMsg
|
|
@@ -7058,7 +7058,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7058
7058
|
}
|
|
7059
7059
|
let targetPageOverride = null;
|
|
7060
7060
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
7061
|
-
const { findTargetPage } = await import("./browser-
|
|
7061
|
+
const { findTargetPage } = await import("./browser-JXIJF4VL.js");
|
|
7062
7062
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
7063
7063
|
if (!targetPageOverride) {
|
|
7064
7064
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -7075,7 +7075,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7075
7075
|
params = result.data;
|
|
7076
7076
|
}
|
|
7077
7077
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
7078
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7078
|
+
const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
|
|
7079
7079
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
7080
7080
|
if (result) return result;
|
|
7081
7081
|
}
|
|
@@ -8487,7 +8487,7 @@ var PluginInstaller = class {
|
|
|
8487
8487
|
import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
|
|
8488
8488
|
var formatter = new OutputFormatter();
|
|
8489
8489
|
function outputResult(result, mode = "text") {
|
|
8490
|
-
if (typeof result === "object" && result !== null) {
|
|
8490
|
+
if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
|
|
8491
8491
|
const r = result;
|
|
8492
8492
|
if (r.success === false) {
|
|
8493
8493
|
outputError(r.message ? String(r.message) : "Unknown error");
|
|
@@ -10496,7 +10496,7 @@ async function handlePlugin(args, options, mode) {
|
|
|
10496
10496
|
} catch {
|
|
10497
10497
|
}
|
|
10498
10498
|
try {
|
|
10499
|
-
const { daemonPing } = await import("./daemon-client-
|
|
10499
|
+
const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
|
|
10500
10500
|
if (await daemonPing()) {
|
|
10501
10501
|
await fetch("http://localhost:9224/rpc", {
|
|
10502
10502
|
method: "POST",
|
|
@@ -10654,7 +10654,8 @@ async function handleRecord(args, options, mode) {
|
|
|
10654
10654
|
const url = options.url;
|
|
10655
10655
|
const sessionName = options.session || "default";
|
|
10656
10656
|
const cdpEndpoint = options.cdp;
|
|
10657
|
-
const
|
|
10657
|
+
const stream = options.stream || "clean";
|
|
10658
|
+
const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
|
|
10658
10659
|
if (!result.ok) {
|
|
10659
10660
|
outputError(String(result.error || "Failed to start recording"));
|
|
10660
10661
|
return;
|
|
@@ -10949,7 +10950,15 @@ async function handleReplay(args, options, mode) {
|
|
|
10949
10950
|
const absPath = await import("path").then((p) => p.resolve(filePath));
|
|
10950
10951
|
const result = await forwardReplay(absPath, sessionName, slowMo);
|
|
10951
10952
|
if (!result.ok) {
|
|
10952
|
-
|
|
10953
|
+
if (mode === "json" || mode === "yaml") {
|
|
10954
|
+
outputResult({
|
|
10955
|
+
...result,
|
|
10956
|
+
success: false,
|
|
10957
|
+
message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
|
|
10958
|
+
}, mode);
|
|
10959
|
+
} else {
|
|
10960
|
+
outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
|
|
10961
|
+
}
|
|
10953
10962
|
return;
|
|
10954
10963
|
}
|
|
10955
10964
|
outputResult(result, mode);
|
|
@@ -11053,7 +11062,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
11053
11062
|
}
|
|
11054
11063
|
}
|
|
11055
11064
|
async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
11056
|
-
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-
|
|
11065
|
+
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
|
|
11057
11066
|
const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
|
|
11058
11067
|
const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
|
|
11059
11068
|
const { join: join13 } = await import("path");
|
|
@@ -12990,7 +12999,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
12990
12999
|
}
|
|
12991
13000
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
12992
13001
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
12993
|
-
const { forwardExec } = await import("./daemon-client-
|
|
13002
|
+
const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
|
|
12994
13003
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
12995
13004
|
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
12996
13005
|
const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
|
|
@@ -13030,7 +13039,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
13030
13039
|
const targetPage = pages[cmdTabIndex];
|
|
13031
13040
|
await targetPage.bringToFront().catch(() => {
|
|
13032
13041
|
});
|
|
13033
|
-
const { setActivePage: setActivePage2 } = await import("./browser-
|
|
13042
|
+
const { setActivePage: setActivePage2 } = await import("./browser-JXIJF4VL.js");
|
|
13034
13043
|
setActivePage2(session, targetPage);
|
|
13035
13044
|
}
|
|
13036
13045
|
}
|
|
@@ -13306,7 +13315,7 @@ async function main() {
|
|
|
13306
13315
|
const command = process.argv[2];
|
|
13307
13316
|
const isLongRunning = command === "preview" || command === "serve";
|
|
13308
13317
|
if (!isLongRunning) {
|
|
13309
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
13318
|
+
const { ensureProcessCanExit } = await import("./browser-JXIJF4VL.js");
|
|
13310
13319
|
await ensureProcessCanExit().catch(() => {
|
|
13311
13320
|
});
|
|
13312
13321
|
process.exit(process.exitCode || exitCode);
|
|
@@ -179,8 +179,8 @@ async function forwardNetworkExport(sessionName, id, lang) {
|
|
|
179
179
|
async function forwardNetworkInspect(sessionName, id) {
|
|
180
180
|
return rpcCall("network:inspect", { session: sessionName, id }, 1e4);
|
|
181
181
|
}
|
|
182
|
-
async function forwardRecordStart(session, url, cdpEndpoint) {
|
|
183
|
-
return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
|
|
182
|
+
async function forwardRecordStart(session, url, cdpEndpoint, stream) {
|
|
183
|
+
return rpcCall("record:start", { session, url, cdpEndpoint, stream }, 15e3);
|
|
184
184
|
}
|
|
185
185
|
async function forwardRecordStop(session, output) {
|
|
186
186
|
const params = { session };
|
package/dist/daemon-main.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./chunk-VEDJ5XSQ.js";
|
|
14
14
|
import {
|
|
15
15
|
SessionRecorder
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-C4O73DQL.js";
|
|
17
17
|
import {
|
|
18
18
|
closeEphemeralContext,
|
|
19
19
|
closeSessionByName,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveLaunchOpts,
|
|
30
30
|
saveSessionDiskMeta,
|
|
31
31
|
setActivePage
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-37SHZSUR.js";
|
|
33
33
|
import "./chunk-VJNMAWPZ.js";
|
|
34
34
|
import "./chunk-TNEN6VQ2.js";
|
|
35
35
|
import {
|
|
@@ -6575,7 +6575,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6575
6575
|
}
|
|
6576
6576
|
let targetPageOverride = null;
|
|
6577
6577
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6578
|
-
const { findTargetPage } = await import("./browser-
|
|
6578
|
+
const { findTargetPage } = await import("./browser-ALPLJNH2.js");
|
|
6579
6579
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6580
6580
|
if (!targetPageOverride) {
|
|
6581
6581
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6592,7 +6592,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6592
6592
|
params = result.data;
|
|
6593
6593
|
}
|
|
6594
6594
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6595
|
-
const { forwardExec } = await import("./daemon-client-
|
|
6595
|
+
const { forwardExec } = await import("./daemon-client-4HJENICO.js");
|
|
6596
6596
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6597
6597
|
if (result) return result;
|
|
6598
6598
|
}
|
|
@@ -8013,6 +8013,7 @@ function createRPCHandler() {
|
|
|
8013
8013
|
const sessionName = params.session || "default";
|
|
8014
8014
|
const url = params.url;
|
|
8015
8015
|
const cdpEndpoint = params.cdpEndpoint;
|
|
8016
|
+
const stream = params.stream || "clean";
|
|
8016
8017
|
if (activeRecorders.has(sessionName)) {
|
|
8017
8018
|
return { ok: false, error: "Recording already in progress for session: " + sessionName };
|
|
8018
8019
|
}
|
|
@@ -8044,7 +8045,7 @@ function createRPCHandler() {
|
|
|
8044
8045
|
}
|
|
8045
8046
|
try {
|
|
8046
8047
|
const recorder = new SessionRecorder(session.context, session.page, sessionName);
|
|
8047
|
-
await recorder.start(url);
|
|
8048
|
+
await recorder.start(url, { stream });
|
|
8048
8049
|
activeRecorders.set(sessionName, recorder);
|
|
8049
8050
|
return { ok: true, session: sessionName, startUrl: url || session.page.url() };
|
|
8050
8051
|
} catch (e) {
|
|
@@ -8195,7 +8196,8 @@ function createRPCHandler() {
|
|
|
8195
8196
|
const isNewFormat = Array.isArray(parsed.actions);
|
|
8196
8197
|
if (isNewFormat) {
|
|
8197
8198
|
try {
|
|
8198
|
-
const
|
|
8199
|
+
const replayErrors = [];
|
|
8200
|
+
const { SessionReplayer } = await import("./session-replayer-3HEIJBJ2.js");
|
|
8199
8201
|
const replayer = new SessionReplayer({
|
|
8200
8202
|
page: session.page,
|
|
8201
8203
|
stepDelay: slowMo * 500,
|
|
@@ -8203,7 +8205,9 @@ function createRPCHandler() {
|
|
|
8203
8205
|
console.log(`[replay] Step ${index + 1}/${total}: ${action.type} ${action.element?.selector || action.url || ""}`);
|
|
8204
8206
|
},
|
|
8205
8207
|
onError: (action, error) => {
|
|
8206
|
-
|
|
8208
|
+
const msg = `[${action?.type || "unknown"}] ${error?.message || String(error)}`;
|
|
8209
|
+
console.error("[replay] Error at step:", msg);
|
|
8210
|
+
replayErrors.push({ eventIndex: -1, error: msg });
|
|
8207
8211
|
}
|
|
8208
8212
|
});
|
|
8209
8213
|
if (!Array.isArray(parsed.actions) || typeof parsed.startUrl !== "string") {
|
|
@@ -8219,7 +8223,7 @@ function createRPCHandler() {
|
|
|
8219
8223
|
duration,
|
|
8220
8224
|
eventsPlayed: result2.success,
|
|
8221
8225
|
totalEvents: result2.success + result2.failed + result2.skipped,
|
|
8222
|
-
errors:
|
|
8226
|
+
errors: replayErrors
|
|
8223
8227
|
};
|
|
8224
8228
|
} catch (e) {
|
|
8225
8229
|
const msg = e instanceof Error ? e.message : String(e);
|
package/dist/index.d.ts
CHANGED
|
@@ -1812,6 +1812,13 @@ interface RecordingStep {
|
|
|
1812
1812
|
networkId: number;
|
|
1813
1813
|
paramName: string;
|
|
1814
1814
|
}>;
|
|
1815
|
+
signals: ActionSignal[];
|
|
1816
|
+
}
|
|
1817
|
+
/** Success / verification signal attached to a recording step */
|
|
1818
|
+
interface ActionSignal {
|
|
1819
|
+
type: 'network_success' | 'url_change' | 'dialog' | 'dom_change';
|
|
1820
|
+
value?: string;
|
|
1821
|
+
label: string;
|
|
1815
1822
|
}
|
|
1816
1823
|
interface RecordingSummary {
|
|
1817
1824
|
startUrl: string;
|
|
@@ -1871,12 +1878,17 @@ declare class SessionRecorder {
|
|
|
1871
1878
|
private lastActionTs;
|
|
1872
1879
|
private activePages;
|
|
1873
1880
|
private lastKnownUrl;
|
|
1874
|
-
/** Dedup
|
|
1875
|
-
|
|
1881
|
+
/** Dedup map: key = normalizedType|tag|value → expiration timestamp.
|
|
1882
|
+
* Replaces old single-entry cdpActionDedup for bidirectional dedup. */
|
|
1883
|
+
private dedupMap;
|
|
1884
|
+
private dedupActionCount;
|
|
1876
1885
|
/** Network dedup: last request key for short-window dedup */
|
|
1877
1886
|
private _lastRequestKey;
|
|
1878
1887
|
private _lastRequestTs;
|
|
1879
1888
|
private _isRecording;
|
|
1889
|
+
private streamMode;
|
|
1890
|
+
/** Ambient action types: filtered out in clean mode */
|
|
1891
|
+
private static readonly AMBIENT_TYPES;
|
|
1880
1892
|
constructor(context: XBContext, page: XBPage, sessionName: string);
|
|
1881
1893
|
get isRecording(): boolean;
|
|
1882
1894
|
get actionCount(): number;
|
|
@@ -1888,6 +1900,8 @@ declare class SessionRecorder {
|
|
|
1888
1900
|
url?: string;
|
|
1889
1901
|
element?: UserAction['element'];
|
|
1890
1902
|
}): Promise<void>;
|
|
1903
|
+
/** Generate dedup key for an action (normalized type + selector/tag + value). */
|
|
1904
|
+
private dedupKey;
|
|
1891
1905
|
get networkCount(): number;
|
|
1892
1906
|
getLiveData(): RecordingData;
|
|
1893
1907
|
addManualCheckpoint(type: string, hint: string, selector?: string): CheckpointEntry;
|
|
@@ -1898,7 +1912,9 @@ declare class SessionRecorder {
|
|
|
1898
1912
|
get controlFilePath(): string;
|
|
1899
1913
|
/** Path to the stop signal file (written by `record stop`). */
|
|
1900
1914
|
get stopSignalPath(): string;
|
|
1901
|
-
start(url?: string
|
|
1915
|
+
start(url?: string, options?: {
|
|
1916
|
+
stream?: 'clean' | 'raw';
|
|
1917
|
+
}): Promise<void>;
|
|
1902
1918
|
stop(): Promise<{
|
|
1903
1919
|
data: RecordingData;
|
|
1904
1920
|
summary: RecordingSummary;
|
|
@@ -1932,6 +1948,11 @@ declare class SessionRecorder {
|
|
|
1932
1948
|
*/
|
|
1933
1949
|
private captureClickContext;
|
|
1934
1950
|
private detectCheckpoints;
|
|
1951
|
+
/**
|
|
1952
|
+
* After a key action (click/submit/input), check for success signals:
|
|
1953
|
+
* URL changes, recent successful network responses, dialog appearances.
|
|
1954
|
+
*/
|
|
1955
|
+
private verifyAction;
|
|
1935
1956
|
private flushToDisk;
|
|
1936
1957
|
private writeFinalOutput;
|
|
1937
1958
|
private buildData;
|