@monostate/node-scraper 2.0.0 → 2.2.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/README.md +74 -0
- package/browser-session.js +685 -0
- package/computer-use-provider.js +168 -0
- package/index.d.ts +159 -0
- package/index.js +6 -0
- package/lightpanda-server.js +151 -0
- package/package.json +8 -1
- package/providers/local-provider.js +322 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalProvider — runs Xvfb + Chrome + xdotool on the local machine.
|
|
3
|
+
*
|
|
4
|
+
* Requires Linux with: xvfb, xdotool, chromium/google-chrome
|
|
5
|
+
* Optional: x11vnc, noVNC (for VNC streaming)
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createSession, LocalProvider } from '@monostate/node-scraper';
|
|
9
|
+
*
|
|
10
|
+
* const session = await createSession({
|
|
11
|
+
* mode: 'computer-use',
|
|
12
|
+
* provider: new LocalProvider({ screenWidth: 1280, screenHeight: 800 }),
|
|
13
|
+
* });
|
|
14
|
+
* await session.goto('https://example.com');
|
|
15
|
+
* await session.clickAt(640, 400);
|
|
16
|
+
* await session.close();
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ComputerUseProvider } from '../computer-use-provider.js';
|
|
20
|
+
import { spawn, execFile } from 'child_process';
|
|
21
|
+
import { createServer } from 'net';
|
|
22
|
+
|
|
23
|
+
function findFreePort() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const srv = createServer();
|
|
26
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
27
|
+
const port = srv.address().port;
|
|
28
|
+
srv.close(() => resolve(port));
|
|
29
|
+
});
|
|
30
|
+
srv.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function waitForProcess(proc, readyCheck, timeoutMs = 10000) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const timer = setTimeout(() => reject(new Error('Process startup timed out')), timeoutMs);
|
|
37
|
+
const check = setInterval(async () => {
|
|
38
|
+
try {
|
|
39
|
+
if (await readyCheck()) {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
clearInterval(check);
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
} catch { /* still starting */ }
|
|
45
|
+
}, 200);
|
|
46
|
+
proc.on('error', (err) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
clearInterval(check);
|
|
49
|
+
reject(err);
|
|
50
|
+
});
|
|
51
|
+
proc.on('exit', (code) => {
|
|
52
|
+
if (code !== 0 && code !== null) {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
clearInterval(check);
|
|
55
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function execAsync(cmd, args, env) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
execFile(cmd, args, { env, timeout: 10000 }, (err, stdout, stderr) => {
|
|
64
|
+
if (err) reject(err);
|
|
65
|
+
else resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class LocalProvider extends ComputerUseProvider {
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} [options]
|
|
73
|
+
* @param {number} [options.screenWidth=1280]
|
|
74
|
+
* @param {number} [options.screenHeight=800]
|
|
75
|
+
* @param {boolean} [options.enableVnc=false]
|
|
76
|
+
* @param {string} [options.chromePath] - Path to Chrome/Chromium binary (auto-detected)
|
|
77
|
+
* @param {string[]} [options.chromeArgs] - Additional Chrome launch args
|
|
78
|
+
*/
|
|
79
|
+
constructor(options = {}) {
|
|
80
|
+
super();
|
|
81
|
+
this.screenWidth = options.screenWidth || 1280;
|
|
82
|
+
this.screenHeight = options.screenHeight || 800;
|
|
83
|
+
this.enableVnc = options.enableVnc ?? false;
|
|
84
|
+
this.chromePath = options.chromePath || null;
|
|
85
|
+
this.chromeArgs = options.chromeArgs || [];
|
|
86
|
+
|
|
87
|
+
this._display = null;
|
|
88
|
+
this._displayNum = null;
|
|
89
|
+
this._cdpPort = null;
|
|
90
|
+
this._vncPort = null;
|
|
91
|
+
this._xvfbProc = null;
|
|
92
|
+
this._chromeProc = null;
|
|
93
|
+
this._vncProc = null;
|
|
94
|
+
this._env = null;
|
|
95
|
+
this._cdpWsUrl = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async start() {
|
|
99
|
+
if (process.platform !== 'linux') {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'LocalProvider requires Linux (Xvfb + xdotool). ' +
|
|
102
|
+
'On macOS/Windows, use Docker or a remote provider.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find free ports
|
|
107
|
+
this._cdpPort = await findFreePort();
|
|
108
|
+
this._displayNum = 10 + Math.floor(Math.random() * 90); // :10-:99
|
|
109
|
+
this._display = `:${this._displayNum}`;
|
|
110
|
+
|
|
111
|
+
this._env = {
|
|
112
|
+
...process.env,
|
|
113
|
+
DISPLAY: this._display,
|
|
114
|
+
DBUS_SESSION_BUS_ADDRESS: '/dev/null',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// 1. Start Xvfb
|
|
118
|
+
this._xvfbProc = spawn('Xvfb', [
|
|
119
|
+
this._display,
|
|
120
|
+
'-screen', '0', `${this.screenWidth}x${this.screenHeight}x24`,
|
|
121
|
+
'-ac',
|
|
122
|
+
], { stdio: 'pipe', env: this._env });
|
|
123
|
+
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
125
|
+
|
|
126
|
+
// 2. Find Chrome binary
|
|
127
|
+
const chromePath = this.chromePath || await this._findChrome();
|
|
128
|
+
|
|
129
|
+
// 3. Start Chrome
|
|
130
|
+
const chromeArgs = [
|
|
131
|
+
'--no-sandbox',
|
|
132
|
+
'--disable-setuid-sandbox',
|
|
133
|
+
'--disable-dev-shm-usage',
|
|
134
|
+
'--disable-gpu',
|
|
135
|
+
`--remote-debugging-port=${this._cdpPort}`,
|
|
136
|
+
'--remote-debugging-address=127.0.0.1',
|
|
137
|
+
`--window-size=${this.screenWidth},${this.screenHeight}`,
|
|
138
|
+
'--disable-features=dbus',
|
|
139
|
+
'--disable-sync',
|
|
140
|
+
'--disable-extensions',
|
|
141
|
+
'--disable-component-update',
|
|
142
|
+
'--no-first-run',
|
|
143
|
+
'--user-data-dir=/tmp/chrome-local-provider-' + this._displayNum,
|
|
144
|
+
...this.chromeArgs,
|
|
145
|
+
'about:blank',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
this._chromeProc = spawn(chromePath, chromeArgs, {
|
|
149
|
+
stdio: 'pipe',
|
|
150
|
+
env: this._env,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait for CDP to be ready
|
|
154
|
+
await waitForProcess(this._chromeProc, async () => {
|
|
155
|
+
const res = await fetch(`http://127.0.0.1:${this._cdpPort}/json/version`);
|
|
156
|
+
return res.ok;
|
|
157
|
+
}, 15000);
|
|
158
|
+
|
|
159
|
+
// Get the CDP WebSocket URL
|
|
160
|
+
const versionRes = await fetch(`http://127.0.0.1:${this._cdpPort}/json/version`);
|
|
161
|
+
const versionData = await versionRes.json();
|
|
162
|
+
this._cdpWsUrl = versionData.webSocketDebuggerUrl;
|
|
163
|
+
|
|
164
|
+
// 4. Optionally start VNC
|
|
165
|
+
let vncUrl = null;
|
|
166
|
+
if (this.enableVnc) {
|
|
167
|
+
try {
|
|
168
|
+
this._vncPort = await findFreePort();
|
|
169
|
+
this._vncProc = spawn('x11vnc', [
|
|
170
|
+
'-display', this._display,
|
|
171
|
+
'-rfbport', String(this._vncPort),
|
|
172
|
+
'-shared', '-nopw', '-forever',
|
|
173
|
+
], { stdio: 'pipe', env: this._env });
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
175
|
+
vncUrl = `vnc://127.0.0.1:${this._vncPort}`;
|
|
176
|
+
} catch {
|
|
177
|
+
// VNC is optional — don't fail if x11vnc is not installed
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
cdpUrl: this._cdpWsUrl,
|
|
183
|
+
vncUrl,
|
|
184
|
+
screenSize: { width: this.screenWidth, height: this.screenHeight },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async stop() {
|
|
189
|
+
for (const proc of [this._vncProc, this._chromeProc, this._xvfbProc]) {
|
|
190
|
+
if (proc && !proc.killed) {
|
|
191
|
+
try { proc.kill('SIGTERM'); } catch { /* ignore */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this._vncProc = null;
|
|
195
|
+
this._chromeProc = null;
|
|
196
|
+
this._xvfbProc = null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async screenshot() {
|
|
200
|
+
// Use CDP Page.captureScreenshot
|
|
201
|
+
const pagesRes = await fetch(`http://127.0.0.1:${this._cdpPort}/json`);
|
|
202
|
+
const pages = await pagesRes.json();
|
|
203
|
+
const page = pages[0];
|
|
204
|
+
if (!page) throw new Error('No open pages for screenshot');
|
|
205
|
+
|
|
206
|
+
const wsUrl = page.webSocketDebuggerUrl;
|
|
207
|
+
const { default: WebSocket } = await import('ws');
|
|
208
|
+
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const ws = new WebSocket(wsUrl);
|
|
211
|
+
ws.on('open', () => {
|
|
212
|
+
ws.send(JSON.stringify({ id: 1, method: 'Page.captureScreenshot', params: { format: 'png' } }));
|
|
213
|
+
});
|
|
214
|
+
ws.on('message', (data) => {
|
|
215
|
+
const msg = JSON.parse(data.toString());
|
|
216
|
+
if (msg.id === 1) {
|
|
217
|
+
ws.close();
|
|
218
|
+
if (msg.error) {
|
|
219
|
+
reject(new Error(msg.error.message));
|
|
220
|
+
} else {
|
|
221
|
+
resolve({ screenshot: `data:image/png;base64,${msg.result.data}` });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
ws.on('error', reject);
|
|
226
|
+
setTimeout(() => { ws.close(); reject(new Error('Screenshot timed out')); }, 10000);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Coordinate-based actions via xdotool ──────────────────
|
|
231
|
+
|
|
232
|
+
async mouseMove(x, y) {
|
|
233
|
+
await this._xdotool(['mousemove', '--', String(Math.round(x)), String(Math.round(y))]);
|
|
234
|
+
return { success: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async mouseClick(x, y, button = 'left') {
|
|
238
|
+
const btn = { left: '1', right: '3', middle: '2' }[button] || '1';
|
|
239
|
+
await this._xdotool(['mousemove', '--', String(Math.round(x)), String(Math.round(y))]);
|
|
240
|
+
await this._xdotool(['click', btn]);
|
|
241
|
+
return { success: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async mouseDoubleClick(x, y, button = 'left') {
|
|
245
|
+
const btn = { left: '1', right: '3', middle: '2' }[button] || '1';
|
|
246
|
+
await this._xdotool(['mousemove', '--', String(Math.round(x)), String(Math.round(y))]);
|
|
247
|
+
await this._xdotool(['click', '--repeat', '2', '--delay', '50', btn]);
|
|
248
|
+
return { success: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async mouseDrag(startX, startY, endX, endY) {
|
|
252
|
+
await this._xdotool([
|
|
253
|
+
'mousemove', '--', String(Math.round(startX)), String(Math.round(startY)),
|
|
254
|
+
]);
|
|
255
|
+
await this._xdotool(['mousedown', '1']);
|
|
256
|
+
await this._xdotool([
|
|
257
|
+
'mousemove', '--', String(Math.round(endX)), String(Math.round(endY)),
|
|
258
|
+
]);
|
|
259
|
+
await this._xdotool(['mouseup', '1']);
|
|
260
|
+
return { success: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async scroll(x, y, direction, amount = 3) {
|
|
264
|
+
await this._xdotool(['mousemove', '--', String(Math.round(x)), String(Math.round(y))]);
|
|
265
|
+
const btn = direction === 'up' ? '4' : '5';
|
|
266
|
+
await this._xdotool(['click', '--repeat', String(amount), '--delay', '50', btn]);
|
|
267
|
+
return { success: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async pressKey(key) {
|
|
271
|
+
await this._xdotool(['key', key]);
|
|
272
|
+
return { success: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async typeText(text) {
|
|
276
|
+
// Type in chunks to avoid buffer issues
|
|
277
|
+
const CHUNK = 50;
|
|
278
|
+
for (let i = 0; i < text.length; i += CHUNK) {
|
|
279
|
+
const chunk = text.substring(i, i + CHUNK);
|
|
280
|
+
await this._xdotool(['type', '--delay', '12', '--clearmodifiers', chunk]);
|
|
281
|
+
}
|
|
282
|
+
return { success: true };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async getCursorPosition() {
|
|
286
|
+
const { stdout } = await this._xdotool(['getmouselocation', '--shell']);
|
|
287
|
+
const x = parseInt(stdout.match(/X=(\d+)/)?.[1] || '0', 10);
|
|
288
|
+
const y = parseInt(stdout.match(/Y=(\d+)/)?.[1] || '0', 10);
|
|
289
|
+
return { x, y };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async getScreenSize() {
|
|
293
|
+
return { width: this.screenWidth, height: this.screenHeight };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Internal helpers ──────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
async _xdotool(args) {
|
|
299
|
+
return execAsync('xdotool', args, this._env);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async _findChrome() {
|
|
303
|
+
const candidates = [
|
|
304
|
+
'chromium-browser',
|
|
305
|
+
'chromium',
|
|
306
|
+
'google-chrome',
|
|
307
|
+
'google-chrome-stable',
|
|
308
|
+
];
|
|
309
|
+
for (const name of candidates) {
|
|
310
|
+
try {
|
|
311
|
+
const { stdout } = await execAsync('which', [name], this._env);
|
|
312
|
+
if (stdout) return stdout;
|
|
313
|
+
} catch { /* not found */ }
|
|
314
|
+
}
|
|
315
|
+
throw new Error(
|
|
316
|
+
'Chrome/Chromium not found. Install with: apt-get install chromium ' +
|
|
317
|
+
'or pass chromePath option.'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default LocalProvider;
|