@opencraw/driver 0.1.0 → 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 +7 -4
- package/dist/index.esm.js +149 -9
- package/dist/main.esm.js +134 -9
- package/dist/src/callout-bridge/client-hooks.client.d.ts +28 -2
- package/dist/src/callout-bridge/index.d.ts +1 -1
- package/dist/src/index.d.ts +5 -3
- package/dist/src/protocol/driver-message.contract.d.ts +105 -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/run-session.use-case.d.ts +1 -1
- 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/package.json +3 -3
- package/schemas/driver-protocol.schema.json +232 -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.1.0"}}
|
|
21
|
+
← {"jsonrpc":"2.0","id":1,"result":{"driverVersion":"0.2.0","protocolVersion":"1.1.0","methods":["initialize","validate","run","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,16 @@ 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
37
|
| `cancel` | client | Stops a run (its crawler is closed); it still ends with `finished`. |
|
|
38
|
+
| `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`. |
|
|
39
|
+
| `diff` | client | Compares two runs' JSON Lines files by key: counts, the report a person reads, and the changes up to `limit`. |
|
|
40
|
+
| `export` | client | Writes a run's JSON Lines file in another format (the extension of `out`), with an output recipe's columns. |
|
|
38
41
|
| `shutdown` | client | Closes every run, answers, then exits. Closing stdin does the same. |
|
|
39
42
|
| `event` | driver | Every crawl event, as core reports it. |
|
|
40
43
|
| `record` | driver | Each record, unless the run has `out`. |
|
|
41
44
|
| `finished` | driver | The run is over: its report, or why it could not run. |
|
|
42
|
-
| `callout` | driver | A hook the client serves. 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. |
|
|
45
|
+
| `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
46
|
|
|
44
47
|
Every answer comes back before the driver stops, even when a client writes its last requests and closes
|
|
45
48
|
stdin at once. Answers may arrive out of order; match them by `id`.
|
package/dist/index.esm.js
CHANGED
|
@@ -4,8 +4,15 @@ import { once } from "node:events";
|
|
|
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.1.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,11 +48,58 @@ 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() });
|
|
47
57
|
var cancelParamsSchema = z.strictObject({ runId: z.string() });
|
|
48
58
|
var cancelResultSchema = z.strictObject({ cancelled: z.boolean() });
|
|
59
|
+
var probeParamsSchema = z.strictObject({
|
|
60
|
+
url: z.string().min(1),
|
|
61
|
+
/** Render the page in Chromium first (for pages built by scripts). */
|
|
62
|
+
browser: z.boolean().optional(),
|
|
63
|
+
browserPath: z.string().optional(),
|
|
64
|
+
insecureTls: z.boolean().optional(),
|
|
65
|
+
userAgent: z.string().optional(),
|
|
66
|
+
...accessFields
|
|
67
|
+
});
|
|
68
|
+
var probeResultSchema = coreObject;
|
|
69
|
+
var diffParamsSchema = z.strictObject({
|
|
70
|
+
/** The earlier run's JSON Lines file. */
|
|
71
|
+
previous: z.string().min(1),
|
|
72
|
+
current: z.string().min(1),
|
|
73
|
+
/** The fields that identify a record; default: each line's `_key`. */
|
|
74
|
+
key: z.array(z.string()).optional(),
|
|
75
|
+
/** Fields left out of the comparison. */
|
|
76
|
+
ignore: z.array(z.string()).optional(),
|
|
77
|
+
/** Changes returned at most (default 200); the counts cover all of them. */
|
|
78
|
+
limit: z.number().int().min(0).optional()
|
|
79
|
+
});
|
|
80
|
+
var diffResultSchema = z.strictObject({
|
|
81
|
+
added: z.number(),
|
|
82
|
+
removed: z.number(),
|
|
83
|
+
changed: z.number(),
|
|
84
|
+
unchanged: z.number(),
|
|
85
|
+
/** Set when the current run lost most of the previous run's records. */
|
|
86
|
+
shrunk: z.strictObject({ previous: z.number(), current: z.number() }).optional(),
|
|
87
|
+
/** The report a person reads, one line per change. */
|
|
88
|
+
summary: z.array(z.string()),
|
|
89
|
+
/** The changes, up to `limit`: each `added`, `removed` or `changed` (with each field's before and after). */
|
|
90
|
+
changes: z.array(coreObject),
|
|
91
|
+
/** More changes than `limit` were found. */
|
|
92
|
+
truncated: z.boolean()
|
|
93
|
+
});
|
|
94
|
+
var exportParamsSchema = z.strictObject({
|
|
95
|
+
/** A run's JSON Lines file. */
|
|
96
|
+
records: z.string().min(1),
|
|
97
|
+
/** The output recipe that gives the columns. */
|
|
98
|
+
output: z.string().min(1),
|
|
99
|
+
/** The file to write; its extension names the format, as for `run`. */
|
|
100
|
+
out: z.string().min(1)
|
|
101
|
+
});
|
|
102
|
+
var exportResultSchema = z.strictObject({ written: z.number(), location: z.string() });
|
|
49
103
|
var shutdownResultSchema = z.strictObject({});
|
|
50
104
|
var eventParamsSchema = z.strictObject({ runId: z.string(), event: z.record(z.string(), z.unknown()) });
|
|
51
105
|
var recordParamsSchema = z.strictObject({ runId: z.string(), recipeId: z.string(), key: z.string().nullable(), data: z.record(z.string(), z.unknown()) });
|
|
@@ -63,6 +117,9 @@ var MESSAGES = {
|
|
|
63
117
|
validate: { direction: "client-to-driver", params: validateParamsSchema, result: validateResultSchema },
|
|
64
118
|
run: { direction: "client-to-driver", params: runParamsSchema, result: runResultSchema },
|
|
65
119
|
cancel: { direction: "client-to-driver", params: cancelParamsSchema, result: cancelResultSchema },
|
|
120
|
+
probe: { direction: "client-to-driver", params: probeParamsSchema, result: probeResultSchema },
|
|
121
|
+
diff: { direction: "client-to-driver", params: diffParamsSchema, result: diffResultSchema },
|
|
122
|
+
export: { direction: "client-to-driver", params: exportParamsSchema, result: exportResultSchema },
|
|
66
123
|
shutdown: { direction: "client-to-driver", params: void 0, result: shutdownResultSchema },
|
|
67
124
|
callout: { direction: "driver-to-client", params: calloutParamsSchema, result: calloutResultSchema },
|
|
68
125
|
event: { direction: "driver-to-client", params: eventParamsSchema, result: void 0 },
|
|
@@ -99,19 +156,67 @@ function driverProtocolJsonSchema() {
|
|
|
99
156
|
};
|
|
100
157
|
}
|
|
101
158
|
|
|
159
|
+
// src/record-files/diff-record-files.use-case.ts
|
|
160
|
+
import { diffReport } from "@opencraw/cli";
|
|
161
|
+
import { diffRecords, readRecordsFile } from "@opencraw/core";
|
|
162
|
+
var CHANGE_LIMIT = 200;
|
|
163
|
+
async function diffRecordFiles(params) {
|
|
164
|
+
const [previous, current] = await Promise.all([readRecordsFile(params.previous), readRecordsFile(params.current)]);
|
|
165
|
+
const diff = diffRecords(previous, current, { key: params.key, ignore: params.ignore });
|
|
166
|
+
const limit = params.limit ?? CHANGE_LIMIT;
|
|
167
|
+
return {
|
|
168
|
+
added: diff.added,
|
|
169
|
+
removed: diff.removed,
|
|
170
|
+
changed: diff.changed,
|
|
171
|
+
unchanged: diff.unchanged,
|
|
172
|
+
...diff.shrunk !== void 0 && { shrunk: diff.shrunk },
|
|
173
|
+
summary: diffReport(diff),
|
|
174
|
+
changes: diff.changes.slice(0, limit).map((change) => ({ ...change })),
|
|
175
|
+
truncated: diff.changes.length > limit
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/record-files/export-record-files.use-case.ts
|
|
180
|
+
import { readFile } from "node:fs/promises";
|
|
181
|
+
import { fileSink, outputRecordOf, parseOutputRecipe, readRecordsFile as readRecordsFile2 } from "@opencraw/core";
|
|
182
|
+
async function exportRecordFiles(params) {
|
|
183
|
+
const output = parseOutputRecipe(JSON.parse(await readFile(params.output, "utf8")), params.output);
|
|
184
|
+
const stored = await readRecordsFile2(params.records);
|
|
185
|
+
const sink = fileSink(params.out, { browser: { executablePath: process.env.OPENCRAW_CHROMIUM } });
|
|
186
|
+
await sink.open(output);
|
|
187
|
+
for (const line of stored) await sink.write(outputRecordOf(line));
|
|
188
|
+
const summary = await sink.close();
|
|
189
|
+
return { written: summary.written, location: summary.location ?? params.out };
|
|
190
|
+
}
|
|
191
|
+
|
|
102
192
|
// src/session/run-session.use-case.ts
|
|
103
|
-
import { createCrawler, fileSink, RecipeSet } from "@opencraw/core";
|
|
193
|
+
import { createCrawler, fileSink as fileSink2, RecipeSet } from "@opencraw/core";
|
|
104
194
|
|
|
105
195
|
// src/callout-bridge/client-hooks.client.ts
|
|
106
|
-
import { calloutHook, calloutResponseSchema as calloutResponseSchema2 } from "@opencraw/core";
|
|
107
|
-
function clientTransport(
|
|
196
|
+
import { accessPluginVia, calloutHook, calloutResponseSchema as calloutResponseSchema2, captchaSolverVia } from "@opencraw/core";
|
|
197
|
+
function clientTransport(label, runId, ask) {
|
|
108
198
|
return {
|
|
109
|
-
label
|
|
199
|
+
label,
|
|
110
200
|
call: async (request) => calloutResponseSchema2.parse(await ask("callout", { runId, request }))
|
|
111
201
|
};
|
|
112
202
|
}
|
|
113
203
|
function clientHooks(names, runId, ask) {
|
|
114
|
-
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(name
|
|
204
|
+
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(`client hook ${name}`, runId, ask))]));
|
|
205
|
+
}
|
|
206
|
+
function clientCaptchaSolvers(names, runId, ask) {
|
|
207
|
+
return names.map((name) => captchaSolverVia(name, clientTransport(`client captcha solver ${name}`, runId, ask)));
|
|
208
|
+
}
|
|
209
|
+
function clientAccessPlugins(names, runId, ask) {
|
|
210
|
+
return names.map((name) => accessPluginVia(name, clientTransport(`client access plugin ${name}`, runId, ask)));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/site-access/resolve-site-access.use-case.ts
|
|
214
|
+
import { resolveAccess } from "@opencraw/cli";
|
|
215
|
+
async function resolveSiteAccess(params, runId, ask, env = process.env) {
|
|
216
|
+
const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
|
|
217
|
+
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`);
|
|
218
|
+
const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
|
|
219
|
+
return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
|
|
115
220
|
}
|
|
116
221
|
|
|
117
222
|
// src/session/recipe-loading.use-case.ts
|
|
@@ -171,9 +276,14 @@ function runSessions(notify, ask) {
|
|
|
171
276
|
const loaded = await loadRecipes(params);
|
|
172
277
|
const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
|
|
173
278
|
const runId = `run-${next++}`;
|
|
279
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
|
|
174
280
|
const crawler = createCrawler({
|
|
175
|
-
sink: params.out === void 0 ? streamingSink() :
|
|
281
|
+
sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
|
|
176
282
|
hooks: clientHooks(params.hooks ?? [], runId, ask),
|
|
283
|
+
captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
|
|
284
|
+
access,
|
|
285
|
+
accessPlugins,
|
|
286
|
+
profilesDir: process.env.OPENCRAW_PROFILES,
|
|
177
287
|
parallel: params.parallel,
|
|
178
288
|
resume: params.resume,
|
|
179
289
|
debug: params.dryRun,
|
|
@@ -204,6 +314,21 @@ function runSessions(notify, ask) {
|
|
|
204
314
|
};
|
|
205
315
|
}
|
|
206
316
|
|
|
317
|
+
// src/site-probe/probe-site.use-case.ts
|
|
318
|
+
import { probeUrl } from "@opencraw/cli";
|
|
319
|
+
async function probeSite(params, probeId, ask) {
|
|
320
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, probeId, ask);
|
|
321
|
+
const result = await probeUrl(params.url, {
|
|
322
|
+
browser: params.browser ?? false,
|
|
323
|
+
browserPath: params.browserPath ?? process.env.OPENCRAW_CHROMIUM,
|
|
324
|
+
insecureTls: params.insecureTls ?? false,
|
|
325
|
+
userAgent: params.userAgent,
|
|
326
|
+
accessConfig: access,
|
|
327
|
+
accessPlugins
|
|
328
|
+
});
|
|
329
|
+
return { ...result };
|
|
330
|
+
}
|
|
331
|
+
|
|
207
332
|
// src/transport/line-channel.client.ts
|
|
208
333
|
import { createInterface } from "node:readline";
|
|
209
334
|
|
|
@@ -310,7 +435,7 @@ function openLineChannel(input, output, handle) {
|
|
|
310
435
|
}
|
|
311
436
|
|
|
312
437
|
// src/server/serve-driver.use-case.ts
|
|
313
|
-
var METHODS = ["initialize", "validate", "run", "cancel", "shutdown"];
|
|
438
|
+
var METHODS = ["initialize", "validate", "run", "cancel", "probe", "diff", "export", "shutdown"];
|
|
314
439
|
var major = (version) => version.split(".", 1)[0];
|
|
315
440
|
function parsed(schema, params) {
|
|
316
441
|
const result = schema.safeParse(params ?? {});
|
|
@@ -320,6 +445,7 @@ function parsed(schema, params) {
|
|
|
320
445
|
async function serveDriver(input, output, identity) {
|
|
321
446
|
let initialized = false;
|
|
322
447
|
const stopping = new AbortController();
|
|
448
|
+
let probes = 0;
|
|
323
449
|
const channel = openLineChannel(input, output, async (method, params) => {
|
|
324
450
|
if (method === "initialize") {
|
|
325
451
|
const hello = parsed(initializeParamsSchema, params);
|
|
@@ -333,6 +459,9 @@ async function serveDriver(input, output, identity) {
|
|
|
333
459
|
if (method === "validate") return await validateRecipes(parsed(validateParamsSchema, params));
|
|
334
460
|
if (method === "run") return { runId: await sessions.start(parsed(runParamsSchema, params)) };
|
|
335
461
|
if (method === "cancel") return { cancelled: await sessions.cancel(parsed(cancelParamsSchema, params).runId) };
|
|
462
|
+
if (method === "probe") return await probeSite(parsed(probeParamsSchema, params), `probe-${++probes}`, channel.request);
|
|
463
|
+
if (method === "diff") return await diffRecordFiles(parsed(diffParamsSchema, params));
|
|
464
|
+
if (method === "export") return await exportRecordFiles(parsed(exportParamsSchema, params));
|
|
336
465
|
await sessions.closeAll();
|
|
337
466
|
setImmediate(() => {
|
|
338
467
|
stopping.abort();
|
|
@@ -353,13 +482,24 @@ export {
|
|
|
353
482
|
calloutResultSchema,
|
|
354
483
|
cancelParamsSchema,
|
|
355
484
|
cancelResultSchema,
|
|
485
|
+
clientAccessPlugins,
|
|
486
|
+
clientCaptchaSolvers,
|
|
356
487
|
clientHooks,
|
|
488
|
+
diffParamsSchema,
|
|
489
|
+
diffRecordFiles,
|
|
490
|
+
diffResultSchema,
|
|
357
491
|
driverProtocolJsonSchema,
|
|
358
492
|
eventParamsSchema,
|
|
493
|
+
exportParamsSchema,
|
|
494
|
+
exportRecordFiles,
|
|
495
|
+
exportResultSchema,
|
|
359
496
|
finishedParamsSchema,
|
|
360
497
|
initializeParamsSchema,
|
|
361
498
|
initializeResultSchema,
|
|
362
499
|
openLineChannel,
|
|
500
|
+
probeParamsSchema,
|
|
501
|
+
probeResultSchema,
|
|
502
|
+
probeSite,
|
|
363
503
|
recordParamsSchema,
|
|
364
504
|
runParamsSchema,
|
|
365
505
|
runResultSchema,
|
package/dist/main.esm.js
CHANGED
|
@@ -11,8 +11,15 @@ import { once } from "node:events";
|
|
|
11
11
|
// src/protocol/driver-message.contract.ts
|
|
12
12
|
import { calloutRequestSchema, calloutResponseSchema } from "@opencraw/core";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
var PROTOCOL_VERSION = "1.
|
|
14
|
+
var PROTOCOL_VERSION = "1.1.0";
|
|
15
15
|
var recipeObject = z.record(z.string(), z.unknown());
|
|
16
|
+
var coreObject = z.record(z.string(), z.unknown());
|
|
17
|
+
var accessFields = {
|
|
18
|
+
accessConfig: z.string().optional(),
|
|
19
|
+
access: z.string().optional(),
|
|
20
|
+
/** Access plugins the client serves: a profile `{ "kind": "plugin", "name": … }` naming one leases through a `callout` to the client. */
|
|
21
|
+
accessPlugins: z.array(z.string()).optional()
|
|
22
|
+
};
|
|
16
23
|
var recipeSource = {
|
|
17
24
|
paths: z.array(z.string()).min(1).optional(),
|
|
18
25
|
recipes: z.array(recipeObject).min(1).optional()
|
|
@@ -48,11 +55,57 @@ var runParamsSchema = z.strictObject({
|
|
|
48
55
|
parallel: z.number().int().min(1).max(16).optional(),
|
|
49
56
|
browser: z.strictObject({ headless: z.boolean().optional(), executablePath: z.string().optional(), ignoreHTTPSErrors: z.boolean().optional() }).optional(),
|
|
50
57
|
/** The hooks the client answers itself: each becomes a `callout` request to the client when a recipe calls it. */
|
|
51
|
-
hooks: z.array(z.string()).optional()
|
|
58
|
+
hooks: z.array(z.string()).optional(),
|
|
59
|
+
/** Captcha solvers the client serves, by name: a challenge goes to the client as a `captcha` callout. */
|
|
60
|
+
captchaSolvers: z.array(z.string()).optional(),
|
|
61
|
+
...accessFields
|
|
52
62
|
});
|
|
53
63
|
var runResultSchema = z.strictObject({ runId: z.string() });
|
|
54
64
|
var cancelParamsSchema = z.strictObject({ runId: z.string() });
|
|
55
65
|
var cancelResultSchema = z.strictObject({ cancelled: z.boolean() });
|
|
66
|
+
var probeParamsSchema = z.strictObject({
|
|
67
|
+
url: z.string().min(1),
|
|
68
|
+
/** Render the page in Chromium first (for pages built by scripts). */
|
|
69
|
+
browser: z.boolean().optional(),
|
|
70
|
+
browserPath: z.string().optional(),
|
|
71
|
+
insecureTls: z.boolean().optional(),
|
|
72
|
+
userAgent: z.string().optional(),
|
|
73
|
+
...accessFields
|
|
74
|
+
});
|
|
75
|
+
var diffParamsSchema = z.strictObject({
|
|
76
|
+
/** The earlier run's JSON Lines file. */
|
|
77
|
+
previous: z.string().min(1),
|
|
78
|
+
current: z.string().min(1),
|
|
79
|
+
/** The fields that identify a record; default: each line's `_key`. */
|
|
80
|
+
key: z.array(z.string()).optional(),
|
|
81
|
+
/** Fields left out of the comparison. */
|
|
82
|
+
ignore: z.array(z.string()).optional(),
|
|
83
|
+
/** Changes returned at most (default 200); the counts cover all of them. */
|
|
84
|
+
limit: z.number().int().min(0).optional()
|
|
85
|
+
});
|
|
86
|
+
var diffResultSchema = z.strictObject({
|
|
87
|
+
added: z.number(),
|
|
88
|
+
removed: z.number(),
|
|
89
|
+
changed: z.number(),
|
|
90
|
+
unchanged: z.number(),
|
|
91
|
+
/** Set when the current run lost most of the previous run's records. */
|
|
92
|
+
shrunk: z.strictObject({ previous: z.number(), current: z.number() }).optional(),
|
|
93
|
+
/** The report a person reads, one line per change. */
|
|
94
|
+
summary: z.array(z.string()),
|
|
95
|
+
/** The changes, up to `limit`: each `added`, `removed` or `changed` (with each field's before and after). */
|
|
96
|
+
changes: z.array(coreObject),
|
|
97
|
+
/** More changes than `limit` were found. */
|
|
98
|
+
truncated: z.boolean()
|
|
99
|
+
});
|
|
100
|
+
var exportParamsSchema = z.strictObject({
|
|
101
|
+
/** A run's JSON Lines file. */
|
|
102
|
+
records: z.string().min(1),
|
|
103
|
+
/** The output recipe that gives the columns. */
|
|
104
|
+
output: z.string().min(1),
|
|
105
|
+
/** The file to write; its extension names the format, as for `run`. */
|
|
106
|
+
out: z.string().min(1)
|
|
107
|
+
});
|
|
108
|
+
var exportResultSchema = z.strictObject({ written: z.number(), location: z.string() });
|
|
56
109
|
var shutdownResultSchema = z.strictObject({});
|
|
57
110
|
var eventParamsSchema = z.strictObject({ runId: z.string(), event: z.record(z.string(), z.unknown()) });
|
|
58
111
|
var recordParamsSchema = z.strictObject({ runId: z.string(), recipeId: z.string(), key: z.string().nullable(), data: z.record(z.string(), z.unknown()) });
|
|
@@ -68,19 +121,67 @@ var calloutParamsSchema = z.strictObject({ runId: z.string(), request: calloutRe
|
|
|
68
121
|
// src/protocol/driver-json-schema.mapper.ts
|
|
69
122
|
import { z as z2 } from "zod";
|
|
70
123
|
|
|
124
|
+
// src/record-files/diff-record-files.use-case.ts
|
|
125
|
+
import { diffReport } from "@opencraw/cli";
|
|
126
|
+
import { diffRecords, readRecordsFile } from "@opencraw/core";
|
|
127
|
+
var CHANGE_LIMIT = 200;
|
|
128
|
+
async function diffRecordFiles(params) {
|
|
129
|
+
const [previous, current] = await Promise.all([readRecordsFile(params.previous), readRecordsFile(params.current)]);
|
|
130
|
+
const diff = diffRecords(previous, current, { key: params.key, ignore: params.ignore });
|
|
131
|
+
const limit = params.limit ?? CHANGE_LIMIT;
|
|
132
|
+
return {
|
|
133
|
+
added: diff.added,
|
|
134
|
+
removed: diff.removed,
|
|
135
|
+
changed: diff.changed,
|
|
136
|
+
unchanged: diff.unchanged,
|
|
137
|
+
...diff.shrunk !== void 0 && { shrunk: diff.shrunk },
|
|
138
|
+
summary: diffReport(diff),
|
|
139
|
+
changes: diff.changes.slice(0, limit).map((change) => ({ ...change })),
|
|
140
|
+
truncated: diff.changes.length > limit
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/record-files/export-record-files.use-case.ts
|
|
145
|
+
import { readFile } from "node:fs/promises";
|
|
146
|
+
import { fileSink, outputRecordOf, parseOutputRecipe, readRecordsFile as readRecordsFile2 } from "@opencraw/core";
|
|
147
|
+
async function exportRecordFiles(params) {
|
|
148
|
+
const output = parseOutputRecipe(JSON.parse(await readFile(params.output, "utf8")), params.output);
|
|
149
|
+
const stored = await readRecordsFile2(params.records);
|
|
150
|
+
const sink = fileSink(params.out, { browser: { executablePath: process.env.OPENCRAW_CHROMIUM } });
|
|
151
|
+
await sink.open(output);
|
|
152
|
+
for (const line of stored) await sink.write(outputRecordOf(line));
|
|
153
|
+
const summary = await sink.close();
|
|
154
|
+
return { written: summary.written, location: summary.location ?? params.out };
|
|
155
|
+
}
|
|
156
|
+
|
|
71
157
|
// src/session/run-session.use-case.ts
|
|
72
|
-
import { createCrawler, fileSink, RecipeSet } from "@opencraw/core";
|
|
158
|
+
import { createCrawler, fileSink as fileSink2, RecipeSet } from "@opencraw/core";
|
|
73
159
|
|
|
74
160
|
// src/callout-bridge/client-hooks.client.ts
|
|
75
|
-
import { calloutHook, calloutResponseSchema as calloutResponseSchema2 } from "@opencraw/core";
|
|
76
|
-
function clientTransport(
|
|
161
|
+
import { accessPluginVia, calloutHook, calloutResponseSchema as calloutResponseSchema2, captchaSolverVia } from "@opencraw/core";
|
|
162
|
+
function clientTransport(label, runId, ask) {
|
|
77
163
|
return {
|
|
78
|
-
label
|
|
164
|
+
label,
|
|
79
165
|
call: async (request) => calloutResponseSchema2.parse(await ask("callout", { runId, request }))
|
|
80
166
|
};
|
|
81
167
|
}
|
|
82
168
|
function clientHooks(names, runId, ask) {
|
|
83
|
-
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(name
|
|
169
|
+
return Object.fromEntries(names.map((name) => [name, calloutHook(name, clientTransport(`client hook ${name}`, runId, ask))]));
|
|
170
|
+
}
|
|
171
|
+
function clientCaptchaSolvers(names, runId, ask) {
|
|
172
|
+
return names.map((name) => captchaSolverVia(name, clientTransport(`client captcha solver ${name}`, runId, ask)));
|
|
173
|
+
}
|
|
174
|
+
function clientAccessPlugins(names, runId, ask) {
|
|
175
|
+
return names.map((name) => accessPluginVia(name, clientTransport(`client access plugin ${name}`, runId, ask)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/site-access/resolve-site-access.use-case.ts
|
|
179
|
+
import { resolveAccess } from "@opencraw/cli";
|
|
180
|
+
async function resolveSiteAccess(params, runId, ask, env = process.env) {
|
|
181
|
+
const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
|
|
182
|
+
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`);
|
|
183
|
+
const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
|
|
184
|
+
return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
|
|
84
185
|
}
|
|
85
186
|
|
|
86
187
|
// src/session/recipe-loading.use-case.ts
|
|
@@ -140,9 +241,14 @@ function runSessions(notify, ask) {
|
|
|
140
241
|
const loaded = await loadRecipes(params);
|
|
141
242
|
const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
|
|
142
243
|
const runId = `run-${next++}`;
|
|
244
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
|
|
143
245
|
const crawler = createCrawler({
|
|
144
|
-
sink: params.out === void 0 ? streamingSink() :
|
|
246
|
+
sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
|
|
145
247
|
hooks: clientHooks(params.hooks ?? [], runId, ask),
|
|
248
|
+
captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
|
|
249
|
+
access,
|
|
250
|
+
accessPlugins,
|
|
251
|
+
profilesDir: process.env.OPENCRAW_PROFILES,
|
|
146
252
|
parallel: params.parallel,
|
|
147
253
|
resume: params.resume,
|
|
148
254
|
debug: params.dryRun,
|
|
@@ -173,6 +279,21 @@ function runSessions(notify, ask) {
|
|
|
173
279
|
};
|
|
174
280
|
}
|
|
175
281
|
|
|
282
|
+
// src/site-probe/probe-site.use-case.ts
|
|
283
|
+
import { probeUrl } from "@opencraw/cli";
|
|
284
|
+
async function probeSite(params, probeId, ask) {
|
|
285
|
+
const { access, accessPlugins } = await resolveSiteAccess(params, probeId, ask);
|
|
286
|
+
const result = await probeUrl(params.url, {
|
|
287
|
+
browser: params.browser ?? false,
|
|
288
|
+
browserPath: params.browserPath ?? process.env.OPENCRAW_CHROMIUM,
|
|
289
|
+
insecureTls: params.insecureTls ?? false,
|
|
290
|
+
userAgent: params.userAgent,
|
|
291
|
+
accessConfig: access,
|
|
292
|
+
accessPlugins
|
|
293
|
+
});
|
|
294
|
+
return { ...result };
|
|
295
|
+
}
|
|
296
|
+
|
|
176
297
|
// src/transport/line-channel.client.ts
|
|
177
298
|
import { createInterface } from "node:readline";
|
|
178
299
|
|
|
@@ -279,7 +400,7 @@ function openLineChannel(input, output, handle) {
|
|
|
279
400
|
}
|
|
280
401
|
|
|
281
402
|
// src/server/serve-driver.use-case.ts
|
|
282
|
-
var METHODS = ["initialize", "validate", "run", "cancel", "shutdown"];
|
|
403
|
+
var METHODS = ["initialize", "validate", "run", "cancel", "probe", "diff", "export", "shutdown"];
|
|
283
404
|
var major = (version) => version.split(".", 1)[0];
|
|
284
405
|
function parsed(schema, params) {
|
|
285
406
|
const result = schema.safeParse(params ?? {});
|
|
@@ -289,6 +410,7 @@ function parsed(schema, params) {
|
|
|
289
410
|
async function serveDriver(input, output, identity) {
|
|
290
411
|
let initialized = false;
|
|
291
412
|
const stopping = new AbortController();
|
|
413
|
+
let probes = 0;
|
|
292
414
|
const channel = openLineChannel(input, output, async (method, params) => {
|
|
293
415
|
if (method === "initialize") {
|
|
294
416
|
const hello = parsed(initializeParamsSchema, params);
|
|
@@ -302,6 +424,9 @@ async function serveDriver(input, output, identity) {
|
|
|
302
424
|
if (method === "validate") return await validateRecipes(parsed(validateParamsSchema, params));
|
|
303
425
|
if (method === "run") return { runId: await sessions.start(parsed(runParamsSchema, params)) };
|
|
304
426
|
if (method === "cancel") return { cancelled: await sessions.cancel(parsed(cancelParamsSchema, params).runId) };
|
|
427
|
+
if (method === "probe") return await probeSite(parsed(probeParamsSchema, params), `probe-${++probes}`, channel.request);
|
|
428
|
+
if (method === "diff") return await diffRecordFiles(parsed(diffParamsSchema, params));
|
|
429
|
+
if (method === "export") return await exportRecordFiles(parsed(exportParamsSchema, params));
|
|
305
430
|
await sessions.closeAll();
|
|
306
431
|
setImmediate(() => {
|
|
307
432
|
stopping.abort();
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { CalloutTransport, HookMap } from '@opencraw/core';
|
|
1
|
+
import type { AccessPlugin, CalloutTransport, CaptchaSolver, HookMap } from '@opencraw/core';
|
|
2
2
|
/** Sends one request to the client and waits for its answer: the channel's `request`. */
|
|
3
3
|
export type AskClient = (method: string, params: unknown) => Promise<unknown>;
|
|
4
4
|
/**
|
|
5
5
|
* A transport whose handler is the client: each call is a `callout` request carrying core's own
|
|
6
6
|
* `CalloutRequest`, and the client answers with a `CalloutResponse`, so `pending` and idempotency keys
|
|
7
7
|
* work exactly as they do for a command or an HTTP hook.
|
|
8
|
+
*
|
|
9
|
+
* @param label - What the logs call it (`client hook tidy`).
|
|
10
|
+
* @param runId - The run it belongs to, sent with every callout.
|
|
11
|
+
* @param ask - The channel's request.
|
|
12
|
+
* @returns The transport.
|
|
8
13
|
*/
|
|
9
|
-
export declare function clientTransport(
|
|
14
|
+
export declare function clientTransport(label: string, runId: string, ask: AskClient): CalloutTransport;
|
|
10
15
|
/**
|
|
11
16
|
* The hooks a run's client serves, each answered by the client over the channel.
|
|
12
17
|
*
|
|
@@ -16,4 +21,25 @@ export declare function clientTransport(hookName: string, runId: string, ask: As
|
|
|
16
21
|
* @returns A hook map for `createCrawler`.
|
|
17
22
|
*/
|
|
18
23
|
export declare function clientHooks(names: readonly string[], runId: string, ask: AskClient): HookMap;
|
|
24
|
+
/**
|
|
25
|
+
* The captcha solvers a run's client serves: a challenge goes to the client as a `captcha` callout (its
|
|
26
|
+
* input is core's `CaptchaCalloutInput`), and the answer's `token` or `text` is put on the page.
|
|
27
|
+
*
|
|
28
|
+
* @param names - The solvers the client said it serves.
|
|
29
|
+
* @param runId - The run they belong to.
|
|
30
|
+
* @param ask - The channel's request.
|
|
31
|
+
* @returns Solvers for `createCrawler`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function clientCaptchaSolvers(names: readonly string[], runId: string, ask: AskClient): CaptchaSolver[];
|
|
34
|
+
/**
|
|
35
|
+
* The access plugins a client serves: a profile `{ "kind": "plugin", "name": … }` leases through an
|
|
36
|
+
* `access` callout to the client (`phase: "lease"`), and releases through another (`phase: "release"`)
|
|
37
|
+
* when the client's lease asked for it.
|
|
38
|
+
*
|
|
39
|
+
* @param names - The plugins the client said it serves.
|
|
40
|
+
* @param runId - The run, or the probe, they belong to.
|
|
41
|
+
* @param ask - The channel's request.
|
|
42
|
+
* @returns Plugins for `createCrawler` or a probe.
|
|
43
|
+
*/
|
|
44
|
+
export declare function clientAccessPlugins(names: readonly string[], runId: string, ask: AskClient): AccessPlugin[];
|
|
19
45
|
//# sourceMappingURL=client-hooks.client.d.ts.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { clientHooks, clientTransport } from './client-hooks.client.js';
|
|
1
|
+
export { clientAccessPlugins, clientCaptchaSolvers, clientHooks, clientTransport } from './client-hooks.client.js';
|
|
2
2
|
export type { AskClient } from './client-hooks.client.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { serveDriver } from './server/index.js';
|
|
2
2
|
export type { DriverIdentity } from './server/index.js';
|
|
3
|
-
export { PROTOCOL_VERSION, MESSAGES, driverProtocolJsonSchema, initializeParamsSchema, initializeResultSchema, validateParamsSchema, validateResultSchema, runParamsSchema, runResultSchema, cancelParamsSchema, cancelResultSchema, eventParamsSchema, recordParamsSchema, finishedParamsSchema, calloutParamsSchema, calloutResultSchema, } from './protocol/index.js';
|
|
4
|
-
export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult, RunParams, RunResult, CancelParams, CancelResult, EventParams, RecordParams, FinishedParams, CalloutParams, } from './protocol/index.js';
|
|
3
|
+
export { PROTOCOL_VERSION, MESSAGES, driverProtocolJsonSchema, initializeParamsSchema, initializeResultSchema, validateParamsSchema, validateResultSchema, runParamsSchema, runResultSchema, cancelParamsSchema, cancelResultSchema, eventParamsSchema, recordParamsSchema, finishedParamsSchema, calloutParamsSchema, calloutResultSchema, probeParamsSchema, probeResultSchema, diffParamsSchema, diffResultSchema, exportParamsSchema, exportResultSchema, } from './protocol/index.js';
|
|
4
|
+
export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult, RunParams, RunResult, CancelParams, CancelResult, EventParams, RecordParams, FinishedParams, CalloutParams, ProbeParams, DiffParams, DiffResult, ExportParams, ExportResult, } from './protocol/index.js';
|
|
5
5
|
export { openLineChannel, RPC_ERRORS, RpcFailure } from './transport/index.js';
|
|
6
6
|
export type { LineChannel, RequestHandler, RpcError, RpcMessage } from './transport/index.js';
|
|
7
7
|
export { runSessions, validateRecipes, streamingSink } from './session/index.js';
|
|
8
8
|
export type { RunSessions, NotifyClient } from './session/index.js';
|
|
9
|
-
export { clientHooks } from './callout-bridge/index.js';
|
|
9
|
+
export { clientHooks, clientCaptchaSolvers, clientAccessPlugins } from './callout-bridge/index.js';
|
|
10
|
+
export { probeSite } from './site-probe/index.js';
|
|
11
|
+
export { diffRecordFiles, exportRecordFiles } from './record-files/index.js';
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
* The driver protocol's version, separate from the package's: a client pins a major and the driver refuses
|
|
4
4
|
* another. Additions (methods, optional fields) raise the minor; anything a client could break on, the major.
|
|
5
5
|
*/
|
|
6
|
-
export declare const PROTOCOL_VERSION = "1.
|
|
6
|
+
export declare const PROTOCOL_VERSION = "1.1.0";
|
|
7
7
|
export declare const initializeParamsSchema: z.ZodObject<{
|
|
8
8
|
clientName: z.ZodString;
|
|
9
9
|
clientVersion: z.ZodString;
|
|
@@ -35,6 +35,9 @@ export declare const validateResultSchema: z.ZodObject<{
|
|
|
35
35
|
}, z.core.$strict>;
|
|
36
36
|
export type ValidateResult = z.infer<typeof validateResultSchema>;
|
|
37
37
|
export declare const runParamsSchema: z.ZodObject<{
|
|
38
|
+
accessConfig: z.ZodOptional<z.ZodString>;
|
|
39
|
+
access: z.ZodOptional<z.ZodString>;
|
|
40
|
+
accessPlugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
41
|
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
42
|
recipes: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
40
43
|
only: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -49,6 +52,7 @@ export declare const runParamsSchema: z.ZodObject<{
|
|
|
49
52
|
ignoreHTTPSErrors: z.ZodOptional<z.ZodBoolean>;
|
|
50
53
|
}, z.core.$strict>>;
|
|
51
54
|
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
captchaSolvers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
52
56
|
}, z.core.$strict>;
|
|
53
57
|
export type RunParams = z.infer<typeof runParamsSchema>;
|
|
54
58
|
export declare const runResultSchema: z.ZodObject<{
|
|
@@ -63,6 +67,52 @@ export declare const cancelResultSchema: z.ZodObject<{
|
|
|
63
67
|
cancelled: z.ZodBoolean;
|
|
64
68
|
}, z.core.$strict>;
|
|
65
69
|
export type CancelResult = z.infer<typeof cancelResultSchema>;
|
|
70
|
+
export declare const probeParamsSchema: z.ZodObject<{
|
|
71
|
+
accessConfig: z.ZodOptional<z.ZodString>;
|
|
72
|
+
access: z.ZodOptional<z.ZodString>;
|
|
73
|
+
accessPlugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
74
|
+
url: z.ZodString;
|
|
75
|
+
browser: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
browserPath: z.ZodOptional<z.ZodString>;
|
|
77
|
+
insecureTls: z.ZodOptional<z.ZodBoolean>;
|
|
78
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
79
|
+
}, z.core.$strict>;
|
|
80
|
+
export type ProbeParams = z.infer<typeof probeParamsSchema>;
|
|
81
|
+
/** What the page holds and how a recipe could reach it (`ProbeResult`); its shape is the probe's, versioned with it. */
|
|
82
|
+
export declare const probeResultSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
83
|
+
export declare const diffParamsSchema: z.ZodObject<{
|
|
84
|
+
previous: z.ZodString;
|
|
85
|
+
current: z.ZodString;
|
|
86
|
+
key: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
87
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
88
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
}, z.core.$strict>;
|
|
90
|
+
export type DiffParams = z.infer<typeof diffParamsSchema>;
|
|
91
|
+
export declare const diffResultSchema: z.ZodObject<{
|
|
92
|
+
added: z.ZodNumber;
|
|
93
|
+
removed: z.ZodNumber;
|
|
94
|
+
changed: z.ZodNumber;
|
|
95
|
+
unchanged: z.ZodNumber;
|
|
96
|
+
shrunk: z.ZodOptional<z.ZodObject<{
|
|
97
|
+
previous: z.ZodNumber;
|
|
98
|
+
current: z.ZodNumber;
|
|
99
|
+
}, z.core.$strict>>;
|
|
100
|
+
summary: z.ZodArray<z.ZodString>;
|
|
101
|
+
changes: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
102
|
+
truncated: z.ZodBoolean;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
export type DiffResult = z.infer<typeof diffResultSchema>;
|
|
105
|
+
export declare const exportParamsSchema: z.ZodObject<{
|
|
106
|
+
records: z.ZodString;
|
|
107
|
+
output: z.ZodString;
|
|
108
|
+
out: z.ZodString;
|
|
109
|
+
}, z.core.$strict>;
|
|
110
|
+
export type ExportParams = z.infer<typeof exportParamsSchema>;
|
|
111
|
+
export declare const exportResultSchema: z.ZodObject<{
|
|
112
|
+
written: z.ZodNumber;
|
|
113
|
+
location: z.ZodString;
|
|
114
|
+
}, z.core.$strict>;
|
|
115
|
+
export type ExportResult = z.infer<typeof exportResultSchema>;
|
|
66
116
|
export declare const shutdownResultSchema: z.ZodObject<{}, z.core.$strict>;
|
|
67
117
|
/** Every crawl event, as core reports it (`CrawlEvent`); its shape is core's, versioned with core. */
|
|
68
118
|
export declare const eventParamsSchema: z.ZodObject<{
|
|
@@ -83,7 +133,7 @@ export declare const finishedParamsSchema: z.ZodObject<{
|
|
|
83
133
|
error: z.ZodOptional<z.ZodString>;
|
|
84
134
|
}, z.core.$strict>;
|
|
85
135
|
export type FinishedParams = z.infer<typeof finishedParamsSchema>;
|
|
86
|
-
/** A hook the client serves: core's own callout request, answered with core's callout response. */
|
|
136
|
+
/** A hook, captcha solver or access plugin the client serves (`request.kind`): core's own callout request, answered with core's callout response. */
|
|
87
137
|
export declare const calloutParamsSchema: z.ZodObject<{
|
|
88
138
|
runId: z.ZodString;
|
|
89
139
|
request: z.ZodObject<{
|
|
@@ -153,6 +203,9 @@ export declare const MESSAGES: {
|
|
|
153
203
|
readonly run: {
|
|
154
204
|
readonly direction: 'client-to-driver';
|
|
155
205
|
readonly params: z.ZodObject<{
|
|
206
|
+
accessConfig: z.ZodOptional<z.ZodString>;
|
|
207
|
+
access: z.ZodOptional<z.ZodString>;
|
|
208
|
+
accessPlugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
156
209
|
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
157
210
|
recipes: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
158
211
|
only: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -167,6 +220,7 @@ export declare const MESSAGES: {
|
|
|
167
220
|
ignoreHTTPSErrors: z.ZodOptional<z.ZodBoolean>;
|
|
168
221
|
}, z.core.$strict>>;
|
|
169
222
|
hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
223
|
+
captchaSolvers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
170
224
|
}, z.core.$strict>;
|
|
171
225
|
readonly result: z.ZodObject<{
|
|
172
226
|
runId: z.ZodString;
|
|
@@ -181,6 +235,55 @@ export declare const MESSAGES: {
|
|
|
181
235
|
cancelled: z.ZodBoolean;
|
|
182
236
|
}, z.core.$strict>;
|
|
183
237
|
};
|
|
238
|
+
readonly probe: {
|
|
239
|
+
readonly direction: 'client-to-driver';
|
|
240
|
+
readonly params: z.ZodObject<{
|
|
241
|
+
accessConfig: z.ZodOptional<z.ZodString>;
|
|
242
|
+
access: z.ZodOptional<z.ZodString>;
|
|
243
|
+
accessPlugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
244
|
+
url: z.ZodString;
|
|
245
|
+
browser: z.ZodOptional<z.ZodBoolean>;
|
|
246
|
+
browserPath: z.ZodOptional<z.ZodString>;
|
|
247
|
+
insecureTls: z.ZodOptional<z.ZodBoolean>;
|
|
248
|
+
userAgent: z.ZodOptional<z.ZodString>;
|
|
249
|
+
}, z.core.$strict>;
|
|
250
|
+
readonly result: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
251
|
+
};
|
|
252
|
+
readonly diff: {
|
|
253
|
+
readonly direction: 'client-to-driver';
|
|
254
|
+
readonly params: z.ZodObject<{
|
|
255
|
+
previous: z.ZodString;
|
|
256
|
+
current: z.ZodString;
|
|
257
|
+
key: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
258
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
259
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
260
|
+
}, z.core.$strict>;
|
|
261
|
+
readonly result: z.ZodObject<{
|
|
262
|
+
added: z.ZodNumber;
|
|
263
|
+
removed: z.ZodNumber;
|
|
264
|
+
changed: z.ZodNumber;
|
|
265
|
+
unchanged: z.ZodNumber;
|
|
266
|
+
shrunk: z.ZodOptional<z.ZodObject<{
|
|
267
|
+
previous: z.ZodNumber;
|
|
268
|
+
current: z.ZodNumber;
|
|
269
|
+
}, z.core.$strict>>;
|
|
270
|
+
summary: z.ZodArray<z.ZodString>;
|
|
271
|
+
changes: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
272
|
+
truncated: z.ZodBoolean;
|
|
273
|
+
}, z.core.$strict>;
|
|
274
|
+
};
|
|
275
|
+
readonly export: {
|
|
276
|
+
readonly direction: 'client-to-driver';
|
|
277
|
+
readonly params: z.ZodObject<{
|
|
278
|
+
records: z.ZodString;
|
|
279
|
+
output: z.ZodString;
|
|
280
|
+
out: z.ZodString;
|
|
281
|
+
}, z.core.$strict>;
|
|
282
|
+
readonly result: z.ZodObject<{
|
|
283
|
+
written: z.ZodNumber;
|
|
284
|
+
location: z.ZodString;
|
|
285
|
+
}, z.core.$strict>;
|
|
286
|
+
};
|
|
184
287
|
readonly shutdown: {
|
|
185
288
|
readonly direction: 'client-to-driver';
|
|
186
289
|
readonly params: undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { PROTOCOL_VERSION, MESSAGES, initializeParamsSchema, initializeResultSchema, validateParamsSchema, validateResultSchema, runParamsSchema, runResultSchema, cancelParamsSchema, cancelResultSchema, shutdownResultSchema, eventParamsSchema, recordParamsSchema, finishedParamsSchema, calloutParamsSchema, calloutResultSchema, } from './driver-message.contract.js';
|
|
2
|
-
export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult, RunParams, RunResult, CancelParams, CancelResult, EventParams, RecordParams, FinishedParams, CalloutParams, } from './driver-message.contract.js';
|
|
1
|
+
export { PROTOCOL_VERSION, MESSAGES, initializeParamsSchema, initializeResultSchema, validateParamsSchema, validateResultSchema, runParamsSchema, runResultSchema, cancelParamsSchema, cancelResultSchema, shutdownResultSchema, eventParamsSchema, recordParamsSchema, finishedParamsSchema, calloutParamsSchema, calloutResultSchema, probeParamsSchema, probeResultSchema, diffParamsSchema, diffResultSchema, exportParamsSchema, exportResultSchema, } from './driver-message.contract.js';
|
|
2
|
+
export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult, RunParams, RunResult, CancelParams, CancelResult, EventParams, RecordParams, FinishedParams, CalloutParams, ProbeParams, DiffParams, DiffResult, ExportParams, ExportResult, } from './driver-message.contract.js';
|
|
3
3
|
export { driverProtocolJsonSchema } from './driver-json-schema.mapper.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DiffParams, DiffResult } from '../protocol/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compares two runs' JSON Lines files by key: how many records were added, removed, changed and left
|
|
4
|
+
* alone, the report a person reads, and the changes themselves, up to `limit`.
|
|
5
|
+
*
|
|
6
|
+
* @param params - The two files, the key fields, the fields to ignore and the limit.
|
|
7
|
+
* @returns The comparison.
|
|
8
|
+
*/
|
|
9
|
+
export declare function diffRecordFiles(params: DiffParams): Promise<DiffResult>;
|
|
10
|
+
//# sourceMappingURL=diff-record-files.use-case.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExportParams, ExportResult } from '../protocol/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Writes a finished run's JSON Lines file again in another format, the extension of `out` naming it, as
|
|
4
|
+
* the CLI's `export` does. The output recipe gives the columns: its fields, in order, with their types.
|
|
5
|
+
*
|
|
6
|
+
* @param params - The records, the output recipe and the file to write.
|
|
7
|
+
* @returns How many records were written, and where.
|
|
8
|
+
* @throws RecipeValidationError When `output` is not an output recipe.
|
|
9
|
+
*/
|
|
10
|
+
export declare function exportRecordFiles(params: ExportParams): Promise<ExportResult>;
|
|
11
|
+
//# sourceMappingURL=export-record-files.use-case.d.ts.map
|
|
@@ -13,7 +13,7 @@ export interface RunSessions {
|
|
|
13
13
|
* itself, with its problems), answers with a run id at once, and crawls in the background: every crawl
|
|
14
14
|
* event goes to the client as an `event` notification, every record as a `record` one unless `out`
|
|
15
15
|
* names a file, and the end as `finished` with the report. A hook the client serves is asked back with
|
|
16
|
-
* a `callout` request. `cancel` closes the run's crawler, which stops its recipes; the run still ends
|
|
16
|
+
* a `callout` request, and so are the captcha solvers and access plugins it serves. `cancel` closes the run's crawler, which stops its recipes; the run still ends
|
|
17
17
|
* with `finished`.
|
|
18
18
|
*
|
|
19
19
|
* @param notify - The channel's notify.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AccessConfig, AccessPlugin } from '@opencraw/core';
|
|
2
|
+
import type { AskClient } from '../callout-bridge/index.js';
|
|
3
|
+
/** What a run or a probe asks of access: the config file, the profile, and the plugins the client serves. */
|
|
4
|
+
export interface SiteAccessParams {
|
|
5
|
+
accessConfig?: string;
|
|
6
|
+
access?: string;
|
|
7
|
+
accessPlugins?: string[];
|
|
8
|
+
}
|
|
9
|
+
/** How a run or a probe reaches sites. */
|
|
10
|
+
export interface SiteAccess {
|
|
11
|
+
access?: AccessConfig;
|
|
12
|
+
accessPlugins: AccessPlugin[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Loads the access config (the file the client names, else `OPENCRAW_ACCESS`), with `access` as its
|
|
16
|
+
* default profile, and builds a plugin for each one the client serves.
|
|
17
|
+
*
|
|
18
|
+
* @param params - The method's access fields.
|
|
19
|
+
* @param runId - The run, or the probe, the plugins' callouts belong to.
|
|
20
|
+
* @param ask - The channel's request.
|
|
21
|
+
* @param env - Where `OPENCRAW_ACCESS` is read.
|
|
22
|
+
* @returns The config and the plugins.
|
|
23
|
+
* @throws Error When a profile is named without a config, or the config has no such profile.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveSiteAccess(params: SiteAccessParams, runId: string, ask: AskClient, env?: NodeJS.ProcessEnv): Promise<SiteAccess>;
|
|
26
|
+
//# sourceMappingURL=resolve-site-access.use-case.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AskClient } from '../callout-bridge/index.js';
|
|
2
|
+
import type { ProbeParams } from '../protocol/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fetches a page, or renders it with `browser`, and says where its data lives: the probe the CLI and the
|
|
5
|
+
* MCP server use, through the access the client names (its plugins served by the client).
|
|
6
|
+
*
|
|
7
|
+
* @param params - The page and how to reach it.
|
|
8
|
+
* @param probeId - What the plugins' callouts carry as their `runId`.
|
|
9
|
+
* @param ask - The channel's request.
|
|
10
|
+
* @returns The probe's findings.
|
|
11
|
+
*/
|
|
12
|
+
export declare function probeSite(params: ProbeParams, probeId: string, ask: AskClient): Promise<Record<string, unknown>>;
|
|
13
|
+
//# sourceMappingURL=probe-site.use-case.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencraw/driver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCraw's driver: the engine over JSON-RPC on stdio, for clients in other languages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"schemas": "node tools/emit-protocol-schema.mjs"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@opencraw/cli": "^0.1.
|
|
44
|
-
"@opencraw/core": "^0.
|
|
43
|
+
"@opencraw/cli": "^0.1.56",
|
|
44
|
+
"@opencraw/core": "^0.3.0",
|
|
45
45
|
"zod": "^4.6.5"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://opencraw/schemas/driver-protocol.schema.json",
|
|
4
4
|
"title": "OpenCraw driver protocol",
|
|
5
5
|
"description": "JSON-RPC 2.0 over stdio, one message per line. Methods with a result are requests; the rest are notifications.",
|
|
6
|
-
"protocolVersion": "1.
|
|
6
|
+
"protocolVersion": "1.1.0",
|
|
7
7
|
"methods": {
|
|
8
8
|
"initialize": {
|
|
9
9
|
"direction": "client-to-driver",
|
|
@@ -41,6 +41,33 @@
|
|
|
41
41
|
"$ref": "#/$defs/cancel.result"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
+
"probe": {
|
|
45
|
+
"direction": "client-to-driver",
|
|
46
|
+
"params": {
|
|
47
|
+
"$ref": "#/$defs/probe.params"
|
|
48
|
+
},
|
|
49
|
+
"result": {
|
|
50
|
+
"$ref": "#/$defs/probe.result"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"diff": {
|
|
54
|
+
"direction": "client-to-driver",
|
|
55
|
+
"params": {
|
|
56
|
+
"$ref": "#/$defs/diff.params"
|
|
57
|
+
},
|
|
58
|
+
"result": {
|
|
59
|
+
"$ref": "#/$defs/diff.result"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"export": {
|
|
63
|
+
"direction": "client-to-driver",
|
|
64
|
+
"params": {
|
|
65
|
+
"$ref": "#/$defs/export.params"
|
|
66
|
+
},
|
|
67
|
+
"result": {
|
|
68
|
+
"$ref": "#/$defs/export.result"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
44
71
|
"shutdown": {
|
|
45
72
|
"direction": "client-to-driver",
|
|
46
73
|
"result": {
|
|
@@ -273,6 +300,24 @@
|
|
|
273
300
|
"items": {
|
|
274
301
|
"type": "string"
|
|
275
302
|
}
|
|
303
|
+
},
|
|
304
|
+
"captchaSolvers": {
|
|
305
|
+
"type": "array",
|
|
306
|
+
"items": {
|
|
307
|
+
"type": "string"
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"accessConfig": {
|
|
311
|
+
"type": "string"
|
|
312
|
+
},
|
|
313
|
+
"access": {
|
|
314
|
+
"type": "string"
|
|
315
|
+
},
|
|
316
|
+
"accessPlugins": {
|
|
317
|
+
"type": "array",
|
|
318
|
+
"items": {
|
|
319
|
+
"type": "string"
|
|
320
|
+
}
|
|
276
321
|
}
|
|
277
322
|
},
|
|
278
323
|
"additionalProperties": false
|
|
@@ -316,6 +361,192 @@
|
|
|
316
361
|
],
|
|
317
362
|
"additionalProperties": false
|
|
318
363
|
},
|
|
364
|
+
"probe.params": {
|
|
365
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
366
|
+
"type": "object",
|
|
367
|
+
"properties": {
|
|
368
|
+
"url": {
|
|
369
|
+
"type": "string",
|
|
370
|
+
"minLength": 1
|
|
371
|
+
},
|
|
372
|
+
"browser": {
|
|
373
|
+
"type": "boolean"
|
|
374
|
+
},
|
|
375
|
+
"browserPath": {
|
|
376
|
+
"type": "string"
|
|
377
|
+
},
|
|
378
|
+
"insecureTls": {
|
|
379
|
+
"type": "boolean"
|
|
380
|
+
},
|
|
381
|
+
"userAgent": {
|
|
382
|
+
"type": "string"
|
|
383
|
+
},
|
|
384
|
+
"accessConfig": {
|
|
385
|
+
"type": "string"
|
|
386
|
+
},
|
|
387
|
+
"access": {
|
|
388
|
+
"type": "string"
|
|
389
|
+
},
|
|
390
|
+
"accessPlugins": {
|
|
391
|
+
"type": "array",
|
|
392
|
+
"items": {
|
|
393
|
+
"type": "string"
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
"required": [
|
|
398
|
+
"url"
|
|
399
|
+
],
|
|
400
|
+
"additionalProperties": false
|
|
401
|
+
},
|
|
402
|
+
"probe.result": {
|
|
403
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
404
|
+
"type": "object",
|
|
405
|
+
"propertyNames": {
|
|
406
|
+
"type": "string"
|
|
407
|
+
},
|
|
408
|
+
"additionalProperties": {}
|
|
409
|
+
},
|
|
410
|
+
"diff.params": {
|
|
411
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
412
|
+
"type": "object",
|
|
413
|
+
"properties": {
|
|
414
|
+
"previous": {
|
|
415
|
+
"type": "string",
|
|
416
|
+
"minLength": 1
|
|
417
|
+
},
|
|
418
|
+
"current": {
|
|
419
|
+
"type": "string",
|
|
420
|
+
"minLength": 1
|
|
421
|
+
},
|
|
422
|
+
"key": {
|
|
423
|
+
"type": "array",
|
|
424
|
+
"items": {
|
|
425
|
+
"type": "string"
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
"ignore": {
|
|
429
|
+
"type": "array",
|
|
430
|
+
"items": {
|
|
431
|
+
"type": "string"
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
"limit": {
|
|
435
|
+
"type": "integer",
|
|
436
|
+
"minimum": 0,
|
|
437
|
+
"maximum": 9007199254740991
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
"required": [
|
|
441
|
+
"previous",
|
|
442
|
+
"current"
|
|
443
|
+
],
|
|
444
|
+
"additionalProperties": false
|
|
445
|
+
},
|
|
446
|
+
"diff.result": {
|
|
447
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
448
|
+
"type": "object",
|
|
449
|
+
"properties": {
|
|
450
|
+
"added": {
|
|
451
|
+
"type": "number"
|
|
452
|
+
},
|
|
453
|
+
"removed": {
|
|
454
|
+
"type": "number"
|
|
455
|
+
},
|
|
456
|
+
"changed": {
|
|
457
|
+
"type": "number"
|
|
458
|
+
},
|
|
459
|
+
"unchanged": {
|
|
460
|
+
"type": "number"
|
|
461
|
+
},
|
|
462
|
+
"shrunk": {
|
|
463
|
+
"type": "object",
|
|
464
|
+
"properties": {
|
|
465
|
+
"previous": {
|
|
466
|
+
"type": "number"
|
|
467
|
+
},
|
|
468
|
+
"current": {
|
|
469
|
+
"type": "number"
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
"required": [
|
|
473
|
+
"previous",
|
|
474
|
+
"current"
|
|
475
|
+
],
|
|
476
|
+
"additionalProperties": false
|
|
477
|
+
},
|
|
478
|
+
"summary": {
|
|
479
|
+
"type": "array",
|
|
480
|
+
"items": {
|
|
481
|
+
"type": "string"
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
"changes": {
|
|
485
|
+
"type": "array",
|
|
486
|
+
"items": {
|
|
487
|
+
"type": "object",
|
|
488
|
+
"propertyNames": {
|
|
489
|
+
"type": "string"
|
|
490
|
+
},
|
|
491
|
+
"additionalProperties": {}
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
"truncated": {
|
|
495
|
+
"type": "boolean"
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
"required": [
|
|
499
|
+
"added",
|
|
500
|
+
"removed",
|
|
501
|
+
"changed",
|
|
502
|
+
"unchanged",
|
|
503
|
+
"summary",
|
|
504
|
+
"changes",
|
|
505
|
+
"truncated"
|
|
506
|
+
],
|
|
507
|
+
"additionalProperties": false
|
|
508
|
+
},
|
|
509
|
+
"export.params": {
|
|
510
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
511
|
+
"type": "object",
|
|
512
|
+
"properties": {
|
|
513
|
+
"records": {
|
|
514
|
+
"type": "string",
|
|
515
|
+
"minLength": 1
|
|
516
|
+
},
|
|
517
|
+
"output": {
|
|
518
|
+
"type": "string",
|
|
519
|
+
"minLength": 1
|
|
520
|
+
},
|
|
521
|
+
"out": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"minLength": 1
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
"required": [
|
|
527
|
+
"records",
|
|
528
|
+
"output",
|
|
529
|
+
"out"
|
|
530
|
+
],
|
|
531
|
+
"additionalProperties": false
|
|
532
|
+
},
|
|
533
|
+
"export.result": {
|
|
534
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
535
|
+
"type": "object",
|
|
536
|
+
"properties": {
|
|
537
|
+
"written": {
|
|
538
|
+
"type": "number"
|
|
539
|
+
},
|
|
540
|
+
"location": {
|
|
541
|
+
"type": "string"
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
"required": [
|
|
545
|
+
"written",
|
|
546
|
+
"location"
|
|
547
|
+
],
|
|
548
|
+
"additionalProperties": false
|
|
549
|
+
},
|
|
319
550
|
"shutdown.result": {
|
|
320
551
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
321
552
|
"type": "object",
|