@askjo/camoufox-browser 1.0.9 → 1.0.11
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/openclaw.plugin.json +13 -5
- package/package.json +1 -1
- package/plugin.ts +409 -217
- package/server-camoufox.js +339 -1
package/openclaw.plugin.json
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
"id": "camoufox-browser",
|
|
3
3
|
"name": "Camoufox Browser",
|
|
4
4
|
"description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.11",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"url": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"description": "Camoufox browser server URL"
|
|
12
|
-
|
|
11
|
+
"description": "Camoufox browser server URL"
|
|
12
|
+
},
|
|
13
|
+
"port": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"description": "Server port (used if url not set)",
|
|
16
|
+
"default": 9377
|
|
13
17
|
},
|
|
14
18
|
"autoStart": {
|
|
15
19
|
"type": "boolean",
|
|
16
20
|
"description": "Auto-start the camoufox-browser server with the Gateway",
|
|
17
|
-
"default":
|
|
21
|
+
"default": true
|
|
18
22
|
}
|
|
19
23
|
},
|
|
20
24
|
"additionalProperties": false
|
|
@@ -24,8 +28,12 @@
|
|
|
24
28
|
"label": "Server URL",
|
|
25
29
|
"placeholder": "http://localhost:9377"
|
|
26
30
|
},
|
|
31
|
+
"port": {
|
|
32
|
+
"label": "Server Port",
|
|
33
|
+
"placeholder": "9377"
|
|
34
|
+
},
|
|
27
35
|
"autoStart": {
|
|
28
|
-
"label": "Auto-start server"
|
|
36
|
+
"label": "Auto-start server with Gateway"
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askjo/camoufox-browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
|
|
5
5
|
"main": "server-camoufox.js",
|
|
6
6
|
"license": "MIT",
|
package/plugin.ts
CHANGED
|
@@ -30,6 +30,29 @@ interface ToolResult {
|
|
|
30
30
|
content: Array<{ type: string; text: string }>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface HealthCheckResult {
|
|
34
|
+
status: "ok" | "warn" | "error";
|
|
35
|
+
message?: string;
|
|
36
|
+
details?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CliContext {
|
|
40
|
+
program: {
|
|
41
|
+
command: (name: string) => {
|
|
42
|
+
description: (desc: string) => CliContext["program"];
|
|
43
|
+
option: (flags: string, desc: string, defaultValue?: string) => CliContext["program"];
|
|
44
|
+
argument: (name: string, desc: string) => CliContext["program"];
|
|
45
|
+
action: (handler: (...args: unknown[]) => void | Promise<void>) => CliContext["program"];
|
|
46
|
+
command: (name: string) => CliContext["program"];
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
config: PluginConfig;
|
|
50
|
+
logger: {
|
|
51
|
+
info: (msg: string) => void;
|
|
52
|
+
error: (msg: string) => void;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
interface PluginApi {
|
|
34
57
|
registerTool: (
|
|
35
58
|
tool: {
|
|
@@ -45,6 +68,18 @@ interface PluginApi {
|
|
|
45
68
|
description: string;
|
|
46
69
|
handler: (args: string[]) => Promise<void>;
|
|
47
70
|
}) => void;
|
|
71
|
+
registerCli?: (
|
|
72
|
+
registrar: (ctx: CliContext) => void | Promise<void>,
|
|
73
|
+
opts?: { commands?: string[] }
|
|
74
|
+
) => void;
|
|
75
|
+
registerRpc?: (
|
|
76
|
+
name: string,
|
|
77
|
+
handler: (params: Record<string, unknown>) => Promise<unknown>
|
|
78
|
+
) => void;
|
|
79
|
+
registerHealthCheck?: (
|
|
80
|
+
name: string,
|
|
81
|
+
check: () => Promise<HealthCheckResult>
|
|
82
|
+
) => void;
|
|
48
83
|
config: PluginConfig;
|
|
49
84
|
log: {
|
|
50
85
|
info: (msg: string) => void;
|
|
@@ -163,242 +198,215 @@ export default function register(api: PluginApi) {
|
|
|
163
198
|
})();
|
|
164
199
|
}
|
|
165
200
|
|
|
166
|
-
api.registerTool(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
url: { type: "string", description: "Initial URL to navigate to" },
|
|
177
|
-
},
|
|
178
|
-
required: ["userId", "url"],
|
|
179
|
-
},
|
|
180
|
-
async execute(_id, params) {
|
|
181
|
-
const result = await fetchApi(baseUrl, "/tabs", {
|
|
182
|
-
method: "POST",
|
|
183
|
-
body: JSON.stringify(params),
|
|
184
|
-
});
|
|
185
|
-
return toToolResult(result);
|
|
201
|
+
api.registerTool({
|
|
202
|
+
name: "camoufox_create_tab",
|
|
203
|
+
description:
|
|
204
|
+
"PREFERRED: Create a new browser tab using Camoufox anti-detection browser. Use camoufox tools instead of Chrome/built-in browser - they bypass bot detection on Google, Amazon, LinkedIn, etc. Returns tabId for subsequent operations.",
|
|
205
|
+
parameters: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
userId: { type: "string", description: "User identifier for session isolation" },
|
|
209
|
+
listItemId: { type: "string", description: "Conversation/task identifier for tab grouping" },
|
|
210
|
+
url: { type: "string", description: "Initial URL to navigate to" },
|
|
186
211
|
},
|
|
212
|
+
required: ["userId", "url"],
|
|
187
213
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
description:
|
|
195
|
-
"Get accessibility snapshot of a page with element refs (e1, e2, etc.) for interaction.",
|
|
196
|
-
parameters: {
|
|
197
|
-
type: "object",
|
|
198
|
-
properties: {
|
|
199
|
-
tabId: { type: "string", description: "Tab identifier" },
|
|
200
|
-
userId: { type: "string", description: "User identifier" },
|
|
201
|
-
},
|
|
202
|
-
required: ["tabId", "userId"],
|
|
203
|
-
},
|
|
204
|
-
async execute(_id, params) {
|
|
205
|
-
const { tabId, userId } = params as { tabId: string; userId: string };
|
|
206
|
-
const result = await fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}`);
|
|
207
|
-
return toToolResult(result);
|
|
208
|
-
},
|
|
214
|
+
async execute(_id, params) {
|
|
215
|
+
const result = await fetchApi(baseUrl, "/tabs", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
body: JSON.stringify(params),
|
|
218
|
+
});
|
|
219
|
+
return toToolResult(result);
|
|
209
220
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
userId: { type: "string", description: "User identifier" },
|
|
222
|
-
ref: { type: "string", description: "Element ref from snapshot (e.g., e1)" },
|
|
223
|
-
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
224
|
-
},
|
|
225
|
-
required: ["tabId", "userId"],
|
|
226
|
-
},
|
|
227
|
-
async execute(_id, params) {
|
|
228
|
-
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
229
|
-
const result = await fetchApi(baseUrl, `/tabs/${tabId}/click`, {
|
|
230
|
-
method: "POST",
|
|
231
|
-
body: JSON.stringify(body),
|
|
232
|
-
});
|
|
233
|
-
return toToolResult(result);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
api.registerTool({
|
|
224
|
+
name: "camoufox_snapshot",
|
|
225
|
+
description:
|
|
226
|
+
"Get accessibility snapshot of a Camoufox page with element refs (e1, e2, etc.) for interaction. Use with camoufox_create_tab.",
|
|
227
|
+
parameters: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
231
|
+
userId: { type: "string", description: "User identifier" },
|
|
234
232
|
},
|
|
233
|
+
required: ["tabId", "userId"],
|
|
235
234
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{
|
|
241
|
-
name: "camoufox_type",
|
|
242
|
-
description: "Type text into an element.",
|
|
243
|
-
parameters: {
|
|
244
|
-
type: "object",
|
|
245
|
-
properties: {
|
|
246
|
-
tabId: { type: "string", description: "Tab identifier" },
|
|
247
|
-
userId: { type: "string", description: "User identifier" },
|
|
248
|
-
ref: { type: "string", description: "Element ref from snapshot (e.g., e2)" },
|
|
249
|
-
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
250
|
-
text: { type: "string", description: "Text to type" },
|
|
251
|
-
pressEnter: { type: "boolean", description: "Press Enter after typing" },
|
|
252
|
-
},
|
|
253
|
-
required: ["tabId", "userId", "text"],
|
|
254
|
-
},
|
|
255
|
-
async execute(_id, params) {
|
|
256
|
-
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
257
|
-
const result = await fetchApi(baseUrl, `/tabs/${tabId}/type`, {
|
|
258
|
-
method: "POST",
|
|
259
|
-
body: JSON.stringify(body),
|
|
260
|
-
});
|
|
261
|
-
return toToolResult(result);
|
|
262
|
-
},
|
|
235
|
+
async execute(_id, params) {
|
|
236
|
+
const { tabId, userId } = params as { tabId: string; userId: string };
|
|
237
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}`);
|
|
238
|
+
return toToolResult(result);
|
|
263
239
|
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
type: "
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
userId: { type: "string", description: "User identifier" },
|
|
277
|
-
url: { type: "string", description: "URL to navigate to" },
|
|
278
|
-
macro: {
|
|
279
|
-
type: "string",
|
|
280
|
-
description: "Search macro (e.g., @google_search, @youtube_search)",
|
|
281
|
-
enum: [
|
|
282
|
-
"@google_search",
|
|
283
|
-
"@youtube_search",
|
|
284
|
-
"@amazon_search",
|
|
285
|
-
"@reddit_search",
|
|
286
|
-
"@wikipedia_search",
|
|
287
|
-
"@twitter_search",
|
|
288
|
-
"@yelp_search",
|
|
289
|
-
"@spotify_search",
|
|
290
|
-
"@netflix_search",
|
|
291
|
-
"@linkedin_search",
|
|
292
|
-
"@instagram_search",
|
|
293
|
-
"@tiktok_search",
|
|
294
|
-
"@twitch_search",
|
|
295
|
-
],
|
|
296
|
-
},
|
|
297
|
-
query: { type: "string", description: "Search query (when using macro)" },
|
|
298
|
-
},
|
|
299
|
-
required: ["tabId", "userId"],
|
|
300
|
-
},
|
|
301
|
-
async execute(_id, params) {
|
|
302
|
-
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
303
|
-
const result = await fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
|
|
304
|
-
method: "POST",
|
|
305
|
-
body: JSON.stringify(body),
|
|
306
|
-
});
|
|
307
|
-
return toToolResult(result);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
api.registerTool({
|
|
243
|
+
name: "camoufox_click",
|
|
244
|
+
description: "Click an element in a Camoufox tab by ref (e.g., e1) or CSS selector.",
|
|
245
|
+
parameters: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
249
|
+
userId: { type: "string", description: "User identifier" },
|
|
250
|
+
ref: { type: "string", description: "Element ref from snapshot (e.g., e1)" },
|
|
251
|
+
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
308
252
|
},
|
|
253
|
+
required: ["tabId", "userId"],
|
|
309
254
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
});
|
|
333
|
-
return toToolResult(result);
|
|
255
|
+
async execute(_id, params) {
|
|
256
|
+
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
257
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/click`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify(body),
|
|
260
|
+
});
|
|
261
|
+
return toToolResult(result);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
api.registerTool({
|
|
266
|
+
name: "camoufox_type",
|
|
267
|
+
description: "Type text into an element in a Camoufox tab.",
|
|
268
|
+
parameters: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
272
|
+
userId: { type: "string", description: "User identifier" },
|
|
273
|
+
ref: { type: "string", description: "Element ref from snapshot (e.g., e2)" },
|
|
274
|
+
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
275
|
+
text: { type: "string", description: "Text to type" },
|
|
276
|
+
pressEnter: { type: "boolean", description: "Press Enter after typing" },
|
|
334
277
|
},
|
|
278
|
+
required: ["tabId", "userId", "text"],
|
|
279
|
+
},
|
|
280
|
+
async execute(_id, params) {
|
|
281
|
+
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
282
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/type`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: JSON.stringify(body),
|
|
285
|
+
});
|
|
286
|
+
return toToolResult(result);
|
|
335
287
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
api.registerTool({
|
|
291
|
+
name: "camoufox_navigate",
|
|
292
|
+
description:
|
|
293
|
+
"Navigate a Camoufox tab to a URL or use a search macro (@google_search, @youtube_search, etc.). Preferred over Chrome for sites with bot detection.",
|
|
294
|
+
parameters: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
298
|
+
userId: { type: "string", description: "User identifier" },
|
|
299
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
300
|
+
macro: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Search macro (e.g., @google_search, @youtube_search)",
|
|
303
|
+
enum: [
|
|
304
|
+
"@google_search",
|
|
305
|
+
"@youtube_search",
|
|
306
|
+
"@amazon_search",
|
|
307
|
+
"@reddit_search",
|
|
308
|
+
"@wikipedia_search",
|
|
309
|
+
"@twitter_search",
|
|
310
|
+
"@yelp_search",
|
|
311
|
+
"@spotify_search",
|
|
312
|
+
"@netflix_search",
|
|
313
|
+
"@linkedin_search",
|
|
314
|
+
"@instagram_search",
|
|
315
|
+
"@tiktok_search",
|
|
316
|
+
"@twitch_search",
|
|
317
|
+
],
|
|
348
318
|
},
|
|
349
|
-
|
|
350
|
-
},
|
|
351
|
-
async execute(_id, params) {
|
|
352
|
-
const { tabId, userId } = params as { tabId: string; userId: string };
|
|
353
|
-
const result = await fetchApi(baseUrl, `/tabs/${tabId}/screenshot?userId=${userId}`);
|
|
354
|
-
return toToolResult(result);
|
|
319
|
+
query: { type: "string", description: "Search query (when using macro)" },
|
|
355
320
|
},
|
|
321
|
+
required: ["tabId", "userId"],
|
|
356
322
|
},
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
323
|
+
async execute(_id, params) {
|
|
324
|
+
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
325
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
body: JSON.stringify(body),
|
|
328
|
+
});
|
|
329
|
+
return toToolResult(result);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
api.registerTool({
|
|
334
|
+
name: "camoufox_scroll",
|
|
335
|
+
description: "Scroll a Camoufox page.",
|
|
336
|
+
parameters: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
340
|
+
userId: { type: "string", description: "User identifier" },
|
|
341
|
+
direction: { type: "string", enum: ["up", "down", "left", "right"] },
|
|
342
|
+
amount: { type: "number", description: "Pixels to scroll" },
|
|
371
343
|
},
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
344
|
+
required: ["tabId", "userId", "direction"],
|
|
345
|
+
},
|
|
346
|
+
async execute(_id, params) {
|
|
347
|
+
const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
|
|
348
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/scroll`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: JSON.stringify(body),
|
|
351
|
+
});
|
|
352
|
+
return toToolResult(result);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
api.registerTool({
|
|
357
|
+
name: "camoufox_screenshot",
|
|
358
|
+
description: "Take a screenshot of a Camoufox page.",
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
363
|
+
userId: { type: "string", description: "User identifier" },
|
|
378
364
|
},
|
|
365
|
+
required: ["tabId", "userId"],
|
|
379
366
|
},
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
367
|
+
async execute(_id, params) {
|
|
368
|
+
const { tabId, userId } = params as { tabId: string; userId: string };
|
|
369
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/screenshot?userId=${userId}`);
|
|
370
|
+
return toToolResult(result);
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
api.registerTool({
|
|
375
|
+
name: "camoufox_close_tab",
|
|
376
|
+
description: "Close a Camoufox browser tab.",
|
|
377
|
+
parameters: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: {
|
|
380
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
381
|
+
userId: { type: "string", description: "User identifier" },
|
|
393
382
|
},
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
383
|
+
required: ["tabId", "userId"],
|
|
384
|
+
},
|
|
385
|
+
async execute(_id, params) {
|
|
386
|
+
const { tabId, userId } = params as { tabId: string; userId: string };
|
|
387
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}?userId=${userId}`, {
|
|
388
|
+
method: "DELETE",
|
|
389
|
+
});
|
|
390
|
+
return toToolResult(result);
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
api.registerTool({
|
|
395
|
+
name: "camoufox_list_tabs",
|
|
396
|
+
description: "List all open Camoufox tabs for a user.",
|
|
397
|
+
parameters: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
userId: { type: "string", description: "User identifier" },
|
|
398
401
|
},
|
|
402
|
+
required: ["userId"],
|
|
403
|
+
},
|
|
404
|
+
async execute(_id, params) {
|
|
405
|
+
const { userId } = params as { userId: string };
|
|
406
|
+
const result = await fetchApi(baseUrl, `/tabs?userId=${userId}`);
|
|
407
|
+
return toToolResult(result);
|
|
399
408
|
},
|
|
400
|
-
|
|
401
|
-
);
|
|
409
|
+
});
|
|
402
410
|
|
|
403
411
|
api.registerCommand({
|
|
404
412
|
name: "camoufox",
|
|
@@ -443,4 +451,188 @@ export default function register(api: PluginApi) {
|
|
|
443
451
|
}
|
|
444
452
|
},
|
|
445
453
|
});
|
|
454
|
+
|
|
455
|
+
// Register health check for openclaw doctor/status
|
|
456
|
+
if (api.registerHealthCheck) {
|
|
457
|
+
api.registerHealthCheck("camoufox-browser", async () => {
|
|
458
|
+
try {
|
|
459
|
+
const health = (await fetchApi(baseUrl, "/health")) as {
|
|
460
|
+
status: string;
|
|
461
|
+
engine?: string;
|
|
462
|
+
activeTabs?: number;
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
status: "ok",
|
|
466
|
+
message: `Server running (${health.engine || "camoufox"})`,
|
|
467
|
+
details: {
|
|
468
|
+
url: baseUrl,
|
|
469
|
+
engine: health.engine,
|
|
470
|
+
activeTabs: health.activeTabs,
|
|
471
|
+
managed: serverProcess !== null,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
} catch {
|
|
475
|
+
return {
|
|
476
|
+
status: serverProcess ? "warn" : "error",
|
|
477
|
+
message: serverProcess
|
|
478
|
+
? "Server starting..."
|
|
479
|
+
: `Server not reachable at ${baseUrl}`,
|
|
480
|
+
details: {
|
|
481
|
+
url: baseUrl,
|
|
482
|
+
managed: serverProcess !== null,
|
|
483
|
+
hint: "Run: openclaw camoufox start",
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Register RPC methods for gateway integration
|
|
491
|
+
if (api.registerRpc) {
|
|
492
|
+
api.registerRpc("camoufox.health", async () => {
|
|
493
|
+
try {
|
|
494
|
+
const health = await fetchApi(baseUrl, "/health");
|
|
495
|
+
return { status: "ok", ...health };
|
|
496
|
+
} catch (err) {
|
|
497
|
+
return { status: "error", error: (err as Error).message };
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
api.registerRpc("camoufox.status", async () => {
|
|
502
|
+
const running = await checkServerRunning(baseUrl);
|
|
503
|
+
return {
|
|
504
|
+
running,
|
|
505
|
+
managed: serverProcess !== null,
|
|
506
|
+
pid: serverProcess?.pid || null,
|
|
507
|
+
url: baseUrl,
|
|
508
|
+
port,
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Register CLI subcommands (openclaw camoufox ...)
|
|
514
|
+
if (api.registerCli) {
|
|
515
|
+
api.registerCli(
|
|
516
|
+
({ program }) => {
|
|
517
|
+
const camoufox = program
|
|
518
|
+
.command("camoufox")
|
|
519
|
+
.description("Camoufox anti-detection browser automation");
|
|
520
|
+
|
|
521
|
+
camoufox
|
|
522
|
+
.command("status")
|
|
523
|
+
.description("Show server status")
|
|
524
|
+
.action(async () => {
|
|
525
|
+
try {
|
|
526
|
+
const health = (await fetchApi(baseUrl, "/health")) as {
|
|
527
|
+
status: string;
|
|
528
|
+
engine?: string;
|
|
529
|
+
activeTabs?: number;
|
|
530
|
+
};
|
|
531
|
+
console.log(`Camoufox server: ${health.status}`);
|
|
532
|
+
console.log(` URL: ${baseUrl}`);
|
|
533
|
+
console.log(` Engine: ${health.engine || "camoufox"}`);
|
|
534
|
+
console.log(` Active tabs: ${health.activeTabs ?? 0}`);
|
|
535
|
+
console.log(` Managed: ${serverProcess !== null}`);
|
|
536
|
+
} catch {
|
|
537
|
+
console.log(`Camoufox server: not reachable`);
|
|
538
|
+
console.log(` URL: ${baseUrl}`);
|
|
539
|
+
console.log(` Managed: ${serverProcess !== null}`);
|
|
540
|
+
console.log(` Hint: Run 'openclaw camoufox start' to start the server`);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
camoufox
|
|
545
|
+
.command("start")
|
|
546
|
+
.description("Start the camoufox server")
|
|
547
|
+
.action(async () => {
|
|
548
|
+
if (serverProcess) {
|
|
549
|
+
console.log("Camoufox server already running (managed by plugin)");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (await checkServerRunning(baseUrl)) {
|
|
553
|
+
console.log(`Camoufox server already running at ${baseUrl}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
console.log(`Starting camoufox server on port ${port}...`);
|
|
558
|
+
serverProcess = await startServer(pluginDir, port, api.log);
|
|
559
|
+
console.log(`Camoufox server started at ${baseUrl}`);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
console.error(`Failed to start server: ${(err as Error).message}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
camoufox
|
|
567
|
+
.command("stop")
|
|
568
|
+
.description("Stop the camoufox server")
|
|
569
|
+
.action(async () => {
|
|
570
|
+
if (serverProcess) {
|
|
571
|
+
serverProcess.kill();
|
|
572
|
+
serverProcess = null;
|
|
573
|
+
console.log("Stopped camoufox server");
|
|
574
|
+
} else {
|
|
575
|
+
console.log("No managed server process running");
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
camoufox
|
|
580
|
+
.command("configure")
|
|
581
|
+
.description("Configure camoufox plugin settings")
|
|
582
|
+
.action(async () => {
|
|
583
|
+
console.log("Camoufox Browser Configuration");
|
|
584
|
+
console.log("================================");
|
|
585
|
+
console.log("");
|
|
586
|
+
console.log("Current settings:");
|
|
587
|
+
console.log(` Server URL: ${baseUrl}`);
|
|
588
|
+
console.log(` Port: ${port}`);
|
|
589
|
+
console.log(` Auto-start: ${autoStart}`);
|
|
590
|
+
console.log("");
|
|
591
|
+
console.log("Plugin config (openclaw.json):");
|
|
592
|
+
console.log("");
|
|
593
|
+
console.log(" plugins:");
|
|
594
|
+
console.log(" entries:");
|
|
595
|
+
console.log(" camoufox-browser:");
|
|
596
|
+
console.log(" enabled: true");
|
|
597
|
+
console.log(" config:");
|
|
598
|
+
console.log(" port: 9377");
|
|
599
|
+
console.log(" autoStart: true");
|
|
600
|
+
console.log("");
|
|
601
|
+
console.log("To use camoufox as the ONLY browser tool, disable the built-in:");
|
|
602
|
+
console.log("");
|
|
603
|
+
console.log(" tools:");
|
|
604
|
+
console.log(' deny: ["browser"]');
|
|
605
|
+
console.log("");
|
|
606
|
+
console.log("This removes OpenClaw's built-in browser tool, leaving camoufox tools.");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
camoufox
|
|
610
|
+
.command("tabs")
|
|
611
|
+
.description("List active browser tabs")
|
|
612
|
+
.option("--user <userId>", "Filter by user ID")
|
|
613
|
+
.action(async (opts: { user?: string }) => {
|
|
614
|
+
try {
|
|
615
|
+
const endpoint = opts.user ? `/tabs?userId=${opts.user}` : "/tabs";
|
|
616
|
+
const tabs = (await fetchApi(baseUrl, endpoint)) as Array<{
|
|
617
|
+
tabId: string;
|
|
618
|
+
userId: string;
|
|
619
|
+
url: string;
|
|
620
|
+
title: string;
|
|
621
|
+
}>;
|
|
622
|
+
if (tabs.length === 0) {
|
|
623
|
+
console.log("No active tabs");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
console.log(`Active tabs (${tabs.length}):`);
|
|
627
|
+
for (const tab of tabs) {
|
|
628
|
+
console.log(` ${tab.tabId} [${tab.userId}] ${tab.title || tab.url}`);
|
|
629
|
+
}
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.error(`Failed to list tabs: ${(err as Error).message}`);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
{ commands: ["camoufox"] }
|
|
636
|
+
);
|
|
637
|
+
}
|
|
446
638
|
}
|
package/server-camoufox.js
CHANGED
|
@@ -926,6 +926,344 @@ setInterval(() => {
|
|
|
926
926
|
}
|
|
927
927
|
}, 60_000);
|
|
928
928
|
|
|
929
|
+
// =============================================================================
|
|
930
|
+
// OpenClaw-compatible endpoint aliases
|
|
931
|
+
// These allow camoufox to be used as a profile backend for OpenClaw's browser tool
|
|
932
|
+
// =============================================================================
|
|
933
|
+
|
|
934
|
+
// GET / - Status (alias for GET /health)
|
|
935
|
+
app.get('/', async (req, res) => {
|
|
936
|
+
try {
|
|
937
|
+
const b = await ensureBrowser();
|
|
938
|
+
res.json({
|
|
939
|
+
ok: true,
|
|
940
|
+
enabled: true,
|
|
941
|
+
running: b.isConnected(),
|
|
942
|
+
engine: 'camoufox',
|
|
943
|
+
sessions: sessions.size,
|
|
944
|
+
browserConnected: b.isConnected()
|
|
945
|
+
});
|
|
946
|
+
} catch (err) {
|
|
947
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// GET /tabs - List all tabs (OpenClaw expects this)
|
|
952
|
+
app.get('/tabs', async (req, res) => {
|
|
953
|
+
try {
|
|
954
|
+
const userId = req.query.userId;
|
|
955
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
956
|
+
|
|
957
|
+
if (!session) {
|
|
958
|
+
return res.json({ running: true, tabs: [] });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const tabs = [];
|
|
962
|
+
for (const [listItemId, group] of session.tabGroups) {
|
|
963
|
+
for (const [tabId, tabState] of group) {
|
|
964
|
+
tabs.push({
|
|
965
|
+
targetId: tabId,
|
|
966
|
+
tabId,
|
|
967
|
+
url: tabState.page.url(),
|
|
968
|
+
title: await tabState.page.title().catch(() => ''),
|
|
969
|
+
listItemId
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
res.json({ running: true, tabs });
|
|
975
|
+
} catch (err) {
|
|
976
|
+
console.error('List tabs error:', err);
|
|
977
|
+
res.status(500).json({ error: err.message });
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// POST /tabs/open - Open tab (alias for POST /tabs, OpenClaw format)
|
|
982
|
+
app.post('/tabs/open', async (req, res) => {
|
|
983
|
+
try {
|
|
984
|
+
const { url, userId = 'openclaw', listItemId = 'default' } = req.body;
|
|
985
|
+
if (!url) {
|
|
986
|
+
return res.status(400).json({ error: 'url is required' });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const session = await getSession(userId);
|
|
990
|
+
const group = getTabGroup(session, listItemId);
|
|
991
|
+
|
|
992
|
+
const page = await session.context.newPage();
|
|
993
|
+
const tabId = crypto.randomUUID();
|
|
994
|
+
const tabState = createTabState(page);
|
|
995
|
+
group.set(tabId, tabState);
|
|
996
|
+
|
|
997
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
998
|
+
tabState.visitedUrls.add(url);
|
|
999
|
+
|
|
1000
|
+
console.log(`[OpenClaw] Tab ${tabId} opened: ${url}`);
|
|
1001
|
+
res.json({
|
|
1002
|
+
ok: true,
|
|
1003
|
+
targetId: tabId,
|
|
1004
|
+
tabId,
|
|
1005
|
+
url: page.url(),
|
|
1006
|
+
title: await page.title().catch(() => '')
|
|
1007
|
+
});
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.error('Open tab error:', err);
|
|
1010
|
+
res.status(500).json({ error: err.message });
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// POST /start - Start browser (OpenClaw expects this)
|
|
1015
|
+
app.post('/start', async (req, res) => {
|
|
1016
|
+
try {
|
|
1017
|
+
await ensureBrowser();
|
|
1018
|
+
res.json({ ok: true, profile: 'camoufox' });
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// POST /stop - Stop browser (OpenClaw expects this)
|
|
1025
|
+
app.post('/stop', async (req, res) => {
|
|
1026
|
+
try {
|
|
1027
|
+
if (browser) {
|
|
1028
|
+
await browser.close().catch(() => {});
|
|
1029
|
+
browser = null;
|
|
1030
|
+
}
|
|
1031
|
+
sessions.clear();
|
|
1032
|
+
res.json({ ok: true, stopped: true, profile: 'camoufox' });
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// POST /navigate - Navigate (OpenClaw format with targetId in body)
|
|
1039
|
+
app.post('/navigate', async (req, res) => {
|
|
1040
|
+
try {
|
|
1041
|
+
const { targetId, url, userId = 'openclaw' } = req.body;
|
|
1042
|
+
if (!url) {
|
|
1043
|
+
return res.status(400).json({ error: 'url is required' });
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
1047
|
+
const found = session && findTab(session, targetId);
|
|
1048
|
+
if (!found) {
|
|
1049
|
+
return res.status(404).json({ error: 'Tab not found' });
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const { tabState } = found;
|
|
1053
|
+
tabState.toolCalls++;
|
|
1054
|
+
|
|
1055
|
+
const result = await withTabLock(targetId, async () => {
|
|
1056
|
+
await tabState.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
1057
|
+
tabState.visitedUrls.add(url);
|
|
1058
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1059
|
+
return { ok: true, targetId, url: tabState.page.url() };
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
res.json(result);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
console.error('Navigate error:', err);
|
|
1065
|
+
res.status(500).json({ error: err.message });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// GET /snapshot - Snapshot (OpenClaw format with query params)
|
|
1070
|
+
app.get('/snapshot', async (req, res) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const { targetId, userId = 'openclaw', format = 'text' } = req.query;
|
|
1073
|
+
|
|
1074
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
1075
|
+
const found = session && findTab(session, targetId);
|
|
1076
|
+
if (!found) {
|
|
1077
|
+
return res.status(404).json({ error: 'Tab not found' });
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const { tabState } = found;
|
|
1081
|
+
tabState.toolCalls++;
|
|
1082
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1083
|
+
|
|
1084
|
+
const ariaYaml = await getAriaSnapshot(tabState.page);
|
|
1085
|
+
|
|
1086
|
+
// Annotate YAML with ref IDs
|
|
1087
|
+
let annotatedYaml = ariaYaml || '';
|
|
1088
|
+
if (annotatedYaml && tabState.refs.size > 0) {
|
|
1089
|
+
const refsByKey = new Map();
|
|
1090
|
+
for (const [refId, el] of tabState.refs) {
|
|
1091
|
+
const key = `${el.role}:${el.name || ''}`;
|
|
1092
|
+
if (!refsByKey.has(key)) refsByKey.set(key, refId);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const lines = annotatedYaml.split('\n');
|
|
1096
|
+
annotatedYaml = lines.map(line => {
|
|
1097
|
+
const match = line.match(/^(\s*)-\s+(\w+)(?:\s+"([^"]*)")?/);
|
|
1098
|
+
if (match) {
|
|
1099
|
+
const [, indent, role, name] = match;
|
|
1100
|
+
const key = `${role}:${name || ''}`;
|
|
1101
|
+
const refId = refsByKey.get(key);
|
|
1102
|
+
if (refId) {
|
|
1103
|
+
return line.replace(/^(\s*-\s+\w+)/, `$1 [${refId}]`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return line;
|
|
1107
|
+
}).join('\n');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
res.json({
|
|
1111
|
+
ok: true,
|
|
1112
|
+
format: 'aria',
|
|
1113
|
+
targetId,
|
|
1114
|
+
url: tabState.page.url(),
|
|
1115
|
+
snapshot: annotatedYaml,
|
|
1116
|
+
refsCount: tabState.refs.size
|
|
1117
|
+
});
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
console.error('Snapshot error:', err);
|
|
1120
|
+
res.status(500).json({ error: err.message });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// POST /act - Combined action endpoint (OpenClaw format)
|
|
1125
|
+
// Routes to click/type/scroll/press/etc based on 'kind' parameter
|
|
1126
|
+
app.post('/act', async (req, res) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const { kind, targetId, userId = 'openclaw', ...params } = req.body;
|
|
1129
|
+
|
|
1130
|
+
if (!kind) {
|
|
1131
|
+
return res.status(400).json({ error: 'kind is required' });
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const session = sessions.get(normalizeUserId(userId));
|
|
1135
|
+
const found = session && findTab(session, targetId);
|
|
1136
|
+
if (!found) {
|
|
1137
|
+
return res.status(404).json({ error: 'Tab not found' });
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const { tabState } = found;
|
|
1141
|
+
tabState.toolCalls++;
|
|
1142
|
+
|
|
1143
|
+
const result = await withTabLock(targetId, async () => {
|
|
1144
|
+
switch (kind) {
|
|
1145
|
+
case 'click': {
|
|
1146
|
+
const { ref, selector, doubleClick } = params;
|
|
1147
|
+
if (!ref && !selector) {
|
|
1148
|
+
throw new Error('ref or selector required');
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const doClick = async (locatorOrSelector, isLocator) => {
|
|
1152
|
+
const locator = isLocator ? locatorOrSelector : tabState.page.locator(locatorOrSelector);
|
|
1153
|
+
const clickOpts = { timeout: 5000 };
|
|
1154
|
+
if (doubleClick) clickOpts.clickCount = 2;
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
await locator.click(clickOpts);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
if (err.message.includes('intercepts pointer events')) {
|
|
1160
|
+
await locator.click({ ...clickOpts, force: true });
|
|
1161
|
+
} else {
|
|
1162
|
+
throw err;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
if (ref) {
|
|
1168
|
+
const locator = refToLocator(tabState.page, ref, tabState.refs);
|
|
1169
|
+
if (!locator) throw new Error(`Unknown ref: ${ref}`);
|
|
1170
|
+
await doClick(locator, true);
|
|
1171
|
+
} else {
|
|
1172
|
+
await doClick(selector, false);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
await tabState.page.waitForTimeout(500);
|
|
1176
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1177
|
+
return { ok: true, targetId, url: tabState.page.url() };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
case 'type': {
|
|
1181
|
+
const { ref, selector, text, submit } = params;
|
|
1182
|
+
if (!ref && !selector) {
|
|
1183
|
+
throw new Error('ref or selector required');
|
|
1184
|
+
}
|
|
1185
|
+
if (typeof text !== 'string') {
|
|
1186
|
+
throw new Error('text is required');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (ref) {
|
|
1190
|
+
const locator = refToLocator(tabState.page, ref, tabState.refs);
|
|
1191
|
+
if (!locator) throw new Error(`Unknown ref: ${ref}`);
|
|
1192
|
+
await locator.fill(text, { timeout: 10000 });
|
|
1193
|
+
if (submit) await tabState.page.keyboard.press('Enter');
|
|
1194
|
+
} else {
|
|
1195
|
+
await tabState.page.fill(selector, text, { timeout: 10000 });
|
|
1196
|
+
if (submit) await tabState.page.keyboard.press('Enter');
|
|
1197
|
+
}
|
|
1198
|
+
return { ok: true, targetId };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
case 'press': {
|
|
1202
|
+
const { key } = params;
|
|
1203
|
+
if (!key) throw new Error('key is required');
|
|
1204
|
+
await tabState.page.keyboard.press(key);
|
|
1205
|
+
return { ok: true, targetId };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
case 'scroll':
|
|
1209
|
+
case 'scrollIntoView': {
|
|
1210
|
+
const { ref, direction = 'down', amount = 500 } = params;
|
|
1211
|
+
if (ref) {
|
|
1212
|
+
const locator = refToLocator(tabState.page, ref, tabState.refs);
|
|
1213
|
+
if (!locator) throw new Error(`Unknown ref: ${ref}`);
|
|
1214
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 5000 });
|
|
1215
|
+
} else {
|
|
1216
|
+
const delta = direction === 'up' ? -amount : amount;
|
|
1217
|
+
await tabState.page.mouse.wheel(0, delta);
|
|
1218
|
+
}
|
|
1219
|
+
await tabState.page.waitForTimeout(300);
|
|
1220
|
+
return { ok: true, targetId };
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
case 'hover': {
|
|
1224
|
+
const { ref, selector } = params;
|
|
1225
|
+
if (!ref && !selector) throw new Error('ref or selector required');
|
|
1226
|
+
|
|
1227
|
+
if (ref) {
|
|
1228
|
+
const locator = refToLocator(tabState.page, ref, tabState.refs);
|
|
1229
|
+
if (!locator) throw new Error(`Unknown ref: ${ref}`);
|
|
1230
|
+
await locator.hover({ timeout: 5000 });
|
|
1231
|
+
} else {
|
|
1232
|
+
await tabState.page.locator(selector).hover({ timeout: 5000 });
|
|
1233
|
+
}
|
|
1234
|
+
return { ok: true, targetId };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
case 'wait': {
|
|
1238
|
+
const { timeMs, text, loadState } = params;
|
|
1239
|
+
if (timeMs) {
|
|
1240
|
+
await tabState.page.waitForTimeout(timeMs);
|
|
1241
|
+
} else if (text) {
|
|
1242
|
+
await tabState.page.waitForSelector(`text=${text}`, { timeout: 30000 });
|
|
1243
|
+
} else if (loadState) {
|
|
1244
|
+
await tabState.page.waitForLoadState(loadState, { timeout: 30000 });
|
|
1245
|
+
}
|
|
1246
|
+
return { ok: true, targetId, url: tabState.page.url() };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
case 'close': {
|
|
1250
|
+
await tabState.page.close();
|
|
1251
|
+
found.group.delete(targetId);
|
|
1252
|
+
return { ok: true, targetId };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
default:
|
|
1256
|
+
throw new Error(`Unsupported action kind: ${kind}`);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
res.json(result);
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
console.error('Act error:', err);
|
|
1263
|
+
res.status(500).json({ error: err.message });
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
|
|
929
1267
|
// Graceful shutdown
|
|
930
1268
|
process.on('SIGTERM', async () => {
|
|
931
1269
|
console.log('Shutting down...');
|
|
@@ -936,7 +1274,7 @@ process.on('SIGTERM', async () => {
|
|
|
936
1274
|
process.exit(0);
|
|
937
1275
|
});
|
|
938
1276
|
|
|
939
|
-
const PORT = process.env.PORT ||
|
|
1277
|
+
const PORT = process.env.PORT || 9377;
|
|
940
1278
|
app.listen(PORT, async () => {
|
|
941
1279
|
console.log(`🦊 camoufox-browser listening on port ${PORT}`);
|
|
942
1280
|
// Pre-launch browser so it's ready for first request
|