browser-debugging-daemon 1.0.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/daemon.js +931 -0
- package/dashboard/app.js +1139 -0
- package/dashboard/index.html +277 -0
- package/dashboard/styles.css +774 -0
- package/index.js +223 -0
- package/mcp_server.js +999 -0
- package/orchestrator/RunTemplateStore.js +30 -0
- package/orchestrator/TaskRunStore.js +33 -0
- package/orchestrator/TaskRunner.js +803 -0
- package/package.json +66 -0
- package/runtime/ArtifactStore.js +202 -0
- package/runtime/BrowserRuntime.js +1706 -0
- package/shared.js +358 -0
- package/subagent/BrowserSubagent.js +689 -0
- package/subagent/OpenAIPlanner.js +382 -0
package/daemon.js
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { BrowserRuntime } from './runtime/BrowserRuntime.js';
|
|
8
|
+
import { BrowserSubagent } from './subagent/BrowserSubagent.js';
|
|
9
|
+
import { TaskRunner } from './orchestrator/TaskRunner.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
const DAEMON_HOST = process.env.BROWSER_DAEMON_HOST || '127.0.0.1';
|
|
15
|
+
const DAEMON_PORT = Number.parseInt(process.env.BROWSER_DAEMON_PORT, 10) || 3005;
|
|
16
|
+
const DAEMON_TOKEN = typeof process.env.BROWSER_DAEMON_TOKEN === 'string'
|
|
17
|
+
? process.env.BROWSER_DAEMON_TOKEN.trim()
|
|
18
|
+
: '';
|
|
19
|
+
const CORS_ORIGINS_RAW = typeof process.env.BROWSER_DAEMON_CORS_ORIGINS === 'string'
|
|
20
|
+
? process.env.BROWSER_DAEMON_CORS_ORIGINS.trim()
|
|
21
|
+
: '';
|
|
22
|
+
const CORS_ALLOWED_ORIGINS = CORS_ORIGINS_RAW
|
|
23
|
+
? CORS_ORIGINS_RAW.split(',').map((item) => item.trim()).filter(Boolean)
|
|
24
|
+
: [
|
|
25
|
+
`http://${DAEMON_HOST}:${DAEMON_PORT}`,
|
|
26
|
+
`http://127.0.0.1:${DAEMON_PORT}`,
|
|
27
|
+
`http://localhost:${DAEMON_PORT}`,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const app = express();
|
|
31
|
+
app.use(cors({
|
|
32
|
+
origin(origin, callback) {
|
|
33
|
+
if (!origin) {
|
|
34
|
+
callback(null, true);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (CORS_ALLOWED_ORIGINS.includes(origin)) {
|
|
38
|
+
callback(null, true);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
callback(null, false);
|
|
42
|
+
},
|
|
43
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
44
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
45
|
+
}));
|
|
46
|
+
app.use(express.json());
|
|
47
|
+
const runtime = new BrowserRuntime(__dirname);
|
|
48
|
+
const subagent = new BrowserSubagent(runtime);
|
|
49
|
+
const taskRunner = new TaskRunner(__dirname, { runtime, subagent });
|
|
50
|
+
const dashboardDir = path.join(__dirname, 'dashboard');
|
|
51
|
+
const artifactsDir = path.join(__dirname, 'artifacts');
|
|
52
|
+
const traceViewerDir = path.join(__dirname, 'node_modules', 'playwright-core', 'lib', 'vite', 'traceViewer');
|
|
53
|
+
const SUPPORTED_BROWSER_SOURCES = new Set(['auto', 'managed', 'attached']);
|
|
54
|
+
|
|
55
|
+
function normalizeBrowserSource(value) {
|
|
56
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
57
|
+
if (SUPPORTED_BROWSER_SOURCES.has(normalized)) {
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
return 'auto';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeOptionalBrowserSource(value) {
|
|
64
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return normalizeBrowserSource(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveAuthToken(req) {
|
|
71
|
+
const authorization = req.headers.authorization || '';
|
|
72
|
+
if (authorization.startsWith('Bearer ')) {
|
|
73
|
+
return authorization.slice('Bearer '.length).trim();
|
|
74
|
+
}
|
|
75
|
+
if (typeof req.query?.token === 'string') {
|
|
76
|
+
return req.query.token.trim();
|
|
77
|
+
}
|
|
78
|
+
if (typeof req.body?.token === 'string') {
|
|
79
|
+
return req.body.token.trim();
|
|
80
|
+
}
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function apiAuthMiddleware(req, res, next) {
|
|
85
|
+
if (!DAEMON_TOKEN || req.method === 'OPTIONS') {
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const token = resolveAuthToken(req);
|
|
90
|
+
if (token === DAEMON_TOKEN) {
|
|
91
|
+
return next();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return res.status(401).json({
|
|
95
|
+
error: 'Unauthorized. Provide Bearer token or token query parameter.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toPublicAssetUrl(filePath) {
|
|
100
|
+
if (!filePath) return null;
|
|
101
|
+
const relativePath = path.relative(__dirname, filePath);
|
|
102
|
+
if (relativePath.startsWith('..')) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return `/${relativePath.split(path.sep).join('/')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readTextFileSafe(filePath) {
|
|
109
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readJsonFileSafe(filePath) {
|
|
116
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readJsonLinesSafe(filePath, limit = 120) {
|
|
128
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lines = fs.readFileSync(filePath, 'utf8')
|
|
133
|
+
.split('\n')
|
|
134
|
+
.map((line) => line.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
|
|
137
|
+
return lines.slice(-limit).map((line) => {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(line);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
type: 'invalid_event',
|
|
144
|
+
payload: {
|
|
145
|
+
raw: line,
|
|
146
|
+
error: error.message,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function serializeRun(run, { includeDetails = false } = {}) {
|
|
154
|
+
if (!run) return null;
|
|
155
|
+
|
|
156
|
+
const lastHistoryEntry = run.result?.history?.[run.result.history.length - 1] || null;
|
|
157
|
+
const reportJsonUrl = toPublicAssetUrl(run.reports?.reportJsonPath);
|
|
158
|
+
const walkthroughUrl = toPublicAssetUrl(run.reports?.walkthroughPath);
|
|
159
|
+
const timelineJsonUrl = toPublicAssetUrl(run.reports?.timelineJsonPath);
|
|
160
|
+
|
|
161
|
+
const serialized = {
|
|
162
|
+
id: run.id,
|
|
163
|
+
taskInstruction: run.taskInstruction,
|
|
164
|
+
maxSteps: run.maxSteps,
|
|
165
|
+
browserSource: run.browserSource || "managed",
|
|
166
|
+
cdpEndpoint: run.cdpEndpoint || null,
|
|
167
|
+
status: run.status,
|
|
168
|
+
createdAt: run.createdAt,
|
|
169
|
+
startedAt: run.startedAt,
|
|
170
|
+
finishedAt: run.finishedAt,
|
|
171
|
+
summary: run.summary,
|
|
172
|
+
error: run.error,
|
|
173
|
+
pendingInput: run.pendingInput,
|
|
174
|
+
template: run.template ? {
|
|
175
|
+
id: run.template.id,
|
|
176
|
+
name: run.template.name,
|
|
177
|
+
description: run.template.description || "",
|
|
178
|
+
} : null,
|
|
179
|
+
templateEvaluation: run.templateEvaluation || run.result?.templateEvaluation || null,
|
|
180
|
+
artifacts: run.artifacts ? {
|
|
181
|
+
...run.artifacts,
|
|
182
|
+
currentViewUrl: toPublicAssetUrl(run.artifacts.currentViewPath),
|
|
183
|
+
traceUrl: toPublicAssetUrl(run.artifacts.tracePath),
|
|
184
|
+
traceUrls: (run.artifacts.traceFiles || []).map((filePath) => toPublicAssetUrl(filePath)).filter(Boolean),
|
|
185
|
+
eventsUrl: toPublicAssetUrl(run.artifacts.eventsPath),
|
|
186
|
+
videoUrls: (run.artifacts.videoFiles || []).map((filePath) => toPublicAssetUrl(filePath)).filter(Boolean),
|
|
187
|
+
} : null,
|
|
188
|
+
reports: run.reports ? {
|
|
189
|
+
reportJsonPath: run.reports.reportJsonPath,
|
|
190
|
+
walkthroughPath: run.reports.walkthroughPath,
|
|
191
|
+
timelineJsonPath: run.reports.timelineJsonPath || null,
|
|
192
|
+
reportJsonUrl,
|
|
193
|
+
walkthroughUrl,
|
|
194
|
+
timelineJsonUrl,
|
|
195
|
+
} : null,
|
|
196
|
+
latestScreenshotUrl: toPublicAssetUrl(lastHistoryEntry?.actionResult?.screenshotPath) || toPublicAssetUrl(run.artifacts?.currentViewPath),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (!includeDetails) {
|
|
200
|
+
return serialized;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
...serialized,
|
|
205
|
+
walkthroughContent: readTextFileSafe(run.reports?.walkthroughPath),
|
|
206
|
+
timelineContent: readJsonFileSafe(run.reports?.timelineJsonPath),
|
|
207
|
+
recentEvents: readJsonLinesSafe(run.artifacts?.eventsPath, 120),
|
|
208
|
+
result: run.result ? {
|
|
209
|
+
status: run.result.status,
|
|
210
|
+
step: run.result.step,
|
|
211
|
+
summary: run.result.summary,
|
|
212
|
+
page: run.result.page,
|
|
213
|
+
verification: run.result.verification,
|
|
214
|
+
operatorMessages: run.result.operatorMessages,
|
|
215
|
+
pendingInput: run.result.pendingInput,
|
|
216
|
+
templateEvaluation: run.result.templateEvaluation || null,
|
|
217
|
+
debug: run.result.debug,
|
|
218
|
+
capabilities: run.result.debug?.capabilities || null,
|
|
219
|
+
history: (run.result.history || []).map((entry) => ({
|
|
220
|
+
step: entry.step,
|
|
221
|
+
stepStartedAt: entry.stepStartedAt || null,
|
|
222
|
+
stepFinishedAt: entry.stepFinishedAt || null,
|
|
223
|
+
actionDurationMs: entry.actionDurationMs ?? null,
|
|
224
|
+
elapsedMs: entry.elapsedMs ?? null,
|
|
225
|
+
videoOffsetSeconds: entry.videoOffsetSeconds ?? null,
|
|
226
|
+
thinking: entry.thinking,
|
|
227
|
+
summary: entry.summary,
|
|
228
|
+
action: entry.action,
|
|
229
|
+
page: entry.page || null,
|
|
230
|
+
actionResult: {
|
|
231
|
+
...entry.actionResult,
|
|
232
|
+
screenshotUrl: toPublicAssetUrl(entry.actionResult?.screenshotPath),
|
|
233
|
+
},
|
|
234
|
+
verification: entry.verification,
|
|
235
|
+
debug: entry.debug,
|
|
236
|
+
})),
|
|
237
|
+
} : null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
app.use('/dashboard-assets', express.static(dashboardDir));
|
|
242
|
+
|
|
243
|
+
app.get('/dashboard', async (req, res) => {
|
|
244
|
+
res.sendFile(path.join(dashboardDir, 'index.html'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
app.use((req, res, next) => {
|
|
248
|
+
if (
|
|
249
|
+
req.path === '/dashboard'
|
|
250
|
+
|| req.path.startsWith('/dashboard-assets')
|
|
251
|
+
|| req.path.startsWith('/trace-viewer')
|
|
252
|
+
|| req.path.startsWith('/trace-zip')
|
|
253
|
+
) {
|
|
254
|
+
return next();
|
|
255
|
+
}
|
|
256
|
+
return apiAuthMiddleware(req, res, next);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.use('/artifacts', express.static(artifactsDir));
|
|
260
|
+
|
|
261
|
+
// Serve Playwright's built-in trace viewer SPA directly from node_modules
|
|
262
|
+
if (fs.existsSync(traceViewerDir)) {
|
|
263
|
+
app.use('/trace-viewer', express.static(traceViewerDir));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Redirect to trace viewer with the correct trace URL
|
|
267
|
+
app.get('/trace/:sessionId/:traceFile', (req, res) => {
|
|
268
|
+
const { sessionId, traceFile } = req.params;
|
|
269
|
+
if (!sessionId || !traceFile) {
|
|
270
|
+
return res.status(400).json({ error: 'sessionId and traceFile are required.' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Build the trace zip URL using the auth-aware proxy route
|
|
274
|
+
const token = resolveAuthToken(req);
|
|
275
|
+
const proxyUrl = `${req.protocol}://${req.get('host')}/trace-zip/${sessionId}/${encodeURIComponent(traceFile)}`;
|
|
276
|
+
const traceUrl = token ? `${proxyUrl}?token=${encodeURIComponent(token)}` : proxyUrl;
|
|
277
|
+
const viewerUrl = `/trace-viewer/?trace=${encodeURIComponent(traceUrl)}`;
|
|
278
|
+
res.redirect(viewerUrl);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Auth-aware proxy for trace zip files (service worker cannot send Bearer tokens)
|
|
282
|
+
app.get('/trace-zip/:sessionId/:traceFile', (req, res) => {
|
|
283
|
+
const { sessionId, traceFile } = req.params;
|
|
284
|
+
if (!sessionId || !traceFile) {
|
|
285
|
+
return res.status(400).json({ error: 'sessionId and traceFile are required.' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Validate token via query param (same as SSE)
|
|
289
|
+
if (DAEMON_TOKEN) {
|
|
290
|
+
const token = resolveAuthToken(req);
|
|
291
|
+
if (token !== DAEMON_TOKEN) {
|
|
292
|
+
return res.status(401).json({ error: 'Unauthorized.' });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Prevent path traversal
|
|
297
|
+
const safeSessionId = path.basename(sessionId);
|
|
298
|
+
const safeTraceFile = path.basename(traceFile);
|
|
299
|
+
const tracePath = path.join(artifactsDir, safeSessionId, 'traces', safeTraceFile);
|
|
300
|
+
|
|
301
|
+
if (!tracePath.startsWith(artifactsDir)) {
|
|
302
|
+
return res.status(403).json({ error: 'Invalid path.' });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!fs.existsSync(tracePath)) {
|
|
306
|
+
return res.status(404).json({ error: 'Trace file not found.' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
310
|
+
res.setHeader('Content-Disposition', `inline; filename="${safeTraceFile}"`);
|
|
311
|
+
fs.createReadStream(tracePath).pipe(res);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
app.get('/current_view.png', async (req, res) => {
|
|
315
|
+
res.sendFile(path.join(__dirname, 'current_view.png'));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
app.post('/start', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const browserSource = normalizeBrowserSource(req.body?.browser_source);
|
|
321
|
+
const result = await runtime.start({
|
|
322
|
+
source: browserSource,
|
|
323
|
+
cdpEndpoint: req.body?.cdp_endpoint || undefined,
|
|
324
|
+
});
|
|
325
|
+
if (result.alreadyRunning) {
|
|
326
|
+
return res.json({ status: "ok", message: "Browser already running" });
|
|
327
|
+
}
|
|
328
|
+
res.json({
|
|
329
|
+
status: "ok",
|
|
330
|
+
message: "Browser started successfully",
|
|
331
|
+
runtime: {
|
|
332
|
+
requested_source: result.requestedSource,
|
|
333
|
+
effective_source: result.sessionSource,
|
|
334
|
+
fallback_reason: result.autoFallbackReason || null,
|
|
335
|
+
},
|
|
336
|
+
artifacts: result.artifacts,
|
|
337
|
+
});
|
|
338
|
+
} catch (err) {
|
|
339
|
+
res.status(500).json({ error: err.message });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
app.get('/cdp-health', async (req, res) => {
|
|
344
|
+
try {
|
|
345
|
+
const health = await runtime.getCdpHealth({
|
|
346
|
+
endpoint: req.query.cdp_endpoint || undefined,
|
|
347
|
+
timeoutMs: Number.parseInt(req.query.timeout_ms, 10) || 3000,
|
|
348
|
+
});
|
|
349
|
+
res.json({
|
|
350
|
+
status: health.ok ? "ok" : "error",
|
|
351
|
+
health,
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
res.status(500).json({ error: err.message });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
app.get('/attach-diagnostics', async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const diagnostics = await runtime.getCdpDiagnostics({
|
|
361
|
+
endpoint: req.query.cdp_endpoint || undefined,
|
|
362
|
+
timeoutMs: Number.parseInt(req.query.timeout_ms, 10) || 3000,
|
|
363
|
+
});
|
|
364
|
+
res.json({
|
|
365
|
+
status: diagnostics.ok ? "ok" : "warn",
|
|
366
|
+
diagnostics,
|
|
367
|
+
});
|
|
368
|
+
} catch (err) {
|
|
369
|
+
res.status(500).json({ error: err.message });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
app.post('/goto', async (req, res) => {
|
|
374
|
+
try {
|
|
375
|
+
const { url } = req.body;
|
|
376
|
+
console.log(`🌐 Navigating to ${url}...`);
|
|
377
|
+
const result = await runtime.goto(url);
|
|
378
|
+
res.json({ status: "ok", message: `Navigated to ${url}`, screenshot_path: result.screenshotPath });
|
|
379
|
+
} catch (err) {
|
|
380
|
+
res.status(500).json({ error: err.message });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
app.get('/observe', async (req, res) => {
|
|
385
|
+
try {
|
|
386
|
+
console.log(`👁️ Injecting Set-of-Mark (SoM) indicators...`);
|
|
387
|
+
const result = await runtime.observe();
|
|
388
|
+
|
|
389
|
+
console.log(`📸 Screenshot with ${result.elements.length} markers saved to ${result.screenshotPath}`);
|
|
390
|
+
res.json({
|
|
391
|
+
status: "ok",
|
|
392
|
+
elements: result.elements,
|
|
393
|
+
screenshot_path: result.screenshotPath,
|
|
394
|
+
message: `Observed ${result.elements.length} interactive elements.`
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
res.status(500).json({ error: err.message });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
app.post('/click', async (req, res) => {
|
|
402
|
+
try {
|
|
403
|
+
const { id } = req.body;
|
|
404
|
+
const result = await runtime.click(parseInt(id, 10));
|
|
405
|
+
|
|
406
|
+
console.log(`🖱️ Clicking element ID ${id} at (${result.target.x}, ${result.target.y}) [${result.target.tag}]`);
|
|
407
|
+
res.json({ status: "ok", message: `Clicked element ${id}`, screenshot_path: result.screenshotPath });
|
|
408
|
+
} catch (err) {
|
|
409
|
+
res.status(500).json({ error: err.message });
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
app.post('/type', async (req, res) => {
|
|
414
|
+
try {
|
|
415
|
+
const { id, text, submit } = req.body;
|
|
416
|
+
const result = await runtime.type(parseInt(id, 10), text, { submit: !!submit });
|
|
417
|
+
console.log(`⌨️ Typing into element ID ${id} at (${result.target.x}, ${result.target.y}): "${text}"${submit ? " +Enter" : ""}`);
|
|
418
|
+
res.json({ status: "ok", message: `Typed text into element ${id}`, screenshot_path: result.screenshotPath });
|
|
419
|
+
} catch (err) {
|
|
420
|
+
res.status(500).json({ error: err.message });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.post('/hover', async (req, res) => {
|
|
425
|
+
try {
|
|
426
|
+
const { id } = req.body;
|
|
427
|
+
const result = await runtime.hover(parseInt(id, 10));
|
|
428
|
+
|
|
429
|
+
console.log(`✨ Hovering element ID ${id} at (${result.target.x}, ${result.target.y})`);
|
|
430
|
+
|
|
431
|
+
res.json({ status: "ok", message: `Hovered over element ${id}`, screenshot_path: result.screenshotPath });
|
|
432
|
+
} catch (err) {
|
|
433
|
+
res.status(500).json({ error: err.message });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
app.post('/scroll', async (req, res) => {
|
|
438
|
+
try {
|
|
439
|
+
const { direction } = req.body; // 'up', 'down', 'top', 'bottom'
|
|
440
|
+
console.log(`📜 Scrolling ${direction}...`);
|
|
441
|
+
const result = await runtime.scroll(direction);
|
|
442
|
+
res.json({ status: "ok", message: `Scrolled ${direction}`, screenshot_path: result.screenshotPath });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
res.status(500).json({ error: err.message });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
app.post('/keypress', async (req, res) => {
|
|
449
|
+
try {
|
|
450
|
+
const { key } = req.body;
|
|
451
|
+
console.log(`🎯 Pressing key: ${key}`);
|
|
452
|
+
const result = await runtime.keypress(key);
|
|
453
|
+
res.json({ status: "ok", message: `Pressed key ${key}`, screenshot_path: result.screenshotPath });
|
|
454
|
+
} catch (err) {
|
|
455
|
+
res.status(500).json({ error: err.message });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
app.post('/upload', async (req, res) => {
|
|
460
|
+
try {
|
|
461
|
+
const { id, paths, files } = req.body;
|
|
462
|
+
const result = await runtime.upload(parseInt(id, 10), { paths: paths || [], files: files || [] });
|
|
463
|
+
console.log(`📎 Uploaded ${result.fileCount} file(s) to element ID ${id}`);
|
|
464
|
+
res.json({ status: "ok", message: `Uploaded ${result.fileCount} file(s) to element ${id}`, screenshot_path: result.screenshotPath });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
res.status(500).json({ error: err.message });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
app.post('/drag', async (req, res) => {
|
|
471
|
+
try {
|
|
472
|
+
const { fromId, toId } = req.body;
|
|
473
|
+
const result = await runtime.drag(parseInt(fromId, 10), parseInt(toId, 10));
|
|
474
|
+
console.log(`↔️ Dragged element ID ${fromId} to element ID ${toId}`);
|
|
475
|
+
res.json({ status: "ok", message: `Dragged element ${fromId} to ${toId}`, screenshot_path: result.screenshotPath });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
res.status(500).json({ error: err.message });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
app.post('/drag-file', async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const { toId, paths, files } = req.body;
|
|
484
|
+
const result = await runtime.dragFile({ paths: paths || [], files: files || [], toId: parseInt(toId, 10) });
|
|
485
|
+
console.log(`📂 Dragged ${result.fileCount} file(s) to element ID ${toId}`);
|
|
486
|
+
res.json({ status: "ok", message: `Dragged ${result.fileCount} file(s) to element ${toId}`, screenshot_path: result.screenshotPath });
|
|
487
|
+
} catch (err) {
|
|
488
|
+
res.status(500).json({ error: err.message });
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
app.post('/wait-for', async (req, res) => {
|
|
493
|
+
try {
|
|
494
|
+
const { text, textGone, selector, timeout } = req.body;
|
|
495
|
+
console.log(`⏳ Waiting for: ${text || textGone || selector || "unknown"} (timeout: ${timeout || 30000}ms)`);
|
|
496
|
+
const result = await runtime.waitFor({ text, textGone, selector, timeout });
|
|
497
|
+
if (result.timedOut) {
|
|
498
|
+
res.json({ status: "timeout", message: `Condition not met after ${result.waitedMs}ms`, ...result });
|
|
499
|
+
} else {
|
|
500
|
+
res.json({ status: "ok", message: `Wait condition met: ${result.matched}`, ...result });
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
res.status(500).json({ error: err.message });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
app.post('/evaluate', async (req, res) => {
|
|
508
|
+
try {
|
|
509
|
+
const { expression } = req.body;
|
|
510
|
+
if (!expression) throw new Error("'expression' is required.");
|
|
511
|
+
console.log(`🔧 Evaluating JS (${expression.length} chars)`);
|
|
512
|
+
const result = await runtime.evaluate(expression);
|
|
513
|
+
res.json({ status: "ok", ...result });
|
|
514
|
+
} catch (err) {
|
|
515
|
+
res.status(500).json({ error: err.message });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
app.post('/select-option', async (req, res) => {
|
|
520
|
+
try {
|
|
521
|
+
const { id, values } = req.body;
|
|
522
|
+
const result = await runtime.selectOption(parseInt(id, 10), values);
|
|
523
|
+
console.log(`📋 Selected option(s) ${JSON.stringify(values)} in element ID ${id}`);
|
|
524
|
+
res.json({ status: "ok", message: `Selected option(s) in element ${id}`, screenshot_path: result.screenshotPath });
|
|
525
|
+
} catch (err) {
|
|
526
|
+
res.status(500).json({ error: err.message });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
app.post('/dialog', async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
const { action, promptText } = req.body;
|
|
533
|
+
console.log(`💬 Handling dialog: ${action || "accept"}`);
|
|
534
|
+
const result = await runtime.handleDialog({ action: action || "accept", promptText: promptText || "" });
|
|
535
|
+
res.json({ status: "ok", ...result });
|
|
536
|
+
} catch (err) {
|
|
537
|
+
res.status(500).json({ error: err.message });
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
app.post('/navigate-back', async (req, res) => {
|
|
542
|
+
try {
|
|
543
|
+
console.log(`◀️ Navigating back`);
|
|
544
|
+
const result = await runtime.goBack();
|
|
545
|
+
res.json({ status: "ok", message: `Navigated back to ${result.url}`, screenshot_path: result.screenshotPath });
|
|
546
|
+
} catch (err) {
|
|
547
|
+
res.status(500).json({ error: err.message });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
app.post('/tabs', async (req, res) => {
|
|
552
|
+
try {
|
|
553
|
+
const { action, index, url } = req.body;
|
|
554
|
+
console.log(`📑 Tab action: ${action}`);
|
|
555
|
+
const result = await runtime.tabAction(action, { index, url });
|
|
556
|
+
res.json({ status: "ok", ...result });
|
|
557
|
+
} catch (err) {
|
|
558
|
+
res.status(500).json({ error: err.message });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
app.get('/debug-state', async (req, res) => {
|
|
563
|
+
try {
|
|
564
|
+
const limit = Number.parseInt(req.query.limit, 10) || 20;
|
|
565
|
+
res.json({
|
|
566
|
+
status: "ok",
|
|
567
|
+
debug_state: runtime.getDebugState(limit),
|
|
568
|
+
});
|
|
569
|
+
} catch (err) {
|
|
570
|
+
res.status(500).json({ error: err.message });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
app.get('/text-layout-audit', async (req, res) => {
|
|
575
|
+
try {
|
|
576
|
+
const limit = Number.parseInt(req.query.limit, 10) || 80;
|
|
577
|
+
const selectors = typeof req.query.selectors === "string" ? req.query.selectors : undefined;
|
|
578
|
+
const overflowThreshold = Number.parseFloat(req.query.overflow_threshold);
|
|
579
|
+
const audit = await runtime.auditTextLayout({
|
|
580
|
+
limit,
|
|
581
|
+
selectors,
|
|
582
|
+
overflowThreshold: Number.isFinite(overflowThreshold) ? overflowThreshold : undefined,
|
|
583
|
+
});
|
|
584
|
+
res.json({
|
|
585
|
+
status: "ok",
|
|
586
|
+
audit,
|
|
587
|
+
});
|
|
588
|
+
} catch (err) {
|
|
589
|
+
res.status(500).json({ error: err.message });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
app.post('/delegate', async (req, res) => {
|
|
594
|
+
try {
|
|
595
|
+
const {
|
|
596
|
+
task_instruction: taskInstruction,
|
|
597
|
+
max_steps: maxSteps,
|
|
598
|
+
browser_source: browserSource,
|
|
599
|
+
cdp_endpoint: cdpEndpoint,
|
|
600
|
+
auto_stop: autoStopInput,
|
|
601
|
+
} = req.body;
|
|
602
|
+
const autoStop = autoStopInput !== false;
|
|
603
|
+
if (!taskInstruction) {
|
|
604
|
+
throw new Error("task_instruction is required.");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let result = await subagent.delegateTask(taskInstruction, {
|
|
608
|
+
maxSteps,
|
|
609
|
+
startOptions: {
|
|
610
|
+
source: normalizeBrowserSource(browserSource),
|
|
611
|
+
cdpEndpoint: cdpEndpoint || undefined,
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (autoStop) {
|
|
616
|
+
const stopResult = await runtime.stop().catch(() => null);
|
|
617
|
+
if (stopResult?.artifacts) {
|
|
618
|
+
result.artifacts = stopResult.artifacts;
|
|
619
|
+
result.debug = {
|
|
620
|
+
...(result.debug || {}),
|
|
621
|
+
artifacts: stopResult.artifacts,
|
|
622
|
+
};
|
|
623
|
+
const refreshedReports = subagent.writeReports(result);
|
|
624
|
+
result.reports = refreshedReports;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
res.json({
|
|
629
|
+
status: "ok",
|
|
630
|
+
result,
|
|
631
|
+
runtime: {
|
|
632
|
+
auto_stop: autoStop,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
} catch (err) {
|
|
636
|
+
res.status(500).json({ error: err.message });
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
app.get('/run-templates', async (req, res) => {
|
|
641
|
+
try {
|
|
642
|
+
const limit = Number.parseInt(req.query.limit, 10) || 100;
|
|
643
|
+
res.json({
|
|
644
|
+
status: "ok",
|
|
645
|
+
templates: taskRunner.listTemplates(limit),
|
|
646
|
+
});
|
|
647
|
+
} catch (err) {
|
|
648
|
+
res.status(500).json({ error: err.message });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
app.post('/run-templates', async (req, res) => {
|
|
653
|
+
try {
|
|
654
|
+
const timeoutPolicyInput = req.body?.timeout_policy || req.body?.timeoutPolicy || {};
|
|
655
|
+
const template = taskRunner.saveTemplate({
|
|
656
|
+
id: req.body?.id || null,
|
|
657
|
+
name: req.body?.name,
|
|
658
|
+
description: req.body?.description,
|
|
659
|
+
taskInstruction: req.body?.task_instruction ?? req.body?.taskInstruction,
|
|
660
|
+
browserSource: normalizeOptionalBrowserSource(req.body?.browser_source ?? req.body?.browserSource) || "auto",
|
|
661
|
+
cdpEndpoint: req.body?.cdp_endpoint ?? req.body?.cdpEndpoint ?? null,
|
|
662
|
+
startUrl: req.body?.start_url ?? req.body?.startUrl ?? "",
|
|
663
|
+
preLoginChecks: req.body?.pre_login_checks ?? req.body?.preLoginChecks ?? [],
|
|
664
|
+
assertionRules: req.body?.assertion_rules ?? req.body?.assertionRules ?? [],
|
|
665
|
+
timeoutPolicy: {
|
|
666
|
+
maxSteps: timeoutPolicyInput.max_steps ?? timeoutPolicyInput.maxSteps,
|
|
667
|
+
handoffTimeoutMs: timeoutPolicyInput.handoff_timeout_ms ?? timeoutPolicyInput.handoffTimeoutMs,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
res.json({
|
|
671
|
+
status: "ok",
|
|
672
|
+
template,
|
|
673
|
+
});
|
|
674
|
+
} catch (err) {
|
|
675
|
+
res.status(400).json({ error: err.message });
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
app.delete('/run-templates/:templateId', async (req, res) => {
|
|
680
|
+
try {
|
|
681
|
+
const template = taskRunner.deleteTemplate(req.params.templateId);
|
|
682
|
+
res.json({
|
|
683
|
+
status: "ok",
|
|
684
|
+
template,
|
|
685
|
+
});
|
|
686
|
+
} catch (err) {
|
|
687
|
+
res.status(404).json({ error: err.message });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
app.get('/run-templates/:templateId/compare', async (req, res) => {
|
|
692
|
+
try {
|
|
693
|
+
const templateId = req.params.templateId;
|
|
694
|
+
const comparison = taskRunner.compareTemplateRuns(templateId, {
|
|
695
|
+
limit: Number.parseInt(req.query.limit, 10) || 8,
|
|
696
|
+
});
|
|
697
|
+
if (!comparison.template) {
|
|
698
|
+
res.status(404).json({ error: `Template not found: ${templateId}` });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
res.json({
|
|
702
|
+
status: "ok",
|
|
703
|
+
comparison: {
|
|
704
|
+
...comparison,
|
|
705
|
+
runs: comparison.runs.map((run) => serializeRun(run)),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
} catch (err) {
|
|
709
|
+
res.status(500).json({ error: err.message });
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
app.post('/run-templates/:templateId/runs', async (req, res) => {
|
|
714
|
+
try {
|
|
715
|
+
const run = taskRunner.createRunFromTemplate(req.params.templateId, {
|
|
716
|
+
taskInstruction: req.body?.task_instruction || req.body?.taskInstruction || "",
|
|
717
|
+
maxSteps: req.body?.max_steps ?? req.body?.maxSteps,
|
|
718
|
+
browserSource: normalizeOptionalBrowserSource(req.body?.browser_source ?? req.body?.browserSource) || undefined,
|
|
719
|
+
cdpEndpoint: req.body?.cdp_endpoint ?? req.body?.cdpEndpoint ?? null,
|
|
720
|
+
handoffTimeoutMs: req.body?.handoff_timeout_ms ?? req.body?.handoffTimeoutMs,
|
|
721
|
+
});
|
|
722
|
+
res.json({
|
|
723
|
+
status: "ok",
|
|
724
|
+
run: serializeRun(run),
|
|
725
|
+
});
|
|
726
|
+
} catch (err) {
|
|
727
|
+
res.status(400).json({ error: err.message });
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
app.post('/runs', async (req, res) => {
|
|
732
|
+
try {
|
|
733
|
+
const {
|
|
734
|
+
task_instruction: taskInstruction,
|
|
735
|
+
max_steps: maxSteps,
|
|
736
|
+
browser_source: browserSource,
|
|
737
|
+
cdp_endpoint: cdpEndpoint,
|
|
738
|
+
template_id: templateId,
|
|
739
|
+
handoff_timeout_ms: handoffTimeoutMs,
|
|
740
|
+
} = req.body;
|
|
741
|
+
let run = null;
|
|
742
|
+
if (templateId) {
|
|
743
|
+
run = taskRunner.createRunFromTemplate(templateId, {
|
|
744
|
+
taskInstruction: taskInstruction || "",
|
|
745
|
+
maxSteps,
|
|
746
|
+
browserSource: normalizeOptionalBrowserSource(browserSource) || undefined,
|
|
747
|
+
cdpEndpoint: cdpEndpoint || null,
|
|
748
|
+
handoffTimeoutMs,
|
|
749
|
+
});
|
|
750
|
+
} else if (!taskInstruction) {
|
|
751
|
+
throw new Error("task_instruction is required.");
|
|
752
|
+
} else {
|
|
753
|
+
run = taskRunner.createRun(taskInstruction, {
|
|
754
|
+
maxSteps,
|
|
755
|
+
browserSource: normalizeBrowserSource(browserSource),
|
|
756
|
+
cdpEndpoint: cdpEndpoint || null,
|
|
757
|
+
handoffTimeoutMs,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
res.json({
|
|
761
|
+
status: "ok",
|
|
762
|
+
run: serializeRun(run),
|
|
763
|
+
});
|
|
764
|
+
} catch (err) {
|
|
765
|
+
res.status(500).json({ error: err.message });
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
app.get('/runs', async (req, res) => {
|
|
770
|
+
try {
|
|
771
|
+
const limit = Number.parseInt(req.query.limit, 10) || 20;
|
|
772
|
+
res.json({
|
|
773
|
+
status: "ok",
|
|
774
|
+
runs: taskRunner.listRuns(limit).map((run) => serializeRun(run)),
|
|
775
|
+
});
|
|
776
|
+
} catch (err) {
|
|
777
|
+
res.status(500).json({ error: err.message });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
app.get('/runs/stream', async (req, res) => {
|
|
782
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
783
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
784
|
+
res.setHeader('Connection', 'keep-alive');
|
|
785
|
+
res.flushHeaders?.();
|
|
786
|
+
|
|
787
|
+
const send = (payload) => {
|
|
788
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
send({
|
|
792
|
+
type: 'snapshot',
|
|
793
|
+
timestamp: new Date().toISOString(),
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const unsubscribe = taskRunner.subscribe((event) => {
|
|
797
|
+
send(event);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const keepAlive = setInterval(() => {
|
|
801
|
+
res.write(': ping\n\n');
|
|
802
|
+
}, 15000);
|
|
803
|
+
|
|
804
|
+
req.on('close', () => {
|
|
805
|
+
clearInterval(keepAlive);
|
|
806
|
+
unsubscribe();
|
|
807
|
+
res.end();
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
app.get('/runs/:runId', async (req, res) => {
|
|
812
|
+
try {
|
|
813
|
+
const run = taskRunner.getRun(req.params.runId);
|
|
814
|
+
if (!run) {
|
|
815
|
+
res.status(404).json({ error: `Run not found: ${req.params.runId}` });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
res.json({
|
|
820
|
+
status: "ok",
|
|
821
|
+
run: serializeRun(run, { includeDetails: true }),
|
|
822
|
+
});
|
|
823
|
+
} catch (err) {
|
|
824
|
+
res.status(500).json({ error: err.message });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
app.post('/runs/:runId/reply', async (req, res) => {
|
|
829
|
+
try {
|
|
830
|
+
const { instruction } = req.body;
|
|
831
|
+
if (!instruction || !String(instruction).trim()) {
|
|
832
|
+
throw new Error("instruction is required.");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const run = await taskRunner.replyToRun(req.params.runId, String(instruction).trim());
|
|
836
|
+
res.json({
|
|
837
|
+
status: "ok",
|
|
838
|
+
run: serializeRun(run, { includeDetails: true }),
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
res.status(400).json({ error: err.message });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
app.post('/runs/:runId/manual-control', async (req, res) => {
|
|
846
|
+
try {
|
|
847
|
+
const reason = String(req.body?.reason || "Manual control requested by operator.").trim();
|
|
848
|
+
const run = await taskRunner.requestManualControl(req.params.runId, reason);
|
|
849
|
+
res.json({
|
|
850
|
+
status: "ok",
|
|
851
|
+
run: serializeRun(run, { includeDetails: true }),
|
|
852
|
+
});
|
|
853
|
+
} catch (err) {
|
|
854
|
+
res.status(400).json({ error: err.message });
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
app.post('/runs/:runId/resume', async (req, res) => {
|
|
859
|
+
try {
|
|
860
|
+
const instruction = String(req.body?.instruction || "Manual control complete. Continue from the current page.").trim();
|
|
861
|
+
const run = await taskRunner.resumeRun(req.params.runId, instruction);
|
|
862
|
+
res.json({
|
|
863
|
+
status: "ok",
|
|
864
|
+
run: serializeRun(run, { includeDetails: true }),
|
|
865
|
+
});
|
|
866
|
+
} catch (err) {
|
|
867
|
+
res.status(400).json({ error: err.message });
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
app.post('/runs/:runId/abort', async (req, res) => {
|
|
872
|
+
try {
|
|
873
|
+
const reason = String(req.body?.reason || "Run aborted by operator.").trim();
|
|
874
|
+
const run = taskRunner.abortRun(req.params.runId, reason);
|
|
875
|
+
res.json({
|
|
876
|
+
status: "ok",
|
|
877
|
+
run: serializeRun(run, { includeDetails: true }),
|
|
878
|
+
});
|
|
879
|
+
} catch (err) {
|
|
880
|
+
res.status(400).json({ error: err.message });
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
app.post('/stop', async (req, res) => {
|
|
885
|
+
try {
|
|
886
|
+
const result = await runtime.stop();
|
|
887
|
+
if (result.alreadyStopped) {
|
|
888
|
+
return res.json({ status: "ok", message: "Browser not running" });
|
|
889
|
+
}
|
|
890
|
+
console.log(`🛑 Stopping browser daemon...`);
|
|
891
|
+
res.json({ status: "ok", message: `Browser stopped. Trace saved to ${result.artifacts?.tracePath}`, artifacts: result.artifacts });
|
|
892
|
+
} catch (err) {
|
|
893
|
+
res.status(500).json({ error: err.message });
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
app.listen(DAEMON_PORT, DAEMON_HOST, () => {
|
|
898
|
+
console.log(`=========================================`);
|
|
899
|
+
console.log(`🤖 Browser Agent Daemon running on ${DAEMON_HOST}:${DAEMON_PORT}`);
|
|
900
|
+
console.log(` - Auth token required: ${DAEMON_TOKEN ? 'yes' : 'no'}`);
|
|
901
|
+
console.log(` - Allowed CORS origins: ${CORS_ALLOWED_ORIGINS.join(', ') || 'none'}`);
|
|
902
|
+
console.log(` - /artifacts and API routes are protected when auth token is enabled`);
|
|
903
|
+
console.log(` - POST /start`);
|
|
904
|
+
console.log(` - GET /cdp-health?cdp_endpoint=http://127.0.0.1:9222`);
|
|
905
|
+
console.log(` - GET /attach-diagnostics?cdp_endpoint=http://127.0.0.1:9222`);
|
|
906
|
+
console.log(` - POST /goto { "url": "..." }`);
|
|
907
|
+
console.log(` - GET /observe`);
|
|
908
|
+
console.log(` - POST /click { "id": 1 }`);
|
|
909
|
+
console.log(` - POST /hover { "id": 1 }`);
|
|
910
|
+
console.log(` - POST /type { "id": 1, "text": "..." }`);
|
|
911
|
+
console.log(` - POST /keypress { "key": "Enter" }`);
|
|
912
|
+
console.log(` - POST /scroll { "direction": "down|up|top|bottom" }`);
|
|
913
|
+
console.log(` - GET /debug-state?limit=20`);
|
|
914
|
+
console.log(` - GET /text-layout-audit?limit=80`);
|
|
915
|
+
console.log(` - POST /delegate { "task_instruction": "...", "max_steps": 12 }`);
|
|
916
|
+
console.log(` - POST /runs { "task_instruction": "...", "max_steps": 12 }`);
|
|
917
|
+
console.log(` - GET /run-templates`);
|
|
918
|
+
console.log(` - POST /run-templates { "name": "...", "start_url": "...", "pre_login_checks": [], "assertion_rules": [] }`);
|
|
919
|
+
console.log(` - POST /run-templates/:templateId/runs`);
|
|
920
|
+
console.log(` - GET /run-templates/:templateId/compare`);
|
|
921
|
+
console.log(` - GET /runs?limit=20`);
|
|
922
|
+
console.log(` - GET /runs/:runId`);
|
|
923
|
+
console.log(` - POST /runs/:runId/reply { "instruction": "..." }`);
|
|
924
|
+
console.log(` - POST /runs/:runId/manual-control { "reason": "..." }`);
|
|
925
|
+
console.log(` - POST /runs/:runId/resume { "instruction": "..." }`);
|
|
926
|
+
console.log(` - POST /runs/:runId/abort { "reason": "..." }`);
|
|
927
|
+
console.log(` - GET /dashboard`);
|
|
928
|
+
console.log(` - GET /trace/:sessionId/:traceFile (Playwright Trace Viewer)`);
|
|
929
|
+
console.log(` - POST /stop`);
|
|
930
|
+
console.log(`=========================================`);
|
|
931
|
+
});
|