web-agent-bridge 2.7.0 → 2.9.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 +1 -1
- package/sdk/package.json +1 -1
- package/server/index.js +4 -0
- package/server/routes/runtime.js +497 -0
- package/server/services/cluster.js +894 -0
- package/server/services/lfd.js +616 -0
- package/server/services/vision.js +292 -0
package/package.json
CHANGED
package/sdk/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { maybeBootstrapAdmin, db } = require('./models/db');
|
|
|
15
15
|
const { initSearchEngine, search, getSuggestions, getTrendingSearches, getSearchStats, purgeOldCache } = require('./services/search-engine');
|
|
16
16
|
const { processMessage: agentChat } = require('./services/agent-chat');
|
|
17
17
|
const agentTasks = require('./services/agent-tasks');
|
|
18
|
+
const { cluster } = require('./services/cluster');
|
|
18
19
|
|
|
19
20
|
const authRoutes = require('./routes/auth');
|
|
20
21
|
const apiRoutes = require('./routes/api');
|
|
@@ -359,6 +360,9 @@ if (process.env.NODE_ENV !== 'test') {
|
|
|
359
360
|
// Start Agent OS runtime
|
|
360
361
|
runtime.start();
|
|
361
362
|
|
|
363
|
+
// Start Cluster Orchestrator
|
|
364
|
+
cluster.start();
|
|
365
|
+
|
|
362
366
|
server.listen(PORT, () => {
|
|
363
367
|
console.log(`\n ╔══════════════════════════════════════════╗`);
|
|
364
368
|
console.log(` ║ Web Agent Bridge v${pkg.version} ║`);
|
package/server/routes/runtime.js
CHANGED
|
@@ -34,6 +34,9 @@ const metering = require('../services/metering');
|
|
|
34
34
|
const { marketplace } = require('../services/marketplace');
|
|
35
35
|
const { hostedRuntime } = require('../services/hosted-runtime');
|
|
36
36
|
const { sessionEngine } = require('../runtime/session-engine');
|
|
37
|
+
const vision = require('../services/vision');
|
|
38
|
+
const { lfdEngine } = require('../services/lfd');
|
|
39
|
+
const { cluster, distributor } = require('../services/cluster');
|
|
37
40
|
|
|
38
41
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
42
|
// AUTH MIDDLEWARE
|
|
@@ -55,6 +58,10 @@ const PUBLIC_PATHS = [
|
|
|
55
58
|
'/registry/templates',
|
|
56
59
|
'/plans',
|
|
57
60
|
'/marketplace',
|
|
61
|
+
'/recipes',
|
|
62
|
+
'/vision/models',
|
|
63
|
+
'/vision/extraction-script',
|
|
64
|
+
'/cluster/status',
|
|
58
65
|
];
|
|
59
66
|
|
|
60
67
|
function authMiddleware(req, res, next) {
|
|
@@ -534,6 +541,8 @@ router.get('/observability/health', (req, res) => {
|
|
|
534
541
|
health.marketplace = marketplace.getStats();
|
|
535
542
|
health.hostedRuntime = hostedRuntime.getStats();
|
|
536
543
|
health.metering = metering.getStats();
|
|
544
|
+
health.lfd = lfdEngine.getStats();
|
|
545
|
+
health.cluster = cluster.getClusterStatus();
|
|
537
546
|
res.json(health);
|
|
538
547
|
});
|
|
539
548
|
|
|
@@ -1443,4 +1452,492 @@ router.get('/hosted/stats', (req, res) => {
|
|
|
1443
1452
|
res.json(hostedRuntime.getStats());
|
|
1444
1453
|
});
|
|
1445
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
|
+
|
|
1749
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1750
|
+
// CLUSTER — DISTRIBUTED EXECUTION & WORKER NODES
|
|
1751
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Get cluster status (public)
|
|
1755
|
+
*/
|
|
1756
|
+
router.get('/cluster/status', (req, res) => {
|
|
1757
|
+
res.json(cluster.getClusterStatus());
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Register a worker node
|
|
1762
|
+
*/
|
|
1763
|
+
router.post('/cluster/nodes', (req, res) => {
|
|
1764
|
+
try {
|
|
1765
|
+
const result = cluster.registerNode({
|
|
1766
|
+
name: req.body.name,
|
|
1767
|
+
endpoint: req.body.endpoint,
|
|
1768
|
+
region: req.body.region,
|
|
1769
|
+
zone: req.body.zone,
|
|
1770
|
+
role: req.body.role,
|
|
1771
|
+
capacity: req.body.capacity,
|
|
1772
|
+
tags: req.body.tags,
|
|
1773
|
+
hardware: req.body.hardware,
|
|
1774
|
+
version: req.body.version,
|
|
1775
|
+
secret: req.body.secret,
|
|
1776
|
+
});
|
|
1777
|
+
res.json(result);
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
res.status(400).json({ error: err.message });
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* List cluster nodes
|
|
1785
|
+
*/
|
|
1786
|
+
router.get('/cluster/nodes', (req, res) => {
|
|
1787
|
+
const nodes = cluster.listNodes({
|
|
1788
|
+
region: req.query.region,
|
|
1789
|
+
active: req.query.active === 'true',
|
|
1790
|
+
});
|
|
1791
|
+
res.json({ nodes });
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Get a specific node
|
|
1796
|
+
*/
|
|
1797
|
+
router.get('/cluster/nodes/:nodeId', (req, res) => {
|
|
1798
|
+
const node = cluster.getNode(req.params.nodeId);
|
|
1799
|
+
if (!node) return res.status(404).json({ error: 'Node not found' });
|
|
1800
|
+
res.json(node);
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Remove a node
|
|
1805
|
+
*/
|
|
1806
|
+
router.delete('/cluster/nodes/:nodeId', (req, res) => {
|
|
1807
|
+
const result = cluster.deregisterNode(req.params.nodeId);
|
|
1808
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1809
|
+
res.json(result);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Worker heartbeat
|
|
1814
|
+
*/
|
|
1815
|
+
router.post('/cluster/nodes/:nodeId/heartbeat', (req, res) => {
|
|
1816
|
+
const result = cluster.heartbeat(req.params.nodeId, {
|
|
1817
|
+
capacityUsed: req.body.capacityUsed,
|
|
1818
|
+
capacityTotal: req.body.capacityTotal,
|
|
1819
|
+
hardware: req.body.hardware,
|
|
1820
|
+
tags: req.body.tags,
|
|
1821
|
+
version: req.body.version,
|
|
1822
|
+
});
|
|
1823
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1824
|
+
res.json(result);
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Drain a node (stop new tasks, wait for running)
|
|
1829
|
+
*/
|
|
1830
|
+
router.post('/cluster/nodes/:nodeId/drain', (req, res) => {
|
|
1831
|
+
const result = cluster.drainNode(req.params.nodeId);
|
|
1832
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1833
|
+
res.json(result);
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Cordon a node (prevent scheduling)
|
|
1838
|
+
*/
|
|
1839
|
+
router.post('/cluster/nodes/:nodeId/cordon', (req, res) => {
|
|
1840
|
+
const result = cluster.cordonNode(req.params.nodeId);
|
|
1841
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1842
|
+
res.json(result);
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Uncordon a node (allow scheduling again)
|
|
1847
|
+
*/
|
|
1848
|
+
router.post('/cluster/nodes/:nodeId/uncordon', (req, res) => {
|
|
1849
|
+
const result = cluster.uncordonNode(req.params.nodeId);
|
|
1850
|
+
if (!result) return res.status(404).json({ error: 'Node not found' });
|
|
1851
|
+
res.json(result);
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Submit a task for distributed execution
|
|
1856
|
+
*/
|
|
1857
|
+
router.post('/cluster/tasks', (req, res) => {
|
|
1858
|
+
try {
|
|
1859
|
+
const result = distributor.submit({
|
|
1860
|
+
type: req.body.type,
|
|
1861
|
+
objective: req.body.objective,
|
|
1862
|
+
params: req.body.params,
|
|
1863
|
+
priority: req.body.priority,
|
|
1864
|
+
affinityTags: req.body.affinityTags,
|
|
1865
|
+
affinityRegion: req.body.affinityRegion,
|
|
1866
|
+
timeout: req.body.timeout,
|
|
1867
|
+
maxAttempts: req.body.maxAttempts,
|
|
1868
|
+
externalId: req.body.externalId,
|
|
1869
|
+
});
|
|
1870
|
+
res.json(result);
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
res.status(400).json({ error: err.message });
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Get task details
|
|
1878
|
+
*/
|
|
1879
|
+
router.get('/cluster/tasks/:taskId', (req, res) => {
|
|
1880
|
+
const task = cluster.getTask(req.params.taskId);
|
|
1881
|
+
if (!task) return res.status(404).json({ error: 'Task not found' });
|
|
1882
|
+
res.json(task);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* List tasks
|
|
1887
|
+
*/
|
|
1888
|
+
router.get('/cluster/tasks', (req, res) => {
|
|
1889
|
+
const tasks = cluster.listTasks({
|
|
1890
|
+
status: req.query.status,
|
|
1891
|
+
nodeId: req.query.nodeId,
|
|
1892
|
+
limit: parseInt(req.query.limit) || 50,
|
|
1893
|
+
});
|
|
1894
|
+
res.json({ tasks });
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Worker pulls tasks (poll-based)
|
|
1899
|
+
*/
|
|
1900
|
+
router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
|
|
1901
|
+
const tasks = distributor.pullTasks(req.params.nodeId, parseInt(req.body.limit) || 5);
|
|
1902
|
+
res.json({ tasks });
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Worker reports task started
|
|
1907
|
+
*/
|
|
1908
|
+
router.post('/cluster/tasks/:taskId/started', (req, res) => {
|
|
1909
|
+
const result = cluster.reportTaskStarted(req.params.taskId);
|
|
1910
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1911
|
+
res.json(result);
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Worker reports task completed
|
|
1916
|
+
*/
|
|
1917
|
+
router.post('/cluster/tasks/:taskId/completed', (req, res) => {
|
|
1918
|
+
const result = cluster.reportTaskCompleted(req.params.taskId, req.body.result);
|
|
1919
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1920
|
+
res.json(result);
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Worker reports task failed
|
|
1925
|
+
*/
|
|
1926
|
+
router.post('/cluster/tasks/:taskId/failed', (req, res) => {
|
|
1927
|
+
const result = cluster.reportTaskFailed(req.params.taskId, req.body.error);
|
|
1928
|
+
if (!result) return res.status(404).json({ error: 'Task not found' });
|
|
1929
|
+
res.json(result);
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Get cluster events log
|
|
1934
|
+
*/
|
|
1935
|
+
router.get('/cluster/events', (req, res) => {
|
|
1936
|
+
const events = cluster.getEvents(
|
|
1937
|
+
parseInt(req.query.limit) || 100,
|
|
1938
|
+
req.query.nodeId || null
|
|
1939
|
+
);
|
|
1940
|
+
res.json({ events });
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1446
1943
|
module.exports = router;
|