web-agent-bridge 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +79 -79
- package/sdk/package.json +22 -14
- package/server/config/plans.js +367 -0
- package/server/middleware/featureGate.js +88 -0
- package/server/migrations/005_marketplace_metering.sql +126 -0
- package/server/routes/runtime.js +616 -3
- package/server/services/hosted-runtime.js +205 -0
- package/server/services/lfd.js +616 -0
- package/server/services/marketplace.js +270 -0
- package/server/services/metering.js +182 -0
- package/server/services/vision.js +292 -0
package/server/routes/runtime.js
CHANGED
|
@@ -28,7 +28,14 @@ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry
|
|
|
28
28
|
const { certificationEngine } = require('../registry/certification');
|
|
29
29
|
const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
|
|
30
30
|
const { replayEngine } = require('../runtime/replay');
|
|
31
|
+
const { featureGate, usageLimit } = require('../middleware/featureGate');
|
|
32
|
+
const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
|
|
33
|
+
const metering = require('../services/metering');
|
|
34
|
+
const { marketplace } = require('../services/marketplace');
|
|
35
|
+
const { hostedRuntime } = require('../services/hosted-runtime');
|
|
31
36
|
const { sessionEngine } = require('../runtime/session-engine');
|
|
37
|
+
const vision = require('../services/vision');
|
|
38
|
+
const { lfdEngine } = require('../services/lfd');
|
|
32
39
|
|
|
33
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
41
|
// AUTH MIDDLEWARE
|
|
@@ -48,6 +55,14 @@ const PUBLIC_PATHS = [
|
|
|
48
55
|
'/registry/commands',
|
|
49
56
|
'/registry/sites',
|
|
50
57
|
'/registry/templates',
|
|
58
|
+
'/plans',
|
|
59
|
+
'/marketplace',
|
|
60
|
+
'/recipes',
|
|
61
|
+
'/vision/models',
|
|
62
|
+
'/vision/extraction-script',
|
|
63
|
+
'/recipes',
|
|
64
|
+
'/vision/models',
|
|
65
|
+
'/vision/extraction-script',
|
|
51
66
|
];
|
|
52
67
|
|
|
53
68
|
function authMiddleware(req, res, next) {
|
|
@@ -99,6 +114,7 @@ function authMiddleware(req, res, next) {
|
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
router.use(authMiddleware);
|
|
117
|
+
router.use(featureGate);
|
|
102
118
|
|
|
103
119
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
120
|
// PROTOCOL ENDPOINTS
|
|
@@ -241,7 +257,7 @@ router.delete('/agents/:agentId', (req, res) => {
|
|
|
241
257
|
/**
|
|
242
258
|
* Submit a task
|
|
243
259
|
*/
|
|
244
|
-
router.post('/tasks', (req, res) => {
|
|
260
|
+
router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
|
|
245
261
|
try {
|
|
246
262
|
const result = runtime.submitTask(req.body);
|
|
247
263
|
metrics.increment('tasks.submitted', 1, { type: req.body.type });
|
|
@@ -299,7 +315,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
|
|
|
299
315
|
/**
|
|
300
316
|
* Execute a semantic action
|
|
301
317
|
*/
|
|
302
|
-
router.post('/execute', async (req, res) => {
|
|
318
|
+
router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
|
|
303
319
|
try {
|
|
304
320
|
const result = await executor.execute(req.body);
|
|
305
321
|
res.json(result);
|
|
@@ -523,6 +539,10 @@ router.get('/observability/health', (req, res) => {
|
|
|
523
539
|
health.sessions = sessionEngine.getStats();
|
|
524
540
|
health.failures = failureAnalyzer.getStats();
|
|
525
541
|
health.certification = certificationEngine.getStats();
|
|
542
|
+
health.marketplace = marketplace.getStats();
|
|
543
|
+
health.hostedRuntime = hostedRuntime.getStats();
|
|
544
|
+
health.metering = metering.getStats();
|
|
545
|
+
health.lfd = lfdEngine.getStats();
|
|
526
546
|
res.json(health);
|
|
527
547
|
});
|
|
528
548
|
|
|
@@ -634,7 +654,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
|
|
|
634
654
|
/**
|
|
635
655
|
* LLM completion
|
|
636
656
|
*/
|
|
637
|
-
router.post('/llm/complete', async (req, res) => {
|
|
657
|
+
router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
|
|
638
658
|
try {
|
|
639
659
|
const result = await llm.complete(req.body.prompt, req.body.options || req.body);
|
|
640
660
|
metrics.increment('llm.api.requests');
|
|
@@ -1133,4 +1153,597 @@ router.delete('/certification/:domain', (req, res) => {
|
|
|
1133
1153
|
res.json({ success: true });
|
|
1134
1154
|
});
|
|
1135
1155
|
|
|
1156
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1157
|
+
// PLANS & PRICING
|
|
1158
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* List available plans
|
|
1162
|
+
*/
|
|
1163
|
+
router.get('/plans', (req, res) => {
|
|
1164
|
+
const plans = listPlans().map(p => ({
|
|
1165
|
+
id: p.id,
|
|
1166
|
+
name: p.name,
|
|
1167
|
+
price: p.price,
|
|
1168
|
+
interval: p.interval,
|
|
1169
|
+
description: p.description,
|
|
1170
|
+
limits: p.limits,
|
|
1171
|
+
features: Object.entries(p.features)
|
|
1172
|
+
.filter(([, v]) => v === true)
|
|
1173
|
+
.map(([k]) => k),
|
|
1174
|
+
}));
|
|
1175
|
+
res.json({ plans, usagePricing: USAGE_PRICING });
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Get specific plan details
|
|
1180
|
+
*/
|
|
1181
|
+
router.get('/plans/:planId', (req, res) => {
|
|
1182
|
+
const plan = getPlan(req.params.planId);
|
|
1183
|
+
if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
|
|
1184
|
+
return res.status(404).json({ error: 'Plan not found' });
|
|
1185
|
+
}
|
|
1186
|
+
res.json(plan);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1190
|
+
// USAGE METERING
|
|
1191
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Get usage for current agent
|
|
1195
|
+
*/
|
|
1196
|
+
router.get('/usage', (req, res) => {
|
|
1197
|
+
const entityId = req.agentId || req.ip;
|
|
1198
|
+
const tier = req.agentTier || req.session?.tier || 'free';
|
|
1199
|
+
res.json(metering.getUsage(entityId, tier));
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Get billing summary (overages)
|
|
1204
|
+
*/
|
|
1205
|
+
router.get('/usage/billing', (req, res) => {
|
|
1206
|
+
const entityId = req.agentId || req.ip;
|
|
1207
|
+
res.json(metering.getBillingSummary(entityId));
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Get metering stats (admin)
|
|
1212
|
+
*/
|
|
1213
|
+
router.get('/usage/stats', (req, res) => {
|
|
1214
|
+
res.json(metering.getStats());
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1218
|
+
// MARKETPLACE
|
|
1219
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Search marketplace
|
|
1223
|
+
*/
|
|
1224
|
+
router.get('/marketplace', (req, res) => {
|
|
1225
|
+
const listings = marketplace.search({
|
|
1226
|
+
type: req.query.type,
|
|
1227
|
+
category: req.query.category,
|
|
1228
|
+
query: req.query.q,
|
|
1229
|
+
tag: req.query.tag,
|
|
1230
|
+
free: req.query.free === 'true',
|
|
1231
|
+
paid: req.query.paid === 'true',
|
|
1232
|
+
minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
|
|
1233
|
+
sortBy: req.query.sortBy,
|
|
1234
|
+
}, parseInt(req.query.limit) || 50);
|
|
1235
|
+
res.json({ listings, total: listings.length });
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Get listing
|
|
1240
|
+
*/
|
|
1241
|
+
router.get('/marketplace/:listingId', (req, res) => {
|
|
1242
|
+
const listing = marketplace.getListing(req.params.listingId);
|
|
1243
|
+
if (!listing) return res.status(404).json({ error: 'Listing not found' });
|
|
1244
|
+
res.json(listing);
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Get reviews
|
|
1249
|
+
*/
|
|
1250
|
+
router.get('/marketplace/:listingId/reviews', (req, res) => {
|
|
1251
|
+
res.json({ reviews: marketplace.getReviews(req.params.listingId) });
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Publish listing
|
|
1256
|
+
*/
|
|
1257
|
+
router.post('/marketplace/publish', (req, res) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const listing = marketplace.publish({
|
|
1260
|
+
...req.body,
|
|
1261
|
+
sellerId: req.agentId || req.body.sellerId,
|
|
1262
|
+
});
|
|
1263
|
+
res.json(listing);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
res.status(400).json({ error: err.message });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Purchase/install listing
|
|
1271
|
+
*/
|
|
1272
|
+
router.post('/marketplace/:listingId/purchase', (req, res) => {
|
|
1273
|
+
try {
|
|
1274
|
+
const buyerId = req.agentId || req.body.buyerId;
|
|
1275
|
+
if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
|
|
1276
|
+
const purchase = marketplace.purchase(req.params.listingId, buyerId);
|
|
1277
|
+
res.json(purchase);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
res.status(400).json({ error: err.message });
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Add review
|
|
1285
|
+
*/
|
|
1286
|
+
router.post('/marketplace/:listingId/review', (req, res) => {
|
|
1287
|
+
try {
|
|
1288
|
+
const review = marketplace.addReview(req.params.listingId, {
|
|
1289
|
+
userId: req.agentId || req.body.userId,
|
|
1290
|
+
rating: req.body.rating,
|
|
1291
|
+
comment: req.body.comment,
|
|
1292
|
+
});
|
|
1293
|
+
res.json(review);
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
res.status(400).json({ error: err.message });
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Get my purchases
|
|
1301
|
+
*/
|
|
1302
|
+
router.get('/marketplace/my/purchases', (req, res) => {
|
|
1303
|
+
const buyerId = req.agentId || req.query.buyerId;
|
|
1304
|
+
res.json({ purchases: marketplace.getPurchases(buyerId) });
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Get seller earnings
|
|
1309
|
+
*/
|
|
1310
|
+
router.get('/marketplace/my/earnings', (req, res) => {
|
|
1311
|
+
const sellerId = req.agentId || req.query.sellerId;
|
|
1312
|
+
res.json(marketplace.getEarnings(sellerId));
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Admin: pending listings
|
|
1317
|
+
*/
|
|
1318
|
+
router.get('/marketplace/admin/pending', (req, res) => {
|
|
1319
|
+
res.json({ listings: marketplace.getPendingListings() });
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Admin: approve listing
|
|
1324
|
+
*/
|
|
1325
|
+
router.post('/marketplace/admin/:listingId/approve', (req, res) => {
|
|
1326
|
+
try {
|
|
1327
|
+
const listing = marketplace.approve(req.params.listingId);
|
|
1328
|
+
res.json(listing);
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
res.status(400).json({ error: err.message });
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Admin: reject listing
|
|
1336
|
+
*/
|
|
1337
|
+
router.post('/marketplace/admin/:listingId/reject', (req, res) => {
|
|
1338
|
+
try {
|
|
1339
|
+
const listing = marketplace.reject(req.params.listingId, req.body.reason);
|
|
1340
|
+
res.json(listing);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
res.status(400).json({ error: err.message });
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Marketplace stats
|
|
1348
|
+
*/
|
|
1349
|
+
router.get('/marketplace/stats', (req, res) => {
|
|
1350
|
+
res.json(marketplace.getStats());
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1354
|
+
// HOSTED RUNTIME
|
|
1355
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Launch hosted instance
|
|
1359
|
+
*/
|
|
1360
|
+
router.post('/hosted/launch', (req, res) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const instance = hostedRuntime.launch({
|
|
1363
|
+
agentId: req.agentId || req.body.agentId,
|
|
1364
|
+
tier: req.agentTier || req.session?.tier || 'starter',
|
|
1365
|
+
region: req.body.region,
|
|
1366
|
+
cpu: req.body.cpu,
|
|
1367
|
+
memory: req.body.memory,
|
|
1368
|
+
timeout: req.body.timeout,
|
|
1369
|
+
});
|
|
1370
|
+
res.json(instance);
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
res.status(400).json({ error: err.message });
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Execute on hosted instance
|
|
1378
|
+
*/
|
|
1379
|
+
router.post('/hosted/:instanceId/execute', async (req, res) => {
|
|
1380
|
+
try {
|
|
1381
|
+
const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
|
|
1382
|
+
res.json(execution);
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
res.status(400).json({ error: err.message });
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Complete execution
|
|
1390
|
+
*/
|
|
1391
|
+
router.post('/hosted/executions/:executionId/complete', (req, res) => {
|
|
1392
|
+
const execution = hostedRuntime.completeExecution(
|
|
1393
|
+
req.params.executionId,
|
|
1394
|
+
req.body.result,
|
|
1395
|
+
req.body.error ? new Error(req.body.error) : null
|
|
1396
|
+
);
|
|
1397
|
+
if (!execution) return res.status(404).json({ error: 'Execution not found' });
|
|
1398
|
+
res.json(execution);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Stop hosted instance
|
|
1403
|
+
*/
|
|
1404
|
+
router.post('/hosted/:instanceId/stop', (req, res) => {
|
|
1405
|
+
const success = hostedRuntime.stop(req.params.instanceId);
|
|
1406
|
+
res.json({ success });
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Get hosted instance
|
|
1411
|
+
*/
|
|
1412
|
+
router.get('/hosted/:instanceId', (req, res) => {
|
|
1413
|
+
const instance = hostedRuntime.getInstance(req.params.instanceId);
|
|
1414
|
+
if (!instance) return res.status(404).json({ error: 'Instance not found' });
|
|
1415
|
+
res.json(instance);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* List instances
|
|
1420
|
+
*/
|
|
1421
|
+
router.get('/hosted', (req, res) => {
|
|
1422
|
+
const instances = hostedRuntime.listInstances({
|
|
1423
|
+
agentId: req.query.agentId,
|
|
1424
|
+
status: req.query.status,
|
|
1425
|
+
region: req.query.region,
|
|
1426
|
+
}, parseInt(req.query.limit) || 50);
|
|
1427
|
+
res.json({ instances, total: instances.length });
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* List executions for instance
|
|
1432
|
+
*/
|
|
1433
|
+
router.get('/hosted/:instanceId/executions', (req, res) => {
|
|
1434
|
+
const executions = hostedRuntime.listExecutions(
|
|
1435
|
+
req.params.instanceId,
|
|
1436
|
+
parseInt(req.query.limit) || 50
|
|
1437
|
+
);
|
|
1438
|
+
res.json({ executions, total: executions.length });
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Get compute usage
|
|
1443
|
+
*/
|
|
1444
|
+
router.get('/hosted/usage/:agentId', (req, res) => {
|
|
1445
|
+
res.json(hostedRuntime.getComputeUsage(req.params.agentId));
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Hosted runtime stats
|
|
1450
|
+
*/
|
|
1451
|
+
router.get('/hosted/stats', (req, res) => {
|
|
1452
|
+
res.json(hostedRuntime.getStats());
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1456
|
+
// LOCAL VISION ENGINE (Self-contained — no external API)
|
|
1457
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Analyze page DOM locally (no external API calls)
|
|
1461
|
+
*/
|
|
1462
|
+
router.post('/vision/analyze-dom', async (req, res) => {
|
|
1463
|
+
try {
|
|
1464
|
+
const siteId = req.body.siteId || req.agentId || 'default';
|
|
1465
|
+
const result = await vision.analyzePageDOM(siteId, {
|
|
1466
|
+
domSnapshot: req.body.domSnapshot,
|
|
1467
|
+
url: req.body.url,
|
|
1468
|
+
});
|
|
1469
|
+
res.json(result);
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
res.status(400).json({ error: err.message });
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Get DOM extraction script to inject into pages
|
|
1477
|
+
*/
|
|
1478
|
+
router.get('/vision/extraction-script', (req, res) => {
|
|
1479
|
+
res.json({ script: vision.getDomExtractionScript() });
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Find elements in cached vision data
|
|
1484
|
+
*/
|
|
1485
|
+
router.get('/vision/elements', (req, res) => {
|
|
1486
|
+
const siteId = req.query.siteId || req.agentId || 'default';
|
|
1487
|
+
const results = vision.findElement(siteId, req.query.url, {
|
|
1488
|
+
description: req.query.q,
|
|
1489
|
+
type: req.query.type,
|
|
1490
|
+
label: req.query.label,
|
|
1491
|
+
});
|
|
1492
|
+
res.json({ elements: results, total: results.length });
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Vision history
|
|
1497
|
+
*/
|
|
1498
|
+
router.get('/vision/history', (req, res) => {
|
|
1499
|
+
const siteId = req.query.siteId || req.agentId || 'default';
|
|
1500
|
+
const history = vision.getVisionHistory(siteId, {
|
|
1501
|
+
limit: parseInt(req.query.limit) || 50,
|
|
1502
|
+
url: req.query.url,
|
|
1503
|
+
});
|
|
1504
|
+
res.json({ history, total: history.length });
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Supported vision models
|
|
1509
|
+
*/
|
|
1510
|
+
router.get('/vision/models', (req, res) => {
|
|
1511
|
+
res.json({ models: vision.getSupportedModels() });
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1515
|
+
// LEARNING FROM DEMONSTRATION (LfD)
|
|
1516
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Start a recording session
|
|
1520
|
+
*/
|
|
1521
|
+
router.post('/lfd/record', (req, res) => {
|
|
1522
|
+
try {
|
|
1523
|
+
const session = lfdEngine.startRecording({
|
|
1524
|
+
name: req.body.name,
|
|
1525
|
+
description: req.body.description,
|
|
1526
|
+
agentId: req.agentId || req.body.agentId,
|
|
1527
|
+
startUrl: req.body.startUrl,
|
|
1528
|
+
tags: req.body.tags,
|
|
1529
|
+
});
|
|
1530
|
+
res.json(session);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
res.status(400).json({ error: err.message });
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Record events into a session
|
|
1538
|
+
*/
|
|
1539
|
+
router.post('/lfd/:sessionId/events', (req, res) => {
|
|
1540
|
+
try {
|
|
1541
|
+
const events = req.body.events || [req.body];
|
|
1542
|
+
const results = events.map(evt => lfdEngine.recordEvent(req.params.sessionId, evt));
|
|
1543
|
+
res.json({ recorded: results.filter(Boolean).length });
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
res.status(400).json({ error: err.message });
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Record a DOM snapshot
|
|
1551
|
+
*/
|
|
1552
|
+
router.post('/lfd/:sessionId/snapshot', (req, res) => {
|
|
1553
|
+
try {
|
|
1554
|
+
lfdEngine.recordSnapshot(req.params.sessionId, req.body);
|
|
1555
|
+
res.json({ success: true });
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
res.status(400).json({ error: err.message });
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Pause recording
|
|
1563
|
+
*/
|
|
1564
|
+
router.post('/lfd/:sessionId/pause', (req, res) => {
|
|
1565
|
+
try { res.json(lfdEngine.pauseRecording(req.params.sessionId)); }
|
|
1566
|
+
catch (err) { res.status(400).json({ error: err.message }); }
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Resume recording
|
|
1571
|
+
*/
|
|
1572
|
+
router.post('/lfd/:sessionId/resume', (req, res) => {
|
|
1573
|
+
try { res.json(lfdEngine.resumeRecording(req.params.sessionId)); }
|
|
1574
|
+
catch (err) { res.status(400).json({ error: err.message }); }
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Stop recording and generate recipe
|
|
1579
|
+
*/
|
|
1580
|
+
router.post('/lfd/:sessionId/stop', (req, res) => {
|
|
1581
|
+
try { res.json(lfdEngine.stopRecording(req.params.sessionId)); }
|
|
1582
|
+
catch (err) { res.status(400).json({ error: err.message }); }
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Cancel recording
|
|
1587
|
+
*/
|
|
1588
|
+
router.post('/lfd/:sessionId/cancel', (req, res) => {
|
|
1589
|
+
try { res.json(lfdEngine.cancelRecording(req.params.sessionId)); }
|
|
1590
|
+
catch (err) { res.status(400).json({ error: err.message }); }
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Get recording details
|
|
1595
|
+
*/
|
|
1596
|
+
router.get('/lfd/:sessionId', (req, res) => {
|
|
1597
|
+
const recording = lfdEngine.getRecording(req.params.sessionId);
|
|
1598
|
+
if (!recording) return res.status(404).json({ error: 'Recording not found' });
|
|
1599
|
+
res.json(recording);
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* List recordings
|
|
1604
|
+
*/
|
|
1605
|
+
router.get('/lfd', (req, res) => {
|
|
1606
|
+
res.json({ recordings: lfdEngine.listRecordings(parseInt(req.query.limit) || 50) });
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Get recording script to inject into pages
|
|
1611
|
+
*/
|
|
1612
|
+
router.get('/lfd/:sessionId/script', (req, res) => {
|
|
1613
|
+
const serverUrl = `${req.protocol}://${req.get('host')}`;
|
|
1614
|
+
res.json({ script: lfdEngine.getRecordingScript(req.params.sessionId, serverUrl) });
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// ── Recipes ──
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* List recipes
|
|
1621
|
+
*/
|
|
1622
|
+
router.get('/recipes', (req, res) => {
|
|
1623
|
+
const recipes = lfdEngine.listRecipes({
|
|
1624
|
+
domain: req.query.domain,
|
|
1625
|
+
tag: req.query.tag,
|
|
1626
|
+
query: req.query.q,
|
|
1627
|
+
}, parseInt(req.query.limit) || 50);
|
|
1628
|
+
res.json({ recipes, total: recipes.length });
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Get recipe
|
|
1633
|
+
*/
|
|
1634
|
+
router.get('/recipes/:recipeId', (req, res) => {
|
|
1635
|
+
const recipe = lfdEngine.getRecipe(req.params.recipeId);
|
|
1636
|
+
if (!recipe) return res.status(404).json({ error: 'Recipe not found' });
|
|
1637
|
+
res.json(recipe);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Save/import recipe manually
|
|
1642
|
+
*/
|
|
1643
|
+
router.post('/recipes', (req, res) => {
|
|
1644
|
+
try { res.json(lfdEngine.saveRecipe(req.body)); }
|
|
1645
|
+
catch (err) { res.status(400).json({ error: err.message }); }
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Delete recipe
|
|
1650
|
+
*/
|
|
1651
|
+
router.delete('/recipes/:recipeId', (req, res) => {
|
|
1652
|
+
const deleted = lfdEngine.deleteRecipe(req.params.recipeId);
|
|
1653
|
+
res.json({ deleted });
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Execute a recipe
|
|
1658
|
+
*/
|
|
1659
|
+
router.post('/recipes/:recipeId/execute', (req, res) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const execution = lfdEngine.executeRecipe(req.params.recipeId, {
|
|
1662
|
+
variables: req.body.variables,
|
|
1663
|
+
speed: req.body.speed,
|
|
1664
|
+
stopOnError: req.body.stopOnError,
|
|
1665
|
+
skipWaits: req.body.skipWaits,
|
|
1666
|
+
humanInTheLoop: req.body.humanInTheLoop,
|
|
1667
|
+
});
|
|
1668
|
+
res.json(execution);
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
res.status(400).json({ error: err.message });
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Get next step in execution
|
|
1676
|
+
*/
|
|
1677
|
+
router.get('/executions/:executionId/next', (req, res) => {
|
|
1678
|
+
const step = lfdEngine.getNextStep(req.params.executionId);
|
|
1679
|
+
if (!step) {
|
|
1680
|
+
const exec = lfdEngine.getExecution(req.params.executionId);
|
|
1681
|
+
return res.json({ done: true, status: exec?.status || 'unknown' });
|
|
1682
|
+
}
|
|
1683
|
+
res.json(step);
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Report step result
|
|
1688
|
+
*/
|
|
1689
|
+
router.post('/executions/:executionId/steps/:stepIndex', (req, res) => {
|
|
1690
|
+
const exec = lfdEngine.reportStep(
|
|
1691
|
+
req.params.executionId,
|
|
1692
|
+
parseInt(req.params.stepIndex),
|
|
1693
|
+
{ success: req.body.success, error: req.body.error, duration: req.body.duration, selectorUsed: req.body.selectorUsed }
|
|
1694
|
+
);
|
|
1695
|
+
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
|
1696
|
+
res.json({ status: exec.status, currentStep: exec.currentStep, totalSteps: exec.totalSteps });
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Pause execution
|
|
1701
|
+
*/
|
|
1702
|
+
router.post('/executions/:executionId/pause', (req, res) => {
|
|
1703
|
+
const exec = lfdEngine.pauseExecution(req.params.executionId);
|
|
1704
|
+
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
|
1705
|
+
res.json({ status: exec.status });
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Resume execution
|
|
1710
|
+
*/
|
|
1711
|
+
router.post('/executions/:executionId/resume', (req, res) => {
|
|
1712
|
+
const exec = lfdEngine.resumeExecution(req.params.executionId);
|
|
1713
|
+
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
|
1714
|
+
res.json({ status: exec.status });
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Abort execution
|
|
1719
|
+
*/
|
|
1720
|
+
router.post('/executions/:executionId/abort', (req, res) => {
|
|
1721
|
+
const exec = lfdEngine.abortExecution(req.params.executionId);
|
|
1722
|
+
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
|
1723
|
+
res.json({ status: exec.status });
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* Get execution details
|
|
1728
|
+
*/
|
|
1729
|
+
router.get('/executions/:executionId', (req, res) => {
|
|
1730
|
+
const exec = lfdEngine.getExecution(req.params.executionId);
|
|
1731
|
+
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
|
1732
|
+
res.json(exec);
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* List executions
|
|
1737
|
+
*/
|
|
1738
|
+
router.get('/executions', (req, res) => {
|
|
1739
|
+
res.json({ executions: lfdEngine.listExecutions(parseInt(req.query.limit) || 50) });
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* LfD stats
|
|
1744
|
+
*/
|
|
1745
|
+
router.get('/lfd/stats', (req, res) => {
|
|
1746
|
+
res.json(lfdEngine.getStats());
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1136
1749
|
module.exports = router;
|