farming-code 2.2.28 → 2.2.30

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.
Files changed (83) hide show
  1. package/README.md +27 -90
  2. package/README.zh_cn.md +17 -90
  3. package/THIRD_PARTY_NOTICES.md +25 -6
  4. package/backend/acp/packaged-claude-acp.js +13 -0
  5. package/backend/acp/packaged-codex-acp.js +2 -2
  6. package/backend/acp-runtime.js +38 -17
  7. package/backend/acp-session-state.js +78 -88
  8. package/backend/acp-transcript.js +174 -9
  9. package/backend/agent-manager.js +27 -2
  10. package/backend/agent-session-history.js +45 -12
  11. package/backend/codex-session-history.js +1 -1
  12. package/backend/codex-transcript.js +0 -202
  13. package/backend/config-manager.js +44 -16
  14. package/backend/data/LICENSE.agent-browser +201 -0
  15. package/backend/data/runtime-dependency-manifest.json +199 -0
  16. package/backend/data/runtime-dependency-sources.json +5 -0
  17. package/backend/executable-discovery.js +7 -0
  18. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  19. package/backend/farming-app-cli.js +161 -65
  20. package/backend/farming-cli.js +7 -59
  21. package/backend/index-html.js +31 -0
  22. package/backend/main-agent-skills.js +7 -28
  23. package/backend/npm-update-helper.js +43 -6
  24. package/backend/provider-adapters.js +10 -0
  25. package/backend/runtime-dependency-manager.js +759 -0
  26. package/backend/runtime-executable-invocation.js +20 -0
  27. package/backend/server-process-identity.js +47 -0
  28. package/backend/server.js +145 -204
  29. package/backend/slash-command-discovery.js +138 -1
  30. package/backend/storage-layout.js +30 -0
  31. package/backend/update-service.js +60 -914
  32. package/config/farming.deploy.env.example +1 -1
  33. package/config/farming.install.env.example +5 -1
  34. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  35. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  36. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  37. package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
  38. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  39. package/dist/assets/App-Z1N7T0eM.js +695 -0
  40. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  41. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  42. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  43. package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
  44. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  45. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  46. package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
  47. package/dist/assets/main-DUTz-Rd2.css +1 -0
  48. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  49. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  50. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  51. package/dist/index.html +33 -3
  52. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  53. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  54. package/extensions/browser/backend/browser-resource-manager.js +921 -114
  55. package/extensions/browser/backend/browser-resource-store.js +47 -6
  56. package/extensions/browser/backend/browser-router.js +15 -2
  57. package/extensions/browser/backend/executable-discovery.js +164 -17
  58. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  59. package/extensions/browser/bin/farming-browser +384 -30
  60. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  61. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  62. package/extensions/browser/frontend/browser.css +68 -17
  63. package/extensions/browser/frontend/types.ts +13 -0
  64. package/extensions/browser/frontend/useBrowserResources.ts +18 -3
  65. package/index.html +32 -2
  66. package/package.json +10 -6
  67. package/shared/backend-connection-status.d.ts +12 -0
  68. package/shared/backend-connection-status.js +35 -4
  69. package/backend/agent-activity.js +0 -35
  70. package/backend/agent-memory-report.js +0 -583
  71. package/backend/bundle-update-helper.js +0 -113
  72. package/backend/codex-rollout-follower.js +0 -326
  73. package/dist/assets/App-CPG4WcjO.js +0 -658
  74. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  75. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  76. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  77. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  78. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  79. package/dist/assets/main-BlVUXfxa.css +0 -1
  80. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  81. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  82. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  83. package/extensions/browser/backend/cdp-client.js +0 -167
