@askjo/camofox-browser 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -8
- package/camofox.config.json +8 -0
- package/lib/config.js +26 -1
- package/lib/extract.js +74 -0
- package/lib/openapi.js +100 -0
- package/lib/plugins.js +1 -0
- package/lib/reporter.js +743 -0
- package/lib/tracing.js +137 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/plugins/persistence/index.js +7 -3
- package/plugins/persistence/plugin.test.js +2 -2
- package/plugins/vnc/spawn.js +8 -0
- package/plugins/vnc/vnc-launcher.js +2 -2
- package/scripts/exec.js +8 -0
- package/scripts/generate-openapi.js +24 -0
- package/scripts/plugin.js +1 -1
- package/scripts/plugin.test.js +1 -1
- package/server.js +1846 -25
package/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { VirtualDisplay } from 'camoufox-js/dist/virtdisplay.js';
|
|
|
3
3
|
import { firefox } from 'playwright-core';
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
+
import fs from 'fs';
|
|
6
7
|
import os from 'os';
|
|
7
8
|
import { expandMacro } from './lib/macros.js';
|
|
8
9
|
import { loadConfig } from './lib/config.js';
|
|
@@ -19,6 +20,11 @@ import {
|
|
|
19
20
|
getDownloadsList,
|
|
20
21
|
} from './lib/downloads.js';
|
|
21
22
|
import { extractPageImages } from './lib/images.js';
|
|
23
|
+
import { extractDeterministic, validateSchema as validateExtractSchema } from './lib/extract.js';
|
|
24
|
+
import {
|
|
25
|
+
ensureTracesDir, resolveTracePath, tracePathFor, makeTraceFilename,
|
|
26
|
+
listUserTraces, statTrace, deleteTrace, sweepOldTraces,
|
|
27
|
+
} from './lib/tracing.js';
|
|
22
28
|
|
|
23
29
|
import {
|
|
24
30
|
initMetrics, getRegister, isMetricsEnabled, createMetric,
|
|
@@ -27,9 +33,29 @@ import {
|
|
|
27
33
|
import { actionFromReq, classifyError } from './lib/request-utils.js';
|
|
28
34
|
import { cleanupOrphanedTempFiles } from './lib/tmp-cleanup.js';
|
|
29
35
|
import { coalesceInflight } from './lib/inflight.js';
|
|
36
|
+
import { createReporter, createTabHealthTracker } from './lib/reporter.js';
|
|
37
|
+
import { mountDocs } from './lib/openapi.js';
|
|
30
38
|
|
|
31
39
|
const CONFIG = loadConfig();
|
|
32
40
|
|
|
41
|
+
// --- Crash reporter (opt-in, anonymized GitHub issues) ---
|
|
42
|
+
import { readFileSync } from 'fs';
|
|
43
|
+
const _pkgVersion = (() => { try { return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version; } catch { return 'unknown'; } })();
|
|
44
|
+
const reporter = createReporter({ ...CONFIG, version: _pkgVersion });
|
|
45
|
+
reporter.startWatchdog(5000, () => {
|
|
46
|
+
const summary = [];
|
|
47
|
+
for (const [userId, session] of sessions) {
|
|
48
|
+
const urls = [];
|
|
49
|
+
for (const group of session.tabGroups.values()) {
|
|
50
|
+
for (const tab of group.values()) {
|
|
51
|
+
try { if (tab.page) urls.push(tab.page.url()); } catch {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
summary.push({ userId, urls });
|
|
55
|
+
}
|
|
56
|
+
return { sessions: sessions.size, summary };
|
|
57
|
+
});
|
|
58
|
+
|
|
33
59
|
// --- Plugin event bus ---
|
|
34
60
|
const pluginEvents = createPluginEvents();
|
|
35
61
|
|
|
@@ -168,10 +194,81 @@ function validateUrl(url) {
|
|
|
168
194
|
//
|
|
169
195
|
// SECURITY:
|
|
170
196
|
// Cookie injection moves this from "anonymous browsing" to "authenticated browsing".
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
/**
|
|
198
|
+
* @openapi
|
|
199
|
+
* /sessions/{userId}/cookies:
|
|
200
|
+
* post:
|
|
201
|
+
* tags: [Sessions]
|
|
202
|
+
* summary: Import cookies into a user session
|
|
203
|
+
* description: Import cookies for authenticated browsing. Requires BearerAuth in production.
|
|
204
|
+
* security:
|
|
205
|
+
* - BearerAuth: []
|
|
206
|
+
* parameters:
|
|
207
|
+
* - name: userId
|
|
208
|
+
* in: path
|
|
209
|
+
* required: true
|
|
210
|
+
* schema:
|
|
211
|
+
* type: string
|
|
212
|
+
* description: Session owner identifier.
|
|
213
|
+
* requestBody:
|
|
214
|
+
* required: true
|
|
215
|
+
* content:
|
|
216
|
+
* application/json:
|
|
217
|
+
* schema:
|
|
218
|
+
* type: object
|
|
219
|
+
* required: [cookies]
|
|
220
|
+
* properties:
|
|
221
|
+
* cookies:
|
|
222
|
+
* type: array
|
|
223
|
+
* maxItems: 500
|
|
224
|
+
* items:
|
|
225
|
+
* type: object
|
|
226
|
+
* required: [name, value, domain]
|
|
227
|
+
* properties:
|
|
228
|
+
* name:
|
|
229
|
+
* type: string
|
|
230
|
+
* value:
|
|
231
|
+
* type: string
|
|
232
|
+
* domain:
|
|
233
|
+
* type: string
|
|
234
|
+
* path:
|
|
235
|
+
* type: string
|
|
236
|
+
* expires:
|
|
237
|
+
* type: number
|
|
238
|
+
* httpOnly:
|
|
239
|
+
* type: boolean
|
|
240
|
+
* secure:
|
|
241
|
+
* type: boolean
|
|
242
|
+
* sameSite:
|
|
243
|
+
* type: string
|
|
244
|
+
* enum: [Strict, Lax, None]
|
|
245
|
+
* responses:
|
|
246
|
+
* 200:
|
|
247
|
+
* description: Cookies imported.
|
|
248
|
+
* content:
|
|
249
|
+
* application/json:
|
|
250
|
+
* schema:
|
|
251
|
+
* type: object
|
|
252
|
+
* properties:
|
|
253
|
+
* ok:
|
|
254
|
+
* type: boolean
|
|
255
|
+
* userId:
|
|
256
|
+
* type: string
|
|
257
|
+
* count:
|
|
258
|
+
* type: integer
|
|
259
|
+
* 400:
|
|
260
|
+
* description: Invalid cookie data.
|
|
261
|
+
* content:
|
|
262
|
+
* application/json:
|
|
263
|
+
* schema:
|
|
264
|
+
* $ref: '#/components/schemas/Error'
|
|
265
|
+
* 403:
|
|
266
|
+
* description: Forbidden.
|
|
267
|
+
* content:
|
|
268
|
+
* application/json:
|
|
269
|
+
* schema:
|
|
270
|
+
* $ref: '#/components/schemas/Error'
|
|
271
|
+
*/
|
|
175
272
|
app.post('/sessions/:userId/cookies', express.json({ limit: '512kb' }), async (req, res) => {
|
|
176
273
|
try {
|
|
177
274
|
if (CONFIG.apiKey) {
|
|
@@ -726,6 +823,16 @@ async function closeSession(userId, session, {
|
|
|
726
823
|
await clearSessionDownloads(session).catch(() => {});
|
|
727
824
|
}
|
|
728
825
|
|
|
826
|
+
await pluginEvents.emitAsync('session:destroying', { userId: key, reason });
|
|
827
|
+
if (session.tracePath) {
|
|
828
|
+
try {
|
|
829
|
+
await session.context.tracing.stop({ path: session.tracePath });
|
|
830
|
+
log('info', 'tracing saved', { userId: key, path: session.tracePath });
|
|
831
|
+
} catch (err) {
|
|
832
|
+
log('warn', 'tracing.stop failed', { userId: key, error: err.message });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
729
836
|
await session.context.close().catch(() => {});
|
|
730
837
|
sessions.delete(key);
|
|
731
838
|
await pluginEvents.emitAsync('session:destroyed', { userId: key, reason });
|
|
@@ -744,7 +851,7 @@ async function closeAllSessions(reason, { clearDownloads = true, clearLocks = tr
|
|
|
744
851
|
}
|
|
745
852
|
}
|
|
746
853
|
|
|
747
|
-
async function getSession(userId) {
|
|
854
|
+
async function getSession(userId, { trace = false } = {}) {
|
|
748
855
|
const key = normalizeUserId(userId);
|
|
749
856
|
let session = sessions.get(key);
|
|
750
857
|
|
|
@@ -794,8 +901,21 @@ async function getSession(userId) {
|
|
|
794
901
|
}
|
|
795
902
|
await pluginEvents.emitAsync('session:creating', { userId: key, contextOptions });
|
|
796
903
|
const context = await b.newContext(contextOptions);
|
|
797
|
-
|
|
798
|
-
|
|
904
|
+
|
|
905
|
+
let tracePath = null;
|
|
906
|
+
if (trace) {
|
|
907
|
+
const traceDir = ensureTracesDir(CONFIG.tracesDir, key);
|
|
908
|
+
tracePath = tracePathFor(CONFIG.tracesDir, key, makeTraceFilename());
|
|
909
|
+
try {
|
|
910
|
+
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
911
|
+
log('info', 'tracing enabled for session', { userId: key, traceDir, tracePath });
|
|
912
|
+
} catch (err) {
|
|
913
|
+
log('warn', 'tracing.start failed; session will not be traced', { userId: key, error: err.message });
|
|
914
|
+
tracePath = null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const created = { context, tabGroups: new Map(), lastAccess: Date.now(), proxySessionId: sessionProxy?.sessionId || null, tracePath };
|
|
799
919
|
sessions.set(key, created);
|
|
800
920
|
await pluginEvents.emitAsync('session:created', { userId: key, context });
|
|
801
921
|
log('info', 'session created', {
|
|
@@ -889,7 +1009,6 @@ function handleRouteError(err, req, res, extraFields = {}) {
|
|
|
889
1009
|
}
|
|
890
1010
|
// Lock queue timeout = tab is stuck. Destroy immediately.
|
|
891
1011
|
if (userId && isTabLockQueueTimeout(err)) {
|
|
892
|
-
const tabId = req.body?.tabId || req.query?.tabId || req.params?.tabId;
|
|
893
1012
|
const session = sessions.get(normalizeUserId(userId));
|
|
894
1013
|
if (session && tabId) {
|
|
895
1014
|
destroyTab(session, tabId, 'lock_queue', userId);
|
|
@@ -900,6 +1019,34 @@ function handleRouteError(err, req, res, extraFields = {}) {
|
|
|
900
1019
|
if (isTabDestroyedError(err)) {
|
|
901
1020
|
return res.status(410).json({ error: 'Tab was destroyed. Open a new tab.', ...extraFields });
|
|
902
1021
|
}
|
|
1022
|
+
// --- Frustration detection: report when a tab hits a streak of failures ---
|
|
1023
|
+
// Individual failures are noise. 3+ consecutive = the site is persistently broken.
|
|
1024
|
+
const FRUSTRATION_TYPES = new Set(['timeout', 'dead_context', 'nav_aborted']);
|
|
1025
|
+
if (FRUSTRATION_TYPES.has(failureType) && userId && tabId) {
|
|
1026
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
1027
|
+
const found = session && findTab(session, tabId);
|
|
1028
|
+
if (found) {
|
|
1029
|
+
const ts = found.tabState;
|
|
1030
|
+
ts.consecutiveFailures = (ts.consecutiveFailures || 0) + 1;
|
|
1031
|
+
if (!ts.failureJournal) ts.failureJournal = [];
|
|
1032
|
+
ts.failureJournal.push({ type: failureType, action, at: Date.now() });
|
|
1033
|
+
if (ts.failureJournal.length > 20) ts.failureJournal = ts.failureJournal.slice(-20);
|
|
1034
|
+
|
|
1035
|
+
if (ts.consecutiveFailures === 3) {
|
|
1036
|
+
reporter.reportHang(action, req.startTime ? Date.now() - req.startTime : 0, {
|
|
1037
|
+
error: err,
|
|
1038
|
+
url: ts.lastRequestedUrl || undefined,
|
|
1039
|
+
healthSnapshot: ts.healthTracker ? ts.healthTracker.snapshot() : undefined,
|
|
1040
|
+
context: {
|
|
1041
|
+
failureType,
|
|
1042
|
+
consecutiveFailures: ts.consecutiveFailures,
|
|
1043
|
+
toolCalls: ts.toolCalls,
|
|
1044
|
+
journal: ts.failureJournal.map(j => `${j.type}:${j.action}`),
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
903
1050
|
sendError(res, err, extraFields);
|
|
904
1051
|
}
|
|
905
1052
|
|
|
@@ -980,6 +1127,7 @@ function findTab(session, tabId) {
|
|
|
980
1127
|
}
|
|
981
1128
|
|
|
982
1129
|
function createTabState(page) {
|
|
1130
|
+
const healthTracker = createTabHealthTracker(page);
|
|
983
1131
|
return {
|
|
984
1132
|
page,
|
|
985
1133
|
refs: new Map(),
|
|
@@ -987,6 +1135,9 @@ function createTabState(page) {
|
|
|
987
1135
|
downloads: [],
|
|
988
1136
|
toolCalls: 0,
|
|
989
1137
|
consecutiveTimeouts: 0,
|
|
1138
|
+
consecutiveFailures: 0,
|
|
1139
|
+
failureJournal: [],
|
|
1140
|
+
healthTracker,
|
|
990
1141
|
lastSnapshot: null,
|
|
991
1142
|
lastRequestedUrl: null,
|
|
992
1143
|
googleRetryCount: 0,
|
|
@@ -1509,6 +1660,47 @@ async function refreshTabRefs(tabState, options = {}) {
|
|
|
1509
1660
|
}
|
|
1510
1661
|
|
|
1511
1662
|
|
|
1663
|
+
/**
|
|
1664
|
+
* @openapi
|
|
1665
|
+
* /health:
|
|
1666
|
+
* get:
|
|
1667
|
+
* tags: [System]
|
|
1668
|
+
* summary: Health check
|
|
1669
|
+
* description: Detailed health with tab/session counts and failure tracking.
|
|
1670
|
+
* responses:
|
|
1671
|
+
* 200:
|
|
1672
|
+
* description: Healthy.
|
|
1673
|
+
* content:
|
|
1674
|
+
* application/json:
|
|
1675
|
+
* schema:
|
|
1676
|
+
* type: object
|
|
1677
|
+
* properties:
|
|
1678
|
+
* ok:
|
|
1679
|
+
* type: boolean
|
|
1680
|
+
* engine:
|
|
1681
|
+
* type: string
|
|
1682
|
+
* browserConnected:
|
|
1683
|
+
* type: boolean
|
|
1684
|
+
* browserRunning:
|
|
1685
|
+
* type: boolean
|
|
1686
|
+
* activeTabs:
|
|
1687
|
+
* type: integer
|
|
1688
|
+
* activeSessions:
|
|
1689
|
+
* type: integer
|
|
1690
|
+
* consecutiveFailures:
|
|
1691
|
+
* type: integer
|
|
1692
|
+
* 503:
|
|
1693
|
+
* description: Unhealthy or recovering.
|
|
1694
|
+
* content:
|
|
1695
|
+
* application/json:
|
|
1696
|
+
* schema:
|
|
1697
|
+
* type: object
|
|
1698
|
+
* properties:
|
|
1699
|
+
* ok:
|
|
1700
|
+
* type: boolean
|
|
1701
|
+
* recovering:
|
|
1702
|
+
* type: boolean
|
|
1703
|
+
*/
|
|
1512
1704
|
app.get('/health', (req, res) => {
|
|
1513
1705
|
if (healthState.isRecovering) {
|
|
1514
1706
|
return res.status(503).json({ ok: false, engine: 'camoufox', recovering: true });
|
|
@@ -1537,6 +1729,27 @@ app.get('/health', (req, res) => {
|
|
|
1537
1729
|
});
|
|
1538
1730
|
});
|
|
1539
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* @openapi
|
|
1734
|
+
* /metrics:
|
|
1735
|
+
* get:
|
|
1736
|
+
* tags: [System]
|
|
1737
|
+
* summary: Prometheus metrics
|
|
1738
|
+
* description: Returns Prometheus text exposition format. Requires PROMETHEUS_ENABLED=1.
|
|
1739
|
+
* responses:
|
|
1740
|
+
* 200:
|
|
1741
|
+
* description: Prometheus metrics.
|
|
1742
|
+
* content:
|
|
1743
|
+
* text/plain:
|
|
1744
|
+
* schema:
|
|
1745
|
+
* type: string
|
|
1746
|
+
* 404:
|
|
1747
|
+
* description: Metrics disabled.
|
|
1748
|
+
* content:
|
|
1749
|
+
* application/json:
|
|
1750
|
+
* schema:
|
|
1751
|
+
* $ref: '#/components/schemas/Error'
|
|
1752
|
+
*/
|
|
1540
1753
|
app.get('/metrics', async (_req, res) => {
|
|
1541
1754
|
const reg = getRegister();
|
|
1542
1755
|
if (!reg) {
|
|
@@ -1548,17 +1761,85 @@ app.get('/metrics', async (_req, res) => {
|
|
|
1548
1761
|
});
|
|
1549
1762
|
|
|
1550
1763
|
// Create new tab
|
|
1764
|
+
/**
|
|
1765
|
+
* @openapi
|
|
1766
|
+
* /tabs:
|
|
1767
|
+
* post:
|
|
1768
|
+
* tags: [Tabs]
|
|
1769
|
+
* summary: Create a new tab
|
|
1770
|
+
* description: Creates a tab in the given session. Optionally navigates to an initial URL.
|
|
1771
|
+
* requestBody:
|
|
1772
|
+
* required: true
|
|
1773
|
+
* content:
|
|
1774
|
+
* application/json:
|
|
1775
|
+
* schema:
|
|
1776
|
+
* type: object
|
|
1777
|
+
* required: [userId, sessionKey]
|
|
1778
|
+
* properties:
|
|
1779
|
+
* userId:
|
|
1780
|
+
* type: string
|
|
1781
|
+
* description: Session owner.
|
|
1782
|
+
* sessionKey:
|
|
1783
|
+
* type: string
|
|
1784
|
+
* description: Tab group identifier.
|
|
1785
|
+
* listItemId:
|
|
1786
|
+
* type: string
|
|
1787
|
+
* description: Legacy alias for sessionKey.
|
|
1788
|
+
* url:
|
|
1789
|
+
* type: string
|
|
1790
|
+
* description: Optional initial URL.
|
|
1791
|
+
* trace:
|
|
1792
|
+
* type: boolean
|
|
1793
|
+
* description: Enable Playwright tracing for this session (screenshots, DOM snapshots, network). Must be set on first tab creation; cannot be added to an existing session.
|
|
1794
|
+
* responses:
|
|
1795
|
+
* 200:
|
|
1796
|
+
* description: Tab created.
|
|
1797
|
+
* content:
|
|
1798
|
+
* application/json:
|
|
1799
|
+
* schema:
|
|
1800
|
+
* type: object
|
|
1801
|
+
* properties:
|
|
1802
|
+
* tabId:
|
|
1803
|
+
* type: string
|
|
1804
|
+
* url:
|
|
1805
|
+
* type: string
|
|
1806
|
+
* 400:
|
|
1807
|
+
* description: Missing required fields.
|
|
1808
|
+
* content:
|
|
1809
|
+
* application/json:
|
|
1810
|
+
* schema:
|
|
1811
|
+
* $ref: '#/components/schemas/Error'
|
|
1812
|
+
* 429:
|
|
1813
|
+
* description: Tab limit reached.
|
|
1814
|
+
* content:
|
|
1815
|
+
* application/json:
|
|
1816
|
+
* schema:
|
|
1817
|
+
* $ref: '#/components/schemas/Error'
|
|
1818
|
+
* 409:
|
|
1819
|
+
* description: Cannot enable tracing on an existing session.
|
|
1820
|
+
* content:
|
|
1821
|
+
* application/json:
|
|
1822
|
+
* schema:
|
|
1823
|
+
* $ref: '#/components/schemas/Error'
|
|
1824
|
+
*/
|
|
1551
1825
|
app.post('/tabs', async (req, res) => {
|
|
1552
1826
|
try {
|
|
1553
|
-
const { userId, sessionKey, listItemId, url } = req.body;
|
|
1827
|
+
const { userId, sessionKey, listItemId, url, trace } = req.body;
|
|
1554
1828
|
// Accept both sessionKey (preferred) and listItemId (legacy) for backward compatibility
|
|
1555
1829
|
const resolvedSessionKey = sessionKey || listItemId;
|
|
1556
1830
|
if (!userId || !resolvedSessionKey) {
|
|
1557
1831
|
return res.status(400).json({ error: 'userId and sessionKey required' });
|
|
1558
1832
|
}
|
|
1559
|
-
|
|
1833
|
+
|
|
1560
1834
|
const result = await withTimeout((async () => {
|
|
1561
|
-
const
|
|
1835
|
+
const existing = sessions.get(normalizeUserId(userId));
|
|
1836
|
+
if (trace && existing && !existing.tracePath) {
|
|
1837
|
+
throw Object.assign(
|
|
1838
|
+
new Error('trace must be set on session creation. DELETE /sessions/:userId first to restart with tracing.'),
|
|
1839
|
+
{ statusCode: 409 },
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
const session = await getSession(userId, { trace: !!trace });
|
|
1562
1843
|
|
|
1563
1844
|
let totalTabs = 0;
|
|
1564
1845
|
for (const group of session.tabGroups.values()) totalTabs += group.size;
|
|
@@ -1601,6 +1882,61 @@ app.post('/tabs', async (req, res) => {
|
|
|
1601
1882
|
});
|
|
1602
1883
|
|
|
1603
1884
|
// Navigate
|
|
1885
|
+
/**
|
|
1886
|
+
* @openapi
|
|
1887
|
+
* /tabs/{tabId}/navigate:
|
|
1888
|
+
* post:
|
|
1889
|
+
* tags: [Navigation]
|
|
1890
|
+
* summary: Navigate a tab to a URL or macro
|
|
1891
|
+
* description: Navigate to a URL or expand a search macro. Auto-creates tab if not found.
|
|
1892
|
+
* parameters:
|
|
1893
|
+
* - name: tabId
|
|
1894
|
+
* in: path
|
|
1895
|
+
* required: true
|
|
1896
|
+
* schema:
|
|
1897
|
+
* type: string
|
|
1898
|
+
* requestBody:
|
|
1899
|
+
* required: true
|
|
1900
|
+
* content:
|
|
1901
|
+
* application/json:
|
|
1902
|
+
* schema:
|
|
1903
|
+
* type: object
|
|
1904
|
+
* required: [userId]
|
|
1905
|
+
* properties:
|
|
1906
|
+
* userId:
|
|
1907
|
+
* type: string
|
|
1908
|
+
* url:
|
|
1909
|
+
* type: string
|
|
1910
|
+
* macro:
|
|
1911
|
+
* type: string
|
|
1912
|
+
* description: Search macro (e.g. @google_search).
|
|
1913
|
+
* query:
|
|
1914
|
+
* type: string
|
|
1915
|
+
* description: Search query for macro.
|
|
1916
|
+
* sessionKey:
|
|
1917
|
+
* type: string
|
|
1918
|
+
* listItemId:
|
|
1919
|
+
* type: string
|
|
1920
|
+
* responses:
|
|
1921
|
+
* 200:
|
|
1922
|
+
* description: Navigation result with snapshot.
|
|
1923
|
+
* content:
|
|
1924
|
+
* application/json:
|
|
1925
|
+
* schema:
|
|
1926
|
+
* type: object
|
|
1927
|
+
* 400:
|
|
1928
|
+
* description: Bad request.
|
|
1929
|
+
* content:
|
|
1930
|
+
* application/json:
|
|
1931
|
+
* schema:
|
|
1932
|
+
* $ref: '#/components/schemas/Error'
|
|
1933
|
+
* 404:
|
|
1934
|
+
* description: Tab not found.
|
|
1935
|
+
* content:
|
|
1936
|
+
* application/json:
|
|
1937
|
+
* schema:
|
|
1938
|
+
* $ref: '#/components/schemas/Error'
|
|
1939
|
+
*/
|
|
1604
1940
|
app.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
1605
1941
|
const tabId = req.params.tabId;
|
|
1606
1942
|
|
|
@@ -1638,7 +1974,7 @@ app.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
|
1638
1974
|
} else {
|
|
1639
1975
|
tabState = found.tabState;
|
|
1640
1976
|
}
|
|
1641
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
1977
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
1642
1978
|
|
|
1643
1979
|
let targetUrl = url;
|
|
1644
1980
|
if (macro && macro !== '__NO__' && macro !== 'none' && macro !== 'null') {
|
|
@@ -1749,6 +2085,69 @@ app.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
|
1749
2085
|
});
|
|
1750
2086
|
|
|
1751
2087
|
// Snapshot
|
|
2088
|
+
/**
|
|
2089
|
+
* @openapi
|
|
2090
|
+
* /tabs/{tabId}/snapshot:
|
|
2091
|
+
* get:
|
|
2092
|
+
* tags: [Content]
|
|
2093
|
+
* summary: Accessibility snapshot
|
|
2094
|
+
* description: Returns accessibility tree with element refs. Supports pagination via offset.
|
|
2095
|
+
* parameters:
|
|
2096
|
+
* - name: tabId
|
|
2097
|
+
* in: path
|
|
2098
|
+
* required: true
|
|
2099
|
+
* schema:
|
|
2100
|
+
* type: string
|
|
2101
|
+
* - name: userId
|
|
2102
|
+
* in: query
|
|
2103
|
+
* required: true
|
|
2104
|
+
* schema:
|
|
2105
|
+
* type: string
|
|
2106
|
+
* - name: format
|
|
2107
|
+
* in: query
|
|
2108
|
+
* schema:
|
|
2109
|
+
* type: string
|
|
2110
|
+
* enum: [text, json]
|
|
2111
|
+
* default: text
|
|
2112
|
+
* - name: offset
|
|
2113
|
+
* in: query
|
|
2114
|
+
* schema:
|
|
2115
|
+
* type: integer
|
|
2116
|
+
* description: Character offset for paginated retrieval.
|
|
2117
|
+
* - name: includeScreenshot
|
|
2118
|
+
* in: query
|
|
2119
|
+
* schema:
|
|
2120
|
+
* type: string
|
|
2121
|
+
* enum: ['true', 'false']
|
|
2122
|
+
* responses:
|
|
2123
|
+
* 200:
|
|
2124
|
+
* description: Snapshot.
|
|
2125
|
+
* content:
|
|
2126
|
+
* application/json:
|
|
2127
|
+
* schema:
|
|
2128
|
+
* type: object
|
|
2129
|
+
* properties:
|
|
2130
|
+
* url:
|
|
2131
|
+
* type: string
|
|
2132
|
+
* snapshot:
|
|
2133
|
+
* type: string
|
|
2134
|
+
* refsCount:
|
|
2135
|
+
* type: integer
|
|
2136
|
+
* truncated:
|
|
2137
|
+
* type: boolean
|
|
2138
|
+
* totalChars:
|
|
2139
|
+
* type: integer
|
|
2140
|
+
* hasMore:
|
|
2141
|
+
* type: boolean
|
|
2142
|
+
* nextOffset:
|
|
2143
|
+
* type: integer
|
|
2144
|
+
* 404:
|
|
2145
|
+
* description: Tab not found.
|
|
2146
|
+
* content:
|
|
2147
|
+
* application/json:
|
|
2148
|
+
* schema:
|
|
2149
|
+
* $ref: '#/components/schemas/Error'
|
|
2150
|
+
*/
|
|
1752
2151
|
app.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
1753
2152
|
try {
|
|
1754
2153
|
const userId = req.query.userId;
|
|
@@ -1760,7 +2159,7 @@ app.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
1760
2159
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
1761
2160
|
|
|
1762
2161
|
const { tabState } = found;
|
|
1763
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2162
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
1764
2163
|
|
|
1765
2164
|
// Cached chunk retrieval for offset>0 requests
|
|
1766
2165
|
if (offset > 0 && tabState.lastSnapshot) {
|
|
@@ -1889,6 +2288,50 @@ app.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
1889
2288
|
});
|
|
1890
2289
|
|
|
1891
2290
|
// Wait for page ready
|
|
2291
|
+
/**
|
|
2292
|
+
* @openapi
|
|
2293
|
+
* /tabs/{tabId}/wait:
|
|
2294
|
+
* post:
|
|
2295
|
+
* tags: [Interaction]
|
|
2296
|
+
* summary: Wait for a selector or timeout
|
|
2297
|
+
* parameters:
|
|
2298
|
+
* - name: tabId
|
|
2299
|
+
* in: path
|
|
2300
|
+
* required: true
|
|
2301
|
+
* schema:
|
|
2302
|
+
* type: string
|
|
2303
|
+
* requestBody:
|
|
2304
|
+
* required: true
|
|
2305
|
+
* content:
|
|
2306
|
+
* application/json:
|
|
2307
|
+
* schema:
|
|
2308
|
+
* type: object
|
|
2309
|
+
* required: [userId]
|
|
2310
|
+
* properties:
|
|
2311
|
+
* userId:
|
|
2312
|
+
* type: string
|
|
2313
|
+
* selector:
|
|
2314
|
+
* type: string
|
|
2315
|
+
* timeout:
|
|
2316
|
+
* type: integer
|
|
2317
|
+
* description: Max wait in ms.
|
|
2318
|
+
* responses:
|
|
2319
|
+
* 200:
|
|
2320
|
+
* description: Wait completed.
|
|
2321
|
+
* content:
|
|
2322
|
+
* application/json:
|
|
2323
|
+
* schema:
|
|
2324
|
+
* type: object
|
|
2325
|
+
* properties:
|
|
2326
|
+
* ok:
|
|
2327
|
+
* type: boolean
|
|
2328
|
+
* 404:
|
|
2329
|
+
* description: Tab not found.
|
|
2330
|
+
* content:
|
|
2331
|
+
* application/json:
|
|
2332
|
+
* schema:
|
|
2333
|
+
* $ref: '#/components/schemas/Error'
|
|
2334
|
+
*/
|
|
1892
2335
|
app.post('/tabs/:tabId/wait', async (req, res) => {
|
|
1893
2336
|
try {
|
|
1894
2337
|
const { userId, timeout = 10000, waitForNetwork = true } = req.body;
|
|
@@ -1907,6 +2350,64 @@ app.post('/tabs/:tabId/wait', async (req, res) => {
|
|
|
1907
2350
|
});
|
|
1908
2351
|
|
|
1909
2352
|
// Click
|
|
2353
|
+
/**
|
|
2354
|
+
* @openapi
|
|
2355
|
+
* /tabs/{tabId}/click:
|
|
2356
|
+
* post:
|
|
2357
|
+
* tags: [Interaction]
|
|
2358
|
+
* summary: Click an element
|
|
2359
|
+
* description: Click by element ref, CSS selector, or coordinates.
|
|
2360
|
+
* parameters:
|
|
2361
|
+
* - name: tabId
|
|
2362
|
+
* in: path
|
|
2363
|
+
* required: true
|
|
2364
|
+
* schema:
|
|
2365
|
+
* type: string
|
|
2366
|
+
* requestBody:
|
|
2367
|
+
* required: true
|
|
2368
|
+
* content:
|
|
2369
|
+
* application/json:
|
|
2370
|
+
* schema:
|
|
2371
|
+
* type: object
|
|
2372
|
+
* required: [userId]
|
|
2373
|
+
* properties:
|
|
2374
|
+
* userId:
|
|
2375
|
+
* type: string
|
|
2376
|
+
* ref:
|
|
2377
|
+
* type: string
|
|
2378
|
+
* description: Element ref ID (e.g. "e3").
|
|
2379
|
+
* selector:
|
|
2380
|
+
* type: string
|
|
2381
|
+
* description: CSS selector fallback.
|
|
2382
|
+
* doubleClick:
|
|
2383
|
+
* type: boolean
|
|
2384
|
+
* coordinates:
|
|
2385
|
+
* type: object
|
|
2386
|
+
* properties:
|
|
2387
|
+
* x:
|
|
2388
|
+
* type: number
|
|
2389
|
+
* y:
|
|
2390
|
+
* type: number
|
|
2391
|
+
* responses:
|
|
2392
|
+
* 200:
|
|
2393
|
+
* description: Click result with optional post-action snapshot.
|
|
2394
|
+
* content:
|
|
2395
|
+
* application/json:
|
|
2396
|
+
* schema:
|
|
2397
|
+
* type: object
|
|
2398
|
+
* 400:
|
|
2399
|
+
* description: Bad request.
|
|
2400
|
+
* content:
|
|
2401
|
+
* application/json:
|
|
2402
|
+
* schema:
|
|
2403
|
+
* $ref: '#/components/schemas/Error'
|
|
2404
|
+
* 404:
|
|
2405
|
+
* description: Tab not found.
|
|
2406
|
+
* content:
|
|
2407
|
+
* application/json:
|
|
2408
|
+
* schema:
|
|
2409
|
+
* $ref: '#/components/schemas/Error'
|
|
2410
|
+
*/
|
|
1910
2411
|
app.post('/tabs/:tabId/click', async (req, res) => {
|
|
1911
2412
|
const tabId = req.params.tabId;
|
|
1912
2413
|
|
|
@@ -1918,7 +2419,7 @@ app.post('/tabs/:tabId/click', async (req, res) => {
|
|
|
1918
2419
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
1919
2420
|
|
|
1920
2421
|
const { tabState } = found;
|
|
1921
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2422
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
1922
2423
|
|
|
1923
2424
|
if (!ref && !selector) {
|
|
1924
2425
|
return res.status(400).json({ error: 'ref or selector required' });
|
|
@@ -2079,6 +2580,61 @@ app.post('/tabs/:tabId/click', async (req, res) => {
|
|
|
2079
2580
|
});
|
|
2080
2581
|
|
|
2081
2582
|
// Type
|
|
2583
|
+
/**
|
|
2584
|
+
* @openapi
|
|
2585
|
+
* /tabs/{tabId}/type:
|
|
2586
|
+
* post:
|
|
2587
|
+
* tags: [Interaction]
|
|
2588
|
+
* summary: Type text into an element
|
|
2589
|
+
* description: Types text into a focused element or a specific ref/selector.
|
|
2590
|
+
* parameters:
|
|
2591
|
+
* - name: tabId
|
|
2592
|
+
* in: path
|
|
2593
|
+
* required: true
|
|
2594
|
+
* schema:
|
|
2595
|
+
* type: string
|
|
2596
|
+
* requestBody:
|
|
2597
|
+
* required: true
|
|
2598
|
+
* content:
|
|
2599
|
+
* application/json:
|
|
2600
|
+
* schema:
|
|
2601
|
+
* type: object
|
|
2602
|
+
* required: [userId, text]
|
|
2603
|
+
* properties:
|
|
2604
|
+
* userId:
|
|
2605
|
+
* type: string
|
|
2606
|
+
* ref:
|
|
2607
|
+
* type: string
|
|
2608
|
+
* selector:
|
|
2609
|
+
* type: string
|
|
2610
|
+
* text:
|
|
2611
|
+
* type: string
|
|
2612
|
+
* clear:
|
|
2613
|
+
* type: boolean
|
|
2614
|
+
* description: Clear field before typing.
|
|
2615
|
+
* submit:
|
|
2616
|
+
* type: boolean
|
|
2617
|
+
* description: Press Enter after typing.
|
|
2618
|
+
* responses:
|
|
2619
|
+
* 200:
|
|
2620
|
+
* description: Type result.
|
|
2621
|
+
* content:
|
|
2622
|
+
* application/json:
|
|
2623
|
+
* schema:
|
|
2624
|
+
* type: object
|
|
2625
|
+
* 400:
|
|
2626
|
+
* description: Bad request.
|
|
2627
|
+
* content:
|
|
2628
|
+
* application/json:
|
|
2629
|
+
* schema:
|
|
2630
|
+
* $ref: '#/components/schemas/Error'
|
|
2631
|
+
* 404:
|
|
2632
|
+
* description: Tab not found.
|
|
2633
|
+
* content:
|
|
2634
|
+
* application/json:
|
|
2635
|
+
* schema:
|
|
2636
|
+
* $ref: '#/components/schemas/Error'
|
|
2637
|
+
*/
|
|
2082
2638
|
app.post('/tabs/:tabId/type', async (req, res) => {
|
|
2083
2639
|
const tabId = req.params.tabId;
|
|
2084
2640
|
|
|
@@ -2089,7 +2645,7 @@ app.post('/tabs/:tabId/type', async (req, res) => {
|
|
|
2089
2645
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2090
2646
|
|
|
2091
2647
|
const { tabState } = found;
|
|
2092
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2648
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2093
2649
|
|
|
2094
2650
|
if (mode !== 'fill' && mode !== 'keyboard') {
|
|
2095
2651
|
return res.status(400).json({ error: "mode must be 'fill' or 'keyboard'" });
|
|
@@ -2161,6 +2717,48 @@ app.post('/tabs/:tabId/type', async (req, res) => {
|
|
|
2161
2717
|
});
|
|
2162
2718
|
|
|
2163
2719
|
// Press key
|
|
2720
|
+
/**
|
|
2721
|
+
* @openapi
|
|
2722
|
+
* /tabs/{tabId}/press:
|
|
2723
|
+
* post:
|
|
2724
|
+
* tags: [Interaction]
|
|
2725
|
+
* summary: Press a keyboard key
|
|
2726
|
+
* parameters:
|
|
2727
|
+
* - name: tabId
|
|
2728
|
+
* in: path
|
|
2729
|
+
* required: true
|
|
2730
|
+
* schema:
|
|
2731
|
+
* type: string
|
|
2732
|
+
* requestBody:
|
|
2733
|
+
* required: true
|
|
2734
|
+
* content:
|
|
2735
|
+
* application/json:
|
|
2736
|
+
* schema:
|
|
2737
|
+
* type: object
|
|
2738
|
+
* required: [userId, key]
|
|
2739
|
+
* properties:
|
|
2740
|
+
* userId:
|
|
2741
|
+
* type: string
|
|
2742
|
+
* key:
|
|
2743
|
+
* type: string
|
|
2744
|
+
* description: Key name (e.g. "Enter", "Escape", "Tab").
|
|
2745
|
+
* responses:
|
|
2746
|
+
* 200:
|
|
2747
|
+
* description: Key pressed.
|
|
2748
|
+
* content:
|
|
2749
|
+
* application/json:
|
|
2750
|
+
* schema:
|
|
2751
|
+
* type: object
|
|
2752
|
+
* properties:
|
|
2753
|
+
* ok:
|
|
2754
|
+
* type: boolean
|
|
2755
|
+
* 404:
|
|
2756
|
+
* description: Tab not found.
|
|
2757
|
+
* content:
|
|
2758
|
+
* application/json:
|
|
2759
|
+
* schema:
|
|
2760
|
+
* $ref: '#/components/schemas/Error'
|
|
2761
|
+
*/
|
|
2164
2762
|
app.post('/tabs/:tabId/press', async (req, res) => {
|
|
2165
2763
|
const tabId = req.params.tabId;
|
|
2166
2764
|
|
|
@@ -2171,7 +2769,7 @@ app.post('/tabs/:tabId/press', async (req, res) => {
|
|
|
2171
2769
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2172
2770
|
|
|
2173
2771
|
const { tabState } = found;
|
|
2174
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2772
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2175
2773
|
|
|
2176
2774
|
await withTabLock(tabId, async () => {
|
|
2177
2775
|
await tabState.page.keyboard.press(key);
|
|
@@ -2186,6 +2784,51 @@ app.post('/tabs/:tabId/press', async (req, res) => {
|
|
|
2186
2784
|
});
|
|
2187
2785
|
|
|
2188
2786
|
// Scroll
|
|
2787
|
+
/**
|
|
2788
|
+
* @openapi
|
|
2789
|
+
* /tabs/{tabId}/scroll:
|
|
2790
|
+
* post:
|
|
2791
|
+
* tags: [Interaction]
|
|
2792
|
+
* summary: Scroll the page
|
|
2793
|
+
* parameters:
|
|
2794
|
+
* - name: tabId
|
|
2795
|
+
* in: path
|
|
2796
|
+
* required: true
|
|
2797
|
+
* schema:
|
|
2798
|
+
* type: string
|
|
2799
|
+
* requestBody:
|
|
2800
|
+
* required: true
|
|
2801
|
+
* content:
|
|
2802
|
+
* application/json:
|
|
2803
|
+
* schema:
|
|
2804
|
+
* type: object
|
|
2805
|
+
* required: [userId]
|
|
2806
|
+
* properties:
|
|
2807
|
+
* userId:
|
|
2808
|
+
* type: string
|
|
2809
|
+
* direction:
|
|
2810
|
+
* type: string
|
|
2811
|
+
* description: '"up" or "down" (default "down").'
|
|
2812
|
+
* amount:
|
|
2813
|
+
* type: integer
|
|
2814
|
+
* description: Pixels to scroll.
|
|
2815
|
+
* responses:
|
|
2816
|
+
* 200:
|
|
2817
|
+
* description: Scroll result.
|
|
2818
|
+
* content:
|
|
2819
|
+
* application/json:
|
|
2820
|
+
* schema:
|
|
2821
|
+
* type: object
|
|
2822
|
+
* properties:
|
|
2823
|
+
* ok:
|
|
2824
|
+
* type: boolean
|
|
2825
|
+
* 404:
|
|
2826
|
+
* description: Tab not found.
|
|
2827
|
+
* content:
|
|
2828
|
+
* application/json:
|
|
2829
|
+
* schema:
|
|
2830
|
+
* $ref: '#/components/schemas/Error'
|
|
2831
|
+
*/
|
|
2189
2832
|
app.post('/tabs/:tabId/scroll', async (req, res) => {
|
|
2190
2833
|
try {
|
|
2191
2834
|
const { userId, direction = 'down', amount = 500 } = req.body;
|
|
@@ -2194,7 +2837,7 @@ app.post('/tabs/:tabId/scroll', async (req, res) => {
|
|
|
2194
2837
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2195
2838
|
|
|
2196
2839
|
const { tabState } = found;
|
|
2197
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2840
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2198
2841
|
|
|
2199
2842
|
const isVertical = direction === 'up' || direction === 'down';
|
|
2200
2843
|
const delta = (direction === 'up' || direction === 'left') ? -amount : amount;
|
|
@@ -2210,6 +2853,47 @@ app.post('/tabs/:tabId/scroll', async (req, res) => {
|
|
|
2210
2853
|
});
|
|
2211
2854
|
|
|
2212
2855
|
// Back
|
|
2856
|
+
/**
|
|
2857
|
+
* @openapi
|
|
2858
|
+
* /tabs/{tabId}/back:
|
|
2859
|
+
* post:
|
|
2860
|
+
* tags: [Navigation]
|
|
2861
|
+
* summary: Go back
|
|
2862
|
+
* parameters:
|
|
2863
|
+
* - name: tabId
|
|
2864
|
+
* in: path
|
|
2865
|
+
* required: true
|
|
2866
|
+
* schema:
|
|
2867
|
+
* type: string
|
|
2868
|
+
* requestBody:
|
|
2869
|
+
* required: true
|
|
2870
|
+
* content:
|
|
2871
|
+
* application/json:
|
|
2872
|
+
* schema:
|
|
2873
|
+
* type: object
|
|
2874
|
+
* required: [userId]
|
|
2875
|
+
* properties:
|
|
2876
|
+
* userId:
|
|
2877
|
+
* type: string
|
|
2878
|
+
* responses:
|
|
2879
|
+
* 200:
|
|
2880
|
+
* description: Navigated back.
|
|
2881
|
+
* content:
|
|
2882
|
+
* application/json:
|
|
2883
|
+
* schema:
|
|
2884
|
+
* type: object
|
|
2885
|
+
* properties:
|
|
2886
|
+
* ok:
|
|
2887
|
+
* type: boolean
|
|
2888
|
+
* url:
|
|
2889
|
+
* type: string
|
|
2890
|
+
* 404:
|
|
2891
|
+
* description: Tab not found.
|
|
2892
|
+
* content:
|
|
2893
|
+
* application/json:
|
|
2894
|
+
* schema:
|
|
2895
|
+
* $ref: '#/components/schemas/Error'
|
|
2896
|
+
*/
|
|
2213
2897
|
app.post('/tabs/:tabId/back', async (req, res) => {
|
|
2214
2898
|
const tabId = req.params.tabId;
|
|
2215
2899
|
|
|
@@ -2220,7 +2904,7 @@ app.post('/tabs/:tabId/back', async (req, res) => {
|
|
|
2220
2904
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2221
2905
|
|
|
2222
2906
|
const { tabState } = found;
|
|
2223
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2907
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2224
2908
|
|
|
2225
2909
|
const result = await withTabLock(tabId, async () => {
|
|
2226
2910
|
try {
|
|
@@ -2246,6 +2930,47 @@ app.post('/tabs/:tabId/back', async (req, res) => {
|
|
|
2246
2930
|
});
|
|
2247
2931
|
|
|
2248
2932
|
// Forward
|
|
2933
|
+
/**
|
|
2934
|
+
* @openapi
|
|
2935
|
+
* /tabs/{tabId}/forward:
|
|
2936
|
+
* post:
|
|
2937
|
+
* tags: [Navigation]
|
|
2938
|
+
* summary: Go forward
|
|
2939
|
+
* parameters:
|
|
2940
|
+
* - name: tabId
|
|
2941
|
+
* in: path
|
|
2942
|
+
* required: true
|
|
2943
|
+
* schema:
|
|
2944
|
+
* type: string
|
|
2945
|
+
* requestBody:
|
|
2946
|
+
* required: true
|
|
2947
|
+
* content:
|
|
2948
|
+
* application/json:
|
|
2949
|
+
* schema:
|
|
2950
|
+
* type: object
|
|
2951
|
+
* required: [userId]
|
|
2952
|
+
* properties:
|
|
2953
|
+
* userId:
|
|
2954
|
+
* type: string
|
|
2955
|
+
* responses:
|
|
2956
|
+
* 200:
|
|
2957
|
+
* description: Navigated forward.
|
|
2958
|
+
* content:
|
|
2959
|
+
* application/json:
|
|
2960
|
+
* schema:
|
|
2961
|
+
* type: object
|
|
2962
|
+
* properties:
|
|
2963
|
+
* ok:
|
|
2964
|
+
* type: boolean
|
|
2965
|
+
* url:
|
|
2966
|
+
* type: string
|
|
2967
|
+
* 404:
|
|
2968
|
+
* description: Tab not found.
|
|
2969
|
+
* content:
|
|
2970
|
+
* application/json:
|
|
2971
|
+
* schema:
|
|
2972
|
+
* $ref: '#/components/schemas/Error'
|
|
2973
|
+
*/
|
|
2249
2974
|
app.post('/tabs/:tabId/forward', async (req, res) => {
|
|
2250
2975
|
const tabId = req.params.tabId;
|
|
2251
2976
|
|
|
@@ -2256,7 +2981,7 @@ app.post('/tabs/:tabId/forward', async (req, res) => {
|
|
|
2256
2981
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2257
2982
|
|
|
2258
2983
|
const { tabState } = found;
|
|
2259
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
2984
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2260
2985
|
|
|
2261
2986
|
const result = await withTabLock(tabId, async () => {
|
|
2262
2987
|
await tabState.page.goForward({ timeout: 10000 });
|
|
@@ -2272,6 +2997,47 @@ app.post('/tabs/:tabId/forward', async (req, res) => {
|
|
|
2272
2997
|
});
|
|
2273
2998
|
|
|
2274
2999
|
// Refresh
|
|
3000
|
+
/**
|
|
3001
|
+
* @openapi
|
|
3002
|
+
* /tabs/{tabId}/refresh:
|
|
3003
|
+
* post:
|
|
3004
|
+
* tags: [Navigation]
|
|
3005
|
+
* summary: Refresh page
|
|
3006
|
+
* parameters:
|
|
3007
|
+
* - name: tabId
|
|
3008
|
+
* in: path
|
|
3009
|
+
* required: true
|
|
3010
|
+
* schema:
|
|
3011
|
+
* type: string
|
|
3012
|
+
* requestBody:
|
|
3013
|
+
* required: true
|
|
3014
|
+
* content:
|
|
3015
|
+
* application/json:
|
|
3016
|
+
* schema:
|
|
3017
|
+
* type: object
|
|
3018
|
+
* required: [userId]
|
|
3019
|
+
* properties:
|
|
3020
|
+
* userId:
|
|
3021
|
+
* type: string
|
|
3022
|
+
* responses:
|
|
3023
|
+
* 200:
|
|
3024
|
+
* description: Page refreshed.
|
|
3025
|
+
* content:
|
|
3026
|
+
* application/json:
|
|
3027
|
+
* schema:
|
|
3028
|
+
* type: object
|
|
3029
|
+
* properties:
|
|
3030
|
+
* ok:
|
|
3031
|
+
* type: boolean
|
|
3032
|
+
* url:
|
|
3033
|
+
* type: string
|
|
3034
|
+
* 404:
|
|
3035
|
+
* description: Tab not found.
|
|
3036
|
+
* content:
|
|
3037
|
+
* application/json:
|
|
3038
|
+
* schema:
|
|
3039
|
+
* $ref: '#/components/schemas/Error'
|
|
3040
|
+
*/
|
|
2275
3041
|
app.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
2276
3042
|
const tabId = req.params.tabId;
|
|
2277
3043
|
|
|
@@ -2282,7 +3048,7 @@ app.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
|
2282
3048
|
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
2283
3049
|
|
|
2284
3050
|
const { tabState } = found;
|
|
2285
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
3051
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2286
3052
|
|
|
2287
3053
|
const result = await withTabLock(tabId, async () => {
|
|
2288
3054
|
await tabState.page.reload({ timeout: 30000 });
|
|
@@ -2298,6 +3064,49 @@ app.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
|
2298
3064
|
});
|
|
2299
3065
|
|
|
2300
3066
|
// Get links
|
|
3067
|
+
/**
|
|
3068
|
+
* @openapi
|
|
3069
|
+
* /tabs/{tabId}/links:
|
|
3070
|
+
* get:
|
|
3071
|
+
* tags: [Content]
|
|
3072
|
+
* summary: Extract page links
|
|
3073
|
+
* parameters:
|
|
3074
|
+
* - name: tabId
|
|
3075
|
+
* in: path
|
|
3076
|
+
* required: true
|
|
3077
|
+
* schema:
|
|
3078
|
+
* type: string
|
|
3079
|
+
* - name: userId
|
|
3080
|
+
* in: query
|
|
3081
|
+
* required: true
|
|
3082
|
+
* schema:
|
|
3083
|
+
* type: string
|
|
3084
|
+
* responses:
|
|
3085
|
+
* 200:
|
|
3086
|
+
* description: Links extracted.
|
|
3087
|
+
* content:
|
|
3088
|
+
* application/json:
|
|
3089
|
+
* schema:
|
|
3090
|
+
* type: object
|
|
3091
|
+
* properties:
|
|
3092
|
+
* links:
|
|
3093
|
+
* type: array
|
|
3094
|
+
* items:
|
|
3095
|
+
* type: object
|
|
3096
|
+
* properties:
|
|
3097
|
+
* text:
|
|
3098
|
+
* type: string
|
|
3099
|
+
* href:
|
|
3100
|
+
* type: string
|
|
3101
|
+
* ref:
|
|
3102
|
+
* type: string
|
|
3103
|
+
* 404:
|
|
3104
|
+
* description: Tab not found.
|
|
3105
|
+
* content:
|
|
3106
|
+
* application/json:
|
|
3107
|
+
* schema:
|
|
3108
|
+
* $ref: '#/components/schemas/Error'
|
|
3109
|
+
*/
|
|
2301
3110
|
app.get('/tabs/:tabId/links', async (req, res) => {
|
|
2302
3111
|
try {
|
|
2303
3112
|
const userId = req.query.userId;
|
|
@@ -2311,7 +3120,7 @@ app.get('/tabs/:tabId/links', async (req, res) => {
|
|
|
2311
3120
|
}
|
|
2312
3121
|
|
|
2313
3122
|
const { tabState } = found;
|
|
2314
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
3123
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2315
3124
|
|
|
2316
3125
|
const allLinks = await tabState.page.evaluate(() => {
|
|
2317
3126
|
const links = [];
|
|
@@ -2339,6 +3148,49 @@ app.get('/tabs/:tabId/links', async (req, res) => {
|
|
|
2339
3148
|
});
|
|
2340
3149
|
|
|
2341
3150
|
// Get captured downloads
|
|
3151
|
+
/**
|
|
3152
|
+
* @openapi
|
|
3153
|
+
* /tabs/{tabId}/downloads:
|
|
3154
|
+
* get:
|
|
3155
|
+
* tags: [Content]
|
|
3156
|
+
* summary: List tab downloads
|
|
3157
|
+
* parameters:
|
|
3158
|
+
* - name: tabId
|
|
3159
|
+
* in: path
|
|
3160
|
+
* required: true
|
|
3161
|
+
* schema:
|
|
3162
|
+
* type: string
|
|
3163
|
+
* - name: userId
|
|
3164
|
+
* in: query
|
|
3165
|
+
* required: true
|
|
3166
|
+
* schema:
|
|
3167
|
+
* type: string
|
|
3168
|
+
* responses:
|
|
3169
|
+
* 200:
|
|
3170
|
+
* description: Downloads list.
|
|
3171
|
+
* content:
|
|
3172
|
+
* application/json:
|
|
3173
|
+
* schema:
|
|
3174
|
+
* type: object
|
|
3175
|
+
* properties:
|
|
3176
|
+
* downloads:
|
|
3177
|
+
* type: array
|
|
3178
|
+
* items:
|
|
3179
|
+
* type: object
|
|
3180
|
+
* properties:
|
|
3181
|
+
* filename:
|
|
3182
|
+
* type: string
|
|
3183
|
+
* url:
|
|
3184
|
+
* type: string
|
|
3185
|
+
* state:
|
|
3186
|
+
* type: string
|
|
3187
|
+
* 404:
|
|
3188
|
+
* description: Tab not found.
|
|
3189
|
+
* content:
|
|
3190
|
+
* application/json:
|
|
3191
|
+
* schema:
|
|
3192
|
+
* $ref: '#/components/schemas/Error'
|
|
3193
|
+
*/
|
|
2342
3194
|
app.get('/tabs/:tabId/downloads', async (req, res) => {
|
|
2343
3195
|
try {
|
|
2344
3196
|
const userId = req.query.userId;
|
|
@@ -2368,6 +3220,51 @@ app.get('/tabs/:tabId/downloads', async (req, res) => {
|
|
|
2368
3220
|
});
|
|
2369
3221
|
|
|
2370
3222
|
// Get image elements from current page
|
|
3223
|
+
/**
|
|
3224
|
+
* @openapi
|
|
3225
|
+
* /tabs/{tabId}/images:
|
|
3226
|
+
* get:
|
|
3227
|
+
* tags: [Content]
|
|
3228
|
+
* summary: Extract page images
|
|
3229
|
+
* parameters:
|
|
3230
|
+
* - name: tabId
|
|
3231
|
+
* in: path
|
|
3232
|
+
* required: true
|
|
3233
|
+
* schema:
|
|
3234
|
+
* type: string
|
|
3235
|
+
* - name: userId
|
|
3236
|
+
* in: query
|
|
3237
|
+
* required: true
|
|
3238
|
+
* schema:
|
|
3239
|
+
* type: string
|
|
3240
|
+
* responses:
|
|
3241
|
+
* 200:
|
|
3242
|
+
* description: Images extracted.
|
|
3243
|
+
* content:
|
|
3244
|
+
* application/json:
|
|
3245
|
+
* schema:
|
|
3246
|
+
* type: object
|
|
3247
|
+
* properties:
|
|
3248
|
+
* images:
|
|
3249
|
+
* type: array
|
|
3250
|
+
* items:
|
|
3251
|
+
* type: object
|
|
3252
|
+
* properties:
|
|
3253
|
+
* src:
|
|
3254
|
+
* type: string
|
|
3255
|
+
* alt:
|
|
3256
|
+
* type: string
|
|
3257
|
+
* width:
|
|
3258
|
+
* type: integer
|
|
3259
|
+
* height:
|
|
3260
|
+
* type: integer
|
|
3261
|
+
* 404:
|
|
3262
|
+
* description: Tab not found.
|
|
3263
|
+
* content:
|
|
3264
|
+
* application/json:
|
|
3265
|
+
* schema:
|
|
3266
|
+
* $ref: '#/components/schemas/Error'
|
|
3267
|
+
*/
|
|
2371
3268
|
app.get('/tabs/:tabId/images', async (req, res) => {
|
|
2372
3269
|
try {
|
|
2373
3270
|
const userId = req.query.userId;
|
|
@@ -2394,6 +3291,46 @@ app.get('/tabs/:tabId/images', async (req, res) => {
|
|
|
2394
3291
|
});
|
|
2395
3292
|
|
|
2396
3293
|
// Screenshot
|
|
3294
|
+
/**
|
|
3295
|
+
* @openapi
|
|
3296
|
+
* /tabs/{tabId}/screenshot:
|
|
3297
|
+
* get:
|
|
3298
|
+
* tags: [Content]
|
|
3299
|
+
* summary: Take a screenshot
|
|
3300
|
+
* description: Returns a base64-encoded PNG screenshot.
|
|
3301
|
+
* parameters:
|
|
3302
|
+
* - name: tabId
|
|
3303
|
+
* in: path
|
|
3304
|
+
* required: true
|
|
3305
|
+
* schema:
|
|
3306
|
+
* type: string
|
|
3307
|
+
* - name: userId
|
|
3308
|
+
* in: query
|
|
3309
|
+
* required: true
|
|
3310
|
+
* schema:
|
|
3311
|
+
* type: string
|
|
3312
|
+
* responses:
|
|
3313
|
+
* 200:
|
|
3314
|
+
* description: Screenshot.
|
|
3315
|
+
* content:
|
|
3316
|
+
* application/json:
|
|
3317
|
+
* schema:
|
|
3318
|
+
* type: object
|
|
3319
|
+
* properties:
|
|
3320
|
+
* screenshot:
|
|
3321
|
+
* type: object
|
|
3322
|
+
* properties:
|
|
3323
|
+
* data:
|
|
3324
|
+
* type: string
|
|
3325
|
+
* mimeType:
|
|
3326
|
+
* type: string
|
|
3327
|
+
* 404:
|
|
3328
|
+
* description: Tab not found.
|
|
3329
|
+
* content:
|
|
3330
|
+
* application/json:
|
|
3331
|
+
* schema:
|
|
3332
|
+
* $ref: '#/components/schemas/Error'
|
|
3333
|
+
*/
|
|
2397
3334
|
app.get('/tabs/:tabId/screenshot', async (req, res) => {
|
|
2398
3335
|
try {
|
|
2399
3336
|
const userId = req.query.userId;
|
|
@@ -2414,6 +3351,53 @@ app.get('/tabs/:tabId/screenshot', async (req, res) => {
|
|
|
2414
3351
|
});
|
|
2415
3352
|
|
|
2416
3353
|
// Stats
|
|
3354
|
+
/**
|
|
3355
|
+
* @openapi
|
|
3356
|
+
* /tabs/{tabId}/stats:
|
|
3357
|
+
* get:
|
|
3358
|
+
* tags: [Tabs]
|
|
3359
|
+
* summary: Tab statistics
|
|
3360
|
+
* description: Returns tab metadata including URL, tool call count, visited URLs, download/failure counts.
|
|
3361
|
+
* parameters:
|
|
3362
|
+
* - name: tabId
|
|
3363
|
+
* in: path
|
|
3364
|
+
* required: true
|
|
3365
|
+
* schema:
|
|
3366
|
+
* type: string
|
|
3367
|
+
* - name: userId
|
|
3368
|
+
* in: query
|
|
3369
|
+
* required: true
|
|
3370
|
+
* schema:
|
|
3371
|
+
* type: string
|
|
3372
|
+
* responses:
|
|
3373
|
+
* 200:
|
|
3374
|
+
* description: Tab stats.
|
|
3375
|
+
* content:
|
|
3376
|
+
* application/json:
|
|
3377
|
+
* schema:
|
|
3378
|
+
* type: object
|
|
3379
|
+
* properties:
|
|
3380
|
+
* tabId:
|
|
3381
|
+
* type: string
|
|
3382
|
+
* url:
|
|
3383
|
+
* type: string
|
|
3384
|
+
* toolCalls:
|
|
3385
|
+
* type: integer
|
|
3386
|
+
* visitedUrls:
|
|
3387
|
+
* type: array
|
|
3388
|
+
* items:
|
|
3389
|
+
* type: string
|
|
3390
|
+
* downloadCount:
|
|
3391
|
+
* type: integer
|
|
3392
|
+
* consecutiveFailures:
|
|
3393
|
+
* type: integer
|
|
3394
|
+
* 404:
|
|
3395
|
+
* description: Tab not found.
|
|
3396
|
+
* content:
|
|
3397
|
+
* application/json:
|
|
3398
|
+
* schema:
|
|
3399
|
+
* $ref: '#/components/schemas/Error'
|
|
3400
|
+
*/
|
|
2417
3401
|
app.get('/tabs/:tabId/stats', async (req, res) => {
|
|
2418
3402
|
try {
|
|
2419
3403
|
const userId = req.query.userId;
|
|
@@ -2439,6 +3423,56 @@ app.get('/tabs/:tabId/stats', async (req, res) => {
|
|
|
2439
3423
|
});
|
|
2440
3424
|
|
|
2441
3425
|
// Evaluate JavaScript in page context
|
|
3426
|
+
/**
|
|
3427
|
+
* @openapi
|
|
3428
|
+
* /tabs/{tabId}/evaluate:
|
|
3429
|
+
* post:
|
|
3430
|
+
* tags: [Interaction]
|
|
3431
|
+
* summary: Evaluate JavaScript in tab
|
|
3432
|
+
* description: Runs arbitrary JS in the page context and returns the result.
|
|
3433
|
+
* parameters:
|
|
3434
|
+
* - name: tabId
|
|
3435
|
+
* in: path
|
|
3436
|
+
* required: true
|
|
3437
|
+
* schema:
|
|
3438
|
+
* type: string
|
|
3439
|
+
* requestBody:
|
|
3440
|
+
* required: true
|
|
3441
|
+
* content:
|
|
3442
|
+
* application/json:
|
|
3443
|
+
* schema:
|
|
3444
|
+
* type: object
|
|
3445
|
+
* required: [userId, expression]
|
|
3446
|
+
* properties:
|
|
3447
|
+
* userId:
|
|
3448
|
+
* type: string
|
|
3449
|
+
* expression:
|
|
3450
|
+
* type: string
|
|
3451
|
+
* description: JavaScript expression to evaluate.
|
|
3452
|
+
* responses:
|
|
3453
|
+
* 200:
|
|
3454
|
+
* description: Evaluation result.
|
|
3455
|
+
* content:
|
|
3456
|
+
* application/json:
|
|
3457
|
+
* schema:
|
|
3458
|
+
* type: object
|
|
3459
|
+
* properties:
|
|
3460
|
+
* ok:
|
|
3461
|
+
* type: boolean
|
|
3462
|
+
* result: {}
|
|
3463
|
+
* 400:
|
|
3464
|
+
* description: Bad request.
|
|
3465
|
+
* content:
|
|
3466
|
+
* application/json:
|
|
3467
|
+
* schema:
|
|
3468
|
+
* $ref: '#/components/schemas/Error'
|
|
3469
|
+
* 404:
|
|
3470
|
+
* description: Tab not found.
|
|
3471
|
+
* content:
|
|
3472
|
+
* application/json:
|
|
3473
|
+
* schema:
|
|
3474
|
+
* $ref: '#/components/schemas/Error'
|
|
3475
|
+
*/
|
|
2442
3476
|
app.post('/tabs/:tabId/evaluate', express.json({ limit: '1mb' }), async (req, res) => {
|
|
2443
3477
|
try {
|
|
2444
3478
|
const { userId, expression } = req.body;
|
|
@@ -2451,7 +3485,7 @@ app.post('/tabs/:tabId/evaluate', express.json({ limit: '1mb' }), async (req, re
|
|
|
2451
3485
|
|
|
2452
3486
|
session.lastAccess = Date.now();
|
|
2453
3487
|
const { tabState } = found;
|
|
2454
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
3488
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2455
3489
|
|
|
2456
3490
|
pluginEvents.emit('tab:evaluate', { userId, tabId: req.params.tabId, expression });
|
|
2457
3491
|
const result = await tabState.page.evaluate(expression);
|
|
@@ -2465,7 +3499,192 @@ app.post('/tabs/:tabId/evaluate', express.json({ limit: '1mb' }), async (req, re
|
|
|
2465
3499
|
}
|
|
2466
3500
|
});
|
|
2467
3501
|
|
|
3502
|
+
// Structured extraction using JSON Schema with x-ref hints
|
|
3503
|
+
/**
|
|
3504
|
+
* @openapi
|
|
3505
|
+
* /tabs/{tabId}/extract:
|
|
3506
|
+
* post:
|
|
3507
|
+
* tags: [Content]
|
|
3508
|
+
* summary: Structured data extraction via JSON Schema
|
|
3509
|
+
* description: |
|
|
3510
|
+
* Extracts structured data from the current page using a JSON Schema whose properties
|
|
3511
|
+
* carry `x-ref` hints pointing at snapshot element refs (e.g. `e1`, `e2`).
|
|
3512
|
+
* Call `GET /tabs/{tabId}/snapshot` first to populate the ref table.
|
|
3513
|
+
* parameters:
|
|
3514
|
+
* - name: tabId
|
|
3515
|
+
* in: path
|
|
3516
|
+
* required: true
|
|
3517
|
+
* schema:
|
|
3518
|
+
* type: string
|
|
3519
|
+
* requestBody:
|
|
3520
|
+
* required: true
|
|
3521
|
+
* content:
|
|
3522
|
+
* application/json:
|
|
3523
|
+
* schema:
|
|
3524
|
+
* type: object
|
|
3525
|
+
* required: [userId, schema]
|
|
3526
|
+
* properties:
|
|
3527
|
+
* userId:
|
|
3528
|
+
* type: string
|
|
3529
|
+
* schema:
|
|
3530
|
+
* type: object
|
|
3531
|
+
* description: |
|
|
3532
|
+
* JSON Schema with `type: "object"` and a `properties` map.
|
|
3533
|
+
* Each property may include `x-ref` (a snapshot element ref) and an optional
|
|
3534
|
+
* `type` (`string`, `number`, `integer`, `boolean`).
|
|
3535
|
+
* required: [type, properties]
|
|
3536
|
+
* properties:
|
|
3537
|
+
* type:
|
|
3538
|
+
* type: string
|
|
3539
|
+
* enum: [object]
|
|
3540
|
+
* properties:
|
|
3541
|
+
* type: object
|
|
3542
|
+
* additionalProperties:
|
|
3543
|
+
* type: object
|
|
3544
|
+
* properties:
|
|
3545
|
+
* type:
|
|
3546
|
+
* type: string
|
|
3547
|
+
* enum: [string, number, integer, boolean, object, "null"]
|
|
3548
|
+
* x-ref:
|
|
3549
|
+
* type: string
|
|
3550
|
+
* description: Snapshot element ref (e.g. `e1`).
|
|
3551
|
+
* required:
|
|
3552
|
+
* type: array
|
|
3553
|
+
* items:
|
|
3554
|
+
* type: string
|
|
3555
|
+
* description: Property names that must resolve to a non-null value.
|
|
3556
|
+
* responses:
|
|
3557
|
+
* 200:
|
|
3558
|
+
* description: Extraction succeeded.
|
|
3559
|
+
* content:
|
|
3560
|
+
* application/json:
|
|
3561
|
+
* schema:
|
|
3562
|
+
* type: object
|
|
3563
|
+
* properties:
|
|
3564
|
+
* ok:
|
|
3565
|
+
* type: boolean
|
|
3566
|
+
* data:
|
|
3567
|
+
* type: object
|
|
3568
|
+
* description: Extracted key-value pairs matching the input schema.
|
|
3569
|
+
* 400:
|
|
3570
|
+
* description: Missing userId, missing schema, or invalid schema.
|
|
3571
|
+
* content:
|
|
3572
|
+
* application/json:
|
|
3573
|
+
* schema:
|
|
3574
|
+
* $ref: '#/components/schemas/Error'
|
|
3575
|
+
* 404:
|
|
3576
|
+
* description: Tab not found.
|
|
3577
|
+
* content:
|
|
3578
|
+
* application/json:
|
|
3579
|
+
* schema:
|
|
3580
|
+
* $ref: '#/components/schemas/Error'
|
|
3581
|
+
* 409:
|
|
3582
|
+
* description: No refs available — call snapshot first.
|
|
3583
|
+
* content:
|
|
3584
|
+
* application/json:
|
|
3585
|
+
* schema:
|
|
3586
|
+
* type: object
|
|
3587
|
+
* properties:
|
|
3588
|
+
* error:
|
|
3589
|
+
* type: string
|
|
3590
|
+
* snapshot:
|
|
3591
|
+
* type: string
|
|
3592
|
+
* nullable: true
|
|
3593
|
+
* 422:
|
|
3594
|
+
* description: Extraction failed (e.g. required ref not found).
|
|
3595
|
+
* content:
|
|
3596
|
+
* application/json:
|
|
3597
|
+
* schema:
|
|
3598
|
+
* type: object
|
|
3599
|
+
* properties:
|
|
3600
|
+
* ok:
|
|
3601
|
+
* type: boolean
|
|
3602
|
+
* error:
|
|
3603
|
+
* type: string
|
|
3604
|
+
* snapshot:
|
|
3605
|
+
* type: string
|
|
3606
|
+
* nullable: true
|
|
3607
|
+
* 500:
|
|
3608
|
+
* description: Internal server error.
|
|
3609
|
+
* content:
|
|
3610
|
+
* application/json:
|
|
3611
|
+
* schema:
|
|
3612
|
+
* $ref: '#/components/schemas/Error'
|
|
3613
|
+
*/
|
|
3614
|
+
app.post('/tabs/:tabId/extract', express.json({ limit: '256kb' }), async (req, res) => {
|
|
3615
|
+
try {
|
|
3616
|
+
const { userId, schema } = req.body;
|
|
3617
|
+
if (!userId) return res.status(400).json({ error: 'userId is required' });
|
|
3618
|
+
if (!schema) return res.status(400).json({ error: 'schema is required' });
|
|
3619
|
+
|
|
3620
|
+
const check = validateExtractSchema(schema);
|
|
3621
|
+
if (!check.ok) return res.status(400).json({ error: check.error });
|
|
3622
|
+
|
|
3623
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
3624
|
+
const found = session && findTab(session, req.params.tabId);
|
|
3625
|
+
if (!found) return res.status(404).json({ error: 'Tab not found' });
|
|
3626
|
+
|
|
3627
|
+
session.lastAccess = Date.now();
|
|
3628
|
+
const { tabState } = found;
|
|
3629
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
3630
|
+
|
|
3631
|
+
if (!tabState.refs || tabState.refs.size === 0) {
|
|
3632
|
+
return res.status(409).json({
|
|
3633
|
+
error: 'no refs available — call GET /tabs/:tabId/snapshot first to build the ref table',
|
|
3634
|
+
snapshot: tabState.lastSnapshot || null,
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
try {
|
|
3639
|
+
const data = extractDeterministic({ schema, refs: tabState.refs });
|
|
3640
|
+
log('info', 'extract', { reqId: req.reqId, tabId: req.params.tabId, userId, keys: Object.keys(data) });
|
|
3641
|
+
res.json({ ok: true, data });
|
|
3642
|
+
} catch (extractErr) {
|
|
3643
|
+
log('warn', 'extract failed', { reqId: req.reqId, error: extractErr.message });
|
|
3644
|
+
res.status(422).json({ ok: false, error: extractErr.message, snapshot: tabState.lastSnapshot || null });
|
|
3645
|
+
}
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
failuresTotal.labels(classifyError(err), 'extract').inc();
|
|
3648
|
+
log('error', 'extract error', { reqId: req.reqId, error: err.message });
|
|
3649
|
+
res.status(500).json({ error: safeError(err) });
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
|
|
2468
3653
|
// Close tab
|
|
3654
|
+
/**
|
|
3655
|
+
* @openapi
|
|
3656
|
+
* /tabs/{tabId}:
|
|
3657
|
+
* delete:
|
|
3658
|
+
* tags: [Tabs]
|
|
3659
|
+
* summary: Close a tab
|
|
3660
|
+
* parameters:
|
|
3661
|
+
* - name: tabId
|
|
3662
|
+
* in: path
|
|
3663
|
+
* required: true
|
|
3664
|
+
* schema:
|
|
3665
|
+
* type: string
|
|
3666
|
+
* - name: userId
|
|
3667
|
+
* in: query
|
|
3668
|
+
* required: true
|
|
3669
|
+
* schema:
|
|
3670
|
+
* type: string
|
|
3671
|
+
* responses:
|
|
3672
|
+
* 200:
|
|
3673
|
+
* description: Tab closed.
|
|
3674
|
+
* content:
|
|
3675
|
+
* application/json:
|
|
3676
|
+
* schema:
|
|
3677
|
+
* type: object
|
|
3678
|
+
* properties:
|
|
3679
|
+
* ok:
|
|
3680
|
+
* type: boolean
|
|
3681
|
+
* 404:
|
|
3682
|
+
* description: Tab not found.
|
|
3683
|
+
* content:
|
|
3684
|
+
* application/json:
|
|
3685
|
+
* schema:
|
|
3686
|
+
* $ref: '#/components/schemas/Error'
|
|
3687
|
+
*/
|
|
2469
3688
|
app.delete('/tabs/:tabId', async (req, res) => {
|
|
2470
3689
|
try {
|
|
2471
3690
|
const userId = req.query.userId || req.body?.userId;
|
|
@@ -2492,6 +3711,42 @@ app.delete('/tabs/:tabId', async (req, res) => {
|
|
|
2492
3711
|
});
|
|
2493
3712
|
|
|
2494
3713
|
// Close tab group
|
|
3714
|
+
/**
|
|
3715
|
+
* @openapi
|
|
3716
|
+
* /tabs/group/{listItemId}:
|
|
3717
|
+
* delete:
|
|
3718
|
+
* tags: [Tabs]
|
|
3719
|
+
* summary: Close all tabs in a group
|
|
3720
|
+
* parameters:
|
|
3721
|
+
* - name: listItemId
|
|
3722
|
+
* in: path
|
|
3723
|
+
* required: true
|
|
3724
|
+
* schema:
|
|
3725
|
+
* type: string
|
|
3726
|
+
* - name: userId
|
|
3727
|
+
* in: query
|
|
3728
|
+
* required: true
|
|
3729
|
+
* schema:
|
|
3730
|
+
* type: string
|
|
3731
|
+
* responses:
|
|
3732
|
+
* 200:
|
|
3733
|
+
* description: Group closed.
|
|
3734
|
+
* content:
|
|
3735
|
+
* application/json:
|
|
3736
|
+
* schema:
|
|
3737
|
+
* type: object
|
|
3738
|
+
* properties:
|
|
3739
|
+
* ok:
|
|
3740
|
+
* type: boolean
|
|
3741
|
+
* closed:
|
|
3742
|
+
* type: integer
|
|
3743
|
+
* 404:
|
|
3744
|
+
* description: Session not found.
|
|
3745
|
+
* content:
|
|
3746
|
+
* application/json:
|
|
3747
|
+
* schema:
|
|
3748
|
+
* $ref: '#/components/schemas/Error'
|
|
3749
|
+
*/
|
|
2495
3750
|
app.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
2496
3751
|
try {
|
|
2497
3752
|
const userId = req.query.userId || req.body?.userId;
|
|
@@ -2520,7 +3775,254 @@ app.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
|
2520
3775
|
}
|
|
2521
3776
|
});
|
|
2522
3777
|
|
|
3778
|
+
// List trace files for a session
|
|
3779
|
+
/**
|
|
3780
|
+
* @openapi
|
|
3781
|
+
* /sessions/{userId}/traces:
|
|
3782
|
+
* get:
|
|
3783
|
+
* tags: [Sessions]
|
|
3784
|
+
* summary: List trace files
|
|
3785
|
+
* description: Returns all Playwright trace zip files for the given user session, sorted newest first.
|
|
3786
|
+
* security:
|
|
3787
|
+
* - BearerAuth: []
|
|
3788
|
+
* parameters:
|
|
3789
|
+
* - name: userId
|
|
3790
|
+
* in: path
|
|
3791
|
+
* required: true
|
|
3792
|
+
* schema:
|
|
3793
|
+
* type: string
|
|
3794
|
+
* description: Session owner identifier.
|
|
3795
|
+
* responses:
|
|
3796
|
+
* 200:
|
|
3797
|
+
* description: Trace list.
|
|
3798
|
+
* content:
|
|
3799
|
+
* application/json:
|
|
3800
|
+
* schema:
|
|
3801
|
+
* type: object
|
|
3802
|
+
* properties:
|
|
3803
|
+
* traces:
|
|
3804
|
+
* type: array
|
|
3805
|
+
* items:
|
|
3806
|
+
* type: object
|
|
3807
|
+
* properties:
|
|
3808
|
+
* filename:
|
|
3809
|
+
* type: string
|
|
3810
|
+
* sizeBytes:
|
|
3811
|
+
* type: integer
|
|
3812
|
+
* createdAt:
|
|
3813
|
+
* type: number
|
|
3814
|
+
* modifiedAt:
|
|
3815
|
+
* type: number
|
|
3816
|
+
* 403:
|
|
3817
|
+
* description: Forbidden.
|
|
3818
|
+
* content:
|
|
3819
|
+
* application/json:
|
|
3820
|
+
* schema:
|
|
3821
|
+
* $ref: '#/components/schemas/Error'
|
|
3822
|
+
* 500:
|
|
3823
|
+
* description: Server error.
|
|
3824
|
+
* content:
|
|
3825
|
+
* application/json:
|
|
3826
|
+
* schema:
|
|
3827
|
+
* $ref: '#/components/schemas/Error'
|
|
3828
|
+
*/
|
|
3829
|
+
app.get('/sessions/:userId/traces', authMiddleware(), async (req, res) => {
|
|
3830
|
+
try {
|
|
3831
|
+
const userId = normalizeUserId(req.params.userId);
|
|
3832
|
+
const traces = await listUserTraces(CONFIG.tracesDir, userId);
|
|
3833
|
+
res.json({ traces });
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
log('error', 'list traces failed', { error: err.message });
|
|
3836
|
+
res.status(500).json({ error: err.message });
|
|
3837
|
+
}
|
|
3838
|
+
});
|
|
3839
|
+
|
|
3840
|
+
// Stream one trace file
|
|
3841
|
+
/**
|
|
3842
|
+
* @openapi
|
|
3843
|
+
* /sessions/{userId}/traces/{filename}:
|
|
3844
|
+
* get:
|
|
3845
|
+
* tags: [Sessions]
|
|
3846
|
+
* summary: Download a trace file
|
|
3847
|
+
* description: Streams a Playwright trace zip for viewing in trace.playwright.dev.
|
|
3848
|
+
* security:
|
|
3849
|
+
* - BearerAuth: []
|
|
3850
|
+
* parameters:
|
|
3851
|
+
* - name: userId
|
|
3852
|
+
* in: path
|
|
3853
|
+
* required: true
|
|
3854
|
+
* schema:
|
|
3855
|
+
* type: string
|
|
3856
|
+
* description: Session owner identifier.
|
|
3857
|
+
* - name: filename
|
|
3858
|
+
* in: path
|
|
3859
|
+
* required: true
|
|
3860
|
+
* schema:
|
|
3861
|
+
* type: string
|
|
3862
|
+
* description: Trace zip filename.
|
|
3863
|
+
* responses:
|
|
3864
|
+
* 200:
|
|
3865
|
+
* description: Trace zip stream.
|
|
3866
|
+
* content:
|
|
3867
|
+
* application/zip:
|
|
3868
|
+
* schema:
|
|
3869
|
+
* type: string
|
|
3870
|
+
* format: binary
|
|
3871
|
+
* 400:
|
|
3872
|
+
* description: Invalid filename.
|
|
3873
|
+
* content:
|
|
3874
|
+
* application/json:
|
|
3875
|
+
* schema:
|
|
3876
|
+
* $ref: '#/components/schemas/Error'
|
|
3877
|
+
* 404:
|
|
3878
|
+
* description: Trace not found.
|
|
3879
|
+
* content:
|
|
3880
|
+
* application/json:
|
|
3881
|
+
* schema:
|
|
3882
|
+
* $ref: '#/components/schemas/Error'
|
|
3883
|
+
* 403:
|
|
3884
|
+
* description: Forbidden.
|
|
3885
|
+
* content:
|
|
3886
|
+
* application/json:
|
|
3887
|
+
* schema:
|
|
3888
|
+
* $ref: '#/components/schemas/Error'
|
|
3889
|
+
* 500:
|
|
3890
|
+
* description: Server error.
|
|
3891
|
+
* content:
|
|
3892
|
+
* application/json:
|
|
3893
|
+
* schema:
|
|
3894
|
+
* $ref: '#/components/schemas/Error'
|
|
3895
|
+
*/
|
|
3896
|
+
app.get('/sessions/:userId/traces/:filename', authMiddleware(), async (req, res) => {
|
|
3897
|
+
try {
|
|
3898
|
+
const userId = normalizeUserId(req.params.userId);
|
|
3899
|
+
const full = resolveTracePath(CONFIG.tracesDir, userId, req.params.filename);
|
|
3900
|
+
if (!full) return res.status(400).json({ error: 'invalid filename' });
|
|
3901
|
+
const st = await statTrace(full);
|
|
3902
|
+
if (!st) return res.status(404).json({ error: 'not found' });
|
|
3903
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
3904
|
+
res.setHeader('Content-Length', String(st.size));
|
|
3905
|
+
const stream = fs.createReadStream(full);
|
|
3906
|
+
stream.on('error', (err) => {
|
|
3907
|
+
if (!res.headersSent) res.status(404).json({ error: 'not found' });
|
|
3908
|
+
else res.destroy();
|
|
3909
|
+
});
|
|
3910
|
+
stream.pipe(res);
|
|
3911
|
+
} catch (err) {
|
|
3912
|
+
log('error', 'stream trace failed', { error: err.message });
|
|
3913
|
+
res.status(500).json({ error: err.message });
|
|
3914
|
+
}
|
|
3915
|
+
});
|
|
3916
|
+
|
|
3917
|
+
// Delete one trace file
|
|
3918
|
+
/**
|
|
3919
|
+
* @openapi
|
|
3920
|
+
* /sessions/{userId}/traces/{filename}:
|
|
3921
|
+
* delete:
|
|
3922
|
+
* tags: [Sessions]
|
|
3923
|
+
* summary: Delete a trace file
|
|
3924
|
+
* description: Removes a specific Playwright trace zip from the server.
|
|
3925
|
+
* security:
|
|
3926
|
+
* - BearerAuth: []
|
|
3927
|
+
* parameters:
|
|
3928
|
+
* - name: userId
|
|
3929
|
+
* in: path
|
|
3930
|
+
* required: true
|
|
3931
|
+
* schema:
|
|
3932
|
+
* type: string
|
|
3933
|
+
* description: Session owner identifier.
|
|
3934
|
+
* - name: filename
|
|
3935
|
+
* in: path
|
|
3936
|
+
* required: true
|
|
3937
|
+
* schema:
|
|
3938
|
+
* type: string
|
|
3939
|
+
* description: Trace zip filename.
|
|
3940
|
+
* responses:
|
|
3941
|
+
* 200:
|
|
3942
|
+
* description: Trace deleted.
|
|
3943
|
+
* content:
|
|
3944
|
+
* application/json:
|
|
3945
|
+
* schema:
|
|
3946
|
+
* type: object
|
|
3947
|
+
* properties:
|
|
3948
|
+
* ok:
|
|
3949
|
+
* type: boolean
|
|
3950
|
+
* 400:
|
|
3951
|
+
* description: Invalid filename.
|
|
3952
|
+
* content:
|
|
3953
|
+
* application/json:
|
|
3954
|
+
* schema:
|
|
3955
|
+
* $ref: '#/components/schemas/Error'
|
|
3956
|
+
* 404:
|
|
3957
|
+
* description: Trace not found.
|
|
3958
|
+
* content:
|
|
3959
|
+
* application/json:
|
|
3960
|
+
* schema:
|
|
3961
|
+
* $ref: '#/components/schemas/Error'
|
|
3962
|
+
* 403:
|
|
3963
|
+
* description: Forbidden.
|
|
3964
|
+
* content:
|
|
3965
|
+
* application/json:
|
|
3966
|
+
* schema:
|
|
3967
|
+
* $ref: '#/components/schemas/Error'
|
|
3968
|
+
* 500:
|
|
3969
|
+
* description: Server error.
|
|
3970
|
+
* content:
|
|
3971
|
+
* application/json:
|
|
3972
|
+
* schema:
|
|
3973
|
+
* $ref: '#/components/schemas/Error'
|
|
3974
|
+
*/
|
|
3975
|
+
app.delete('/sessions/:userId/traces/:filename', authMiddleware(), async (req, res) => {
|
|
3976
|
+
try {
|
|
3977
|
+
const userId = normalizeUserId(req.params.userId);
|
|
3978
|
+
const full = resolveTracePath(CONFIG.tracesDir, userId, req.params.filename);
|
|
3979
|
+
if (!full) return res.status(400).json({ error: 'invalid filename' });
|
|
3980
|
+
try {
|
|
3981
|
+
await deleteTrace(full);
|
|
3982
|
+
} catch (err) {
|
|
3983
|
+
if (err.code === 'ENOENT') return res.status(404).json({ error: 'not found' });
|
|
3984
|
+
throw err;
|
|
3985
|
+
}
|
|
3986
|
+
res.json({ ok: true });
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
log('error', 'delete trace failed', { error: err.message });
|
|
3989
|
+
res.status(500).json({ error: err.message });
|
|
3990
|
+
}
|
|
3991
|
+
});
|
|
3992
|
+
|
|
2523
3993
|
// Close session
|
|
3994
|
+
/**
|
|
3995
|
+
* @openapi
|
|
3996
|
+
* /sessions/{userId}:
|
|
3997
|
+
* delete:
|
|
3998
|
+
* tags: [Sessions]
|
|
3999
|
+
* summary: Destroy a user session
|
|
4000
|
+
* description: Closes all tabs and cleans up state for the given userId.
|
|
4001
|
+
* parameters:
|
|
4002
|
+
* - name: userId
|
|
4003
|
+
* in: path
|
|
4004
|
+
* required: true
|
|
4005
|
+
* schema:
|
|
4006
|
+
* type: string
|
|
4007
|
+
* responses:
|
|
4008
|
+
* 200:
|
|
4009
|
+
* description: Session destroyed.
|
|
4010
|
+
* content:
|
|
4011
|
+
* application/json:
|
|
4012
|
+
* schema:
|
|
4013
|
+
* type: object
|
|
4014
|
+
* properties:
|
|
4015
|
+
* ok:
|
|
4016
|
+
* type: boolean
|
|
4017
|
+
* closed:
|
|
4018
|
+
* type: integer
|
|
4019
|
+
* 404:
|
|
4020
|
+
* description: Session not found.
|
|
4021
|
+
* content:
|
|
4022
|
+
* application/json:
|
|
4023
|
+
* schema:
|
|
4024
|
+
* $ref: '#/components/schemas/Error'
|
|
4025
|
+
*/
|
|
2524
4026
|
app.delete('/sessions/:userId', async (req, res) => {
|
|
2525
4027
|
try {
|
|
2526
4028
|
const userId = normalizeUserId(req.params.userId);
|
|
@@ -2605,6 +4107,34 @@ setInterval(() => {
|
|
|
2605
4107
|
// =============================================================================
|
|
2606
4108
|
|
|
2607
4109
|
// GET / - Status (passive — does not launch browser)
|
|
4110
|
+
/**
|
|
4111
|
+
* @openapi
|
|
4112
|
+
* /:
|
|
4113
|
+
* get:
|
|
4114
|
+
* tags: [System]
|
|
4115
|
+
* summary: Server status
|
|
4116
|
+
* description: Returns basic server liveness and browser state.
|
|
4117
|
+
* responses:
|
|
4118
|
+
* 200:
|
|
4119
|
+
* description: Server status.
|
|
4120
|
+
* content:
|
|
4121
|
+
* application/json:
|
|
4122
|
+
* schema:
|
|
4123
|
+
* type: object
|
|
4124
|
+
* properties:
|
|
4125
|
+
* ok:
|
|
4126
|
+
* type: boolean
|
|
4127
|
+
* enabled:
|
|
4128
|
+
* type: boolean
|
|
4129
|
+
* running:
|
|
4130
|
+
* type: boolean
|
|
4131
|
+
* engine:
|
|
4132
|
+
* type: string
|
|
4133
|
+
* browserConnected:
|
|
4134
|
+
* type: boolean
|
|
4135
|
+
* browserRunning:
|
|
4136
|
+
* type: boolean
|
|
4137
|
+
*/
|
|
2608
4138
|
app.get('/', (req, res) => {
|
|
2609
4139
|
const running = browser !== null && (browser.isConnected?.() ?? false);
|
|
2610
4140
|
res.json({
|
|
@@ -2618,6 +4148,45 @@ app.get('/', (req, res) => {
|
|
|
2618
4148
|
});
|
|
2619
4149
|
|
|
2620
4150
|
// GET /tabs - List all tabs (OpenClaw expects this)
|
|
4151
|
+
/**
|
|
4152
|
+
* @openapi
|
|
4153
|
+
* /tabs:
|
|
4154
|
+
* get:
|
|
4155
|
+
* tags: [Tabs]
|
|
4156
|
+
* summary: List open tabs
|
|
4157
|
+
* description: Returns all tabs for a given userId.
|
|
4158
|
+
* parameters:
|
|
4159
|
+
* - name: userId
|
|
4160
|
+
* in: query
|
|
4161
|
+
* schema:
|
|
4162
|
+
* type: string
|
|
4163
|
+
* description: Filter by session owner.
|
|
4164
|
+
* responses:
|
|
4165
|
+
* 200:
|
|
4166
|
+
* description: Tab list.
|
|
4167
|
+
* content:
|
|
4168
|
+
* application/json:
|
|
4169
|
+
* schema:
|
|
4170
|
+
* type: object
|
|
4171
|
+
* properties:
|
|
4172
|
+
* running:
|
|
4173
|
+
* type: boolean
|
|
4174
|
+
* tabs:
|
|
4175
|
+
* type: array
|
|
4176
|
+
* items:
|
|
4177
|
+
* type: object
|
|
4178
|
+
* properties:
|
|
4179
|
+
* tabId:
|
|
4180
|
+
* type: string
|
|
4181
|
+
* targetId:
|
|
4182
|
+
* type: string
|
|
4183
|
+
* url:
|
|
4184
|
+
* type: string
|
|
4185
|
+
* title:
|
|
4186
|
+
* type: string
|
|
4187
|
+
* listItemId:
|
|
4188
|
+
* type: string
|
|
4189
|
+
*/
|
|
2621
4190
|
app.get('/tabs', async (req, res) => {
|
|
2622
4191
|
try {
|
|
2623
4192
|
const userId = req.query.userId;
|
|
@@ -2648,6 +4217,41 @@ app.get('/tabs', async (req, res) => {
|
|
|
2648
4217
|
});
|
|
2649
4218
|
|
|
2650
4219
|
// POST /tabs/open - Open tab (alias for POST /tabs, OpenClaw format)
|
|
4220
|
+
/**
|
|
4221
|
+
* @openapi
|
|
4222
|
+
* /tabs/open:
|
|
4223
|
+
* post:
|
|
4224
|
+
* tags: [Legacy]
|
|
4225
|
+
* summary: Open tab (OpenClaw format)
|
|
4226
|
+
* deprecated: true
|
|
4227
|
+
* requestBody:
|
|
4228
|
+
* required: true
|
|
4229
|
+
* content:
|
|
4230
|
+
* application/json:
|
|
4231
|
+
* schema:
|
|
4232
|
+
* type: object
|
|
4233
|
+
* required: [userId, url]
|
|
4234
|
+
* properties:
|
|
4235
|
+
* userId:
|
|
4236
|
+
* type: string
|
|
4237
|
+
* url:
|
|
4238
|
+
* type: string
|
|
4239
|
+
* listItemId:
|
|
4240
|
+
* type: string
|
|
4241
|
+
* responses:
|
|
4242
|
+
* 200:
|
|
4243
|
+
* description: Tab opened.
|
|
4244
|
+
* content:
|
|
4245
|
+
* application/json:
|
|
4246
|
+
* schema:
|
|
4247
|
+
* type: object
|
|
4248
|
+
* 400:
|
|
4249
|
+
* description: Bad request.
|
|
4250
|
+
* content:
|
|
4251
|
+
* application/json:
|
|
4252
|
+
* schema:
|
|
4253
|
+
* $ref: '#/components/schemas/Error'
|
|
4254
|
+
*/
|
|
2651
4255
|
app.post('/tabs/open', async (req, res) => {
|
|
2652
4256
|
try {
|
|
2653
4257
|
const { url, userId, listItemId = 'default' } = req.body;
|
|
@@ -2700,6 +4304,32 @@ app.post('/tabs/open', async (req, res) => {
|
|
|
2700
4304
|
});
|
|
2701
4305
|
|
|
2702
4306
|
// POST /start - Start browser (OpenClaw expects this)
|
|
4307
|
+
/**
|
|
4308
|
+
* @openapi
|
|
4309
|
+
* /start:
|
|
4310
|
+
* post:
|
|
4311
|
+
* tags: [Browser]
|
|
4312
|
+
* summary: Start browser
|
|
4313
|
+
* description: Ensures the browser process is running. Idempotent.
|
|
4314
|
+
* responses:
|
|
4315
|
+
* 200:
|
|
4316
|
+
* description: Browser started.
|
|
4317
|
+
* content:
|
|
4318
|
+
* application/json:
|
|
4319
|
+
* schema:
|
|
4320
|
+
* type: object
|
|
4321
|
+
* properties:
|
|
4322
|
+
* ok:
|
|
4323
|
+
* type: boolean
|
|
4324
|
+
* profile:
|
|
4325
|
+
* type: string
|
|
4326
|
+
* 500:
|
|
4327
|
+
* description: Launch failed.
|
|
4328
|
+
* content:
|
|
4329
|
+
* application/json:
|
|
4330
|
+
* schema:
|
|
4331
|
+
* $ref: '#/components/schemas/Error'
|
|
4332
|
+
*/
|
|
2703
4333
|
app.post('/start', async (req, res) => {
|
|
2704
4334
|
try {
|
|
2705
4335
|
await ensureBrowser();
|
|
@@ -2711,6 +4341,36 @@ app.post('/start', async (req, res) => {
|
|
|
2711
4341
|
});
|
|
2712
4342
|
|
|
2713
4343
|
// POST /stop - Stop browser (OpenClaw expects this)
|
|
4344
|
+
/**
|
|
4345
|
+
* @openapi
|
|
4346
|
+
* /stop:
|
|
4347
|
+
* post:
|
|
4348
|
+
* tags: [Browser]
|
|
4349
|
+
* summary: Stop browser
|
|
4350
|
+
* description: Stops the browser and closes all sessions. Requires x-admin-key header.
|
|
4351
|
+
* security:
|
|
4352
|
+
* - BearerAuth: []
|
|
4353
|
+
* responses:
|
|
4354
|
+
* 200:
|
|
4355
|
+
* description: Browser stopped.
|
|
4356
|
+
* content:
|
|
4357
|
+
* application/json:
|
|
4358
|
+
* schema:
|
|
4359
|
+
* type: object
|
|
4360
|
+
* properties:
|
|
4361
|
+
* ok:
|
|
4362
|
+
* type: boolean
|
|
4363
|
+
* stopped:
|
|
4364
|
+
* type: boolean
|
|
4365
|
+
* profile:
|
|
4366
|
+
* type: string
|
|
4367
|
+
* 403:
|
|
4368
|
+
* description: Forbidden.
|
|
4369
|
+
* content:
|
|
4370
|
+
* application/json:
|
|
4371
|
+
* schema:
|
|
4372
|
+
* $ref: '#/components/schemas/Error'
|
|
4373
|
+
*/
|
|
2714
4374
|
app.post('/stop', async (req, res) => {
|
|
2715
4375
|
try {
|
|
2716
4376
|
const adminKey = req.headers['x-admin-key'];
|
|
@@ -2729,6 +4389,48 @@ app.post('/stop', async (req, res) => {
|
|
|
2729
4389
|
});
|
|
2730
4390
|
|
|
2731
4391
|
// POST /navigate - Navigate (OpenClaw format with targetId in body)
|
|
4392
|
+
/**
|
|
4393
|
+
* @openapi
|
|
4394
|
+
* /navigate:
|
|
4395
|
+
* post:
|
|
4396
|
+
* tags: [Legacy]
|
|
4397
|
+
* summary: Navigate (OpenClaw format)
|
|
4398
|
+
* description: Navigate with targetId in body instead of path param.
|
|
4399
|
+
* deprecated: true
|
|
4400
|
+
* requestBody:
|
|
4401
|
+
* required: true
|
|
4402
|
+
* content:
|
|
4403
|
+
* application/json:
|
|
4404
|
+
* schema:
|
|
4405
|
+
* type: object
|
|
4406
|
+
* required: [userId, url]
|
|
4407
|
+
* properties:
|
|
4408
|
+
* userId:
|
|
4409
|
+
* type: string
|
|
4410
|
+
* targetId:
|
|
4411
|
+
* type: string
|
|
4412
|
+
* url:
|
|
4413
|
+
* type: string
|
|
4414
|
+
* responses:
|
|
4415
|
+
* 200:
|
|
4416
|
+
* description: Navigation result.
|
|
4417
|
+
* content:
|
|
4418
|
+
* application/json:
|
|
4419
|
+
* schema:
|
|
4420
|
+
* type: object
|
|
4421
|
+
* 400:
|
|
4422
|
+
* description: Bad request.
|
|
4423
|
+
* content:
|
|
4424
|
+
* application/json:
|
|
4425
|
+
* schema:
|
|
4426
|
+
* $ref: '#/components/schemas/Error'
|
|
4427
|
+
* 404:
|
|
4428
|
+
* description: Tab not found.
|
|
4429
|
+
* content:
|
|
4430
|
+
* application/json:
|
|
4431
|
+
* schema:
|
|
4432
|
+
* $ref: '#/components/schemas/Error'
|
|
4433
|
+
*/
|
|
2732
4434
|
app.post('/navigate', async (req, res) => {
|
|
2733
4435
|
try {
|
|
2734
4436
|
const { targetId, url, userId } = req.body;
|
|
@@ -2749,7 +4451,7 @@ app.post('/navigate', async (req, res) => {
|
|
|
2749
4451
|
}
|
|
2750
4452
|
|
|
2751
4453
|
const { tabState } = found;
|
|
2752
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
4454
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2753
4455
|
|
|
2754
4456
|
const result = await withTabLock(targetId, async () => {
|
|
2755
4457
|
await withPageLoadDuration('navigate', () => tabState.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }));
|
|
@@ -2774,6 +4476,58 @@ app.post('/navigate', async (req, res) => {
|
|
|
2774
4476
|
});
|
|
2775
4477
|
|
|
2776
4478
|
// GET /snapshot - Snapshot (OpenClaw format with query params)
|
|
4479
|
+
/**
|
|
4480
|
+
* @openapi
|
|
4481
|
+
* /snapshot:
|
|
4482
|
+
* get:
|
|
4483
|
+
* tags: [Legacy]
|
|
4484
|
+
* summary: Snapshot (OpenClaw format)
|
|
4485
|
+
* description: Snapshot with targetId/userId as query params.
|
|
4486
|
+
* deprecated: true
|
|
4487
|
+
* parameters:
|
|
4488
|
+
* - name: targetId
|
|
4489
|
+
* in: query
|
|
4490
|
+
* required: true
|
|
4491
|
+
* schema:
|
|
4492
|
+
* type: string
|
|
4493
|
+
* - name: userId
|
|
4494
|
+
* in: query
|
|
4495
|
+
* required: true
|
|
4496
|
+
* schema:
|
|
4497
|
+
* type: string
|
|
4498
|
+
* - name: format
|
|
4499
|
+
* in: query
|
|
4500
|
+
* schema:
|
|
4501
|
+
* type: string
|
|
4502
|
+
* - name: offset
|
|
4503
|
+
* in: query
|
|
4504
|
+
* schema:
|
|
4505
|
+
* type: integer
|
|
4506
|
+
* - name: includeScreenshot
|
|
4507
|
+
* in: query
|
|
4508
|
+
* schema:
|
|
4509
|
+
* type: string
|
|
4510
|
+
* enum: ['true', 'false']
|
|
4511
|
+
* responses:
|
|
4512
|
+
* 200:
|
|
4513
|
+
* description: Snapshot.
|
|
4514
|
+
* content:
|
|
4515
|
+
* application/json:
|
|
4516
|
+
* schema:
|
|
4517
|
+
* type: object
|
|
4518
|
+
* 400:
|
|
4519
|
+
* description: Bad request.
|
|
4520
|
+
* content:
|
|
4521
|
+
* application/json:
|
|
4522
|
+
* schema:
|
|
4523
|
+
* $ref: '#/components/schemas/Error'
|
|
4524
|
+
* 404:
|
|
4525
|
+
* description: Tab not found.
|
|
4526
|
+
* content:
|
|
4527
|
+
* application/json:
|
|
4528
|
+
* schema:
|
|
4529
|
+
* $ref: '#/components/schemas/Error'
|
|
4530
|
+
*/
|
|
2777
4531
|
app.get('/snapshot', async (req, res) => {
|
|
2778
4532
|
try {
|
|
2779
4533
|
const { targetId, userId, format = 'text' } = req.query;
|
|
@@ -2789,7 +4543,7 @@ app.get('/snapshot', async (req, res) => {
|
|
|
2789
4543
|
}
|
|
2790
4544
|
|
|
2791
4545
|
const { tabState } = found;
|
|
2792
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
4546
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2793
4547
|
|
|
2794
4548
|
// Cached chunk retrieval
|
|
2795
4549
|
if (offset > 0 && tabState.lastSnapshot) {
|
|
@@ -2884,6 +4638,61 @@ app.get('/snapshot', async (req, res) => {
|
|
|
2884
4638
|
|
|
2885
4639
|
// POST /act - Combined action endpoint (OpenClaw format)
|
|
2886
4640
|
// Routes to click/type/scroll/press/etc based on 'kind' parameter
|
|
4641
|
+
/**
|
|
4642
|
+
* @openapi
|
|
4643
|
+
* /act:
|
|
4644
|
+
* post:
|
|
4645
|
+
* tags: [Legacy]
|
|
4646
|
+
* summary: Combined action (OpenClaw format)
|
|
4647
|
+
* description: Routes to click/type/scroll/press/etc based on "kind" parameter.
|
|
4648
|
+
* deprecated: true
|
|
4649
|
+
* requestBody:
|
|
4650
|
+
* required: true
|
|
4651
|
+
* content:
|
|
4652
|
+
* application/json:
|
|
4653
|
+
* schema:
|
|
4654
|
+
* type: object
|
|
4655
|
+
* required: [userId, kind]
|
|
4656
|
+
* properties:
|
|
4657
|
+
* userId:
|
|
4658
|
+
* type: string
|
|
4659
|
+
* kind:
|
|
4660
|
+
* type: string
|
|
4661
|
+
* description: 'Action kind: click, type, scroll, press, key, select_option, drag, hover, screenshot, wait, back, forward.'
|
|
4662
|
+
* targetId:
|
|
4663
|
+
* type: string
|
|
4664
|
+
* ref:
|
|
4665
|
+
* type: string
|
|
4666
|
+
* selector:
|
|
4667
|
+
* type: string
|
|
4668
|
+
* text:
|
|
4669
|
+
* type: string
|
|
4670
|
+
* key:
|
|
4671
|
+
* type: string
|
|
4672
|
+
* direction:
|
|
4673
|
+
* type: string
|
|
4674
|
+
* url:
|
|
4675
|
+
* type: string
|
|
4676
|
+
* responses:
|
|
4677
|
+
* 200:
|
|
4678
|
+
* description: Action result.
|
|
4679
|
+
* content:
|
|
4680
|
+
* application/json:
|
|
4681
|
+
* schema:
|
|
4682
|
+
* type: object
|
|
4683
|
+
* 400:
|
|
4684
|
+
* description: Bad request.
|
|
4685
|
+
* content:
|
|
4686
|
+
* application/json:
|
|
4687
|
+
* schema:
|
|
4688
|
+
* $ref: '#/components/schemas/Error'
|
|
4689
|
+
* 404:
|
|
4690
|
+
* description: Tab not found.
|
|
4691
|
+
* content:
|
|
4692
|
+
* application/json:
|
|
4693
|
+
* schema:
|
|
4694
|
+
* $ref: '#/components/schemas/Error'
|
|
4695
|
+
*/
|
|
2887
4696
|
app.post('/act', async (req, res) => {
|
|
2888
4697
|
try {
|
|
2889
4698
|
const { kind, targetId, userId, ...params } = req.body;
|
|
@@ -2902,7 +4711,7 @@ app.post('/act', async (req, res) => {
|
|
|
2902
4711
|
}
|
|
2903
4712
|
|
|
2904
4713
|
const { tabState } = found;
|
|
2905
|
-
tabState.toolCalls++; tabState.consecutiveTimeouts = 0;
|
|
4714
|
+
tabState.toolCalls++; tabState.consecutiveTimeouts = 0; tabState.consecutiveFailures = 0;
|
|
2906
4715
|
|
|
2907
4716
|
const result = await withTabLock(targetId, async () => {
|
|
2908
4717
|
switch (kind) {
|
|
@@ -3119,6 +4928,7 @@ setInterval(async () => {
|
|
|
3119
4928
|
process.on('uncaughtException', (err) => {
|
|
3120
4929
|
pluginEvents.emit('browser:error', { error: err });
|
|
3121
4930
|
log('error', 'uncaughtException', { error: err.message, stack: err.stack });
|
|
4931
|
+
reporter.reportCrash(err);
|
|
3122
4932
|
process.exit(1);
|
|
3123
4933
|
});
|
|
3124
4934
|
process.on('unhandledRejection', (reason) => {
|
|
@@ -3191,6 +5001,9 @@ const pluginCtx = {
|
|
|
3191
5001
|
};
|
|
3192
5002
|
const loadedPlugins = await loadPlugins(app, pluginCtx);
|
|
3193
5003
|
|
|
5004
|
+
// --- OpenAPI docs (after all routes are registered) ---
|
|
5005
|
+
mountDocs(app);
|
|
5006
|
+
|
|
3194
5007
|
const server = app.listen(PORT, async () => {
|
|
3195
5008
|
startMemoryReporter();
|
|
3196
5009
|
refreshActiveTabsGauge();
|
|
@@ -3205,6 +5018,14 @@ const server = app.listen(PORT, async () => {
|
|
|
3205
5018
|
if (tmpCleanup.removed > 0) {
|
|
3206
5019
|
log('info', 'cleaned up orphaned camoufox temp files', tmpCleanup);
|
|
3207
5020
|
}
|
|
5021
|
+
const traceSweep = sweepOldTraces({
|
|
5022
|
+
baseDir: CONFIG.tracesDir,
|
|
5023
|
+
ttlMs: CONFIG.tracesTtlHours * 3600 * 1000,
|
|
5024
|
+
maxBytesPerFile: CONFIG.tracesMaxBytes,
|
|
5025
|
+
});
|
|
5026
|
+
if (traceSweep.removedTtl > 0 || traceSweep.removedOversized > 0) {
|
|
5027
|
+
log('info', 'swept old traces', traceSweep);
|
|
5028
|
+
}
|
|
3208
5029
|
// Pre-warm browser so first request doesn't eat a 6-7s cold start
|
|
3209
5030
|
try {
|
|
3210
5031
|
const start = Date.now();
|