agentmb 0.1.1 → 0.3.2
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 +1002 -153
- package/dist/browser/actions.d.ts +25 -2
- package/dist/browser/actions.d.ts.map +1 -1
- package/dist/browser/actions.js +122 -22
- package/dist/browser/actions.js.map +1 -1
- package/dist/browser/manager.d.ts +35 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/manager.js +244 -16
- package/dist/browser/manager.js.map +1 -1
- package/dist/cli/commands/actions.d.ts.map +1 -1
- package/dist/cli/commands/actions.js +342 -80
- 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 +76 -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 +516 -78
- package/dist/daemon/routes/actions.js.map +1 -1
- package/dist/daemon/routes/interaction.d.ts.map +1 -1
- package/dist/daemon/routes/interaction.js +10 -1
- package/dist/daemon/routes/interaction.js.map +1 -1
- package/dist/daemon/routes/sessions.d.ts.map +1 -1
- package/dist/daemon/routes/sessions.js +314 -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 +4 -2
- package/skills/agentmb/SKILL.md +541 -0
- package/skills/agentmb/references/authentication.md +180 -0
- package/skills/agentmb/references/browser-modes.md +167 -0
- package/skills/agentmb/references/commands.md +231 -0
- package/skills/agentmb/references/locator-modes.md +254 -0
- package/skills/agentmb/references/session-management.md +260 -0
|
@@ -38,10 +38,48 @@ 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"));
|
|
44
|
+
const url_1 = require("url");
|
|
41
45
|
require("../types"); // T11: Fastify type augmentation
|
|
42
46
|
const Actions = __importStar(require("../../browser/actions"));
|
|
43
47
|
const actions_1 = require("../../browser/actions");
|
|
44
48
|
const engine_1 = require("../../policy/engine");
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// R09-C04-T12: Sensitive domain detection
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const SENSITIVE_DOMAIN_PATTERNS = [
|
|
53
|
+
{ re: /bank|banking|finance|financial|payment|paypal|stripe/i, category: 'financial' },
|
|
54
|
+
{ re: /medical|health|hospital|clinic|pharma/i, category: 'medical' },
|
|
55
|
+
{ re: /gambling|casino|betting|poker/i, category: 'gambling' },
|
|
56
|
+
{ re: /adult|porn|nsfw|xxx/i, category: 'adult' },
|
|
57
|
+
{ re: /crypto|bitcoin|wallet|exchange/i, category: 'crypto' },
|
|
58
|
+
];
|
|
59
|
+
// Append extra patterns from env (comma-separated regexes)
|
|
60
|
+
const _envPatterns = process.env.AGENTMB_SENSITIVE_DOMAINS;
|
|
61
|
+
if (_envPatterns) {
|
|
62
|
+
for (const p of _envPatterns.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
63
|
+
try {
|
|
64
|
+
SENSITIVE_DOMAIN_PATTERNS.push({ re: new RegExp(p, 'i'), category: 'custom' });
|
|
65
|
+
}
|
|
66
|
+
catch { /* ignore invalid regex */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function detectSensitiveDomain(url) {
|
|
70
|
+
let domain = '';
|
|
71
|
+
try {
|
|
72
|
+
domain = new URL(url).hostname;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { sensitive: false };
|
|
76
|
+
}
|
|
77
|
+
for (const { re, category } of SENSITIVE_DOMAIN_PATTERNS) {
|
|
78
|
+
if (re.test(domain))
|
|
79
|
+
return { sensitive: true, category, domain };
|
|
80
|
+
}
|
|
81
|
+
return { sensitive: false };
|
|
82
|
+
}
|
|
45
83
|
/** Structured error when a frame selector matches no frame in the page. */
|
|
46
84
|
class FrameResolutionError extends Error {
|
|
47
85
|
selector;
|
|
@@ -146,6 +184,59 @@ async function applyPolicy(server, sessionId, domain, action, opts, reply) {
|
|
|
146
184
|
}
|
|
147
185
|
return true;
|
|
148
186
|
}
|
|
187
|
+
function pfRange(field, value, min, max) {
|
|
188
|
+
if (value === undefined)
|
|
189
|
+
return null;
|
|
190
|
+
return value < min || value > max ? { field, constraint: `must be ${min}–${max}`, value } : null;
|
|
191
|
+
}
|
|
192
|
+
function pfMaxLen(field, value, max) {
|
|
193
|
+
if (!value)
|
|
194
|
+
return null;
|
|
195
|
+
return value.length > max ? { field, constraint: `max length ${max} chars`, value: value.length } : null;
|
|
196
|
+
}
|
|
197
|
+
/** Run an ordered list of checks; sends 400 + returns false on first violation. */
|
|
198
|
+
function preflight(checks, reply) {
|
|
199
|
+
const v = checks.find(Boolean);
|
|
200
|
+
if (v) {
|
|
201
|
+
reply.code(400).send({ error: 'preflight_failed', field: v.field, constraint: v.constraint, value: v.value });
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
async function applyStabilityPre(page, opts) {
|
|
207
|
+
if (!opts)
|
|
208
|
+
return;
|
|
209
|
+
if (opts.wait_before_ms)
|
|
210
|
+
await new Promise(r => setTimeout(r, opts.wait_before_ms));
|
|
211
|
+
if (opts.wait_dom_stable_ms) {
|
|
212
|
+
try {
|
|
213
|
+
await page.waitForFunction('document.readyState === "complete"', undefined, { timeout: opts.wait_dom_stable_ms });
|
|
214
|
+
}
|
|
215
|
+
catch { /* timeout is acceptable */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function applyStabilityPost(page, opts) {
|
|
219
|
+
if (!opts)
|
|
220
|
+
return;
|
|
221
|
+
if (opts.wait_after_ms)
|
|
222
|
+
await new Promise(r => setTimeout(r, opts.wait_after_ms));
|
|
223
|
+
}
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// R08-R13: Error recovery hints — enrich 422 ActionDiagnostics with hint
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
function enrichDiag(diag) {
|
|
228
|
+
const msg = diag.error.toLowerCase();
|
|
229
|
+
let recovery_hint;
|
|
230
|
+
if (msg.includes('timeout') || msg.includes('waiting for'))
|
|
231
|
+
recovery_hint = 'Increase timeout_ms or add stability.wait_before_ms; ensure element is visible before acting';
|
|
232
|
+
else if (msg.includes('target closed') || msg.includes('execution context') || msg.includes('detached'))
|
|
233
|
+
recovery_hint = 'Page may have navigated or element was removed; re-navigate or re-snapshot';
|
|
234
|
+
else if (msg.includes('not found') || msg.includes('no element') || msg.includes('failed to find'))
|
|
235
|
+
recovery_hint = 'Check selector; use snapshot_map to verify element exists on current page';
|
|
236
|
+
else if (msg.includes('intercept') || msg.includes('overlap') || msg.includes('obscur'))
|
|
237
|
+
recovery_hint = 'Element may be covered by overlay; try executor=auto_fallback or scroll into view first';
|
|
238
|
+
return recovery_hint ? { ...diag, recovery_hint } : diag;
|
|
239
|
+
}
|
|
149
240
|
function registerActionRoutes(server, registry) {
|
|
150
241
|
function getLogger() {
|
|
151
242
|
return server.auditLogger;
|
|
@@ -172,7 +263,11 @@ function registerActionRoutes(server, registry) {
|
|
|
172
263
|
const eid = input.ref_id.slice(colonIdx + 1);
|
|
173
264
|
const snapshot = bm.getSnapshot(sessionId, snapshotId);
|
|
174
265
|
if (!snapshot) {
|
|
175
|
-
reply.code(409).send({
|
|
266
|
+
reply.code(409).send({
|
|
267
|
+
error: 'stale_ref', ref_id: input.ref_id,
|
|
268
|
+
message: 'Snapshot not found or expired; call snapshot_map again',
|
|
269
|
+
suggestions: ['call snapshot_map to get fresh ref_ids', 'use selector or element_id as fallback'],
|
|
270
|
+
});
|
|
176
271
|
return null;
|
|
177
272
|
}
|
|
178
273
|
const currentRev = bm.getPageRev(sessionId);
|
|
@@ -183,6 +278,7 @@ function registerActionRoutes(server, registry) {
|
|
|
183
278
|
snapshot_page_rev: snapshot.page_rev,
|
|
184
279
|
current_page_rev: currentRev,
|
|
185
280
|
message: 'Page has changed since snapshot was taken; call snapshot_map again',
|
|
281
|
+
suggestions: ['call snapshot_map to get fresh ref_ids', 'if page is stable, the navigation event incremented page_rev — wait and resnapshot'],
|
|
186
282
|
});
|
|
187
283
|
return null;
|
|
188
284
|
}
|
|
@@ -213,24 +309,78 @@ function registerActionRoutes(server, registry) {
|
|
|
213
309
|
}
|
|
214
310
|
return result;
|
|
215
311
|
}
|
|
312
|
+
/** Resolve a live session with optional page_id override (R09-C03 multi-page targeting). */
|
|
313
|
+
function resolveWithPage(id, pageId, reply) {
|
|
314
|
+
const s = resolve(id, reply);
|
|
315
|
+
if (!s)
|
|
316
|
+
return null;
|
|
317
|
+
if (!pageId)
|
|
318
|
+
return s;
|
|
319
|
+
const bm = server.browserManager;
|
|
320
|
+
const page = bm?.getPageById(id, pageId);
|
|
321
|
+
if (!page) {
|
|
322
|
+
reply.code(404).send({ error: `Page ${pageId} not found in session ${id}. Use GET /api/v1/sessions/${id}/pages to list available pages.` });
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return { ...s, page };
|
|
326
|
+
}
|
|
216
327
|
// POST /api/v1/sessions/:id/navigate
|
|
217
328
|
server.post('/api/v1/sessions/:id/navigate', async (req, reply) => {
|
|
218
|
-
const s =
|
|
329
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
219
330
|
if (!s)
|
|
220
331
|
return;
|
|
221
|
-
const { url, wait_until = 'load', purpose, operator, sensitive, retry } = req.body;
|
|
332
|
+
const { url, wait_until = 'load', timeout_ms = 30_000, purpose, operator, sensitive, retry } = req.body;
|
|
333
|
+
// R09-C07-P0: validate timeout_ms to prevent abuse (0 disables, max 60 s)
|
|
334
|
+
if (!preflight([pfRange('timeout_ms', timeout_ms, 0, 60_000)], reply))
|
|
335
|
+
return;
|
|
336
|
+
// R09-C06-P1: file:// URL guard — require allow_dirs whitelist
|
|
337
|
+
// R09-C07-P0: use fs.realpath to resolve symlinks before whitelist check (symlink traversal fix)
|
|
338
|
+
if (url.startsWith('file://')) {
|
|
339
|
+
const bm = server.browserManager;
|
|
340
|
+
const allowDirs = bm?.getAllowDirs(req.params.id) ?? [];
|
|
341
|
+
if (allowDirs.length === 0) {
|
|
342
|
+
return reply.code(403).send({ error: 'file:// navigation requires allow_dirs on the session. Set allow_dirs when creating session.' });
|
|
343
|
+
}
|
|
344
|
+
let filePath;
|
|
345
|
+
try {
|
|
346
|
+
filePath = (0, url_1.fileURLToPath)(url);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return reply.code(400).send({ error: 'Invalid file:// URL format' });
|
|
350
|
+
}
|
|
351
|
+
let abs;
|
|
352
|
+
try {
|
|
353
|
+
abs = await fs_1.default.promises.realpath(filePath);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return reply.code(404).send({ error: `file:// path does not exist: ${filePath}` });
|
|
357
|
+
}
|
|
358
|
+
const allowed = allowDirs.some(d => abs === d || abs.startsWith(d + path_1.default.sep));
|
|
359
|
+
if (!allowed) {
|
|
360
|
+
return reply.code(403).send({ error: `file:// path ${abs} is not within allowed directories.` });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
222
363
|
const domain = (0, engine_1.extractDomain)(url);
|
|
223
364
|
if (!await applyPolicy(server, req.params.id, domain, 'navigate', { sensitive, retry }, reply))
|
|
224
365
|
return;
|
|
225
|
-
|
|
366
|
+
const navResult = await Actions.navigate(s.page, url, wait_until, getLogger(), s.id, purpose, inferOperator(req, s, operator), timeout_ms);
|
|
367
|
+
const sensitiveInfo = detectSensitiveDomain(url);
|
|
368
|
+
const sensitive_warning = sensitiveInfo.sensitive
|
|
369
|
+
? { domain: sensitiveInfo.domain, category: sensitiveInfo.category, message: `Navigating to potentially sensitive domain: ${sensitiveInfo.domain}` }
|
|
370
|
+
: undefined;
|
|
371
|
+
return { ...navResult, ...(sensitive_warning ? { sensitive_warning } : {}) };
|
|
226
372
|
});
|
|
227
373
|
// POST /api/v1/sessions/:id/click
|
|
228
|
-
//
|
|
374
|
+
// R08-R06: executor='auto_fallback' automatically retries via bbox coords on DOM failure
|
|
375
|
+
// R08-R02: stability.wait_before_ms / wait_after_ms / wait_dom_stable_ms
|
|
376
|
+
// R08-R09: preflight validates timeout_ms range
|
|
229
377
|
server.post('/api/v1/sessions/:id/click', async (req, reply) => {
|
|
230
|
-
const s =
|
|
378
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
231
379
|
if (!s)
|
|
232
380
|
return;
|
|
233
|
-
const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y } = req.body;
|
|
381
|
+
const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y, executor = 'strict', stability } = req.body;
|
|
382
|
+
if (!preflight([pfRange('timeout_ms', timeout_ms, 50, 60000)], reply))
|
|
383
|
+
return;
|
|
234
384
|
const selector = resolveTarget(req.body, reply, s.id);
|
|
235
385
|
if (!selector)
|
|
236
386
|
return;
|
|
@@ -239,32 +389,63 @@ function registerActionRoutes(server, registry) {
|
|
|
239
389
|
const target = resolveOrReply(s.page, frame, reply);
|
|
240
390
|
if (!target)
|
|
241
391
|
return;
|
|
392
|
+
await applyStabilityPre(s.page, stability);
|
|
242
393
|
try {
|
|
243
|
-
|
|
394
|
+
const result = await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
395
|
+
await applyStabilityPost(s.page, stability);
|
|
396
|
+
return { ...result, executed_via: 'high_level' };
|
|
244
397
|
}
|
|
245
398
|
catch (domErr) {
|
|
246
|
-
//
|
|
247
|
-
if (
|
|
399
|
+
// auto_fallback: resolve element bbox and retry via mouse.click()
|
|
400
|
+
if (executor === 'auto_fallback') {
|
|
248
401
|
try {
|
|
249
|
-
await
|
|
250
|
-
|
|
251
|
-
|
|
402
|
+
const bbox = await target.locator(selector).boundingBox();
|
|
403
|
+
if (bbox) {
|
|
404
|
+
let cx = Math.round(bbox.x + bbox.width / 2);
|
|
405
|
+
let cy = Math.round(bbox.y + bbox.height / 2);
|
|
406
|
+
// Frame offset compensation: when target is a Frame (not the Page),
|
|
407
|
+
// boundingBox() coords are relative to the frame viewport; add the
|
|
408
|
+
// frame element's page-level offset so mouse.click lands correctly.
|
|
409
|
+
if (frame && target !== s.page) {
|
|
410
|
+
try {
|
|
411
|
+
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}; })()');
|
|
412
|
+
cx += Math.round(frameRect.x);
|
|
413
|
+
cy += Math.round(frameRect.y);
|
|
414
|
+
}
|
|
415
|
+
catch { /* best-effort; proceed with uncompensated coords */ }
|
|
416
|
+
}
|
|
417
|
+
await s.page.mouse.click(cx, cy);
|
|
418
|
+
await applyStabilityPost(s.page, stability);
|
|
419
|
+
return { status: 'ok', selector, executed_via: 'low_level', fallback_x: cx, fallback_y: cy, duration_ms: 0 };
|
|
420
|
+
}
|
|
252
421
|
}
|
|
253
|
-
catch
|
|
254
|
-
|
|
422
|
+
catch { /* fallback also failed — fall through to original error */ }
|
|
423
|
+
}
|
|
424
|
+
// explicit fallback coordinates (legacy T21 path)
|
|
425
|
+
if (fallback_x !== undefined && fallback_y !== undefined) {
|
|
426
|
+
try {
|
|
427
|
+
await s.page.mouse.click(fallback_x, fallback_y);
|
|
428
|
+
return { status: 'ok', selector, executed_via: 'low_level', track: 'coords', fallback_x, fallback_y, duration_ms: 0 };
|
|
255
429
|
}
|
|
430
|
+
catch { /* both tracks failed */ }
|
|
431
|
+
}
|
|
432
|
+
if (domErr instanceof Actions.ActionDiagnosticsError) {
|
|
433
|
+
const diag = enrichDiag({ ...domErr.diagnostics, suggested_fallback: 'retry with executor="auto_fallback" to attempt coordinates-based click' });
|
|
434
|
+
return reply.code(422).send(diag);
|
|
256
435
|
}
|
|
257
|
-
if (domErr instanceof Actions.ActionDiagnosticsError)
|
|
258
|
-
return reply.code(422).send(domErr.diagnostics);
|
|
259
436
|
throw domErr;
|
|
260
437
|
}
|
|
261
438
|
});
|
|
262
439
|
// POST /api/v1/sessions/:id/fill
|
|
440
|
+
// R08-R01: fill_strategy='type' uses pressSequentially with char_delay_ms (humanization)
|
|
441
|
+
// R08-R02: stability options; R08-R09: preflight value length
|
|
263
442
|
server.post('/api/v1/sessions/:id/fill', async (req, reply) => {
|
|
264
|
-
const s =
|
|
443
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
265
444
|
if (!s)
|
|
266
445
|
return;
|
|
267
|
-
const { value, frame, purpose, operator, sensitive, retry } = req.body;
|
|
446
|
+
const { value, frame, purpose, operator, sensitive, retry, stability, fill_strategy = 'instant', char_delay_ms = 0 } = req.body;
|
|
447
|
+
if (!preflight([pfMaxLen('value', value, 100_000)], reply))
|
|
448
|
+
return;
|
|
268
449
|
const selector = resolveTarget(req.body, reply, s.id);
|
|
269
450
|
if (!selector)
|
|
270
451
|
return;
|
|
@@ -273,11 +454,16 @@ function registerActionRoutes(server, registry) {
|
|
|
273
454
|
const target = resolveOrReply(s.page, frame, reply);
|
|
274
455
|
if (!target)
|
|
275
456
|
return;
|
|
276
|
-
|
|
457
|
+
await applyStabilityPre(s.page, stability);
|
|
458
|
+
const result = fill_strategy === 'type'
|
|
459
|
+
? await Actions.typeText(target, selector, value, char_delay_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator))
|
|
460
|
+
: await Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
461
|
+
await applyStabilityPost(s.page, stability);
|
|
462
|
+
return result;
|
|
277
463
|
});
|
|
278
464
|
// POST /api/v1/sessions/:id/eval
|
|
279
465
|
server.post('/api/v1/sessions/:id/eval', async (req, reply) => {
|
|
280
|
-
const s =
|
|
466
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
281
467
|
if (!s)
|
|
282
468
|
return;
|
|
283
469
|
const { expression, frame, purpose, operator, sensitive, retry } = req.body;
|
|
@@ -291,7 +477,7 @@ function registerActionRoutes(server, registry) {
|
|
|
291
477
|
}
|
|
292
478
|
catch (e) {
|
|
293
479
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
294
|
-
return reply.code(422).send(e.diagnostics);
|
|
480
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
295
481
|
throw e;
|
|
296
482
|
}
|
|
297
483
|
});
|
|
@@ -311,13 +497,13 @@ function registerActionRoutes(server, registry) {
|
|
|
311
497
|
}
|
|
312
498
|
catch (e) {
|
|
313
499
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
314
|
-
return reply.code(422).send(e.diagnostics);
|
|
500
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
315
501
|
throw e;
|
|
316
502
|
}
|
|
317
503
|
});
|
|
318
504
|
// POST /api/v1/sessions/:id/screenshot
|
|
319
505
|
server.post('/api/v1/sessions/:id/screenshot', async (req, reply) => {
|
|
320
|
-
const s =
|
|
506
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
321
507
|
if (!s)
|
|
322
508
|
return;
|
|
323
509
|
const { format = 'png', full_page = false, purpose, operator } = req.body ?? {};
|
|
@@ -326,13 +512,13 @@ function registerActionRoutes(server, registry) {
|
|
|
326
512
|
}
|
|
327
513
|
catch (e) {
|
|
328
514
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
329
|
-
return reply.code(422).send(e.diagnostics);
|
|
515
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
330
516
|
throw e;
|
|
331
517
|
}
|
|
332
518
|
});
|
|
333
519
|
// POST /api/v1/sessions/:id/type
|
|
334
520
|
server.post('/api/v1/sessions/:id/type', async (req, reply) => {
|
|
335
|
-
const s =
|
|
521
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
336
522
|
if (!s)
|
|
337
523
|
return;
|
|
338
524
|
const { text, delay_ms = 0, frame, purpose, operator, sensitive, retry } = req.body;
|
|
@@ -350,13 +536,13 @@ function registerActionRoutes(server, registry) {
|
|
|
350
536
|
}
|
|
351
537
|
catch (e) {
|
|
352
538
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
353
|
-
return reply.code(422).send(e.diagnostics);
|
|
539
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
354
540
|
throw e;
|
|
355
541
|
}
|
|
356
542
|
});
|
|
357
543
|
// POST /api/v1/sessions/:id/press
|
|
358
544
|
server.post('/api/v1/sessions/:id/press', async (req, reply) => {
|
|
359
|
-
const s =
|
|
545
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
360
546
|
if (!s)
|
|
361
547
|
return;
|
|
362
548
|
const { key, frame, purpose, operator, sensitive, retry } = req.body;
|
|
@@ -373,7 +559,7 @@ function registerActionRoutes(server, registry) {
|
|
|
373
559
|
}
|
|
374
560
|
catch (e) {
|
|
375
561
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
376
|
-
return reply.code(422).send(e.diagnostics);
|
|
562
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
377
563
|
throw e;
|
|
378
564
|
}
|
|
379
565
|
});
|
|
@@ -391,7 +577,7 @@ function registerActionRoutes(server, registry) {
|
|
|
391
577
|
}
|
|
392
578
|
catch (e) {
|
|
393
579
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
394
|
-
return reply.code(422).send(e.diagnostics);
|
|
580
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
395
581
|
throw e;
|
|
396
582
|
}
|
|
397
583
|
});
|
|
@@ -412,7 +598,7 @@ function registerActionRoutes(server, registry) {
|
|
|
412
598
|
}
|
|
413
599
|
catch (e) {
|
|
414
600
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
415
|
-
return reply.code(422).send(e.diagnostics);
|
|
601
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
416
602
|
throw e;
|
|
417
603
|
}
|
|
418
604
|
});
|
|
@@ -430,7 +616,7 @@ function registerActionRoutes(server, registry) {
|
|
|
430
616
|
}
|
|
431
617
|
catch (e) {
|
|
432
618
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
433
|
-
return reply.code(422).send(e.diagnostics);
|
|
619
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
434
620
|
throw e;
|
|
435
621
|
}
|
|
436
622
|
});
|
|
@@ -445,7 +631,7 @@ function registerActionRoutes(server, registry) {
|
|
|
445
631
|
}
|
|
446
632
|
catch (e) {
|
|
447
633
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
448
|
-
return reply.code(422).send(e.diagnostics);
|
|
634
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
449
635
|
throw e;
|
|
450
636
|
}
|
|
451
637
|
});
|
|
@@ -461,7 +647,7 @@ function registerActionRoutes(server, registry) {
|
|
|
461
647
|
}
|
|
462
648
|
catch (e) {
|
|
463
649
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
464
|
-
return reply.code(422).send(e.diagnostics);
|
|
650
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
465
651
|
throw e;
|
|
466
652
|
}
|
|
467
653
|
});
|
|
@@ -481,22 +667,35 @@ function registerActionRoutes(server, registry) {
|
|
|
481
667
|
}
|
|
482
668
|
catch (e) {
|
|
483
669
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
484
|
-
return reply.code(422).send(e.diagnostics);
|
|
670
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
485
671
|
throw e;
|
|
486
672
|
}
|
|
487
673
|
});
|
|
488
674
|
// POST /api/v1/sessions/:id/download
|
|
675
|
+
// T07: guard on accept_downloads; T08: element_id / ref_id support
|
|
489
676
|
server.post('/api/v1/sessions/:id/download', async (req, reply) => {
|
|
490
677
|
const s = resolve(req.params.id, reply);
|
|
491
678
|
if (!s)
|
|
492
679
|
return;
|
|
493
|
-
|
|
680
|
+
// T07: check accept_downloads is enabled
|
|
681
|
+
const bm = server.browserManager;
|
|
682
|
+
if (bm && !bm.getAcceptDownloads(s.id)) {
|
|
683
|
+
return reply.code(422).send({
|
|
684
|
+
error: 'download_not_enabled',
|
|
685
|
+
message: 'Downloads are disabled for this session. Create the session with accept_downloads=true (CLI: agentmb session new --accept-downloads).',
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const { timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
|
|
689
|
+
// T08: resolve selector/element_id/ref_id to CSS selector
|
|
690
|
+
const selector = resolveTarget(req.body, reply, s.id);
|
|
691
|
+
if (!selector)
|
|
692
|
+
return;
|
|
494
693
|
try {
|
|
495
694
|
return await Actions.downloadFile(s.page, selector, timeout_ms, max_bytes, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
496
695
|
}
|
|
497
696
|
catch (e) {
|
|
498
697
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
499
|
-
return reply.code(422).send(e.diagnostics);
|
|
698
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
500
699
|
throw e;
|
|
501
700
|
}
|
|
502
701
|
});
|
|
@@ -513,16 +712,16 @@ function registerActionRoutes(server, registry) {
|
|
|
513
712
|
// R07-T01: element_map — scan page, assign stable element IDs
|
|
514
713
|
// ---------------------------------------------------------------------------
|
|
515
714
|
server.post('/api/v1/sessions/:id/element_map', async (req, reply) => {
|
|
516
|
-
const s =
|
|
715
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
517
716
|
if (!s)
|
|
518
717
|
return;
|
|
519
|
-
const { scope, limit = 500, purpose, operator } = req.body ?? {};
|
|
718
|
+
const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
|
|
520
719
|
try {
|
|
521
|
-
return await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
720
|
+
return await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
522
721
|
}
|
|
523
722
|
catch (e) {
|
|
524
723
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
525
|
-
return reply.code(422).send(e.diagnostics);
|
|
724
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
526
725
|
throw e;
|
|
527
726
|
}
|
|
528
727
|
});
|
|
@@ -547,7 +746,7 @@ function registerActionRoutes(server, registry) {
|
|
|
547
746
|
}
|
|
548
747
|
catch (e) {
|
|
549
748
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
550
|
-
return reply.code(422).send(e.diagnostics);
|
|
749
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
551
750
|
throw e;
|
|
552
751
|
}
|
|
553
752
|
});
|
|
@@ -572,7 +771,7 @@ function registerActionRoutes(server, registry) {
|
|
|
572
771
|
}
|
|
573
772
|
catch (e) {
|
|
574
773
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
575
|
-
return reply.code(422).send(e.diagnostics);
|
|
774
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
576
775
|
throw e;
|
|
577
776
|
}
|
|
578
777
|
});
|
|
@@ -589,7 +788,7 @@ function registerActionRoutes(server, registry) {
|
|
|
589
788
|
}
|
|
590
789
|
catch (e) {
|
|
591
790
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
592
|
-
return reply.code(422).send(e.diagnostics);
|
|
791
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
593
792
|
throw e;
|
|
594
793
|
}
|
|
595
794
|
});
|
|
@@ -611,7 +810,7 @@ function registerActionRoutes(server, registry) {
|
|
|
611
810
|
}
|
|
612
811
|
catch (e) {
|
|
613
812
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
614
|
-
return reply.code(422).send(e.diagnostics);
|
|
813
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
615
814
|
throw e;
|
|
616
815
|
}
|
|
617
816
|
});
|
|
@@ -630,7 +829,7 @@ function registerActionRoutes(server, registry) {
|
|
|
630
829
|
}
|
|
631
830
|
catch (e) {
|
|
632
831
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
633
|
-
return reply.code(422).send(e.diagnostics);
|
|
832
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
634
833
|
throw e;
|
|
635
834
|
}
|
|
636
835
|
});
|
|
@@ -649,7 +848,7 @@ function registerActionRoutes(server, registry) {
|
|
|
649
848
|
}
|
|
650
849
|
catch (e) {
|
|
651
850
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
652
|
-
return reply.code(422).send(e.diagnostics);
|
|
851
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
653
852
|
throw e;
|
|
654
853
|
}
|
|
655
854
|
});
|
|
@@ -668,12 +867,12 @@ function registerActionRoutes(server, registry) {
|
|
|
668
867
|
}
|
|
669
868
|
catch (e) {
|
|
670
869
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
671
|
-
return reply.code(422).send(e.diagnostics);
|
|
870
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
672
871
|
throw e;
|
|
673
872
|
}
|
|
674
873
|
});
|
|
675
874
|
server.post('/api/v1/sessions/:id/scroll', async (req, reply) => {
|
|
676
|
-
const s =
|
|
875
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
677
876
|
if (!s)
|
|
678
877
|
return;
|
|
679
878
|
const selector = resolveTarget(req.body, reply, s.id);
|
|
@@ -688,7 +887,7 @@ function registerActionRoutes(server, registry) {
|
|
|
688
887
|
}
|
|
689
888
|
catch (e) {
|
|
690
889
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
691
|
-
return reply.code(422).send(e.diagnostics);
|
|
890
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
692
891
|
throw e;
|
|
693
892
|
}
|
|
694
893
|
});
|
|
@@ -707,7 +906,7 @@ function registerActionRoutes(server, registry) {
|
|
|
707
906
|
}
|
|
708
907
|
catch (e) {
|
|
709
908
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
710
|
-
return reply.code(422).send(e.diagnostics);
|
|
909
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
711
910
|
throw e;
|
|
712
911
|
}
|
|
713
912
|
});
|
|
@@ -715,31 +914,48 @@ function registerActionRoutes(server, registry) {
|
|
|
715
914
|
const s = resolve(req.params.id, reply);
|
|
716
915
|
if (!s)
|
|
717
916
|
return;
|
|
718
|
-
const { source, source_element_id, target, target_element_id, purpose, operator } = req.body;
|
|
719
|
-
const src =
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
917
|
+
const { source, source_element_id, source_ref_id, target, target_element_id, target_ref_id, purpose, operator } = req.body;
|
|
918
|
+
const src = resolveTarget({ selector: source, element_id: source_element_id, ref_id: source_ref_id }, reply, s.id);
|
|
919
|
+
if (!src)
|
|
920
|
+
return;
|
|
921
|
+
const tgt = resolveTarget({ selector: target, element_id: target_element_id, ref_id: target_ref_id }, reply, s.id);
|
|
922
|
+
if (!tgt)
|
|
923
|
+
return;
|
|
723
924
|
try {
|
|
724
925
|
return await Actions.drag(s.page, src, tgt, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
725
926
|
}
|
|
726
927
|
catch (e) {
|
|
727
928
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
728
|
-
return reply.code(422).send(e.diagnostics);
|
|
929
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
729
930
|
throw e;
|
|
730
931
|
}
|
|
731
932
|
});
|
|
933
|
+
// R08-R05/R08: Ref->Box->Input — mouse_move accepts ref_id + steps for smooth trajectory
|
|
732
934
|
server.post('/api/v1/sessions/:id/mouse_move', async (req, reply) => {
|
|
733
935
|
const s = resolve(req.params.id, reply);
|
|
734
936
|
if (!s)
|
|
735
937
|
return;
|
|
736
|
-
const {
|
|
938
|
+
const { purpose, operator, steps } = req.body;
|
|
939
|
+
let { x, y } = req.body;
|
|
940
|
+
// Ref->Box->Input: if ref_id/element_id/selector provided, resolve to bbox center coords
|
|
941
|
+
if (req.body.ref_id || req.body.element_id || req.body.selector) {
|
|
942
|
+
const cssSelector = resolveTarget(req.body, reply, s.id);
|
|
943
|
+
if (!cssSelector)
|
|
944
|
+
return;
|
|
945
|
+
const bbox = await s.page.locator(cssSelector).boundingBox();
|
|
946
|
+
if (!bbox)
|
|
947
|
+
return reply.code(404).send({ error: 'Element not found or not visible for mouse_move', selector: cssSelector });
|
|
948
|
+
x = Math.round(bbox.x + bbox.width / 2);
|
|
949
|
+
y = Math.round(bbox.y + bbox.height / 2);
|
|
950
|
+
}
|
|
951
|
+
if (x === undefined || y === undefined)
|
|
952
|
+
return reply.code(400).send({ error: 'Either x+y coordinates or ref_id/element_id/selector is required' });
|
|
737
953
|
try {
|
|
738
|
-
return await Actions.mouseMove(s.page, x, y, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
954
|
+
return await Actions.mouseMove(s.page, x, y, steps ?? 1, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
739
955
|
}
|
|
740
956
|
catch (e) {
|
|
741
957
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
742
|
-
return reply.code(422).send(e.diagnostics);
|
|
958
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
743
959
|
throw e;
|
|
744
960
|
}
|
|
745
961
|
});
|
|
@@ -753,7 +969,7 @@ function registerActionRoutes(server, registry) {
|
|
|
753
969
|
}
|
|
754
970
|
catch (e) {
|
|
755
971
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
756
|
-
return reply.code(422).send(e.diagnostics);
|
|
972
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
757
973
|
throw e;
|
|
758
974
|
}
|
|
759
975
|
});
|
|
@@ -767,7 +983,7 @@ function registerActionRoutes(server, registry) {
|
|
|
767
983
|
}
|
|
768
984
|
catch (e) {
|
|
769
985
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
770
|
-
return reply.code(422).send(e.diagnostics);
|
|
986
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
771
987
|
throw e;
|
|
772
988
|
}
|
|
773
989
|
});
|
|
@@ -781,7 +997,7 @@ function registerActionRoutes(server, registry) {
|
|
|
781
997
|
}
|
|
782
998
|
catch (e) {
|
|
783
999
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
784
|
-
return reply.code(422).send(e.diagnostics);
|
|
1000
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
785
1001
|
throw e;
|
|
786
1002
|
}
|
|
787
1003
|
});
|
|
@@ -795,7 +1011,7 @@ function registerActionRoutes(server, registry) {
|
|
|
795
1011
|
}
|
|
796
1012
|
catch (e) {
|
|
797
1013
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
798
|
-
return reply.code(422).send(e.diagnostics);
|
|
1014
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
799
1015
|
throw e;
|
|
800
1016
|
}
|
|
801
1017
|
});
|
|
@@ -812,7 +1028,7 @@ function registerActionRoutes(server, registry) {
|
|
|
812
1028
|
}
|
|
813
1029
|
catch (e) {
|
|
814
1030
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
815
|
-
return reply.code(422).send(e.diagnostics);
|
|
1031
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
816
1032
|
throw e;
|
|
817
1033
|
}
|
|
818
1034
|
});
|
|
@@ -826,7 +1042,7 @@ function registerActionRoutes(server, registry) {
|
|
|
826
1042
|
}
|
|
827
1043
|
catch (e) {
|
|
828
1044
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
829
|
-
return reply.code(422).send(e.diagnostics);
|
|
1045
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
830
1046
|
throw e;
|
|
831
1047
|
}
|
|
832
1048
|
});
|
|
@@ -840,7 +1056,7 @@ function registerActionRoutes(server, registry) {
|
|
|
840
1056
|
}
|
|
841
1057
|
catch (e) {
|
|
842
1058
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
843
|
-
return reply.code(422).send(e.diagnostics);
|
|
1059
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
844
1060
|
throw e;
|
|
845
1061
|
}
|
|
846
1062
|
});
|
|
@@ -857,7 +1073,7 @@ function registerActionRoutes(server, registry) {
|
|
|
857
1073
|
}
|
|
858
1074
|
catch (e) {
|
|
859
1075
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
860
|
-
return reply.code(422).send(e.diagnostics);
|
|
1076
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
861
1077
|
throw e;
|
|
862
1078
|
}
|
|
863
1079
|
});
|
|
@@ -871,7 +1087,7 @@ function registerActionRoutes(server, registry) {
|
|
|
871
1087
|
}
|
|
872
1088
|
catch (e) {
|
|
873
1089
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
874
|
-
return reply.code(422).send(e.diagnostics);
|
|
1090
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
875
1091
|
throw e;
|
|
876
1092
|
}
|
|
877
1093
|
});
|
|
@@ -885,7 +1101,7 @@ function registerActionRoutes(server, registry) {
|
|
|
885
1101
|
}
|
|
886
1102
|
catch (e) {
|
|
887
1103
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
888
|
-
return reply.code(422).send(e.diagnostics);
|
|
1104
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
889
1105
|
throw e;
|
|
890
1106
|
}
|
|
891
1107
|
});
|
|
@@ -899,11 +1115,12 @@ function registerActionRoutes(server, registry) {
|
|
|
899
1115
|
const { purpose, operator, ...opts } = req.body ?? {};
|
|
900
1116
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
901
1117
|
try {
|
|
902
|
-
|
|
1118
|
+
const result = await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1119
|
+
return { ...result, session_id: s.id };
|
|
903
1120
|
}
|
|
904
1121
|
catch (e) {
|
|
905
1122
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
906
|
-
return reply.code(422).send(e.diagnostics);
|
|
1123
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
907
1124
|
throw e;
|
|
908
1125
|
}
|
|
909
1126
|
});
|
|
@@ -913,11 +1130,12 @@ function registerActionRoutes(server, registry) {
|
|
|
913
1130
|
return;
|
|
914
1131
|
const { purpose, operator, ...opts } = req.body;
|
|
915
1132
|
try {
|
|
916
|
-
|
|
1133
|
+
const result = await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1134
|
+
return { ...result, session_id: s.id };
|
|
917
1135
|
}
|
|
918
1136
|
catch (e) {
|
|
919
1137
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
920
|
-
return reply.code(422).send(e.diagnostics);
|
|
1138
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
921
1139
|
throw e;
|
|
922
1140
|
}
|
|
923
1141
|
});
|
|
@@ -925,13 +1143,13 @@ function registerActionRoutes(server, registry) {
|
|
|
925
1143
|
// R07-T13: snapshot_map — versioned element scan with page_rev tracking
|
|
926
1144
|
// ---------------------------------------------------------------------------
|
|
927
1145
|
server.post('/api/v1/sessions/:id/snapshot_map', async (req, reply) => {
|
|
928
|
-
const s =
|
|
1146
|
+
const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
|
|
929
1147
|
if (!s)
|
|
930
1148
|
return;
|
|
931
|
-
const { scope, limit = 500, purpose, operator } = req.body ?? {};
|
|
1149
|
+
const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
|
|
932
1150
|
const bm = server.browserManager;
|
|
933
1151
|
try {
|
|
934
|
-
const elemResult = await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1152
|
+
const elemResult = await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
935
1153
|
const snapshotId = 'snap_' + crypto_1.default.randomBytes(4).toString('hex');
|
|
936
1154
|
const pageRev = bm?.getPageRev(s.id) ?? 0;
|
|
937
1155
|
const elements = elemResult.elements.map((el) => ({ ...el, ref_id: `${snapshotId}:${el.element_id}` }));
|
|
@@ -940,9 +1158,229 @@ function registerActionRoutes(server, registry) {
|
|
|
940
1158
|
}
|
|
941
1159
|
catch (e) {
|
|
942
1160
|
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
943
|
-
return reply.code(422).send(e.diagnostics);
|
|
1161
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
1162
|
+
throw e;
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
// ---------------------------------------------------------------------------
|
|
1166
|
+
// R08-R10: Semantic element locator — find by role/text/label/placeholder/alt_text
|
|
1167
|
+
// ---------------------------------------------------------------------------
|
|
1168
|
+
server.post('/api/v1/sessions/:id/find', async (req, reply) => {
|
|
1169
|
+
const s = resolve(req.params.id, reply);
|
|
1170
|
+
if (!s)
|
|
1171
|
+
return;
|
|
1172
|
+
const { query_type, query, name, exact = false, nth = 0 } = req.body;
|
|
1173
|
+
try {
|
|
1174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1175
|
+
let locator;
|
|
1176
|
+
if (query_type === 'role')
|
|
1177
|
+
locator = s.page.getByRole(query, { name, exact });
|
|
1178
|
+
else if (query_type === 'text')
|
|
1179
|
+
locator = s.page.getByText(query, { exact });
|
|
1180
|
+
else if (query_type === 'label')
|
|
1181
|
+
locator = s.page.getByLabel(query, { exact });
|
|
1182
|
+
else if (query_type === 'placeholder')
|
|
1183
|
+
locator = s.page.getByPlaceholder(query, { exact });
|
|
1184
|
+
else if (query_type === 'alt_text')
|
|
1185
|
+
locator = s.page.getByAltText(query, { exact });
|
|
1186
|
+
else
|
|
1187
|
+
return reply.code(400).send({ error: `Unknown query_type: ${query_type}`, valid: ['role', 'text', 'label', 'placeholder', 'alt_text'] });
|
|
1188
|
+
const count = await locator.count();
|
|
1189
|
+
if (count === 0)
|
|
1190
|
+
return { status: 'ok', found: false, count: 0, nth: 0, query_type, query };
|
|
1191
|
+
const target = locator.nth(nth);
|
|
1192
|
+
const bbox = await target.boundingBox().catch(() => null);
|
|
1193
|
+
const text = await target.textContent().catch(() => null);
|
|
1194
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1195
|
+
const tag = await target.evaluate((el) => el.tagName.toLowerCase()).catch(() => null);
|
|
1196
|
+
return { status: 'ok', found: true, count, nth, query_type, query, tag, text: text?.trim() || null, bbox };
|
|
1197
|
+
}
|
|
1198
|
+
catch (e) {
|
|
1199
|
+
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
1200
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
1201
|
+
throw e;
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
// ---------------------------------------------------------------------------
|
|
1205
|
+
// R08-R16: Upload URL — fetch remote URL and upload as file input
|
|
1206
|
+
// ---------------------------------------------------------------------------
|
|
1207
|
+
server.post('/api/v1/sessions/:id/upload_url', async (req, reply) => {
|
|
1208
|
+
const s = resolve(req.params.id, reply);
|
|
1209
|
+
if (!s)
|
|
1210
|
+
return;
|
|
1211
|
+
const { url, filename: filenameHint, mime_type, purpose, operator } = req.body;
|
|
1212
|
+
if (!url)
|
|
1213
|
+
return reply.code(400).send({ error: 'url is required' });
|
|
1214
|
+
const cssSelector = resolveTarget(req.body, reply, s.id);
|
|
1215
|
+
if (!cssSelector)
|
|
1216
|
+
return;
|
|
1217
|
+
let resp;
|
|
1218
|
+
try {
|
|
1219
|
+
resp = await fetch(url);
|
|
1220
|
+
}
|
|
1221
|
+
catch (e) {
|
|
1222
|
+
return reply.code(422).send({ error: 'fetch_failed', message: e.message, url });
|
|
1223
|
+
}
|
|
1224
|
+
if (!resp.ok)
|
|
1225
|
+
return reply.code(422).send({ error: 'fetch_failed', http_status: resp.status, url });
|
|
1226
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
1227
|
+
const filename = filenameHint ?? url.split('/').pop()?.split('?')[0] ?? 'file';
|
|
1228
|
+
const ext = path_1.default.extname(filename) || '.bin';
|
|
1229
|
+
const tmpPath = path_1.default.join(os_1.default.tmpdir(), `agentmb_${crypto_1.default.randomBytes(4).toString('hex')}${ext}`);
|
|
1230
|
+
try {
|
|
1231
|
+
await fs_1.default.promises.writeFile(tmpPath, buf);
|
|
1232
|
+
const b64 = buf.toString('base64');
|
|
1233
|
+
const result = await Actions.uploadFile(s.page, cssSelector, b64, filename, mime_type, getLogger(), s.id, purpose, inferOperator(req, s, operator));
|
|
1234
|
+
return { ...result, url, fetched_bytes: buf.length };
|
|
1235
|
+
}
|
|
1236
|
+
catch (e) {
|
|
1237
|
+
if (e instanceof actions_1.ActionDiagnosticsError)
|
|
1238
|
+
return reply.code(422).send(enrichDiag(e.diagnostics));
|
|
944
1239
|
throw e;
|
|
945
1240
|
}
|
|
1241
|
+
finally {
|
|
1242
|
+
fs_1.default.promises.unlink(tmpPath).catch(() => { });
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
// ---------------------------------------------------------------------------
|
|
1246
|
+
// R08-R18: run_steps — batch action dispatcher
|
|
1247
|
+
// Supported actions: navigate, click, fill, type, press, hover, scroll,
|
|
1248
|
+
// wait_for_selector, wait_text, screenshot, eval
|
|
1249
|
+
// r08-c07: resolveRefIdForStep — throws (instead of reply) for use inside step loops
|
|
1250
|
+
// ---------------------------------------------------------------------------
|
|
1251
|
+
function resolveRefIdForStep(params, sessionId) {
|
|
1252
|
+
if (params.ref_id) {
|
|
1253
|
+
const bm = server.browserManager;
|
|
1254
|
+
if (!bm)
|
|
1255
|
+
throw new Error('ref_id resolution requires BrowserManager');
|
|
1256
|
+
const colonIdx = params.ref_id.lastIndexOf(':');
|
|
1257
|
+
if (colonIdx === -1)
|
|
1258
|
+
throw new Error(`Invalid ref_id format: "${params.ref_id}"; expected "snap_XXXXXX:eN"`);
|
|
1259
|
+
const snapshotId = params.ref_id.slice(0, colonIdx);
|
|
1260
|
+
const eid = params.ref_id.slice(colonIdx + 1);
|
|
1261
|
+
const snapshot = bm.getSnapshot(sessionId, snapshotId);
|
|
1262
|
+
if (!snapshot)
|
|
1263
|
+
throw new Error(`stale_ref: snapshot "${snapshotId}" not found or expired; call snapshot_map again`);
|
|
1264
|
+
const currentRev = bm.getPageRev(sessionId);
|
|
1265
|
+
if (snapshot.page_rev !== currentRev)
|
|
1266
|
+
throw new Error(`stale_ref: page changed (snapshot_rev=${snapshot.page_rev}, current=${currentRev}); call snapshot_map again`);
|
|
1267
|
+
return `[data-agentmb-eid="${eid}"]`;
|
|
1268
|
+
}
|
|
1269
|
+
if (params.element_id)
|
|
1270
|
+
return `[data-agentmb-eid="${params.element_id}"]`;
|
|
1271
|
+
return params.selector;
|
|
1272
|
+
}
|
|
1273
|
+
server.post('/api/v1/sessions/:id/run_steps', async (req, reply) => {
|
|
1274
|
+
const s = resolve(req.params.id, reply);
|
|
1275
|
+
if (!s)
|
|
1276
|
+
return;
|
|
1277
|
+
const { steps, stop_on_error = true, purpose, operator } = req.body;
|
|
1278
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
1279
|
+
return reply.code(400).send({ error: 'steps must be a non-empty array' });
|
|
1280
|
+
if (steps.length > 100)
|
|
1281
|
+
return reply.code(400).send({ error: 'steps must not exceed 100' });
|
|
1282
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1283
|
+
const results = [];
|
|
1284
|
+
const op = inferOperator(req, s, operator);
|
|
1285
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1286
|
+
const step = steps[i];
|
|
1287
|
+
const { action, params = {} } = step;
|
|
1288
|
+
const stepPurpose = params.purpose ?? purpose;
|
|
1289
|
+
try {
|
|
1290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1291
|
+
let result;
|
|
1292
|
+
switch (action) {
|
|
1293
|
+
case 'navigate':
|
|
1294
|
+
result = await Actions.navigate(s.page, params.url, params.wait_until ?? 'load', getLogger(), s.id, stepPurpose, op);
|
|
1295
|
+
break;
|
|
1296
|
+
case 'click': {
|
|
1297
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1298
|
+
throw new Error('click requires selector, element_id, or ref_id');
|
|
1299
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1300
|
+
result = await Actions.click(s.page, sel, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
case 'fill': {
|
|
1304
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1305
|
+
throw new Error('fill requires selector, element_id, or ref_id');
|
|
1306
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1307
|
+
result = await Actions.fill(s.page, sel, params.value ?? '', getLogger(), s.id, stepPurpose, op);
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
case 'type': {
|
|
1311
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1312
|
+
throw new Error('type requires selector, element_id, or ref_id');
|
|
1313
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1314
|
+
result = await Actions.typeText(s.page, sel, params.text ?? '', params.delay_ms ?? 0, getLogger(), s.id, stepPurpose, op);
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
case 'press': {
|
|
1318
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1319
|
+
throw new Error('press requires selector, element_id, or ref_id');
|
|
1320
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1321
|
+
result = await Actions.press(s.page, sel, params.key ?? '', getLogger(), s.id, stepPurpose, op);
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
case 'hover': {
|
|
1325
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1326
|
+
throw new Error('hover requires selector, element_id, or ref_id');
|
|
1327
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1328
|
+
result = await Actions.hover(s.page, sel, getLogger(), s.id, stepPurpose, op);
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case 'scroll': {
|
|
1332
|
+
if (!params.selector && !params.element_id && !params.ref_id)
|
|
1333
|
+
throw new Error('scroll requires selector, element_id, or ref_id');
|
|
1334
|
+
const sel = resolveRefIdForStep(params, s.id);
|
|
1335
|
+
result = await Actions.scroll(s.page, sel, { delta_x: params.delta_x ?? 0, delta_y: params.delta_y ?? 300 }, getLogger(), s.id, stepPurpose, op);
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
case 'wait_for_selector':
|
|
1339
|
+
result = await Actions.waitForSelector(s.page, params.selector, params.state ?? 'visible', params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1340
|
+
break;
|
|
1341
|
+
case 'wait_text':
|
|
1342
|
+
result = await Actions.waitForText(s.page, params.text, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
|
|
1343
|
+
break;
|
|
1344
|
+
case 'screenshot':
|
|
1345
|
+
result = await Actions.screenshot(s.page, params.format ?? 'png', params.full_page ?? false, getLogger(), s.id, stepPurpose, op);
|
|
1346
|
+
break;
|
|
1347
|
+
case 'eval':
|
|
1348
|
+
result = await Actions.evaluate(s.page, params.expression, getLogger(), s.id, stepPurpose, op);
|
|
1349
|
+
break;
|
|
1350
|
+
default:
|
|
1351
|
+
throw new Error(`unsupported action: ${action}`);
|
|
1352
|
+
}
|
|
1353
|
+
results.push({ step: i + 1, action, result });
|
|
1354
|
+
}
|
|
1355
|
+
catch (e) {
|
|
1356
|
+
const errPayload = e instanceof actions_1.ActionDiagnosticsError
|
|
1357
|
+
? enrichDiag(e.diagnostics)
|
|
1358
|
+
: { error: e.message ?? String(e) };
|
|
1359
|
+
results.push({ step: i + 1, action, error: errPayload });
|
|
1360
|
+
if (stop_on_error)
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const failed = results.filter(r => r.error);
|
|
1365
|
+
return {
|
|
1366
|
+
status: failed.length === 0 ? 'ok' : (results.filter(r => r.result).length > 0 ? 'partial' : 'failed'),
|
|
1367
|
+
total_steps: steps.length,
|
|
1368
|
+
completed_steps: results.filter(r => r.result).length,
|
|
1369
|
+
failed_steps: failed.length,
|
|
1370
|
+
results,
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
// ---------------------------------------------------------------------------
|
|
1374
|
+
// R08-R12: Snapshot Ref 强化 — GET page_rev endpoint
|
|
1375
|
+
// Lets clients cheaply check if the page has changed since last snapshot.
|
|
1376
|
+
// ---------------------------------------------------------------------------
|
|
1377
|
+
server.get('/api/v1/sessions/:id/page_rev', async (req, reply) => {
|
|
1378
|
+
const s = resolve(req.params.id, reply);
|
|
1379
|
+
if (!s)
|
|
1380
|
+
return;
|
|
1381
|
+
const bm = server.browserManager;
|
|
1382
|
+
const page_rev = bm?.getPageRev(s.id) ?? 0;
|
|
1383
|
+
return { status: 'ok', session_id: req.params.id, page_rev, url: s.page.url() };
|
|
946
1384
|
});
|
|
947
1385
|
}
|
|
948
1386
|
//# sourceMappingURL=actions.js.map
|