@@ -1,648 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { spawn } = require('child_process');
4
- const { EventEmitter } = require('events');
5
- const { CdpClient } = require('./cdp-client');
6
-
7
- const DEFAULT_VIEWPORT = Object.freeze({ width: 1280, height: 800 });
8
- const MAX_VIEWPORT = Object.freeze({ width: 4096, height: 4096 });
9
- // A Viewer may itself be captured by another Browser Resource, including the
10
- // Browser it displays. Bound that video-feedback loop while retaining its last frame.
11
- const VIEWER_FRAME_INTERVAL_MS = 1_000 / 15;
12
- const ACTIONABLE_ROLES = new Set([
13
- 'button',
14
- 'checkbox',
15
- 'combobox',
16
- 'link',
17
- 'listbox',
18
- 'menuitem',
19
- 'option',
20
- 'radio',
21
- 'searchbox',
22
- 'slider',
23
- 'spinbutton',
24
- 'switch',
25
- 'tab',
26
- 'textbox',
27
- 'treeitem',
28
- ]);
29
-
30
- function delay(ms) {
31
- return new Promise(resolve => setTimeout(resolve, ms));
32
- }
33
-
34
- function clampViewport(value = {}) {
35
- const width = Math.round(Number(value.width));
36
- const height = Math.round(Number(value.height));
37
- return {
38
- width: Number.isFinite(width) ? Math.min(MAX_VIEWPORT.width, Math.max(320, width)) : DEFAULT_VIEWPORT.width,
39
- height: Number.isFinite(height) ? Math.min(MAX_VIEWPORT.height, Math.max(240, height)) : DEFAULT_VIEWPORT.height,
40
- };
41
- }
42
-
43
- function processExitPromise(child) {
44
- if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
45
- return new Promise(resolve => child.once('exit', resolve));
46
- }
47
-
48
- async function waitForExit(child, timeoutMs) {
49
- let timer;
50
- const timedOut = new Promise(resolve => {
51
- timer = setTimeout(() => resolve(false), timeoutMs);
52
- timer.unref?.();
53
- });
54
- const exited = processExitPromise(child).then(() => true);
55
- const result = await Promise.race([exited, timedOut]);
56
- clearTimeout(timer);
57
- return result;
58
- }
59
-
60
- function signalBrowserProcess(child, signal) {
61
- if (!child || child.exitCode !== null || child.signalCode !== null) return;
62
- try {
63
- if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
64
- else child.kill(signal);
65
- } catch (error) {
66
- if (error?.code !== 'ESRCH') throw error;
67
- }
68
- }
69
-
70
- async function stopBrowserProcess(child) {
71
- if (!child || child.exitCode !== null || child.signalCode !== null) return;
72
- signalBrowserProcess(child, 'SIGTERM');
73
- if (await waitForExit(child, 3_000)) return;
74
- signalBrowserProcess(child, 'SIGKILL');
75
- if (!await waitForExit(child, 3_000)) {
76
- throw new Error(`System browser process ${child.pid || '(unknown)'} did not exit`);
77
- }
78
- }
79
-
80
- async function waitForDevToolsPort(profileDir, child, timeoutMs = 15_000) {
81
- const portFile = path.join(profileDir, 'DevToolsActivePort');
82
- const deadline = Date.now() + timeoutMs;
83
- while (Date.now() < deadline) {
84
- if (child.farmingStartError) throw child.farmingStartError;
85
- if (child.exitCode !== null || child.signalCode !== null) {
86
- const detail = child.farmingStderr ? `: ${child.farmingStderr}` : '';
87
- throw new Error(`System browser exited during startup (${child.exitCode ?? child.signalCode})${detail}`);
88
- }
89
- try {
90
- const lines = fs.readFileSync(portFile, 'utf8').trim().split(/\r?\n/);
91
- const port = Number(lines[0]);
92
- if (Number.isInteger(port) && port > 0 && lines[1]?.startsWith('/')) {
93
- return `ws://127.0.0.1:${port}${lines[1]}`;
94
- }
95
- } catch (error) {
96
- if (error?.code !== 'ENOENT') throw error;
97
- }
98
- await delay(50);
99
- }
100
- const detail = child.farmingStderr ? `: ${child.farmingStderr}` : '';
101
- throw new Error(`System browser did not expose a CDP endpoint before startup timed out${detail}`);
102
- }
103
-
104
- function launchSystemBrowser(executablePath, profileDir, options = {}) {
105
- fs.mkdirSync(profileDir, { recursive: true });
106
- fs.rmSync(path.join(profileDir, 'DevToolsActivePort'), { force: true });
107
- const platform = options.platform || process.platform;
108
- const noSandbox = options.noSandbox === true
109
- || (options.noSandbox === undefined && process.env.FARMING_BROWSER_NO_SANDBOX === '1');
110
- const child = (options.spawn || spawn)(executablePath, [
111
- '--headless=new',
112
- '--remote-debugging-port=0',
113
- `--user-data-dir=${profileDir}`,
114
- `--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`,
115
- ...(platform === 'linux' ? ['--disable-dev-shm-usage'] : []),
116
- ...(noSandbox ? ['--no-sandbox', '--disable-setuid-sandbox'] : []),
117
- '--no-first-run',
118
- '--no-default-browser-check',
119
- '--disable-sync',
120
- '--disable-background-networking',
121
- '--disable-component-update',
122
- '--disable-features=Translate,MediaRouter',
123
- '--disable-popup-blocking',
124
- 'about:blank',
125
- ], {
126
- detached: process.platform !== 'win32',
127
- stdio: ['ignore', 'ignore', 'pipe'],
128
- });
129
- return child;
130
- }
131
-
132
- function axValue(node, field) {
133
- const value = node?.[field]?.value;
134
- return value === undefined || value === null ? '' : String(value);
135
- }
136
-
137
- function compactAxTree(nodes) {
138
- const lines = [];
139
- for (const node of nodes.slice(0, 500)) {
140
- const role = axValue(node, 'role');
141
- const name = axValue(node, 'name').replace(/\s+/g, ' ').trim();
142
- if ((!role || role === 'generic' || role === 'none') && !name) continue;
143
- const value = axValue(node, 'value').replace(/\s+/g, ' ').trim();
144
- lines.push(`${role || 'node'}${name ? ` "${name.slice(0, 200)}"` : ''}${value ? ` value="${value.slice(0, 200)}"` : ''}`);
145
- }
146
- return lines.join('\n');
147
- }
148
-
149
- function keyDescription(input) {
150
- const key = String(input.key || '');
151
- const code = String(input.code || '');
152
- const known = {
153
- Enter: { code: 'Enter', windowsVirtualKeyCode: 13 },
154
- Tab: { code: 'Tab', windowsVirtualKeyCode: 9 },
155
- Escape: { code: 'Escape', windowsVirtualKeyCode: 27 },
156
- Backspace: { code: 'Backspace', windowsVirtualKeyCode: 8 },
157
- Delete: { code: 'Delete', windowsVirtualKeyCode: 46 },
158
- ArrowLeft: { code: 'ArrowLeft', windowsVirtualKeyCode: 37 },
159
- ArrowUp: { code: 'ArrowUp', windowsVirtualKeyCode: 38 },
160
- ArrowRight: { code: 'ArrowRight', windowsVirtualKeyCode: 39 },
161
- ArrowDown: { code: 'ArrowDown', windowsVirtualKeyCode: 40 },
162
- Home: { code: 'Home', windowsVirtualKeyCode: 36 },
163
- End: { code: 'End', windowsVirtualKeyCode: 35 },
164
- PageUp: { code: 'PageUp', windowsVirtualKeyCode: 33 },
165
- PageDown: { code: 'PageDown', windowsVirtualKeyCode: 34 },
166
- ' ': { code: 'Space', windowsVirtualKeyCode: 32 },
167
- }[key];
168
- const upper = key.length === 1 ? key.toUpperCase() : '';
169
- return {
170
- key,
171
- code: code || known?.code || (upper ? `Key${upper}` : key),
172
- windowsVirtualKeyCode: known?.windowsVirtualKeyCode || (upper ? upper.charCodeAt(0) : 0),
173
- nativeVirtualKeyCode: known?.windowsVirtualKeyCode || (upper ? upper.charCodeAt(0) : 0),
174
- modifiers: Number(input.modifiers) || 0,
175
- };
176
- }
177
-
178
- class CdpBrowserRuntime extends EventEmitter {
179
- constructor(options) {
180
- super();
181
- this.id = options.id;
182
- this.generation = options.generation;
183
- this.executablePath = options.executablePath;
184
- this.profileDir = options.profileDir;
185
- this.launchBrowser = options.launchBrowser || launchSystemBrowser;
186
- this.createClient = options.createClient || (() => new CdpClient());
187
- this.child = null;
188
- this.client = null;
189
- this.sessionId = null;
190
- this.targetId = null;
191
- this.sessionListeners = [];
192
- this.globalListeners = [];
193
- this.refs = new Map();
194
- this.viewport = { ...DEFAULT_VIEWPORT };
195
- this.latestFrame = null;
196
- this.closedByOwner = false;
197
- this.closeComplete = false;
198
- this.closePromise = null;
199
- this.started = false;
200
- this.targetChange = Promise.resolve();
201
- this.screencastFrameTimer = null;
202
- this.screencastFrameEpoch = 0;
203
- this.pendingScreencastFrame = null;
204
- this.lastScreencastFrameAt = null;
205
- this.now = options.now || Date.now;
206
- this.scheduleTimeout = options.scheduleTimeout || setTimeout;
207
- this.cancelTimeout = options.cancelTimeout || clearTimeout;
208
- }
209
-
210
- async start(initialUrl) {
211
- this.child = this.launchBrowser(this.executablePath, this.profileDir);
212
- this.child.stderr?.on('data', chunk => {
213
- const combined = `${this.child.farmingStderr || ''}${chunk.toString('utf8')}`;
214
- this.child.farmingStderr = combined.slice(-8_192).trim();
215
- });
216
- this.child.once('error', error => {
217
- this.child.farmingStartError = error;
218
- if (this.started && !this.closedByOwner) this.emit('exit', `System browser failed: ${error.message}`);
219
- });
220
- this.child.once('exit', (code, signal) => {
221
- if (this.closedByOwner) return;
222
- this.emit('exit', `System browser exited (${code ?? signal ?? 'unknown'})`);
223
- });
224
- try {
225
- const wsUrl = await waitForDevToolsPort(this.profileDir, this.child);
226
- this.client = this.createClient();
227
- await this.client.connect(wsUrl);
228
- this.globalListeners.push(this.client.onClose(error => {
229
- if (this.closedByOwner) return;
230
- this.emit('exit', error?.message || 'CDP connection closed');
231
- }));
232
- this.globalListeners.push(this.client.on('Target.targetCreated', event => {
233
- const target = event.targetInfo;
234
- if (!this.started || target?.type !== 'page' || target.targetId === this.targetId) return;
235
- this.targetChange = this.targetChange
236
- .catch(() => {})
237
- .then(() => this.setActiveTarget(target.targetId))
238
- .catch(error => this.emit('error', error));
239
- }));
240
- this.globalListeners.push(this.client.on('Target.targetDestroyed', event => {
241
- if (!this.started || event.targetId !== this.targetId || this.closedByOwner) return;
242
- this.emit('exit', 'Browser page closed');
243
- }));
244
- await this.client.send('Target.setDiscoverTargets', { discover: true });
245
- const { targetInfos = [] } = await this.client.send('Target.getTargets');
246
- const target = targetInfos.find(candidate => candidate.type === 'page');
247
- if (!target) throw new Error('System browser did not create a page target');
248
- await this.setActiveTarget(target.targetId);
249
- this.started = true;
250
- if (initialUrl && initialUrl !== 'about:blank') await this.navigate(initialUrl);
251
- return await this.metadata();
252
- } catch (error) {
253
- await this.close();
254
- throw error;
255
- }
256
- }
257
-
258
- async setActiveTarget(targetId) {
259
- if (!this.client || targetId === this.targetId) return;
260
- await this.detachActiveTarget();
261
- const { sessionId } = await this.client.send('Target.attachToTarget', {
262
- targetId,
263
- flatten: true,
264
- });
265
- this.sessionId = sessionId;
266
- this.targetId = targetId;
267
- this.refs.clear();
268
- const session = sessionId;
269
- this.sessionListeners.push(
270
- this.client.on('Page.screencastFrame', event => {
271
- void this.client?.send('Page.screencastFrameAck', { sessionId: event.sessionId }, session).catch(() => {});
272
- const frame = {
273
- type: 'browser-frame',
274
- generation: this.generation,
275
- data: event.data,
276
- metadata: event.metadata,
277
- };
278
- this.latestFrame = frame;
279
- this.publishScreencastFrame(frame, session);
280
- }, session),
281
- this.client.on('Page.frameNavigated', event => {
282
- if (event.frame?.parentId) return;
283
- void this.publishMetadata();
284
- }, session),
285
- this.client.on('Page.loadEventFired', () => {
286
- void this.publishMetadata();
287
- }, session),
288
- );
289
- await Promise.all([
290
- this.client.send('Page.enable', {}, session),
291
- this.client.send('Runtime.enable', {}, session),
292
- this.client.send('DOM.enable', {}, session),
293
- this.client.send('Accessibility.enable', {}, session),
294
- ]);
295
- await this.resize(this.viewport);
296
- await this.client.send('Page.startScreencast', {
297
- format: 'jpeg',
298
- quality: 80,
299
- maxWidth: MAX_VIEWPORT.width,
300
- maxHeight: MAX_VIEWPORT.height,
301
- everyNthFrame: 1,
302
- }, session);
303
- await this.captureViewerFrame();
304
- await this.publishMetadata();
305
- }
306
-
307
- publishScreencastFrame(frame, targetSession) {
308
- const waitMs = this.lastScreencastFrameAt === null
309
- ? 0
310
- : Math.ceil(Math.max(0, VIEWER_FRAME_INTERVAL_MS - (this.now() - this.lastScreencastFrameAt)));
311
- if (waitMs > 0) {
312
- this.pendingScreencastFrame = frame;
313
- if (this.screencastFrameTimer) return;
314
- const epoch = this.screencastFrameEpoch;
315
- this.screencastFrameTimer = this.scheduleTimeout(() => {
316
- this.screencastFrameTimer = null;
317
- if (epoch !== this.screencastFrameEpoch || targetSession !== this.sessionId) return;
318
- const pending = this.pendingScreencastFrame;
319
- this.pendingScreencastFrame = null;
320
- if (!pending) return;
321
- this.lastScreencastFrameAt = this.now();
322
- this.emit('frame', pending);
323
- }, waitMs);
324
- return;
325
- }
326
- this.lastScreencastFrameAt = this.now();
327
- this.emit('frame', frame);
328
- }
329
-
330
- async detachActiveTarget() {
331
- const session = this.sessionId;
332
- this.sessionId = null;
333
- this.targetId = null;
334
- if (this.screencastFrameTimer) this.cancelTimeout(this.screencastFrameTimer);
335
- this.screencastFrameTimer = null;
336
- this.screencastFrameEpoch += 1;
337
- this.pendingScreencastFrame = null;
338
- this.lastScreencastFrameAt = null;
339
- for (const off of this.sessionListeners.splice(0)) off();
340
- if (!this.client || !session) return;
341
- await this.client.send('Page.stopScreencast', {}, session).catch(() => {});
342
- await this.client.send('Target.detachFromTarget', { sessionId: session }).catch(() => {});
343
- }
344
-
345
- requireSession() {
346
- if (!this.client || !this.sessionId) throw new Error('Browser page is not attached');
347
- return { client: this.client, sessionId: this.sessionId };
348
- }
349
-
350
- async evaluate(expression, options = {}) {
351
- const { client, sessionId } = this.requireSession();
352
- const result = await client.send('Runtime.evaluate', {
353
- expression,
354
- awaitPromise: true,
355
- returnByValue: options.returnByValue !== false,
356
- userGesture: true,
357
- }, sessionId);
358
- if (result.exceptionDetails) {
359
- throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Browser evaluation failed');
360
- }
361
- return result.result;
362
- }
363
-
364
- async metadata() {
365
- const result = await this.evaluate(`({
366
- url: location.href,
367
- title: document.title
368
- })`);
369
- return {
370
- url: String(result.value?.url || 'about:blank'),
371
- title: String(result.value?.title || ''),
372
- };
373
- }
374
-
375
- async publishMetadata() {
376
- try {
377
- this.emit('metadata', await this.metadata());
378
- } catch {
379
- // Navigation may briefly replace the execution context.
380
- }
381
- }
382
-
383
- async navigate(url) {
384
- const { client, sessionId } = this.requireSession();
385
- const result = await client.send('Page.navigate', { url }, sessionId);
386
- if (result.errorText) throw new Error(result.errorText);
387
- await delay(50);
388
- const metadata = await this.metadata().catch(() => ({ url, title: '' }));
389
- await this.captureViewerFrame();
390
- this.emit('metadata', metadata);
391
- return metadata;
392
- }
393
-
394
- async goBack() {
395
- return this.navigateHistory(-1);
396
- }
397
-
398
- async goForward() {
399
- return this.navigateHistory(1);
400
- }
401
-
402
- async navigateHistory(delta) {
403
- const { client, sessionId } = this.requireSession();
404
- const history = await client.send('Page.getNavigationHistory', {}, sessionId);
405
- const target = history.entries?.[history.currentIndex + delta];
406
- if (!target) return this.metadata();
407
- await client.send('Page.navigateToHistoryEntry', { entryId: target.id }, sessionId);
408
- await delay(50);
409
- const metadata = await this.metadata().catch(() => ({ url: target.url, title: target.title || '' }));
410
- await this.captureViewerFrame();
411
- this.emit('metadata', metadata);
412
- return metadata;
413
- }
414
-
415
- async reload() {
416
- const { client, sessionId } = this.requireSession();
417
- await client.send('Page.reload', { ignoreCache: false }, sessionId);
418
- await delay(50);
419
- await this.captureViewerFrame();
420
- return this.metadata();
421
- }
422
-
423
- async resize(value) {
424
- this.viewport = clampViewport(value);
425
- if (!this.client || !this.sessionId) return;
426
- await this.client.send('Emulation.setDeviceMetricsOverride', {
427
- ...this.viewport,
428
- deviceScaleFactor: 1,
429
- mobile: false,
430
- screenWidth: this.viewport.width,
431
- screenHeight: this.viewport.height,
432
- }, this.sessionId);
433
- }
434
-
435
- async screenshot() {
436
- const { client, sessionId } = this.requireSession();
437
- const result = await client.send('Page.captureScreenshot', {
438
- format: 'png',
439
- fromSurface: true,
440
- }, sessionId);
441
- return { mimeType: 'image/png', data: result.data };
442
- }
443
-
444
- async captureViewerFrame() {
445
- const { client, sessionId } = this.requireSession();
446
- const result = await client.send('Page.captureScreenshot', {
447
- format: 'jpeg',
448
- quality: 80,
449
- fromSurface: true,
450
- captureBeyondViewport: false,
451
- }, sessionId);
452
- if (sessionId !== this.sessionId || !result.data) return;
453
- const frame = {
454
- type: 'browser-frame',
455
- generation: this.generation,
456
- data: result.data,
457
- };
458
- this.latestFrame = frame;
459
- this.publishScreencastFrame(frame, sessionId);
460
- }
461
-
462
- async snapshot() {
463
- const { client, sessionId } = this.requireSession();
464
- const { nodes = [] } = await client.send('Accessibility.getFullAXTree', {}, sessionId);
465
- this.refs.clear();
466
- const elements = [];
467
- for (const node of nodes) {
468
- const role = axValue(node, 'role');
469
- if (!ACTIONABLE_ROLES.has(role) || !node.backendDOMNodeId || elements.length >= 200) continue;
470
- const ref = `e${elements.length + 1}`;
471
- this.refs.set(ref, node.backendDOMNodeId);
472
- elements.push({
473
- ref,
474
- role,
475
- name: axValue(node, 'name').replace(/\s+/g, ' ').trim().slice(0, 240),
476
- value: axValue(node, 'value').replace(/\s+/g, ' ').trim().slice(0, 240),
477
- disabled: Boolean(node.properties?.find(property => property.name === 'disabled')?.value?.value),
478
- });
479
- }
480
- const metadata = await this.metadata();
481
- return {
482
- ...metadata,
483
- elements,
484
- accessibilityTree: compactAxTree(nodes),
485
- };
486
- }
487
-
488
- async resolveElement(input) {
489
- const { client, sessionId } = this.requireSession();
490
- const ref = String(input.ref || '').trim();
491
- const selector = String(input.selector || '').trim();
492
- if (ref) {
493
- const backendNodeId = this.refs.get(ref);
494
- if (!backendNodeId) throw new Error(`Browser ref ${ref} is stale or unknown; take a new snapshot`);
495
- const { object } = await client.send('DOM.resolveNode', { backendNodeId }, sessionId);
496
- if (!object?.objectId) throw new Error(`Browser ref ${ref} no longer resolves to an element`);
497
- return object.objectId;
498
- }
499
- if (!selector) throw new Error('ref or selector is required');
500
- const result = await this.evaluate(`document.querySelector(${JSON.stringify(selector)})`, { returnByValue: false });
501
- if (!result.objectId || result.subtype === 'null') throw new Error(`No element matches selector: ${selector}`);
502
- return result.objectId;
503
- }
504
-
505
- async callOnElement(objectId, functionDeclaration, args = [], returnByValue = true) {
506
- const { client, sessionId } = this.requireSession();
507
- try {
508
- const result = await client.send('Runtime.callFunctionOn', {
509
- objectId,
510
- functionDeclaration,
511
- arguments: args.map(value => ({ value })),
512
- returnByValue,
513
- awaitPromise: true,
514
- userGesture: true,
515
- }, sessionId);
516
- if (result.exceptionDetails) {
517
- throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Element action failed');
518
- }
519
- return result.result;
520
- } finally {
521
- await client.send('Runtime.releaseObject', { objectId }, sessionId).catch(() => {});
522
- }
523
- }
524
-
525
- async focusElement(input, clear = false) {
526
- const objectId = await this.resolveElement(input);
527
- return this.callOnElement(objectId, `function(clear) {
528
- if (!(this instanceof Element)) throw new Error('Target is not an element');
529
- if (typeof this.scrollIntoView === 'function') this.scrollIntoView({ block: 'center', inline: 'center' });
530
- if (typeof this.focus === 'function') this.focus();
531
- if (clear) {
532
- if ('value' in this) {
533
- this.value = '';
534
- this.dispatchEvent(new Event('input', { bubbles: true }));
535
- } else if (this.isContentEditable) {
536
- this.textContent = '';
537
- this.dispatchEvent(new Event('input', { bubbles: true }));
538
- }
539
- }
540
- const rect = this.getBoundingClientRect();
541
- return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
542
- }`, [clear]);
543
- }
544
-
545
- async click(input) {
546
- const position = await this.focusElement(input);
547
- await this.pointer({ action: 'move', ...position.value });
548
- await this.pointer({ action: 'down', button: 'left', ...position.value });
549
- await this.pointer({ action: 'up', button: 'left', ...position.value });
550
- return { ok: true };
551
- }
552
-
553
- async type(input, clear) {
554
- await this.focusElement(input, clear);
555
- const { client, sessionId } = this.requireSession();
556
- await client.send('Input.insertText', { text: String(input.text ?? '') }, sessionId);
557
- await this.captureViewerFrame();
558
- return { ok: true };
559
- }
560
-
561
- async press(input) {
562
- const { client, sessionId } = this.requireSession();
563
- const description = keyDescription(input);
564
- if (!description.key) throw new Error('key is required');
565
- await client.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', ...description }, sessionId);
566
- await client.send('Input.dispatchKeyEvent', { type: 'keyUp', ...description }, sessionId);
567
- await this.captureViewerFrame();
568
- return { ok: true };
569
- }
570
-
571
- async pointer(input) {
572
- const { client, sessionId } = this.requireSession();
573
- const x = Number(input.x);
574
- const y = Number(input.y);
575
- if (!Number.isFinite(x) || !Number.isFinite(y)) return;
576
- const actionTypes = {
577
- move: 'mouseMoved',
578
- down: 'mousePressed',
579
- up: 'mouseReleased',
580
- };
581
- const type = actionTypes[input.action];
582
- if (!type) return;
583
- await client.send('Input.dispatchMouseEvent', {
584
- type,
585
- x,
586
- y,
587
- button: input.button || 'none',
588
- buttons: Number(input.buttons) || 0,
589
- clickCount: input.action === 'move' ? 0 : 1,
590
- modifiers: Number(input.modifiers) || 0,
591
- }, sessionId);
592
- if (input.action === 'up') await this.captureViewerFrame();
593
- }
594
-
595
- async wheel(input) {
596
- const { client, sessionId } = this.requireSession();
597
- const x = Number(input.x);
598
- const y = Number(input.y);
599
- await client.send('Input.dispatchMouseEvent', {
600
- type: 'mouseWheel',
601
- x: Number.isFinite(x) ? x : this.viewport.width / 2,
602
- y: Number.isFinite(y) ? y : this.viewport.height / 2,
603
- deltaX: Number(input.deltaX) || 0,
604
- deltaY: Number(input.deltaY) || 0,
605
- modifiers: Number(input.modifiers) || 0,
606
- }, sessionId);
607
- await this.captureViewerFrame();
608
- }
609
-
610
- async insertText(text) {
611
- const { client, sessionId } = this.requireSession();
612
- await client.send('Input.insertText', { text: String(text || '') }, sessionId);
613
- await this.captureViewerFrame();
614
- }
615
-
616
- async close() {
617
- if (this.closeComplete) return;
618
- if (this.closePromise) return this.closePromise;
619
- this.closedByOwner = true;
620
- this.closePromise = (async () => {
621
- for (const off of this.globalListeners.splice(0)) off();
622
- await this.detachActiveTarget();
623
- if (this.client) {
624
- await this.client.send('Browser.close').catch(() => {});
625
- this.client.close();
626
- this.client = null;
627
- }
628
- if (this.child && !await waitForExit(this.child, 3_000)) {
629
- await stopBrowserProcess(this.child);
630
- }
631
- this.child = null;
632
- this.closeComplete = true;
633
- })().finally(() => {
634
- this.closePromise = null;
635
- });
636
- return this.closePromise;
637
- }
638
- }
639
-
640
- module.exports = {
641
- CdpBrowserRuntime,
642
- DEFAULT_VIEWPORT,
643
- MAX_VIEWPORT,
644
- clampViewport,
645
- launchSystemBrowser,
646
- stopBrowserProcess,
647
- waitForDevToolsPort,
648
- };