@projectservan8n/cnapse 0.7.0 → 0.8.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/dist/ProviderSelector-MXRZFAOB.js +6 -0
- package/dist/chunk-OPX7FFL6.js +391 -0
- package/dist/index.js +733 -702
- package/package.json +17 -16
- package/src/agents/executor.ts +20 -13
- package/src/index.tsx +32 -6
- package/src/lib/tasks.ts +307 -323
- package/src/services/browser.ts +669 -0
- package/src/tools/index.ts +0 -1
- package/dist/ConfigUI-I2CJVODT.js +0 -305
- package/dist/Setup-KGYXCA7Y.js +0 -177
- package/dist/chunk-COKO6V5J.js +0 -50
- package/src/components/ConfigUI.tsx +0 -352
- package/src/components/Setup.tsx +0 -202
- package/src/lib/screen.ts +0 -118
- package/src/tools/vision.ts +0 -65
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Service - Playwright-based web automation
|
|
3
|
+
*
|
|
4
|
+
* Provides reliable browser automation for:
|
|
5
|
+
* - Web searches
|
|
6
|
+
* - AI chat interactions (Perplexity, ChatGPT, Claude, etc.)
|
|
7
|
+
* - Email (Gmail, Outlook)
|
|
8
|
+
* - Google Sheets/Docs
|
|
9
|
+
* - General web browsing
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chromium, Browser, Page, BrowserContext } from 'playwright';
|
|
13
|
+
|
|
14
|
+
// Singleton browser instance
|
|
15
|
+
let browser: Browser | null = null;
|
|
16
|
+
let context: BrowserContext | null = null;
|
|
17
|
+
let activePage: Page | null = null;
|
|
18
|
+
|
|
19
|
+
// Browser configuration
|
|
20
|
+
interface BrowserConfig {
|
|
21
|
+
headless: boolean;
|
|
22
|
+
slowMo: number;
|
|
23
|
+
viewport: { width: number; height: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const defaultConfig: BrowserConfig = {
|
|
27
|
+
headless: false, // Show browser so user can see what's happening
|
|
28
|
+
slowMo: 50, // Slight delay for visibility
|
|
29
|
+
viewport: { width: 1280, height: 800 }
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize browser if not already running
|
|
34
|
+
*/
|
|
35
|
+
export async function initBrowser(config: Partial<BrowserConfig> = {}): Promise<Page> {
|
|
36
|
+
const cfg = { ...defaultConfig, ...config };
|
|
37
|
+
|
|
38
|
+
if (!browser) {
|
|
39
|
+
browser = await chromium.launch({
|
|
40
|
+
headless: cfg.headless,
|
|
41
|
+
slowMo: cfg.slowMo,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!context) {
|
|
46
|
+
context = await browser.newContext({
|
|
47
|
+
viewport: cfg.viewport,
|
|
48
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!activePage) {
|
|
53
|
+
activePage = await context.newPage();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return activePage;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get current page or create one
|
|
61
|
+
*/
|
|
62
|
+
export async function getPage(): Promise<Page> {
|
|
63
|
+
if (!activePage) {
|
|
64
|
+
return initBrowser();
|
|
65
|
+
}
|
|
66
|
+
return activePage;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Close browser
|
|
71
|
+
*/
|
|
72
|
+
export async function closeBrowser(): Promise<void> {
|
|
73
|
+
if (browser) {
|
|
74
|
+
await browser.close();
|
|
75
|
+
browser = null;
|
|
76
|
+
context = null;
|
|
77
|
+
activePage = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Navigate to URL
|
|
83
|
+
*/
|
|
84
|
+
export async function navigateTo(url: string): Promise<void> {
|
|
85
|
+
const page = await getPage();
|
|
86
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Take screenshot and return as base64
|
|
91
|
+
*/
|
|
92
|
+
export async function takeScreenshot(): Promise<string> {
|
|
93
|
+
const page = await getPage();
|
|
94
|
+
const buffer = await page.screenshot({ type: 'png' });
|
|
95
|
+
return buffer.toString('base64');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Take screenshot of specific element
|
|
100
|
+
*/
|
|
101
|
+
export async function screenshotElement(selector: string): Promise<string | null> {
|
|
102
|
+
const page = await getPage();
|
|
103
|
+
try {
|
|
104
|
+
const element = await page.waitForSelector(selector, { timeout: 5000 });
|
|
105
|
+
if (element) {
|
|
106
|
+
const buffer = await element.screenshot({ type: 'png' });
|
|
107
|
+
return buffer.toString('base64');
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Wait for element and click
|
|
117
|
+
*/
|
|
118
|
+
export async function clickElement(selector: string, timeout = 10000): Promise<boolean> {
|
|
119
|
+
const page = await getPage();
|
|
120
|
+
try {
|
|
121
|
+
await page.click(selector, { timeout });
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Type text into element
|
|
130
|
+
*/
|
|
131
|
+
export async function typeInElement(selector: string, text: string, timeout = 10000): Promise<boolean> {
|
|
132
|
+
const page = await getPage();
|
|
133
|
+
try {
|
|
134
|
+
await page.fill(selector, text, { timeout });
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Type text character by character (for sites that need keypresses)
|
|
143
|
+
*/
|
|
144
|
+
export async function typeSlowly(selector: string, text: string, delay = 50): Promise<boolean> {
|
|
145
|
+
const page = await getPage();
|
|
146
|
+
try {
|
|
147
|
+
await page.click(selector);
|
|
148
|
+
await page.type(selector, text, { delay });
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Press keyboard key
|
|
157
|
+
*/
|
|
158
|
+
export async function pressKey(key: string): Promise<void> {
|
|
159
|
+
const page = await getPage();
|
|
160
|
+
await page.keyboard.press(key);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Scroll page
|
|
165
|
+
*/
|
|
166
|
+
export async function scroll(direction: 'up' | 'down', amount = 500): Promise<void> {
|
|
167
|
+
const page = await getPage();
|
|
168
|
+
await page.mouse.wheel(0, direction === 'down' ? amount : -amount);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Wait for text to appear on page
|
|
173
|
+
*/
|
|
174
|
+
export async function waitForText(text: string, timeout = 30000): Promise<boolean> {
|
|
175
|
+
const page = await getPage();
|
|
176
|
+
try {
|
|
177
|
+
await page.waitForFunction(
|
|
178
|
+
(searchText) => document.body.innerText.includes(searchText),
|
|
179
|
+
text,
|
|
180
|
+
{ timeout }
|
|
181
|
+
);
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get text content of element
|
|
190
|
+
*/
|
|
191
|
+
export async function getTextContent(selector: string): Promise<string | null> {
|
|
192
|
+
const page = await getPage();
|
|
193
|
+
try {
|
|
194
|
+
return await page.textContent(selector);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get all text from page
|
|
202
|
+
*/
|
|
203
|
+
export async function getPageText(): Promise<string> {
|
|
204
|
+
const page = await getPage();
|
|
205
|
+
return await page.evaluate(() => document.body.innerText);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Wait for navigation
|
|
210
|
+
*/
|
|
211
|
+
export async function waitForNavigation(timeout = 30000): Promise<void> {
|
|
212
|
+
const page = await getPage();
|
|
213
|
+
await page.waitForLoadState('domcontentloaded', { timeout });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if element exists
|
|
218
|
+
*/
|
|
219
|
+
export async function elementExists(selector: string): Promise<boolean> {
|
|
220
|
+
const page = await getPage();
|
|
221
|
+
try {
|
|
222
|
+
const element = await page.$(selector);
|
|
223
|
+
return element !== null;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ========================================
|
|
230
|
+
// AI Chat Site Helpers
|
|
231
|
+
// ========================================
|
|
232
|
+
|
|
233
|
+
interface AIChatConfig {
|
|
234
|
+
url: string;
|
|
235
|
+
inputSelector: string;
|
|
236
|
+
submitSelector?: string;
|
|
237
|
+
submitKey?: string;
|
|
238
|
+
responseSelector: string;
|
|
239
|
+
waitForResponse: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const aiChatConfigs: Record<string, AIChatConfig> = {
|
|
243
|
+
perplexity: {
|
|
244
|
+
url: 'https://www.perplexity.ai',
|
|
245
|
+
inputSelector: 'textarea[placeholder*="Ask"]',
|
|
246
|
+
submitKey: 'Enter',
|
|
247
|
+
responseSelector: '.prose, [class*="answer"], [class*="response"]',
|
|
248
|
+
waitForResponse: 15000
|
|
249
|
+
},
|
|
250
|
+
chatgpt: {
|
|
251
|
+
url: 'https://chat.openai.com',
|
|
252
|
+
inputSelector: 'textarea[id="prompt-textarea"], textarea[data-id="root"]',
|
|
253
|
+
submitSelector: 'button[data-testid="send-button"]',
|
|
254
|
+
responseSelector: '[data-message-author-role="assistant"]',
|
|
255
|
+
waitForResponse: 20000
|
|
256
|
+
},
|
|
257
|
+
claude: {
|
|
258
|
+
url: 'https://claude.ai',
|
|
259
|
+
inputSelector: '[contenteditable="true"], textarea',
|
|
260
|
+
submitKey: 'Enter',
|
|
261
|
+
responseSelector: '[data-testid="message-content"]',
|
|
262
|
+
waitForResponse: 20000
|
|
263
|
+
},
|
|
264
|
+
copilot: {
|
|
265
|
+
url: 'https://copilot.microsoft.com',
|
|
266
|
+
inputSelector: 'textarea, [contenteditable="true"]',
|
|
267
|
+
submitKey: 'Enter',
|
|
268
|
+
responseSelector: '[class*="response"], [class*="message"]',
|
|
269
|
+
waitForResponse: 15000
|
|
270
|
+
},
|
|
271
|
+
google: {
|
|
272
|
+
url: 'https://www.google.com',
|
|
273
|
+
inputSelector: 'textarea[name="q"], input[name="q"]',
|
|
274
|
+
submitKey: 'Enter',
|
|
275
|
+
responseSelector: '#search',
|
|
276
|
+
waitForResponse: 5000
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Ask AI chat and get response
|
|
282
|
+
*/
|
|
283
|
+
export async function askAI(
|
|
284
|
+
site: keyof typeof aiChatConfigs,
|
|
285
|
+
question: string,
|
|
286
|
+
includeScreenshot = false
|
|
287
|
+
): Promise<{ response: string; screenshot?: string }> {
|
|
288
|
+
const config = aiChatConfigs[site];
|
|
289
|
+
if (!config) {
|
|
290
|
+
throw new Error(`Unknown AI site: ${site}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const page = await getPage();
|
|
294
|
+
|
|
295
|
+
// Navigate to site
|
|
296
|
+
await page.goto(config.url, { waitUntil: 'domcontentloaded' });
|
|
297
|
+
await page.waitForTimeout(2000); // Let page fully load
|
|
298
|
+
|
|
299
|
+
// Find and fill input
|
|
300
|
+
try {
|
|
301
|
+
await page.waitForSelector(config.inputSelector, { timeout: 10000 });
|
|
302
|
+
await page.fill(config.inputSelector, question);
|
|
303
|
+
} catch {
|
|
304
|
+
// Try clicking first then typing
|
|
305
|
+
await page.click(config.inputSelector);
|
|
306
|
+
await page.type(config.inputSelector, question, { delay: 30 });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Submit
|
|
310
|
+
if (config.submitSelector) {
|
|
311
|
+
await page.click(config.submitSelector);
|
|
312
|
+
} else if (config.submitKey) {
|
|
313
|
+
await page.keyboard.press(config.submitKey);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Wait for response
|
|
317
|
+
await page.waitForTimeout(config.waitForResponse);
|
|
318
|
+
|
|
319
|
+
// Try to get response text
|
|
320
|
+
let response = '';
|
|
321
|
+
try {
|
|
322
|
+
const elements = await page.$$(config.responseSelector);
|
|
323
|
+
if (elements.length > 0) {
|
|
324
|
+
const lastElement = elements[elements.length - 1];
|
|
325
|
+
response = await lastElement.textContent() || '';
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Fallback: get all page text
|
|
329
|
+
response = await getPageText();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Optional screenshot
|
|
333
|
+
let screenshot: string | undefined;
|
|
334
|
+
if (includeScreenshot) {
|
|
335
|
+
screenshot = await takeScreenshot();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { response: response.trim(), screenshot };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Scroll and capture full response (for long answers)
|
|
343
|
+
*/
|
|
344
|
+
export async function getFullAIResponse(
|
|
345
|
+
site: keyof typeof aiChatConfigs,
|
|
346
|
+
maxScrolls = 5
|
|
347
|
+
): Promise<string[]> {
|
|
348
|
+
const config = aiChatConfigs[site];
|
|
349
|
+
const page = await getPage();
|
|
350
|
+
const responseParts: string[] = [];
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
353
|
+
try {
|
|
354
|
+
const elements = await page.$$(config.responseSelector);
|
|
355
|
+
if (elements.length > 0) {
|
|
356
|
+
const lastElement = elements[elements.length - 1];
|
|
357
|
+
const text = await lastElement.textContent();
|
|
358
|
+
if (text) {
|
|
359
|
+
responseParts.push(text.trim());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Scroll down
|
|
364
|
+
await page.mouse.wheel(0, 500);
|
|
365
|
+
await page.waitForTimeout(1000);
|
|
366
|
+
|
|
367
|
+
// Check if we've reached the bottom
|
|
368
|
+
const atBottom = await page.evaluate(() => {
|
|
369
|
+
return window.innerHeight + window.scrollY >= document.body.scrollHeight - 100;
|
|
370
|
+
});
|
|
371
|
+
if (atBottom) break;
|
|
372
|
+
} catch {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return responseParts;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ========================================
|
|
381
|
+
// Email Helpers
|
|
382
|
+
// ========================================
|
|
383
|
+
|
|
384
|
+
interface EmailData {
|
|
385
|
+
to: string;
|
|
386
|
+
subject: string;
|
|
387
|
+
body: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Send email via Gmail web interface
|
|
392
|
+
*/
|
|
393
|
+
export async function sendGmail(email: EmailData): Promise<boolean> {
|
|
394
|
+
const page = await getPage();
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Go to Gmail compose
|
|
398
|
+
await page.goto('https://mail.google.com/mail/u/0/#inbox?compose=new');
|
|
399
|
+
await page.waitForTimeout(3000);
|
|
400
|
+
|
|
401
|
+
// Wait for compose dialog
|
|
402
|
+
await page.waitForSelector('input[aria-label*="To"]', { timeout: 10000 });
|
|
403
|
+
|
|
404
|
+
// Fill To field
|
|
405
|
+
await page.fill('input[aria-label*="To"]', email.to);
|
|
406
|
+
await page.keyboard.press('Tab');
|
|
407
|
+
|
|
408
|
+
// Fill Subject
|
|
409
|
+
await page.fill('input[name="subjectbox"]', email.subject);
|
|
410
|
+
await page.keyboard.press('Tab');
|
|
411
|
+
|
|
412
|
+
// Fill Body
|
|
413
|
+
await page.fill('[aria-label*="Message Body"], [role="textbox"]', email.body);
|
|
414
|
+
|
|
415
|
+
// Click Send (Ctrl+Enter is faster)
|
|
416
|
+
await page.keyboard.press('Control+Enter');
|
|
417
|
+
|
|
418
|
+
await page.waitForTimeout(2000);
|
|
419
|
+
return true;
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Send email via Outlook web interface
|
|
427
|
+
*/
|
|
428
|
+
export async function sendOutlook(email: EmailData): Promise<boolean> {
|
|
429
|
+
const page = await getPage();
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Go to Outlook compose
|
|
433
|
+
await page.goto('https://outlook.office.com/mail/0/inbox');
|
|
434
|
+
await page.waitForTimeout(3000);
|
|
435
|
+
|
|
436
|
+
// Click New Message
|
|
437
|
+
await page.click('button[aria-label*="New mail"], button[title*="New mail"]');
|
|
438
|
+
await page.waitForTimeout(2000);
|
|
439
|
+
|
|
440
|
+
// Fill To
|
|
441
|
+
await page.fill('input[aria-label*="To"]', email.to);
|
|
442
|
+
await page.keyboard.press('Tab');
|
|
443
|
+
|
|
444
|
+
// Fill Subject
|
|
445
|
+
await page.fill('input[aria-label*="Subject"], input[placeholder*="Subject"]', email.subject);
|
|
446
|
+
await page.keyboard.press('Tab');
|
|
447
|
+
|
|
448
|
+
// Fill Body
|
|
449
|
+
await page.fill('[aria-label*="Message body"], [role="textbox"]', email.body);
|
|
450
|
+
|
|
451
|
+
// Click Send
|
|
452
|
+
await page.click('button[aria-label*="Send"], button[title*="Send"]');
|
|
453
|
+
|
|
454
|
+
await page.waitForTimeout(2000);
|
|
455
|
+
return true;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ========================================
|
|
462
|
+
// Google Apps Helpers
|
|
463
|
+
// ========================================
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Create new Google Sheet and type in cells
|
|
467
|
+
*/
|
|
468
|
+
export async function googleSheetsType(cellData: { cell: string; value: string }[]): Promise<boolean> {
|
|
469
|
+
const page = await getPage();
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
// Go to Google Sheets
|
|
473
|
+
await page.goto('https://docs.google.com/spreadsheets/create');
|
|
474
|
+
await page.waitForTimeout(5000);
|
|
475
|
+
|
|
476
|
+
for (const { cell, value } of cellData) {
|
|
477
|
+
// Click on name box and type cell reference
|
|
478
|
+
await page.click('input#t-name-box');
|
|
479
|
+
await page.fill('input#t-name-box', cell);
|
|
480
|
+
await page.keyboard.press('Enter');
|
|
481
|
+
await page.waitForTimeout(500);
|
|
482
|
+
|
|
483
|
+
// Type value
|
|
484
|
+
await page.keyboard.type(value);
|
|
485
|
+
await page.keyboard.press('Enter');
|
|
486
|
+
await page.waitForTimeout(300);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return true;
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Create new Google Doc and type
|
|
497
|
+
*/
|
|
498
|
+
export async function googleDocsType(text: string): Promise<boolean> {
|
|
499
|
+
const page = await getPage();
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
// Go to Google Docs
|
|
503
|
+
await page.goto('https://docs.google.com/document/create');
|
|
504
|
+
await page.waitForTimeout(5000);
|
|
505
|
+
|
|
506
|
+
// Click on document body
|
|
507
|
+
await page.click('.kix-appview-editor');
|
|
508
|
+
await page.waitForTimeout(500);
|
|
509
|
+
|
|
510
|
+
// Type text
|
|
511
|
+
await page.keyboard.type(text, { delay: 20 });
|
|
512
|
+
|
|
513
|
+
return true;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ========================================
|
|
520
|
+
// Web Search
|
|
521
|
+
// ========================================
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Perform web search and get results
|
|
525
|
+
*/
|
|
526
|
+
export async function webSearch(query: string, engine: 'google' | 'bing' | 'duckduckgo' = 'google'): Promise<string[]> {
|
|
527
|
+
const page = await getPage();
|
|
528
|
+
const results: string[] = [];
|
|
529
|
+
|
|
530
|
+
const urls = {
|
|
531
|
+
google: 'https://www.google.com',
|
|
532
|
+
bing: 'https://www.bing.com',
|
|
533
|
+
duckduckgo: 'https://duckduckgo.com'
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const selectors = {
|
|
537
|
+
google: { input: 'textarea[name="q"]', results: '#search .g h3' },
|
|
538
|
+
bing: { input: 'input[name="q"]', results: '#b_results h2 a' },
|
|
539
|
+
duckduckgo: { input: 'input[name="q"]', results: '[data-result] h2' }
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
await page.goto(urls[engine]);
|
|
544
|
+
await page.waitForTimeout(2000);
|
|
545
|
+
|
|
546
|
+
// Search
|
|
547
|
+
await page.fill(selectors[engine].input, query);
|
|
548
|
+
await page.keyboard.press('Enter');
|
|
549
|
+
await page.waitForTimeout(3000);
|
|
550
|
+
|
|
551
|
+
// Get result titles
|
|
552
|
+
const elements = await page.$$(selectors[engine].results);
|
|
553
|
+
for (const el of elements.slice(0, 10)) {
|
|
554
|
+
const text = await el.textContent();
|
|
555
|
+
if (text) results.push(text);
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
// Return empty on error
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return results;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Click on search result by index
|
|
566
|
+
*/
|
|
567
|
+
export async function clickSearchResult(index: number): Promise<boolean> {
|
|
568
|
+
const page = await getPage();
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const results = await page.$$('#search .g h3, #b_results h2 a, [data-result] h2 a');
|
|
572
|
+
if (results[index]) {
|
|
573
|
+
await results[index].click();
|
|
574
|
+
await page.waitForTimeout(2000);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
} catch {}
|
|
578
|
+
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ========================================
|
|
583
|
+
// Research Helper (multi-step)
|
|
584
|
+
// ========================================
|
|
585
|
+
|
|
586
|
+
export interface ResearchResult {
|
|
587
|
+
query: string;
|
|
588
|
+
sources: { title: string; url: string; content: string }[];
|
|
589
|
+
summary: string;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Research a topic: search, visit results, gather info
|
|
594
|
+
*/
|
|
595
|
+
export async function research(topic: string, maxSources = 3): Promise<ResearchResult> {
|
|
596
|
+
const page = await getPage();
|
|
597
|
+
const sources: { title: string; url: string; content: string }[] = [];
|
|
598
|
+
|
|
599
|
+
// Search
|
|
600
|
+
await webSearch(topic);
|
|
601
|
+
await page.waitForTimeout(2000);
|
|
602
|
+
|
|
603
|
+
// Visit top results
|
|
604
|
+
for (let i = 0; i < maxSources; i++) {
|
|
605
|
+
try {
|
|
606
|
+
const results = await page.$$('#search .g');
|
|
607
|
+
if (results[i]) {
|
|
608
|
+
// Get title and URL
|
|
609
|
+
const titleEl = await results[i].$('h3');
|
|
610
|
+
const linkEl = await results[i].$('a');
|
|
611
|
+
|
|
612
|
+
const title = await titleEl?.textContent() || 'Unknown';
|
|
613
|
+
const url = await linkEl?.getAttribute('href') || '';
|
|
614
|
+
|
|
615
|
+
// Click and get content
|
|
616
|
+
await titleEl?.click();
|
|
617
|
+
await page.waitForTimeout(3000);
|
|
618
|
+
|
|
619
|
+
// Get main content
|
|
620
|
+
const content = await page.evaluate(() => {
|
|
621
|
+
const article = document.querySelector('article, main, .content, #content');
|
|
622
|
+
return article?.textContent?.slice(0, 2000) || document.body.innerText.slice(0, 2000);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
sources.push({ title, url, content: content.trim() });
|
|
626
|
+
|
|
627
|
+
// Go back
|
|
628
|
+
await page.goBack();
|
|
629
|
+
await page.waitForTimeout(1500);
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
query: topic,
|
|
638
|
+
sources,
|
|
639
|
+
summary: '' // To be filled by AI
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export default {
|
|
644
|
+
initBrowser,
|
|
645
|
+
getPage,
|
|
646
|
+
closeBrowser,
|
|
647
|
+
navigateTo,
|
|
648
|
+
takeScreenshot,
|
|
649
|
+
screenshotElement,
|
|
650
|
+
clickElement,
|
|
651
|
+
typeInElement,
|
|
652
|
+
typeSlowly,
|
|
653
|
+
pressKey,
|
|
654
|
+
scroll,
|
|
655
|
+
waitForText,
|
|
656
|
+
getTextContent,
|
|
657
|
+
getPageText,
|
|
658
|
+
waitForNavigation,
|
|
659
|
+
elementExists,
|
|
660
|
+
askAI,
|
|
661
|
+
getFullAIResponse,
|
|
662
|
+
sendGmail,
|
|
663
|
+
sendOutlook,
|
|
664
|
+
googleSheetsType,
|
|
665
|
+
googleDocsType,
|
|
666
|
+
webSearch,
|
|
667
|
+
clickSearchResult,
|
|
668
|
+
research
|
|
669
|
+
};
|