@web-auto/camo 0.1.26 → 0.2.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/LICENSE +21 -21
- package/README.md +586 -586
- package/bin/browser-service.mjs +11 -11
- package/bin/camo.mjs +22 -22
- package/package.json +48 -48
- package/scripts/build.mjs +19 -19
- package/scripts/bump-version.mjs +34 -34
- package/scripts/check-file-size.mjs +80 -80
- package/scripts/file-size-policy.json +12 -2
- package/scripts/install.mjs +76 -76
- package/scripts/release.sh +54 -54
- package/src/autoscript/action-providers/index.mjs +6 -6
- package/src/autoscript/impact-engine.mjs +78 -78
- package/src/autoscript/runtime.mjs +1017 -1017
- package/src/autoscript/schema.mjs +376 -376
- package/src/cli.mjs +405 -405
- package/src/commands/attach.mjs +141 -141
- package/src/commands/autoscript.mjs +1011 -1011
- package/src/commands/browser.mjs +1255 -1257
- package/src/commands/container.mjs +401 -401
- package/src/commands/cookies.mjs +69 -69
- package/src/commands/create.mjs +98 -98
- package/src/commands/devtools.mjs +349 -349
- package/src/commands/events.mjs +152 -152
- package/src/commands/highlight-mode.mjs +24 -24
- package/src/commands/init.mjs +68 -68
- package/src/commands/lifecycle.mjs +275 -275
- package/src/commands/mouse.mjs +45 -45
- package/src/commands/profile.mjs +46 -46
- package/src/commands/record.mjs +115 -115
- package/src/commands/system.mjs +14 -14
- package/src/commands/window.mjs +123 -123
- package/src/container/change-notifier.mjs +362 -362
- package/src/container/element-filter.mjs +143 -143
- package/src/container/index.mjs +3 -3
- package/src/container/runtime-core/checkpoint.mjs +209 -209
- package/src/container/runtime-core/index.mjs +21 -21
- package/src/container/runtime-core/operations/index.mjs +774 -774
- package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
- package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
- package/src/container/runtime-core/operations/viewport.mjs +189 -189
- package/src/container/runtime-core/search.mjs +190 -190
- package/src/container/runtime-core/subscription.mjs +224 -224
- package/src/container/runtime-core/utils.mjs +94 -94
- package/src/container/runtime-core/validation.mjs +127 -184
- package/src/container/runtime-core.mjs +1 -1
- package/src/container/subscription-registry.mjs +459 -459
- package/src/core/actions.mjs +561 -561
- package/src/core/browser.mjs +266 -266
- package/src/core/index.mjs +52 -52
- package/src/core/utils.mjs +91 -91
- package/src/events/daemon-entry.mjs +33 -33
- package/src/events/daemon.mjs +80 -80
- package/src/events/progress-log.mjs +109 -109
- package/src/events/ws-server.mjs +239 -239
- package/src/lib/client.mjs +200 -200
- package/src/lifecycle/cleanup.mjs +83 -83
- package/src/lifecycle/lock.mjs +126 -126
- package/src/lifecycle/session-registry.mjs +279 -279
- package/src/lifecycle/session-view.mjs +76 -76
- package/src/lifecycle/session-watchdog.mjs +281 -281
- package/src/services/browser-service/index.js +671 -674
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -336
- package/src/services/browser-service/internal/ElementRegistry.js +60 -60
- package/src/services/browser-service/internal/ProfileLock.js +84 -84
- package/src/services/browser-service/internal/SessionManager.js +184 -184
- package/src/services/browser-service/internal/SessionManager.test.js +39 -39
- package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
- package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
- package/src/services/browser-service/internal/browser-session/logging.js +46 -46
- package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
- package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
- package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
- package/src/services/browser-service/internal/browser-session/recording.js +198 -198
- package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
- package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
- package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
- package/src/services/browser-service/internal/browser-session/types.js +14 -14
- package/src/services/browser-service/internal/browser-session/utils.js +95 -95
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
- package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
- package/src/services/browser-service/internal/container-matcher.js +851 -851
- package/src/services/browser-service/internal/container-registry.js +182 -182
- package/src/services/browser-service/internal/engine-manager.js +259 -259
- package/src/services/browser-service/internal/fingerprint.js +203 -203
- package/src/services/browser-service/internal/heartbeat.js +137 -137
- package/src/services/browser-service/internal/logging.js +46 -46
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
- package/src/services/browser-service/internal/pageRuntime.js +28 -28
- package/src/services/browser-service/internal/runtimeInjector.js +31 -31
- package/src/services/browser-service/internal/service-process-logger.js +140 -140
- package/src/services/browser-service/internal/state-bus.js +45 -45
- package/src/services/browser-service/internal/storage-paths.js +42 -42
- package/src/services/browser-service/internal/ws-server.js +1194 -1194
- package/src/services/browser-service/internal/ws-server.test.js +58 -58
- package/src/services/browser-service/server.mjs +6 -6
- package/src/services/controller/cli-bridge.js +93 -93
- package/src/services/controller/container-index.js +50 -50
- package/src/services/controller/container-storage.js +36 -36
- package/src/services/controller/controller-actions.js +207 -207
- package/src/services/controller/controller.js +1138 -1138
- package/src/services/controller/selectors.js +54 -54
- package/src/services/controller/transport.js +125 -125
- package/src/utils/args.mjs +26 -26
- package/src/utils/browser-service.mjs +544 -544
- package/src/utils/command-log.mjs +64 -64
- package/src/utils/config.mjs +214 -214
- package/src/utils/fingerprint.mjs +181 -181
- package/src/utils/help.mjs +216 -216
- package/src/utils/js-policy.mjs +13 -13
- package/src/utils/ws-client.mjs +30 -30
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
- package/src/services/browser-service/index.js.bak +0 -671
package/src/core/actions.mjs
CHANGED
|
@@ -1,561 +1,561 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser actions module - Playwright-based page operations
|
|
3
|
-
* No external browser-service dependency
|
|
4
|
-
*/
|
|
5
|
-
import { getActiveBrowser, getCurrentPage, isBrowserRunning } from './browser.mjs';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Navigate to URL
|
|
9
|
-
*/
|
|
10
|
-
export async function navigateTo(profileId, url) {
|
|
11
|
-
if (!isBrowserRunning(profileId)) {
|
|
12
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const page = await getCurrentPage(profileId);
|
|
16
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
ok: true,
|
|
20
|
-
profileId,
|
|
21
|
-
url: page.url(),
|
|
22
|
-
title: await page.title(),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Go back
|
|
28
|
-
*/
|
|
29
|
-
export async function goBack(profileId) {
|
|
30
|
-
if (!isBrowserRunning(profileId)) {
|
|
31
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const page = await getCurrentPage(profileId);
|
|
35
|
-
await page.goBack({ waitUntil: 'domcontentloaded' });
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
ok: true,
|
|
39
|
-
profileId,
|
|
40
|
-
url: page.url(),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Take screenshot
|
|
46
|
-
*/
|
|
47
|
-
export async function takeScreenshot(profileId, options = {}) {
|
|
48
|
-
if (!isBrowserRunning(profileId)) {
|
|
49
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const page = await getCurrentPage(profileId);
|
|
53
|
-
const buffer = await page.screenshot({
|
|
54
|
-
fullPage: options.fullPage || false,
|
|
55
|
-
type: 'png',
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
ok: true,
|
|
60
|
-
profileId,
|
|
61
|
-
data: buffer.toString('base64'),
|
|
62
|
-
width: options.fullPage ? undefined : page.viewportSize()?.width,
|
|
63
|
-
height: options.fullPage ? undefined : page.viewportSize()?.height,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Scroll page
|
|
69
|
-
*/
|
|
70
|
-
export async function scrollPage(profileId, options = {}) {
|
|
71
|
-
if (!isBrowserRunning(profileId)) {
|
|
72
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const page = await getCurrentPage(profileId);
|
|
76
|
-
const direction = options.direction || 'down';
|
|
77
|
-
const amount = options.amount || 300;
|
|
78
|
-
|
|
79
|
-
const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
80
|
-
const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
81
|
-
|
|
82
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
ok: true,
|
|
86
|
-
profileId,
|
|
87
|
-
direction,
|
|
88
|
-
amount,
|
|
89
|
-
scrolled: true,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Click element
|
|
95
|
-
*/
|
|
96
|
-
export async function clickElement(profileId, selector, options = {}) {
|
|
97
|
-
if (!isBrowserRunning(profileId)) {
|
|
98
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const page = await getCurrentPage(profileId);
|
|
102
|
-
|
|
103
|
-
// Wait for element to be visible
|
|
104
|
-
await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
|
|
105
|
-
|
|
106
|
-
// Scroll into view if needed
|
|
107
|
-
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
108
|
-
|
|
109
|
-
// Click with system-level mouse
|
|
110
|
-
const element = await page.$(selector);
|
|
111
|
-
const box = await element.boundingBox();
|
|
112
|
-
|
|
113
|
-
if (!box) {
|
|
114
|
-
throw new Error(`Element not visible: ${selector}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const x = box.x + box.width / 2;
|
|
118
|
-
const y = box.y + box.height / 2;
|
|
119
|
-
|
|
120
|
-
await page.mouse.click(x, y, { button: options.button || 'left', clickCount: options.clickCount || 1 });
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
ok: true,
|
|
124
|
-
profileId,
|
|
125
|
-
selector,
|
|
126
|
-
clicked: true,
|
|
127
|
-
position: { x, y },
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Type text
|
|
133
|
-
*/
|
|
134
|
-
export async function typeText(profileId, selector, text, options = {}) {
|
|
135
|
-
if (!isBrowserRunning(profileId)) {
|
|
136
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const page = await getCurrentPage(profileId);
|
|
140
|
-
|
|
141
|
-
// Wait for element to be visible
|
|
142
|
-
await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
|
|
143
|
-
|
|
144
|
-
// Scroll into view if needed
|
|
145
|
-
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
146
|
-
|
|
147
|
-
// Focus and type
|
|
148
|
-
const element = await page.$(selector);
|
|
149
|
-
await element.focus();
|
|
150
|
-
await element.fill('');
|
|
151
|
-
|
|
152
|
-
if (options.slowly) {
|
|
153
|
-
await element.type(text, { delay: 50 });
|
|
154
|
-
} else {
|
|
155
|
-
await element.fill(text);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (options.pressEnter) {
|
|
159
|
-
await page.keyboard.press('Enter');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
ok: true,
|
|
164
|
-
profileId,
|
|
165
|
-
selector,
|
|
166
|
-
typed: text.length,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Press key
|
|
172
|
-
*/
|
|
173
|
-
export async function pressKey(profileId, key) {
|
|
174
|
-
if (!isBrowserRunning(profileId)) {
|
|
175
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const page = await getCurrentPage(profileId);
|
|
179
|
-
await page.keyboard.press(key);
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
ok: true,
|
|
183
|
-
profileId,
|
|
184
|
-
key,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Highlight element
|
|
190
|
-
*/
|
|
191
|
-
export async function highlightElement(profileId, selector, options = {}) {
|
|
192
|
-
if (!isBrowserRunning(profileId)) {
|
|
193
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const page = await getCurrentPage(profileId);
|
|
197
|
-
|
|
198
|
-
const result = await page.evaluate((sel) => {
|
|
199
|
-
const el = document.querySelector(sel);
|
|
200
|
-
if (!el) return null;
|
|
201
|
-
|
|
202
|
-
const prev = el.style.outline;
|
|
203
|
-
el.style.outline = '3px solid #ff4444';
|
|
204
|
-
|
|
205
|
-
const rect = el.getBoundingClientRect();
|
|
206
|
-
return {
|
|
207
|
-
highlighted: true,
|
|
208
|
-
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
209
|
-
prevOutline: prev,
|
|
210
|
-
};
|
|
211
|
-
}, selector);
|
|
212
|
-
|
|
213
|
-
if (!result) {
|
|
214
|
-
throw new Error(`Element not found: ${selector}`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Auto-clear highlight after duration
|
|
218
|
-
const duration = options.duration || 2000;
|
|
219
|
-
setTimeout(async () => {
|
|
220
|
-
try {
|
|
221
|
-
const currentPage = await getCurrentPage(profileId);
|
|
222
|
-
await currentPage.evaluate((sel) => {
|
|
223
|
-
const el = document.querySelector(sel);
|
|
224
|
-
if (el) el.style.outline = '';
|
|
225
|
-
}, selector);
|
|
226
|
-
} catch {
|
|
227
|
-
// Browser may have been closed
|
|
228
|
-
}
|
|
229
|
-
}, duration);
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
ok: true,
|
|
233
|
-
profileId,
|
|
234
|
-
selector,
|
|
235
|
-
...result,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Clear all highlights
|
|
241
|
-
*/
|
|
242
|
-
export async function clearHighlights(profileId) {
|
|
243
|
-
if (!isBrowserRunning(profileId)) {
|
|
244
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const page = await getCurrentPage(profileId);
|
|
248
|
-
await page.evaluate(() => {
|
|
249
|
-
document.querySelectorAll('[style*="outline"]').forEach((el) => {
|
|
250
|
-
el.style.outline = '';
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
ok: true,
|
|
256
|
-
profileId,
|
|
257
|
-
cleared: true,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Set viewport size
|
|
263
|
-
*/
|
|
264
|
-
export async function setViewport(profileId, width, height) {
|
|
265
|
-
if (!isBrowserRunning(profileId)) {
|
|
266
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const page = await getCurrentPage(profileId);
|
|
270
|
-
await page.setViewportSize({ width, height });
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
ok: true,
|
|
274
|
-
profileId,
|
|
275
|
-
viewport: { width, height },
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Get page info (URL, title, etc)
|
|
281
|
-
*/
|
|
282
|
-
export async function getPageInfo(profileId) {
|
|
283
|
-
if (!isBrowserRunning(profileId)) {
|
|
284
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const page = await getCurrentPage(profileId);
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
ok: true,
|
|
291
|
-
profileId,
|
|
292
|
-
url: page.url(),
|
|
293
|
-
title: await page.title(),
|
|
294
|
-
viewport: page.viewportSize(),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get DOM snapshot
|
|
300
|
-
*/
|
|
301
|
-
export async function getDOMSnapshot(profileId, options = {}) {
|
|
302
|
-
if (!isBrowserRunning(profileId)) {
|
|
303
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const page = await getCurrentPage(profileId);
|
|
307
|
-
|
|
308
|
-
const snapshot = await page.evaluate((opts) => {
|
|
309
|
-
const collectNodes = (node, depth = 0, path = 'root') => {
|
|
310
|
-
if (depth > 10) return null;
|
|
311
|
-
if (!node) return null;
|
|
312
|
-
|
|
313
|
-
const result = {
|
|
314
|
-
tag: node.tagName?.toLowerCase() || node.nodeName?.toLowerCase(),
|
|
315
|
-
id: node.id || null,
|
|
316
|
-
classes: node.className ? node.className.split(' ').filter(Boolean) : [],
|
|
317
|
-
path,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const rect = node.getBoundingClientRect?.() || null;
|
|
321
|
-
if (rect) {
|
|
322
|
-
result.rect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const text = node.textContent?.trim().slice(0, 50) || '';
|
|
326
|
-
if (text && node.children?.length === 0) {
|
|
327
|
-
result.text = text;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (node.children && node.children.length > 0) {
|
|
331
|
-
result.children = [];
|
|
332
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
333
|
-
const child = collectNodes(node.children[i], depth + 1, `${path}/${i}`);
|
|
334
|
-
if (child) result.children.push(child);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return result;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
342
|
-
const root = collectNodes(document.body, 0, 'root');
|
|
343
|
-
|
|
344
|
-
return { viewport, dom_tree: root };
|
|
345
|
-
}, options);
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
ok: true,
|
|
349
|
-
profileId,
|
|
350
|
-
...snapshot,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Query elements
|
|
356
|
-
*/
|
|
357
|
-
export async function queryElements(profileId, selector, options = {}) {
|
|
358
|
-
if (!isBrowserRunning(profileId)) {
|
|
359
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const page = await getCurrentPage(profileId);
|
|
363
|
-
|
|
364
|
-
const elements = await page.evaluate((sel) => {
|
|
365
|
-
const nodes = document.querySelectorAll(sel);
|
|
366
|
-
return Array.from(nodes).map((el, idx) => {
|
|
367
|
-
const rect = el.getBoundingClientRect();
|
|
368
|
-
return {
|
|
369
|
-
index: idx,
|
|
370
|
-
tag: el.tagName.toLowerCase(),
|
|
371
|
-
id: el.id || null,
|
|
372
|
-
classes: el.className.split(' ').filter(Boolean),
|
|
373
|
-
text: el.textContent?.trim().slice(0, 100) || '',
|
|
374
|
-
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
375
|
-
visible: rect.width > 0 && rect.height > 0,
|
|
376
|
-
};
|
|
377
|
-
});
|
|
378
|
-
}, selector);
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
ok: true,
|
|
382
|
-
profileId,
|
|
383
|
-
selector,
|
|
384
|
-
count: elements.length,
|
|
385
|
-
elements,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Execute JavaScript
|
|
391
|
-
*/
|
|
392
|
-
export async function evaluateJS(profileId, script) {
|
|
393
|
-
if (!isBrowserRunning(profileId)) {
|
|
394
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const page = await getCurrentPage(profileId);
|
|
398
|
-
const result = await page.evaluate(script);
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
ok: true,
|
|
402
|
-
profileId,
|
|
403
|
-
result,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Create new page
|
|
409
|
-
*/
|
|
410
|
-
export async function createNewPage(profileId, options = {}) {
|
|
411
|
-
const browser = getActiveBrowser(profileId);
|
|
412
|
-
if (!browser) {
|
|
413
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const context = browser.pwBrowser?.contexts()[0];
|
|
417
|
-
if (!context) {
|
|
418
|
-
throw new Error('No browser context available');
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const page = await context.newPage();
|
|
422
|
-
if (options.url) {
|
|
423
|
-
await page.goto(options.url, { waitUntil: 'domcontentloaded' });
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
browser.pages.push(page);
|
|
427
|
-
browser.currentPage = page;
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
ok: true,
|
|
431
|
-
profileId,
|
|
432
|
-
pageIndex: browser.pages.length - 1,
|
|
433
|
-
url: options.url || 'about:blank',
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* List pages
|
|
439
|
-
*/
|
|
440
|
-
export async function listPages(profileId) {
|
|
441
|
-
const browser = getActiveBrowser(profileId);
|
|
442
|
-
if (!browser) {
|
|
443
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const context = browser.pwBrowser?.contexts()[0];
|
|
447
|
-
if (!context) {
|
|
448
|
-
throw new Error('No browser context available');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const pages = context.pages();
|
|
452
|
-
const pageInfos = await Promise.all(pages.map(async (page, idx) => ({
|
|
453
|
-
index: idx,
|
|
454
|
-
url: page.url(),
|
|
455
|
-
title: await page.title().catch(() => ''),
|
|
456
|
-
})));
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
ok: true,
|
|
460
|
-
profileId,
|
|
461
|
-
count: pageInfos.length,
|
|
462
|
-
pages: pageInfos,
|
|
463
|
-
currentPage: browser.currentPage?._guid || 0,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Switch page
|
|
469
|
-
*/
|
|
470
|
-
export async function switchPage(profileId, pageIndex) {
|
|
471
|
-
const browser = getActiveBrowser(profileId);
|
|
472
|
-
if (!browser) {
|
|
473
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const context = browser.pwBrowser?.contexts()[0];
|
|
477
|
-
if (!context) {
|
|
478
|
-
throw new Error('No browser context available');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const pages = context.pages();
|
|
482
|
-
if (pageIndex < 0 || pageIndex >= pages.length) {
|
|
483
|
-
throw new Error(`Invalid page index: ${pageIndex}`);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
browser.currentPage = pages[pageIndex];
|
|
487
|
-
await pages[pageIndex].bringToFront();
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
ok: true,
|
|
491
|
-
profileId,
|
|
492
|
-
pageIndex,
|
|
493
|
-
url: pages[pageIndex].url(),
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Close page
|
|
499
|
-
*/
|
|
500
|
-
export async function closePage(profileId, pageIndex) {
|
|
501
|
-
const browser = getActiveBrowser(profileId);
|
|
502
|
-
if (!browser) {
|
|
503
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const context = browser.pwBrowser?.contexts()[0];
|
|
507
|
-
if (!context) {
|
|
508
|
-
throw new Error('No browser context available');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const pages = context.pages();
|
|
512
|
-
const idx = pageIndex !== undefined ? pageIndex : pages.length - 1;
|
|
513
|
-
|
|
514
|
-
if (idx < 0 || idx >= pages.length) {
|
|
515
|
-
throw new Error(`Invalid page index: ${idx}`);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
await pages[idx].close();
|
|
519
|
-
browser.pages = pages.filter((_, i) => i !== idx);
|
|
520
|
-
|
|
521
|
-
// Update current page if needed
|
|
522
|
-
if (browser.currentPage === pages[idx]) {
|
|
523
|
-
browser.currentPage = browser.pages[0] || null;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return {
|
|
527
|
-
ok: true,
|
|
528
|
-
profileId,
|
|
529
|
-
closedIndex: idx,
|
|
530
|
-
remaining: browser.pages.length,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Mouse operations
|
|
536
|
-
*/
|
|
537
|
-
export async function mouseClick(profileId, x, y, options = {}) {
|
|
538
|
-
if (!isBrowserRunning(profileId)) {
|
|
539
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const page = await getCurrentPage(profileId);
|
|
543
|
-
await page.mouse.click(x, y, {
|
|
544
|
-
button: options.button || 'left',
|
|
545
|
-
clickCount: options.clickCount || 1,
|
|
546
|
-
delay: options.delay || 0,
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
return { ok: true, profileId, x, y, button: options.button || 'left' };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export async function mouseWheel(profileId, deltaX, deltaY) {
|
|
553
|
-
if (!isBrowserRunning(profileId)) {
|
|
554
|
-
throw new Error(`No browser running for profile: ${profileId}`);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const page = await getCurrentPage(profileId);
|
|
558
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
559
|
-
|
|
560
|
-
return { ok: true, profileId, deltaX, deltaY };
|
|
561
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Browser actions module - Playwright-based page operations
|
|
3
|
+
* No external browser-service dependency
|
|
4
|
+
*/
|
|
5
|
+
import { getActiveBrowser, getCurrentPage, isBrowserRunning } from './browser.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Navigate to URL
|
|
9
|
+
*/
|
|
10
|
+
export async function navigateTo(profileId, url) {
|
|
11
|
+
if (!isBrowserRunning(profileId)) {
|
|
12
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const page = await getCurrentPage(profileId);
|
|
16
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
profileId,
|
|
21
|
+
url: page.url(),
|
|
22
|
+
title: await page.title(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Go back
|
|
28
|
+
*/
|
|
29
|
+
export async function goBack(profileId) {
|
|
30
|
+
if (!isBrowserRunning(profileId)) {
|
|
31
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const page = await getCurrentPage(profileId);
|
|
35
|
+
await page.goBack({ waitUntil: 'domcontentloaded' });
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
profileId,
|
|
40
|
+
url: page.url(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Take screenshot
|
|
46
|
+
*/
|
|
47
|
+
export async function takeScreenshot(profileId, options = {}) {
|
|
48
|
+
if (!isBrowserRunning(profileId)) {
|
|
49
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const page = await getCurrentPage(profileId);
|
|
53
|
+
const buffer = await page.screenshot({
|
|
54
|
+
fullPage: options.fullPage || false,
|
|
55
|
+
type: 'png',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
profileId,
|
|
61
|
+
data: buffer.toString('base64'),
|
|
62
|
+
width: options.fullPage ? undefined : page.viewportSize()?.width,
|
|
63
|
+
height: options.fullPage ? undefined : page.viewportSize()?.height,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Scroll page
|
|
69
|
+
*/
|
|
70
|
+
export async function scrollPage(profileId, options = {}) {
|
|
71
|
+
if (!isBrowserRunning(profileId)) {
|
|
72
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const page = await getCurrentPage(profileId);
|
|
76
|
+
const direction = options.direction || 'down';
|
|
77
|
+
const amount = options.amount || 300;
|
|
78
|
+
|
|
79
|
+
const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
80
|
+
const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
81
|
+
|
|
82
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
profileId,
|
|
87
|
+
direction,
|
|
88
|
+
amount,
|
|
89
|
+
scrolled: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Click element
|
|
95
|
+
*/
|
|
96
|
+
export async function clickElement(profileId, selector, options = {}) {
|
|
97
|
+
if (!isBrowserRunning(profileId)) {
|
|
98
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const page = await getCurrentPage(profileId);
|
|
102
|
+
|
|
103
|
+
// Wait for element to be visible
|
|
104
|
+
await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
|
|
105
|
+
|
|
106
|
+
// Scroll into view if needed
|
|
107
|
+
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
108
|
+
|
|
109
|
+
// Click with system-level mouse
|
|
110
|
+
const element = await page.$(selector);
|
|
111
|
+
const box = await element.boundingBox();
|
|
112
|
+
|
|
113
|
+
if (!box) {
|
|
114
|
+
throw new Error(`Element not visible: ${selector}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const x = box.x + box.width / 2;
|
|
118
|
+
const y = box.y + box.height / 2;
|
|
119
|
+
|
|
120
|
+
await page.mouse.click(x, y, { button: options.button || 'left', clickCount: options.clickCount || 1 });
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
profileId,
|
|
125
|
+
selector,
|
|
126
|
+
clicked: true,
|
|
127
|
+
position: { x, y },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Type text
|
|
133
|
+
*/
|
|
134
|
+
export async function typeText(profileId, selector, text, options = {}) {
|
|
135
|
+
if (!isBrowserRunning(profileId)) {
|
|
136
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const page = await getCurrentPage(profileId);
|
|
140
|
+
|
|
141
|
+
// Wait for element to be visible
|
|
142
|
+
await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
|
|
143
|
+
|
|
144
|
+
// Scroll into view if needed
|
|
145
|
+
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
146
|
+
|
|
147
|
+
// Focus and type
|
|
148
|
+
const element = await page.$(selector);
|
|
149
|
+
await element.focus();
|
|
150
|
+
await element.fill('');
|
|
151
|
+
|
|
152
|
+
if (options.slowly) {
|
|
153
|
+
await element.type(text, { delay: 50 });
|
|
154
|
+
} else {
|
|
155
|
+
await element.fill(text);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.pressEnter) {
|
|
159
|
+
await page.keyboard.press('Enter');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
profileId,
|
|
165
|
+
selector,
|
|
166
|
+
typed: text.length,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Press key
|
|
172
|
+
*/
|
|
173
|
+
export async function pressKey(profileId, key) {
|
|
174
|
+
if (!isBrowserRunning(profileId)) {
|
|
175
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const page = await getCurrentPage(profileId);
|
|
179
|
+
await page.keyboard.press(key);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
profileId,
|
|
184
|
+
key,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Highlight element
|
|
190
|
+
*/
|
|
191
|
+
export async function highlightElement(profileId, selector, options = {}) {
|
|
192
|
+
if (!isBrowserRunning(profileId)) {
|
|
193
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const page = await getCurrentPage(profileId);
|
|
197
|
+
|
|
198
|
+
const result = await page.evaluate((sel) => {
|
|
199
|
+
const el = document.querySelector(sel);
|
|
200
|
+
if (!el) return null;
|
|
201
|
+
|
|
202
|
+
const prev = el.style.outline;
|
|
203
|
+
el.style.outline = '3px solid #ff4444';
|
|
204
|
+
|
|
205
|
+
const rect = el.getBoundingClientRect();
|
|
206
|
+
return {
|
|
207
|
+
highlighted: true,
|
|
208
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
209
|
+
prevOutline: prev,
|
|
210
|
+
};
|
|
211
|
+
}, selector);
|
|
212
|
+
|
|
213
|
+
if (!result) {
|
|
214
|
+
throw new Error(`Element not found: ${selector}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Auto-clear highlight after duration
|
|
218
|
+
const duration = options.duration || 2000;
|
|
219
|
+
setTimeout(async () => {
|
|
220
|
+
try {
|
|
221
|
+
const currentPage = await getCurrentPage(profileId);
|
|
222
|
+
await currentPage.evaluate((sel) => {
|
|
223
|
+
const el = document.querySelector(sel);
|
|
224
|
+
if (el) el.style.outline = '';
|
|
225
|
+
}, selector);
|
|
226
|
+
} catch {
|
|
227
|
+
// Browser may have been closed
|
|
228
|
+
}
|
|
229
|
+
}, duration);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
profileId,
|
|
234
|
+
selector,
|
|
235
|
+
...result,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear all highlights
|
|
241
|
+
*/
|
|
242
|
+
export async function clearHighlights(profileId) {
|
|
243
|
+
if (!isBrowserRunning(profileId)) {
|
|
244
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const page = await getCurrentPage(profileId);
|
|
248
|
+
await page.evaluate(() => {
|
|
249
|
+
document.querySelectorAll('[style*="outline"]').forEach((el) => {
|
|
250
|
+
el.style.outline = '';
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
profileId,
|
|
257
|
+
cleared: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Set viewport size
|
|
263
|
+
*/
|
|
264
|
+
export async function setViewport(profileId, width, height) {
|
|
265
|
+
if (!isBrowserRunning(profileId)) {
|
|
266
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const page = await getCurrentPage(profileId);
|
|
270
|
+
await page.setViewportSize({ width, height });
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
profileId,
|
|
275
|
+
viewport: { width, height },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get page info (URL, title, etc)
|
|
281
|
+
*/
|
|
282
|
+
export async function getPageInfo(profileId) {
|
|
283
|
+
if (!isBrowserRunning(profileId)) {
|
|
284
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const page = await getCurrentPage(profileId);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
profileId,
|
|
292
|
+
url: page.url(),
|
|
293
|
+
title: await page.title(),
|
|
294
|
+
viewport: page.viewportSize(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get DOM snapshot
|
|
300
|
+
*/
|
|
301
|
+
export async function getDOMSnapshot(profileId, options = {}) {
|
|
302
|
+
if (!isBrowserRunning(profileId)) {
|
|
303
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const page = await getCurrentPage(profileId);
|
|
307
|
+
|
|
308
|
+
const snapshot = await page.evaluate((opts) => {
|
|
309
|
+
const collectNodes = (node, depth = 0, path = 'root') => {
|
|
310
|
+
if (depth > 10) return null;
|
|
311
|
+
if (!node) return null;
|
|
312
|
+
|
|
313
|
+
const result = {
|
|
314
|
+
tag: node.tagName?.toLowerCase() || node.nodeName?.toLowerCase(),
|
|
315
|
+
id: node.id || null,
|
|
316
|
+
classes: node.className ? node.className.split(' ').filter(Boolean) : [],
|
|
317
|
+
path,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const rect = node.getBoundingClientRect?.() || null;
|
|
321
|
+
if (rect) {
|
|
322
|
+
result.rect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const text = node.textContent?.trim().slice(0, 50) || '';
|
|
326
|
+
if (text && node.children?.length === 0) {
|
|
327
|
+
result.text = text;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (node.children && node.children.length > 0) {
|
|
331
|
+
result.children = [];
|
|
332
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
333
|
+
const child = collectNodes(node.children[i], depth + 1, `${path}/${i}`);
|
|
334
|
+
if (child) result.children.push(child);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return result;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
342
|
+
const root = collectNodes(document.body, 0, 'root');
|
|
343
|
+
|
|
344
|
+
return { viewport, dom_tree: root };
|
|
345
|
+
}, options);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
ok: true,
|
|
349
|
+
profileId,
|
|
350
|
+
...snapshot,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Query elements
|
|
356
|
+
*/
|
|
357
|
+
export async function queryElements(profileId, selector, options = {}) {
|
|
358
|
+
if (!isBrowserRunning(profileId)) {
|
|
359
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const page = await getCurrentPage(profileId);
|
|
363
|
+
|
|
364
|
+
const elements = await page.evaluate((sel) => {
|
|
365
|
+
const nodes = document.querySelectorAll(sel);
|
|
366
|
+
return Array.from(nodes).map((el, idx) => {
|
|
367
|
+
const rect = el.getBoundingClientRect();
|
|
368
|
+
return {
|
|
369
|
+
index: idx,
|
|
370
|
+
tag: el.tagName.toLowerCase(),
|
|
371
|
+
id: el.id || null,
|
|
372
|
+
classes: el.className.split(' ').filter(Boolean),
|
|
373
|
+
text: el.textContent?.trim().slice(0, 100) || '',
|
|
374
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
375
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}, selector);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
profileId,
|
|
383
|
+
selector,
|
|
384
|
+
count: elements.length,
|
|
385
|
+
elements,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Execute JavaScript
|
|
391
|
+
*/
|
|
392
|
+
export async function evaluateJS(profileId, script) {
|
|
393
|
+
if (!isBrowserRunning(profileId)) {
|
|
394
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const page = await getCurrentPage(profileId);
|
|
398
|
+
const result = await page.evaluate(script);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
profileId,
|
|
403
|
+
result,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create new page
|
|
409
|
+
*/
|
|
410
|
+
export async function createNewPage(profileId, options = {}) {
|
|
411
|
+
const browser = getActiveBrowser(profileId);
|
|
412
|
+
if (!browser) {
|
|
413
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const context = browser.pwBrowser?.contexts()[0];
|
|
417
|
+
if (!context) {
|
|
418
|
+
throw new Error('No browser context available');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const page = await context.newPage();
|
|
422
|
+
if (options.url) {
|
|
423
|
+
await page.goto(options.url, { waitUntil: 'domcontentloaded' });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
browser.pages.push(page);
|
|
427
|
+
browser.currentPage = page;
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
profileId,
|
|
432
|
+
pageIndex: browser.pages.length - 1,
|
|
433
|
+
url: options.url || 'about:blank',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* List pages
|
|
439
|
+
*/
|
|
440
|
+
export async function listPages(profileId) {
|
|
441
|
+
const browser = getActiveBrowser(profileId);
|
|
442
|
+
if (!browser) {
|
|
443
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const context = browser.pwBrowser?.contexts()[0];
|
|
447
|
+
if (!context) {
|
|
448
|
+
throw new Error('No browser context available');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const pages = context.pages();
|
|
452
|
+
const pageInfos = await Promise.all(pages.map(async (page, idx) => ({
|
|
453
|
+
index: idx,
|
|
454
|
+
url: page.url(),
|
|
455
|
+
title: await page.title().catch(() => ''),
|
|
456
|
+
})));
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
profileId,
|
|
461
|
+
count: pageInfos.length,
|
|
462
|
+
pages: pageInfos,
|
|
463
|
+
currentPage: browser.currentPage?._guid || 0,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Switch page
|
|
469
|
+
*/
|
|
470
|
+
export async function switchPage(profileId, pageIndex) {
|
|
471
|
+
const browser = getActiveBrowser(profileId);
|
|
472
|
+
if (!browser) {
|
|
473
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const context = browser.pwBrowser?.contexts()[0];
|
|
477
|
+
if (!context) {
|
|
478
|
+
throw new Error('No browser context available');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const pages = context.pages();
|
|
482
|
+
if (pageIndex < 0 || pageIndex >= pages.length) {
|
|
483
|
+
throw new Error(`Invalid page index: ${pageIndex}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
browser.currentPage = pages[pageIndex];
|
|
487
|
+
await pages[pageIndex].bringToFront();
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
profileId,
|
|
492
|
+
pageIndex,
|
|
493
|
+
url: pages[pageIndex].url(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Close page
|
|
499
|
+
*/
|
|
500
|
+
export async function closePage(profileId, pageIndex) {
|
|
501
|
+
const browser = getActiveBrowser(profileId);
|
|
502
|
+
if (!browser) {
|
|
503
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const context = browser.pwBrowser?.contexts()[0];
|
|
507
|
+
if (!context) {
|
|
508
|
+
throw new Error('No browser context available');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const pages = context.pages();
|
|
512
|
+
const idx = pageIndex !== undefined ? pageIndex : pages.length - 1;
|
|
513
|
+
|
|
514
|
+
if (idx < 0 || idx >= pages.length) {
|
|
515
|
+
throw new Error(`Invalid page index: ${idx}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await pages[idx].close();
|
|
519
|
+
browser.pages = pages.filter((_, i) => i !== idx);
|
|
520
|
+
|
|
521
|
+
// Update current page if needed
|
|
522
|
+
if (browser.currentPage === pages[idx]) {
|
|
523
|
+
browser.currentPage = browser.pages[0] || null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
ok: true,
|
|
528
|
+
profileId,
|
|
529
|
+
closedIndex: idx,
|
|
530
|
+
remaining: browser.pages.length,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Mouse operations
|
|
536
|
+
*/
|
|
537
|
+
export async function mouseClick(profileId, x, y, options = {}) {
|
|
538
|
+
if (!isBrowserRunning(profileId)) {
|
|
539
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const page = await getCurrentPage(profileId);
|
|
543
|
+
await page.mouse.click(x, y, {
|
|
544
|
+
button: options.button || 'left',
|
|
545
|
+
clickCount: options.clickCount || 1,
|
|
546
|
+
delay: options.delay || 0,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return { ok: true, profileId, x, y, button: options.button || 'left' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export async function mouseWheel(profileId, deltaX, deltaY) {
|
|
553
|
+
if (!isBrowserRunning(profileId)) {
|
|
554
|
+
throw new Error(`No browser running for profile: ${profileId}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const page = await getCurrentPage(profileId);
|
|
558
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
559
|
+
|
|
560
|
+
return { ok: true, profileId, deltaX, deltaY };
|
|
561
|
+
}
|