claw-browser-automation 0.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 +288 -0
- package/dist/actions/action.d.ts +50 -0
- package/dist/actions/action.d.ts.map +1 -0
- package/dist/actions/action.js +189 -0
- package/dist/actions/action.js.map +1 -0
- package/dist/actions/assertions.d.ts +15 -0
- package/dist/actions/assertions.d.ts.map +1 -0
- package/dist/actions/assertions.js +75 -0
- package/dist/actions/assertions.js.map +1 -0
- package/dist/actions/extract-structured.d.ts +26 -0
- package/dist/actions/extract-structured.d.ts.map +1 -0
- package/dist/actions/extract-structured.js +112 -0
- package/dist/actions/extract-structured.js.map +1 -0
- package/dist/actions/extract.d.ts +13 -0
- package/dist/actions/extract.d.ts.map +1 -0
- package/dist/actions/extract.js +119 -0
- package/dist/actions/extract.js.map +1 -0
- package/dist/actions/interact.d.ts +32 -0
- package/dist/actions/interact.d.ts.map +1 -0
- package/dist/actions/interact.js +263 -0
- package/dist/actions/interact.js.map +1 -0
- package/dist/actions/navigate.d.ts +13 -0
- package/dist/actions/navigate.d.ts.map +1 -0
- package/dist/actions/navigate.js +91 -0
- package/dist/actions/navigate.js.map +1 -0
- package/dist/actions/page.d.ts +21 -0
- package/dist/actions/page.d.ts.map +1 -0
- package/dist/actions/page.js +63 -0
- package/dist/actions/page.js.map +1 -0
- package/dist/actions/resilience.d.ts +21 -0
- package/dist/actions/resilience.d.ts.map +1 -0
- package/dist/actions/resilience.js +112 -0
- package/dist/actions/resilience.js.map +1 -0
- package/dist/actions/semantic.d.ts +58 -0
- package/dist/actions/semantic.d.ts.map +1 -0
- package/dist/actions/semantic.js +181 -0
- package/dist/actions/semantic.js.map +1 -0
- package/dist/actions/wait.d.ts +10 -0
- package/dist/actions/wait.d.ts.map +1 -0
- package/dist/actions/wait.js +69 -0
- package/dist/actions/wait.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +54 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/observe/logger.d.ts +4 -0
- package/dist/observe/logger.d.ts.map +1 -0
- package/dist/observe/logger.js +52 -0
- package/dist/observe/logger.js.map +1 -0
- package/dist/observe/trace.d.ts +46 -0
- package/dist/observe/trace.d.ts.map +1 -0
- package/dist/observe/trace.js +102 -0
- package/dist/observe/trace.js.map +1 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +58 -0
- package/dist/plugin.js.map +1 -0
- package/dist/pool/browser-pool.d.ts +48 -0
- package/dist/pool/browser-pool.d.ts.map +1 -0
- package/dist/pool/browser-pool.js +216 -0
- package/dist/pool/browser-pool.js.map +1 -0
- package/dist/pool/health.d.ts +28 -0
- package/dist/pool/health.d.ts.map +1 -0
- package/dist/pool/health.js +96 -0
- package/dist/pool/health.js.map +1 -0
- package/dist/selectors/strategy.d.ts +37 -0
- package/dist/selectors/strategy.d.ts.map +1 -0
- package/dist/selectors/strategy.js +111 -0
- package/dist/selectors/strategy.js.map +1 -0
- package/dist/session/handle-registry.d.ts +59 -0
- package/dist/session/handle-registry.d.ts.map +1 -0
- package/dist/session/handle-registry.js +92 -0
- package/dist/session/handle-registry.js.map +1 -0
- package/dist/session/profiles.d.ts +11 -0
- package/dist/session/profiles.d.ts.map +1 -0
- package/dist/session/profiles.js +76 -0
- package/dist/session/profiles.js.map +1 -0
- package/dist/session/session.d.ts +34 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +155 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/snapshot.d.ts +18 -0
- package/dist/session/snapshot.d.ts.map +1 -0
- package/dist/session/snapshot.js +2 -0
- package/dist/session/snapshot.js.map +1 -0
- package/dist/store/action-log.d.ts +31 -0
- package/dist/store/action-log.d.ts.map +1 -0
- package/dist/store/action-log.js +52 -0
- package/dist/store/action-log.js.map +1 -0
- package/dist/store/artifacts.d.ts +22 -0
- package/dist/store/artifacts.d.ts.map +1 -0
- package/dist/store/artifacts.js +101 -0
- package/dist/store/artifacts.js.map +1 -0
- package/dist/store/db.d.ts +16 -0
- package/dist/store/db.d.ts.map +1 -0
- package/dist/store/db.js +91 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/sessions.d.ts +25 -0
- package/dist/store/sessions.d.ts.map +1 -0
- package/dist/store/sessions.js +63 -0
- package/dist/store/sessions.js.map +1 -0
- package/dist/tools/action-tools.d.ts +4 -0
- package/dist/tools/action-tools.d.ts.map +1 -0
- package/dist/tools/action-tools.js +356 -0
- package/dist/tools/action-tools.js.map +1 -0
- package/dist/tools/approval-tools.d.ts +4 -0
- package/dist/tools/approval-tools.d.ts.map +1 -0
- package/dist/tools/approval-tools.js +28 -0
- package/dist/tools/approval-tools.js.map +1 -0
- package/dist/tools/context.d.ts +31 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +32 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/handle-tools.d.ts +4 -0
- package/dist/tools/handle-tools.d.ts.map +1 -0
- package/dist/tools/handle-tools.js +103 -0
- package/dist/tools/handle-tools.js.map +1 -0
- package/dist/tools/page-tools.d.ts +4 -0
- package/dist/tools/page-tools.d.ts.map +1 -0
- package/dist/tools/page-tools.js +109 -0
- package/dist/tools/page-tools.js.map +1 -0
- package/dist/tools/semantic-tools.d.ts +4 -0
- package/dist/tools/semantic-tools.d.ts.map +1 -0
- package/dist/tools/semantic-tools.js +173 -0
- package/dist/tools/semantic-tools.js.map +1 -0
- package/dist/tools/session-tools.d.ts +18 -0
- package/dist/tools/session-tools.d.ts.map +1 -0
- package/dist/tools/session-tools.js +118 -0
- package/dist/tools/session-tools.js.map +1 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# claw-browser-automation
|
|
2
|
+
|
|
3
|
+
Reliable browser automation layer for [OpenClaw](https://github.com/openclaw/openclaw). Replaces the flaky extension relay with managed Playwright sessions that the AI agent drives directly.
|
|
4
|
+
|
|
5
|
+
The agent (clawbot) is the workflow engine — it decides *what* to do. This layer provides reliable *how*: atomic browser actions with postcondition verification, automatic retries, session persistence, and full observability.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────┐
|
|
9
|
+
│ OpenClaw Agent (clawbot) │
|
|
10
|
+
│ - Receives user intent via any channel │
|
|
11
|
+
│ - Decides what browser actions to take │
|
|
12
|
+
│ - Calls browser tools exposed by this skill │
|
|
13
|
+
└──────────────┬──────────────────────────────────┘
|
|
14
|
+
│ tool calls
|
|
15
|
+
┌──────────────▼──────────────────────────────────┐
|
|
16
|
+
│ claw-browser-automation (this project) │
|
|
17
|
+
│ │
|
|
18
|
+
│ Tool Layer → Action Engine → Browser Pool │
|
|
19
|
+
│ │ │
|
|
20
|
+
│ State & Artifacts (SQLite) │
|
|
21
|
+
└──────────────┬──────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
┌──────────▼──────────┐
|
|
24
|
+
│ Chromium (managed) │
|
|
25
|
+
└─────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
- **Node.js** >= 22.12.0
|
|
31
|
+
- **Bun** (package manager)
|
|
32
|
+
- **OpenClaw** >= 2026.2.9 installed globally
|
|
33
|
+
- **Playwright browsers** installed (`npx playwright install chromium`)
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
### 1. Install as an OpenClaw plugin
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
openclaw plugins install claw-browser-automation
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Install Playwright browsers (first time only)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx playwright install chromium
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Configure (optional)
|
|
50
|
+
|
|
51
|
+
All configuration has sensible defaults. To customize, edit `~/.openclaw/openclaw.json`:
|
|
52
|
+
|
|
53
|
+
```jsonc
|
|
54
|
+
{
|
|
55
|
+
"skills": {
|
|
56
|
+
"entries": {
|
|
57
|
+
"browser-automation": {
|
|
58
|
+
"enabled": true,
|
|
59
|
+
"config": {
|
|
60
|
+
"maxContexts": 4,
|
|
61
|
+
"headless": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Start OpenClaw
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
openclaw start
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The browser automation plugin loads automatically. The agent now has access to 19 browser tools and can perform any browser task you ask for via Telegram, the CLI, or any other configured channel.
|
|
76
|
+
|
|
77
|
+
### Alternative: install from source
|
|
78
|
+
|
|
79
|
+
If you prefer to work from a local checkout instead of the published package:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/ametel01/claw-browser-automation
|
|
83
|
+
cd claw-browser-automation
|
|
84
|
+
bun install
|
|
85
|
+
bun run build
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then point OpenClaw at the source tree in `~/.openclaw/openclaw.json`:
|
|
89
|
+
|
|
90
|
+
```jsonc
|
|
91
|
+
{
|
|
92
|
+
"skills": {
|
|
93
|
+
"load": {
|
|
94
|
+
"extraDirs": ["/path/to/claw-browser-automation"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Programmatic usage
|
|
101
|
+
|
|
102
|
+
You can also use the library directly without OpenClaw:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { createSkill } from "claw-browser-automation";
|
|
106
|
+
|
|
107
|
+
const skill = await createSkill({
|
|
108
|
+
maxContexts: 4,
|
|
109
|
+
headless: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// skill.tools — array of 19 ToolDefinition objects
|
|
113
|
+
// skill.context — internal state (pool, store, trace, etc.)
|
|
114
|
+
// skill.shutdown() — graceful shutdown
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## How it works with OpenClaw
|
|
118
|
+
|
|
119
|
+
When you send a message like "go to example.com and extract the main heading", the flow is:
|
|
120
|
+
|
|
121
|
+
1. **You** send a message via Telegram / CLI / API
|
|
122
|
+
2. **OpenClaw agent** receives the intent and plans a tool sequence
|
|
123
|
+
3. **Agent calls** `browser_open` → `browser_navigate` → `browser_extract_text` → `browser_close`
|
|
124
|
+
4. **This skill** executes each tool against a managed Playwright browser
|
|
125
|
+
5. **Agent returns** the extracted data to you
|
|
126
|
+
|
|
127
|
+
The agent composes tools into arbitrary workflows. This layer doesn't hardcode any specific task — it provides primitives the agent chains together.
|
|
128
|
+
|
|
129
|
+
## Available tools
|
|
130
|
+
|
|
131
|
+
The skill exposes 19 tools to the agent, grouped by function:
|
|
132
|
+
|
|
133
|
+
### Session management
|
|
134
|
+
|
|
135
|
+
| Tool | Description |
|
|
136
|
+
|------|-------------|
|
|
137
|
+
| `browser_open` | Open a new browser session, optionally with a URL or named profile |
|
|
138
|
+
| `browser_close` | Close session and save a snapshot for later restore |
|
|
139
|
+
| `browser_list` | List all active sessions with URLs and health status |
|
|
140
|
+
| `browser_restore` | Restore a previously suspended session from its snapshot |
|
|
141
|
+
| `browser_state` | Get current page state (URL, title, loading status) |
|
|
142
|
+
|
|
143
|
+
### Page actions
|
|
144
|
+
|
|
145
|
+
| Tool | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `browser_navigate` | Navigate to a URL and wait for load |
|
|
148
|
+
| `browser_click` | Click an element by CSS selector |
|
|
149
|
+
| `browser_type` | Type text into an input field |
|
|
150
|
+
| `browser_select` | Select a dropdown option |
|
|
151
|
+
| `browser_fill_form` | Fill multiple form fields at once |
|
|
152
|
+
| `browser_wait` | Wait for an element state or a JS condition |
|
|
153
|
+
|
|
154
|
+
### Data extraction
|
|
155
|
+
|
|
156
|
+
| Tool | Description |
|
|
157
|
+
|------|-------------|
|
|
158
|
+
| `browser_extract_text` | Extract text content from a single element |
|
|
159
|
+
| `browser_extract_all` | Extract data from all matching elements (lists, tables) |
|
|
160
|
+
| `browser_get_content` | Get cleaned page text (scripts/styles removed) |
|
|
161
|
+
|
|
162
|
+
### Page utilities
|
|
163
|
+
|
|
164
|
+
| Tool | Description |
|
|
165
|
+
|------|-------------|
|
|
166
|
+
| `browser_screenshot` | Capture a screenshot and save as artifact |
|
|
167
|
+
| `browser_evaluate` | Execute arbitrary JavaScript in the page |
|
|
168
|
+
| `browser_scroll` | Scroll the page in a direction |
|
|
169
|
+
| `browser_session_trace` | Get the full action trace for a session |
|
|
170
|
+
|
|
171
|
+
### Safety
|
|
172
|
+
|
|
173
|
+
| Tool | Description |
|
|
174
|
+
|------|-------------|
|
|
175
|
+
| `browser_request_approval` | Pause and ask the human for confirmation before proceeding |
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
All configuration is optional. Defaults work out of the box.
|
|
180
|
+
|
|
181
|
+
| Option | Default | Description |
|
|
182
|
+
|--------|---------|-------------|
|
|
183
|
+
| `maxContexts` | `4` | Maximum concurrent browser sessions |
|
|
184
|
+
| `headless` | `true` | Run browsers without a visible window |
|
|
185
|
+
| `dbPath` | `~/.openclaw/browser-automation/store.db` | SQLite database for session persistence |
|
|
186
|
+
| `artifactsDir` | `~/.openclaw/workspace/browser-automation/artifacts` | Screenshot and DOM snapshot storage |
|
|
187
|
+
| `logLevel` | `info` | Log level (`debug`, `info`, `warn`, `error`) |
|
|
188
|
+
|
|
189
|
+
Pass these via the `config` key in your `openclaw.json` skill entry, or programmatically via `createSkill()` (see [Quick start](#quick-start)).
|
|
190
|
+
|
|
191
|
+
## Reliability features
|
|
192
|
+
|
|
193
|
+
These are the mechanisms that make this layer production-grade compared to an extension relay:
|
|
194
|
+
|
|
195
|
+
- **3-tier timeouts** — short (5s), medium (15s), long (45s) per action type
|
|
196
|
+
- **Exponential backoff retries** with jitter on every action
|
|
197
|
+
- **DOM stability checks** before reads and clicks (MutationObserver-based)
|
|
198
|
+
- **Automatic popup/cookie banner dismissal** — recognizes 13 common patterns
|
|
199
|
+
- **Health probes with circuit breaker** — detects browser crashes, auto-restarts
|
|
200
|
+
- **Session snapshots** — URL, cookies, localStorage checkpointed to SQLite
|
|
201
|
+
- **Pre/postcondition verification** per action
|
|
202
|
+
- **Layered selector resolution** — CSS, ARIA, text, label, test ID, XPath with fallback chains
|
|
203
|
+
|
|
204
|
+
## Data persistence
|
|
205
|
+
|
|
206
|
+
All session state survives process restarts:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
~/.openclaw/browser-automation/
|
|
210
|
+
├── store.db # SQLite: sessions, action log, schema
|
|
211
|
+
└── ...
|
|
212
|
+
|
|
213
|
+
~/.openclaw/workspace/browser-automation/
|
|
214
|
+
├── artifacts/
|
|
215
|
+
│ └── {sessionId}/ # Screenshots and DOM snapshots per session
|
|
216
|
+
└── logs/
|
|
217
|
+
└── browser-automation-YYYY-MM-DD.log
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The agent can suspend a session, shut down, restart hours later, and resume exactly where it left off — same URL, same cookies, same localStorage.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
### Scripts
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
bun run build # Compile TypeScript to dist/
|
|
228
|
+
bun run test # Run all tests (183 tests across 12 files)
|
|
229
|
+
bun run test:watch # Watch mode
|
|
230
|
+
bun run check # Biome lint + format check
|
|
231
|
+
bun run check:fix # Auto-fix lint/format issues
|
|
232
|
+
bun run typecheck # TypeScript strict mode check
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Running integration tests
|
|
236
|
+
|
|
237
|
+
The integration tests prove end-to-end reliability across 9 scenarios: pool lifecycle, DOM extraction, crash recovery, popup dismissal, form filling, concurrent sessions, action retry, full tool chain, and session suspend/restore.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Single run
|
|
241
|
+
bun run test -- tests/integration/integration.test.ts
|
|
242
|
+
|
|
243
|
+
# Reliability check (10 consecutive passes required)
|
|
244
|
+
for i in {1..10}; do bun run test -- tests/integration/integration.test.ts || exit 1; done
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Project structure
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
src/
|
|
251
|
+
├── index.ts # Library entry point — createSkill()
|
|
252
|
+
├── plugin.ts # OpenClaw plugin adapter — register(api)
|
|
253
|
+
├── pool/
|
|
254
|
+
│ ├── browser-pool.ts # Session lifecycle, max-context enforcement
|
|
255
|
+
│ └── health.ts # Health probes, circuit breaker recovery
|
|
256
|
+
├── session/
|
|
257
|
+
│ ├── session.ts # BrowserSession with snapshot/restore
|
|
258
|
+
│ ├── snapshot.ts # SessionSnapshot type (URL, cookies, localStorage)
|
|
259
|
+
│ └── profiles.ts # Named profile persistence
|
|
260
|
+
├── actions/
|
|
261
|
+
│ ├── action.ts # executeAction framework (retries, timeouts, tracing)
|
|
262
|
+
│ ├── interact.ts # click, type, fill, select, check, hover, drag
|
|
263
|
+
│ ├── extract.ts # getText, getAll, getPageContent
|
|
264
|
+
│ ├── navigate.ts # navigate, reload, goBack, goForward
|
|
265
|
+
│ ├── wait.ts # waitForSelector, waitForCondition, waitForNetworkIdle
|
|
266
|
+
│ ├── page.ts # screenshot, evaluate, scroll, getPageState
|
|
267
|
+
│ └── resilience.ts # PopupDismisser, waitForDomStability
|
|
268
|
+
├── selectors/
|
|
269
|
+
│ └── strategy.ts # Layered selector resolution (CSS/ARIA/text/label/xpath)
|
|
270
|
+
├── store/
|
|
271
|
+
│ ├── db.ts # SQLite with auto-migrations
|
|
272
|
+
│ ├── sessions.ts # SessionStore (CRUD + suspend/restore)
|
|
273
|
+
│ ├── action-log.ts # Every action logged with timing and result
|
|
274
|
+
│ └── artifacts.ts # Screenshot/snapshot storage with retention
|
|
275
|
+
├── observe/
|
|
276
|
+
│ ├── logger.ts # Pino structured logging (stdout + file)
|
|
277
|
+
│ └── trace.ts # Per-session action traces with p50/p95 stats
|
|
278
|
+
└── tools/
|
|
279
|
+
├── context.ts # SkillContext type, helper functions
|
|
280
|
+
├── session-tools.ts # browser_open, close, list, restore, state
|
|
281
|
+
├── action-tools.ts # browser_navigate, click, type, fill, extract, wait
|
|
282
|
+
├── page-tools.ts # browser_screenshot, evaluate, scroll, trace
|
|
283
|
+
└── approval-tools.ts # browser_request_approval
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
import type { Logger } from "../observe/logger.js";
|
|
3
|
+
import type { ActionTrace, SelectorResolutionTrace } from "../observe/trace.js";
|
|
4
|
+
export interface StructuredError {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
recoveryHint: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ActionResult<T = unknown> {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
data?: T;
|
|
12
|
+
error?: string;
|
|
13
|
+
structuredError?: StructuredError;
|
|
14
|
+
retries: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
screenshot?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function toStructuredError(err: unknown): string | StructuredError;
|
|
19
|
+
export type TimeoutTier = "short" | "medium" | "long";
|
|
20
|
+
export declare function resolveTimeout(timeout: TimeoutTier | number | undefined): number;
|
|
21
|
+
export interface TraceMetadata {
|
|
22
|
+
selectorResolved?: SelectorResolutionTrace;
|
|
23
|
+
eventsDispatched?: string[];
|
|
24
|
+
waitsPerformed?: string[];
|
|
25
|
+
assertionsChecked?: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface ActionContext {
|
|
28
|
+
page: Page;
|
|
29
|
+
logger: Logger;
|
|
30
|
+
screenshotDir?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
trace?: ActionTrace;
|
|
33
|
+
_traceMeta?: TraceMetadata;
|
|
34
|
+
_retryState?: RetryState;
|
|
35
|
+
}
|
|
36
|
+
export interface RetryState {
|
|
37
|
+
lastClickSelector?: string;
|
|
38
|
+
lastClickTime?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ActionOptions {
|
|
41
|
+
timeout?: TimeoutTier | number;
|
|
42
|
+
retries?: number;
|
|
43
|
+
screenshotOnFailure?: boolean;
|
|
44
|
+
precondition?: (ctx: ActionContext) => Promise<boolean>;
|
|
45
|
+
postcondition?: (ctx: ActionContext) => Promise<boolean>;
|
|
46
|
+
/** Internal: mutable array of selector strategies for rotation on retry. */
|
|
47
|
+
_selectorStrategies?: unknown[];
|
|
48
|
+
}
|
|
49
|
+
export declare function executeAction<T>(ctx: ActionContext, name: string, opts: ActionOptions, fn: (ctx: ActionContext, timeoutMs: number) => Promise<T>): Promise<ActionResult<T>>;
|
|
50
|
+
//# sourceMappingURL=action.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/actions/action.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAM5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAGhF,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACxC,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,eAAe,CAQxE;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAQtD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAQhF;AAED,MAAM,WAAW,aAAa;IAC7B,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,WAAW,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,OAAO,EAAE,CAAC;CAChC;AAgHD,wBAAsB,aAAa,CAAC,CAAC,EACpC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,EACnB,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACvD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CA8C1B"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { BrowserAutomationError, NavigationInterruptedError, TargetNotFoundError, } from "../errors.js";
|
|
2
|
+
import { PopupDismisser } from "./resilience.js";
|
|
3
|
+
export function toStructuredError(err) {
|
|
4
|
+
if (err instanceof BrowserAutomationError) {
|
|
5
|
+
return { code: err.code, message: err.message, recoveryHint: err.recoveryHint };
|
|
6
|
+
}
|
|
7
|
+
if (err instanceof Error) {
|
|
8
|
+
return err.message;
|
|
9
|
+
}
|
|
10
|
+
return String(err);
|
|
11
|
+
}
|
|
12
|
+
const TIMEOUT_VALUES = {
|
|
13
|
+
short: 5_000,
|
|
14
|
+
medium: 15_000,
|
|
15
|
+
long: 45_000,
|
|
16
|
+
};
|
|
17
|
+
export function resolveTimeout(timeout) {
|
|
18
|
+
if (timeout === undefined) {
|
|
19
|
+
return TIMEOUT_VALUES.medium;
|
|
20
|
+
}
|
|
21
|
+
if (typeof timeout === "number") {
|
|
22
|
+
return timeout;
|
|
23
|
+
}
|
|
24
|
+
return TIMEOUT_VALUES[timeout];
|
|
25
|
+
}
|
|
26
|
+
const DEFAULT_RETRIES = 3;
|
|
27
|
+
async function runAttempt(ctx, opts, fn, timeoutMs) {
|
|
28
|
+
if (opts.precondition && !(await opts.precondition(ctx))) {
|
|
29
|
+
return { tag: "retry", error: "precondition failed" };
|
|
30
|
+
}
|
|
31
|
+
const data = await fn(ctx, timeoutMs);
|
|
32
|
+
if (opts.postcondition && !(await opts.postcondition(ctx))) {
|
|
33
|
+
return { tag: "retry", error: "postcondition failed" };
|
|
34
|
+
}
|
|
35
|
+
return { tag: "success", data };
|
|
36
|
+
}
|
|
37
|
+
async function retryLoop(ctx, name, opts, fn, timeoutMs, maxRetries, startUrl, popupDismisser) {
|
|
38
|
+
let lastError = "";
|
|
39
|
+
let lastCaughtError;
|
|
40
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
41
|
+
lastCaughtError = undefined;
|
|
42
|
+
const navCheck = checkNavigationGuard(attempt, ctx, startUrl);
|
|
43
|
+
if (navCheck) {
|
|
44
|
+
return { tag: "exhausted", lastError: navCheck.message, lastCaughtError: navCheck };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await popupDismisser.dismissOnce();
|
|
48
|
+
const outcome = await runAttempt(ctx, opts, fn, timeoutMs);
|
|
49
|
+
if (outcome.tag === "success") {
|
|
50
|
+
return { tag: "success", data: outcome.data, attempt };
|
|
51
|
+
}
|
|
52
|
+
lastError = outcome.error;
|
|
53
|
+
ctx.logger.warn({ action: name, attempt }, lastError);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
lastCaughtError = err;
|
|
57
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
58
|
+
ctx.logger.warn({ action: name, attempt, error: lastError }, "action attempt failed");
|
|
59
|
+
handleRetryError(err, opts, ctx, name);
|
|
60
|
+
}
|
|
61
|
+
if (attempt < maxRetries) {
|
|
62
|
+
await backoff(attempt);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { tag: "exhausted", lastError, lastCaughtError };
|
|
66
|
+
}
|
|
67
|
+
function checkNavigationGuard(attempt, ctx, startUrl) {
|
|
68
|
+
if (attempt === 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const currentUrl = ctx.page.url();
|
|
72
|
+
if (currentUrl !== startUrl) {
|
|
73
|
+
return new NavigationInterruptedError(`page navigated from ${startUrl} to ${currentUrl} during retry`);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function handleRetryError(err, opts, ctx, name) {
|
|
78
|
+
if (err instanceof TargetNotFoundError && opts._selectorStrategies) {
|
|
79
|
+
rotateStrategies(opts._selectorStrategies);
|
|
80
|
+
ctx.logger.debug({ action: name }, "rotated selector strategies for retry");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function rotateStrategies(strategies) {
|
|
84
|
+
if (strategies.length <= 1) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const first = strategies.shift();
|
|
88
|
+
if (first !== undefined) {
|
|
89
|
+
strategies.push(first);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function executeAction(ctx, name, opts, fn) {
|
|
93
|
+
const maxRetries = opts.retries ?? DEFAULT_RETRIES;
|
|
94
|
+
const timeoutMs = resolveTimeout(opts.timeout);
|
|
95
|
+
const startedAt = performance.now();
|
|
96
|
+
const popupDismisser = new PopupDismisser(ctx.page, ctx.logger);
|
|
97
|
+
popupDismisser.start();
|
|
98
|
+
ctx._traceMeta = {};
|
|
99
|
+
ctx._retryState = {};
|
|
100
|
+
const startUrl = ctx.page.url();
|
|
101
|
+
try {
|
|
102
|
+
const loopResult = await retryLoop(ctx, name, opts, fn, timeoutMs, maxRetries, startUrl, popupDismisser);
|
|
103
|
+
if (loopResult.tag === "success") {
|
|
104
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
105
|
+
recordTraceEntry(ctx, name, {
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
durationMs,
|
|
108
|
+
ok: true,
|
|
109
|
+
retries: loopResult.attempt,
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, data: loopResult.data, retries: loopResult.attempt, durationMs };
|
|
112
|
+
}
|
|
113
|
+
return buildFailureResult(ctx, name, loopResult.lastError, loopResult.lastCaughtError, maxRetries, startedAt, opts);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
popupDismisser.stop();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function buildFailureResult(ctx, name, lastError, lastCaughtError, maxRetries, startedAt, opts) {
|
|
120
|
+
const structured = toStructuredError(lastCaughtError ?? lastError);
|
|
121
|
+
const result = {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: lastError,
|
|
124
|
+
retries: maxRetries,
|
|
125
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
126
|
+
};
|
|
127
|
+
if (typeof structured === "object") {
|
|
128
|
+
result.structuredError = structured;
|
|
129
|
+
}
|
|
130
|
+
if (opts.screenshotOnFailure !== false) {
|
|
131
|
+
try {
|
|
132
|
+
const screenshotPath = await captureFailureScreenshot(ctx, name);
|
|
133
|
+
if (screenshotPath) {
|
|
134
|
+
result.screenshot = screenshotPath;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
ctx.logger.debug("failed to capture failure screenshot");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
ctx.logger.error({ action: name, error: lastError, retries: maxRetries }, "action failed");
|
|
142
|
+
recordTraceEntry(ctx, name, {
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
durationMs: result.durationMs,
|
|
145
|
+
ok: false,
|
|
146
|
+
...(result.error ? { error: result.error } : {}),
|
|
147
|
+
retries: result.retries,
|
|
148
|
+
});
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function recordTraceEntry(ctx, action, entry) {
|
|
152
|
+
if (!(ctx.trace && ctx.sessionId)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const meta = ctx._traceMeta;
|
|
156
|
+
ctx.trace.record(ctx.sessionId, {
|
|
157
|
+
action,
|
|
158
|
+
timestamp: entry.timestamp,
|
|
159
|
+
durationMs: entry.durationMs,
|
|
160
|
+
ok: entry.ok,
|
|
161
|
+
...(entry.error ? { error: entry.error } : {}),
|
|
162
|
+
retries: entry.retries,
|
|
163
|
+
...(meta?.selectorResolved ? { selectorResolved: meta.selectorResolved } : {}),
|
|
164
|
+
...(meta?.eventsDispatched?.length ? { eventsDispatched: meta.eventsDispatched } : {}),
|
|
165
|
+
...(meta?.waitsPerformed?.length ? { waitsPerformed: meta.waitsPerformed } : {}),
|
|
166
|
+
...(meta?.assertionsChecked?.length ? { assertionsChecked: meta.assertionsChecked } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function backoff(attempt) {
|
|
170
|
+
const base = Math.min(100 * 2 ** attempt, 2000);
|
|
171
|
+
const jitter = Math.floor(Math.random() * 500);
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, base + jitter);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function captureFailureScreenshot(ctx, actionName) {
|
|
177
|
+
if (!ctx.screenshotDir) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
181
|
+
const { join } = await import("node:path");
|
|
182
|
+
mkdirSync(ctx.screenshotDir, { recursive: true });
|
|
183
|
+
const filename = `${Date.now()}-${actionName}-failure.png`;
|
|
184
|
+
const filepath = join(ctx.screenshotDir, filename);
|
|
185
|
+
const buffer = await ctx.page.screenshot();
|
|
186
|
+
writeFileSync(filepath, buffer);
|
|
187
|
+
return filepath;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/actions/action.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative postcondition assertion factories.
|
|
3
|
+
*
|
|
4
|
+
* Each function returns a `(ctx: ActionContext) => Promise<boolean>` compatible
|
|
5
|
+
* with ActionOptions.postcondition. Compose with `allOf()` for multiple checks.
|
|
6
|
+
*/
|
|
7
|
+
import type { Selector } from "../selectors/strategy.js";
|
|
8
|
+
import type { ActionContext } from "./action.js";
|
|
9
|
+
export type AssertionCheck = (ctx: ActionContext) => Promise<boolean>;
|
|
10
|
+
export declare function assertUrlContains(substring: string): AssertionCheck;
|
|
11
|
+
export declare function assertElementVisible(selector: Selector): AssertionCheck;
|
|
12
|
+
export declare function assertElementText(selector: Selector, expected: string | RegExp): AssertionCheck;
|
|
13
|
+
export declare function assertElementGone(selector: Selector): AssertionCheck;
|
|
14
|
+
export declare function allOf(...checks: AssertionCheck[]): AssertionCheck;
|
|
15
|
+
//# sourceMappingURL=assertions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../../src/actions/assertions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAYtE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAKnE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAUvE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAiB/F;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAYpE;AAED,wBAAgB,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,GAAG,cAAc,CASjE"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative postcondition assertion factories.
|
|
3
|
+
*
|
|
4
|
+
* Each function returns a `(ctx: ActionContext) => Promise<boolean>` compatible
|
|
5
|
+
* with ActionOptions.postcondition. Compose with `allOf()` for multiple checks.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveSelector } from "../selectors/strategy.js";
|
|
8
|
+
function recordAssertion(ctx, label) {
|
|
9
|
+
if (!ctx._traceMeta) {
|
|
10
|
+
ctx._traceMeta = {};
|
|
11
|
+
}
|
|
12
|
+
if (!ctx._traceMeta.assertionsChecked) {
|
|
13
|
+
ctx._traceMeta.assertionsChecked = [];
|
|
14
|
+
}
|
|
15
|
+
ctx._traceMeta.assertionsChecked.push(label);
|
|
16
|
+
}
|
|
17
|
+
export function assertUrlContains(substring) {
|
|
18
|
+
return async (ctx) => {
|
|
19
|
+
recordAssertion(ctx, `urlContains:${substring}`);
|
|
20
|
+
return ctx.page.url().includes(substring);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function assertElementVisible(selector) {
|
|
24
|
+
return async (ctx) => {
|
|
25
|
+
recordAssertion(ctx, "elementVisible");
|
|
26
|
+
try {
|
|
27
|
+
const locator = resolveSelector(ctx.page, selector).first();
|
|
28
|
+
return await locator.isVisible({ timeout: 2000 });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function assertElementText(selector, expected) {
|
|
36
|
+
return async (ctx) => {
|
|
37
|
+
recordAssertion(ctx, typeof expected === "string" ? `elementText:${expected}` : `elementText:${expected.source}`);
|
|
38
|
+
try {
|
|
39
|
+
const locator = resolveSelector(ctx.page, selector).first();
|
|
40
|
+
const text = await locator.innerText({ timeout: 2000 });
|
|
41
|
+
if (typeof expected === "string") {
|
|
42
|
+
return text.trim() === expected;
|
|
43
|
+
}
|
|
44
|
+
return expected.test(text.trim());
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function assertElementGone(selector) {
|
|
52
|
+
return async (ctx) => {
|
|
53
|
+
recordAssertion(ctx, "elementGone");
|
|
54
|
+
try {
|
|
55
|
+
const locator = resolveSelector(ctx.page, selector).first();
|
|
56
|
+
const visible = await locator.isVisible({ timeout: 2000 });
|
|
57
|
+
return !visible;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Element not found at all → gone
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function allOf(...checks) {
|
|
66
|
+
return async (ctx) => {
|
|
67
|
+
for (const check of checks) {
|
|
68
|
+
if (!(await check(ctx))) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=assertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.js","sourceRoot":"","sources":["../../src/actions/assertions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-based structured extraction with provenance tracking.
|
|
3
|
+
*
|
|
4
|
+
* Uses TypeBox schemas to define the expected shape. Each extracted item
|
|
5
|
+
* includes provenance metadata showing which DOM node produced it.
|
|
6
|
+
*/
|
|
7
|
+
import type { TObject, TProperties } from "@sinclair/typebox";
|
|
8
|
+
import type { Selector } from "../selectors/strategy.js";
|
|
9
|
+
import type { ActionContext, ActionOptions, ActionResult } from "./action.js";
|
|
10
|
+
export interface ItemProvenance {
|
|
11
|
+
index: number;
|
|
12
|
+
tagName: string;
|
|
13
|
+
id: string;
|
|
14
|
+
className: string;
|
|
15
|
+
strategy: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExtractionResult<T> {
|
|
18
|
+
data: T[];
|
|
19
|
+
provenance: ItemProvenance[];
|
|
20
|
+
}
|
|
21
|
+
export interface ExtractStructuredOptions extends ActionOptions {
|
|
22
|
+
/** Maximum number of items to extract. Default: unlimited. */
|
|
23
|
+
limit?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function extractStructured<P extends TProperties>(ctx: ActionContext, selector: Selector, schema: TObject<P>, opts?: ExtractStructuredOptions): Promise<ActionResult<ExtractionResult<Record<string, unknown>>>>;
|
|
26
|
+
//# sourceMappingURL=extract-structured.d.ts.map
|