agentmb 0.1.1 → 0.3.1
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 +818 -153
- package/dist/browser/actions.d.ts +24 -1
- package/dist/browser/actions.d.ts.map +1 -1
- package/dist/browser/actions.js +118 -19
- package/dist/browser/actions.js.map +1 -1
- package/dist/browser/manager.d.ts +14 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/manager.js +117 -4
- package/dist/browser/manager.js.map +1 -1
- package/dist/cli/commands/actions.d.ts.map +1 -1
- package/dist/cli/commands/actions.js +305 -70
- package/dist/cli/commands/actions.js.map +1 -1
- package/dist/cli/commands/browser-launch.d.ts +7 -0
- package/dist/cli/commands/browser-launch.d.ts.map +1 -0
- package/dist/cli/commands/browser-launch.js +116 -0
- package/dist/cli/commands/browser-launch.js.map +1 -0
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +67 -4
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.js +2 -2
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/routes/actions.d.ts.map +1 -1
- package/dist/daemon/routes/actions.js +419 -66
- package/dist/daemon/routes/actions.js.map +1 -1
- package/dist/daemon/routes/sessions.d.ts.map +1 -1
- package/dist/daemon/routes/sessions.js +208 -3
- package/dist/daemon/routes/sessions.js.map +1 -1
- package/dist/daemon/routes/state.d.ts.map +1 -1
- package/dist/daemon/routes/state.js +26 -0
- package/dist/daemon/routes/state.js.map +1 -1
- package/dist/daemon/server.js +1 -1
- package/dist/daemon/session.d.ts +19 -0
- package/dist/daemon/session.d.ts.map +1 -1
- package/dist/daemon/session.js +13 -0
- package/dist/daemon/session.js.map +1 -1
- package/dist/policy/types.d.ts.map +1 -1
- package/dist/policy/types.js +14 -12
- package/dist/policy/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -38,6 +38,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.registerActionRoutes = registerActionRoutes;
|
|
40
40
|
const crypto_1 = __importDefault(require("crypto"));
|
|
41
|
+
const fs_1 = __importDefault(require("fs"));
|
|
42
|
+
const os_1 = __importDefault(require("os"));
|
|
43
|
+
const path_1 = __importDefault(require("path"));
|
|
41
44
|
require("../types"); // T11: Fastify type augmentation
|
|
42
45
|
const Actions = __importStar(require("../../browser/actions"));
|
|
43
46
|
const actions_1 = require("../../browser/actions");
|
|
@@ -146,6 +149,59 @@ async function applyPolicy(server, sessionId, domain, action, opts, reply) {
|
|
|
146
149
|
}
|
|
147
150
|
return true;
|
|
148
151
|
}
|
|
152
|
+
function pfRange(field, value, min, max) {
|
|
153
|
+
if (value === undefined)
|
|
154
|
+
return null;
|
|
155
|
+
return value < min || value > max ? { field, constraint: `must be ${min}–${max}`, value } : null;
|
|
156
|
+
}
|
|
157
|
+
function pfMaxLen(field, value, max) {
|
|
158
|
+
if (!value)
|
|
159
|
+
return null;
|
|
160
|
+
return value.length > max ? { field, constraint: `max length ${max} chars`, value: value.length } : null;
|
|
161
|
+
}
|
|
162
|
+
/** Run an ordered list of checks; sends 400 + returns false on first violation. */
|
|
163
|
+
function preflight(checks, reply) {
|
|
164
|
+
const v = checks.find(Boolean);
|
|
165
|
+
if (v) {
|
|
166
|
+
reply.code(400).send({ error: 'preflight_failed', field: v.field, constraint: v.constraint, value: v.value });
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
async function applyStabilityPre(page, opts) {
|
|
172
|
+
if (!opts)
|
|
173
|
+
return;
|
|
174
|
+
if (opts.wait_before_ms)
|
|
175
|
+
await new Promise(r => setTimeout(r, opts.wait_before_ms));
|
|
176
|
+
if (opts.wait_dom_stable_ms) {
|
|
177
|
+
try {
|
|
178
|
+
await page.waitForFunction('document.readyState === "complete"', undefined, { timeout: opts.wait_dom_stable_ms });
|
|
179
|
+
}
|
|
180
|
+
catch { /* timeout is acceptable */ }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function applyStabilityPost(page, opts) {
|
|
184
|
+
if (!opts)
|
|
185
|
+
return;
|
|
186
|
+
if (opts.wait_after_ms)
|
|
187
|
+
await new Promise(r => setTimeout(r, opts.wait_after_ms));
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// R08-R13: Error recovery hints — enrich 422 ActionDiagnostics with hint
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function enrichDiag(diag) {
|
|
193
|
+
const msg = diag.error.toLowerCase();
|
|
194
|
+
let recovery_hint;
|
|
195
|
+
if (msg.includes('timeout') || msg.includes('waiting for'))
|
|
196
|
+
recovery_hint = 'Increase timeout_ms or add stability.wait_before_ms; ensure element is visible before acting';
|
|
197
|
+
else if (msg.includes('target closed') || msg.includes('execution context') || msg.includes('detached'))
|
|
198
|
+
recovery_hint = 'Page may have navigated or element was removed; re-navigate or re-snapshot';
|
|
199
|
+
else if (msg.includes('not found') || msg.includes('no element') || msg.includes('failed to find'))
|
|
200
|
+
recovery_hint = 'Check selector; use snapshot_map to verify element exists on current page';
|
|
201
|
+
else if (msg.includes('intercept') || msg.includes('overlap') || msg.includes('obscur'))
|
|
202
|
+
recovery_hint = 'Element may be covered by overlay; try executor=auto_fallback or scroll into view first';
|
|
203
|
+
return recovery_hint ? { ...diag, recovery_hint } : diag;
|
|
204
|
+
}
|
|
149
205
|
function registerActionRoutes(server, registry) {
|
|
150
206
|
function getLogger() {
|
|
151
207
|
return server.auditLogger;
|
|
@@ -172,7 +228,11 @@ function registerActionRoutes(server, registry) {
|
|
|
172
228
|
const eid = input.ref_id.slice(colonIdx + 1);
|
|
173
229
|
const snapshot = bm.getSnapshot(sessionId, snapshotId);
|
|
174
230
|
if (!snapshot) {
|
|
175
|
-
reply.code(409).send({
|
|
231
|
+
reply.code(409).send({
|
|
232
|
+
error: 'stale_ref', ref_id: input.ref_id,
|
|
233
|
+
message: 'Snapshot not found or expired; call snapshot_map again',
|
|
234
|
+
suggestions: ['call snapshot_map to get fresh ref_ids', 'use selector or element_id as fallback'],
|
|
235
|
+
});
|
|
176
236
|
return null;
|
|
177
237
|
}
|
|
178
238
|
const currentRev = bm.getPageRev(sessionId);
|
|
@@ -183,6 +243,7 @@ function registerActionRoutes(server, registry) {
|
|
|
183
243
|
snapshot_page_rev: snapshot.page_rev,
|
|
184
244
|
current_page_rev: currentRev,
|
|
185
245
|
message: 'Page has changed since snapshot was taken; call snapshot_map again',
|
|
246
|
+
suggestions: ['call snapshot_map to get fresh ref_ids', 'if page is stable, the navigation event incremented page_rev — wait and resnapshot'],
|
|
186
247
|
});
|
|
187
248
|
return null;
|
|
188
249
|
}
|
|
@@ -225,12 +286,16 @@ function registerActionRoutes(server, registry) {
|
|
|
225
286
|
return Actions.navigate(s.page, url, wait_until, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
226
287
|
});
|
|
227
288
|
// POST /api/v1/sessions/:id/click
|
|
228
|
-
//
|
|
289
|
+
// R08-R06: executor='auto_fallback' automatically retries via bbox coords on DOM failure
|
|
290
|
+
// R08-R02: stability.wait_before_ms / wait_after_ms / wait_dom_stable_ms
|
|
291
|
+
// R08-R09: preflight validates timeout_ms range
|
|
229
292
|
server.post('/api/v1/sessions/:id/click', async (req, reply) => {
|
|
230
293
|
const s = resolve(req.params.id, reply);
|
|
231
294
|
if (!s)
|
|
232
295
|
return;
|
|
233
|
-
const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y } = req.body;
|
|
296
|
+
const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y, executor = 'strict', stability } = req.body;
|
|
297
|
+
if (!preflight([pfRange('timeout_ms', timeout_ms, 50, 60000)], reply))
|
|
298
|
+
return;
|
|
234
299
|
const selector = resolveTarget(req.body, reply, s.id);
|
|
235
300
|
if (!selector)
|
|
236
301
|
return;
|
|
@@ -239,32 +304,63 @@ function registerActionRoutes(server, registry) {
|
|
|
239
304
|
const target = resolveOrReply(s.page, frame, reply);
|
|
240
305
|
if (!target)
|
|
241
306
|
return;
|
|
307
|
+
await applyStabilityPre(s.page, stability);
|
|
242
308
|
try {
|
|
243
|
-
|
|
309
|
+
const result = await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
310
|
+
await applyStabilityPost(s.page, stability);
|
|
311
|
+
return { ...result, executed_via: 'high_level' };
|
|
244
312
|
}
|
|
245
313
|
catch (domErr) {
|
|
246
|
-
//
|
|
247
|
-
if (
|
|
314
|
+
// auto_fallback: resolve element bbox and retry via mouse.click()
|
|
315
|
+
if (executor === 'auto_fallback') {
|
|
248
316
|
try {
|
|
249
|
-
await
|
|
250
|
-
|
|
251
|
-
|
|
317
|
+
const bbox = await target.locator(selector).boundingBox();
|
|
318
|
+
if (bbox) {
|
|
319
|
+
let cx = Math.round(bbox.x + bbox.width / 2);
|
|
320
|
+
let cy = Math.round(bbox.y + bbox.height / 2);
|
|
321
|
+
// Frame offset compensation: when target is a Frame (not the Page),
|
|
322
|
+
// boundingBox() coords are relative to the frame viewport; add the
|
|
323
|
+
// frame element's page-level offset so mouse.click lands correctly.
|
|
324
|
+
if (frame && target !== s.page) {
|
|
325
|
+
try {
|
|
326
|
+
const frameRect = await target.evaluate('(() => { const el = window.frameElement; if (!el) return {x:0,y:0}; const r = el.getBoundingClientRect(); return {x: r.x, y: r.y}; })()');
|
|
327
|
+
cx += Math.round(frameRect.x);
|
|
328
|
+
cy += Math.round(frameRect.y);
|
|
329
|
+
}
|
|
330
|
+
catch { /* best-effort; proceed with uncompensated coords */ }
|
|
331
|
+
}
|
|
332
|
+
await s.page.mouse.click(cx, cy);
|
|
333
|
+
await applyStabilityPost(s.page, stability);
|
|
334
|
+
return { status: 'ok', selector, executed_via: 'low_level', fallback_x: cx, fallback_y: cy, duration_ms: 0 };
|
|
335
|
+
}
|
|
252
336
|
}
|
|
253
|
-
catch
|
|
254
|
-
|
|
337
|
+
catch { /* fallback also failed — fall through to original error */ }
|
|
338
|
+
}
|
|
339
|
+
// explicit fallback coordinates (legacy T21 path)
|
|
340
|
+
if (fallback_x !== undefined && fallback_y !== undefined) {
|
|
341
|
+
try {
|
|
342
|
+
await s.page.mouse.click(fallback_x, fallback_y);
|
|
343
|
+
return { status: 'ok', selector, executed_via: 'low_level', track: 'coords', fallback_x, fallback_y, duration_ms: 0 };
|
|
255
344
|
}
|
|
345
|
+
catch { /* both tracks failed */ }
|
|
346
|
+
}
|
|
347
|
+
if (domErr instanceof Actions.ActionDiagnosticsError) {
|
|
348
|
+
const diag = enrichDiag({ ...domErr.diagnostics, suggested_fallback: 'retry with executor="auto_fallback" to attempt coordinates-based click' });
|
|
349
|
+
return reply.code(422).send(diag);
|
|
256
350
|
}
|
|
257
|
-
if (domErr instanceof Actions.ActionDiagnosticsError)
|
|
258
|
-
return reply.code(422).send(domErr.diagnostics);
|
|
259
351
|
throw domErr;
|
|
260
352
|
}
|
|
261
353
|
});
|
|
262
354
|
// POST /api/v1/sessions/:id/fill
|
|
355
|
+
// R08-R01: fill_strategy='type' uses pressSequentially with char_delay_ms (humanization)
|
|
356
|
+
// R08-R02: stability options; R08-R09: preflight value length
|
|
263
357
|
server.post('/api/v1/sessions/:id/fill', async (req, reply) => {
|
|
264
358
|
const s = resolve(req.params.id, reply);
|
|
265
359
|
if (!s)
|
|
266
360
|
return;
|
|
267
|
-
const { value, frame, purpose, operator, sensitive, retry } = req.body;
|
|
361
|
+
const { value, frame, purpose, operator, sensitive, retry, stability, fill_strategy = 'instant', char_delay_ms = 0 } = req.body;
|
|
362
|
+
if (!preflight([pfMaxLen('value', value, 100_000)], reply))
|
|
363
|
+
return;
|
|
268
364
|
const selector = resolveTarget(req.body, reply, s.id);
|
|
269
365
|
if (!selector)
|
|
270
366
|
return;
|
|
@@ -273,7 +369,12 @@ function registerActionRoutes(server, registry) {
|
|
|
273
369
|
const target = resolveOrReply(s.page, frame, reply);
|
|
274
370
|
if (!target)
|
|
275
371
|
return;
|
|
276
|
-
|
|
372
|
+
await applyStabilityPre(s.page, stability);
|
|
373
|
+
const result = fill_strategy === 'type'
|
|
374
|
+
? await Actions.typeText(target, selector, value, char_delay_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator))
|
|
375
|
+
: await Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
376
|
+
await applyStabilityPost(s.page, stability);
|
|
377
|
+
return result;
|
|
277
378
|
});
|
|
278
379
|
// POST /api/v1/sessions/:id/eval
|
|
279
380
|
server.post('/api/v1/sessions/:id/eval', async (req, reply) => {
|
|
@@ -291,7 +392,7 @@ function registerActionRoutes(server, registry) {
|
|
|
291
392
|
}
|
|
292
393
|
catch (e) {
|
|
293
394
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
294
|
-
return reply.code(422).send(e.diagnostics);
|
|
395
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
295
396
|
throw e;
|
|
296
397
|
}
|
|
297
398
|
});
|
|
@@ -311,7 +412,7 @@ function registerActionRoutes(server, registry) {
|
|
|
311
412
|
}
|
|
312
413
|
catch (e) {
|
|
313
414
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
314
|
-
return reply.code(422).send(e.diagnostics);
|
|
415
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
315
416
|
throw e;
|
|
316
417
|
}
|
|
317
418
|
});
|
|
@@ -326,7 +427,7 @@ function registerActionRoutes(server, registry) {
|
|
|
326
427
|
}
|
|
327
428
|
catch (e) {
|
|
328
429
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
329
|
-
return reply.code(422).send(e.diagnostics);
|
|
430
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
330
431
|
throw e;
|
|
331
432
|
}
|
|
332
433
|
});
|
|
@@ -350,7 +451,7 @@ function registerActionRoutes(server, registry) {
|
|
|
350
451
|
}
|
|
351
452
|
catch (e) {
|
|
352
453
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
353
|
-
return reply.code(422).send(e.diagnostics);
|
|
454
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
354
455
|
throw e;
|
|
355
456
|
}
|
|
356
457
|
});
|
|
@@ -373,7 +474,7 @@ function registerActionRoutes(server, registry) {
|
|
|
373
474
|
}
|
|
374
475
|
catch (e) {
|
|
375
476
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
376
|
-
return reply.code(422).send(e.diagnostics);
|
|
477
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
377
478
|
throw e;
|
|
378
479
|
}
|
|
379
480
|
});
|
|
@@ -391,7 +492,7 @@ function registerActionRoutes(server, registry) {
|
|
|
391
492
|
}
|
|
392
493
|
catch (e) {
|
|
393
494
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
394
|
-
return reply.code(422).send(e.diagnostics);
|
|
495
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
395
496
|
throw e;
|
|
396
497
|
}
|
|
397
498
|
});
|
|
@@ -412,7 +513,7 @@ function registerActionRoutes(server, registry) {
|
|
|
412
513
|
}
|
|
413
514
|
catch (e) {
|
|
414
515
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
415
|
-
return reply.code(422).send(e.diagnostics);
|
|
516
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
416
517
|
throw e;
|
|
417
518
|
}
|
|
418
519
|
});
|
|
@@ -430,7 +531,7 @@ function registerActionRoutes(server, registry) {
|
|
|
430
531
|
}
|
|
431
532
|
catch (e) {
|
|
432
533
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
433
|
-
return reply.code(422).send(e.diagnostics);
|
|
534
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
434
535
|
throw e;
|
|
435
536
|
}
|
|
436
537
|
});
|
|
@@ -445,7 +546,7 @@ function registerActionRoutes(server, registry) {
|
|
|
445
546
|
}
|
|
446
547
|
catch (e) {
|
|
447
548
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
448
|
-
return reply.code(422).send(e.diagnostics);
|
|
549
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
449
550
|
throw e;
|
|
450
551
|
}
|
|
451
552
|
});
|
|
@@ -461,7 +562,7 @@ function registerActionRoutes(server, registry) {
|
|
|
461
562
|
}
|
|
462
563
|
catch (e) {
|
|
463
564
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
464
|
-
return reply.code(422).send(e.diagnostics);
|
|
565
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
465
566
|
throw e;
|
|
466
567
|
}
|
|
467
568
|
});
|
|
@@ -481,22 +582,35 @@ function registerActionRoutes(server, registry) {
|
|
|
481
582
|
}
|
|
482
583
|
catch (e) {
|
|
483
584
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
484
|
-
return reply.code(422).send(e.diagnostics);
|
|
585
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
485
586
|
throw e;
|
|
486
587
|
}
|
|
487
588
|
});
|
|
488
589
|
// POST /api/v1/sessions/:id/download
|
|
590
|
+
// T07: guard on accept_downloads; T08: element_id / ref_id support
|
|
489
591
|
server.post('/api/v1/sessions/:id/download', async (req, reply) => {
|
|
490
592
|
const s = resolve(req.params.id, reply);
|
|
491
593
|
if (!s)
|
|
492
594
|
return;
|
|
493
|
-
|
|
595
|
+
// T07: check accept_downloads is enabled
|
|
596
|
+
const bm = server.browserManager;
|
|
597
|
+
if (bm && !bm.getAcceptDownloads(s.id)) {
|
|
598
|
+
return reply.code(422).send({
|
|
599
|
+
error: 'download_not_enabled',
|
|
600
|
+
message: 'Downloads are disabled for this session. Create the session with accept_downloads=true (CLI: agentmb session new --accept-downloads).',
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
const { timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
|
|
604
|
+
// T08: resolve selector/element_id/ref_id to CSS selector
|
|
605
|
+
const selector = resolveTarget(req.body, reply, s.id);
|
|
606
|
+
if (!selector)
|
|
607
|
+
return;
|
|
494
608
|
try {
|
|
495
609
|
return await Actions.downloadFile(s.page, selector, timeout_ms, max_bytes, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
496
610
|
}
|
|
497
611
|
catch (e) {
|
|
498
612
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
499
|
-
return reply.code(422).send(e.diagnostics);
|
|
613
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
500
614
|
throw e;
|
|
501
615
|
}
|
|
502
616
|
});
|
|
@@ -516,13 +630,13 @@ function registerActionRoutes(server, registry) {
|
|
|
516
630
|
const s = resolve(req.params.id, reply);
|
|
517
631
|
if (!s)
|
|
518
632
|
return;
|
|
519
|
-
const { scope, limit = 500, purpose, operator } = req.body ?? {};
|
|
633
|
+
const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
|
|
520
634
|
try {
|
|
521
|
-
return await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
635
|
+
return await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
522
636
|
}
|
|
523
637
|
catch (e) {
|
|
524
638
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
525
|
-
return reply.code(422).send(e.diagnostics);
|
|
639
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
526
640
|
throw e;
|
|
527
641
|
}
|
|
528
642
|
});
|
|
@@ -547,7 +661,7 @@ function registerActionRoutes(server, registry) {
|
|
|
547
661
|
}
|
|
548
662
|
catch (e) {
|
|
549
663
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
550
|
-
return reply.code(422).send(e.diagnostics);
|
|
664
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
551
665
|
throw e;
|
|
552
666
|
}
|
|
553
667
|
});
|
|
@@ -572,7 +686,7 @@ function registerActionRoutes(server, registry) {
|
|
|
572
686
|
}
|
|
573
687
|
catch (e) {
|
|
574
688
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
575
|
-
return reply.code(422).send(e.diagnostics);
|
|
689
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
576
690
|
throw e;
|
|
577
691
|
}
|
|
578
692
|
});
|
|
@@ -589,7 +703,7 @@ function registerActionRoutes(server, registry) {
|
|
|
589
703
|
}
|
|
590
704
|
catch (e) {
|
|
591
705
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
592
|
-
return reply.code(422).send(e.diagnostics);
|
|
706
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
593
707
|
throw e;
|
|
594
708
|
}
|
|
595
709
|
});
|
|
@@ -611,7 +725,7 @@ function registerActionRoutes(server, registry) {
|
|
|
611
725
|
}
|
|
612
726
|
catch (e) {
|
|
613
727
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
614
|
-
return reply.code(422).send(e.diagnostics);
|
|
728
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
615
729
|
throw e;
|
|
616
730
|
}
|
|
617
731
|
});
|
|
@@ -630,7 +744,7 @@ function registerActionRoutes(server, registry) {
|
|
|
630
744
|
}
|
|
631
745
|
catch (e) {
|
|
632
746
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
633
|
-
return reply.code(422).send(e.diagnostics);
|
|
747
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
634
748
|
throw e;
|
|
635
749
|
}
|
|
636
750
|
});
|
|
@@ -649,7 +763,7 @@ function registerActionRoutes(server, registry) {
|
|
|
649
763
|
}
|
|
650
764
|
catch (e) {
|
|
651
765
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
652
|
-
return reply.code(422).send(e.diagnostics);
|
|
766
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
653
767
|
throw e;
|
|
654
768
|
}
|
|
655
769
|
});
|
|
@@ -668,7 +782,7 @@ function registerActionRoutes(server, registry) {
|
|
|
668
782
|
}
|
|
669
783
|
catch (e) {
|
|
670
784
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
671
|
-
return reply.code(422).send(e.diagnostics);
|
|
785
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
672
786
|
throw e;
|
|
673
787
|
}
|
|
674
788
|
});
|
|
@@ -688,7 +802,7 @@ function registerActionRoutes(server, registry) {
|
|
|
688
802
|
}
|
|
689
803
|
catch (e) {
|
|
690
804
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
691
|
-
return reply.code(422).send(e.diagnostics);
|
|
805
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
692
806
|
throw e;
|
|
693
807
|
}
|
|
694
808
|
});
|
|
@@ -707,7 +821,7 @@ function registerActionRoutes(server, registry) {
|
|
|
707
821
|
}
|
|
708
822
|
catch (e) {
|
|
709
823
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
710
|
-
return reply.code(422).send(e.diagnostics);
|
|
824
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
711
825
|
throw e;
|
|
712
826
|
}
|
|
713
827
|
});
|
|
@@ -715,31 +829,48 @@ function registerActionRoutes(server, registry) {
|
|
|
715
829
|
const s = resolve(req.params.id, reply);
|
|
716
830
|
if (!s)
|
|
717
831
|
return;
|
|
718
|
-
const { source, source_element_id, target, target_element_id, purpose, operator } = req.body;
|
|
719
|
-
const src =
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
832
|
+
const { source, source_element_id, source_ref_id, target, target_element_id, target_ref_id, purpose, operator } = req.body;
|
|
833
|
+
const src = resolveTarget({ selector: source, element_id: source_element_id, ref_id: source_ref_id }, reply, s.id);
|
|
834
|
+
if (!src)
|
|
835
|
+
return;
|
|
836
|
+
const tgt = resolveTarget({ selector: target, element_id: target_element_id, ref_id: target_ref_id }, reply, s.id);
|
|
837
|
+
if (!tgt)
|
|
838
|
+
return;
|
|
723
839
|
try {
|
|
724
840
|
return await Actions.drag(s.page, src, tgt, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
725
841
|
}
|
|
726
842
|
catch (e) {
|
|
727
843
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
728
|
-
return reply.code(422).send(e.diagnostics);
|
|
844
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
729
845
|
throw e;
|
|
730
846
|
}
|
|
731
847
|
});
|
|
848
|
+
// R08-R05/R08: Ref->Box->Input — mouse_move accepts ref_id + steps for smooth trajectory
|
|
732
849
|
server.post('/api/v1/sessions/:id/mouse_move', async (req, reply) => {
|
|
733
850
|
const s = resolve(req.params.id, reply);
|
|
734
851
|
if (!s)
|
|
735
852
|
return;
|
|
736
|
-
const {
|
|
853
|
+
const { purpose, operator, steps } = req.body;
|
|
854
|
+
let { x, y } = req.body;
|
|
855
|
+
// Ref->Box->Input: if ref_id/element_id/selector provided, resolve to bbox center coords
|
|
856
|
+
if (req.body.ref_id || req.body.element_id || req.body.selector) {
|
|
857
|
+
const cssSelector = resolveTarget(req.body, reply, s.id);
|
|
858
|
+
if (!cssSelector)
|
|
859
|
+
return;
|
|
860
|
+
const bbox = await s.page.locator(cssSelector).boundingBox();
|
|
861
|
+
if (!bbox)
|
|
862
|
+
return reply.code(404).send({ error: 'Element not found or not visible for mouse_move', selector: cssSelector });
|
|
863
|
+
x = Math.round(bbox.x + bbox.width / 2);
|
|
864
|
+
y = Math.round(bbox.y + bbox.height / 2);
|
|
865
|
+
}
|
|
866
|
+
if (x === undefined || y === undefined)
|
|
867
|
+
return reply.code(400).send({ error: 'Either x+y coordinates or ref_id/element_id/selector is required' });
|
|
737
868
|
try {
|
|
738
|
-
return await Actions.mouseMove(s.page, x, y, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
869
|
+
return await Actions.mouseMove(s.page, x, y, steps ?? 1, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
739
870
|
}
|
|
740
871
|
catch (e) {
|
|
741
872
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
742
|
-
return reply.code(422).send(e.diagnostics);
|
|
873
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
743
874
|
throw e;
|
|
744
875
|
}
|
|
745
876
|
});
|
|
@@ -753,7 +884,7 @@ function registerActionRoutes(server, registry) {
|
|
|
753
884
|
}
|
|
754
885
|
catch (e) {
|
|
755
886
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
756
|
-
return reply.code(422).send(e.diagnostics);
|
|
887
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
757
888
|
throw e;
|
|
758
889
|
}
|
|
759
890
|
});
|
|
@@ -767,7 +898,7 @@ function registerActionRoutes(server, registry) {
|
|
|
767
898
|
}
|
|
768
899
|
catch (e) {
|
|
769
900
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
770
|
-
return reply.code(422).send(e.diagnostics);
|
|
901
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
771
902
|
throw e;
|
|
772
903
|
}
|
|
773
904
|
});
|
|
@@ -781,7 +912,7 @@ function registerActionRoutes(server, registry) {
|
|
|
781
912
|
}
|
|
782
913
|
catch (e) {
|
|
783
914
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
784
|
-
return reply.code(422).send(e.diagnostics);
|
|
915
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
785
916
|
throw e;
|
|
786
917
|
}
|
|
787
918
|
});
|
|
@@ -795,7 +926,7 @@ function registerActionRoutes(server, registry) {
|
|
|
795
926
|
}
|
|
796
927
|
catch (e) {
|
|
797
928
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
798
|
-
return reply.code(422).send(e.diagnostics);
|
|
929
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
799
930
|
throw e;
|
|
800
931
|
}
|
|
801
932
|
});
|
|
@@ -812,7 +943,7 @@ function registerActionRoutes(server, registry) {
|
|
|
812
943
|
}
|
|
813
944
|
catch (e) {
|
|
814
945
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
815
|
-
return reply.code(422).send(e.diagnostics);
|
|
946
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
816
947
|
throw e;
|
|
817
948
|
}
|
|
818
949
|
});
|
|
@@ -826,7 +957,7 @@ function registerActionRoutes(server, registry) {
|
|
|
826
957
|
}
|
|
827
958
|
catch (e) {
|
|
828
959
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
829
|
-
return reply.code(422).send(e.diagnostics);
|
|
960
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
830
961
|
throw e;
|
|
831
962
|
}
|
|
832
963
|
});
|
|
@@ -840,7 +971,7 @@ function registerActionRoutes(server, registry) {
|
|
|
840
971
|
}
|
|
841
972
|
catch (e) {
|
|
842
973
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
843
|
-
return reply.code(422).send(e.diagnostics);
|
|
974
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
844
975
|
throw e;
|
|
845
976
|
}
|
|
846
977
|
});
|
|
@@ -857,7 +988,7 @@ function registerActionRoutes(server, registry) {
|
|
|
857
988
|
}
|
|
858
989
|
catch (e) {
|
|
859
990
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
860
|
-
return reply.code(422).send(e.diagnostics);
|
|
991
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
861
992
|
throw e;
|
|
862
993
|
}
|
|
863
994
|
});
|
|
@@ -871,7 +1002,7 @@ function registerActionRoutes(server, registry) {
|
|
|
871
1002
|
}
|
|
872
1003
|
catch (e) {
|
|
873
1004
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
874
|
-
return reply.code(422).send(e.diagnostics);
|
|
1005
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
875
1006
|
throw e;
|
|
876
1007
|
}
|
|
877
1008
|
});
|
|
@@ -885,7 +1016,7 @@ function registerActionRoutes(server, registry) {
|
|
|
885
1016
|
}
|
|
886
1017
|
catch (e) {
|
|
887
1018
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
888
|
-
return reply.code(422).send(e.diagnostics);
|
|
1019
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
889
1020
|
throw e;
|
|
890
1021
|
}
|
|
891
1022
|
});
|
|
@@ -899,11 +1030,12 @@ function registerActionRoutes(server, registry) {
|
|
|
899
1030
|
const { purpose, operator, ...opts } = req.body ?? {};
|
|
900
1031
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
901
1032
|
try {
|
|
902
|
-
|
|
1033
|
+
const result = await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1034
|
+
return { ...result, session_id: s.id };
|
|
903
1035
|
}
|
|
904
1036
|
catch (e) {
|
|
905
1037
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
906
|
-
return reply.code(422).send(e.diagnostics);
|
|
1038
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
907
1039
|
throw e;
|
|
908
1040
|
}
|
|
909
1041
|
});
|
|
@@ -913,11 +1045,12 @@ function registerActionRoutes(server, registry) {
|
|
|
913
1045
|
return;
|
|
914
1046
|
const { purpose, operator, ...opts } = req.body;
|
|
915
1047
|
try {
|
|
916
|
-
|
|
1048
|
+
const result = await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1049
|
+
return { ...result, session_id: s.id };
|
|
917
1050
|
}
|
|
918
1051
|
catch (e) {
|
|
919
1052
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
920
|
-
return reply.code(422).send(e.diagnostics);
|
|
1053
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
921
1054
|
throw e;
|
|
922
1055
|
}
|
|
923
1056
|
});
|
|
@@ -928,10 +1061,10 @@ function registerActionRoutes(server, registry) {
|
|
|
928
1061
|
const s = resolve(req.params.id, reply);
|
|
929
1062
|
if (!s)
|
|
930
1063
|
return;
|
|
931
|
-
const { scope, limit = 500, purpose, operator } = req.body ?? {};
|
|
1064
|
+
const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
|
|
932
1065
|
const bm = server.browserManager;
|
|
933
1066
|
try {
|
|
934
|
-
const elemResult = await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1067
|
+
const elemResult = await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
935
1068
|
const snapshotId = 'snap_' + crypto_1.default.randomBytes(4).toString('hex');
|
|
936
1069
|
const pageRev = bm?.getPageRev(s.id) ?? 0;
|
|
937
1070
|
const elements = elemResult.elements.map((el) => ({ ...el, ref_id: `${snapshotId}:${el.element_id}` }));
|
|
@@ -940,9 +1073,229 @@ function registerActionRoutes(server, registry) {
|
|
|
940
1073
|
}
|
|
941
1074
|
catch (e) {
|
|
942
1075
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
943
|
-
return reply.code(422).send(e.diagnostics);
|
|
1076
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
1077
|
+
throw e;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
// ---------------------------------------------------------------------------
|
|
1081
|
+
// R08-R10: Semantic element locator — find by role/text/label/placeholder/alt_text
|
|
1082
|
+
// ---------------------------------------------------------------------------
|
|
1083
|
+
server.post('/api/v1/sessions/:id/find', async (req, reply) => {
|
|
1084
|
+
const s = resolve(req.params.id, reply);
|
|
1085
|
+
if (!s)
|
|
1086
|
+
return;
|
|
1087
|
+
const { query_type, query, name, exact = false, nth = 0 } = req.body;
|
|
1088
|
+
try {
|
|
1089
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1090
|
+
let locator;
|
|
1091
|
+
if (query_type === 'role')
|
|
1092
|
+
locator = s.page.getByRole(query, { name, exact });
|
|
1093
|
+
else if (query_type === 'text')
|
|
1094
|
+
locator = s.page.getByText(query, { exact });
|
|
1095
|
+
else if (query_type === 'label')
|
|
1096
|
+
locator = s.page.getByLabel(query, { exact });
|
|
1097
|
+
else if (query_type === 'placeholder')
|
|
1098
|
+
locator = s.page.getByPlaceholder(query, { exact });
|
|
1099
|
+
else if (query_type === 'alt_text')
|
|
1100
|
+
locator = s.page.getByAltText(query, { exact });
|
|
1101
|
+
else
|
|
1102
|
+
return reply.code(400).send({ error: `Unknown query_type: ${query_type}`, valid: ['role', 'text', 'label', 'placeholder', 'alt_text'] });
|
|
1103
|
+
const count = await locator.count();
|
|
1104
|
+
if (count === 0)
|
|
1105
|
+
return { status: 'ok', found: false, count: 0, nth: 0, query_type, query };
|
|
1106
|
+
const target = locator.nth(nth);
|
|
1107
|
+
const bbox = await target.boundingBox().catch(() => null);
|
|
1108
|
+
const text = await target.textContent().catch(() => null);
|
|
1109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1110
|
+
const tag = await target.evaluate((el) => el.tagName.toLowerCase()).catch(() => null);
|
|
1111
|
+
return { status: 'ok', found: true, count, nth, query_type, query, tag, text: text?.trim() || null, bbox };
|
|
1112
|
+
}
|
|
1113
|
+
catch (e) {
|
|
1114
|
+
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
1115
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
1116
|
+
throw e;
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
// ---------------------------------------------------------------------------
|
|
1120
|
+
// R08-R16: Upload URL — fetch remote URL and upload as file input
|
|
1121
|
+
// ---------------------------------------------------------------------------
|
|
1122
|
+
server.post('/api/v1/sessions/:id/upload_url', async (req, reply) => {
|
|
1123
|
+
const s = resolve(req.params.id, reply);
|
|
1124
|
+
if (!s)
|
|
1125
|
+
return;
|
|
1126
|
+
const { url, filename: filenameHint, mime_type, purpose, operator } = req.body;
|
|
1127
|
+
if (!url)
|
|
1128
|
+
return reply.code(400).send({ error: 'url is required' });
|
|
1129
|
+
const cssSelector = resolveTarget(req.body, reply, s.id);
|
|
1130
|
+
if (!cssSelector)
|
|
1131
|
+
return;
|
|
1132
|
+
let resp;
|
|
1133
|
+
try {
|
|
1134
|
+
resp = await fetch(url);
|
|
1135
|
+
}
|
|
1136
|
+
catch (e) {
|
|
1137
|
+
return reply.code(422).send({ error: 'fetch_failed', message: e.message, url });
|
|
1138
|
+
}
|
|
1139
|
+
if (!resp.ok)
|
|
1140
|
+
return reply.code(422).send({ error: 'fetch_failed', http_status: resp.status, url });
|
|
1141
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
1142
|
+
const filename = filenameHint ?? url.split('/').pop()?.split('?')[0] ?? 'file';
|
|
1143
|
+
const ext = path_1.default.extname(filename) || '.bin';
|
|
1144
|
+
const tmpPath = path_1.default.join(os_1.default.tmpdir(), `agentmb_${crypto_1.default.randomBytes(4).toString('hex')}${ext}`);
|
|
1145
|
+
try {
|
|
1146
|
+
await fs_1.default.promises.writeFile(tmpPath, buf);
|
|
1147
|
+
const b64 = buf.toString('base64');
|
|
1148
|
+
const result = await Actions.uploadFile(s.page, cssSelector, b64, filename, mime_type, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1149
|
+
return { ...result, url, fetched_bytes: buf.length };
|
|
1150
|
+
}
|
|
1151
|
+
catch (e) {
|
|
1152
|
+
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
1153
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
944
1154
|
throw e;
|
|
945
1155
|
}
|
|
1156
|
+
finally {
|
|
1157
|
+
fs_1.default.promises.unlink(tmpPath).catch(() => { });
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
// ---------------------------------------------------------------------------
|
|
1161
|
+
// R08-R18: run_steps — batch action dispatcher
|
|
1162
|
+
// Supported actions: navigate, click, fill, type, press, hover, scroll,
|
|
1163
|
+
// wait_for_selector, wait_text, screenshot, eval
|
|
1164
|
+
// r08-c07: resolveRefIdForStep — throws (instead of reply) for use inside step loops
|
|
1165
|
+
// ---------------------------------------------------------------------------
|
|
1166
|
+
function resolveRefIdForStep(params, sessionId) {
|
|
1167
|
+
if (params.ref_id) {
|
|
1168
|
+
const bm = server.browserManager;
|
|
1169
|
+
if (!bm)
|
|
1170
|
+
throw new Error('ref_id resolution requires BrowserManager');
|
|
1171
|
+
const colonIdx = params.ref_id.lastIndexOf(':');
|
|
1172
|
+
if (colonIdx === -1)
|
|
1173
|
+
throw new Error(`Invalid ref_id format: "${params.ref_id}"; expected "snap_XXXXXX:eN"`);
|
|
1174
|
+
const snapshotId = params.ref_id.slice(0, colonIdx);
|
|
1175
|
+
const eid = params.ref_id.slice(colonIdx + 1);
|
|
1176
|
+
const snapshot = bm.getSnapshot(sessionId, snapshotId);
|
|
1177
|
+
if (!snapshot)
|
|
1178
|
+
throw new Error(`stale_ref: snapshot "${snapshotId}" not found or expired; call snapshot_map again`);
|
|
1179
|
+
const currentRev = bm.getPageRev(sessionId);
|
|
1180
|
+
if (snapshot.page_rev !== currentRev)
|
|
1181
|
+
throw new Error(`stale_ref: page changed (snapshot_rev=${snapshot.page_rev}, current=${currentRev}); call snapshot_map again`);
|
|
1182
|
+
return `[data-agentmb-eid="${eid}"]`;
|
|
1183
|
+
}
|
|
1184
|
+
if (params.element_id)
|
|
1185
|
+
return `[data-agentmb-eid="${params.element_id}"]`;
|
|
1186
|
+
return params.selector;
|
|
1187
|
+
}
|
|
1188
|
+
server.post('/api/v1/sessions/:id/run_steps', async (req, reply) => {
|
|
1189
|
+
const s = resolve(req.params.id, reply);
|
|
1190
|
+
if (!s)
|
|
1191
|
+
return;
|
|
1192
|
+
const { steps, stop_on_error = true, purpose, operator } = req.body;
|
|
1193
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
1194
|
+
return reply.code(400).send({ error: 'steps must be a non-empty array' });
|
|
1195
|
+
if (steps.length > 100)
|
|
1196
|
+
return reply.code(400).send({ error: 'steps must not exceed 100' });
|
|
1197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1198
|
+
const results = [];
|
|
1199
|
+
const op = inferOperator(req, s, operator);
|
|
1200
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1201
|
+
const step = steps[i];
|
|
1202
|
+
const { action, params = {} } = step;
|
|
1203
|
+
const stepPurpose = params.purpose ?? purpose;
|
|
1204
|
+
try {
|
|
1205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1206
|
+
let result;
|
|
1207
|
+
switch (action) {
|
|
1208
|
+
case 'navigate':
|
|
1209
|
+
result = await Actions.navigate(s.page, params.url, params.wait_until ?? 'load', getLogger(), s.id, stepPurpose, op);
|
|
1210
|
+
break;
|
|
1211
|
+
case 'click': {
|
|
1212
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1213
|
+
throw new Error('click requires selector, element_id, or ref_id');
|
|
1214
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1215
|
+
result = await Actions.click(s.page, sel, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
case 'fill': {
|
|
1219
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1220
|
+
throw new Error('fill requires selector, element_id, or ref_id');
|
|
1221
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1222
|
+
result = await Actions.fill(s.page, sel, params.value ?? '', getLogger(), s.id, stepPurpose, op);
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
case 'type': {
|
|
1226
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1227
|
+
throw new Error('type requires selector, element_id, or ref_id');
|
|
1228
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1229
|
+
result = await Actions.typeText(s.page, sel, params.text ?? '', params.delay_ms ?? 0, getLogger(), s.id, stepPurpose, op);
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
case 'press': {
|
|
1233
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1234
|
+
throw new Error('press requires selector, element_id, or ref_id');
|
|
1235
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1236
|
+
result = await Actions.press(s.page, sel, params.key ?? '', getLogger(), s.id, stepPurpose, op);
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
case 'hover': {
|
|
1240
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1241
|
+
throw new Error('hover requires selector, element_id, or ref_id');
|
|
1242
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1243
|
+
result = await Actions.hover(s.page, sel, getLogger(), s.id, stepPurpose, op);
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
case 'scroll': {
|
|
1247
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1248
|
+
throw new Error('scroll requires selector, element_id, or ref_id');
|
|
1249
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1250
|
+
result = await Actions.scroll(s.page, sel, { delta_x: params.delta_x ?? 0, delta_y: params.delta_y ?? 300 }, getLogger(), s.id, stepPurpose, op);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
case 'wait_for_selector':
|
|
1254
|
+
result = await Actions.waitForSelector(s.page, params.selector, params.state ?? 'visible', params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1255
|
+
break;
|
|
1256
|
+
case 'wait_text':
|
|
1257
|
+
result = await Actions.waitForText(s.page, params.text, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1258
|
+
break;
|
|
1259
|
+
case 'screenshot':
|
|
1260
|
+
result = await Actions.screenshot(s.page, params.format ?? 'png', params.full_page ?? false, getLogger(), s.id, stepPurpose, op);
|
|
1261
|
+
break;
|
|
1262
|
+
case 'eval':
|
|
1263
|
+
result = await Actions.evaluate(s.page, params.expression, getLogger(), s.id, stepPurpose, op);
|
|
1264
|
+
break;
|
|
1265
|
+
default:
|
|
1266
|
+
throw new Error(`unsupported action: ${action}`);
|
|
1267
|
+
}
|
|
1268
|
+
results.push({ step: i + 1, action, result });
|
|
1269
|
+
}
|
|
1270
|
+
catch (e) {
|
|
1271
|
+
const errPayload = e instanceof actions_1.ActionDiagnosticsError
|
|
1272
|
+
? enrichDiag(e.diagnostics)
|
|
1273
|
+
: { error: e.message ?? String(e) };
|
|
1274
|
+
results.push({ step: i + 1, action, error: errPayload });
|
|
1275
|
+
if (stop_on_error)
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const failed = results.filter(r => r.error);
|
|
1280
|
+
return {
|
|
1281
|
+
status: failed.length === 0 ? 'ok' : (results.filter(r => r.result).length > 0 ? 'partial' : 'failed'),
|
|
1282
|
+
total_steps: steps.length,
|
|
1283
|
+
completed_steps: results.filter(r => r.result).length,
|
|
1284
|
+
failed_steps: failed.length,
|
|
1285
|
+
results,
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
// R08-R12: Snapshot Ref 强化 — GET page_rev endpoint
|
|
1290
|
+
// Lets clients cheaply check if the page has changed since last snapshot.
|
|
1291
|
+
// ---------------------------------------------------------------------------
|
|
1292
|
+
server.get('/api/v1/sessions/:id/page_rev', async (req, reply) => {
|
|
1293
|
+
const s = resolve(req.params.id, reply);
|
|
1294
|
+
if (!s)
|
|
1295
|
+
return;
|
|
1296
|
+
const bm = server.browserManager;
|
|
1297
|
+
const page_rev = bm?.getPageRev(s.id) ?? 0;
|
|
1298
|
+
return { status: 'ok', session_id: req.params.id, page_rev, url: s.page.url() };
|
|
946
1299
|
});
|
|
947
1300
|
}
|
|
948
1301
|
//# sourceMappingURL=actions.js.map
|