@opencraw/mcp 0.1.45 → 0.1.48
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 +28 -1
- package/dist/diff-tool/diff-tool.handler.d.ts +42 -0
- package/dist/diff-tool/index.d.ts +3 -0
- package/dist/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/docs-tool/index.d.ts +3 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.esm.js +517 -1
- package/dist/list-tool/index.d.ts +3 -0
- package/dist/list-tool/list-recipes-tool.handler.d.ts +32 -0
- package/dist/main.d.ts +7 -0
- package/dist/probe-tool/index.d.ts +2 -0
- package/dist/probe-tool/probe-tool.handler.d.ts +27 -0
- package/dist/recipe-source/index.d.ts +3 -0
- package/dist/recipe-source/recipe-source.mapper.d.ts +21 -0
- package/dist/run-tool/index.d.ts +3 -0
- package/dist/run-tool/run-tool.handler.d.ts +51 -0
- package/dist/server/create-server.use-case.d.ts +11 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/src/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/src/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/src/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/src/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/src/docs-tool/index.d.ts +3 -0
- package/dist/src/server/create-server.use-case.d.ts +1 -1
- package/dist/src/studio-tool/index.d.ts +2 -0
- package/dist/src/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/src/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/src/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/studio-tool/index.d.ts +2 -0
- package/dist/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/validate-tool/index.d.ts +3 -0
- package/dist/validate-tool/validate-tool.handler.d.ts +31 -0
- package/docs/how-it-works/01-recipes.md +698 -0
- package/docs/how-it-works/02-page-steps.md +672 -0
- package/docs/how-it-works/03-data-steps.md +813 -0
- package/docs/how-it-works/04-flow-steps.md +738 -0
- package/docs/how-it-works/05-templates-and-scope.md +428 -0
- package/docs/how-it-works/06-documents.md +1413 -0
- package/docs/how-it-works/07-mapping-and-transforms.md +699 -0
- package/docs/how-it-works/08-policies.md +405 -0
- package/docs/how-it-works/09-running.md +804 -0
- package/docs/how-it-works/README.md +225 -0
- package/docs/recipes/README.md +22 -0
- package/docs/recipes/access.md +440 -0
- package/docs/recipes/authoring.md +1503 -0
- package/docs/recipes/azure-durable.md +180 -0
- package/docs/recipes/captcha.md +204 -0
- package/docs/recipes/movies-example.md +12 -0
- package/docs/recipes/programmatic-authoring.md +101 -0
- package/docs/recipes/quick-guide.md +157 -0
- package/docs/recipes/vehicles-example.md +9 -0
- package/docs/recipes/worker-mode.md +221 -0
- package/docs/studio/manual.md +314 -0
- package/docs/studio/pdf-agreements.md +150 -0
- package/package.json +29 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Worked example: vehicle configurations
|
|
6
|
+
|
|
7
|
+
This write-up now lives with its example: [Every version and configuration of one model: BYD UK and Kia UK](../../examples/vehicles/README.md#how-the-recipes-were-written). Finding the data in script-heavy configurators, the BYD and Kia routes, and the trace.
|
|
8
|
+
|
|
9
|
+
Every example is listed in [examples/README.md](../../examples/README.md).
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Worker mode: a pool of windows working through a queue
|
|
6
|
+
|
|
7
|
+
`crawler.run` crawls a recipe set and stops. **Worker mode** is for long crawls of one site with thousands of
|
|
8
|
+
items: one report per month × region × filter set, fed from a queue that another process fills. The pool:
|
|
9
|
+
|
|
10
|
+
- **keeps every window busy.** A window that finishes an item takes the next one at once. There are no
|
|
11
|
+
batches, and no window waits for the others;
|
|
12
|
+
- **keeps each window's page between items.** A window remembers what its page already shows (the report
|
|
13
|
+
type, the axes, the region picked) and an item only changes what differs, then submits;
|
|
14
|
+
- **matches the site's health.** It adds a window while items succeed and backs off when they fail. It never
|
|
15
|
+
takes a window away from an item it is running.
|
|
16
|
+
|
|
17
|
+
It is a restaurant floor. Waiters take the next order as soon as their hands are free, and leave the
|
|
18
|
+
cutlery on the table between guests. When the kitchen falls behind, the manager sends waiters home as they
|
|
19
|
+
finish their tables. The manager never pulls a plate from under a guest.
|
|
20
|
+
|
|
21
|
+
## 1. The recipe
|
|
22
|
+
|
|
23
|
+
A worker recipe is an ordinary input recipe. Two additions tell the window what it may keep:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"kind": "input", "id": "report", "output": "row", "mode": "web",
|
|
28
|
+
"vars": { "state": "DL", "rto": "", "group": "" },
|
|
29
|
+
"start": [{ "url": "https://stats.example.gov/report" }],
|
|
30
|
+
"window": { "check": "#axis", "maxItems": 200 },
|
|
31
|
+
"steps": [
|
|
32
|
+
{ "type": "goto", "url": "{{ start.url }}", "keep": true },
|
|
33
|
+
{ "type": "select", "selector": "#axis", "value": "maker", "keep": true },
|
|
34
|
+
{ "type": "select", "selector": "#state", "values": ["{{ vars.state }}"], "force": true, "keep": true },
|
|
35
|
+
{ "type": "select", "selector": "#rto", "values": ["{{ split(vars.rto) }}"], "force": true, "clear": true, "keep": true },
|
|
36
|
+
{ "type": "select", "selector": "#group", "values": ["{{ split(vars.group) }}"], "force": true, "clear": true, "keep": true },
|
|
37
|
+
{ "type": "click", "selector": "#apply" },
|
|
38
|
+
{ "type": "wait", "selector": "#result .row" },
|
|
39
|
+
{ "type": "extract", "id": "rows", "selector": "#result .row", "kind": "css", "many": true },
|
|
40
|
+
{ "type": "forEach", "over": "rows", "as": "row", "emit": true, "steps": [] }
|
|
41
|
+
],
|
|
42
|
+
"mapping": { "row": { "from": "row" } }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**`keep: true`** marks a page action the window may skip:
|
|
47
|
+
- The window remembers each kept step as it last ran it, with every template rendered (`#state` set to `DL`).
|
|
48
|
+
- On the next item, a kept step that would do the same is skipped (`step:kept` in the trace).
|
|
49
|
+
- A kept step that runs again, because its value changed, makes the window forget **the kept steps after it**.
|
|
50
|
+
A change cascades: a new state reloads the RTO list, so the RTO pick must run again even when its value is
|
|
51
|
+
the one the window holds.
|
|
52
|
+
- Keep is for top-level page actions only (`goto`, `click`, `fill`, `press`, `select`, `scroll`, `wait`,
|
|
53
|
+
`evaluate`), and never on a step with an `id`: a skipped step binds nothing, so its value would be unset
|
|
54
|
+
for the next item. Loading refuses both.
|
|
55
|
+
- Steps without `keep` (Apply, the wait, the extract) run for every item.
|
|
56
|
+
|
|
57
|
+
**`clear: true`** on a `select` is not optional on a reused page. A blank filter var normally leaves the
|
|
58
|
+
control alone. On a reused page, the control would then keep the last item's pick, and item 2 ("every
|
|
59
|
+
group") would silently report item 1's group. With `clear`, blank values empty the control.
|
|
60
|
+
|
|
61
|
+
**`window.check`** is an element a reused page must still show before an item runs on it. A session that
|
|
62
|
+
expired or a page that went elsewhere fails the check, and the window is replaced instead of failing the
|
|
63
|
+
item. **`window.maxItems`** replaces a window after that many items anyway: memory, and server state that
|
|
64
|
+
grows stale.
|
|
65
|
+
|
|
66
|
+
The same recipe runs as it is with `crawler.run` or the CLI: without a window, `keep` changes nothing.
|
|
67
|
+
|
|
68
|
+
## 2. The work source
|
|
69
|
+
|
|
70
|
+
The pool asks a **work source** for items, from several windows at once, and tells it how each ended:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
interface WorkItem { id: string, vars: Record<string, string | number | boolean>, recipe?: string }
|
|
74
|
+
interface WorkSource {
|
|
75
|
+
next (options?: { signal }): Promise<WorkItem | undefined> // may wait (a refill); undefined = no more work
|
|
76
|
+
done? (item, report, records): Promise<void> // succeeded: its records are written, and given here too
|
|
77
|
+
failed? (item, report, outcome): Promise<void> // 'failure' or 'neutral'; nothing of it was written
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `workFrom(items)` turns a plain list into a source.
|
|
82
|
+
- A real queue (a table with claims and leases, priorities, a local buffer refilled when it runs low) is
|
|
83
|
+
the source's business. `next` returns from the buffer and refills it; `done` marks the item consumed;
|
|
84
|
+
`failed` counts a retry (`failure`) or puts it back as it was (`neutral`).
|
|
85
|
+
- `item.id` is on every event of the item, on its report (`item`) and on its records (`record.source.item`).
|
|
86
|
+
- `item.recipe` picks the input recipe when the set has several.
|
|
87
|
+
|
|
88
|
+
### Pushing items: the inbox
|
|
89
|
+
|
|
90
|
+
Some callers don't own a queue: they receive items one at a time (an HTTP request each, a message handler)
|
|
91
|
+
and want each item's result back where they asked for it. `createWorkInbox()` turns pushes into a source:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createWorkInbox } from '@opencraw/core'
|
|
95
|
+
|
|
96
|
+
const inbox = createWorkInbox()
|
|
97
|
+
const working = crawler.work(recipes, inbox.source, { windows: { min: 1, max: 8 } })
|
|
98
|
+
working.catch(error => inbox.abort(error)) // work itself failed: reject what is still waiting
|
|
99
|
+
|
|
100
|
+
const result = await inbox.submit({ id: 'r-42', vars: { state: 'DL' } })
|
|
101
|
+
// { outcome: 'success', report, records } | { outcome: 'failure' | 'neutral', report }
|
|
102
|
+
|
|
103
|
+
inbox.close() // no new items; queued and running ones finish
|
|
104
|
+
const report = await working // then work() ends with its report
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Items first come, first served; windows last come, first served.** The window that just finished takes
|
|
108
|
+
the next item while its page is warm. One that has waited long stays idle and, with `windows.idle`, retires.
|
|
109
|
+
- **One result per item.** `submit` resolves when that item ends, with its own records only. `neutral` is
|
|
110
|
+
returned, not retried: the caller decides whether to submit it again.
|
|
111
|
+
- **No double runs.** Submitting an id that is queued or running returns that run's result. Once it has
|
|
112
|
+
ended, the same id runs again.
|
|
113
|
+
- **Demand.** `inbox.idle` counts windows waiting for an item, `inbox.queued` items waiting for a window,
|
|
114
|
+
`inbox.running` items in progress. Keep submitting while `idle` is above zero, or the pool can't grow: it
|
|
115
|
+
only keeps as many windows busy as the caller keeps items in flight.
|
|
116
|
+
- **Cancel before start.** `submit(item, { signal })` takes the item back while it is queued; a running item
|
|
117
|
+
is not interrupted.
|
|
118
|
+
- `close()` refuses new items; `abort(reason)` also rejects every item not finished yet.
|
|
119
|
+
|
|
120
|
+
**Records are held until the item succeeds.** Mapped records wait in memory. De-duplication, `resume` and
|
|
121
|
+
the sink see them only when the whole item succeeded. A failed item therefore writes nothing and leaves no
|
|
122
|
+
key behind, so running it again later is safe. `done` receives the records, for a source that files one
|
|
123
|
+
document per item.
|
|
124
|
+
|
|
125
|
+
**De-duplication is per item.** A key seen twice within one item is a duplicate. The same key in another item
|
|
126
|
+
is that item's record: two state reports that both list the same maker each keep their row. This is the
|
|
127
|
+
`recipe` scope, since each item is one run of its recipe. Pass `dedupe: 'run'` to `createCrawler` to drop a
|
|
128
|
+
key for the rest of the `work` call, or `'off'` to keep everything.
|
|
129
|
+
|
|
130
|
+
## 3. Running it
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { createCrawler, loadRecipeSet, jsonLinesSink } from '@opencraw/core'
|
|
134
|
+
|
|
135
|
+
const recipes = await loadRecipeSet({ output: 'row.output.json', inputs: ['report.input.json'] })
|
|
136
|
+
const crawler = createCrawler({ sink: jsonLinesSink('rows.jsonl'), captchaSolvers: [reader], onEvent })
|
|
137
|
+
try {
|
|
138
|
+
const report = await crawler.work(recipes, source, {
|
|
139
|
+
windows: { min: 5, max: 20, start: 12, grow: { after: 10 }, shrink: 'one', restart: { after: 20 } },
|
|
140
|
+
})
|
|
141
|
+
// report: { items: { success, failure, neutral }, records, windows: { start, peak, final }, restarts, sink, durationMs }
|
|
142
|
+
} finally {
|
|
143
|
+
await crawler.close()
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
One `run` or `work` at a time per crawler: they share its sink.
|
|
148
|
+
|
|
149
|
+
## 4. How many windows
|
|
150
|
+
|
|
151
|
+
| Setting | Default | What it does |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `windows: 4` | 1 | A fixed pool of four. |
|
|
154
|
+
| `min`, `max` | 1, `min` (or `start` when higher) | The bounds. |
|
|
155
|
+
| `start` | `min` | Where the pool starts. |
|
|
156
|
+
| `grow.after` | 10 | One more window after this many successes in a row, counted only while every window is busy: with a window waiting for work, another would only wait too. |
|
|
157
|
+
| `shrink` | `half` | On a failure: half the windows (`half`) or one fewer (`one`). |
|
|
158
|
+
| `restart.after` | never | After this many failures in a row, the browser is relaunched and the pool goes back to `min`. |
|
|
159
|
+
| `idle.afterMs` | never | A window that has waited this long for an item retires, never below `min`. For push-fed sources (the inbox), whose callers slow down. |
|
|
160
|
+
|
|
161
|
+
What makes the pool safe to leave running overnight:
|
|
162
|
+
|
|
163
|
+
- **A window leaves only between items.** A shrink lowers the number the pool aims for. The windows above it
|
|
164
|
+
leave as they finish their current item; the window whose item failed is usually the first. No item is
|
|
165
|
+
ever cut off by a shrink, so one failure never causes another.
|
|
166
|
+
- **One bad moment shrinks the pool once.** When the site hiccups, every item in flight fails at about the
|
|
167
|
+
same time. Only failures of items that started *after* the last shrink shrink it again. The others count
|
|
168
|
+
towards a restart, but the shrink has already answered them. TCP treats a burst of losses the same way.
|
|
169
|
+
- **A failed item gets a fresh window.** Its page is in a state nobody knows: the window is closed and the
|
|
170
|
+
lane opens a new one for its next item (`window:open fresh`).
|
|
171
|
+
- **A restart waits for every running item to finish.** New items wait for the relaunch; windows opened in
|
|
172
|
+
the old browser are replaced when their next item comes. After a restart the pool is back at `min` and
|
|
173
|
+
grows again from there.
|
|
174
|
+
- **An idle window goes, then comes back when needed.** With `idle.afterMs`, a window that waits that long for
|
|
175
|
+
an item retires through the same path as a shrink (`window:close idle`, `windows:change`), never below
|
|
176
|
+
`min`. Successes grow the pool again. The pool passes `next` a signal that aborts when the window retires;
|
|
177
|
+
the inbox takes the wait back. A source that can't may still return an item for it later: that item gets a
|
|
178
|
+
window of its own (`window:open late`), so nothing is lost.
|
|
179
|
+
- **A browser that dies is relaunched.** Items that were running end as `neutral` (the source gets them back
|
|
180
|
+
untouched), and the next window launches a new browser.
|
|
181
|
+
|
|
182
|
+
**How an item ended** (`classify`, overridable):
|
|
183
|
+
|
|
184
|
+
| Outcome | Default for | Effect |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `success` | no error | Counts towards growing; records written; `done`. |
|
|
187
|
+
| `failure` | a block, retries used up (HTTP errors, timeouts, connection failures), a step that failed | Shrinks the pool; fresh window; `failed(…, 'failure')`. |
|
|
188
|
+
| `neutral` | a captcha the solver could not get past; the browser closed under the item | The pool does not move; fresh window; `failed(…, 'neutral')`: put it back as it was. |
|
|
189
|
+
|
|
190
|
+
`report.errorKind` says why a run stopped (`captcha`, `blocked`, `browser`, `http`, `timeout`, `network`,
|
|
191
|
+
`step`, `mapping`, `host`, `error`). Write your own `classify` from it, for instance to treat `step` failures (a
|
|
192
|
+
selector that matched nothing: a recipe problem, not the site's health) as `neutral`.
|
|
193
|
+
|
|
194
|
+
## 5. Watching it
|
|
195
|
+
|
|
196
|
+
Every event of a window carries `window` and `item`, and `traceLine` prefixes its lines with `[w3]`:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
[w1] ⧉ window opened (start)
|
|
200
|
+
[w1] ▶ report [state=DL, rto=DL53, group=Bus] (web)
|
|
201
|
+
[w1] ≡ steps.0 goto kept
|
|
202
|
+
[w1] ≡ steps.1 select kept
|
|
203
|
+
[w1] · steps.3 select 812 ms
|
|
204
|
+
[w1] ■ report [state=DL, rto=DL53, group=Bus]: 12 emitted, 0 rejected, 0 duplicates, 1 pages, 4213 ms
|
|
205
|
+
[w1] ✓ item DL-DL53-Bus success, 4214 ms
|
|
206
|
+
[w1] ⇅ windows 12 → 13: 10 successes in a row
|
|
207
|
+
[w13] ⧉ window opened (grow)
|
|
208
|
+
[w4] ✖ item GA-GA1-Car failure, 30211 ms: step steps.6 (wait) failed: Timeout 30000ms exceeded
|
|
209
|
+
[w4] ⇅ windows 13 → 12: an item failed
|
|
210
|
+
[w4] ⧉ window closed (fresh)
|
|
211
|
+
[w9] ⧉ window closed (retire)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 6. Limits
|
|
215
|
+
|
|
216
|
+
- Kept steps are the top-level list's. A page whose state cannot be read back from what the recipe did
|
|
217
|
+
(a wizard that moves forward only) is not a good fit: leave `keep` off and each item redoes its steps on
|
|
218
|
+
the same window, which still saves the context and the browser start.
|
|
219
|
+
- A window runs one recipe. An item for another recipe of the set replaces the window.
|
|
220
|
+
- The pool lives in one process. Several machines each run their own pool; claiming items so that no two
|
|
221
|
+
machines take the same one is the source's job.
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# The OpenCraw Studio manual
|
|
6
|
+
|
|
7
|
+
Studio is a visual editor for recipes: open a site or a document, click the data you want, and get a
|
|
8
|
+
recipe that runs. This guide builds one, start to finish — [books.toscrape.com](https://books.toscrape.com),
|
|
9
|
+
the same site [How OpenCraw works](../how-it-works/README.md) walks through, a site built for scraping
|
|
10
|
+
practice — without hand-writing a line of JSON. By the end you will have a working `input`/`output` recipe
|
|
11
|
+
pair and know your way around every tab.
|
|
12
|
+
|
|
13
|
+
If you would rather write recipes by hand, [`docs/recipes/authoring.md`](../recipes/authoring.md) is the
|
|
14
|
+
full reference; this guide is the other way in.
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
1. [Install and start](#1-install-and-start)
|
|
19
|
+
2. [Opening a workspace](#2-opening-a-workspace)
|
|
20
|
+
3. [The shell](#3-the-shell)
|
|
21
|
+
4. [The Steps tab: picking on a live page](#4-the-steps-tab-picking-on-a-live-page)
|
|
22
|
+
5. [The Record tab: fields and mapping](#5-the-record-tab-fields-and-mapping)
|
|
23
|
+
6. [Running a sample](#6-running-a-sample)
|
|
24
|
+
7. [The Inspect panel: data that isn't on the page](#7-the-inspect-panel-data-that-isnt-on-the-page)
|
|
25
|
+
8. [Document canvases](#8-document-canvases)
|
|
26
|
+
9. [Recording a login](#9-recording-a-login)
|
|
27
|
+
10. [The JSON tab](#10-the-json-tab)
|
|
28
|
+
11. [Tips and troubleshooting](#11-tips-and-troubleshooting)
|
|
29
|
+
|
|
30
|
+
## 1. Install and start
|
|
31
|
+
|
|
32
|
+
Studio is its own package, kept separate from the engine so a plain crawl never pulls in a browser UI:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install -g @opencraw/cli @opencraw/studio
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start it on a folder of recipes (or an empty folder — see [§2](#2-opening-a-workspace)):
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
opencraw studio recipes/
|
|
42
|
+
# or, without the cli:
|
|
43
|
+
npx @opencraw/studio recipes/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Either prints the URL to open, with a random access token already in it:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
OpenCraw Studio: http://127.0.0.1:52341/?token=8f2a1c9e4b7d3f6a0e5c8b1d2a4f7e9c
|
|
50
|
+
Workspace: /home/you/project/recipes
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Open that URL in a browser. The server only listens on `127.0.0.1` and only answers requests carrying that
|
|
54
|
+
token, so nothing else on your machine (or network) can reach it — see
|
|
55
|
+
[the plan's §8](../research/recipe-studio.md#8-security) for the full list of what it does and doesn't do.
|
|
56
|
+
Leave the terminal running; `Ctrl+C` stops the server. If you started it with `opencraw studio` and get
|
|
57
|
+
"the studio is not installed", `npm install @opencraw/studio` — the cli keeps it as an optional dependency
|
|
58
|
+
so a plain crawl install stays light.
|
|
59
|
+
|
|
60
|
+
## 2. Opening a workspace
|
|
61
|
+
|
|
62
|
+
The toolbar has three jobs, and each has one place:
|
|
63
|
+
|
|
64
|
+
| Button | Question it answers | What is in it |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| **Recipe ▾** | Which recipe am I editing? | the recipes in the open folder, **New recipe…**, **Open folder…** |
|
|
67
|
+
| **Load ▾** | What does it read? | **Document…**, **Web page (URL)…** |
|
|
68
|
+
| **Run sample** / **Stop** | Does it work? | runs the open recipe |
|
|
69
|
+
|
|
70
|
+
Next to them, **All changes saved** says the edits you make are written as you make them (the Steps tab, loading, creating a
|
|
71
|
+
recipe); it reads **Saving…** while a write is on its way. The **Record** and **JSON** tabs keep their edits
|
|
72
|
+
as a draft until you press their own **Save**, so a change there is not written before that.
|
|
73
|
+
|
|
74
|
+
**A recipe is an input and its output.** The input recipe (`<id>.ocw-in.json`) names its output recipe
|
|
75
|
+
(`<id>.ocw-out.json`, by the input's `output` id). Opening a recipe from **Recipe ▾** opens both: Steps edits the
|
|
76
|
+
input, Record edits the output and the mapping between them. You never open the pair separately.
|
|
77
|
+
|
|
78
|
+
**Opening a folder.** **Recipe ▾ → Open folder…** takes any folder — one that already has `.ocw-in.json` /
|
|
79
|
+
`.ocw-out.json` files in it, or a brand new one. It opens the operating system's folder dialog (the desktop app's native one, or the VS Code extension's own
|
|
80
|
+
dialog when Studio runs from there); on a machine with no dialog, a box to type the path in appears instead. Starting Studio
|
|
81
|
+
with `opencraw studio <folder>` opens that folder already.
|
|
82
|
+
|
|
83
|
+
**Starting from nothing?** **Recipe ▾ → New recipe…**: type an id (lowercase letters, digits and hyphens — the
|
|
84
|
+
same pattern every recipe id follows), optionally a start URL, and **Create**. That writes a minimal, valid
|
|
85
|
+
pair — `<id>.ocw-in.json` and `<id>.ocw-out.json` — into the open folder and selects it: a `web` recipe with one
|
|
86
|
+
`goto` step, an `emit`, and no fields mapped yet. Everything from here builds on top of that.
|
|
87
|
+
|
|
88
|
+
**Loading what it reads.** A page or a document is the sample a recipe is built against, so **Load ▾** puts it
|
|
89
|
+
into the recipe that is open:
|
|
90
|
+
|
|
91
|
+
- **Web page (URL)…** with a web recipe open replaces its start page; the steps and the mapping stay.
|
|
92
|
+
- **Document…** (or dropping a PDF, spreadsheet, CSV, PowerPoint, Word, Markdown, JSON, YAML or XML file
|
|
93
|
+
anywhere on the window) with a document recipe open replaces the file it reads. The studio copies the file
|
|
94
|
+
into the workspace folder, next to the recipes, so the folder stays self-contained.
|
|
95
|
+
- If the open recipe reads the other kind of thing — a document into a web recipe, or a page into a document
|
|
96
|
+
recipe — or no recipe is open, Load starts a new recipe instead and leaves the open one untouched. A bar
|
|
97
|
+
under the toolbar says which happened. New recipes are named after the file (`Q3 Report.pdf` → `q3-report`;
|
|
98
|
+
a second one becomes `q3-report-2`) or the page's host, and a document opens on its own canvas
|
|
99
|
+
([§8](#8-document-canvases)).
|
|
100
|
+
- No folder open yet? Load needs one to write into: open one first.
|
|
101
|
+
|
|
102
|
+
For this guide's example, open any folder and create a recipe with the id `books`.
|
|
103
|
+
|
|
104
|
+
## 3. The shell
|
|
105
|
+
|
|
106
|
+
The window splits into three: the **content pane** on the left shows what the engine actually sees at the
|
|
107
|
+
recipe's start point (not a live, interactive copy of the site — see [§4](#4-the-steps-tab-picking-on-a-live-page)
|
|
108
|
+
for why); the **editor pane** on the right has three tabs, **Steps**, **Record** and **JSON**; a **preview
|
|
109
|
+
strip** runs along the bottom with the last sample's **Records**, its **Trace**, and (once something is
|
|
110
|
+
missing) a **Why?** explanation. Both the content/editor split and the workspace/preview split are
|
|
111
|
+
draggable.
|
|
112
|
+
|
|
113
|
+
The toolbar's recipe picker lists every `input` recipe in the open folder; **Run sample** crawls the
|
|
114
|
+
selected one with a small budget (a handful of pages and records) and streams the result into the preview
|
|
115
|
+
strip, live.
|
|
116
|
+
|
|
117
|
+
## 4. The Steps tab: picking on a live page
|
|
118
|
+
|
|
119
|
+
Open the **Steps** tab. A new recipe has one card: **Go to `https://example.com`** (the placeholder start
|
|
120
|
+
URL — edit its address field to `https://books.toscrape.com/catalogue/category/books/mystery_3/index.html`,
|
|
121
|
+
the Mystery category). A second card, **Emit**, closes the loop for now; you'll move it once there's
|
|
122
|
+
something to loop over.
|
|
123
|
+
|
|
124
|
+
The content pane does not load the live site — it loads a **snapshot the engine itself took** by running
|
|
125
|
+
the recipe up to that step (a real Playwright page for a `web` recipe, a real HTTP fetch for `api`), with
|
|
126
|
+
scripts stripped and every element marked with a stable id. That is deliberate: it is exactly what the
|
|
127
|
+
engine will read, so a pick here is a pick the engine reproduces exactly, byte for byte, every time it
|
|
128
|
+
runs — not a "close enough" guess at what you saw in a live browser.
|
|
129
|
+
|
|
130
|
+
Click **Read** above the content pane to enter pick mode, then hover over a book's title in the page. The
|
|
131
|
+
element under the cursor outlines, with its candidate CSS selector shown. Click it: a **Read** card appears
|
|
132
|
+
in the Steps outline — *Read `title` from `.product_pod h3 a` (attr: title)*.
|
|
133
|
+
|
|
134
|
+
Now click a **second** book's title. Studio walks up from both clicked elements to the nearest ancestors
|
|
135
|
+
that repeat with the same tag and class (`article.product_pod`), and rewrites the two single reads into a
|
|
136
|
+
safe loop by construction: an `extract` of every matching item, a `forEach` over them, and the field read
|
|
137
|
+
`from` each item — never the "wrapper trap" a hand-written selector can fall into (an ancestor that isn't
|
|
138
|
+
actually one-per-item). The outline now shows **Read `books`... → For each `book` in `books` → Read `title`
|
|
139
|
+
from `book`**.
|
|
140
|
+
|
|
141
|
+
Repeat picking inside that loop for a **price** (`.price_color`) and an **availability** (`.availability`)
|
|
142
|
+
on the same two books, each producing another **Read** card nested in the same `forEach`. Move the **Emit**
|
|
143
|
+
card (drag, or the `+` menu's own **Emit**) inside the loop, after the reads, so one record comes out per
|
|
144
|
+
book instead of one for the whole page.
|
|
145
|
+
|
|
146
|
+
A card's **pill** — the small coloured `books`, `book`, `title` labels — is the id the next card downstream
|
|
147
|
+
can read `from`. Hovering a card highlights every matching element in the content pane with a count, so you
|
|
148
|
+
can confirm a selector actually matches what you expect before moving on. The document canvases do the same
|
|
149
|
+
in both directions: a tree node, a region of a PDF page or a slide, a chart, or a cell a step already reads
|
|
150
|
+
is marked with the step's id (a dashed box, a pill), hovering the card fills it in, and hovering the marked
|
|
151
|
+
node, line, text box, chart or cell lights the card up. Container steps (`forEach`,
|
|
152
|
+
`paginate`, `if`) draw as brackets around their children: a card can only use a pill from its own bracket or
|
|
153
|
+
an outer one, so the outline can never let you build a binding error the loader would later reject.
|
|
154
|
+
|
|
155
|
+
The `+` row between any two cards (or at the end of a bracket) offers every step kind: **Go to**, **Read**,
|
|
156
|
+
**Loop**, **Next page**, **If**, **Fill form**, **Click**, **Set**, **Collect**, **Emit**. **Next page**
|
|
157
|
+
wraps everything already in the outline in a `paginate` bracket around a "next" link you then pick the same
|
|
158
|
+
way — turn it into one now around the whole loop, pointing at the category page's "next" link, and every
|
|
159
|
+
page of Mystery books feeds the same reads.
|
|
160
|
+
|
|
161
|
+
Every edit here — a pick, a move, a reorder — writes straight to the recipe's JSON file and re-validates
|
|
162
|
+
against the server's real loader (the same one `opencraw validate` uses): a binding problem shows on the
|
|
163
|
+
card that causes it, in place, not as a wall of text elsewhere.
|
|
164
|
+
|
|
165
|
+
**Hidden elements**: the **Show hidden** switch reveals (greyed) anything the snapshot marked as hidden on
|
|
166
|
+
the live page (`display: none`, zero size) — some sites keep a machine-readable value in a hidden node or a
|
|
167
|
+
`<meta>` tag that is easier to read than the rendered text.
|
|
168
|
+
|
|
169
|
+
## 5. The Record tab: fields and mapping
|
|
170
|
+
|
|
171
|
+
Switch to the **Record** tab. This is the paired **output** recipe: the fields a record actually has, each
|
|
172
|
+
with a type, and the input recipe's **mapping** — which pill each field reads from, and what happens to the
|
|
173
|
+
value on the way in.
|
|
174
|
+
|
|
175
|
+
A strip at the top of the tab lists every value the steps read that no field uses yet, one chip each:
|
|
176
|
+
the pill, the field it would become and a guessed type (`price` becomes a `number`, `availability` a
|
|
177
|
+
`boolean`, a link taken from `href` a `url` made absolute; hover a chip for why). Click a chip's **+** to add
|
|
178
|
+
the field **and** its mapping at once, or **Add all**. Nothing is guessed silently: change a type or a name
|
|
179
|
+
afterwards like any field.
|
|
180
|
+
|
|
181
|
+
A recipe with output fields but no **Emit** step reads values and returns no records, so the tab says so and
|
|
182
|
+
offers **Add Emit step**.
|
|
183
|
+
|
|
184
|
+
Each field row opens with a one-line summary: a coloured **type** badge, the **pill** it reads from (or
|
|
185
|
+
*not mapped*, *generated*) and, once a sample has run, the **value** the selected record got.
|
|
186
|
+
|
|
187
|
+
**Drag to build the output.** Anything you can see can be dropped on the field list to become a field with its
|
|
188
|
+
mapping, typed the way its proposal would be:
|
|
189
|
+
|
|
190
|
+
- a **pill** from a **Steps** card: grab it and hold it over the **Record** tab (it opens after a moment),
|
|
191
|
+
then drop it on the list. Dropped on a field's own source box instead, it only maps that field;
|
|
192
|
+
- a **selection** staged in the content pane (a cell, a text box, a region of a PDF, a node of the page): drag
|
|
193
|
+
its chip onto the **Record** tab and the list. The studio adds the step to the recipe, as dropping it on the
|
|
194
|
+
Steps tab does, and the value it reads appears as a new field. The new field is a draft until you **Save**.
|
|
195
|
+
|
|
196
|
+
You can still add a field by hand with **+ field** and drag a **Steps** pill onto its row (or type the pill
|
|
197
|
+
name directly). Mark `title` as the record's **key**, so a duplicate never gets emitted twice.
|
|
198
|
+
|
|
199
|
+
Each field's **transform chain** is a row of small blocks — `trim`, `currency`, `boolean`, and so on
|
|
200
|
+
(the same closed set [§5.1 of the authoring guide](../recipes/authoring.md#51-transforms) documents) — add
|
|
201
|
+
one by clicking the row's own `+`. After a sample has run (see [§6](#6-running-a-sample)), each block
|
|
202
|
+
shows the **real value** at that point, taken from the last run's mapping trace: read `"£47.82"`, after
|
|
203
|
+
`currency` → `{"amount":47.82,"currency":"GBP"}`. A transform that fails on a real value turns red with the
|
|
204
|
+
engine's own reason, right on the block that failed — no guessing which one from a stack trace.
|
|
205
|
+
|
|
206
|
+
## 6. Running a sample
|
|
207
|
+
|
|
208
|
+
Click **Run sample** in the toolbar. This is not a mocked preview: it is the same engine, in the same
|
|
209
|
+
process, running for real against a small budget (a few pages, a handful of records) so it finishes fast
|
|
210
|
+
while you're editing. The **Records** panel at the bottom fills in as they arrive — toggle between a
|
|
211
|
+
**Table** and a compact **JSONL** view, export what you see (JSON/JSONL/CSV), or **Enlarge** it into a full
|
|
212
|
+
window. The **Trace** panel streams the engine's own `traceLine` output live, one line per step, indented by
|
|
213
|
+
nesting — the exact same trace `opencraw run --trace` prints from the terminal.
|
|
214
|
+
|
|
215
|
+
Click an empty cell in the Records table (a field that came out missing) to get **Why?**: a plain-language
|
|
216
|
+
explanation built from the run's own events — which read matched nothing, on which item, and what
|
|
217
|
+
selectors were actually present nearby to try instead. **Stop** ends a run early; recipes with a start point
|
|
218
|
+
that changes each run (pagination, matrix variants) can be safely stopped mid-way without leaving anything
|
|
219
|
+
half-written, since nothing is saved until you click Save.
|
|
220
|
+
|
|
221
|
+
## 7. The Inspect panel: data that isn't on the page
|
|
222
|
+
|
|
223
|
+
Not everything worth reading is visible text. Click **Inspect** above the content pane to open a DOM tree of
|
|
224
|
+
the same snapshot, alongside the rendered view — a browser inspector, but of exactly what the engine reads,
|
|
225
|
+
with hidden nodes greyed the same way the content pane marks them.
|
|
226
|
+
|
|
227
|
+
Two more tabs live here:
|
|
228
|
+
|
|
229
|
+
- **Data in the page**: `<script type="application/ld+json">` blocks, inline JSON state
|
|
230
|
+
(`window.__STATE__ = …`), and `<meta>` tags, listed apart from the DOM tree — click one to pick it as a
|
|
231
|
+
`regex` or `jsonpath` read, often more reliable than a CSS selector on rendered text that a redesign could
|
|
232
|
+
break.
|
|
233
|
+
- **Responses seen**: every JSON response the page fetched while it rendered (the same findings
|
|
234
|
+
`opencraw probe` reports from the terminal). Picking one switches the recipe from `web` mode to `api`
|
|
235
|
+
mode on that endpoint directly — usually faster and steadier than driving a browser at all, when a site's
|
|
236
|
+
data already arrives as clean JSON.
|
|
237
|
+
|
|
238
|
+
Clicking a node in either the DOM tree or a finding is the same pick a click in the content pane would be:
|
|
239
|
+
it lands the same kind of **Read** card in the Steps outline.
|
|
240
|
+
|
|
241
|
+
## 8. Document canvases
|
|
242
|
+
|
|
243
|
+
The editor pane never changes — Steps, Record and JSON work the same regardless of source. Only the content
|
|
244
|
+
pane's **canvas** changes, to fit what it's actually showing:
|
|
245
|
+
|
|
246
|
+
| Source | Canvas | A click gives you |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| A web page, or Word/Markdown (read as HTML) | the snapshot iframe from [§4](#4-the-steps-tab-picking-on-a-live-page) | a CSS selector |
|
|
249
|
+
| JSON, YAML, XML, JSON Lines | a tree — click a value to read it, `[*]` reads every item of a list | a `jsonpath` (or `xpath` for XML); a "next" value offers the `paginate` cursor |
|
|
250
|
+
| PDF | the page rendered with the engine's own detected cells and rows drawn over it | in **Text** mode (the default) the line under the mouse snaps; a click stages it (a wrapped paragraph is staged whole, and a line under a heading stages everything between that heading and the next, as a `section` extract: the chip says what it is tied to, and a box you draw says it is a fixed position that breaks when the layout moves), shift+click extends to another line, dragging stages the box drawn — a chip shows exactly what the engine reads there, with **Add to recipe** (a `region` extract: `page=1 x=72..252 y=640..664`; a `Label: value` line adds a `regex` extract named after its label that reads the value only, wherever it sits on the page) or drag the chip onto the Steps tab. The page is fitted to the pane's width. A worked example, with every step pictured and a narrated video, is [One recipe for two PDFs of one layout](./pdf-agreements.md). In the table modes, the header row gives `selector`, the last row `until`, a column its name |
|
|
251
|
+
| Excel, CSV | a grid: sheet tabs, hidden sheets/rows marked, merged cells shown as one | in **Cell** mode (the default) the cell under the mouse lights up; a click stages it, shift+click (or press on one cell and release on another) stages the rectangle between — the chip shows the values, with **Add to recipe** (a `jsonpath` into the sheet by name and the cell by position: `$.sheets[?(@.name=='Prices')].rows[0][0]`, a rectangle a list) or drag it onto the Steps tab. In the table modes a sheet tab gives `sheet`, the header row `headerRows`, a merged group `fillDown` |
|
|
252
|
+
| PowerPoint | each slide redrawn from its shapes, with tables and charts listed apart | in **Text** mode (the default) the text box under the mouse snaps; a click stages it, shift+click extends, dragging stages the box drawn — the same chip as the PDF's, with **Add to recipe** (a `region` extract: `slide=3 x=60..900 y=30..90`, points from the slide's top-left corner) or drag it onto the Steps tab. In the table modes a table gives `table`/`slide`; a chart a `jsonpath` into its series |
|
|
253
|
+
|
|
254
|
+
Every one of these writes the matching `extract` step the same way a page pick does — the outline, the
|
|
255
|
+
Record tab and the JSON tab don't know or care which canvas produced it.
|
|
256
|
+
|
|
257
|
+
## 9. Recording a login
|
|
258
|
+
|
|
259
|
+
Some data sits behind a login, or needs a click, a search box filled in, or an infinite-scroll load — none
|
|
260
|
+
of which a static snapshot can do. Click **Record** above the content pane: a real, headed browser window
|
|
261
|
+
opens for you to drive by hand. A banner across the top tracks what you've done as you go; every click,
|
|
262
|
+
fill, select and key press you make in that window becomes a step, live.
|
|
263
|
+
|
|
264
|
+
When you're done, **Stop recording**. Two ways to keep what you did:
|
|
265
|
+
|
|
266
|
+
- **Make this the login**: moves the recorded steps into the recipe's `session.bootstrap` — they run once,
|
|
267
|
+
before the crawl proper, to obtain cookies the rest of the recipe reuses. Use this for an actual sign-in.
|
|
268
|
+
- **Keep as steps**: appends them to the recipe's own `steps`, in place, the same as if you'd built them
|
|
269
|
+
with the Steps tab's `+` menu. Use this for something that's part of the crawl itself, not a one-time
|
|
270
|
+
setup (a search box, an infinite-scroll trigger).
|
|
271
|
+
|
|
272
|
+
**Discard** throws the recording away. Either way, the content pane's snapshot is retaken after the
|
|
273
|
+
recording closes, so the next pick reflects whatever state the page ended up in.
|
|
274
|
+
|
|
275
|
+
## 10. The JSON tab
|
|
276
|
+
|
|
277
|
+
The recipe's file, live, in a real code editor (syntax highlighting, multi-cursor with `Ctrl`/`Cmd`-`D`,
|
|
278
|
+
search with `Ctrl`/`Cmd`-`F`) rather than a plain text box — with the server's own validation, from the same
|
|
279
|
+
loader `opencraw validate` uses, shown as inline markers at the exact spot each issue is about. Anything the
|
|
280
|
+
Steps/Record tabs cannot yet express (a construct they don't have a card for) still round-trips here as
|
|
281
|
+
readable JSON — nothing is ever silently dropped for being "too advanced" for the visual editors. Edit here
|
|
282
|
+
and the other tabs pick it up on the next save; edit there and this tab shows exactly what got written,
|
|
283
|
+
so the two never drift out of sync.
|
|
284
|
+
|
|
285
|
+
## 11. Tips and troubleshooting
|
|
286
|
+
|
|
287
|
+
- **"Playwright's Chromium is not downloaded yet"**: the first time a `web` recipe needs a browser and none
|
|
288
|
+
is installed, the studio installs one for you automatically and retries — you'll see a line about it in
|
|
289
|
+
the terminal the studio is running in. If it can't (offline, no permission), it says so with the exact
|
|
290
|
+
command to run yourself: `npx playwright install chromium`.
|
|
291
|
+
- **A pick lands in the wrong place**: a pick always adds to the *end* of the recipe's top-level steps, not
|
|
292
|
+
inside whichever bracket happens to be open in the Steps tab — drag the new card into place afterward, or
|
|
293
|
+
build the loop first (as in [§4](#4-the-steps-tab-picking-on-a-live-page)) and pick from inside it.
|
|
294
|
+
- **Nothing to pick from**: an empty response, a page that needs JavaScript the snapshot didn't wait for, or
|
|
295
|
+
a login wall are all signs to reach for [§7](#7-the-inspect-panel-data-that-isnt-on-the-page)'s "responses
|
|
296
|
+
seen" tab or [§9](#9-recording-a-login)'s recorder before assuming the data isn't reachable at all.
|
|
297
|
+
- **The recipe files are the truth.** Nothing the studio does is hidden state: close it, and every pick,
|
|
298
|
+
every mapping, every transform is sitting in the JSON files exactly as saved. `opencraw validate` and
|
|
299
|
+
`opencraw run` work on them the same as any hand-written recipe.
|
|
300
|
+
|
|
301
|
+
## 12. Letting an agent drive Studio
|
|
302
|
+
|
|
303
|
+
An AI agent connected to OpenCraw's MCP server (see the VS Code extension's *AI agent* view) can work in the Studio you have open, so you watch it
|
|
304
|
+
happen instead of reading a report:
|
|
305
|
+
|
|
306
|
+
- **`studio_status`** tells it whether Studio is running, which folder it has open and what it is sampling.
|
|
307
|
+
- **`studio_open_recipe`** opens a recipe, a folder or a document in your Studio, as if you had followed a link to it.
|
|
308
|
+
- **`studio_pick`** picks a value on a web page for you to see: by CSS selector or by the text it shows. It is staged the way your own click is, with the
|
|
309
|
+
ways to find it to choose from, and the agent can add it to the recipe.
|
|
310
|
+
- **`studio_run_sample`** runs an input recipe as a sample: the records arrive in your Records tab, and the agent gets the counts and the first
|
|
311
|
+
records back.
|
|
312
|
+
|
|
313
|
+
Nothing needs setting up: a running Studio announces itself in `~/.opencraw/studio` (its address and its access token, readable only by you), and the
|
|
314
|
+
agent finds it there. Studio still listens on this machine only. On a PDF, a spreadsheet or a deck the picking is still your click: the agent tells you where.
|