veil-browser 0.1.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 +146 -0
- package/dist/ai.d.ts +19 -0
- package/dist/ai.js +253 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +46 -0
- package/dist/captcha.d.ts +27 -0
- package/dist/captcha.js +203 -0
- package/dist/commands/act.d.ts +7 -0
- package/dist/commands/act.js +107 -0
- package/dist/commands/extract.d.ts +6 -0
- package/dist/commands/extract.js +64 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/navigate.d.ts +5 -0
- package/dist/commands/navigate.js +23 -0
- package/dist/commands/screenshot.d.ts +7 -0
- package/dist/commands/screenshot.js +27 -0
- package/dist/commands/search.d.ts +13 -0
- package/dist/commands/search.js +196 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -0
- package/dist/local-captcha.d.ts +17 -0
- package/dist/local-captcha.js +285 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +232 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +37 -0
- package/dist/startup.d.ts +5 -0
- package/dist/startup.js +9 -0
- package/dist/state.d.ts +13 -0
- package/dist/state.js +45 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# š¶ļø veil
|
|
2
|
+
|
|
3
|
+
> Stealth browser CLI for AI agents ā bypass bot detection, persist sessions, full web control.
|
|
4
|
+
|
|
5
|
+
Built for [OpenClaw](https://openclaw.ai) agents but works standalone. Playwright under the hood with stealth anti-detection baked in from day one.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- š„· **Stealth by default** ā powered by `playwright-extra` + `puppeteer-extra-plugin-stealth`
|
|
12
|
+
- š **Persistent sessions** ā log in once, use forever (saved to `~/.veil/sessions/`)
|
|
13
|
+
- šļø **Interactive login mode** ā opens a real visible browser for you to authenticate manually
|
|
14
|
+
- š¤ **Agent-friendly commands** ā natural language actions, JSON output
|
|
15
|
+
- š **Works everywhere** ā X/Twitter, Reddit, LinkedIn, GitHub, Instagram, any site
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd veil
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
npm link # makes 'veil' available globally
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install Playwright browsers:
|
|
29
|
+
```bash
|
|
30
|
+
npx playwright install chromium
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Login to a platform
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
veil login twitter
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Opens a visible Chromium browser. Log in normally (username, password, 2FA). Veil detects when you're on the home page and saves the session automatically.
|
|
44
|
+
|
|
45
|
+
### Post a tweet
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
veil act "post tweet: Hello from veil š¶ļø" --platform twitter
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Navigate headlessly
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
veil navigate https://x.com --platform twitter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Extract data
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
veil extract "tweets" --url https://x.com/home --platform twitter --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Take a screenshot
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
veil screenshot --url https://x.com --platform twitter --output ./x-home.png
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### List saved sessions
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
veil session list
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Remove a session
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
veil logout twitter
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Status
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
veil status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Command Reference
|
|
90
|
+
|
|
91
|
+
| Command | Description |
|
|
92
|
+
|---------|-------------|
|
|
93
|
+
| `veil login <platform>` | Open visible browser for manual login, save session |
|
|
94
|
+
| `veil logout <platform>` | Remove saved session |
|
|
95
|
+
| `veil navigate <url>` | Navigate to URL (headless stealth) |
|
|
96
|
+
| `veil act "<instruction>"` | Perform natural language action |
|
|
97
|
+
| `veil extract "<query>"` | Extract data as JSON |
|
|
98
|
+
| `veil screenshot` | Take screenshot |
|
|
99
|
+
| `veil session list` | List all saved sessions |
|
|
100
|
+
| `veil status` | Show veil status |
|
|
101
|
+
|
|
102
|
+
### Common flags
|
|
103
|
+
|
|
104
|
+
| Flag | Description |
|
|
105
|
+
|------|-------------|
|
|
106
|
+
| `-p, --platform <name>` | Use saved session for this platform |
|
|
107
|
+
| `-u, --url <url>` | Navigate to URL before action |
|
|
108
|
+
| `-H, --headed` | Run in visible browser mode |
|
|
109
|
+
| `-o, --output <path>` | Output file path (screenshot) |
|
|
110
|
+
| `--json` | Output machine-readable JSON |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Supported `act` instructions
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
veil act "click Login"
|
|
118
|
+
veil act "type 'hello world' into search"
|
|
119
|
+
veil act "post tweet: your content here"
|
|
120
|
+
veil act "like tweet"
|
|
121
|
+
veil act "scroll down"
|
|
122
|
+
veil act "press enter"
|
|
123
|
+
veil act "go to https://example.com"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Session storage
|
|
129
|
+
|
|
130
|
+
Sessions are saved to `~/.veil/sessions/<platform>.json` as Playwright storage state (cookies + localStorage). No encryption currently ā keep your machine secure.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## OpenClaw integration
|
|
135
|
+
|
|
136
|
+
Use `veil` as a drop-in for the OpenClaw Browser Relay. Add it as a skill and call it from any agent:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
veil act "post tweet: launched something today" --platform twitter --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
interface ActionStep {
|
|
3
|
+
action: 'click' | 'type' | 'press' | 'navigate' | 'wait' | 'scroll' | 'select';
|
|
4
|
+
selector?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
key?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
direction?: 'up' | 'down';
|
|
9
|
+
ms?: number;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function aiAct(page: Page, instruction: string, opts?: {
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
success: boolean;
|
|
16
|
+
steps: ActionStep[];
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { humanDelay } from './browser.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
async function loadConfig() {
|
|
8
|
+
const configFile = join(homedir(), '.veil', 'config.json');
|
|
9
|
+
try {
|
|
10
|
+
const raw = await fs.readFile(configFile, 'utf-8');
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getLLMConfig(config) {
|
|
18
|
+
// Priority: config file > env vars
|
|
19
|
+
if (config.llm?.apiKey)
|
|
20
|
+
return config.llm;
|
|
21
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
22
|
+
if (openaiKey)
|
|
23
|
+
return { provider: 'openai', apiKey: openaiKey, model: 'gpt-4o-mini' };
|
|
24
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
25
|
+
if (anthropicKey)
|
|
26
|
+
return { provider: 'anthropic', apiKey: anthropicKey, model: 'claude-haiku-4-5' };
|
|
27
|
+
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
|
28
|
+
if (openrouterKey)
|
|
29
|
+
return { provider: 'openrouter', apiKey: openrouterKey, model: 'openai/gpt-4o-mini' };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Get a compact accessibility snapshot of the page for LLM consumption
|
|
33
|
+
async function getPageSnapshot(page) {
|
|
34
|
+
const snapshot = await page.evaluate(() => {
|
|
35
|
+
const elements = [];
|
|
36
|
+
function processNode(el, depth = 0) {
|
|
37
|
+
if (depth > 6)
|
|
38
|
+
return;
|
|
39
|
+
const tag = el.tagName.toLowerCase();
|
|
40
|
+
const role = el.getAttribute('role');
|
|
41
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
42
|
+
const testId = el.getAttribute('data-testid');
|
|
43
|
+
const type = el.getAttribute('type');
|
|
44
|
+
const placeholder = el.getAttribute('placeholder');
|
|
45
|
+
const text = el.innerText?.slice(0, 80).trim().replace(/\n/g, ' ');
|
|
46
|
+
const href = el.getAttribute('href');
|
|
47
|
+
const disabled = el.getAttribute('disabled') !== null || el.getAttribute('aria-disabled') === 'true';
|
|
48
|
+
const isInteractive = ['a', 'button', 'input', 'textarea', 'select'].includes(tag) || role;
|
|
49
|
+
if (!isInteractive && !text)
|
|
50
|
+
return;
|
|
51
|
+
const attrs = [];
|
|
52
|
+
if (testId)
|
|
53
|
+
attrs.push(`data-testid="${testId}"`);
|
|
54
|
+
if (role)
|
|
55
|
+
attrs.push(`role="${role}"`);
|
|
56
|
+
if (ariaLabel)
|
|
57
|
+
attrs.push(`aria-label="${ariaLabel}"`);
|
|
58
|
+
if (type)
|
|
59
|
+
attrs.push(`type="${type}"`);
|
|
60
|
+
if (placeholder)
|
|
61
|
+
attrs.push(`placeholder="${placeholder}"`);
|
|
62
|
+
if (href)
|
|
63
|
+
attrs.push(`href="${href.slice(0, 60)}"`);
|
|
64
|
+
if (disabled)
|
|
65
|
+
attrs.push('disabled');
|
|
66
|
+
const indent = ' '.repeat(depth);
|
|
67
|
+
const attrStr = attrs.length ? ` [${attrs.join(', ')}]` : '';
|
|
68
|
+
const textStr = text ? ` "${text.slice(0, 60)}"` : '';
|
|
69
|
+
elements.push(`${indent}<${tag}${attrStr}${textStr}>`);
|
|
70
|
+
for (const child of el.children) {
|
|
71
|
+
processNode(child, depth + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
processNode(document.body);
|
|
75
|
+
return elements.slice(0, 200).join('\n');
|
|
76
|
+
});
|
|
77
|
+
return snapshot;
|
|
78
|
+
}
|
|
79
|
+
// Call LLM to get action steps
|
|
80
|
+
async function getActionsFromLLM(instruction, snapshot, pageUrl, llm) {
|
|
81
|
+
const systemPrompt = `You are a browser automation assistant. Given a page snapshot (ARIA/DOM tree) and a user instruction, return a JSON array of action steps to complete the task.
|
|
82
|
+
|
|
83
|
+
Available actions:
|
|
84
|
+
- click: { action: "click", selector: "CSS or data-testid selector", description: "..." }
|
|
85
|
+
- type: { action: "type", selector: "...", text: "the text to type", description: "..." }
|
|
86
|
+
- press: { action: "press", key: "Enter|Tab|Escape|...", description: "..." }
|
|
87
|
+
- navigate: { action: "navigate", url: "https://...", description: "..." }
|
|
88
|
+
- wait: { action: "wait", ms: 1000, description: "..." }
|
|
89
|
+
- scroll: { action: "scroll", direction: "down", description: "..." }
|
|
90
|
+
|
|
91
|
+
Rules:
|
|
92
|
+
- Prefer data-testid selectors when available (most stable)
|
|
93
|
+
- For Twitter/X: use [data-testid="tweetTextarea_0"] for tweet box, [data-testid="tweetButtonInline"] for post button
|
|
94
|
+
- Return ONLY valid JSON array, no explanation
|
|
95
|
+
- Add a wait step after clicks on buttons that trigger UI changes
|
|
96
|
+
- For typing in contenteditable areas, click first then type`;
|
|
97
|
+
const userPrompt = `Current URL: ${pageUrl}
|
|
98
|
+
|
|
99
|
+
Page snapshot:
|
|
100
|
+
${snapshot.slice(0, 4000)}
|
|
101
|
+
|
|
102
|
+
Instruction: ${instruction}
|
|
103
|
+
|
|
104
|
+
Return JSON array of action steps:`;
|
|
105
|
+
let response;
|
|
106
|
+
if (llm.provider === 'openai' || llm.provider === 'openrouter') {
|
|
107
|
+
const baseUrl = llm.provider === 'openrouter'
|
|
108
|
+
? 'https://openrouter.ai/api/v1'
|
|
109
|
+
: 'https://api.openai.com/v1';
|
|
110
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'Authorization': `Bearer ${llm.apiKey}`,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
model: llm.model,
|
|
118
|
+
messages: [
|
|
119
|
+
{ role: 'system', content: systemPrompt },
|
|
120
|
+
{ role: 'user', content: userPrompt },
|
|
121
|
+
],
|
|
122
|
+
temperature: 0,
|
|
123
|
+
max_tokens: 1000,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Anthropic
|
|
129
|
+
response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'x-api-key': llm.apiKey,
|
|
134
|
+
'anthropic-version': '2023-06-01',
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
model: llm.model,
|
|
138
|
+
max_tokens: 1000,
|
|
139
|
+
system: systemPrompt,
|
|
140
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`LLM API error: ${response.status} ${await response.text()}`);
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
const content = llm.provider === 'anthropic'
|
|
149
|
+
? data.content[0].text
|
|
150
|
+
: data.choices[0].message.content;
|
|
151
|
+
// Extract JSON from response
|
|
152
|
+
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
153
|
+
if (!jsonMatch)
|
|
154
|
+
throw new Error('LLM returned no valid JSON array');
|
|
155
|
+
return JSON.parse(jsonMatch[0]);
|
|
156
|
+
}
|
|
157
|
+
// Execute a single action step
|
|
158
|
+
async function executeStep(page, step) {
|
|
159
|
+
switch (step.action) {
|
|
160
|
+
case 'click': {
|
|
161
|
+
if (!step.selector)
|
|
162
|
+
throw new Error('click requires selector');
|
|
163
|
+
// Try multiple selector strategies
|
|
164
|
+
const el = page.locator(step.selector).first();
|
|
165
|
+
await el.waitFor({ timeout: 5000 }).catch(() => { });
|
|
166
|
+
await el.click({ timeout: 5000 });
|
|
167
|
+
await humanDelay(300, 700);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'type': {
|
|
171
|
+
if (!step.selector || !step.text)
|
|
172
|
+
throw new Error('type requires selector and text');
|
|
173
|
+
const el = page.locator(step.selector).first();
|
|
174
|
+
await el.waitFor({ timeout: 5000 }).catch(() => { });
|
|
175
|
+
await el.click();
|
|
176
|
+
await humanDelay(200, 400);
|
|
177
|
+
// Type with human-like delays
|
|
178
|
+
for (const char of step.text) {
|
|
179
|
+
await page.keyboard.type(char, { delay: Math.random() * 60 + 30 });
|
|
180
|
+
}
|
|
181
|
+
await humanDelay(300, 600);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'press': {
|
|
185
|
+
if (!step.key)
|
|
186
|
+
throw new Error('press requires key');
|
|
187
|
+
await page.keyboard.press(step.key);
|
|
188
|
+
await humanDelay(200, 400);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'navigate': {
|
|
192
|
+
if (!step.url)
|
|
193
|
+
throw new Error('navigate requires url');
|
|
194
|
+
await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
195
|
+
await humanDelay(800, 1500);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'wait': {
|
|
199
|
+
await new Promise((r) => setTimeout(r, step.ms ?? 1000));
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'scroll': {
|
|
203
|
+
const amount = step.direction === 'up' ? -600 : 600;
|
|
204
|
+
await page.evaluate((y) => window.scrollBy(0, y), amount);
|
|
205
|
+
await humanDelay(300, 500);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Main AI-powered act function
|
|
211
|
+
export async function aiAct(page, instruction, opts = {}) {
|
|
212
|
+
const config = await loadConfig();
|
|
213
|
+
const llm = getLLMConfig(config);
|
|
214
|
+
if (!llm) {
|
|
215
|
+
throw new Error('No LLM configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY env var, ' +
|
|
216
|
+
'or run: veil config llm.provider openai && veil config llm.apiKey YOUR_KEY');
|
|
217
|
+
}
|
|
218
|
+
const spinner = ora({ text: 'š§ Analyzing page...', color: 'cyan' }).start();
|
|
219
|
+
try {
|
|
220
|
+
// 1. Get page snapshot
|
|
221
|
+
const snapshot = await getPageSnapshot(page);
|
|
222
|
+
const pageUrl = page.url();
|
|
223
|
+
spinner.text = 'š§ Asking AI what to do...';
|
|
224
|
+
// 2. Get action steps from LLM
|
|
225
|
+
const steps = await getActionsFromLLM(instruction, snapshot, pageUrl, llm);
|
|
226
|
+
if (opts.verbose) {
|
|
227
|
+
spinner.stop();
|
|
228
|
+
console.log(chalk.cyan('\nš AI action plan:'));
|
|
229
|
+
steps.forEach((s, i) => {
|
|
230
|
+
console.log(chalk.gray(` ${i + 1}. ${s.action}${s.description ? ': ' + s.description : ''}`));
|
|
231
|
+
});
|
|
232
|
+
console.log('');
|
|
233
|
+
spinner.start('Executing...');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
spinner.text = `Executing ${steps.length} steps...`;
|
|
237
|
+
}
|
|
238
|
+
// 3. Execute each step
|
|
239
|
+
for (let i = 0; i < steps.length; i++) {
|
|
240
|
+
const step = steps[i];
|
|
241
|
+
if (opts.verbose) {
|
|
242
|
+
spinner.text = `Step ${i + 1}/${steps.length}: ${step.action} ${step.description ?? ''}`;
|
|
243
|
+
}
|
|
244
|
+
await executeStep(page, step);
|
|
245
|
+
}
|
|
246
|
+
spinner.succeed(chalk.green(`ā
Done: ${instruction}`));
|
|
247
|
+
return { success: true, steps };
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
spinner.fail(chalk.red(`ā AI act failed: ${err.message}`));
|
|
251
|
+
return { success: false, steps: [], error: err.message };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
export interface LaunchOptions {
|
|
3
|
+
headed?: boolean;
|
|
4
|
+
platform?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getBrowser(opts?: LaunchOptions): Promise<{
|
|
7
|
+
browser: Browser;
|
|
8
|
+
context: BrowserContext;
|
|
9
|
+
page: Page;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function closeBrowser(platform?: string): Promise<void>;
|
|
12
|
+
export declare function humanDelay(min?: number, max?: number): Promise<void>;
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { chromium } from 'playwright-extra';
|
|
2
|
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
3
|
+
import { loadSession, saveSession } from './session.js';
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
chromium.use(StealthPlugin());
|
|
6
|
+
let _browser = null;
|
|
7
|
+
let _context = null;
|
|
8
|
+
let _page = null;
|
|
9
|
+
export async function getBrowser(opts = {}) {
|
|
10
|
+
const headless = !opts.headed;
|
|
11
|
+
_browser = await chromium.launch({
|
|
12
|
+
headless,
|
|
13
|
+
args: [
|
|
14
|
+
'--no-sandbox',
|
|
15
|
+
'--disable-blink-features=AutomationControlled',
|
|
16
|
+
'--disable-infobars',
|
|
17
|
+
'--window-size=1280,800',
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
const storageState = opts.platform ? await loadSession(opts.platform) : undefined;
|
|
21
|
+
_context = await _browser.newContext({
|
|
22
|
+
storageState: storageState ?? undefined,
|
|
23
|
+
viewport: { width: 1280, height: 800 },
|
|
24
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
25
|
+
locale: 'en-US',
|
|
26
|
+
timezoneId: 'Europe/Bratislava',
|
|
27
|
+
});
|
|
28
|
+
_page = await _context.newPage();
|
|
29
|
+
return { browser: _browser, context: _context, page: _page };
|
|
30
|
+
}
|
|
31
|
+
export async function closeBrowser(platform) {
|
|
32
|
+
if (_context && platform) {
|
|
33
|
+
const state = await _context.storageState();
|
|
34
|
+
await saveSession(platform, state);
|
|
35
|
+
}
|
|
36
|
+
if (_browser) {
|
|
37
|
+
await _browser.close();
|
|
38
|
+
_browser = null;
|
|
39
|
+
_context = null;
|
|
40
|
+
_page = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function humanDelay(min = 300, max = 900) {
|
|
44
|
+
const ms = Math.floor(Math.random() * (max - min) + min);
|
|
45
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export interface VeilConfig {
|
|
3
|
+
captcha?: {
|
|
4
|
+
provider?: '2captcha' | 'capmonster' | 'anticaptcha';
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
};
|
|
7
|
+
proxy?: {
|
|
8
|
+
server?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export type VeilErrorCode = 'SELECTOR_NOT_FOUND' | 'NAVIGATION_TIMEOUT' | 'SESSION_EXPIRED' | 'CAPTCHA_DETECTED' | 'RATE_LIMITED' | 'UNKNOWN';
|
|
14
|
+
export declare class VeilError extends Error {
|
|
15
|
+
code: VeilErrorCode;
|
|
16
|
+
screenshotPath?: string;
|
|
17
|
+
suggestion?: string;
|
|
18
|
+
constructor(code: VeilErrorCode, message: string, suggestion?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare function detectCaptcha(page: Page): Promise<'turnstile' | 'recaptcha' | 'hcaptcha' | 'image' | null>;
|
|
21
|
+
export declare function handleCaptcha(page: Page, screenshotDir?: string): Promise<boolean>;
|
|
22
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: {
|
|
23
|
+
attempts?: number;
|
|
24
|
+
delay?: number;
|
|
25
|
+
label?: string;
|
|
26
|
+
}): Promise<T>;
|
|
27
|
+
export declare function screenshotOnError(page: Page, label: string): Promise<string | null>;
|