@opencraw/driver 0.1.0 → 0.3.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 +11 -5
- package/dist/index.esm.js +292 -45
- package/dist/main.esm.js +256 -35
- package/dist/src/callout-bridge/client-hooks.client.d.ts +30 -2
- package/dist/src/callout-bridge/index.d.ts +2 -2
- package/dist/src/index.d.ts +8 -4
- package/dist/src/protocol/driver-message.contract.d.ts +243 -2
- package/dist/src/protocol/index.d.ts +2 -2
- package/dist/src/record-files/diff-record-files.use-case.d.ts +10 -0
- package/dist/src/record-files/export-record-files.use-case.d.ts +11 -0
- package/dist/src/record-files/index.d.ts +3 -0
- package/dist/src/session/index.d.ts +1 -1
- package/dist/src/session/run-session.use-case.d.ts +11 -11
- package/dist/src/site-access/index.d.ts +3 -0
- package/dist/src/site-access/resolve-site-access.use-case.d.ts +26 -0
- package/dist/src/site-probe/index.d.ts +2 -0
- package/dist/src/site-probe/probe-site.use-case.d.ts +13 -0
- package/dist/src/work-bridge/client-work-source.client.d.ts +17 -0
- package/dist/src/work-bridge/index.d.ts +2 -0
- package/package.json +3 -3
- package/schemas/driver-protocol.schema.json +566 -1
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ npx opencraw-driver # then write JSON-RPC lines to it
|
|
|
17
17
|
## The conversation
|
|
18
18
|
|
|
19
19
|
```jsonc
|
|
20
|
-
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientName":"opencraw-python","clientVersion":"1.0.0","protocolVersion":"1.
|
|
21
|
-
← {"jsonrpc":"2.0","id":1,"result":{"driverVersion":"0.
|
|
20
|
+
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientName":"opencraw-python","clientVersion":"1.0.0","protocolVersion":"1.2.0"}}
|
|
21
|
+
← {"jsonrpc":"2.0","id":1,"result":{"driverVersion":"0.2.0","protocolVersion":"1.2.0","methods":["initialize","validate","run","work","cancel","probe","diff","export","shutdown"]}}
|
|
22
22
|
→ {"jsonrpc":"2.0","id":2,"method":"run","params":{"paths":["recipes/"],"hooks":["tidy"]}}
|
|
23
23
|
← {"jsonrpc":"2.0","id":2,"result":{"runId":"run-1"}}
|
|
24
24
|
← {"jsonrpc":"2.0","method":"event","params":{"runId":"run-1","event":{"type":"recipe:start",…}}}
|
|
@@ -33,13 +33,19 @@ npx opencraw-driver # then write JSON-RPC lines to it
|
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| `initialize` | client | Must come first. The driver refuses a protocol of another major. |
|
|
35
35
|
| `validate` | client | Loads recipes (`paths` or inline `recipes`) without running them: the inputs that would run, every hook they call, and every problem with its path. |
|
|
36
|
-
| `run` | client | Starts a run and answers its `runId` at once; the crawl goes on in the background. `hooks`
|
|
36
|
+
| `run` | client | Starts a run and answers its `runId` at once; the crawl goes on in the background. `hooks`, `captchaSolvers` and `accessPlugins` name what the client answers; `accessConfig` and `access` pick the access config and profile; `out` writes records to a file instead of sending them. |
|
|
37
|
+
| `work` | client | Starts a worker pool and answers its `runId` at once. Its windows ask the client for items (`work.next`) and keep the steps marked `keep` between them; `windows` is a count or core's `WindowsPolicy`. Takes the same crawler fields as `run`. |
|
|
37
38
|
| `cancel` | client | Stops a run (its crawler is closed); it still ends with `finished`. |
|
|
39
|
+
| `probe` | client | Fetches a page (or renders it, with `browser`) and says where its data lives: the CLI's and MCP's probe, through the same access fields as `run`. |
|
|
40
|
+
| `diff` | client | Compares two runs' JSON Lines files by key: counts, the report a person reads, and the changes up to `limit`. |
|
|
41
|
+
| `export` | client | Writes a run's JSON Lines file in another format (the extension of `out`), with an output recipe's columns. |
|
|
38
42
|
| `shutdown` | client | Closes every run, answers, then exits. Closing stdin does the same. |
|
|
39
43
|
| `event` | driver | Every crawl event, as core reports it. |
|
|
40
44
|
| `record` | driver | Each record, unless the run has `out`. |
|
|
41
|
-
| `finished` | driver | The run is over: its report, or why it could not run. |
|
|
42
|
-
| `
|
|
45
|
+
| `finished` | driver | The run is over: its report (for `work`, the work report), or why it could not run. |
|
|
46
|
+
| `work.next` | driver | A window wants an item: answer `{ "item": { "id", "vars", "recipe"? } }`, holding the answer until there is one, or `{ "item": null }` to end the work. An id already queued or running runs once. |
|
|
47
|
+
| `work.done`, `work.failed` | driver | How an item ended: its report and records, or its outcome (`failure`, `neutral`). |
|
|
48
|
+
| `callout` | driver | A hook, captcha solver or access plugin the client serves (`request.kind`: `hook`, `captcha`, `access`). The request and its answer are core's [callout contract](../core/README.md): `ok`, `error`, or `pending` with `retryAfterMs`, asked again with the same idempotency key. |
|
|
43
49
|
|
|
44
50
|
Every answer comes back before the driver stops, even when a client writes its last requests and closes
|
|
45
51
|
stdin at once. Answers may arrive out of order; match them by `id`.
|
package/dist/index.esm.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// src/server/serve-driver.use-case.ts
|
|
2
|
-
import { once } from "node:events";
|
|
2
|
+
import { once as once2 } from "node:events";
|
|
3
3
|
|
|
4
4
|
// src/protocol/driver-message.contract.ts
|
|
5
5
|
import { calloutRequestSchema, calloutResponseSchema } from "@opencraw/core";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
var PROTOCOL_VERSION = "1.
|
|
7
|
+
var PROTOCOL_VERSION = "1.2.0";
|
|
8
8
|
var recipeObject = z.record(z.string(), z.unknown());
|
|
9
|
+
var coreObject = z.record(z.string(), z.unknown());
|
|
10
|
+
var accessFields = {
|
|
11
|
+
accessConfig: z.string().optional(),
|
|
12
|
+
access: z.string().optional(),
|
|
13
|
+
/** Access plugins the client serves: a profile `{ "kind": "plugin", "name": … }` naming one leases through a `callout` to the client. */
|
|
14
|
+
accessPlugins: z.array(z.string()).optional()
|
|
15
|
+
};
|
|
9
16
|
var recipeSource = {
|
|
10
17
|
paths: z.array(z.string()).min(1).optional(),
|
|
11
18
|
recipes: z.array(recipeObject).min(1).optional()
|
|
@@ -41,17 +48,87 @@ var runParamsSchema = z.strictObject({
|
|
|
41
48
|
parallel: z.number().int().min(1).max(16).optional(),
|
|
42
49
|
browser: z.strictObject({ headless: z.boolean().optional(), executablePath: z.string().optional(), ignoreHTTPSErrors: z.boolean().optional() }).optional(),
|
|
43
50
|
/** The hooks the client answers itself: each becomes a `callout` request to the client when a recipe calls it. */
|
|
44
|
-
hooks: z.array(z.string()).optional()
|
|
51
|
+
hooks: z.array(z.string()).optional(),
|
|
52
|
+
/** Captcha solvers the client serves, by name: a challenge goes to the client as a `captcha` callout. */
|
|
53
|
+
captchaSolvers: z.array(z.string()).optional(),
|
|
54
|
+
...accessFields
|
|
45
55
|
});
|
|
46
56
|
var runResultSchema = z.strictObject({ runId: z.string() });
|
|
57
|
+
var windowsSchema = z.union([z.number().int().min(1).max(64), z.record(z.string(), z.unknown())]);
|
|
58
|
+
var workParamsSchema = z.strictObject({
|
|
59
|
+
...recipeSource,
|
|
60
|
+
only: z.array(z.string()).optional(),
|
|
61
|
+
out: z.string().optional(),
|
|
62
|
+
append: z.boolean().optional(),
|
|
63
|
+
browser: z.strictObject({ headless: z.boolean().optional(), executablePath: z.string().optional(), ignoreHTTPSErrors: z.boolean().optional() }).optional(),
|
|
64
|
+
hooks: z.array(z.string()).optional(),
|
|
65
|
+
captchaSolvers: z.array(z.string()).optional(),
|
|
66
|
+
...accessFields,
|
|
67
|
+
windows: windowsSchema.optional()
|
|
68
|
+
});
|
|
69
|
+
var varValue = z.union([z.string(), z.number(), z.boolean()]);
|
|
70
|
+
var workItemSchema = z.strictObject({
|
|
71
|
+
id: z.string().min(1),
|
|
72
|
+
vars: z.record(z.string(), varValue),
|
|
73
|
+
recipe: z.string().optional()
|
|
74
|
+
});
|
|
47
75
|
var cancelParamsSchema = z.strictObject({ runId: z.string() });
|
|
48
76
|
var cancelResultSchema = z.strictObject({ cancelled: z.boolean() });
|
|
77
|
+
var probeParamsSchema = z.strictObject({
|
|
78
|
+
url: z.string().min(1),
|
|
79
|
+
/** Render the page in Chromium first (for pages built by scripts). */
|
|
80
|
+
browser: z.boolean().optional(),
|
|
81
|
+
browserPath: z.string().optional(),
|
|
82
|
+
insecureTls: z.boolean().optional(),
|
|
83
|
+
userAgent: z.string().optional(),
|
|
84
|
+
...accessFields
|
|
85
|
+
});
|
|
86
|
+
var probeResultSchema = coreObject;
|
|
87
|
+
var diffParamsSchema = z.strictObject({
|
|
88
|
+
/** The earlier run's JSON Lines file. */
|
|
89
|
+
previous: z.string().min(1),
|
|
90
|
+
current: z.string().min(1),
|
|
91
|
+
/** The fields that identify a record; default: each line's `_key`. */
|
|
92
|
+
key: z.array(z.string()).optional(),
|
|
93
|
+
/** Fields left out of the comparison. */
|
|
94
|
+
ignore: z.array(z.string()).optional(),
|
|
95
|
+
/** Changes returned at most (default 200); the counts cover all of them. */
|
|
96
|
+
limit: z.number().int().min(0).optional()
|
|
97
|
+
});
|
|
98
|
+
var diffResultSchema = z.strictObject({
|
|
99
|
+
added: z.number(),
|
|
100
|
+
removed: z.number(),
|
|
101
|
+
changed: z.number(),
|
|
102
|
+
unchanged: z.number(),
|
|
103
|
+
/** Set when the current run lost most of the previous run's records. */
|
|
104
|
+
shrunk: z.strictObject({ previous: z.number(), current: z.number() }).optional(),
|
|
105
|
+
/** The report a person reads, one line per change. */
|
|
106
|
+
summary: z.array(z.string()),
|
|
107
|
+
/** The changes, up to `limit`: each `added`, `removed` or `changed` (with each field's before and after). */
|
|
108
|
+
changes: z.array(coreObject),
|
|
109
|
+
/** More changes than `limit` were found. */
|
|
110
|
+
truncated: z.boolean()
|
|
111
|
+
});
|
|
112
|
+
var exportParamsSchema = z.strictObject({
|
|
113
|
+
/** A run's JSON Lines file. */
|
|
114
|
+
records: z.string().min(1),
|
|
115
|
+
/** The output recipe that gives the columns. */
|
|
116
|
+
output: z.string().min(1),
|
|
117
|
+
/** The file to write; its extension names the format, as for `run`. */
|
|
118
|
+
out: z.string().min(1)
|
|
119
|
+
});
|
|
120
|
+
var exportResultSchema = z.strictObject({ written: z.number(), location: z.string() });
|
|
49
121
|
var shutdownResultSchema = z.strictObject({});
|
|
122
|
+
var workNextParamsSchema = z.strictObject({ runId: z.string() });
|
|
123
|
+
var workNextResultSchema = z.strictObject({ item: workItemSchema.nullable() });
|
|
124
|
+
var workRecordSchema = z.strictObject({ key: z.string().nullable(), data: z.record(z.string(), z.unknown()) });
|
|
125
|
+
var workDoneParamsSchema = z.strictObject({ runId: z.string(), item: workItemSchema, report: coreObject, records: z.array(workRecordSchema) });
|
|
126
|
+
var workFailedParamsSchema = z.strictObject({ runId: z.string(), item: workItemSchema, report: coreObject, outcome: z.enum(["failure", "neutral"]) });
|
|
50
127
|
var eventParamsSchema = z.strictObject({ runId: z.string(), event: z.record(z.string(), z.unknown()) });
|
|
51
128
|
var recordParamsSchema = z.strictObject({ runId: z.string(), recipeId: z.string(), key: z.string().nullable(), data: z.record(z.string(), z.unknown()) });
|
|
52
129
|
var finishedParamsSchema = z.strictObject({
|
|
53
130
|
runId: z.string(),
|
|
54
|
-
/** The crawl report (`CrawlReport`), absent when the run could not start. */
|
|
131
|
+
/** The crawl report (`CrawlReport`), or for `work` the work report (`WorkReport`); absent when the run could not start. */
|
|
55
132
|
report: z.record(z.string(), z.unknown()).optional(),
|
|
56
133
|
/** Why the run could not start, or stopped as a whole. */
|
|
57
134
|
error: z.string().optional()
|
|
@@ -59,15 +136,22 @@ var finishedParamsSchema = z.strictObject({
|
|
|
59
136
|
var calloutParamsSchema = z.strictObject({ runId: z.string(), request: calloutRequestSchema });
|
|
60
137
|
var calloutResultSchema = calloutResponseSchema;
|
|
61
138
|
var MESSAGES = {
|
|
62
|
-
initialize: { direction: "client-to-driver", params: initializeParamsSchema, result: initializeResultSchema },
|
|
63
|
-
validate: { direction: "client-to-driver", params: validateParamsSchema, result: validateResultSchema },
|
|
64
|
-
run: { direction: "client-to-driver", params: runParamsSchema, result: runResultSchema },
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
139
|
+
"initialize": { direction: "client-to-driver", params: initializeParamsSchema, result: initializeResultSchema },
|
|
140
|
+
"validate": { direction: "client-to-driver", params: validateParamsSchema, result: validateResultSchema },
|
|
141
|
+
"run": { direction: "client-to-driver", params: runParamsSchema, result: runResultSchema },
|
|
142
|
+
"work": { direction: "client-to-driver", params: workParamsSchema, result: runResultSchema },
|
|
143
|
+
"cancel": { direction: "client-to-driver", params: cancelParamsSchema, result: cancelResultSchema },
|
|
144
|
+
"probe": { direction: "client-to-driver", params: probeParamsSchema, result: probeResultSchema },
|
|
145
|
+
"diff": { direction: "client-to-driver", params: diffParamsSchema, result: diffResultSchema },
|
|
146
|
+
"export": { direction: "client-to-driver", params: exportParamsSchema, result: exportResultSchema },
|
|
147
|
+
"shutdown": { direction: "client-to-driver", params: void 0, result: shutdownResultSchema },
|
|
148
|
+
"callout": { direction: "driver-to-client", params: calloutParamsSchema, result: calloutResultSchema },
|
|
149
|
+
"work.next": { direction: "driver-to-client", params: workNextParamsSchema, result: workNextResultSchema },
|
|
150
|
+
"work.done": { direction: "driver-to-client", params: workDoneParamsSchema, result: void 0 },
|
|
151
|
+
"work.failed": { direction: "driver-to-client", params: workFailedParamsSchema, result: void 0 },
|
|
152
|
+
"event": { direction: "driver-to-client", params: eventParamsSchema, result: void 0 },
|
|
153
|
+
"record": { direction: "driver-to-client", params: recordParamsSchema, result: void 0 },
|
|
154
|
+
"finished": { direction: "driver-to-client", params: finishedParamsSchema, result: void 0 }
|
|
71
155
|
};
|
|
72
156
|
|
|
73
157
|
// src/protocol/driver-json-schema.mapper.ts
|
|
@@ -99,19 +183,127 @@ function driverProtocolJsonSchema() {
|
|
|
99
183
|
};
|
|
100
184
|
}
|
|
101
185
|
|
|
186
|
+
// src/record-files/diff-record-files.use-case.ts
|
|
187
|
+
import { diffReport } from "@opencraw/cli";
|
|
188
|
+
import { diffRecords, readRecordsFile } from "@opencraw/core";
|
|
189
|
+
var CHANGE_LIMIT = 200;
|
|
190
|
+
async function diffRecordFiles(params) {
|
|
191
|
+
const [previous, current] = await Promise.all([readRecordsFile(params.previous), readRecordsFile(params.current)]);
|
|
192
|
+
const diff = diffRecords(previous, current, { key: params.key, ignore: params.ignore });
|
|
193
|
+
const limit = params.limit ?? CHANGE_LIMIT;
|
|
194
|
+
return {
|
|
195
|
+
added: diff.added,
|
|
196
|
+
removed: diff.removed,
|
|
197
|
+
changed: diff.changed,
|
|
198
|
+
unchanged: diff.unchanged,
|
|
199
|
+
...diff.shrunk !== void 0 && { shrunk: diff.shrunk },
|
|
200
|
+
summary: diffReport(diff),
|
|
201
|
+
changes: diff.changes.slice(0, limit).map((change) => ({ ...change })),
|
|
202
|
+
truncated: diff.changes.length > limit
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/record-files/export-record-files.use-case.ts
|
|
207
|
+
import { readFile } from "node:fs/promises";
|
|
208
|
+
import { fileSink, outputRecordOf, parseOutputRecipe, readRecordsFile as readRecordsFile2 } from "@opencraw/core";
|
|
209
|
+
async function exportRecordFiles(params) {
|
|
210
|
+
const output = parseOutputRecipe(JSON.parse(await readFile(params.output, "utf8")), params.output);
|
|
211
|
+
const stored = await readRecordsFile2(params.records);
|
|
212
|
+
const sink = fileSink(params.out, { browser: { executablePath: process.env.OPENCRAW_CHROMIUM } });
|
|
213
|
+
await sink.open(output);
|
|
214
|
+
for (const line of stored) await sink.write(outputRecordOf(line));
|
|
215
|
+
const summary = await sink.close();
|
|
216
|
+
return { written: summary.written, location: summary.location ?? params.out };
|
|
217
|
+
}
|
|
218
|
+
|
|
102
219
|
// src/session/run-session.use-case.ts
|
|
103
|
-
import { createCrawler, fileSink, RecipeSet } from "@opencraw/core";
|
|
220
|
+
import { createCrawler, fileSink as fileSink2, RecipeSet } from "@opencraw/core";
|
|
104
221
|
|
|
105
222
|
// src/callout-bridge/client-hooks.client.ts
|
|
106
|
-
import { calloutHook, calloutResponseSchema as calloutResponseSchema2 } from "@opencraw/core";
|
|
107
|
-
function clientTransport(
|
|
223
|
+
import { accessPluginVia, calloutHook, calloutResponseSchema as calloutResponseSchema2, captchaSolverVia } from "@opencraw/core";
|
|
224
|
+
function clientTransport(label, runId, ask) {
|
|
108
225
|
return {
|
|
109
|
-
label
|
|
226
|
+
label,
|
|
110
227
|
call: async (request) => calloutResponseSchema2.parse(await ask("callout", { runId, request }))
|
|
111
228
|
};
|
|
112
229
|
}
|
|
113
230
|
function clientHooks(names, runId, ask) {
|
|
114
|
-
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(name
|
|
231
|
+
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(`client hook ${name}`, runId, ask))]));
|
|
232
|
+
}
|
|
233
|
+
function clientCaptchaSolvers(names, runId, ask) {
|
|
234
|
+
return names.map((name) => captchaSolverVia(name, clientTransport(`client captcha solver ${name}`, runId, ask)));
|
|
235
|
+
}
|
|
236
|
+
function clientAccessPlugins(names, runId, ask) {
|
|
237
|
+
return names.map((name) => accessPluginVia(name, clientTransport(`client access plugin ${name}`, runId, ask)));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/site-access/resolve-site-access.use-case.ts
|
|
241
|
+
import { resolveAccess } from "@opencraw/cli";
|
|
242
|
+
async function resolveSiteAccess(params, runId, ask, env = process.env) {
|
|
243
|
+
const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
|
|
244
|
+
if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config: give "accessConfig", or set OPENCRAW_ACCESS where the driver runs`);
|
|
245
|
+
const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
|
|
246
|
+
return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/work-bridge/client-work-source.client.ts
|
|
250
|
+
import { once } from "node:events";
|
|
251
|
+
function clientWorkSource(runId, ask, notify) {
|
|
252
|
+
const kept = [];
|
|
253
|
+
let ended = false;
|
|
254
|
+
const askClient = async () => {
|
|
255
|
+
const { item } = workNextResultSchema.parse(await ask("work.next", { runId }));
|
|
256
|
+
if (item === null) {
|
|
257
|
+
ended = true;
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
return item;
|
|
261
|
+
};
|
|
262
|
+
const keepLate = async (answer) => {
|
|
263
|
+
try {
|
|
264
|
+
const item = await answer;
|
|
265
|
+
if (item !== void 0) kept.push(item);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
next: async (options) => {
|
|
271
|
+
const waiting = kept.shift();
|
|
272
|
+
if (waiting !== void 0) return waiting;
|
|
273
|
+
if (ended) return;
|
|
274
|
+
const answer = askClient();
|
|
275
|
+
const signal = options?.signal;
|
|
276
|
+
if (signal === void 0) return await answer;
|
|
277
|
+
signal.throwIfAborted();
|
|
278
|
+
const settled = new AbortController();
|
|
279
|
+
const answered = (async () => {
|
|
280
|
+
const item = await answer;
|
|
281
|
+
return { item };
|
|
282
|
+
})();
|
|
283
|
+
const retired = (async () => {
|
|
284
|
+
try {
|
|
285
|
+
await once(signal, "abort", { signal: settled.signal });
|
|
286
|
+
return "retired";
|
|
287
|
+
} catch {
|
|
288
|
+
return "answered";
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
try {
|
|
292
|
+
const first = await Promise.race([answered, retired]);
|
|
293
|
+
if (first !== "retired" && first !== "answered") return first.item;
|
|
294
|
+
void keepLate(answer);
|
|
295
|
+
throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
|
|
296
|
+
} finally {
|
|
297
|
+
settled.abort();
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
done: (item, report, records) => {
|
|
301
|
+
notify("work.done", { runId, item, report, records: records.map((record) => ({ key: record.key, data: record.data })) });
|
|
302
|
+
},
|
|
303
|
+
failed: (item, report, outcome) => {
|
|
304
|
+
notify("work.failed", { runId, item, report, outcome });
|
|
305
|
+
}
|
|
306
|
+
};
|
|
115
307
|
}
|
|
116
308
|
|
|
117
309
|
// src/session/recipe-loading.use-case.ts
|
|
@@ -155,36 +347,53 @@ function streamingSink() {
|
|
|
155
347
|
function runSessions(notify, ask) {
|
|
156
348
|
const running = /* @__PURE__ */ new Map();
|
|
157
349
|
let next = 1;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
350
|
+
const crawlerFor = async (runId, params) => {
|
|
351
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
|
|
352
|
+
return createCrawler({
|
|
353
|
+
sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
|
|
354
|
+
hooks: clientHooks(params.hooks ?? [], runId, ask),
|
|
355
|
+
captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
|
|
356
|
+
access,
|
|
357
|
+
accessPlugins,
|
|
358
|
+
profilesDir: process.env.OPENCRAW_PROFILES,
|
|
359
|
+
parallel: params.parallel,
|
|
360
|
+
resume: params.resume,
|
|
361
|
+
debug: params.dryRun,
|
|
362
|
+
browser: { headless: params.browser?.headless ?? true, executablePath: params.browser?.executablePath, ignoreHTTPSErrors: params.browser?.ignoreHTTPSErrors },
|
|
363
|
+
onEvent: (event) => {
|
|
364
|
+
notify("event", { runId, event });
|
|
365
|
+
if (event.type === "record:emit" && params.out === void 0) notify("record", { runId, recipeId: event.recipeId, key: event.key, data: event.data });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
const track = (runId, crawler, crawling) => {
|
|
370
|
+
running.set(runId, crawler);
|
|
371
|
+
void (async () => {
|
|
372
|
+
try {
|
|
373
|
+
notify("finished", { runId, report: await crawling() });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
notify("finished", { runId, error: error instanceof Error ? error.message : String(error) });
|
|
376
|
+
} finally {
|
|
377
|
+
running.delete(runId);
|
|
378
|
+
await crawler.close();
|
|
379
|
+
}
|
|
380
|
+
})();
|
|
168
381
|
};
|
|
169
382
|
return {
|
|
170
383
|
start: async (params) => {
|
|
171
384
|
const loaded = await loadRecipes(params);
|
|
172
385
|
const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
|
|
173
386
|
const runId = `run-${next++}`;
|
|
174
|
-
const crawler =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
running.set(runId, crawler);
|
|
187
|
-
void crawl(runId, crawler, set);
|
|
387
|
+
const crawler = await crawlerFor(runId, params);
|
|
388
|
+
track(runId, crawler, async () => await crawler.run(set));
|
|
389
|
+
return runId;
|
|
390
|
+
},
|
|
391
|
+
startWork: async (params) => {
|
|
392
|
+
const set = await loadRecipes(params);
|
|
393
|
+
const runId = `run-${next++}`;
|
|
394
|
+
const crawler = await crawlerFor(runId, params);
|
|
395
|
+
const windows = params.windows;
|
|
396
|
+
track(runId, crawler, async () => await crawler.work(set, clientWorkSource(runId, ask, notify), { windows }));
|
|
188
397
|
return runId;
|
|
189
398
|
},
|
|
190
399
|
cancel: async (runId) => {
|
|
@@ -204,6 +413,21 @@ function runSessions(notify, ask) {
|
|
|
204
413
|
};
|
|
205
414
|
}
|
|
206
415
|
|
|
416
|
+
// src/site-probe/probe-site.use-case.ts
|
|
417
|
+
import { probeUrl } from "@opencraw/cli";
|
|
418
|
+
async function probeSite(params, probeId, ask) {
|
|
419
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, probeId, ask);
|
|
420
|
+
const result = await probeUrl(params.url, {
|
|
421
|
+
browser: params.browser ?? false,
|
|
422
|
+
browserPath: params.browserPath ?? process.env.OPENCRAW_CHROMIUM,
|
|
423
|
+
insecureTls: params.insecureTls ?? false,
|
|
424
|
+
userAgent: params.userAgent,
|
|
425
|
+
accessConfig: access,
|
|
426
|
+
accessPlugins
|
|
427
|
+
});
|
|
428
|
+
return { ...result };
|
|
429
|
+
}
|
|
430
|
+
|
|
207
431
|
// src/transport/line-channel.client.ts
|
|
208
432
|
import { createInterface } from "node:readline";
|
|
209
433
|
|
|
@@ -310,7 +534,7 @@ function openLineChannel(input, output, handle) {
|
|
|
310
534
|
}
|
|
311
535
|
|
|
312
536
|
// src/server/serve-driver.use-case.ts
|
|
313
|
-
var METHODS = ["initialize", "validate", "run", "cancel", "shutdown"];
|
|
537
|
+
var METHODS = ["initialize", "validate", "run", "work", "cancel", "probe", "diff", "export", "shutdown"];
|
|
314
538
|
var major = (version) => version.split(".", 1)[0];
|
|
315
539
|
function parsed(schema, params) {
|
|
316
540
|
const result = schema.safeParse(params ?? {});
|
|
@@ -320,6 +544,7 @@ function parsed(schema, params) {
|
|
|
320
544
|
async function serveDriver(input, output, identity) {
|
|
321
545
|
let initialized = false;
|
|
322
546
|
const stopping = new AbortController();
|
|
547
|
+
let probes = 0;
|
|
323
548
|
const channel = openLineChannel(input, output, async (method, params) => {
|
|
324
549
|
if (method === "initialize") {
|
|
325
550
|
const hello = parsed(initializeParamsSchema, params);
|
|
@@ -332,7 +557,11 @@ async function serveDriver(input, output, identity) {
|
|
|
332
557
|
if (!initialized) throw new RpcFailure(RPC_ERRORS.server, "call initialize first");
|
|
333
558
|
if (method === "validate") return await validateRecipes(parsed(validateParamsSchema, params));
|
|
334
559
|
if (method === "run") return { runId: await sessions.start(parsed(runParamsSchema, params)) };
|
|
560
|
+
if (method === "work") return { runId: await sessions.startWork(parsed(workParamsSchema, params)) };
|
|
335
561
|
if (method === "cancel") return { cancelled: await sessions.cancel(parsed(cancelParamsSchema, params).runId) };
|
|
562
|
+
if (method === "probe") return await probeSite(parsed(probeParamsSchema, params), `probe-${++probes}`, channel.request);
|
|
563
|
+
if (method === "diff") return await diffRecordFiles(parsed(diffParamsSchema, params));
|
|
564
|
+
if (method === "export") return await exportRecordFiles(parsed(exportParamsSchema, params));
|
|
336
565
|
await sessions.closeAll();
|
|
337
566
|
setImmediate(() => {
|
|
338
567
|
stopping.abort();
|
|
@@ -340,7 +569,7 @@ async function serveDriver(input, output, identity) {
|
|
|
340
569
|
return {};
|
|
341
570
|
});
|
|
342
571
|
const sessions = runSessions(channel.notify, channel.request);
|
|
343
|
-
await Promise.race([channel.closed,
|
|
572
|
+
await Promise.race([channel.closed, once2(stopping.signal, "abort")]);
|
|
344
573
|
await channel.drained();
|
|
345
574
|
await sessions.closeAll();
|
|
346
575
|
}
|
|
@@ -353,13 +582,25 @@ export {
|
|
|
353
582
|
calloutResultSchema,
|
|
354
583
|
cancelParamsSchema,
|
|
355
584
|
cancelResultSchema,
|
|
585
|
+
clientAccessPlugins,
|
|
586
|
+
clientCaptchaSolvers,
|
|
356
587
|
clientHooks,
|
|
588
|
+
clientWorkSource,
|
|
589
|
+
diffParamsSchema,
|
|
590
|
+
diffRecordFiles,
|
|
591
|
+
diffResultSchema,
|
|
357
592
|
driverProtocolJsonSchema,
|
|
358
593
|
eventParamsSchema,
|
|
594
|
+
exportParamsSchema,
|
|
595
|
+
exportRecordFiles,
|
|
596
|
+
exportResultSchema,
|
|
359
597
|
finishedParamsSchema,
|
|
360
598
|
initializeParamsSchema,
|
|
361
599
|
initializeResultSchema,
|
|
362
600
|
openLineChannel,
|
|
601
|
+
probeParamsSchema,
|
|
602
|
+
probeResultSchema,
|
|
603
|
+
probeSite,
|
|
363
604
|
recordParamsSchema,
|
|
364
605
|
runParamsSchema,
|
|
365
606
|
runResultSchema,
|
|
@@ -368,6 +609,12 @@ export {
|
|
|
368
609
|
streamingSink,
|
|
369
610
|
validateParamsSchema,
|
|
370
611
|
validateRecipes,
|
|
371
|
-
validateResultSchema
|
|
612
|
+
validateResultSchema,
|
|
613
|
+
workDoneParamsSchema,
|
|
614
|
+
workFailedParamsSchema,
|
|
615
|
+
workItemSchema,
|
|
616
|
+
workNextParamsSchema,
|
|
617
|
+
workNextResultSchema,
|
|
618
|
+
workParamsSchema
|
|
372
619
|
};
|
|
373
620
|
//# sourceMappingURL=index.esm.js.map
|