hobots 4.5.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/dist/{chunk-HKCDMUA3.js → chunk-6YTOYIZM.js} +2 -1
- package/dist/chunk-6YTOYIZM.js.map +1 -0
- package/dist/cli.cjs +250 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +1970 -1464
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -254
- package/dist/index.d.ts +125 -254
- package/dist/index.js +1970 -1465
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +1 -0
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +25 -3
- package/dist/protocol.d.ts +25 -3
- package/dist/protocol.js +1 -1
- package/docs/README.md +2 -2
- package/docs/api-reference.md +21 -7
- package/docs/concepts.md +11 -11
- package/docs/getting-started.md +1 -1
- package/docs/protocol.md +5 -5
- package/docs/runs.md +97 -23
- package/package.json +4 -1
- package/dist/chunk-HKCDMUA3.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
Occurrence: () => Occurrence,
|
|
33
34
|
captureException: () => captureException2,
|
|
34
35
|
close: () => close2,
|
|
35
36
|
default: () => src_default,
|
|
@@ -44,7 +45,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
44
45
|
|
|
45
46
|
// src/utils.ts
|
|
46
47
|
var SDK_NAME = "hobots";
|
|
47
|
-
var SDK_VERSION = true ? "
|
|
48
|
+
var SDK_VERSION = true ? "5.1.0" : "0.0.0-dev";
|
|
48
49
|
function isNode() {
|
|
49
50
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
50
51
|
}
|
|
@@ -97,8 +98,13 @@ function abortableSleep(ms, signal) {
|
|
|
97
98
|
function jitter(baseMs) {
|
|
98
99
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
99
100
|
}
|
|
101
|
+
var TRUNCATION_SUFFIX = "\u2026 [truncado]";
|
|
100
102
|
function truncate(value, max) {
|
|
101
|
-
return value.length > max ? `${value.slice(0, max)}
|
|
103
|
+
return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
|
|
104
|
+
}
|
|
105
|
+
function clip(value, max) {
|
|
106
|
+
if (value.length <= max) return value;
|
|
107
|
+
return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
|
|
102
108
|
}
|
|
103
109
|
function safeString(value) {
|
|
104
110
|
if (typeof value === "string") return value;
|
|
@@ -196,6 +202,8 @@ function colorize(text, color) {
|
|
|
196
202
|
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
197
203
|
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
198
204
|
var RUN_ID_ENV = "HOBOTS_RUN_ID";
|
|
205
|
+
var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
|
|
206
|
+
var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
|
|
199
207
|
var dotenvLoaded = false;
|
|
200
208
|
function loadDotenv() {
|
|
201
209
|
if (dotenvLoaded) return;
|
|
@@ -221,6 +229,24 @@ function resolveRunId(option) {
|
|
|
221
229
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
222
230
|
return process.env[RUN_ID_ENV]?.trim() || void 0;
|
|
223
231
|
}
|
|
232
|
+
function isLocalRun() {
|
|
233
|
+
if (typeof process === "undefined" || !process.env) return false;
|
|
234
|
+
return process.env[LOCAL_RUN_ENV]?.trim() === "1";
|
|
235
|
+
}
|
|
236
|
+
function resolveLocalRunParams() {
|
|
237
|
+
const raw = process.env[RUN_PARAMS_ENV]?.trim();
|
|
238
|
+
if (!raw) return {};
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = JSON.parse(raw);
|
|
242
|
+
} catch {
|
|
243
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
|
|
244
|
+
}
|
|
245
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
246
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
|
|
247
|
+
}
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
224
250
|
function resolveEnvironmentOption(option) {
|
|
225
251
|
const fromOption = option?.trim();
|
|
226
252
|
if (fromOption) return fromOption;
|
|
@@ -332,6 +358,7 @@ var protocol_limits_default = {
|
|
|
332
358
|
MAX_ITEM_PAYLOAD_BYTES: 16384,
|
|
333
359
|
MAX_ITEM_COLUMNS: 12,
|
|
334
360
|
MAX_ITEM_COLUMN_LABEL_CHARS: 60,
|
|
361
|
+
MAX_ITEMS_DONE_PER_PAGE: 1e3,
|
|
335
362
|
MAX_ATTACHMENT_BYTES: 5242880,
|
|
336
363
|
MAX_ATTACHMENTS_PER_RUN: 20,
|
|
337
364
|
MAX_ATTACHMENTS_PER_LOG_ENTRY: 4,
|
|
@@ -460,9 +487,18 @@ var MAX_FINGERPRINT_ITEMS = 10;
|
|
|
460
487
|
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
461
488
|
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
462
489
|
var MAX_STEPS = 200;
|
|
490
|
+
var MAX_STEP_DESCRIPTION_CHARS = 2048;
|
|
491
|
+
var MAX_STEP_OP_CHARS = 255;
|
|
463
492
|
function clipTags(tags) {
|
|
464
493
|
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
465
494
|
}
|
|
495
|
+
function clipStep(step) {
|
|
496
|
+
return {
|
|
497
|
+
...step,
|
|
498
|
+
...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
|
|
499
|
+
...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
466
502
|
function clipFingerprint(fingerprint) {
|
|
467
503
|
if (fingerprint === void 0) return void 0;
|
|
468
504
|
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
@@ -521,12 +557,13 @@ var Client = class {
|
|
|
521
557
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
522
558
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
523
559
|
}
|
|
524
|
-
|
|
560
|
+
/** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
|
|
561
|
+
captureException(error, hint, contexts) {
|
|
525
562
|
if (typeof error === "object" && error !== null) captured.add(error);
|
|
526
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
563
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
|
|
527
564
|
}
|
|
528
|
-
captureEvent(event, hint) {
|
|
529
|
-
const final = this.prepareEvent(event, hint);
|
|
565
|
+
captureEvent(event, hint, contexts) {
|
|
566
|
+
const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
|
|
530
567
|
this.transport.enqueue({ type: "event", payload: final });
|
|
531
568
|
return final.event_id;
|
|
532
569
|
}
|
|
@@ -549,14 +586,15 @@ var Client = class {
|
|
|
549
586
|
sendTransaction(payload) {
|
|
550
587
|
let steps = payload.steps;
|
|
551
588
|
if (steps.length > MAX_STEPS) {
|
|
552
|
-
this.logger.warn(`transaction com ${steps.length}
|
|
589
|
+
this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
553
590
|
steps = steps.slice(0, MAX_STEPS);
|
|
554
591
|
}
|
|
592
|
+
steps = steps.map(clipStep);
|
|
555
593
|
this.transport.enqueue({
|
|
556
594
|
type: "transaction",
|
|
557
595
|
payload: {
|
|
558
596
|
...payload,
|
|
559
|
-
name:
|
|
597
|
+
name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
560
598
|
steps,
|
|
561
599
|
release: payload.release ?? this.options.release,
|
|
562
600
|
environment: this.resolveEnvironment(payload.environment),
|
|
@@ -1129,6 +1167,36 @@ var TasksApi = class {
|
|
|
1129
1167
|
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
1130
1168
|
};
|
|
1131
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Uma página dos ids já concluídos neste trabalho (`items.each` pula). Retry em falha
|
|
1172
|
+
* de rede/5xx; 4xx é definitivo (404 = servidor sem o recurso).
|
|
1173
|
+
*/
|
|
1174
|
+
async listDoneItems(runId, request) {
|
|
1175
|
+
let response;
|
|
1176
|
+
try {
|
|
1177
|
+
response = await fetchWithRetry(
|
|
1178
|
+
() => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/items/done`, {
|
|
1179
|
+
redirect: "error",
|
|
1180
|
+
method: "POST",
|
|
1181
|
+
headers: this.headers(),
|
|
1182
|
+
body: JSON.stringify(request),
|
|
1183
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1184
|
+
}),
|
|
1185
|
+
{ logger: this.logger, label: `itens conclu\xEDdos da run ${runId}` }
|
|
1186
|
+
);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
throw new Error(`consulta de itens conclu\xEDdos da run ${runId} descartada ap\xF3s retries: ${String(error)}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (!response.ok) {
|
|
1191
|
+
throw new TasksApiError(`consulta de itens conclu\xEDdos rejeitada (HTTP ${response.status})`, response.status);
|
|
1192
|
+
}
|
|
1193
|
+
const body = await response.json().catch(() => null);
|
|
1194
|
+
if (!Array.isArray(body?.ids)) throw new TasksApiError("consulta de itens conclu\xEDdos: resposta malformada", 0);
|
|
1195
|
+
return {
|
|
1196
|
+
ids: body.ids.map((id) => String(id)),
|
|
1197
|
+
next: typeof body.next === "string" && body.next.length > 0 ? body.next : null
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1132
1200
|
/**
|
|
1133
1201
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
1134
1202
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1315,6 +1383,20 @@ var Backoff = class {
|
|
|
1315
1383
|
};
|
|
1316
1384
|
|
|
1317
1385
|
// src/tasks/errors.ts
|
|
1386
|
+
var Occurrence = class extends Error {
|
|
1387
|
+
payload;
|
|
1388
|
+
/** Print do item (bytes/caminho ou `hobots.image(...)`) — o `each` pendura no item. */
|
|
1389
|
+
image;
|
|
1390
|
+
constructor(message, payload, options) {
|
|
1391
|
+
super(message);
|
|
1392
|
+
this.name = "Occurrence";
|
|
1393
|
+
this.payload = payload;
|
|
1394
|
+
this.image = options?.image;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
function isOccurrence(error) {
|
|
1398
|
+
return error instanceof Error && error.name === "Occurrence";
|
|
1399
|
+
}
|
|
1318
1400
|
function runCanceledError() {
|
|
1319
1401
|
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1320
1402
|
}
|
|
@@ -1331,1507 +1413,1745 @@ function isCancellation(error) {
|
|
|
1331
1413
|
return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
|
|
1332
1414
|
}
|
|
1333
1415
|
|
|
1334
|
-
// src/tasks/
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
var
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
this.
|
|
1345
|
-
this.
|
|
1346
|
-
|
|
1347
|
-
index;
|
|
1348
|
-
id;
|
|
1349
|
-
step;
|
|
1350
|
-
basePayload;
|
|
1351
|
-
sink;
|
|
1352
|
-
reported = false;
|
|
1353
|
-
status;
|
|
1354
|
-
succeeded(payload, options) {
|
|
1355
|
-
this.record("succeeded", void 0, payload, options?.image);
|
|
1356
|
-
}
|
|
1357
|
-
failed(message, payload, options) {
|
|
1358
|
-
this.record("failed", message, payload, options?.image);
|
|
1359
|
-
if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
|
|
1360
|
-
}
|
|
1361
|
-
occurrence(message, payload, options) {
|
|
1362
|
-
this.record("occurrence", message, payload, options?.image);
|
|
1363
|
-
if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
|
|
1364
|
-
}
|
|
1365
|
-
record(status, message, payload, image3) {
|
|
1366
|
-
this.reported = true;
|
|
1367
|
-
this.status = status;
|
|
1368
|
-
const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
|
|
1369
|
-
this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
|
|
1416
|
+
// src/tasks/attachments.ts
|
|
1417
|
+
var MAX_ATTACHMENT_BYTES = WIRE_LIMITS.MAX_ATTACHMENT_BYTES;
|
|
1418
|
+
var MAX_ATTACHMENTS_PER_RUN = WIRE_LIMITS.MAX_ATTACHMENTS_PER_RUN;
|
|
1419
|
+
var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
|
|
1420
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1421
|
+
var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
|
|
1422
|
+
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1423
|
+
var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
|
|
1424
|
+
var ImageAttachment = class {
|
|
1425
|
+
constructor(input, caption, contentType) {
|
|
1426
|
+
this.input = input;
|
|
1427
|
+
this.caption = caption;
|
|
1428
|
+
this.contentType = contentType;
|
|
1370
1429
|
}
|
|
1430
|
+
input;
|
|
1431
|
+
caption;
|
|
1432
|
+
contentType;
|
|
1433
|
+
[IMAGE_BRAND] = true;
|
|
1371
1434
|
};
|
|
1372
|
-
function
|
|
1373
|
-
|
|
1374
|
-
if (raw === null || raw === void 0) return void 0;
|
|
1375
|
-
const text = safeString(raw);
|
|
1376
|
-
return text.length > 0 ? text : void 0;
|
|
1435
|
+
function image(input, options = {}) {
|
|
1436
|
+
return new ImageAttachment(input, options.caption, options.contentType);
|
|
1377
1437
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
|
|
1438
|
+
function isImageAttachment(value) {
|
|
1439
|
+
return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
|
|
1381
1440
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
sink.checkCanceled();
|
|
1389
|
-
const id = resolveId(options, element, index);
|
|
1390
|
-
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
1391
|
-
const step = sink.startStep(options.op ?? "item", description);
|
|
1392
|
-
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1393
|
-
try {
|
|
1394
|
-
await handler(element, handle, index);
|
|
1395
|
-
if (!handle.reported) handle.succeeded();
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
if (isTerminalError(error) || sink.isAborted()) {
|
|
1398
|
-
step?.setStatus("cancelled").succeeded();
|
|
1399
|
-
throw error;
|
|
1400
|
-
}
|
|
1401
|
-
const message = thrownMessage(error);
|
|
1402
|
-
if (!handle.reported) handle.failed(message);
|
|
1403
|
-
step?.failed(error);
|
|
1404
|
-
sink.logError(`item ${description} falhou: ${message}`);
|
|
1405
|
-
}
|
|
1406
|
-
step?.succeeded();
|
|
1407
|
-
if (options.progress !== false) sink.advance();
|
|
1408
|
-
result.total += 1;
|
|
1409
|
-
result[handle.status ?? "succeeded"] += 1;
|
|
1410
|
-
index += 1;
|
|
1441
|
+
function toImageAttachment(value) {
|
|
1442
|
+
return isImageAttachment(value) ? value : new ImageAttachment(value);
|
|
1443
|
+
}
|
|
1444
|
+
async function readImageFile(path) {
|
|
1445
|
+
if (!isNode()) {
|
|
1446
|
+
throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
|
|
1411
1447
|
}
|
|
1412
|
-
|
|
1448
|
+
const { readFile } = await import("fs/promises");
|
|
1449
|
+
return new Uint8Array(await readFile(path));
|
|
1413
1450
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
1418
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1419
|
-
function isFileRef(value) {
|
|
1420
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1421
|
-
const ref = value;
|
|
1422
|
-
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
1451
|
+
function startsWith(bytes, signature, offset = 0) {
|
|
1452
|
+
if (bytes.length < offset + signature.length) return false;
|
|
1453
|
+
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1423
1454
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1455
|
+
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1456
|
+
var MAX_PNG_DIMENSION = WIRE_LIMITS.MAX_IMAGE_DIMENSION_PX;
|
|
1457
|
+
var JPEG_MAGIC = [255, 216, 255];
|
|
1458
|
+
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1459
|
+
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
1460
|
+
function sniffContentType(bytes) {
|
|
1461
|
+
if (startsWith(bytes, PNG_MAGIC)) return "image/png";
|
|
1462
|
+
if (startsWith(bytes, JPEG_MAGIC)) return "image/jpeg";
|
|
1463
|
+
if (startsWith(bytes, RIFF_MAGIC) && startsWith(bytes, WEBP_MAGIC, 8)) return "image/webp";
|
|
1429
1464
|
return void 0;
|
|
1430
1465
|
}
|
|
1431
|
-
function
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
return
|
|
1466
|
+
function readPngDimensions(bytes) {
|
|
1467
|
+
if (!startsWith(bytes, PNG_MAGIC) || bytes.length < 24) return void 0;
|
|
1468
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1469
|
+
const width = view.getUint32(16);
|
|
1470
|
+
const height = view.getUint32(20);
|
|
1471
|
+
if (width === 0 || height === 0) return void 0;
|
|
1472
|
+
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1473
|
+
return { width, height };
|
|
1438
1474
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1475
|
+
async function toBytes(input) {
|
|
1476
|
+
if (typeof input === "string") return readImageFile(input);
|
|
1477
|
+
if (input instanceof Uint8Array) return input;
|
|
1478
|
+
return new Uint8Array(input);
|
|
1442
1479
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1480
|
+
|
|
1481
|
+
// src/tasks/run-context-store.ts
|
|
1482
|
+
var storage;
|
|
1483
|
+
var stepStorage;
|
|
1484
|
+
var initPromise;
|
|
1485
|
+
async function ensureStorage() {
|
|
1486
|
+
if (storage || !isNode()) return;
|
|
1487
|
+
initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
|
|
1488
|
+
storage = new ALS();
|
|
1489
|
+
stepStorage = new ALS();
|
|
1490
|
+
});
|
|
1491
|
+
await initPromise;
|
|
1446
1492
|
}
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1493
|
+
async function runWithRunContext(store, fn) {
|
|
1494
|
+
await ensureStorage();
|
|
1495
|
+
return storage ? storage.run(store, fn) : fn();
|
|
1449
1496
|
}
|
|
1450
|
-
function
|
|
1451
|
-
return
|
|
1497
|
+
function currentRunReporter() {
|
|
1498
|
+
return storage?.getStore()?.reporter;
|
|
1452
1499
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1500
|
+
async function runWithStepScope(scope, fn) {
|
|
1501
|
+
await ensureStorage();
|
|
1502
|
+
return stepStorage ? stepStorage.run(scope, fn) : fn();
|
|
1503
|
+
}
|
|
1504
|
+
function currentStep(transaction) {
|
|
1505
|
+
const scope = stepStorage?.getStore();
|
|
1506
|
+
return scope?.transaction === transaction ? scope.step : void 0;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/retry.ts
|
|
1510
|
+
var DEFAULT_RETRIES = 3;
|
|
1511
|
+
var DEFAULT_DELAY_MS = 1e3;
|
|
1512
|
+
function resolveDelay(delay, attempt, fallbackMs) {
|
|
1513
|
+
const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
|
|
1514
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1515
|
+
}
|
|
1516
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
1517
|
+
const requested = options.retries ?? DEFAULT_RETRIES;
|
|
1518
|
+
const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
|
|
1519
|
+
const wait = options.wait ?? sleep;
|
|
1520
|
+
for (let attempt = 1; ; attempt++) {
|
|
1521
|
+
if (options.signal?.aborted) throw runCanceledError();
|
|
1473
1522
|
try {
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
return value;
|
|
1483
|
-
}
|
|
1484
|
-
single(refOrField) {
|
|
1485
|
-
if (typeof refOrField === "string") {
|
|
1486
|
-
const value = this.field(refOrField);
|
|
1487
|
-
if (Array.isArray(value)) {
|
|
1488
|
-
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1489
|
-
}
|
|
1490
|
-
return value;
|
|
1523
|
+
return await fn(attempt);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const exhausted = attempt > retries;
|
|
1526
|
+
const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
|
|
1527
|
+
if (exhausted || !retriable) throw error;
|
|
1528
|
+
const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
|
|
1529
|
+
options.onRetry?.(error, attempt, delay);
|
|
1530
|
+
await wait(delay);
|
|
1491
1531
|
}
|
|
1492
|
-
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
1493
|
-
return refOrField;
|
|
1494
|
-
}
|
|
1495
|
-
async downloadAll(field, dest) {
|
|
1496
|
-
const value = this.field(field);
|
|
1497
|
-
const refs = Array.isArray(value) ? value : [value];
|
|
1498
|
-
const paths = [];
|
|
1499
|
-
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
1500
|
-
return paths;
|
|
1501
1532
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/transaction.ts
|
|
1536
|
+
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
1537
|
+
var DEFAULT_STEP_RETRIES = 1;
|
|
1538
|
+
var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
|
|
1539
|
+
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
1540
|
+
var MAX_BACKLOG = 100;
|
|
1541
|
+
function stepMessage(text) {
|
|
1542
|
+
return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
1543
|
+
}
|
|
1544
|
+
function stepId() {
|
|
1545
|
+
return uuid4().slice(0, 16);
|
|
1546
|
+
}
|
|
1547
|
+
function stepLabel(op, description) {
|
|
1548
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
1549
|
+
}
|
|
1550
|
+
function errorText(error) {
|
|
1551
|
+
if (error instanceof Error) return error.message || error.name;
|
|
1552
|
+
return safeString(error);
|
|
1553
|
+
}
|
|
1554
|
+
function errorReason(error) {
|
|
1555
|
+
return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
|
|
1556
|
+
}
|
|
1557
|
+
function levelForStatus(status) {
|
|
1558
|
+
switch (status) {
|
|
1559
|
+
case "warning":
|
|
1560
|
+
return "warning";
|
|
1561
|
+
case "error":
|
|
1562
|
+
return "error";
|
|
1563
|
+
case "cancelled":
|
|
1564
|
+
return "warning";
|
|
1565
|
+
default:
|
|
1566
|
+
return "success";
|
|
1511
1567
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1568
|
+
}
|
|
1569
|
+
function colorForStatus(status) {
|
|
1570
|
+
switch (status) {
|
|
1571
|
+
case "warning":
|
|
1572
|
+
return "yellow";
|
|
1573
|
+
case "error":
|
|
1574
|
+
return "red";
|
|
1575
|
+
case "cancelled":
|
|
1576
|
+
return "gray";
|
|
1577
|
+
default:
|
|
1578
|
+
return "green";
|
|
1518
1579
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1580
|
+
}
|
|
1581
|
+
var Step = class {
|
|
1582
|
+
constructor(transaction, parentStepId, op, description) {
|
|
1583
|
+
this.transaction = transaction;
|
|
1584
|
+
this.parentStepId = parentStepId;
|
|
1585
|
+
this.op = op;
|
|
1586
|
+
this.description = description;
|
|
1521
1587
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1588
|
+
transaction;
|
|
1589
|
+
parentStepId;
|
|
1590
|
+
op;
|
|
1591
|
+
description;
|
|
1592
|
+
stepId = stepId();
|
|
1593
|
+
startTimestamp = nowISO();
|
|
1594
|
+
status = "ok";
|
|
1595
|
+
endTimestamp;
|
|
1596
|
+
_data;
|
|
1597
|
+
// ── API do bot ────────────────────────────────────────────────────────────
|
|
1598
|
+
warning(message, options) {
|
|
1599
|
+
if (this.status !== "error") this._setStatus("warning", message);
|
|
1600
|
+
if (options?.image !== void 0) this.attachImage(options.image);
|
|
1601
|
+
return this;
|
|
1530
1602
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1603
|
+
image(image3) {
|
|
1604
|
+
this.attachImage(image3);
|
|
1605
|
+
return this;
|
|
1606
|
+
}
|
|
1607
|
+
data(key, value) {
|
|
1608
|
+
if (key === "attachments") return this;
|
|
1609
|
+
this._setData(key, value);
|
|
1610
|
+
return this;
|
|
1611
|
+
}
|
|
1612
|
+
// ── Internos (chamados pelo SDK) ──────────────────────────────────────────
|
|
1613
|
+
/** `data` da etapa como vai no wire (undefined quando vazio). */
|
|
1614
|
+
get payload() {
|
|
1615
|
+
return this._data;
|
|
1616
|
+
}
|
|
1617
|
+
_setStatus(status, message) {
|
|
1618
|
+
this.status = status;
|
|
1619
|
+
if (message) this._setData("message", stepMessage(safeString(message)));
|
|
1620
|
+
return this;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
|
|
1624
|
+
* valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
|
|
1625
|
+
*/
|
|
1626
|
+
_failed(error, message) {
|
|
1627
|
+
if (this.endTimestamp) return this;
|
|
1628
|
+
this.status = "error";
|
|
1629
|
+
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
1630
|
+
const raw = message ?? errorText(error);
|
|
1631
|
+
this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
|
|
1632
|
+
this._close();
|
|
1633
|
+
return this;
|
|
1634
|
+
}
|
|
1635
|
+
_setData(key, value) {
|
|
1636
|
+
this._data = { ...this._data, [key]: value };
|
|
1637
|
+
}
|
|
1638
|
+
_deleteData(key) {
|
|
1639
|
+
if (!this._data || !(key in this._data)) return;
|
|
1640
|
+
const rest = { ...this._data };
|
|
1641
|
+
delete rest[key];
|
|
1642
|
+
this._data = Object.keys(rest).length > 0 ? rest : void 0;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
|
|
1646
|
+
* `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
|
|
1647
|
+
* run não há onde pendurar a imagem — descarta com aviso único.
|
|
1648
|
+
*/
|
|
1649
|
+
attachImage(image3) {
|
|
1650
|
+
const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
|
|
1651
|
+
if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
|
|
1652
|
+
const id = this.transaction.attachImage(image3);
|
|
1653
|
+
if (!id) return;
|
|
1654
|
+
this._setData("attachments", [...current, id]);
|
|
1655
|
+
}
|
|
1656
|
+
/** Motivo registrado (warning/erro) — usado no log de FINISH. */
|
|
1657
|
+
reason() {
|
|
1658
|
+
const error = this._data?.error;
|
|
1659
|
+
const text = typeof error?.message === "string" ? error.message : this._data?.message;
|
|
1660
|
+
return typeof text === "string" && text ? text : void 0;
|
|
1661
|
+
}
|
|
1662
|
+
/** Fecha a etapa mantendo o status que estiver. Idempotente. */
|
|
1663
|
+
_close() {
|
|
1664
|
+
if (this.endTimestamp) return;
|
|
1665
|
+
this.endTimestamp = nowISO();
|
|
1666
|
+
this.transaction.emit(
|
|
1667
|
+
"step",
|
|
1668
|
+
"finish",
|
|
1669
|
+
stepLabel(this.op, this.description),
|
|
1670
|
+
this.startTimestamp,
|
|
1671
|
+
this.endTimestamp,
|
|
1672
|
+
this.status,
|
|
1673
|
+
this.reason()
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
toJSON() {
|
|
1677
|
+
return {
|
|
1678
|
+
step_id: this.stepId,
|
|
1679
|
+
...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
|
|
1680
|
+
...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
|
|
1681
|
+
...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
|
|
1682
|
+
status: this.status,
|
|
1683
|
+
start_timestamp: this.startTimestamp,
|
|
1684
|
+
timestamp: this.endTimestamp ?? nowISO(),
|
|
1685
|
+
...this._data ? { data: this._data } : {}
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
var Transaction = class {
|
|
1690
|
+
constructor(client, name, op, runId, options = {}) {
|
|
1691
|
+
this.client = client;
|
|
1692
|
+
this.name = name;
|
|
1693
|
+
this.op = op;
|
|
1694
|
+
this.runId = runId;
|
|
1695
|
+
this.echo = options.echo ?? false;
|
|
1696
|
+
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
1697
|
+
}
|
|
1698
|
+
client;
|
|
1699
|
+
name;
|
|
1700
|
+
op;
|
|
1701
|
+
runId;
|
|
1702
|
+
eventId = uuid4();
|
|
1703
|
+
stepId = stepId();
|
|
1704
|
+
startTimestamp = nowISO();
|
|
1705
|
+
status = "ok";
|
|
1706
|
+
steps = [];
|
|
1707
|
+
finished = false;
|
|
1708
|
+
lifecycleSink;
|
|
1709
|
+
attachmentSink;
|
|
1710
|
+
warnedNoAttachmentSink = false;
|
|
1711
|
+
// instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
|
|
1712
|
+
stepGuard;
|
|
1713
|
+
// modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
|
|
1714
|
+
echo;
|
|
1715
|
+
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
1716
|
+
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
1717
|
+
lifecycleBacklog = [];
|
|
1718
|
+
// ── Lifecycle / eco ───────────────────────────────────────────────────────
|
|
1719
|
+
/**
|
|
1720
|
+
* Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
|
|
1721
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
|
|
1722
|
+
* run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
|
|
1723
|
+
* etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
|
|
1724
|
+
*/
|
|
1725
|
+
emit(kind, phase, label, startTs, endTs, status, reason) {
|
|
1726
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
1727
|
+
const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
|
|
1728
|
+
const localFinish = this.echo && kind === "step" && phase === "finish";
|
|
1729
|
+
if (this.client?.debug || localFinish) {
|
|
1730
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
1731
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
1542
1732
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1733
|
+
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
1734
|
+
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
1735
|
+
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
1736
|
+
else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
|
|
1545
1737
|
}
|
|
1546
|
-
/**
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1738
|
+
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
1739
|
+
setLifecycleSink(sink) {
|
|
1740
|
+
this.lifecycleSink = sink;
|
|
1741
|
+
const backlog = this.lifecycleBacklog;
|
|
1742
|
+
this.lifecycleBacklog = [];
|
|
1743
|
+
for (const [level, message] of backlog) sink(level, message);
|
|
1744
|
+
}
|
|
1745
|
+
/** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
|
|
1746
|
+
setAttachmentSink(sink) {
|
|
1747
|
+
this.attachmentSink = sink;
|
|
1748
|
+
}
|
|
1749
|
+
/** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
|
|
1750
|
+
attachImage(image3) {
|
|
1751
|
+
if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
|
|
1752
|
+
if (!this.warnedNoAttachmentSink) {
|
|
1753
|
+
this.warnedNoAttachmentSink = true;
|
|
1754
|
+
console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
1557
1755
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1756
|
+
return void 0;
|
|
1757
|
+
}
|
|
1758
|
+
// ── Etapas ────────────────────────────────────────────────────────────────
|
|
1759
|
+
/**
|
|
1760
|
+
* Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
|
|
1761
|
+
* cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
|
|
1762
|
+
*/
|
|
1763
|
+
setStepGuard(guard) {
|
|
1764
|
+
this.stepGuard = guard;
|
|
1765
|
+
}
|
|
1766
|
+
/** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
|
|
1767
|
+
currentStep() {
|
|
1768
|
+
return currentStep(this);
|
|
1769
|
+
}
|
|
1770
|
+
/** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
|
|
1771
|
+
scope(step, fn) {
|
|
1772
|
+
if (!step) return fn();
|
|
1773
|
+
return runWithStepScope({ transaction: this, step }, fn);
|
|
1774
|
+
}
|
|
1775
|
+
/** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
|
|
1776
|
+
start(op, description) {
|
|
1777
|
+
this.stepGuard?.();
|
|
1778
|
+
const parent = this.currentStep();
|
|
1779
|
+
const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
|
|
1780
|
+
this.steps.push(step);
|
|
1781
|
+
this.emit("step", "start", stepLabel(op, description));
|
|
1782
|
+
return step;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
|
|
1786
|
+
* tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
|
|
1787
|
+
* Resolve com o retorno de `fn`.
|
|
1788
|
+
*
|
|
1789
|
+
* Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
|
|
1790
|
+
* mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
|
|
1791
|
+
* sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
|
|
1792
|
+
*/
|
|
1793
|
+
async step(name, fn, options = {}) {
|
|
1794
|
+
const requested = options.retries ?? DEFAULT_STEP_RETRIES;
|
|
1795
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
|
|
1796
|
+
const step = this.start(void 0, safeString(name));
|
|
1797
|
+
let attempts = 0;
|
|
1798
|
+
let lastFailure;
|
|
1573
1799
|
try {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
break;
|
|
1800
|
+
const result = await retryWithBackoff(
|
|
1801
|
+
(attempt) => {
|
|
1802
|
+
attempts = attempt;
|
|
1803
|
+
return this.scope(step, async () => fn(step));
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
retries: totalRetries,
|
|
1807
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
1808
|
+
signal: options.signal,
|
|
1809
|
+
wait: options.wait,
|
|
1810
|
+
onRetry: (error, attempt, delayMs) => {
|
|
1811
|
+
lastFailure = { error };
|
|
1812
|
+
options.warn?.(
|
|
1813
|
+
`etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
1814
|
+
);
|
|
1590
1815
|
}
|
|
1591
|
-
await handle.write(chunk.value);
|
|
1592
1816
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
1817
|
+
);
|
|
1818
|
+
this.noteAttempts(step, attempts);
|
|
1819
|
+
if (lastFailure && step.status === "ok") {
|
|
1820
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
1598
1821
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
await rm(part, { force: true });
|
|
1602
|
-
if (signal.aborted) throw runCanceledError();
|
|
1603
|
-
if (timedOut) {
|
|
1604
|
-
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
1605
|
-
}
|
|
1606
|
-
throw this.translate(error, ref);
|
|
1607
|
-
} finally {
|
|
1608
|
-
clearTimeout(timer);
|
|
1609
|
-
signal.removeEventListener("abort", onCancel);
|
|
1610
|
-
}
|
|
1611
|
-
try {
|
|
1612
|
-
await api.confirmFileDownload(runId, ref.id);
|
|
1822
|
+
step._close();
|
|
1823
|
+
return result;
|
|
1613
1824
|
} catch (error) {
|
|
1614
|
-
this.
|
|
1825
|
+
this.noteAttempts(step, attempts);
|
|
1826
|
+
if (isTerminalError(error)) step._setStatus("cancelled")._close();
|
|
1827
|
+
else step._failed(error);
|
|
1828
|
+
throw error;
|
|
1615
1829
|
}
|
|
1616
1830
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1831
|
+
noteAttempts(step, attempts) {
|
|
1832
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
1833
|
+
}
|
|
1834
|
+
// ── Metadados ─────────────────────────────────────────────────────────────
|
|
1835
|
+
setStatus(status) {
|
|
1836
|
+
this.status = status;
|
|
1837
|
+
return this;
|
|
1838
|
+
}
|
|
1839
|
+
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
1840
|
+
/**
|
|
1841
|
+
* Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
|
|
1842
|
+
* ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
|
|
1843
|
+
* `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
|
|
1844
|
+
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
1845
|
+
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
1846
|
+
* Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
|
|
1847
|
+
*/
|
|
1848
|
+
flush() {
|
|
1849
|
+
if (!this.client || this.finished) return 0;
|
|
1850
|
+
const ready = /* @__PURE__ */ new Set();
|
|
1851
|
+
for (const step of this.steps) {
|
|
1852
|
+
if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
|
|
1634
1853
|
}
|
|
1635
|
-
if (
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1854
|
+
if (ready.size === 0) return 0;
|
|
1855
|
+
const batch = [];
|
|
1856
|
+
const remaining = [];
|
|
1857
|
+
for (const step of this.steps) {
|
|
1858
|
+
if (ready.has(step.stepId) || ready.has(step.parentStepId)) {
|
|
1859
|
+
ready.add(step.stepId);
|
|
1860
|
+
batch.push(step);
|
|
1861
|
+
} else {
|
|
1862
|
+
remaining.push(step);
|
|
1644
1863
|
}
|
|
1645
|
-
await rename(part, target);
|
|
1646
|
-
} catch (error) {
|
|
1647
|
-
await rm(part, { force: true });
|
|
1648
|
-
throw error;
|
|
1649
1864
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1865
|
+
for (const step of batch) step._close();
|
|
1866
|
+
this.steps.splice(0, this.steps.length, ...remaining);
|
|
1867
|
+
for (let i = 0; i < batch.length; i += MAX_STEPS) {
|
|
1868
|
+
const chunk = batch.slice(i, i + MAX_STEPS);
|
|
1869
|
+
const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
|
|
1870
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
1871
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
1656
1872
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1873
|
+
return batch.length;
|
|
1874
|
+
}
|
|
1875
|
+
/** Entrega já (com teto) o que está na fila do transport — as transactions parciais inclusive. */
|
|
1876
|
+
async drain(timeoutMs) {
|
|
1877
|
+
await this.client?.flush(timeoutMs);
|
|
1878
|
+
}
|
|
1879
|
+
payload(eventId, op, status, startTimestamp, timestamp, steps) {
|
|
1880
|
+
return {
|
|
1881
|
+
event_id: eventId,
|
|
1882
|
+
name: this.name,
|
|
1883
|
+
op,
|
|
1884
|
+
status,
|
|
1885
|
+
start_timestamp: startTimestamp,
|
|
1886
|
+
timestamp,
|
|
1887
|
+
steps: steps.map((step) => step.toJSON()),
|
|
1888
|
+
// sem tags próprias: o client mescla as do processo
|
|
1889
|
+
tags: void 0,
|
|
1890
|
+
run_id: this.runId
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
finish() {
|
|
1894
|
+
if (this.finished) return;
|
|
1895
|
+
this.finished = true;
|
|
1896
|
+
for (const step of this.steps) step._close();
|
|
1897
|
+
const endTimestamp = nowISO();
|
|
1898
|
+
this.emit(
|
|
1899
|
+
"transaction",
|
|
1900
|
+
"finish",
|
|
1901
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
1902
|
+
this.startTimestamp,
|
|
1903
|
+
endTimestamp,
|
|
1904
|
+
this.status
|
|
1905
|
+
);
|
|
1906
|
+
if (!this.client) return;
|
|
1907
|
+
const steps = this.steps;
|
|
1908
|
+
let first = 0;
|
|
1909
|
+
while (steps.length - first > MAX_STEPS) {
|
|
1910
|
+
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
1911
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
1912
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
1913
|
+
first += MAX_STEPS;
|
|
1659
1914
|
}
|
|
1660
|
-
|
|
1915
|
+
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
1661
1916
|
}
|
|
1662
1917
|
};
|
|
1663
1918
|
|
|
1664
|
-
// src/
|
|
1665
|
-
var
|
|
1666
|
-
var
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1919
|
+
// src/tasks/each.ts
|
|
1920
|
+
var DEFAULT_ITEM_ID_FIELD = "id";
|
|
1921
|
+
var DEFAULT_EACH_RETRIES = 1;
|
|
1922
|
+
var ID_SEPARATOR = "|";
|
|
1923
|
+
function thrownMessage(error) {
|
|
1924
|
+
if (error instanceof Error) return error.message || error.name;
|
|
1925
|
+
return safeString(error);
|
|
1670
1926
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1927
|
+
var ItemHandle = class {
|
|
1928
|
+
constructor(index, id, step, basePayload, sink) {
|
|
1929
|
+
this.index = index;
|
|
1930
|
+
this.step = step;
|
|
1931
|
+
this.basePayload = basePayload;
|
|
1932
|
+
this.sink = sink;
|
|
1933
|
+
this.id = id ?? uuid4();
|
|
1934
|
+
}
|
|
1935
|
+
index;
|
|
1936
|
+
step;
|
|
1937
|
+
basePayload;
|
|
1938
|
+
sink;
|
|
1939
|
+
/** A identidade resolvida por `options.id` ou, sem ela, uma gerada — a mesma em todas as tentativas. */
|
|
1940
|
+
id;
|
|
1941
|
+
reported = false;
|
|
1942
|
+
status;
|
|
1943
|
+
attempt = 1;
|
|
1944
|
+
succeeded(payload, options) {
|
|
1945
|
+
this.record("succeeded", void 0, payload, options?.image);
|
|
1946
|
+
}
|
|
1947
|
+
failed(message, payload, options) {
|
|
1948
|
+
this.record("failed", message, payload, options?.image);
|
|
1949
|
+
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
1950
|
+
}
|
|
1951
|
+
occurrence(message, payload, options) {
|
|
1952
|
+
this.record("occurrence", message, payload, options?.image);
|
|
1953
|
+
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
1954
|
+
}
|
|
1955
|
+
/** Nova tentativa: o desfecho da anterior não vale (o id é o mesmo, o último reporte vence). */
|
|
1956
|
+
_nextAttempt(attempt) {
|
|
1957
|
+
this.attempt = attempt;
|
|
1958
|
+
this.reported = false;
|
|
1959
|
+
this.status = void 0;
|
|
1960
|
+
if (this.step && (this.step.status === "error" || this.step.status === "warning")) {
|
|
1961
|
+
this.step._setStatus("ok");
|
|
1962
|
+
this.step._deleteData("message");
|
|
1686
1963
|
}
|
|
1687
1964
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
|
|
1694
|
-
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1695
|
-
var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
|
|
1696
|
-
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1697
|
-
var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
|
|
1698
|
-
var ImageAttachment = class {
|
|
1699
|
-
constructor(input, caption, contentType) {
|
|
1700
|
-
this.input = input;
|
|
1701
|
-
this.caption = caption;
|
|
1702
|
-
this.contentType = contentType;
|
|
1965
|
+
record(status, message, payload, image3) {
|
|
1966
|
+
this.reported = true;
|
|
1967
|
+
this.status = status;
|
|
1968
|
+
const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
|
|
1969
|
+
this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
|
|
1703
1970
|
}
|
|
1704
|
-
input;
|
|
1705
|
-
caption;
|
|
1706
|
-
contentType;
|
|
1707
|
-
[IMAGE_BRAND] = true;
|
|
1708
1971
|
};
|
|
1709
|
-
function
|
|
1710
|
-
return
|
|
1972
|
+
function field(element, name) {
|
|
1973
|
+
if (typeof element !== "object" || element === null) return { present: false, value: void 0 };
|
|
1974
|
+
const record = element;
|
|
1975
|
+
return { present: name in record, value: record[name] };
|
|
1711
1976
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
1977
|
+
function resolveId(spec, element, index, explicit) {
|
|
1978
|
+
if (spec === null) return void 0;
|
|
1979
|
+
let raw;
|
|
1980
|
+
if (typeof spec === "function") {
|
|
1981
|
+
raw = spec(element, index);
|
|
1982
|
+
} else {
|
|
1983
|
+
const fields = spec === void 0 ? [DEFAULT_ITEM_ID_FIELD] : typeof spec === "string" ? [spec] : spec;
|
|
1984
|
+
const parts = [];
|
|
1985
|
+
for (const name of fields) {
|
|
1986
|
+
const { present, value } = field(element, name);
|
|
1987
|
+
if (!present || value === null || value === void 0) {
|
|
1988
|
+
if (explicit) {
|
|
1989
|
+
throw new Error(
|
|
1990
|
+
`items.each: elemento #${index + 1} n\xE3o tem o campo "${name}" informado em id \u2014 passe o nome certo, uma lista de campos ou uma fun\xE7\xE3o (elemento, \xEDndice) => id`
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
return void 0;
|
|
1994
|
+
}
|
|
1995
|
+
parts.push(safeString(value));
|
|
1996
|
+
}
|
|
1997
|
+
raw = parts.join(ID_SEPARATOR);
|
|
1998
|
+
}
|
|
1999
|
+
if (raw === null || raw === void 0) return void 0;
|
|
2000
|
+
const text = safeString(raw);
|
|
2001
|
+
return text.length > 0 ? text : void 0;
|
|
1714
2002
|
}
|
|
1715
|
-
function
|
|
1716
|
-
|
|
2003
|
+
function knownLength(list) {
|
|
2004
|
+
const length = list?.length;
|
|
2005
|
+
return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
|
|
1717
2006
|
}
|
|
1718
|
-
|
|
1719
|
-
if (
|
|
1720
|
-
|
|
2007
|
+
function skippedMessage(count) {
|
|
2008
|
+
if (count === 1) return "1 item pulado: j\xE1 conclu\xEDdo neste trabalho numa execu\xE7\xE3o anterior";
|
|
2009
|
+
return `${count} itens pulados: j\xE1 conclu\xEDdos neste trabalho numa execu\xE7\xE3o anterior`;
|
|
2010
|
+
}
|
|
2011
|
+
async function eachItem(list, handler, options, sink) {
|
|
2012
|
+
const result = { total: 0, succeeded: 0, failed: 0, occurrence: 0, skipped: 0 };
|
|
2013
|
+
const requested = options.retries ?? DEFAULT_EACH_RETRIES;
|
|
2014
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_EACH_RETRIES;
|
|
2015
|
+
const explicitId = options.id !== void 0;
|
|
2016
|
+
const length = options.progress === false ? void 0 : knownLength(list);
|
|
2017
|
+
if (length !== void 0) sink.declareTotal(length);
|
|
2018
|
+
const done = options.skipDone === false ? void 0 : await sink.doneItemIds();
|
|
2019
|
+
let index = 0;
|
|
2020
|
+
for await (const element of list) {
|
|
2021
|
+
sink.checkCanceled();
|
|
2022
|
+
const id = resolveId(options.id, element, index, explicitId);
|
|
2023
|
+
if (done?.size && id !== void 0 && done.has(id)) {
|
|
2024
|
+
result.total += 1;
|
|
2025
|
+
result.skipped += 1;
|
|
2026
|
+
if (options.progress !== false) sink.advance();
|
|
2027
|
+
index += 1;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
2031
|
+
const step = sink.startStep(options.op ?? "item", description);
|
|
2032
|
+
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
2033
|
+
const current = index;
|
|
2034
|
+
let attempts = 0;
|
|
2035
|
+
let lastFailure;
|
|
2036
|
+
try {
|
|
2037
|
+
await retryWithBackoff(
|
|
2038
|
+
(attempt) => {
|
|
2039
|
+
attempts = attempt;
|
|
2040
|
+
if (attempt > 1) handle._nextAttempt(attempt);
|
|
2041
|
+
return sink.stepScope(step, async () => handler(element, handle, current));
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
retries: totalRetries,
|
|
2045
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
2046
|
+
// sem nova tentativa: ocorrência (regra de negócio), desfecho já registrado pelo
|
|
2047
|
+
// handler (ele decidiu) ou run abortada (o erro é efeito do cancelamento)
|
|
2048
|
+
shouldRetry: (error) => !isOccurrence(error) && !handle.reported && !sink.isAborted(),
|
|
2049
|
+
wait: (ms) => sink.sleep(ms),
|
|
2050
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2051
|
+
lastFailure = { error };
|
|
2052
|
+
sink.logWarning(
|
|
2053
|
+
`item ${description}: tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
);
|
|
2058
|
+
if (!handle.reported) handle.succeeded();
|
|
2059
|
+
if (step) {
|
|
2060
|
+
noteAttempts(step, attempts);
|
|
2061
|
+
if (lastFailure && step.status === "ok") {
|
|
2062
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (isTerminalError(error) || sink.isAborted()) {
|
|
2067
|
+
step?._setStatus("cancelled")._close();
|
|
2068
|
+
throw error;
|
|
2069
|
+
}
|
|
2070
|
+
if (step) noteAttempts(step, attempts);
|
|
2071
|
+
if (isOccurrence(error)) {
|
|
2072
|
+
const occurrence = error;
|
|
2073
|
+
if (!handle.reported) handle.occurrence(occurrence.message, occurrence.payload, { image: occurrence.image });
|
|
2074
|
+
} else {
|
|
2075
|
+
const message = thrownMessage(error);
|
|
2076
|
+
if (!handle.reported) handle.failed(message);
|
|
2077
|
+
step?._failed(error);
|
|
2078
|
+
sink.logError(`item ${description} falhou: ${message}`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
step?._close();
|
|
2082
|
+
if (options.progress !== false) sink.advance();
|
|
2083
|
+
result.total += 1;
|
|
2084
|
+
result[handle.status ?? "succeeded"] += 1;
|
|
2085
|
+
index += 1;
|
|
2086
|
+
if (options.flush !== false) await sink.flush();
|
|
1721
2087
|
}
|
|
1722
|
-
|
|
1723
|
-
return
|
|
2088
|
+
if (result.skipped > 0) sink.logInfo(skippedMessage(result.skipped));
|
|
2089
|
+
return result;
|
|
1724
2090
|
}
|
|
1725
|
-
function
|
|
1726
|
-
if (
|
|
1727
|
-
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
2091
|
+
function noteAttempts(step, attempts) {
|
|
2092
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
1728
2093
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
var
|
|
1732
|
-
var
|
|
1733
|
-
var
|
|
1734
|
-
function
|
|
1735
|
-
if (
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
return void 0;
|
|
2094
|
+
|
|
2095
|
+
// src/tasks/files.ts
|
|
2096
|
+
var LOCAL_FILES_DIR_ENV = "HOBOTS_LOCAL_FILES_DIR";
|
|
2097
|
+
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
2098
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2099
|
+
function isFileRef(value) {
|
|
2100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2101
|
+
const ref = value;
|
|
2102
|
+
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
1739
2103
|
}
|
|
1740
|
-
function
|
|
1741
|
-
if (!
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1747
|
-
return { width, height };
|
|
2104
|
+
function fileField(params, field2) {
|
|
2105
|
+
if (!Object.prototype.hasOwnProperty.call(params, field2)) return void 0;
|
|
2106
|
+
const value = params[field2];
|
|
2107
|
+
if (isFileRef(value)) return value;
|
|
2108
|
+
if (Array.isArray(value) && value.every(isFileRef)) return value;
|
|
2109
|
+
return void 0;
|
|
1748
2110
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2111
|
+
function collectFileRefs(params) {
|
|
2112
|
+
const refs = [];
|
|
2113
|
+
for (const field2 of Object.keys(params)) {
|
|
2114
|
+
const value = fileField(params, field2);
|
|
2115
|
+
if (value !== void 0) refs.push(...Array.isArray(value) ? value : [value]);
|
|
2116
|
+
}
|
|
2117
|
+
return refs;
|
|
1753
2118
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
debug: "gray",
|
|
1758
|
-
info: "cyan",
|
|
1759
|
-
warning: "yellow",
|
|
1760
|
-
error: "red",
|
|
1761
|
-
success: "green"
|
|
1762
|
-
};
|
|
1763
|
-
function joinLogArgs(message, rest) {
|
|
1764
|
-
if (rest.length === 0) return safeString(message);
|
|
1765
|
-
return [message, ...rest].map(safeString).join(" ");
|
|
2119
|
+
function safeFileName(name, fallback) {
|
|
2120
|
+
const cleaned = name.replace(/^.*[\\/]/, "").replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
|
|
2121
|
+
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
1766
2122
|
}
|
|
1767
|
-
function
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
1771
|
-
const text = args.slice(0, -1);
|
|
1772
|
-
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
1773
|
-
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
2123
|
+
function isInsideDir(root, candidate, sep) {
|
|
2124
|
+
const base = root.endsWith(sep) ? root : root + sep;
|
|
2125
|
+
return candidate.startsWith(base) && candidate.length > base.length;
|
|
1774
2126
|
}
|
|
1775
|
-
function
|
|
1776
|
-
|
|
1777
|
-
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1778
|
-
console[method2](line, message);
|
|
2127
|
+
function unavailableLocal(ref) {
|
|
2128
|
+
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
1779
2129
|
}
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
const onAbort = () => {
|
|
1787
|
-
clearTimeout(timer);
|
|
1788
|
-
reject(runCanceledError());
|
|
1789
|
-
};
|
|
1790
|
-
const timer = setTimeout(() => {
|
|
1791
|
-
signal.removeEventListener("abort", onAbort);
|
|
1792
|
-
resolve2();
|
|
1793
|
-
}, ms);
|
|
1794
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1795
|
-
});
|
|
2130
|
+
function localRunPath(ref) {
|
|
2131
|
+
if (!isLocalRun()) return void 0;
|
|
2132
|
+
const path = ref.path;
|
|
2133
|
+
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
1796
2134
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
1800
|
-
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
1801
|
-
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
1802
|
-
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
1803
|
-
var FLUSH_AT_BUFFERED = 20;
|
|
1804
|
-
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
1805
|
-
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
1806
|
-
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
1807
|
-
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1808
|
-
var ITEMS_DRAIN_MS = 15e3;
|
|
1809
|
-
var PROGRESS_FLUSH_MS = 1e3;
|
|
1810
|
-
function scrubPayloadValue(value, depth) {
|
|
1811
|
-
if (typeof value === "string") return scrubSecrets(value);
|
|
1812
|
-
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1813
|
-
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1814
|
-
const out = {};
|
|
1815
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1816
|
-
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1817
|
-
}
|
|
1818
|
-
return out;
|
|
2135
|
+
function errorCode(error) {
|
|
2136
|
+
return error?.code;
|
|
1819
2137
|
}
|
|
1820
|
-
var
|
|
1821
|
-
constructor(
|
|
1822
|
-
this.
|
|
1823
|
-
this.
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
);
|
|
1831
|
-
const logAt = (level) => (message, ...rest) => {
|
|
1832
|
-
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
1833
|
-
this.log(level, text, image3);
|
|
1834
|
-
};
|
|
1835
|
-
const log = {
|
|
1836
|
-
debug: logAt("debug"),
|
|
1837
|
-
info: logAt("info"),
|
|
1838
|
-
warn: logAt("warning"),
|
|
1839
|
-
error: logAt("error"),
|
|
1840
|
-
success: logAt("success")
|
|
1841
|
-
};
|
|
1842
|
-
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1843
|
-
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
1844
|
-
const checkCanceled = () => this.checkCanceled();
|
|
1845
|
-
tx.setStepGuard(checkCanceled);
|
|
1846
|
-
const eachSink = {
|
|
1847
|
-
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
1848
|
-
startStep: (op, description) => tx.start(op, description),
|
|
1849
|
-
declareTotal: (total) => {
|
|
1850
|
-
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
1851
|
-
},
|
|
1852
|
-
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
1853
|
-
checkCanceled,
|
|
1854
|
-
isAborted: () => this.abortController.signal.aborted,
|
|
1855
|
-
logError: (message) => this.log("error", message)
|
|
1856
|
-
};
|
|
1857
|
-
const items = {
|
|
1858
|
-
add: (item) => {
|
|
1859
|
-
checkCanceled();
|
|
1860
|
-
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
1861
|
-
},
|
|
1862
|
-
succeeded: (payload, options) => {
|
|
1863
|
-
checkCanceled();
|
|
1864
|
-
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
1865
|
-
},
|
|
1866
|
-
failed: (message, payload, options) => {
|
|
1867
|
-
checkCanceled();
|
|
1868
|
-
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
1869
|
-
},
|
|
1870
|
-
occurrence: (message, payload, options) => {
|
|
1871
|
-
checkCanceled();
|
|
1872
|
-
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
1873
|
-
},
|
|
1874
|
-
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
2138
|
+
var RunFileStore = class {
|
|
2139
|
+
constructor(deps) {
|
|
2140
|
+
this.deps = deps;
|
|
2141
|
+
this.files = {
|
|
2142
|
+
list: () => collectFileRefs(deps.params),
|
|
2143
|
+
get: (field2) => fileField(deps.params, field2),
|
|
2144
|
+
// async: erro de uso (campo sem arquivo, valor inválido) chega como Promise rejeitada, nunca síncrono
|
|
2145
|
+
download: async (refOrField, options) => this.download(this.single(refOrField), options?.dest),
|
|
2146
|
+
downloadAll: async (field2, options) => this.downloadAll(field2, options?.dest),
|
|
2147
|
+
read: async (refOrField) => this.read(this.single(refOrField))
|
|
1875
2148
|
};
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2149
|
+
}
|
|
2150
|
+
deps;
|
|
2151
|
+
files;
|
|
2152
|
+
tempDir;
|
|
2153
|
+
/** Apaga a pasta temporária da execução (cópias que o bot não levou para `dest`). Nunca lança. */
|
|
2154
|
+
async cleanup() {
|
|
2155
|
+
const pending = this.tempDir;
|
|
2156
|
+
this.tempDir = void 0;
|
|
2157
|
+
if (!pending) return;
|
|
2158
|
+
try {
|
|
2159
|
+
const { rm } = await import("fs/promises");
|
|
2160
|
+
await rm(await pending, { recursive: true, force: true });
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
field(field2) {
|
|
2165
|
+
const value = fileField(this.deps.params, field2);
|
|
2166
|
+
if (value === void 0) throw new Error(`campo "${field2}" n\xE3o tem arquivo nos params`);
|
|
2167
|
+
return value;
|
|
2168
|
+
}
|
|
2169
|
+
single(refOrField) {
|
|
2170
|
+
if (typeof refOrField === "string") {
|
|
2171
|
+
const value = this.field(refOrField);
|
|
2172
|
+
if (Array.isArray(value)) {
|
|
2173
|
+
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1897
2174
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
1903
|
-
this.context = {
|
|
1904
|
-
runId,
|
|
1905
|
-
params,
|
|
1906
|
-
log,
|
|
1907
|
-
items,
|
|
1908
|
-
progress,
|
|
1909
|
-
files: this.fileStore.files,
|
|
1910
|
-
transaction: tx,
|
|
1911
|
-
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1912
|
-
signal,
|
|
1913
|
-
throwIfCanceled: checkCanceled,
|
|
1914
|
-
sleep: sleep2,
|
|
1915
|
-
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1916
|
-
};
|
|
2175
|
+
return value;
|
|
2176
|
+
}
|
|
2177
|
+
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
2178
|
+
return refOrField;
|
|
1917
2179
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
dead = false;
|
|
1925
|
-
chain = Promise.resolve();
|
|
1926
|
-
pendingUpdates = 0;
|
|
1927
|
-
timer;
|
|
1928
|
-
watchTimer;
|
|
1929
|
-
abortController = new AbortController();
|
|
1930
|
-
flushIntervalMs;
|
|
1931
|
-
cancelCheckIntervalMs;
|
|
1932
|
-
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1933
|
-
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1934
|
-
// envio preservada e sem lotes sobrepostos.
|
|
1935
|
-
itemsBuffer = [];
|
|
1936
|
-
itemsChain = Promise.resolve();
|
|
1937
|
-
itemsTimer;
|
|
1938
|
-
itemSeq = 0;
|
|
1939
|
-
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1940
|
-
itemsUnsupported = false;
|
|
1941
|
-
/**
|
|
1942
|
-
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
1943
|
-
* degrada com um aviso só.
|
|
1944
|
-
*/
|
|
1945
|
-
itemsQuotaExceeded = false;
|
|
1946
|
-
/**
|
|
1947
|
-
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
1948
|
-
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
1949
|
-
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
1950
|
-
*/
|
|
1951
|
-
itemsStorageFull = false;
|
|
1952
|
-
/**
|
|
1953
|
-
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1954
|
-
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1955
|
-
*/
|
|
1956
|
-
itemsClosed = false;
|
|
1957
|
-
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1958
|
-
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1959
|
-
pendingAttachments = /* @__PURE__ */ new Set();
|
|
1960
|
-
attachmentCount = 0;
|
|
1961
|
-
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
1962
|
-
uploadQueue = [];
|
|
1963
|
-
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1964
|
-
attachmentsUnsupported = false;
|
|
1965
|
-
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
1966
|
-
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
1967
|
-
progressCurrent = 0;
|
|
1968
|
-
progressTotal = null;
|
|
1969
|
-
/** Snapshot ainda não entregue ao servidor. */
|
|
1970
|
-
progressDirty = false;
|
|
1971
|
-
progressTimer;
|
|
1972
|
-
progressWarned = false;
|
|
1973
|
-
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1974
|
-
fileStore;
|
|
1975
|
-
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
1976
|
-
cleanupFiles() {
|
|
1977
|
-
return this.fileStore.cleanup();
|
|
2180
|
+
async downloadAll(field2, dest) {
|
|
2181
|
+
const value = this.field(field2);
|
|
2182
|
+
const refs = Array.isArray(value) ? value : [value];
|
|
2183
|
+
const paths = [];
|
|
2184
|
+
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
2185
|
+
return paths;
|
|
1978
2186
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
2187
|
+
async read(ref) {
|
|
2188
|
+
const { readFile, rm } = await import("fs/promises");
|
|
2189
|
+
const { dirname } = await import("path");
|
|
2190
|
+
const path = await this.download(ref, void 0);
|
|
2191
|
+
try {
|
|
2192
|
+
return await readFile(path);
|
|
2193
|
+
} finally {
|
|
2194
|
+
await rm(dirname(path), { recursive: true, force: true });
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async download(ref, dest, destIsDir = false) {
|
|
2198
|
+
this.checkCanceled();
|
|
2199
|
+
const target = await this.resolveTarget(ref, dest, destIsDir);
|
|
2200
|
+
if (ref.storage === "local") await this.copyLocal(ref, target);
|
|
2201
|
+
else await this.downloadS3(ref, target);
|
|
2202
|
+
return target;
|
|
1981
2203
|
}
|
|
1982
|
-
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
1983
2204
|
checkCanceled() {
|
|
1984
|
-
if (this.
|
|
2205
|
+
if (this.deps.signal.aborted) throw runCanceledError();
|
|
1985
2206
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2207
|
+
runTempDir() {
|
|
2208
|
+
this.tempDir ??= (async () => {
|
|
2209
|
+
const { mkdtemp } = await import("fs/promises");
|
|
2210
|
+
const { tmpdir } = await import("os");
|
|
2211
|
+
const { join } = await import("path");
|
|
2212
|
+
return mkdtemp(join(tmpdir(), "hobots-files-"));
|
|
2213
|
+
})();
|
|
2214
|
+
return this.tempDir;
|
|
1990
2215
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2216
|
+
async resolveTarget(ref, dest, destIsDir) {
|
|
2217
|
+
const { mkdir, mkdtemp, stat } = await import("fs/promises");
|
|
2218
|
+
const { dirname, join, resolve: resolve2 } = await import("path");
|
|
2219
|
+
const name = safeFileName(ref.name, "arquivo");
|
|
2220
|
+
if (dest === void 0) {
|
|
2221
|
+
return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
|
|
2222
|
+
}
|
|
2223
|
+
const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
|
|
2224
|
+
if (isDir) {
|
|
2225
|
+
await mkdir(dest, { recursive: true });
|
|
2226
|
+
return resolve2(dest, name);
|
|
2227
|
+
}
|
|
2228
|
+
await mkdir(dirname(resolve2(dest)), { recursive: true });
|
|
2229
|
+
return resolve2(dest);
|
|
2230
|
+
}
|
|
2231
|
+
/** storage 's3': URL pré-assinada → GET em streaming → confere o tamanho → só então DELETE. */
|
|
2232
|
+
async downloadS3(ref, target) {
|
|
2233
|
+
const { api, runId, signal } = this.deps;
|
|
2234
|
+
let expected;
|
|
2235
|
+
let url;
|
|
2236
|
+
try {
|
|
2237
|
+
const info = await api.requestFileDownload(runId, ref.id, signal);
|
|
2238
|
+
expected = info.size >= 0 ? info.size : ref.size;
|
|
2239
|
+
url = info.url;
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
throw this.translate(error, ref);
|
|
2242
|
+
}
|
|
2243
|
+
const { open, rename, rm } = await import("fs/promises");
|
|
2244
|
+
const part = `${target}.part`;
|
|
2245
|
+
const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
|
|
2246
|
+
const controller = new AbortController();
|
|
2247
|
+
let timedOut = false;
|
|
2248
|
+
let timer;
|
|
2249
|
+
const arm = () => {
|
|
2250
|
+
clearTimeout(timer);
|
|
2251
|
+
timer = setTimeout(() => {
|
|
2252
|
+
timedOut = true;
|
|
2253
|
+
controller.abort();
|
|
2254
|
+
}, idleTimeoutMs);
|
|
2255
|
+
};
|
|
2256
|
+
const onCancel = () => controller.abort();
|
|
2257
|
+
signal.addEventListener("abort", onCancel, { once: true });
|
|
2258
|
+
try {
|
|
2259
|
+
arm();
|
|
2260
|
+
const response = await api.fetchFile(url, controller.signal);
|
|
2261
|
+
const handle = await open(part, "w");
|
|
2262
|
+
let written = 0;
|
|
2263
|
+
try {
|
|
2264
|
+
const reader = response.body?.getReader();
|
|
2265
|
+
const stopReading = () => void reader?.cancel().catch(() => void 0);
|
|
2266
|
+
controller.signal.addEventListener("abort", stopReading, { once: true });
|
|
2267
|
+
for (; ; ) {
|
|
2268
|
+
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
2269
|
+
if (chunk.done) break;
|
|
2270
|
+
arm();
|
|
2271
|
+
written += chunk.value.byteLength;
|
|
2272
|
+
if (written > expected) {
|
|
2273
|
+
stopReading();
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
await handle.write(chunk.value);
|
|
2277
|
+
}
|
|
2278
|
+
} finally {
|
|
2279
|
+
await handle.close();
|
|
2280
|
+
}
|
|
2281
|
+
if (written !== expected) {
|
|
2282
|
+
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
2283
|
+
}
|
|
2284
|
+
await rename(part, target);
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
await rm(part, { force: true });
|
|
2287
|
+
if (signal.aborted) throw runCanceledError();
|
|
2288
|
+
if (timedOut) {
|
|
2289
|
+
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
2290
|
+
}
|
|
2291
|
+
throw this.translate(error, ref);
|
|
2292
|
+
} finally {
|
|
2293
|
+
clearTimeout(timer);
|
|
2294
|
+
signal.removeEventListener("abort", onCancel);
|
|
2295
|
+
}
|
|
2296
|
+
try {
|
|
2297
|
+
await api.confirmFileDownload(runId, ref.id);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
2007
2300
|
}
|
|
2008
|
-
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
2009
|
-
for (const resume of waiting) resume();
|
|
2010
2301
|
}
|
|
2011
2302
|
/**
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
* então preserva os logs mais recentes, os mais valiosos na morte do processo.
|
|
2303
|
+
* storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
|
|
2304
|
+
* origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
|
|
2015
2305
|
*/
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2306
|
+
async copyLocal(ref, target) {
|
|
2307
|
+
const direct = localRunPath(ref);
|
|
2308
|
+
if (direct !== void 0) {
|
|
2309
|
+
await this.copyFromPath(ref, direct, target);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
2313
|
+
if (!base) throw unavailableLocal(ref);
|
|
2314
|
+
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
2315
|
+
const { join, resolve: resolve2, sep } = await import("path");
|
|
2316
|
+
const root = resolve2(base);
|
|
2317
|
+
const folder = resolve2(root, ref.id);
|
|
2318
|
+
if (!isInsideDir(root, folder, sep) || !UUID_RE.test(ref.id)) {
|
|
2319
|
+
throw new Error(`id de arquivo local inv\xE1lido: ${JSON.stringify(ref.id)}`);
|
|
2320
|
+
}
|
|
2321
|
+
let names;
|
|
2322
|
+
try {
|
|
2323
|
+
names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
|
|
2326
|
+
throw error;
|
|
2327
|
+
}
|
|
2328
|
+
if (names.length === 0) throw unavailableLocal(ref);
|
|
2329
|
+
if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
|
|
2330
|
+
this.checkCanceled();
|
|
2331
|
+
const part = `${target}.part`;
|
|
2332
|
+
try {
|
|
2333
|
+
await copyFile(join(folder, names[0]), part);
|
|
2334
|
+
const { size } = await stat(part);
|
|
2335
|
+
if (size !== ref.size) {
|
|
2336
|
+
throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
|
|
2337
|
+
}
|
|
2338
|
+
await rename(part, target);
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
await rm(part, { force: true });
|
|
2341
|
+
throw error;
|
|
2342
|
+
}
|
|
2343
|
+
await rm(folder, { recursive: true, force: true });
|
|
2019
2344
|
}
|
|
2020
2345
|
/**
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
* intacta e o handler livre da latência do upload.
|
|
2346
|
+
* Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
|
|
2347
|
+
* Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
|
|
2024
2348
|
*/
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
this.
|
|
2349
|
+
async copyFromPath(ref, path, target) {
|
|
2350
|
+
const { copyFile, rename, rm, stat } = await import("fs/promises");
|
|
2351
|
+
const { resolve: resolve2 } = await import("path");
|
|
2352
|
+
const source = resolve2(path);
|
|
2353
|
+
const info = await stat(source).catch(() => void 0);
|
|
2354
|
+
if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
|
|
2355
|
+
this.checkCanceled();
|
|
2356
|
+
const part = `${target}.part`;
|
|
2357
|
+
try {
|
|
2358
|
+
await copyFile(source, part);
|
|
2359
|
+
await rename(part, target);
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
await rm(part, { force: true });
|
|
2362
|
+
throw error;
|
|
2363
|
+
}
|
|
2032
2364
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
if (
|
|
2036
|
-
|
|
2037
|
-
this.buffer.push({
|
|
2038
|
-
ts,
|
|
2039
|
-
level,
|
|
2040
|
-
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
2041
|
-
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
2042
|
-
});
|
|
2043
|
-
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
2044
|
-
this.flushLogs();
|
|
2045
|
-
} else {
|
|
2046
|
-
this.scheduleFlush();
|
|
2365
|
+
translate(error, ref) {
|
|
2366
|
+
if (this.deps.signal.aborted) return runCanceledError();
|
|
2367
|
+
if (error instanceof TasksApiError && error.status === 410) {
|
|
2368
|
+
return new TasksApiError(`arquivo "${ref.name}" j\xE1 baixado ou expirado`, 410);
|
|
2047
2369
|
}
|
|
2048
|
-
|
|
2370
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2371
|
+
return new TasksApiError(`arquivo "${ref.name}" n\xE3o encontrado nesta execu\xE7\xE3o`, 404);
|
|
2372
|
+
}
|
|
2373
|
+
return error;
|
|
2049
2374
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
if (
|
|
2085
|
-
|
|
2086
|
-
await Promise.race([
|
|
2087
|
-
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2088
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2089
|
-
]);
|
|
2090
|
-
}
|
|
2091
|
-
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
2092
|
-
async reportRunning() {
|
|
2093
|
-
this.enqueue({ status: "running" });
|
|
2094
|
-
await this.chain;
|
|
2095
|
-
if (!this.dead) this.startCancelWatch();
|
|
2096
|
-
return !this.dead;
|
|
2097
|
-
}
|
|
2098
|
-
reportSuccess() {
|
|
2099
|
-
this.stopCancelWatch();
|
|
2100
|
-
this.clearFlushTimer();
|
|
2101
|
-
this.forceFinalProgress();
|
|
2102
|
-
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
2103
|
-
}
|
|
2104
|
-
reportFailure(error) {
|
|
2105
|
-
this.stopCancelWatch();
|
|
2106
|
-
this.clearFlushTimer();
|
|
2107
|
-
this.forceFinalProgress();
|
|
2108
|
-
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
2112
|
-
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
2113
|
-
*/
|
|
2114
|
-
takeLogsForTerminal() {
|
|
2115
|
-
let logs = this.takeLogs();
|
|
2116
|
-
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
2117
|
-
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
2118
|
-
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
2119
|
-
}
|
|
2120
|
-
return logs;
|
|
2121
|
-
}
|
|
2122
|
-
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
2123
|
-
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
2124
|
-
setProgress(current, total) {
|
|
2125
|
-
if (this.dead) return;
|
|
2126
|
-
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
2127
|
-
if (!this.progressWarned) {
|
|
2128
|
-
this.progressWarned = true;
|
|
2129
|
-
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
2130
|
-
}
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
// src/tasks/context.ts
|
|
2378
|
+
var LOG_LEVEL_COLOR = {
|
|
2379
|
+
debug: "gray",
|
|
2380
|
+
info: "cyan",
|
|
2381
|
+
warning: "yellow",
|
|
2382
|
+
error: "red",
|
|
2383
|
+
success: "green"
|
|
2384
|
+
};
|
|
2385
|
+
function joinLogArgs(message, rest) {
|
|
2386
|
+
if (rest.length === 0) return safeString(message);
|
|
2387
|
+
return [message, ...rest].map(safeString).join(" ");
|
|
2388
|
+
}
|
|
2389
|
+
function splitLogArgs(message, rest) {
|
|
2390
|
+
const args = [message, ...rest];
|
|
2391
|
+
const last = args[args.length - 1];
|
|
2392
|
+
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
2393
|
+
const text = args.slice(0, -1);
|
|
2394
|
+
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
2395
|
+
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
2396
|
+
}
|
|
2397
|
+
function printRunLog(level, message) {
|
|
2398
|
+
const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
2399
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
2400
|
+
console[method2](line, message);
|
|
2401
|
+
}
|
|
2402
|
+
var PROGRESS_LINE_PREFIX = "[hobots:progress]";
|
|
2403
|
+
var PROGRESS_PRINT_THROTTLE_MS = 250;
|
|
2404
|
+
function printRunProgress(current, total) {
|
|
2405
|
+
console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
|
|
2406
|
+
}
|
|
2407
|
+
function cancelableSleep(ms, signal) {
|
|
2408
|
+
return new Promise((resolve2, reject) => {
|
|
2409
|
+
if (signal.aborted) {
|
|
2410
|
+
reject(runCanceledError());
|
|
2131
2411
|
return;
|
|
2132
2412
|
}
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
takeProgress() {
|
|
2166
|
-
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
2167
|
-
this.progressDirty = false;
|
|
2168
|
-
return { current: this.progressCurrent, total: this.progressTotal };
|
|
2169
|
-
}
|
|
2170
|
-
/**
|
|
2171
|
-
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
2172
|
-
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
2173
|
-
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
2174
|
-
*/
|
|
2175
|
-
startCancelWatch() {
|
|
2176
|
-
if (this.watchTimer) return;
|
|
2177
|
-
this.watchTimer = setInterval(() => {
|
|
2178
|
-
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2179
|
-
this.enqueue({ status: "running" });
|
|
2180
|
-
}, this.cancelCheckIntervalMs);
|
|
2181
|
-
unrefTimer(this.watchTimer);
|
|
2182
|
-
}
|
|
2183
|
-
stopCancelWatch() {
|
|
2184
|
-
if (this.watchTimer) {
|
|
2185
|
-
clearInterval(this.watchTimer);
|
|
2186
|
-
this.watchTimer = void 0;
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
2190
|
-
finalize() {
|
|
2191
|
-
return this.chain;
|
|
2192
|
-
}
|
|
2193
|
-
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
2194
|
-
/**
|
|
2195
|
-
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2196
|
-
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2197
|
-
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2198
|
-
*/
|
|
2199
|
-
addItem(status, message, payload, id, image3) {
|
|
2200
|
-
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2201
|
-
let scrubbedPayload;
|
|
2202
|
-
if (payload) {
|
|
2203
|
-
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2204
|
-
let serializedLength;
|
|
2205
|
-
try {
|
|
2206
|
-
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2207
|
-
} catch {
|
|
2208
|
-
}
|
|
2209
|
-
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2210
|
-
this.logger.warn(
|
|
2211
|
-
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2212
|
-
);
|
|
2213
|
-
scrubbedPayload = void 0;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
const ts = nowISO();
|
|
2217
|
-
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2218
|
-
this.itemsBuffer.push({
|
|
2219
|
-
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2220
|
-
seq: this.itemSeq++,
|
|
2221
|
-
status,
|
|
2222
|
-
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2223
|
-
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2224
|
-
ts,
|
|
2225
|
-
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2226
|
-
});
|
|
2227
|
-
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2228
|
-
this.flushItems();
|
|
2229
|
-
} else {
|
|
2230
|
-
this.scheduleItemsFlush();
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
scheduleItemsFlush() {
|
|
2234
|
-
if (this.itemsTimer) return;
|
|
2235
|
-
this.itemsTimer = setTimeout(() => {
|
|
2236
|
-
this.itemsTimer = void 0;
|
|
2237
|
-
this.flushItems();
|
|
2238
|
-
}, this.flushIntervalMs);
|
|
2239
|
-
unrefTimer(this.itemsTimer);
|
|
2240
|
-
}
|
|
2241
|
-
clearItemsTimer() {
|
|
2242
|
-
if (this.itemsTimer) {
|
|
2243
|
-
clearTimeout(this.itemsTimer);
|
|
2244
|
-
this.itemsTimer = void 0;
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
2248
|
-
flushItems() {
|
|
2249
|
-
this.clearItemsTimer();
|
|
2250
|
-
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2251
|
-
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2252
|
-
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2253
|
-
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2254
|
-
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
async sendItems(batch) {
|
|
2258
|
-
if (this.itemsUnsupported) return;
|
|
2259
|
-
try {
|
|
2260
|
-
await this.api.createItems(this.runId, { items: batch });
|
|
2261
|
-
this.itemsStorageFull = false;
|
|
2262
|
-
} catch (error) {
|
|
2263
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
2264
|
-
this.itemsUnsupported = true;
|
|
2265
|
-
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
2266
|
-
return;
|
|
2267
|
-
}
|
|
2268
|
-
if (error instanceof TasksApiError && error.status === 429) {
|
|
2269
|
-
if (!this.itemsQuotaExceeded) {
|
|
2270
|
-
this.itemsQuotaExceeded = true;
|
|
2271
|
-
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
2272
|
-
}
|
|
2273
|
-
return;
|
|
2274
|
-
}
|
|
2275
|
-
if (error instanceof TasksApiError && error.status === 402) {
|
|
2276
|
-
if (!this.itemsStorageFull) {
|
|
2277
|
-
this.itemsStorageFull = true;
|
|
2278
|
-
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2279
|
-
}
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
if (error instanceof TasksApiError) {
|
|
2283
|
-
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
2284
|
-
this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
|
|
2285
|
-
return;
|
|
2286
|
-
}
|
|
2287
|
-
this.itemsBuffer.unshift(...batch);
|
|
2288
|
-
this.scheduleItemsFlush();
|
|
2289
|
-
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
/**
|
|
2293
|
-
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
2294
|
-
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
2295
|
-
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2296
|
-
*/
|
|
2297
|
-
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2298
|
-
this.flushItems();
|
|
2299
|
-
await Promise.race([
|
|
2300
|
-
this.itemsChain,
|
|
2301
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2302
|
-
]);
|
|
2303
|
-
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2304
|
-
this.flushItems();
|
|
2305
|
-
await Promise.race([
|
|
2306
|
-
this.itemsChain,
|
|
2307
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2308
|
-
]);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2312
|
-
/**
|
|
2313
|
-
* Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
|
|
2314
|
-
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2315
|
-
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2316
|
-
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2317
|
-
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2318
|
-
* também cai aqui via `log()`.
|
|
2319
|
-
*/
|
|
2320
|
-
queueAttachment(imageIn, ts) {
|
|
2321
|
-
if (this.dead) return void 0;
|
|
2322
|
-
const image3 = toImageAttachment(imageIn);
|
|
2323
|
-
if (this.attachmentsUnsupported) return void 0;
|
|
2324
|
-
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
2325
|
-
if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
|
|
2326
|
-
this.attachmentCount++;
|
|
2327
|
-
this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
2328
|
-
}
|
|
2329
|
-
return void 0;
|
|
2330
|
-
}
|
|
2331
|
-
this.attachmentCount++;
|
|
2332
|
-
const id = uuid4();
|
|
2333
|
-
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
2334
|
-
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
2335
|
-
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
2336
|
-
this.pendingAttachments.add(work);
|
|
2337
|
-
void work.finally(() => this.pendingAttachments.delete(work));
|
|
2338
|
-
return id;
|
|
2413
|
+
const onAbort = () => {
|
|
2414
|
+
clearTimeout(timer);
|
|
2415
|
+
reject(runCanceledError());
|
|
2416
|
+
};
|
|
2417
|
+
const timer = setTimeout(() => {
|
|
2418
|
+
signal.removeEventListener("abort", onAbort);
|
|
2419
|
+
resolve2();
|
|
2420
|
+
}, ms);
|
|
2421
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
2425
|
+
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
2426
|
+
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
2427
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
2428
|
+
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
2429
|
+
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
2430
|
+
var FLUSH_AT_BUFFERED = 20;
|
|
2431
|
+
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
2432
|
+
var MAX_ITEMS_DONE_PER_PAGE = WIRE_LIMITS.MAX_ITEMS_DONE_PER_PAGE;
|
|
2433
|
+
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
2434
|
+
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
2435
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
2436
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
2437
|
+
var PROGRESS_FLUSH_MS = 1e3;
|
|
2438
|
+
function scrubPayloadValue(value, depth) {
|
|
2439
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
2440
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
2441
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
2442
|
+
const out = {};
|
|
2443
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2444
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
2339
2445
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
this.log(
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
)
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2446
|
+
return out;
|
|
2447
|
+
}
|
|
2448
|
+
var RunReporter = class {
|
|
2449
|
+
constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
2450
|
+
this.api = api;
|
|
2451
|
+
this.runId = runId;
|
|
2452
|
+
this.logger = logger;
|
|
2453
|
+
this.tx = tx;
|
|
2454
|
+
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
2455
|
+
this.cancelCheckIntervalMs = Math.min(
|
|
2456
|
+
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
2457
|
+
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
2458
|
+
);
|
|
2459
|
+
const logAt = (level) => (message, ...rest) => {
|
|
2460
|
+
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
2461
|
+
this.log(level, text, image3);
|
|
2462
|
+
};
|
|
2463
|
+
const log = {
|
|
2464
|
+
debug: logAt("debug"),
|
|
2465
|
+
info: logAt("info"),
|
|
2466
|
+
warn: logAt("warning"),
|
|
2467
|
+
error: logAt("error"),
|
|
2468
|
+
success: logAt("success")
|
|
2469
|
+
};
|
|
2470
|
+
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
2471
|
+
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
2472
|
+
const checkCanceled = () => this.checkCanceled();
|
|
2473
|
+
tx.setStepGuard(checkCanceled);
|
|
2474
|
+
const eachSink = {
|
|
2475
|
+
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
2476
|
+
startStep: (op, description) => tx.start(op, description),
|
|
2477
|
+
stepScope: (step, fn) => tx.scope(step, fn),
|
|
2478
|
+
declareTotal: (total) => {
|
|
2479
|
+
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
2480
|
+
},
|
|
2481
|
+
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
2482
|
+
checkCanceled,
|
|
2483
|
+
isAborted: () => this.abortController.signal.aborted,
|
|
2484
|
+
logInfo: (message) => this.log("info", message),
|
|
2485
|
+
logWarning: (message) => this.log("warning", message),
|
|
2486
|
+
logError: (message) => this.log("error", message),
|
|
2487
|
+
sleep: (ms) => cancelableSleep(ms, this.abortController.signal),
|
|
2488
|
+
doneItemIds: () => this.listDoneItemIds(),
|
|
2489
|
+
flush: () => this.flush()
|
|
2490
|
+
};
|
|
2491
|
+
const items = {
|
|
2492
|
+
add: (item) => {
|
|
2493
|
+
checkCanceled();
|
|
2494
|
+
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
2495
|
+
},
|
|
2496
|
+
succeeded: (payload, options) => {
|
|
2497
|
+
checkCanceled();
|
|
2498
|
+
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
2499
|
+
},
|
|
2500
|
+
failed: (message, payload, options) => {
|
|
2501
|
+
checkCanceled();
|
|
2502
|
+
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
2503
|
+
},
|
|
2504
|
+
occurrence: (message, payload, options) => {
|
|
2505
|
+
checkCanceled();
|
|
2506
|
+
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
2507
|
+
},
|
|
2508
|
+
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
2509
|
+
};
|
|
2510
|
+
const progress = Object.defineProperties(
|
|
2511
|
+
{
|
|
2512
|
+
total: (total) => {
|
|
2513
|
+
checkCanceled();
|
|
2514
|
+
this.setProgress(this.progressCurrent, total);
|
|
2374
2515
|
},
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2516
|
+
advance: (count = 1) => {
|
|
2517
|
+
checkCanceled();
|
|
2518
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
2519
|
+
},
|
|
2520
|
+
set: (current, total) => {
|
|
2521
|
+
checkCanceled();
|
|
2522
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
2523
|
+
}
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
current: { enumerable: true, get: () => this.progressCurrent },
|
|
2527
|
+
snapshot: {
|
|
2528
|
+
enumerable: true,
|
|
2529
|
+
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
2530
|
+
}
|
|
2390
2531
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2532
|
+
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
2533
|
+
);
|
|
2534
|
+
const signal = this.abortController.signal;
|
|
2535
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
2536
|
+
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
2537
|
+
this.context = {
|
|
2538
|
+
runId,
|
|
2539
|
+
params,
|
|
2540
|
+
log,
|
|
2541
|
+
items,
|
|
2542
|
+
progress,
|
|
2543
|
+
files: this.fileStore.files,
|
|
2544
|
+
step: async (name, fn, options) => {
|
|
2545
|
+
const root = tx.currentStep() === void 0;
|
|
2546
|
+
try {
|
|
2547
|
+
return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
|
|
2548
|
+
} finally {
|
|
2549
|
+
if (options?.flush !== false && root && !signal.aborted) await this.flush();
|
|
2550
|
+
}
|
|
2551
|
+
},
|
|
2552
|
+
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
2553
|
+
signal,
|
|
2554
|
+
throwIfCanceled: checkCanceled,
|
|
2555
|
+
sleep: sleep2,
|
|
2556
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
2410
2557
|
};
|
|
2411
|
-
if (this.uploadSlots > 0) {
|
|
2412
|
-
this.uploadSlots--;
|
|
2413
|
-
return Promise.resolve(release);
|
|
2414
|
-
}
|
|
2415
|
-
return new Promise((resolve2) => {
|
|
2416
|
-
this.uploadQueue.push(() => resolve2(release));
|
|
2417
|
-
});
|
|
2418
2558
|
}
|
|
2559
|
+
api;
|
|
2560
|
+
runId;
|
|
2561
|
+
logger;
|
|
2562
|
+
tx;
|
|
2563
|
+
context;
|
|
2564
|
+
buffer = [];
|
|
2565
|
+
dead = false;
|
|
2566
|
+
chain = Promise.resolve();
|
|
2567
|
+
pendingUpdates = 0;
|
|
2568
|
+
timer;
|
|
2569
|
+
watchTimer;
|
|
2570
|
+
abortController = new AbortController();
|
|
2571
|
+
flushIntervalMs;
|
|
2572
|
+
cancelCheckIntervalMs;
|
|
2573
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
2574
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
2575
|
+
// envio preservada e sem lotes sobrepostos.
|
|
2576
|
+
itemsBuffer = [];
|
|
2577
|
+
itemsChain = Promise.resolve();
|
|
2578
|
+
itemsTimer;
|
|
2579
|
+
itemSeq = 0;
|
|
2580
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
2581
|
+
itemsUnsupported = false;
|
|
2419
2582
|
/**
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2583
|
+
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
2584
|
+
* degrada com um aviso só.
|
|
2422
2585
|
*/
|
|
2423
|
-
|
|
2424
|
-
if (this.pendingAttachments.size === 0) return;
|
|
2425
|
-
await Promise.race([
|
|
2426
|
-
Promise.allSettled([...this.pendingAttachments]),
|
|
2427
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2428
|
-
]);
|
|
2429
|
-
}
|
|
2430
|
-
enqueue(updateIn) {
|
|
2431
|
-
if (this.dead) {
|
|
2432
|
-
this.salvageLogs(updateIn.logs);
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
this.clearProgressTimer();
|
|
2436
|
-
const progress = this.takeProgress();
|
|
2437
|
-
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
2438
|
-
this.pendingUpdates++;
|
|
2439
|
-
this.chain = this.chain.then(async () => {
|
|
2440
|
-
if (this.dead) {
|
|
2441
|
-
this.salvageLogs(update.logs);
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
2445
|
-
try {
|
|
2446
|
-
await this.api.updateRun(this.runId, payload);
|
|
2447
|
-
} catch (error) {
|
|
2448
|
-
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
2449
|
-
this.dead = true;
|
|
2450
|
-
this.stopCancelWatch();
|
|
2451
|
-
this.abort();
|
|
2452
|
-
const pending = this.buffer.splice(0, this.buffer.length);
|
|
2453
|
-
this.salvageLogs(pending);
|
|
2454
|
-
this.logger.warn(
|
|
2455
|
-
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
2456
|
-
);
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
if (update.logs && update.logs.length > 0) {
|
|
2460
|
-
this.buffer.unshift(...update.logs);
|
|
2461
|
-
this.scheduleFlush();
|
|
2462
|
-
}
|
|
2463
|
-
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
2464
|
-
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
2465
|
-
}
|
|
2466
|
-
}).finally(() => {
|
|
2467
|
-
this.pendingUpdates--;
|
|
2468
|
-
});
|
|
2469
|
-
}
|
|
2586
|
+
itemsQuotaExceeded = false;
|
|
2470
2587
|
/**
|
|
2471
|
-
*
|
|
2472
|
-
*
|
|
2473
|
-
* o
|
|
2588
|
+
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
2589
|
+
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
2590
|
+
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
2474
2591
|
*/
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2505
|
-
function stepMessage(text) {
|
|
2506
|
-
return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
2507
|
-
}
|
|
2508
|
-
function stepId() {
|
|
2509
|
-
return uuid4().slice(0, 16);
|
|
2510
|
-
}
|
|
2511
|
-
function stepLabel(op, description) {
|
|
2512
|
-
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
2513
|
-
}
|
|
2514
|
-
function levelForStatus(status) {
|
|
2515
|
-
switch (status) {
|
|
2516
|
-
case "warning":
|
|
2517
|
-
return "warning";
|
|
2518
|
-
case "error":
|
|
2519
|
-
return "error";
|
|
2520
|
-
case "cancelled":
|
|
2521
|
-
return "warning";
|
|
2522
|
-
default:
|
|
2523
|
-
return "success";
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
function colorForStatus(status) {
|
|
2527
|
-
switch (status) {
|
|
2528
|
-
case "warning":
|
|
2529
|
-
return "yellow";
|
|
2530
|
-
case "error":
|
|
2531
|
-
return "red";
|
|
2532
|
-
case "cancelled":
|
|
2533
|
-
return "gray";
|
|
2534
|
-
default:
|
|
2535
|
-
return "green";
|
|
2592
|
+
itemsStorageFull = false;
|
|
2593
|
+
/**
|
|
2594
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
2595
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
2596
|
+
*/
|
|
2597
|
+
itemsClosed = false;
|
|
2598
|
+
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
2599
|
+
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
2600
|
+
pendingAttachments = /* @__PURE__ */ new Set();
|
|
2601
|
+
attachmentCount = 0;
|
|
2602
|
+
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
2603
|
+
uploadQueue = [];
|
|
2604
|
+
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
2605
|
+
attachmentsUnsupported = false;
|
|
2606
|
+
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
2607
|
+
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
2608
|
+
progressCurrent = 0;
|
|
2609
|
+
progressTotal = null;
|
|
2610
|
+
/** Snapshot ainda não entregue ao servidor. */
|
|
2611
|
+
progressDirty = false;
|
|
2612
|
+
progressTimer;
|
|
2613
|
+
progressWarned = false;
|
|
2614
|
+
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
2615
|
+
progressLastPrint = 0;
|
|
2616
|
+
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
2617
|
+
fileStore;
|
|
2618
|
+
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
2619
|
+
cleanupFiles() {
|
|
2620
|
+
return this.fileStore.cleanup();
|
|
2536
2621
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
this.
|
|
2543
|
-
|
|
2622
|
+
get isDead() {
|
|
2623
|
+
return this.dead;
|
|
2624
|
+
}
|
|
2625
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
2626
|
+
checkCanceled() {
|
|
2627
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
2628
|
+
}
|
|
2629
|
+
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
2630
|
+
abort() {
|
|
2631
|
+
this.stopCancelWatch();
|
|
2632
|
+
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
2544
2633
|
}
|
|
2545
|
-
transaction;
|
|
2546
|
-
parentStepId;
|
|
2547
|
-
op;
|
|
2548
|
-
description;
|
|
2549
|
-
stepId = stepId();
|
|
2550
|
-
startTimestamp = nowISO();
|
|
2551
|
-
status = "ok";
|
|
2552
|
-
endTimestamp;
|
|
2553
|
-
data;
|
|
2554
2634
|
/**
|
|
2555
|
-
*
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2635
|
+
* Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
|
|
2636
|
+
* Usado no exit path — depois que o agente reportou AgentTerminated por fora
|
|
2637
|
+
* da fila, um handler zumbi não pode reenviar status por cima.
|
|
2558
2638
|
*/
|
|
2559
|
-
|
|
2560
|
-
this.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2639
|
+
kill() {
|
|
2640
|
+
this.abort();
|
|
2641
|
+
this.dead = true;
|
|
2642
|
+
this.clearFlushTimer();
|
|
2643
|
+
this.clearProgressTimer();
|
|
2644
|
+
this.buffer = [];
|
|
2645
|
+
this.itemsClosed = true;
|
|
2646
|
+
this.clearItemsTimer();
|
|
2647
|
+
if (this.itemsBuffer.length > 0) {
|
|
2648
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
2649
|
+
this.itemsBuffer = [];
|
|
2650
|
+
}
|
|
2651
|
+
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
2652
|
+
for (const resume of waiting) resume();
|
|
2564
2653
|
}
|
|
2565
2654
|
/**
|
|
2566
|
-
*
|
|
2567
|
-
*
|
|
2568
|
-
*
|
|
2569
|
-
* Numa step já fechada não faz nada.
|
|
2655
|
+
* Drena o buffer pendente para o PATCH único do exit path (chame ANTES de kill,
|
|
2656
|
+
* que descarta o buffer). Capado em um chunk — o exit path não tem retry nem fila,
|
|
2657
|
+
* então preserva os logs mais recentes, os mais valiosos na morte do processo.
|
|
2570
2658
|
*/
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
this.
|
|
2574
|
-
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
2575
|
-
const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
|
|
2576
|
-
this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
|
|
2577
|
-
if (options?.image !== void 0) this.attachImage(options.image);
|
|
2578
|
-
this.close();
|
|
2579
|
-
return this;
|
|
2659
|
+
drainLogs() {
|
|
2660
|
+
this.clearFlushTimer();
|
|
2661
|
+
return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
|
|
2580
2662
|
}
|
|
2581
2663
|
/**
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2664
|
+
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
2665
|
+
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
2666
|
+
* intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
|
|
2667
|
+
* imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
|
|
2585
2668
|
*/
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2669
|
+
log(level, message, image3) {
|
|
2670
|
+
if (this.dead) return void 0;
|
|
2671
|
+
const scrubbed = scrubSecrets(safeString(message));
|
|
2672
|
+
const ts = nowISO();
|
|
2673
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2674
|
+
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
2675
|
+
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2676
|
+
return attachmentId;
|
|
2592
2677
|
}
|
|
2593
|
-
/**
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
const
|
|
2597
|
-
|
|
2678
|
+
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
2679
|
+
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
2680
|
+
if (this.dead) return ts;
|
|
2681
|
+
const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
|
|
2682
|
+
this.buffer.push({
|
|
2683
|
+
ts,
|
|
2684
|
+
level,
|
|
2685
|
+
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
2686
|
+
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
2687
|
+
});
|
|
2688
|
+
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
2689
|
+
this.flushLogs();
|
|
2690
|
+
} else {
|
|
2691
|
+
this.scheduleFlush();
|
|
2692
|
+
}
|
|
2693
|
+
return ts;
|
|
2598
2694
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2695
|
+
scheduleFlush() {
|
|
2696
|
+
if (this.timer) return;
|
|
2697
|
+
this.timer = setTimeout(() => {
|
|
2698
|
+
this.timer = void 0;
|
|
2699
|
+
this.flushLogs();
|
|
2700
|
+
}, this.flushIntervalMs);
|
|
2701
|
+
unrefTimer(this.timer);
|
|
2602
2702
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2703
|
+
clearFlushTimer() {
|
|
2704
|
+
if (this.timer) {
|
|
2705
|
+
clearTimeout(this.timer);
|
|
2706
|
+
this.timer = void 0;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
takeLogs() {
|
|
2710
|
+
return this.buffer.splice(0, this.buffer.length);
|
|
2711
|
+
}
|
|
2712
|
+
/** Flush incremental: envia o buffer como status 'running' (em chunks). */
|
|
2713
|
+
flushLogs() {
|
|
2714
|
+
this.clearFlushTimer();
|
|
2715
|
+
if (this.dead || this.buffer.length === 0) return;
|
|
2716
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
2717
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
2718
|
+
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
2719
|
+
}
|
|
2606
2720
|
}
|
|
2607
2721
|
/**
|
|
2608
|
-
*
|
|
2609
|
-
*
|
|
2722
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
|
|
2723
|
+
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2724
|
+
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2610
2725
|
*/
|
|
2611
|
-
|
|
2612
|
-
this.
|
|
2613
|
-
|
|
2726
|
+
async flush(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2727
|
+
this.tx.flush();
|
|
2728
|
+
this.flushItems();
|
|
2729
|
+
if (this.progressDirty) this.flushProgress();
|
|
2730
|
+
else this.flushLogs();
|
|
2731
|
+
await Promise.race([
|
|
2732
|
+
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2733
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2734
|
+
]);
|
|
2614
2735
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
this.
|
|
2618
|
-
this.
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
stepLabel(this.op, this.description),
|
|
2622
|
-
this.startTimestamp,
|
|
2623
|
-
this.endTimestamp,
|
|
2624
|
-
this.status,
|
|
2625
|
-
this.reason()
|
|
2626
|
-
);
|
|
2736
|
+
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
2737
|
+
async reportRunning() {
|
|
2738
|
+
this.enqueue({ status: "running" });
|
|
2739
|
+
await this.chain;
|
|
2740
|
+
if (!this.dead) this.startCancelWatch();
|
|
2741
|
+
return !this.dead;
|
|
2627
2742
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
description: this.description,
|
|
2634
|
-
status: this.status,
|
|
2635
|
-
start_timestamp: this.startTimestamp,
|
|
2636
|
-
timestamp: this.endTimestamp ?? nowISO(),
|
|
2637
|
-
data: this.data
|
|
2638
|
-
};
|
|
2743
|
+
reportSuccess() {
|
|
2744
|
+
this.stopCancelWatch();
|
|
2745
|
+
this.clearFlushTimer();
|
|
2746
|
+
this.forceFinalProgress();
|
|
2747
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
2639
2748
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
this.
|
|
2644
|
-
this.
|
|
2645
|
-
this.op = op;
|
|
2646
|
-
this.runId = runId;
|
|
2647
|
-
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2749
|
+
reportFailure(error) {
|
|
2750
|
+
this.stopCancelWatch();
|
|
2751
|
+
this.clearFlushTimer();
|
|
2752
|
+
this.forceFinalProgress();
|
|
2753
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
2648
2754
|
}
|
|
2649
|
-
client;
|
|
2650
|
-
name;
|
|
2651
|
-
op;
|
|
2652
|
-
runId;
|
|
2653
|
-
eventId = uuid4();
|
|
2654
|
-
stepId = stepId();
|
|
2655
|
-
startTimestamp = nowISO();
|
|
2656
|
-
status = "ok";
|
|
2657
|
-
steps = [];
|
|
2658
|
-
tags = {};
|
|
2659
|
-
finished = false;
|
|
2660
|
-
lifecycleSink;
|
|
2661
|
-
attachmentSink;
|
|
2662
|
-
warnedNoAttachmentSink = false;
|
|
2663
|
-
// instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
|
|
2664
|
-
stepGuard;
|
|
2665
|
-
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
2666
|
-
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
2667
|
-
lifecycleBacklog = [];
|
|
2668
2755
|
/**
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
2672
|
-
* escreve no stdout do processo do usuário sem permissão.
|
|
2756
|
+
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
2757
|
+
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
2673
2758
|
*/
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
console.log(`${tag} ${label}${suffix}`);
|
|
2759
|
+
takeLogsForTerminal() {
|
|
2760
|
+
let logs = this.takeLogs();
|
|
2761
|
+
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
2762
|
+
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
2763
|
+
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
2680
2764
|
}
|
|
2681
|
-
|
|
2682
|
-
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
2683
|
-
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
2684
|
-
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
2765
|
+
return logs;
|
|
2685
2766
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2767
|
+
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
2768
|
+
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
2769
|
+
setProgress(current, total) {
|
|
2770
|
+
if (this.dead) return;
|
|
2771
|
+
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
2772
|
+
if (!this.progressWarned) {
|
|
2773
|
+
this.progressWarned = true;
|
|
2774
|
+
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
2775
|
+
}
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
2779
|
+
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
2780
|
+
const totalChanged = total !== this.progressTotal;
|
|
2781
|
+
if (next === this.progressCurrent && !totalChanged) return;
|
|
2782
|
+
this.progressCurrent = next;
|
|
2783
|
+
this.progressTotal = total;
|
|
2784
|
+
if (total == null) return;
|
|
2785
|
+
const now = Date.now();
|
|
2786
|
+
if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
|
|
2787
|
+
this.progressLastPrint = now;
|
|
2788
|
+
printRunProgress(next, total);
|
|
2789
|
+
}
|
|
2790
|
+
this.progressDirty = true;
|
|
2791
|
+
if (this.progressTimer) return;
|
|
2792
|
+
this.progressTimer = setTimeout(() => {
|
|
2793
|
+
this.progressTimer = void 0;
|
|
2794
|
+
this.flushProgress();
|
|
2795
|
+
}, PROGRESS_FLUSH_MS);
|
|
2796
|
+
unrefTimer(this.progressTimer);
|
|
2692
2797
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
this.
|
|
2798
|
+
flushProgress() {
|
|
2799
|
+
this.clearProgressTimer();
|
|
2800
|
+
if (this.dead || !this.progressDirty) return;
|
|
2801
|
+
if (this.buffer.length > 0) this.flushLogs();
|
|
2802
|
+
else this.enqueue({ status: "running" });
|
|
2696
2803
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
this.warnedNoAttachmentSink = true;
|
|
2702
|
-
console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
2804
|
+
clearProgressTimer() {
|
|
2805
|
+
if (this.progressTimer) {
|
|
2806
|
+
clearTimeout(this.progressTimer);
|
|
2807
|
+
this.progressTimer = void 0;
|
|
2703
2808
|
}
|
|
2704
|
-
|
|
2809
|
+
}
|
|
2810
|
+
/** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
|
|
2811
|
+
forceFinalProgress() {
|
|
2812
|
+
this.clearProgressTimer();
|
|
2813
|
+
this.progressDirty = this.progressTotal != null;
|
|
2814
|
+
}
|
|
2815
|
+
/** Consome o snapshot pendente para pegar carona no próximo PATCH. */
|
|
2816
|
+
takeProgress() {
|
|
2817
|
+
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
2818
|
+
this.progressDirty = false;
|
|
2819
|
+
return { current: this.progressCurrent, total: this.progressTotal };
|
|
2705
2820
|
}
|
|
2706
2821
|
/**
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2822
|
+
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
2823
|
+
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
2824
|
+
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
2709
2825
|
*/
|
|
2710
|
-
|
|
2711
|
-
this.
|
|
2826
|
+
startCancelWatch() {
|
|
2827
|
+
if (this.watchTimer) return;
|
|
2828
|
+
this.watchTimer = setInterval(() => {
|
|
2829
|
+
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2830
|
+
this.enqueue({ status: "running" });
|
|
2831
|
+
}, this.cancelCheckIntervalMs);
|
|
2832
|
+
unrefTimer(this.watchTimer);
|
|
2712
2833
|
}
|
|
2713
|
-
|
|
2714
|
-
this.
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
return step;
|
|
2834
|
+
stopCancelWatch() {
|
|
2835
|
+
if (this.watchTimer) {
|
|
2836
|
+
clearInterval(this.watchTimer);
|
|
2837
|
+
this.watchTimer = void 0;
|
|
2838
|
+
}
|
|
2719
2839
|
}
|
|
2720
|
-
/**
|
|
2721
|
-
|
|
2722
|
-
return this.
|
|
2840
|
+
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
2841
|
+
finalize() {
|
|
2842
|
+
return this.chain;
|
|
2723
2843
|
}
|
|
2844
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
2724
2845
|
/**
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2846
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2847
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2848
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2727
2849
|
*/
|
|
2728
|
-
|
|
2729
|
-
|
|
2850
|
+
addItem(status, message, payload, id, image3) {
|
|
2851
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2852
|
+
let scrubbedPayload;
|
|
2853
|
+
if (payload) {
|
|
2854
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2855
|
+
let serializedLength;
|
|
2856
|
+
try {
|
|
2857
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2858
|
+
} catch {
|
|
2859
|
+
}
|
|
2860
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2861
|
+
this.logger.warn(
|
|
2862
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2863
|
+
);
|
|
2864
|
+
scrubbedPayload = void 0;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
const ts = nowISO();
|
|
2868
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2869
|
+
this.itemsBuffer.push({
|
|
2870
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2871
|
+
seq: this.itemSeq++,
|
|
2872
|
+
status,
|
|
2873
|
+
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2874
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2875
|
+
ts,
|
|
2876
|
+
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2877
|
+
});
|
|
2878
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2879
|
+
this.flushItems();
|
|
2880
|
+
} else {
|
|
2881
|
+
this.scheduleItemsFlush();
|
|
2882
|
+
}
|
|
2730
2883
|
}
|
|
2731
2884
|
/**
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
2885
|
+
* Ids já concluídos (`succeeded`/`occurrence`) neste trabalho — a run e suas reexecuções.
|
|
2886
|
+
* Pagina até o fim. undefined quando a consulta falha (servidor antigo, rede): o `each`
|
|
2887
|
+
* avisa e processa tudo, nunca trava a run por isso.
|
|
2735
2888
|
*/
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2889
|
+
async listDoneItemIds() {
|
|
2890
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2891
|
+
let cursor = null;
|
|
2892
|
+
try {
|
|
2893
|
+
do {
|
|
2894
|
+
const page = await this.api.listDoneItems(this.runId, {
|
|
2895
|
+
limit: MAX_ITEMS_DONE_PER_PAGE,
|
|
2896
|
+
...cursor ? { cursor } : {}
|
|
2897
|
+
});
|
|
2898
|
+
for (const id of page.ids) ids.add(id);
|
|
2899
|
+
cursor = page.next;
|
|
2900
|
+
} while (cursor);
|
|
2901
|
+
return ids;
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
this.log("warning", `n\xE3o foi poss\xEDvel consultar os itens j\xE1 conclu\xEDdos deste trabalho \u2014 todos ser\xE3o processados (${thrownMessage(error)})`);
|
|
2904
|
+
return void 0;
|
|
2743
2905
|
}
|
|
2744
|
-
return void 0;
|
|
2745
2906
|
}
|
|
2746
|
-
|
|
2747
|
-
this.
|
|
2748
|
-
|
|
2907
|
+
scheduleItemsFlush() {
|
|
2908
|
+
if (this.itemsTimer) return;
|
|
2909
|
+
this.itemsTimer = setTimeout(() => {
|
|
2910
|
+
this.itemsTimer = void 0;
|
|
2911
|
+
this.flushItems();
|
|
2912
|
+
}, this.flushIntervalMs);
|
|
2913
|
+
unrefTimer(this.itemsTimer);
|
|
2749
2914
|
}
|
|
2750
|
-
|
|
2751
|
-
this.
|
|
2752
|
-
|
|
2915
|
+
clearItemsTimer() {
|
|
2916
|
+
if (this.itemsTimer) {
|
|
2917
|
+
clearTimeout(this.itemsTimer);
|
|
2918
|
+
this.itemsTimer = void 0;
|
|
2919
|
+
}
|
|
2753
2920
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2921
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
2922
|
+
flushItems() {
|
|
2923
|
+
this.clearItemsTimer();
|
|
2924
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2925
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2926
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2927
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2928
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
async sendItems(batch) {
|
|
2932
|
+
if (this.itemsUnsupported) return;
|
|
2933
|
+
try {
|
|
2934
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
2935
|
+
this.itemsStorageFull = false;
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2938
|
+
this.itemsUnsupported = true;
|
|
2939
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
2943
|
+
if (!this.itemsQuotaExceeded) {
|
|
2944
|
+
this.itemsQuotaExceeded = true;
|
|
2945
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
2946
|
+
}
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (error instanceof TasksApiError && error.status === 402) {
|
|
2950
|
+
if (!this.itemsStorageFull) {
|
|
2951
|
+
this.itemsStorageFull = true;
|
|
2952
|
+
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2953
|
+
}
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (error instanceof TasksApiError) {
|
|
2957
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
2958
|
+
this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
this.itemsBuffer.unshift(...batch);
|
|
2962
|
+
this.scheduleItemsFlush();
|
|
2963
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
2964
|
+
}
|
|
2757
2965
|
}
|
|
2758
2966
|
/**
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2763
|
-
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2764
|
-
* Devolve quantas steps foram enviadas.
|
|
2967
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
2968
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
2969
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2765
2970
|
*/
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2971
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2972
|
+
this.flushItems();
|
|
2973
|
+
await Promise.race([
|
|
2974
|
+
this.itemsChain,
|
|
2975
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2976
|
+
]);
|
|
2977
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2978
|
+
this.flushItems();
|
|
2979
|
+
await Promise.race([
|
|
2980
|
+
this.itemsChain,
|
|
2981
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2982
|
+
]);
|
|
2771
2983
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2984
|
+
}
|
|
2985
|
+
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2986
|
+
/**
|
|
2987
|
+
* Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
|
|
2988
|
+
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2989
|
+
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2990
|
+
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2991
|
+
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2992
|
+
* também cai aqui via `log()`.
|
|
2993
|
+
*/
|
|
2994
|
+
queueAttachment(imageIn, ts) {
|
|
2995
|
+
if (this.dead) return void 0;
|
|
2996
|
+
const image3 = toImageAttachment(imageIn);
|
|
2997
|
+
if (this.attachmentsUnsupported) return void 0;
|
|
2998
|
+
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
2999
|
+
if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
|
|
3000
|
+
this.attachmentCount++;
|
|
3001
|
+
this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
2781
3002
|
}
|
|
3003
|
+
return void 0;
|
|
2782
3004
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3005
|
+
this.attachmentCount++;
|
|
3006
|
+
const id = uuid4();
|
|
3007
|
+
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
3008
|
+
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
3009
|
+
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
3010
|
+
this.pendingAttachments.add(work);
|
|
3011
|
+
void work.finally(() => this.pendingAttachments.delete(work));
|
|
3012
|
+
return id;
|
|
3013
|
+
}
|
|
3014
|
+
/** Retorna o motivo da falha, ou undefined em sucesso. */
|
|
3015
|
+
async uploadAttachment(id, image3, meta) {
|
|
3016
|
+
let bytes;
|
|
3017
|
+
try {
|
|
3018
|
+
bytes = await toBytes(image3);
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
|
|
3021
|
+
return "read-failed";
|
|
3022
|
+
}
|
|
3023
|
+
const contentType = sniffContentType(bytes) ?? meta.contentType;
|
|
3024
|
+
if (!contentType) {
|
|
3025
|
+
this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
|
|
3026
|
+
return "unsupported-format";
|
|
3027
|
+
}
|
|
3028
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
3029
|
+
this.log(
|
|
3030
|
+
"warning",
|
|
3031
|
+
`imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
|
|
3032
|
+
);
|
|
3033
|
+
return "too-large";
|
|
3034
|
+
}
|
|
3035
|
+
const dimensions = readPngDimensions(bytes);
|
|
3036
|
+
const release = await this.acquireUploadSlot();
|
|
3037
|
+
try {
|
|
3038
|
+
if (this.abortController.signal.aborted || this.dead) return "canceled";
|
|
3039
|
+
const created = await this.api.createAttachment(
|
|
3040
|
+
this.runId,
|
|
3041
|
+
{
|
|
3042
|
+
id,
|
|
3043
|
+
content_type: contentType,
|
|
3044
|
+
size_bytes: bytes.byteLength,
|
|
3045
|
+
log_ts: meta.logTs,
|
|
3046
|
+
...meta.caption ? { caption: meta.caption } : {},
|
|
3047
|
+
...dimensions ?? {}
|
|
3048
|
+
},
|
|
3049
|
+
this.abortController.signal
|
|
3050
|
+
);
|
|
3051
|
+
await this.api.uploadAttachment(created.upload_url, created.headers, bytes, this.abortController.signal);
|
|
3052
|
+
await this.api.confirmAttachment(this.runId, id, { status: "uploaded", size_bytes: bytes.byteLength, ...dimensions ?? {} }).catch(() => {
|
|
3053
|
+
});
|
|
3054
|
+
return void 0;
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
if (this.isUnsupported(error)) {
|
|
3057
|
+
this.attachmentsUnsupported = true;
|
|
3058
|
+
this.log("warning", "anexos n\xE3o est\xE3o habilitados nesta instala\xE7\xE3o do Hobots \u2014 imagem n\xE3o enviada");
|
|
3059
|
+
return "unsupported";
|
|
3060
|
+
}
|
|
3061
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
3062
|
+
this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
|
|
3063
|
+
return "quota";
|
|
2791
3064
|
}
|
|
3065
|
+
if (this.abortController.signal.aborted) return "canceled";
|
|
3066
|
+
this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
|
|
3067
|
+
void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
|
|
3068
|
+
});
|
|
3069
|
+
return "upload-failed";
|
|
3070
|
+
} finally {
|
|
3071
|
+
release();
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
/** 404 = API anterior ao recurso; 501/503 = instalação sem S3 configurado. */
|
|
3075
|
+
isUnsupported(error) {
|
|
3076
|
+
return error instanceof TasksApiError && (error.status === 404 || error.status === 501 || error.status === 503);
|
|
3077
|
+
}
|
|
3078
|
+
/** Semáforo simples: no máximo MAX_CONCURRENT_UPLOADS binários em voo por run. */
|
|
3079
|
+
acquireUploadSlot() {
|
|
3080
|
+
const release = () => {
|
|
3081
|
+
const next = this.uploadQueue.shift();
|
|
3082
|
+
if (next) next();
|
|
3083
|
+
else this.uploadSlots++;
|
|
3084
|
+
};
|
|
3085
|
+
if (this.uploadSlots > 0) {
|
|
3086
|
+
this.uploadSlots--;
|
|
3087
|
+
return Promise.resolve(release);
|
|
2792
3088
|
}
|
|
2793
|
-
return
|
|
3089
|
+
return new Promise((resolve2) => {
|
|
3090
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
3091
|
+
});
|
|
2794
3092
|
}
|
|
2795
|
-
/**
|
|
2796
|
-
|
|
2797
|
-
|
|
3093
|
+
/**
|
|
3094
|
+
* Espera os uploads pendentes antes do PATCH terminal, para a run não terminar
|
|
3095
|
+
* exibindo miniaturas em "enviando…". Capado: um upload lento não segura a run.
|
|
3096
|
+
*/
|
|
3097
|
+
async finalizeAttachments(timeoutMs = ATTACHMENT_DRAIN_MS) {
|
|
3098
|
+
if (this.pendingAttachments.size === 0) return;
|
|
3099
|
+
await Promise.race([
|
|
3100
|
+
Promise.allSettled([...this.pendingAttachments]),
|
|
3101
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
3102
|
+
]);
|
|
2798
3103
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3104
|
+
enqueue(updateIn) {
|
|
3105
|
+
if (this.dead) {
|
|
3106
|
+
this.salvageLogs(updateIn.logs);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
this.clearProgressTimer();
|
|
3110
|
+
const progress = this.takeProgress();
|
|
3111
|
+
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
3112
|
+
this.pendingUpdates++;
|
|
3113
|
+
this.chain = this.chain.then(async () => {
|
|
3114
|
+
if (this.dead) {
|
|
3115
|
+
this.salvageLogs(update.logs);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
3119
|
+
try {
|
|
3120
|
+
await this.api.updateRun(this.runId, payload);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
3123
|
+
this.dead = true;
|
|
3124
|
+
this.stopCancelWatch();
|
|
3125
|
+
this.abort();
|
|
3126
|
+
const pending = this.buffer.splice(0, this.buffer.length);
|
|
3127
|
+
this.salvageLogs(pending);
|
|
3128
|
+
this.logger.warn(
|
|
3129
|
+
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
3130
|
+
);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
if (update.logs && update.logs.length > 0) {
|
|
3134
|
+
this.buffer.unshift(...update.logs);
|
|
3135
|
+
this.scheduleFlush();
|
|
3136
|
+
}
|
|
3137
|
+
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
3138
|
+
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
3139
|
+
}
|
|
3140
|
+
}).finally(() => {
|
|
3141
|
+
this.pendingUpdates--;
|
|
3142
|
+
});
|
|
2811
3143
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
this.
|
|
2821
|
-
|
|
2822
|
-
endTimestamp,
|
|
2823
|
-
this.status
|
|
2824
|
-
);
|
|
2825
|
-
if (!this.client) return;
|
|
2826
|
-
const steps = this.steps;
|
|
2827
|
-
let first = 0;
|
|
2828
|
-
while (steps.length - first > MAX_STEPS) {
|
|
2829
|
-
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
2830
|
-
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2831
|
-
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
2832
|
-
first += MAX_STEPS;
|
|
3144
|
+
/**
|
|
3145
|
+
* Best-effort de log tardio: envia logs para uma run já terminal, fora da fila.
|
|
3146
|
+
* O servidor anexa os logs mesmo respondendo 409; erros são engolidos — este é
|
|
3147
|
+
* o último recurso, não um canal confiável.
|
|
3148
|
+
*/
|
|
3149
|
+
salvageLogs(logs) {
|
|
3150
|
+
if (!logs || logs.length === 0) return;
|
|
3151
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
3152
|
+
void this.api.updateRunOnce(this.runId, { status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) }).catch(() => {
|
|
3153
|
+
});
|
|
2833
3154
|
}
|
|
2834
|
-
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
2835
3155
|
}
|
|
2836
3156
|
};
|
|
2837
3157
|
|
|
@@ -2840,7 +3160,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
|
2840
3160
|
async function executeRun(input) {
|
|
2841
3161
|
const { run, handler, api, client, logger, tracker } = input;
|
|
2842
3162
|
const options = handler.options;
|
|
2843
|
-
const tx = new Transaction(client, "run", "hobots.run", run.id);
|
|
3163
|
+
const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
|
|
2844
3164
|
const reporter = new RunReporter(
|
|
2845
3165
|
api,
|
|
2846
3166
|
run.id,
|
|
@@ -3080,42 +3400,26 @@ var TaskAgent = class {
|
|
|
3080
3400
|
}
|
|
3081
3401
|
};
|
|
3082
3402
|
|
|
3083
|
-
// src/tasks/
|
|
3084
|
-
var
|
|
3085
|
-
|
|
3086
|
-
if (!(error instanceof TasksApiError)) return true;
|
|
3087
|
-
return error.status >= 500 || error.status === 429;
|
|
3088
|
-
}
|
|
3089
|
-
function describeRejection(runId, error) {
|
|
3090
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
3091
|
-
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3092
|
-
}
|
|
3093
|
-
if (error instanceof TasksApiError && error.status === 409) {
|
|
3094
|
-
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3095
|
-
}
|
|
3096
|
-
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3097
|
-
}
|
|
3098
|
-
var AttachedRunner = class {
|
|
3099
|
-
constructor(runId, handler, deps) {
|
|
3100
|
-
this.runId = runId;
|
|
3403
|
+
// src/tasks/single-run.ts
|
|
3404
|
+
var SingleRunRunner = class {
|
|
3405
|
+
constructor(handler, deps) {
|
|
3101
3406
|
this.handler = handler;
|
|
3102
3407
|
this.deps = deps;
|
|
3103
3408
|
this.done = new Promise((resolve2) => {
|
|
3104
3409
|
this.resolveDone = resolve2;
|
|
3105
3410
|
});
|
|
3106
3411
|
}
|
|
3107
|
-
runId;
|
|
3108
3412
|
handler;
|
|
3109
3413
|
deps;
|
|
3110
3414
|
abort = new AbortController();
|
|
3111
|
-
/** Resolve quando a run terminou (ou não pôde
|
|
3415
|
+
/** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
|
|
3112
3416
|
done;
|
|
3113
3417
|
resolveDone;
|
|
3114
3418
|
async run() {
|
|
3115
3419
|
try {
|
|
3116
|
-
const run = await this.
|
|
3420
|
+
const run = await this.openRun();
|
|
3117
3421
|
if (!run) return;
|
|
3118
|
-
this.deps.logger.debug(
|
|
3422
|
+
this.deps.logger.debug(this.opened(run));
|
|
3119
3423
|
await executeRun({
|
|
3120
3424
|
run,
|
|
3121
3425
|
handler: this.handler,
|
|
@@ -3130,11 +3434,37 @@ var AttachedRunner = class {
|
|
|
3130
3434
|
this.resolveDone();
|
|
3131
3435
|
}
|
|
3132
3436
|
}
|
|
3133
|
-
/** Desiste de
|
|
3437
|
+
/** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
|
|
3134
3438
|
stop() {
|
|
3135
3439
|
this.abort.abort();
|
|
3136
3440
|
}
|
|
3137
|
-
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3443
|
+
// src/tasks/attached.ts
|
|
3444
|
+
var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
3445
|
+
function isTransient(error) {
|
|
3446
|
+
if (!(error instanceof TasksApiError)) return true;
|
|
3447
|
+
return error.status >= 500 || error.status === 429;
|
|
3448
|
+
}
|
|
3449
|
+
function describeRejection(runId, error) {
|
|
3450
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
3451
|
+
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3452
|
+
}
|
|
3453
|
+
if (error instanceof TasksApiError && error.status === 409) {
|
|
3454
|
+
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3455
|
+
}
|
|
3456
|
+
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3457
|
+
}
|
|
3458
|
+
var AttachedRunner = class extends SingleRunRunner {
|
|
3459
|
+
constructor(runId, handler, deps) {
|
|
3460
|
+
super(handler, deps);
|
|
3461
|
+
this.runId = runId;
|
|
3462
|
+
}
|
|
3463
|
+
runId;
|
|
3464
|
+
opened(run) {
|
|
3465
|
+
return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
|
|
3466
|
+
}
|
|
3467
|
+
async openRun() {
|
|
3138
3468
|
for (let attempt = 0; ; attempt++) {
|
|
3139
3469
|
if (this.abort.signal.aborted) return void 0;
|
|
3140
3470
|
try {
|
|
@@ -3197,46 +3527,16 @@ function normalizeItemColumns(input, logger) {
|
|
|
3197
3527
|
}
|
|
3198
3528
|
|
|
3199
3529
|
// src/tasks/local.ts
|
|
3200
|
-
var LocalRunner = class {
|
|
3530
|
+
var LocalRunner = class extends SingleRunRunner {
|
|
3531
|
+
backoff;
|
|
3201
3532
|
constructor(handler, deps) {
|
|
3202
|
-
|
|
3203
|
-
this.deps = deps;
|
|
3533
|
+
super(handler, deps);
|
|
3204
3534
|
this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
|
|
3205
|
-
this.done = new Promise((resolve2) => {
|
|
3206
|
-
this.resolveDone = resolve2;
|
|
3207
|
-
});
|
|
3208
|
-
}
|
|
3209
|
-
handler;
|
|
3210
|
-
deps;
|
|
3211
|
-
abort = new AbortController();
|
|
3212
|
-
backoff;
|
|
3213
|
-
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
3214
|
-
done;
|
|
3215
|
-
resolveDone;
|
|
3216
|
-
async run() {
|
|
3217
|
-
try {
|
|
3218
|
-
const run = await this.createRun();
|
|
3219
|
-
if (!run) return;
|
|
3220
|
-
this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
|
|
3221
|
-
await executeRun({
|
|
3222
|
-
run,
|
|
3223
|
-
handler: this.handler,
|
|
3224
|
-
api: this.deps.api,
|
|
3225
|
-
client: this.deps.client,
|
|
3226
|
-
logger: this.deps.logger,
|
|
3227
|
-
tracker: this.deps.tracker
|
|
3228
|
-
});
|
|
3229
|
-
} catch (error) {
|
|
3230
|
-
this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
|
|
3231
|
-
} finally {
|
|
3232
|
-
this.resolveDone();
|
|
3233
|
-
}
|
|
3234
3535
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
this.abort.abort();
|
|
3536
|
+
opened(run) {
|
|
3537
|
+
return `run ${run.id} aberta (modo start)`;
|
|
3238
3538
|
}
|
|
3239
|
-
async
|
|
3539
|
+
async openRun() {
|
|
3240
3540
|
for (; ; ) {
|
|
3241
3541
|
if (this.abort.signal.aborted) return void 0;
|
|
3242
3542
|
try {
|
|
@@ -3249,6 +3549,133 @@ var LocalRunner = class {
|
|
|
3249
3549
|
}
|
|
3250
3550
|
};
|
|
3251
3551
|
|
|
3552
|
+
// src/tasks/local-transport.ts
|
|
3553
|
+
var LOCAL_RUN_ID = "local";
|
|
3554
|
+
var ITEM_LINE_PREFIX = "[hobots:item]";
|
|
3555
|
+
var SUMMARY_LINE_PREFIX = "[hobots:summary]";
|
|
3556
|
+
var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
|
|
3557
|
+
function unavailable(name) {
|
|
3558
|
+
return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
|
|
3559
|
+
}
|
|
3560
|
+
function cell(value) {
|
|
3561
|
+
return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
|
|
3562
|
+
}
|
|
3563
|
+
var LocalRunTransport = class {
|
|
3564
|
+
constructor(params = {}, out = (line) => void process.stdout.write(`${line}
|
|
3565
|
+
`)) {
|
|
3566
|
+
this.params = params;
|
|
3567
|
+
this.out = out;
|
|
3568
|
+
}
|
|
3569
|
+
params;
|
|
3570
|
+
out;
|
|
3571
|
+
counts = { succeeded: 0, occurrence: 0, failed: 0 };
|
|
3572
|
+
warnedAttachments = false;
|
|
3573
|
+
/** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
|
|
3574
|
+
terminalStatus;
|
|
3575
|
+
async updateRun(_runId, update) {
|
|
3576
|
+
if (update.status === "running") return;
|
|
3577
|
+
this.terminalStatus = update.status;
|
|
3578
|
+
if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
|
|
3579
|
+
printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
|
|
3580
|
+
if (update.error.stack) console.error(update.error.stack);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
async updateRunOnce() {
|
|
3584
|
+
}
|
|
3585
|
+
async createItems(_runId, request) {
|
|
3586
|
+
for (const item of request.items) {
|
|
3587
|
+
this.counts[item.status]++;
|
|
3588
|
+
this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
|
|
3589
|
+
}
|
|
3590
|
+
return { accepted: request.items.length, duplicates: 0 };
|
|
3591
|
+
}
|
|
3592
|
+
// modo local não tem trabalho anterior: nada a pular
|
|
3593
|
+
async listDoneItems() {
|
|
3594
|
+
return { ids: [], next: null };
|
|
3595
|
+
}
|
|
3596
|
+
/** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
|
|
3597
|
+
printSummary() {
|
|
3598
|
+
const { succeeded, occurrence, failed } = this.counts;
|
|
3599
|
+
this.out(
|
|
3600
|
+
`${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
3603
|
+
async createAttachment(_runId, request) {
|
|
3604
|
+
if (!this.warnedAttachments) {
|
|
3605
|
+
this.warnedAttachments = true;
|
|
3606
|
+
printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
|
|
3607
|
+
}
|
|
3608
|
+
return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
|
|
3609
|
+
}
|
|
3610
|
+
async uploadAttachment() {
|
|
3611
|
+
}
|
|
3612
|
+
async confirmAttachment() {
|
|
3613
|
+
}
|
|
3614
|
+
/** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
|
|
3615
|
+
fileName(fileId) {
|
|
3616
|
+
return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
|
|
3617
|
+
}
|
|
3618
|
+
async requestFileDownload(_runId, fileId) {
|
|
3619
|
+
throw unavailable(this.fileName(fileId));
|
|
3620
|
+
}
|
|
3621
|
+
async fetchFile(url) {
|
|
3622
|
+
throw unavailable(url);
|
|
3623
|
+
}
|
|
3624
|
+
async confirmFileDownload(_runId, fileId) {
|
|
3625
|
+
throw unavailable(this.fileName(fileId));
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
|
|
3629
|
+
// src/tasks/local-run.ts
|
|
3630
|
+
var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
|
|
3631
|
+
var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
|
|
3632
|
+
var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
|
|
3633
|
+
var LocalRunRunner = class extends SingleRunRunner {
|
|
3634
|
+
constructor(params, handler, deps) {
|
|
3635
|
+
super(handler, deps);
|
|
3636
|
+
this.params = params;
|
|
3637
|
+
}
|
|
3638
|
+
params;
|
|
3639
|
+
canceled = false;
|
|
3640
|
+
startedAt = Date.now();
|
|
3641
|
+
finished = false;
|
|
3642
|
+
run() {
|
|
3643
|
+
this.startedAt = Date.now();
|
|
3644
|
+
return super.run();
|
|
3645
|
+
}
|
|
3646
|
+
opened() {
|
|
3647
|
+
return "run local iniciada (HOBOTS_LOCAL_RUN)";
|
|
3648
|
+
}
|
|
3649
|
+
async openRun() {
|
|
3650
|
+
if (this.abort.signal.aborted) return void 0;
|
|
3651
|
+
return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
|
|
3652
|
+
}
|
|
3653
|
+
/** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
|
|
3654
|
+
cancel() {
|
|
3655
|
+
this.canceled = true;
|
|
3656
|
+
this.deps.tracker.singleActive()?.abort();
|
|
3657
|
+
}
|
|
3658
|
+
/** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
|
|
3659
|
+
get outcome() {
|
|
3660
|
+
if (this.canceled) return "canceled";
|
|
3661
|
+
return this.deps.api.terminalStatus ?? "failed";
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
|
|
3665
|
+
* desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
|
|
3666
|
+
* esgota com o handler ainda rodando.
|
|
3667
|
+
*/
|
|
3668
|
+
finish() {
|
|
3669
|
+
const outcome = this.outcome;
|
|
3670
|
+
if (this.finished) return outcome;
|
|
3671
|
+
this.finished = true;
|
|
3672
|
+
this.deps.api.printSummary();
|
|
3673
|
+
const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
|
|
3674
|
+
printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
|
|
3675
|
+
return outcome;
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
|
|
3252
3679
|
// src/tasks/registry.ts
|
|
3253
3680
|
var RunTracker = class {
|
|
3254
3681
|
constructor(api) {
|
|
@@ -3364,26 +3791,10 @@ function hostname() {
|
|
|
3364
3791
|
if (!isNode()) return void 0;
|
|
3365
3792
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
3366
3793
|
}
|
|
3367
|
-
function
|
|
3368
|
-
|
|
3369
|
-
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3370
|
-
const logger = makeLogger(options.debug ?? false);
|
|
3371
|
-
const environment = resolveEnvironmentOption(options.environment);
|
|
3372
|
-
const client = new Client(
|
|
3373
|
-
{
|
|
3374
|
-
environment,
|
|
3375
|
-
release: options.release,
|
|
3376
|
-
tags: options.tags,
|
|
3377
|
-
debug: options.debug,
|
|
3378
|
-
captureRunErrors: options.captureUnhandled !== false
|
|
3379
|
-
},
|
|
3380
|
-
config
|
|
3381
|
-
);
|
|
3382
|
-
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3383
|
-
const rt = {
|
|
3794
|
+
function baseRuntime(options, handler, logger, api) {
|
|
3795
|
+
return {
|
|
3384
3796
|
options: { ...options, clientSecret: void 0 },
|
|
3385
|
-
|
|
3386
|
-
client,
|
|
3797
|
+
client: void 0,
|
|
3387
3798
|
logger,
|
|
3388
3799
|
api,
|
|
3389
3800
|
handler,
|
|
@@ -3393,19 +3804,54 @@ function createRuntime(options, handler) {
|
|
|
3393
3804
|
agent: void 0,
|
|
3394
3805
|
local: void 0,
|
|
3395
3806
|
attached: void 0,
|
|
3807
|
+
localRun: void 0,
|
|
3396
3808
|
runId: void 0,
|
|
3397
3809
|
instanceId: resolveInstanceId(options.instanceId),
|
|
3398
|
-
environment,
|
|
3810
|
+
environment: void 0,
|
|
3399
3811
|
pollIntervalMs: void 0,
|
|
3400
3812
|
booted: false,
|
|
3401
3813
|
bootScheduled: false,
|
|
3402
3814
|
bootPromise: void 0,
|
|
3403
|
-
uninstallGlobalHandlers:
|
|
3815
|
+
uninstallGlobalHandlers: void 0,
|
|
3404
3816
|
uninstallShutdown: void 0
|
|
3405
3817
|
};
|
|
3818
|
+
}
|
|
3819
|
+
function createRuntime(options, handler, logger) {
|
|
3820
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3821
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
3822
|
+
const client = new Client(
|
|
3823
|
+
{
|
|
3824
|
+
environment,
|
|
3825
|
+
release: options.release,
|
|
3826
|
+
tags: options.tags,
|
|
3827
|
+
debug: options.debug,
|
|
3828
|
+
captureRunErrors: options.captureUnhandled !== false
|
|
3829
|
+
},
|
|
3830
|
+
config
|
|
3831
|
+
);
|
|
3832
|
+
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3833
|
+
const rt = baseRuntime(options, handler, logger, api);
|
|
3834
|
+
rt.client = client;
|
|
3835
|
+
rt.environment = environment;
|
|
3836
|
+
rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
|
|
3406
3837
|
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
3407
3838
|
return rt;
|
|
3408
3839
|
}
|
|
3840
|
+
function createLocalRunRuntime(options, handler, logger) {
|
|
3841
|
+
let params;
|
|
3842
|
+
try {
|
|
3843
|
+
params = resolveLocalRunParams();
|
|
3844
|
+
} catch (error) {
|
|
3845
|
+
printRunLog("error", error instanceof Error ? error.message : String(error));
|
|
3846
|
+
process.exit(2);
|
|
3847
|
+
throw error;
|
|
3848
|
+
}
|
|
3849
|
+
const transport = new LocalRunTransport(params);
|
|
3850
|
+
const rt = baseRuntime(options, handler, logger, transport);
|
|
3851
|
+
rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
|
|
3852
|
+
printRunLog("info", LOCAL_RUN_BANNER);
|
|
3853
|
+
return rt;
|
|
3854
|
+
}
|
|
3409
3855
|
function validateHandler(handler, fn) {
|
|
3410
3856
|
if (typeof handler !== "function") {
|
|
3411
3857
|
throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
|
|
@@ -3448,9 +3894,12 @@ function instanceInfo(rt) {
|
|
|
3448
3894
|
machine: rt.sampler?.sample()
|
|
3449
3895
|
};
|
|
3450
3896
|
}
|
|
3897
|
+
function platformApi(rt) {
|
|
3898
|
+
return rt.api;
|
|
3899
|
+
}
|
|
3451
3900
|
function ensureAgent(rt) {
|
|
3452
3901
|
rt.agent ??= new TaskAgent({
|
|
3453
|
-
api: rt
|
|
3902
|
+
api: platformApi(rt),
|
|
3454
3903
|
client: rt.client,
|
|
3455
3904
|
logger: rt.logger,
|
|
3456
3905
|
handler: rt.handler,
|
|
@@ -3462,7 +3911,7 @@ function ensureAgent(rt) {
|
|
|
3462
3911
|
}
|
|
3463
3912
|
function launchLocal(rt) {
|
|
3464
3913
|
const runner = new LocalRunner(rt.handler, {
|
|
3465
|
-
api: rt
|
|
3914
|
+
api: platformApi(rt),
|
|
3466
3915
|
client: rt.client,
|
|
3467
3916
|
logger: rt.logger,
|
|
3468
3917
|
tracker: rt.tracker,
|
|
@@ -3473,7 +3922,7 @@ function launchLocal(rt) {
|
|
|
3473
3922
|
}
|
|
3474
3923
|
function launchAttached(rt, runId) {
|
|
3475
3924
|
const runner = new AttachedRunner(runId, rt.handler, {
|
|
3476
|
-
api: rt
|
|
3925
|
+
api: platformApi(rt),
|
|
3477
3926
|
client: rt.client,
|
|
3478
3927
|
logger: rt.logger,
|
|
3479
3928
|
tracker: rt.tracker,
|
|
@@ -3485,7 +3934,43 @@ function launchAttached(rt, runId) {
|
|
|
3485
3934
|
if (runtime === rt) void close();
|
|
3486
3935
|
});
|
|
3487
3936
|
}
|
|
3937
|
+
function launchLocalRun(rt, runner) {
|
|
3938
|
+
let finished = false;
|
|
3939
|
+
const finish = () => {
|
|
3940
|
+
if (finished) return;
|
|
3941
|
+
finished = true;
|
|
3942
|
+
rt.uninstallShutdown?.();
|
|
3943
|
+
rt.uninstallShutdown = void 0;
|
|
3944
|
+
const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
|
|
3945
|
+
if (runtime === rt) void close(0);
|
|
3946
|
+
process.stdout.write("", () => process.exit(code));
|
|
3947
|
+
};
|
|
3948
|
+
if (rt.options.installExitHandlers !== false) {
|
|
3949
|
+
let signaled = false;
|
|
3950
|
+
const onSignal = (signal) => {
|
|
3951
|
+
if (externalListenerCount(signal) > 0 || signaled) return;
|
|
3952
|
+
signaled = true;
|
|
3953
|
+
runner.cancel();
|
|
3954
|
+
void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
|
|
3955
|
+
};
|
|
3956
|
+
const onInt = markInternal(() => onSignal("SIGINT"));
|
|
3957
|
+
const onTerm = markInternal(() => onSignal("SIGTERM"));
|
|
3958
|
+
process.on("SIGINT", onInt);
|
|
3959
|
+
process.on("SIGTERM", onTerm);
|
|
3960
|
+
rt.uninstallShutdown = () => {
|
|
3961
|
+
process.off("SIGINT", onInt);
|
|
3962
|
+
process.off("SIGTERM", onTerm);
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
void runner.run();
|
|
3966
|
+
void runner.done.then(finish);
|
|
3967
|
+
}
|
|
3488
3968
|
async function boot(rt) {
|
|
3969
|
+
if (rt.localRun) {
|
|
3970
|
+
rt.booted = true;
|
|
3971
|
+
launchLocalRun(rt, rt.localRun);
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3489
3974
|
if (!rt.instanceId) {
|
|
3490
3975
|
const id = await fallbackInstanceId();
|
|
3491
3976
|
if (runtime !== rt) return;
|
|
@@ -3522,9 +4007,12 @@ function startAgent(fn, mode, handler, options) {
|
|
|
3522
4007
|
}
|
|
3523
4008
|
const logger = makeLogger(options.debug ?? false);
|
|
3524
4009
|
const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
if (mode === "remote")
|
|
4010
|
+
loadDotenv();
|
|
4011
|
+
const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
|
|
4012
|
+
if (!rt.localRun && mode === "remote") {
|
|
4013
|
+
if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
|
|
4014
|
+
rt.runId = resolveRunId(options.runId);
|
|
4015
|
+
}
|
|
3528
4016
|
runtime = rt;
|
|
3529
4017
|
scheduleBoot(rt);
|
|
3530
4018
|
}
|
|
@@ -3547,10 +4035,10 @@ function forwardAttachment(rt, attachment, message, type) {
|
|
|
3547
4035
|
const reporter = currentReporter();
|
|
3548
4036
|
if (!reporter) {
|
|
3549
4037
|
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
|
|
3550
|
-
return;
|
|
4038
|
+
return void 0;
|
|
3551
4039
|
}
|
|
3552
4040
|
const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
|
|
3553
|
-
reporter.log(toRunLogLevel(type), message, withMeta);
|
|
4041
|
+
return reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3554
4042
|
}
|
|
3555
4043
|
function captureException(error, hint) {
|
|
3556
4044
|
const rt = runtime;
|
|
@@ -3561,13 +4049,19 @@ function captureException(error, hint) {
|
|
|
3561
4049
|
}
|
|
3562
4050
|
return void 0;
|
|
3563
4051
|
}
|
|
4052
|
+
let attachmentId;
|
|
3564
4053
|
if (hint?.attachment) {
|
|
3565
4054
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
3566
|
-
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
4055
|
+
attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
4056
|
+
}
|
|
4057
|
+
if (!rt.client) {
|
|
4058
|
+
rt.logger.debug("captureException ignorado no modo local (sem Central)");
|
|
4059
|
+
return void 0;
|
|
3567
4060
|
}
|
|
3568
4061
|
const runId = currentReporter()?.runId;
|
|
3569
4062
|
const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
|
|
3570
|
-
|
|
4063
|
+
const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
|
|
4064
|
+
return rt.client.captureException(error, withRun, contexts);
|
|
3571
4065
|
}
|
|
3572
4066
|
async function close(timeoutMs = 3e4) {
|
|
3573
4067
|
const rt = runtime;
|
|
@@ -3577,6 +4071,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3577
4071
|
if (rt.agent) await rt.agent.stop(0);
|
|
3578
4072
|
rt.local?.stop();
|
|
3579
4073
|
rt.attached?.stop();
|
|
4074
|
+
rt.localRun?.stop();
|
|
3580
4075
|
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
3581
4076
|
if (!idle) {
|
|
3582
4077
|
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
@@ -3585,7 +4080,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3585
4080
|
rt.heartbeat?.stop();
|
|
3586
4081
|
rt.uninstallShutdown?.();
|
|
3587
4082
|
rt.uninstallGlobalHandlers?.();
|
|
3588
|
-
const flushed = await rt.client.close(timeoutMs);
|
|
4083
|
+
const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
|
|
3589
4084
|
return idle && flushed;
|
|
3590
4085
|
}
|
|
3591
4086
|
|
|
@@ -3614,11 +4109,19 @@ var noop = () => void 0;
|
|
|
3614
4109
|
var fallbackEachSink = {
|
|
3615
4110
|
report: noop,
|
|
3616
4111
|
startStep: () => void 0,
|
|
4112
|
+
stepScope: (_step, fn) => fn(),
|
|
3617
4113
|
declareTotal: noop,
|
|
3618
4114
|
advance: noop,
|
|
3619
4115
|
checkCanceled: noop,
|
|
3620
4116
|
isAborted: () => false,
|
|
3621
|
-
|
|
4117
|
+
/* v8 ignore start -- fora de uma run não há consulta de concluídos, logo nunca há item pulado para anunciar */
|
|
4118
|
+
logInfo: (message) => printRunLog("info", message),
|
|
4119
|
+
/* v8 ignore stop */
|
|
4120
|
+
logWarning: (message) => printRunLog("warning", message),
|
|
4121
|
+
logError: (message) => printRunLog("error", message),
|
|
4122
|
+
sleep,
|
|
4123
|
+
doneItemIds: () => Promise.resolve(void 0),
|
|
4124
|
+
flush: () => Promise.resolve()
|
|
3622
4125
|
};
|
|
3623
4126
|
var fallbackItems = {
|
|
3624
4127
|
add: noop,
|
|
@@ -3637,7 +4140,6 @@ var fallbackFiles = {
|
|
|
3637
4140
|
read: outsideRunFiles
|
|
3638
4141
|
};
|
|
3639
4142
|
var neverAborts = new AbortController().signal;
|
|
3640
|
-
var detachedTransaction;
|
|
3641
4143
|
function resolve(member) {
|
|
3642
4144
|
const reporter = currentReporter();
|
|
3643
4145
|
if (reporter) return reporter.context;
|
|
@@ -3658,8 +4160,12 @@ var hobots = Object.defineProperties({}, {
|
|
|
3658
4160
|
items: getter("items", () => fallbackItems),
|
|
3659
4161
|
progress: getter("progress", () => fallbackProgress),
|
|
3660
4162
|
files: getter("files", () => fallbackFiles),
|
|
3661
|
-
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
3662
4163
|
signal: getter("signal", () => neverAborts),
|
|
4164
|
+
step: method((name, fn, options) => {
|
|
4165
|
+
const ctx = resolve("step");
|
|
4166
|
+
if (ctx) return ctx.step(name, fn, options);
|
|
4167
|
+
return new Transaction(void 0, "hobots.detached").step(name, fn, options);
|
|
4168
|
+
}),
|
|
3663
4169
|
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
3664
4170
|
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
3665
4171
|
flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
|