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.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WIRE_LIMITS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6YTOYIZM.js";
|
|
4
4
|
|
|
5
5
|
// src/utils.ts
|
|
6
6
|
var SDK_NAME = "hobots";
|
|
7
|
-
var SDK_VERSION = true ? "
|
|
7
|
+
var SDK_VERSION = true ? "5.1.0" : "0.0.0-dev";
|
|
8
8
|
function isNode() {
|
|
9
9
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
10
10
|
}
|
|
@@ -57,8 +57,13 @@ function abortableSleep(ms, signal) {
|
|
|
57
57
|
function jitter(baseMs) {
|
|
58
58
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
59
59
|
}
|
|
60
|
+
var TRUNCATION_SUFFIX = "\u2026 [truncado]";
|
|
60
61
|
function truncate(value, max) {
|
|
61
|
-
return value.length > max ? `${value.slice(0, max)}
|
|
62
|
+
return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
|
|
63
|
+
}
|
|
64
|
+
function clip(value, max) {
|
|
65
|
+
if (value.length <= max) return value;
|
|
66
|
+
return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
|
|
62
67
|
}
|
|
63
68
|
function safeString(value) {
|
|
64
69
|
if (typeof value === "string") return value;
|
|
@@ -156,6 +161,8 @@ function colorize(text, color) {
|
|
|
156
161
|
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
157
162
|
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
158
163
|
var RUN_ID_ENV = "HOBOTS_RUN_ID";
|
|
164
|
+
var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
|
|
165
|
+
var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
|
|
159
166
|
var dotenvLoaded = false;
|
|
160
167
|
function loadDotenv() {
|
|
161
168
|
if (dotenvLoaded) return;
|
|
@@ -181,6 +188,24 @@ function resolveRunId(option) {
|
|
|
181
188
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
182
189
|
return process.env[RUN_ID_ENV]?.trim() || void 0;
|
|
183
190
|
}
|
|
191
|
+
function isLocalRun() {
|
|
192
|
+
if (typeof process === "undefined" || !process.env) return false;
|
|
193
|
+
return process.env[LOCAL_RUN_ENV]?.trim() === "1";
|
|
194
|
+
}
|
|
195
|
+
function resolveLocalRunParams() {
|
|
196
|
+
const raw = process.env[RUN_PARAMS_ENV]?.trim();
|
|
197
|
+
if (!raw) return {};
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(raw);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
|
|
203
|
+
}
|
|
204
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
205
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
184
209
|
function resolveEnvironmentOption(option) {
|
|
185
210
|
const fromOption = option?.trim();
|
|
186
211
|
if (fromOption) return fromOption;
|
|
@@ -394,9 +419,18 @@ var MAX_FINGERPRINT_ITEMS = 10;
|
|
|
394
419
|
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
395
420
|
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
396
421
|
var MAX_STEPS = 200;
|
|
422
|
+
var MAX_STEP_DESCRIPTION_CHARS = 2048;
|
|
423
|
+
var MAX_STEP_OP_CHARS = 255;
|
|
397
424
|
function clipTags(tags) {
|
|
398
425
|
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
399
426
|
}
|
|
427
|
+
function clipStep(step) {
|
|
428
|
+
return {
|
|
429
|
+
...step,
|
|
430
|
+
...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
|
|
431
|
+
...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
400
434
|
function clipFingerprint(fingerprint) {
|
|
401
435
|
if (fingerprint === void 0) return void 0;
|
|
402
436
|
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
@@ -455,12 +489,13 @@ var Client = class {
|
|
|
455
489
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
456
490
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
457
491
|
}
|
|
458
|
-
|
|
492
|
+
/** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
|
|
493
|
+
captureException(error, hint, contexts) {
|
|
459
494
|
if (typeof error === "object" && error !== null) captured.add(error);
|
|
460
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
495
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
|
|
461
496
|
}
|
|
462
|
-
captureEvent(event, hint) {
|
|
463
|
-
const final = this.prepareEvent(event, hint);
|
|
497
|
+
captureEvent(event, hint, contexts) {
|
|
498
|
+
const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
|
|
464
499
|
this.transport.enqueue({ type: "event", payload: final });
|
|
465
500
|
return final.event_id;
|
|
466
501
|
}
|
|
@@ -483,14 +518,15 @@ var Client = class {
|
|
|
483
518
|
sendTransaction(payload) {
|
|
484
519
|
let steps = payload.steps;
|
|
485
520
|
if (steps.length > MAX_STEPS) {
|
|
486
|
-
this.logger.warn(`transaction com ${steps.length}
|
|
521
|
+
this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
487
522
|
steps = steps.slice(0, MAX_STEPS);
|
|
488
523
|
}
|
|
524
|
+
steps = steps.map(clipStep);
|
|
489
525
|
this.transport.enqueue({
|
|
490
526
|
type: "transaction",
|
|
491
527
|
payload: {
|
|
492
528
|
...payload,
|
|
493
|
-
name:
|
|
529
|
+
name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
494
530
|
steps,
|
|
495
531
|
release: payload.release ?? this.options.release,
|
|
496
532
|
environment: this.resolveEnvironment(payload.environment),
|
|
@@ -1063,6 +1099,36 @@ var TasksApi = class {
|
|
|
1063
1099
|
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
1064
1100
|
};
|
|
1065
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Uma página dos ids já concluídos neste trabalho (`items.each` pula). Retry em falha
|
|
1104
|
+
* de rede/5xx; 4xx é definitivo (404 = servidor sem o recurso).
|
|
1105
|
+
*/
|
|
1106
|
+
async listDoneItems(runId, request) {
|
|
1107
|
+
let response;
|
|
1108
|
+
try {
|
|
1109
|
+
response = await fetchWithRetry(
|
|
1110
|
+
() => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/items/done`, {
|
|
1111
|
+
redirect: "error",
|
|
1112
|
+
method: "POST",
|
|
1113
|
+
headers: this.headers(),
|
|
1114
|
+
body: JSON.stringify(request),
|
|
1115
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1116
|
+
}),
|
|
1117
|
+
{ logger: this.logger, label: `itens conclu\xEDdos da run ${runId}` }
|
|
1118
|
+
);
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
throw new Error(`consulta de itens conclu\xEDdos da run ${runId} descartada ap\xF3s retries: ${String(error)}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (!response.ok) {
|
|
1123
|
+
throw new TasksApiError(`consulta de itens conclu\xEDdos rejeitada (HTTP ${response.status})`, response.status);
|
|
1124
|
+
}
|
|
1125
|
+
const body = await response.json().catch(() => null);
|
|
1126
|
+
if (!Array.isArray(body?.ids)) throw new TasksApiError("consulta de itens conclu\xEDdos: resposta malformada", 0);
|
|
1127
|
+
return {
|
|
1128
|
+
ids: body.ids.map((id) => String(id)),
|
|
1129
|
+
next: typeof body.next === "string" && body.next.length > 0 ? body.next : null
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1066
1132
|
/**
|
|
1067
1133
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
1068
1134
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1249,6 +1315,20 @@ var Backoff = class {
|
|
|
1249
1315
|
};
|
|
1250
1316
|
|
|
1251
1317
|
// src/tasks/errors.ts
|
|
1318
|
+
var Occurrence = class extends Error {
|
|
1319
|
+
payload;
|
|
1320
|
+
/** Print do item (bytes/caminho ou `hobots.image(...)`) — o `each` pendura no item. */
|
|
1321
|
+
image;
|
|
1322
|
+
constructor(message, payload, options) {
|
|
1323
|
+
super(message);
|
|
1324
|
+
this.name = "Occurrence";
|
|
1325
|
+
this.payload = payload;
|
|
1326
|
+
this.image = options?.image;
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
function isOccurrence(error) {
|
|
1330
|
+
return error instanceof Error && error.name === "Occurrence";
|
|
1331
|
+
}
|
|
1252
1332
|
function runCanceledError() {
|
|
1253
1333
|
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1254
1334
|
}
|
|
@@ -1265,1507 +1345,1745 @@ function isCancellation(error) {
|
|
|
1265
1345
|
return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
|
|
1266
1346
|
}
|
|
1267
1347
|
|
|
1268
|
-
// src/tasks/
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
var
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
this.
|
|
1279
|
-
this.
|
|
1280
|
-
|
|
1281
|
-
index;
|
|
1282
|
-
id;
|
|
1283
|
-
step;
|
|
1284
|
-
basePayload;
|
|
1285
|
-
sink;
|
|
1286
|
-
reported = false;
|
|
1287
|
-
status;
|
|
1288
|
-
succeeded(payload, options) {
|
|
1289
|
-
this.record("succeeded", void 0, payload, options?.image);
|
|
1290
|
-
}
|
|
1291
|
-
failed(message, payload, options) {
|
|
1292
|
-
this.record("failed", message, payload, options?.image);
|
|
1293
|
-
if (this.step && this.step.status !== "error") this.step.setStatus("error", message);
|
|
1294
|
-
}
|
|
1295
|
-
occurrence(message, payload, options) {
|
|
1296
|
-
this.record("occurrence", message, payload, options?.image);
|
|
1297
|
-
if (this.step && this.step.status === "ok") this.step.setStatus("warning", message);
|
|
1298
|
-
}
|
|
1299
|
-
record(status, message, payload, image3) {
|
|
1300
|
-
this.reported = true;
|
|
1301
|
-
this.status = status;
|
|
1302
|
-
const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
|
|
1303
|
-
this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
|
|
1348
|
+
// src/tasks/attachments.ts
|
|
1349
|
+
var MAX_ATTACHMENT_BYTES = WIRE_LIMITS.MAX_ATTACHMENT_BYTES;
|
|
1350
|
+
var MAX_ATTACHMENTS_PER_RUN = WIRE_LIMITS.MAX_ATTACHMENTS_PER_RUN;
|
|
1351
|
+
var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
|
|
1352
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1353
|
+
var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
|
|
1354
|
+
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1355
|
+
var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
|
|
1356
|
+
var ImageAttachment = class {
|
|
1357
|
+
constructor(input, caption, contentType) {
|
|
1358
|
+
this.input = input;
|
|
1359
|
+
this.caption = caption;
|
|
1360
|
+
this.contentType = contentType;
|
|
1304
1361
|
}
|
|
1362
|
+
input;
|
|
1363
|
+
caption;
|
|
1364
|
+
contentType;
|
|
1365
|
+
[IMAGE_BRAND] = true;
|
|
1305
1366
|
};
|
|
1306
|
-
function
|
|
1307
|
-
|
|
1308
|
-
if (raw === null || raw === void 0) return void 0;
|
|
1309
|
-
const text = safeString(raw);
|
|
1310
|
-
return text.length > 0 ? text : void 0;
|
|
1367
|
+
function image(input, options = {}) {
|
|
1368
|
+
return new ImageAttachment(input, options.caption, options.contentType);
|
|
1311
1369
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
1314
|
-
return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
|
|
1370
|
+
function isImageAttachment(value) {
|
|
1371
|
+
return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
|
|
1315
1372
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
sink.checkCanceled();
|
|
1323
|
-
const id = resolveId(options, element, index);
|
|
1324
|
-
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
1325
|
-
const step = sink.startStep(options.op ?? "item", description);
|
|
1326
|
-
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1327
|
-
try {
|
|
1328
|
-
await handler(element, handle, index);
|
|
1329
|
-
if (!handle.reported) handle.succeeded();
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
if (isTerminalError(error) || sink.isAborted()) {
|
|
1332
|
-
step?.setStatus("cancelled").succeeded();
|
|
1333
|
-
throw error;
|
|
1334
|
-
}
|
|
1335
|
-
const message = thrownMessage(error);
|
|
1336
|
-
if (!handle.reported) handle.failed(message);
|
|
1337
|
-
step?.failed(error);
|
|
1338
|
-
sink.logError(`item ${description} falhou: ${message}`);
|
|
1339
|
-
}
|
|
1340
|
-
step?.succeeded();
|
|
1341
|
-
if (options.progress !== false) sink.advance();
|
|
1342
|
-
result.total += 1;
|
|
1343
|
-
result[handle.status ?? "succeeded"] += 1;
|
|
1344
|
-
index += 1;
|
|
1373
|
+
function toImageAttachment(value) {
|
|
1374
|
+
return isImageAttachment(value) ? value : new ImageAttachment(value);
|
|
1375
|
+
}
|
|
1376
|
+
async function readImageFile(path) {
|
|
1377
|
+
if (!isNode()) {
|
|
1378
|
+
throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
|
|
1345
1379
|
}
|
|
1346
|
-
|
|
1380
|
+
const { readFile } = await import("fs/promises");
|
|
1381
|
+
return new Uint8Array(await readFile(path));
|
|
1347
1382
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
1352
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1353
|
-
function isFileRef(value) {
|
|
1354
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1355
|
-
const ref = value;
|
|
1356
|
-
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
1383
|
+
function startsWith(bytes, signature, offset = 0) {
|
|
1384
|
+
if (bytes.length < offset + signature.length) return false;
|
|
1385
|
+
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1357
1386
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1387
|
+
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1388
|
+
var MAX_PNG_DIMENSION = WIRE_LIMITS.MAX_IMAGE_DIMENSION_PX;
|
|
1389
|
+
var JPEG_MAGIC = [255, 216, 255];
|
|
1390
|
+
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1391
|
+
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
1392
|
+
function sniffContentType(bytes) {
|
|
1393
|
+
if (startsWith(bytes, PNG_MAGIC)) return "image/png";
|
|
1394
|
+
if (startsWith(bytes, JPEG_MAGIC)) return "image/jpeg";
|
|
1395
|
+
if (startsWith(bytes, RIFF_MAGIC) && startsWith(bytes, WEBP_MAGIC, 8)) return "image/webp";
|
|
1363
1396
|
return void 0;
|
|
1364
1397
|
}
|
|
1365
|
-
function
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
return
|
|
1398
|
+
function readPngDimensions(bytes) {
|
|
1399
|
+
if (!startsWith(bytes, PNG_MAGIC) || bytes.length < 24) return void 0;
|
|
1400
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1401
|
+
const width = view.getUint32(16);
|
|
1402
|
+
const height = view.getUint32(20);
|
|
1403
|
+
if (width === 0 || height === 0) return void 0;
|
|
1404
|
+
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1405
|
+
return { width, height };
|
|
1372
1406
|
}
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1375
|
-
|
|
1407
|
+
async function toBytes(input) {
|
|
1408
|
+
if (typeof input === "string") return readImageFile(input);
|
|
1409
|
+
if (input instanceof Uint8Array) return input;
|
|
1410
|
+
return new Uint8Array(input);
|
|
1376
1411
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1412
|
+
|
|
1413
|
+
// src/tasks/run-context-store.ts
|
|
1414
|
+
var storage;
|
|
1415
|
+
var stepStorage;
|
|
1416
|
+
var initPromise;
|
|
1417
|
+
async function ensureStorage() {
|
|
1418
|
+
if (storage || !isNode()) return;
|
|
1419
|
+
initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
|
|
1420
|
+
storage = new ALS();
|
|
1421
|
+
stepStorage = new ALS();
|
|
1422
|
+
});
|
|
1423
|
+
await initPromise;
|
|
1380
1424
|
}
|
|
1381
|
-
function
|
|
1382
|
-
|
|
1425
|
+
async function runWithRunContext(store, fn) {
|
|
1426
|
+
await ensureStorage();
|
|
1427
|
+
return storage ? storage.run(store, fn) : fn();
|
|
1383
1428
|
}
|
|
1384
|
-
function
|
|
1385
|
-
return
|
|
1429
|
+
function currentRunReporter() {
|
|
1430
|
+
return storage?.getStore()?.reporter;
|
|
1386
1431
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1432
|
+
async function runWithStepScope(scope, fn) {
|
|
1433
|
+
await ensureStorage();
|
|
1434
|
+
return stepStorage ? stepStorage.run(scope, fn) : fn();
|
|
1435
|
+
}
|
|
1436
|
+
function currentStep(transaction) {
|
|
1437
|
+
const scope = stepStorage?.getStore();
|
|
1438
|
+
return scope?.transaction === transaction ? scope.step : void 0;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/retry.ts
|
|
1442
|
+
var DEFAULT_RETRIES = 3;
|
|
1443
|
+
var DEFAULT_DELAY_MS = 1e3;
|
|
1444
|
+
function resolveDelay(delay, attempt, fallbackMs) {
|
|
1445
|
+
const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
|
|
1446
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1447
|
+
}
|
|
1448
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
1449
|
+
const requested = options.retries ?? DEFAULT_RETRIES;
|
|
1450
|
+
const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
|
|
1451
|
+
const wait = options.wait ?? sleep;
|
|
1452
|
+
for (let attempt = 1; ; attempt++) {
|
|
1453
|
+
if (options.signal?.aborted) throw runCanceledError();
|
|
1407
1454
|
try {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
return value;
|
|
1417
|
-
}
|
|
1418
|
-
single(refOrField) {
|
|
1419
|
-
if (typeof refOrField === "string") {
|
|
1420
|
-
const value = this.field(refOrField);
|
|
1421
|
-
if (Array.isArray(value)) {
|
|
1422
|
-
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1423
|
-
}
|
|
1424
|
-
return value;
|
|
1455
|
+
return await fn(attempt);
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
const exhausted = attempt > retries;
|
|
1458
|
+
const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
|
|
1459
|
+
if (exhausted || !retriable) throw error;
|
|
1460
|
+
const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
|
|
1461
|
+
options.onRetry?.(error, attempt, delay);
|
|
1462
|
+
await wait(delay);
|
|
1425
1463
|
}
|
|
1426
|
-
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
1427
|
-
return refOrField;
|
|
1428
|
-
}
|
|
1429
|
-
async downloadAll(field, dest) {
|
|
1430
|
-
const value = this.field(field);
|
|
1431
|
-
const refs = Array.isArray(value) ? value : [value];
|
|
1432
|
-
const paths = [];
|
|
1433
|
-
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
1434
|
-
return paths;
|
|
1435
1464
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/transaction.ts
|
|
1468
|
+
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
1469
|
+
var DEFAULT_STEP_RETRIES = 1;
|
|
1470
|
+
var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
|
|
1471
|
+
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
1472
|
+
var MAX_BACKLOG = 100;
|
|
1473
|
+
function stepMessage(text) {
|
|
1474
|
+
return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
1475
|
+
}
|
|
1476
|
+
function stepId() {
|
|
1477
|
+
return uuid4().slice(0, 16);
|
|
1478
|
+
}
|
|
1479
|
+
function stepLabel(op, description) {
|
|
1480
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
1481
|
+
}
|
|
1482
|
+
function errorText(error) {
|
|
1483
|
+
if (error instanceof Error) return error.message || error.name;
|
|
1484
|
+
return safeString(error);
|
|
1485
|
+
}
|
|
1486
|
+
function errorReason(error) {
|
|
1487
|
+
return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
|
|
1488
|
+
}
|
|
1489
|
+
function levelForStatus(status) {
|
|
1490
|
+
switch (status) {
|
|
1491
|
+
case "warning":
|
|
1492
|
+
return "warning";
|
|
1493
|
+
case "error":
|
|
1494
|
+
return "error";
|
|
1495
|
+
case "cancelled":
|
|
1496
|
+
return "warning";
|
|
1497
|
+
default:
|
|
1498
|
+
return "success";
|
|
1445
1499
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1500
|
+
}
|
|
1501
|
+
function colorForStatus(status) {
|
|
1502
|
+
switch (status) {
|
|
1503
|
+
case "warning":
|
|
1504
|
+
return "yellow";
|
|
1505
|
+
case "error":
|
|
1506
|
+
return "red";
|
|
1507
|
+
case "cancelled":
|
|
1508
|
+
return "gray";
|
|
1509
|
+
default:
|
|
1510
|
+
return "green";
|
|
1452
1511
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1512
|
+
}
|
|
1513
|
+
var Step = class {
|
|
1514
|
+
constructor(transaction, parentStepId, op, description) {
|
|
1515
|
+
this.transaction = transaction;
|
|
1516
|
+
this.parentStepId = parentStepId;
|
|
1517
|
+
this.op = op;
|
|
1518
|
+
this.description = description;
|
|
1455
1519
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1520
|
+
transaction;
|
|
1521
|
+
parentStepId;
|
|
1522
|
+
op;
|
|
1523
|
+
description;
|
|
1524
|
+
stepId = stepId();
|
|
1525
|
+
startTimestamp = nowISO();
|
|
1526
|
+
status = "ok";
|
|
1527
|
+
endTimestamp;
|
|
1528
|
+
_data;
|
|
1529
|
+
// ── API do bot ────────────────────────────────────────────────────────────
|
|
1530
|
+
warning(message, options) {
|
|
1531
|
+
if (this.status !== "error") this._setStatus("warning", message);
|
|
1532
|
+
if (options?.image !== void 0) this.attachImage(options.image);
|
|
1533
|
+
return this;
|
|
1464
1534
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1535
|
+
image(image3) {
|
|
1536
|
+
this.attachImage(image3);
|
|
1537
|
+
return this;
|
|
1538
|
+
}
|
|
1539
|
+
data(key, value) {
|
|
1540
|
+
if (key === "attachments") return this;
|
|
1541
|
+
this._setData(key, value);
|
|
1542
|
+
return this;
|
|
1543
|
+
}
|
|
1544
|
+
// ── Internos (chamados pelo SDK) ──────────────────────────────────────────
|
|
1545
|
+
/** `data` da etapa como vai no wire (undefined quando vazio). */
|
|
1546
|
+
get payload() {
|
|
1547
|
+
return this._data;
|
|
1548
|
+
}
|
|
1549
|
+
_setStatus(status, message) {
|
|
1550
|
+
this.status = status;
|
|
1551
|
+
if (message) this._setData("message", stepMessage(safeString(message)));
|
|
1552
|
+
return this;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
|
|
1556
|
+
* valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
|
|
1557
|
+
*/
|
|
1558
|
+
_failed(error, message) {
|
|
1559
|
+
if (this.endTimestamp) return this;
|
|
1560
|
+
this.status = "error";
|
|
1561
|
+
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
1562
|
+
const raw = message ?? errorText(error);
|
|
1563
|
+
this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
|
|
1564
|
+
this._close();
|
|
1565
|
+
return this;
|
|
1566
|
+
}
|
|
1567
|
+
_setData(key, value) {
|
|
1568
|
+
this._data = { ...this._data, [key]: value };
|
|
1569
|
+
}
|
|
1570
|
+
_deleteData(key) {
|
|
1571
|
+
if (!this._data || !(key in this._data)) return;
|
|
1572
|
+
const rest = { ...this._data };
|
|
1573
|
+
delete rest[key];
|
|
1574
|
+
this._data = Object.keys(rest).length > 0 ? rest : void 0;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
|
|
1578
|
+
* `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
|
|
1579
|
+
* run não há onde pendurar a imagem — descarta com aviso único.
|
|
1580
|
+
*/
|
|
1581
|
+
attachImage(image3) {
|
|
1582
|
+
const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
|
|
1583
|
+
if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
|
|
1584
|
+
const id = this.transaction.attachImage(image3);
|
|
1585
|
+
if (!id) return;
|
|
1586
|
+
this._setData("attachments", [...current, id]);
|
|
1587
|
+
}
|
|
1588
|
+
/** Motivo registrado (warning/erro) — usado no log de FINISH. */
|
|
1589
|
+
reason() {
|
|
1590
|
+
const error = this._data?.error;
|
|
1591
|
+
const text = typeof error?.message === "string" ? error.message : this._data?.message;
|
|
1592
|
+
return typeof text === "string" && text ? text : void 0;
|
|
1593
|
+
}
|
|
1594
|
+
/** Fecha a etapa mantendo o status que estiver. Idempotente. */
|
|
1595
|
+
_close() {
|
|
1596
|
+
if (this.endTimestamp) return;
|
|
1597
|
+
this.endTimestamp = nowISO();
|
|
1598
|
+
this.transaction.emit(
|
|
1599
|
+
"step",
|
|
1600
|
+
"finish",
|
|
1601
|
+
stepLabel(this.op, this.description),
|
|
1602
|
+
this.startTimestamp,
|
|
1603
|
+
this.endTimestamp,
|
|
1604
|
+
this.status,
|
|
1605
|
+
this.reason()
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
toJSON() {
|
|
1609
|
+
return {
|
|
1610
|
+
step_id: this.stepId,
|
|
1611
|
+
...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
|
|
1612
|
+
...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
|
|
1613
|
+
...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
|
|
1614
|
+
status: this.status,
|
|
1615
|
+
start_timestamp: this.startTimestamp,
|
|
1616
|
+
timestamp: this.endTimestamp ?? nowISO(),
|
|
1617
|
+
...this._data ? { data: this._data } : {}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
var Transaction = class {
|
|
1622
|
+
constructor(client, name, op, runId, options = {}) {
|
|
1623
|
+
this.client = client;
|
|
1624
|
+
this.name = name;
|
|
1625
|
+
this.op = op;
|
|
1626
|
+
this.runId = runId;
|
|
1627
|
+
this.echo = options.echo ?? false;
|
|
1628
|
+
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
1629
|
+
}
|
|
1630
|
+
client;
|
|
1631
|
+
name;
|
|
1632
|
+
op;
|
|
1633
|
+
runId;
|
|
1634
|
+
eventId = uuid4();
|
|
1635
|
+
stepId = stepId();
|
|
1636
|
+
startTimestamp = nowISO();
|
|
1637
|
+
status = "ok";
|
|
1638
|
+
steps = [];
|
|
1639
|
+
finished = false;
|
|
1640
|
+
lifecycleSink;
|
|
1641
|
+
attachmentSink;
|
|
1642
|
+
warnedNoAttachmentSink = false;
|
|
1643
|
+
// instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
|
|
1644
|
+
stepGuard;
|
|
1645
|
+
// modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
|
|
1646
|
+
echo;
|
|
1647
|
+
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
1648
|
+
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
1649
|
+
lifecycleBacklog = [];
|
|
1650
|
+
// ── Lifecycle / eco ───────────────────────────────────────────────────────
|
|
1651
|
+
/**
|
|
1652
|
+
* Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
|
|
1653
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
|
|
1654
|
+
* run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
|
|
1655
|
+
* etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
|
|
1656
|
+
*/
|
|
1657
|
+
emit(kind, phase, label, startTs, endTs, status, reason) {
|
|
1658
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
1659
|
+
const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
|
|
1660
|
+
const localFinish = this.echo && kind === "step" && phase === "finish";
|
|
1661
|
+
if (this.client?.debug || localFinish) {
|
|
1662
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
1663
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
1476
1664
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1665
|
+
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
1666
|
+
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
1667
|
+
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
1668
|
+
else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
|
|
1479
1669
|
}
|
|
1480
|
-
/**
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1670
|
+
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
1671
|
+
setLifecycleSink(sink) {
|
|
1672
|
+
this.lifecycleSink = sink;
|
|
1673
|
+
const backlog = this.lifecycleBacklog;
|
|
1674
|
+
this.lifecycleBacklog = [];
|
|
1675
|
+
for (const [level, message] of backlog) sink(level, message);
|
|
1676
|
+
}
|
|
1677
|
+
/** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
|
|
1678
|
+
setAttachmentSink(sink) {
|
|
1679
|
+
this.attachmentSink = sink;
|
|
1680
|
+
}
|
|
1681
|
+
/** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
|
|
1682
|
+
attachImage(image3) {
|
|
1683
|
+
if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
|
|
1684
|
+
if (!this.warnedNoAttachmentSink) {
|
|
1685
|
+
this.warnedNoAttachmentSink = true;
|
|
1686
|
+
console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
1491
1687
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1688
|
+
return void 0;
|
|
1689
|
+
}
|
|
1690
|
+
// ── Etapas ────────────────────────────────────────────────────────────────
|
|
1691
|
+
/**
|
|
1692
|
+
* Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
|
|
1693
|
+
* cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
|
|
1694
|
+
*/
|
|
1695
|
+
setStepGuard(guard) {
|
|
1696
|
+
this.stepGuard = guard;
|
|
1697
|
+
}
|
|
1698
|
+
/** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
|
|
1699
|
+
currentStep() {
|
|
1700
|
+
return currentStep(this);
|
|
1701
|
+
}
|
|
1702
|
+
/** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
|
|
1703
|
+
scope(step, fn) {
|
|
1704
|
+
if (!step) return fn();
|
|
1705
|
+
return runWithStepScope({ transaction: this, step }, fn);
|
|
1706
|
+
}
|
|
1707
|
+
/** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
|
|
1708
|
+
start(op, description) {
|
|
1709
|
+
this.stepGuard?.();
|
|
1710
|
+
const parent = this.currentStep();
|
|
1711
|
+
const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
|
|
1712
|
+
this.steps.push(step);
|
|
1713
|
+
this.emit("step", "start", stepLabel(op, description));
|
|
1714
|
+
return step;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
|
|
1718
|
+
* tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
|
|
1719
|
+
* Resolve com o retorno de `fn`.
|
|
1720
|
+
*
|
|
1721
|
+
* Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
|
|
1722
|
+
* mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
|
|
1723
|
+
* sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
|
|
1724
|
+
*/
|
|
1725
|
+
async step(name, fn, options = {}) {
|
|
1726
|
+
const requested = options.retries ?? DEFAULT_STEP_RETRIES;
|
|
1727
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
|
|
1728
|
+
const step = this.start(void 0, safeString(name));
|
|
1729
|
+
let attempts = 0;
|
|
1730
|
+
let lastFailure;
|
|
1507
1731
|
try {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
break;
|
|
1732
|
+
const result = await retryWithBackoff(
|
|
1733
|
+
(attempt) => {
|
|
1734
|
+
attempts = attempt;
|
|
1735
|
+
return this.scope(step, async () => fn(step));
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
retries: totalRetries,
|
|
1739
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
1740
|
+
signal: options.signal,
|
|
1741
|
+
wait: options.wait,
|
|
1742
|
+
onRetry: (error, attempt, delayMs) => {
|
|
1743
|
+
lastFailure = { error };
|
|
1744
|
+
options.warn?.(
|
|
1745
|
+
`etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
1746
|
+
);
|
|
1524
1747
|
}
|
|
1525
|
-
await handle.write(chunk.value);
|
|
1526
1748
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
1749
|
+
);
|
|
1750
|
+
this.noteAttempts(step, attempts);
|
|
1751
|
+
if (lastFailure && step.status === "ok") {
|
|
1752
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
1532
1753
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
await rm(part, { force: true });
|
|
1536
|
-
if (signal.aborted) throw runCanceledError();
|
|
1537
|
-
if (timedOut) {
|
|
1538
|
-
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
1539
|
-
}
|
|
1540
|
-
throw this.translate(error, ref);
|
|
1541
|
-
} finally {
|
|
1542
|
-
clearTimeout(timer);
|
|
1543
|
-
signal.removeEventListener("abort", onCancel);
|
|
1544
|
-
}
|
|
1545
|
-
try {
|
|
1546
|
-
await api.confirmFileDownload(runId, ref.id);
|
|
1754
|
+
step._close();
|
|
1755
|
+
return result;
|
|
1547
1756
|
} catch (error) {
|
|
1548
|
-
this.
|
|
1757
|
+
this.noteAttempts(step, attempts);
|
|
1758
|
+
if (isTerminalError(error)) step._setStatus("cancelled")._close();
|
|
1759
|
+
else step._failed(error);
|
|
1760
|
+
throw error;
|
|
1549
1761
|
}
|
|
1550
1762
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1763
|
+
noteAttempts(step, attempts) {
|
|
1764
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
1765
|
+
}
|
|
1766
|
+
// ── Metadados ─────────────────────────────────────────────────────────────
|
|
1767
|
+
setStatus(status) {
|
|
1768
|
+
this.status = status;
|
|
1769
|
+
return this;
|
|
1770
|
+
}
|
|
1771
|
+
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
1772
|
+
/**
|
|
1773
|
+
* Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
|
|
1774
|
+
* ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
|
|
1775
|
+
* `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
|
|
1776
|
+
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
1777
|
+
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
1778
|
+
* Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
|
|
1779
|
+
*/
|
|
1780
|
+
flush() {
|
|
1781
|
+
if (!this.client || this.finished) return 0;
|
|
1782
|
+
const ready = /* @__PURE__ */ new Set();
|
|
1783
|
+
for (const step of this.steps) {
|
|
1784
|
+
if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
|
|
1568
1785
|
}
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1786
|
+
if (ready.size === 0) return 0;
|
|
1787
|
+
const batch = [];
|
|
1788
|
+
const remaining = [];
|
|
1789
|
+
for (const step of this.steps) {
|
|
1790
|
+
if (ready.has(step.stepId) || ready.has(step.parentStepId)) {
|
|
1791
|
+
ready.add(step.stepId);
|
|
1792
|
+
batch.push(step);
|
|
1793
|
+
} else {
|
|
1794
|
+
remaining.push(step);
|
|
1578
1795
|
}
|
|
1579
|
-
await rename(part, target);
|
|
1580
|
-
} catch (error) {
|
|
1581
|
-
await rm(part, { force: true });
|
|
1582
|
-
throw error;
|
|
1583
1796
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1797
|
+
for (const step of batch) step._close();
|
|
1798
|
+
this.steps.splice(0, this.steps.length, ...remaining);
|
|
1799
|
+
for (let i = 0; i < batch.length; i += MAX_STEPS) {
|
|
1800
|
+
const chunk = batch.slice(i, i + MAX_STEPS);
|
|
1801
|
+
const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
|
|
1802
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
1803
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
1590
1804
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1805
|
+
return batch.length;
|
|
1806
|
+
}
|
|
1807
|
+
/** Entrega já (com teto) o que está na fila do transport — as transactions parciais inclusive. */
|
|
1808
|
+
async drain(timeoutMs) {
|
|
1809
|
+
await this.client?.flush(timeoutMs);
|
|
1810
|
+
}
|
|
1811
|
+
payload(eventId, op, status, startTimestamp, timestamp, steps) {
|
|
1812
|
+
return {
|
|
1813
|
+
event_id: eventId,
|
|
1814
|
+
name: this.name,
|
|
1815
|
+
op,
|
|
1816
|
+
status,
|
|
1817
|
+
start_timestamp: startTimestamp,
|
|
1818
|
+
timestamp,
|
|
1819
|
+
steps: steps.map((step) => step.toJSON()),
|
|
1820
|
+
// sem tags próprias: o client mescla as do processo
|
|
1821
|
+
tags: void 0,
|
|
1822
|
+
run_id: this.runId
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
finish() {
|
|
1826
|
+
if (this.finished) return;
|
|
1827
|
+
this.finished = true;
|
|
1828
|
+
for (const step of this.steps) step._close();
|
|
1829
|
+
const endTimestamp = nowISO();
|
|
1830
|
+
this.emit(
|
|
1831
|
+
"transaction",
|
|
1832
|
+
"finish",
|
|
1833
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
1834
|
+
this.startTimestamp,
|
|
1835
|
+
endTimestamp,
|
|
1836
|
+
this.status
|
|
1837
|
+
);
|
|
1838
|
+
if (!this.client) return;
|
|
1839
|
+
const steps = this.steps;
|
|
1840
|
+
let first = 0;
|
|
1841
|
+
while (steps.length - first > MAX_STEPS) {
|
|
1842
|
+
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
1843
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
1844
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
1845
|
+
first += MAX_STEPS;
|
|
1593
1846
|
}
|
|
1594
|
-
|
|
1847
|
+
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
1595
1848
|
}
|
|
1596
1849
|
};
|
|
1597
1850
|
|
|
1598
|
-
// src/
|
|
1599
|
-
var
|
|
1600
|
-
var
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1851
|
+
// src/tasks/each.ts
|
|
1852
|
+
var DEFAULT_ITEM_ID_FIELD = "id";
|
|
1853
|
+
var DEFAULT_EACH_RETRIES = 1;
|
|
1854
|
+
var ID_SEPARATOR = "|";
|
|
1855
|
+
function thrownMessage(error) {
|
|
1856
|
+
if (error instanceof Error) return error.message || error.name;
|
|
1857
|
+
return safeString(error);
|
|
1604
1858
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1859
|
+
var ItemHandle = class {
|
|
1860
|
+
constructor(index, id, step, basePayload, sink) {
|
|
1861
|
+
this.index = index;
|
|
1862
|
+
this.step = step;
|
|
1863
|
+
this.basePayload = basePayload;
|
|
1864
|
+
this.sink = sink;
|
|
1865
|
+
this.id = id ?? uuid4();
|
|
1866
|
+
}
|
|
1867
|
+
index;
|
|
1868
|
+
step;
|
|
1869
|
+
basePayload;
|
|
1870
|
+
sink;
|
|
1871
|
+
/** A identidade resolvida por `options.id` ou, sem ela, uma gerada — a mesma em todas as tentativas. */
|
|
1872
|
+
id;
|
|
1873
|
+
reported = false;
|
|
1874
|
+
status;
|
|
1875
|
+
attempt = 1;
|
|
1876
|
+
succeeded(payload, options) {
|
|
1877
|
+
this.record("succeeded", void 0, payload, options?.image);
|
|
1878
|
+
}
|
|
1879
|
+
failed(message, payload, options) {
|
|
1880
|
+
this.record("failed", message, payload, options?.image);
|
|
1881
|
+
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
1882
|
+
}
|
|
1883
|
+
occurrence(message, payload, options) {
|
|
1884
|
+
this.record("occurrence", message, payload, options?.image);
|
|
1885
|
+
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
1886
|
+
}
|
|
1887
|
+
/** Nova tentativa: o desfecho da anterior não vale (o id é o mesmo, o último reporte vence). */
|
|
1888
|
+
_nextAttempt(attempt) {
|
|
1889
|
+
this.attempt = attempt;
|
|
1890
|
+
this.reported = false;
|
|
1891
|
+
this.status = void 0;
|
|
1892
|
+
if (this.step && (this.step.status === "error" || this.step.status === "warning")) {
|
|
1893
|
+
this.step._setStatus("ok");
|
|
1894
|
+
this.step._deleteData("message");
|
|
1620
1895
|
}
|
|
1621
1896
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
|
|
1628
|
-
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1629
|
-
var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
|
|
1630
|
-
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1631
|
-
var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
|
|
1632
|
-
var ImageAttachment = class {
|
|
1633
|
-
constructor(input, caption, contentType) {
|
|
1634
|
-
this.input = input;
|
|
1635
|
-
this.caption = caption;
|
|
1636
|
-
this.contentType = contentType;
|
|
1897
|
+
record(status, message, payload, image3) {
|
|
1898
|
+
this.reported = true;
|
|
1899
|
+
this.status = status;
|
|
1900
|
+
const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
|
|
1901
|
+
this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
|
|
1637
1902
|
}
|
|
1638
|
-
input;
|
|
1639
|
-
caption;
|
|
1640
|
-
contentType;
|
|
1641
|
-
[IMAGE_BRAND] = true;
|
|
1642
1903
|
};
|
|
1643
|
-
function
|
|
1644
|
-
return
|
|
1904
|
+
function field(element, name) {
|
|
1905
|
+
if (typeof element !== "object" || element === null) return { present: false, value: void 0 };
|
|
1906
|
+
const record = element;
|
|
1907
|
+
return { present: name in record, value: record[name] };
|
|
1645
1908
|
}
|
|
1646
|
-
function
|
|
1647
|
-
|
|
1909
|
+
function resolveId(spec, element, index, explicit) {
|
|
1910
|
+
if (spec === null) return void 0;
|
|
1911
|
+
let raw;
|
|
1912
|
+
if (typeof spec === "function") {
|
|
1913
|
+
raw = spec(element, index);
|
|
1914
|
+
} else {
|
|
1915
|
+
const fields = spec === void 0 ? [DEFAULT_ITEM_ID_FIELD] : typeof spec === "string" ? [spec] : spec;
|
|
1916
|
+
const parts = [];
|
|
1917
|
+
for (const name of fields) {
|
|
1918
|
+
const { present, value } = field(element, name);
|
|
1919
|
+
if (!present || value === null || value === void 0) {
|
|
1920
|
+
if (explicit) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`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`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
parts.push(safeString(value));
|
|
1928
|
+
}
|
|
1929
|
+
raw = parts.join(ID_SEPARATOR);
|
|
1930
|
+
}
|
|
1931
|
+
if (raw === null || raw === void 0) return void 0;
|
|
1932
|
+
const text = safeString(raw);
|
|
1933
|
+
return text.length > 0 ? text : void 0;
|
|
1648
1934
|
}
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1935
|
+
function knownLength(list) {
|
|
1936
|
+
const length = list?.length;
|
|
1937
|
+
return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
|
|
1651
1938
|
}
|
|
1652
|
-
|
|
1653
|
-
if (
|
|
1654
|
-
|
|
1939
|
+
function skippedMessage(count) {
|
|
1940
|
+
if (count === 1) return "1 item pulado: j\xE1 conclu\xEDdo neste trabalho numa execu\xE7\xE3o anterior";
|
|
1941
|
+
return `${count} itens pulados: j\xE1 conclu\xEDdos neste trabalho numa execu\xE7\xE3o anterior`;
|
|
1942
|
+
}
|
|
1943
|
+
async function eachItem(list, handler, options, sink) {
|
|
1944
|
+
const result = { total: 0, succeeded: 0, failed: 0, occurrence: 0, skipped: 0 };
|
|
1945
|
+
const requested = options.retries ?? DEFAULT_EACH_RETRIES;
|
|
1946
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_EACH_RETRIES;
|
|
1947
|
+
const explicitId = options.id !== void 0;
|
|
1948
|
+
const length = options.progress === false ? void 0 : knownLength(list);
|
|
1949
|
+
if (length !== void 0) sink.declareTotal(length);
|
|
1950
|
+
const done = options.skipDone === false ? void 0 : await sink.doneItemIds();
|
|
1951
|
+
let index = 0;
|
|
1952
|
+
for await (const element of list) {
|
|
1953
|
+
sink.checkCanceled();
|
|
1954
|
+
const id = resolveId(options.id, element, index, explicitId);
|
|
1955
|
+
if (done?.size && id !== void 0 && done.has(id)) {
|
|
1956
|
+
result.total += 1;
|
|
1957
|
+
result.skipped += 1;
|
|
1958
|
+
if (options.progress !== false) sink.advance();
|
|
1959
|
+
index += 1;
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
1963
|
+
const step = sink.startStep(options.op ?? "item", description);
|
|
1964
|
+
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1965
|
+
const current = index;
|
|
1966
|
+
let attempts = 0;
|
|
1967
|
+
let lastFailure;
|
|
1968
|
+
try {
|
|
1969
|
+
await retryWithBackoff(
|
|
1970
|
+
(attempt) => {
|
|
1971
|
+
attempts = attempt;
|
|
1972
|
+
if (attempt > 1) handle._nextAttempt(attempt);
|
|
1973
|
+
return sink.stepScope(step, async () => handler(element, handle, current));
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
retries: totalRetries,
|
|
1977
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
1978
|
+
// sem nova tentativa: ocorrência (regra de negócio), desfecho já registrado pelo
|
|
1979
|
+
// handler (ele decidiu) ou run abortada (o erro é efeito do cancelamento)
|
|
1980
|
+
shouldRetry: (error) => !isOccurrence(error) && !handle.reported && !sink.isAborted(),
|
|
1981
|
+
wait: (ms) => sink.sleep(ms),
|
|
1982
|
+
onRetry: (error, attempt, delayMs) => {
|
|
1983
|
+
lastFailure = { error };
|
|
1984
|
+
sink.logWarning(
|
|
1985
|
+
`item ${description}: tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
);
|
|
1990
|
+
if (!handle.reported) handle.succeeded();
|
|
1991
|
+
if (step) {
|
|
1992
|
+
noteAttempts(step, attempts);
|
|
1993
|
+
if (lastFailure && step.status === "ok") {
|
|
1994
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
if (isTerminalError(error) || sink.isAborted()) {
|
|
1999
|
+
step?._setStatus("cancelled")._close();
|
|
2000
|
+
throw error;
|
|
2001
|
+
}
|
|
2002
|
+
if (step) noteAttempts(step, attempts);
|
|
2003
|
+
if (isOccurrence(error)) {
|
|
2004
|
+
const occurrence = error;
|
|
2005
|
+
if (!handle.reported) handle.occurrence(occurrence.message, occurrence.payload, { image: occurrence.image });
|
|
2006
|
+
} else {
|
|
2007
|
+
const message = thrownMessage(error);
|
|
2008
|
+
if (!handle.reported) handle.failed(message);
|
|
2009
|
+
step?._failed(error);
|
|
2010
|
+
sink.logError(`item ${description} falhou: ${message}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
step?._close();
|
|
2014
|
+
if (options.progress !== false) sink.advance();
|
|
2015
|
+
result.total += 1;
|
|
2016
|
+
result[handle.status ?? "succeeded"] += 1;
|
|
2017
|
+
index += 1;
|
|
2018
|
+
if (options.flush !== false) await sink.flush();
|
|
1655
2019
|
}
|
|
1656
|
-
|
|
1657
|
-
return
|
|
2020
|
+
if (result.skipped > 0) sink.logInfo(skippedMessage(result.skipped));
|
|
2021
|
+
return result;
|
|
1658
2022
|
}
|
|
1659
|
-
function
|
|
1660
|
-
if (
|
|
1661
|
-
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
2023
|
+
function noteAttempts(step, attempts) {
|
|
2024
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
1662
2025
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
var
|
|
1666
|
-
var
|
|
1667
|
-
var
|
|
1668
|
-
function
|
|
1669
|
-
if (
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
return void 0;
|
|
2026
|
+
|
|
2027
|
+
// src/tasks/files.ts
|
|
2028
|
+
var LOCAL_FILES_DIR_ENV = "HOBOTS_LOCAL_FILES_DIR";
|
|
2029
|
+
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
2030
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2031
|
+
function isFileRef(value) {
|
|
2032
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2033
|
+
const ref = value;
|
|
2034
|
+
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
1673
2035
|
}
|
|
1674
|
-
function
|
|
1675
|
-
if (!
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1681
|
-
return { width, height };
|
|
2036
|
+
function fileField(params, field2) {
|
|
2037
|
+
if (!Object.prototype.hasOwnProperty.call(params, field2)) return void 0;
|
|
2038
|
+
const value = params[field2];
|
|
2039
|
+
if (isFileRef(value)) return value;
|
|
2040
|
+
if (Array.isArray(value) && value.every(isFileRef)) return value;
|
|
2041
|
+
return void 0;
|
|
1682
2042
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2043
|
+
function collectFileRefs(params) {
|
|
2044
|
+
const refs = [];
|
|
2045
|
+
for (const field2 of Object.keys(params)) {
|
|
2046
|
+
const value = fileField(params, field2);
|
|
2047
|
+
if (value !== void 0) refs.push(...Array.isArray(value) ? value : [value]);
|
|
2048
|
+
}
|
|
2049
|
+
return refs;
|
|
1687
2050
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
debug: "gray",
|
|
1692
|
-
info: "cyan",
|
|
1693
|
-
warning: "yellow",
|
|
1694
|
-
error: "red",
|
|
1695
|
-
success: "green"
|
|
1696
|
-
};
|
|
1697
|
-
function joinLogArgs(message, rest) {
|
|
1698
|
-
if (rest.length === 0) return safeString(message);
|
|
1699
|
-
return [message, ...rest].map(safeString).join(" ");
|
|
2051
|
+
function safeFileName(name, fallback) {
|
|
2052
|
+
const cleaned = name.replace(/^.*[\\/]/, "").replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
|
|
2053
|
+
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
1700
2054
|
}
|
|
1701
|
-
function
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
1705
|
-
const text = args.slice(0, -1);
|
|
1706
|
-
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
1707
|
-
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
2055
|
+
function isInsideDir(root, candidate, sep) {
|
|
2056
|
+
const base = root.endsWith(sep) ? root : root + sep;
|
|
2057
|
+
return candidate.startsWith(base) && candidate.length > base.length;
|
|
1708
2058
|
}
|
|
1709
|
-
function
|
|
1710
|
-
|
|
1711
|
-
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1712
|
-
console[method2](line, message);
|
|
2059
|
+
function unavailableLocal(ref) {
|
|
2060
|
+
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
1713
2061
|
}
|
|
1714
|
-
function
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
const onAbort = () => {
|
|
1721
|
-
clearTimeout(timer);
|
|
1722
|
-
reject(runCanceledError());
|
|
1723
|
-
};
|
|
1724
|
-
const timer = setTimeout(() => {
|
|
1725
|
-
signal.removeEventListener("abort", onAbort);
|
|
1726
|
-
resolve2();
|
|
1727
|
-
}, ms);
|
|
1728
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1729
|
-
});
|
|
2062
|
+
function localRunPath(ref) {
|
|
2063
|
+
if (!isLocalRun()) return void 0;
|
|
2064
|
+
const path = ref.path;
|
|
2065
|
+
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
1730
2066
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
1734
|
-
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
1735
|
-
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
1736
|
-
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
1737
|
-
var FLUSH_AT_BUFFERED = 20;
|
|
1738
|
-
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
1739
|
-
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
1740
|
-
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
1741
|
-
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1742
|
-
var ITEMS_DRAIN_MS = 15e3;
|
|
1743
|
-
var PROGRESS_FLUSH_MS = 1e3;
|
|
1744
|
-
function scrubPayloadValue(value, depth) {
|
|
1745
|
-
if (typeof value === "string") return scrubSecrets(value);
|
|
1746
|
-
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1747
|
-
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1748
|
-
const out = {};
|
|
1749
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1750
|
-
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1751
|
-
}
|
|
1752
|
-
return out;
|
|
2067
|
+
function errorCode(error) {
|
|
2068
|
+
return error?.code;
|
|
1753
2069
|
}
|
|
1754
|
-
var
|
|
1755
|
-
constructor(
|
|
1756
|
-
this.
|
|
1757
|
-
this.
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
);
|
|
1765
|
-
const logAt = (level) => (message, ...rest) => {
|
|
1766
|
-
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
1767
|
-
this.log(level, text, image3);
|
|
1768
|
-
};
|
|
1769
|
-
const log = {
|
|
1770
|
-
debug: logAt("debug"),
|
|
1771
|
-
info: logAt("info"),
|
|
1772
|
-
warn: logAt("warning"),
|
|
1773
|
-
error: logAt("error"),
|
|
1774
|
-
success: logAt("success")
|
|
1775
|
-
};
|
|
1776
|
-
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1777
|
-
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
1778
|
-
const checkCanceled = () => this.checkCanceled();
|
|
1779
|
-
tx.setStepGuard(checkCanceled);
|
|
1780
|
-
const eachSink = {
|
|
1781
|
-
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
1782
|
-
startStep: (op, description) => tx.start(op, description),
|
|
1783
|
-
declareTotal: (total) => {
|
|
1784
|
-
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
1785
|
-
},
|
|
1786
|
-
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
1787
|
-
checkCanceled,
|
|
1788
|
-
isAborted: () => this.abortController.signal.aborted,
|
|
1789
|
-
logError: (message) => this.log("error", message)
|
|
1790
|
-
};
|
|
1791
|
-
const items = {
|
|
1792
|
-
add: (item) => {
|
|
1793
|
-
checkCanceled();
|
|
1794
|
-
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
1795
|
-
},
|
|
1796
|
-
succeeded: (payload, options) => {
|
|
1797
|
-
checkCanceled();
|
|
1798
|
-
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
1799
|
-
},
|
|
1800
|
-
failed: (message, payload, options) => {
|
|
1801
|
-
checkCanceled();
|
|
1802
|
-
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
1803
|
-
},
|
|
1804
|
-
occurrence: (message, payload, options) => {
|
|
1805
|
-
checkCanceled();
|
|
1806
|
-
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
1807
|
-
},
|
|
1808
|
-
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
2070
|
+
var RunFileStore = class {
|
|
2071
|
+
constructor(deps) {
|
|
2072
|
+
this.deps = deps;
|
|
2073
|
+
this.files = {
|
|
2074
|
+
list: () => collectFileRefs(deps.params),
|
|
2075
|
+
get: (field2) => fileField(deps.params, field2),
|
|
2076
|
+
// async: erro de uso (campo sem arquivo, valor inválido) chega como Promise rejeitada, nunca síncrono
|
|
2077
|
+
download: async (refOrField, options) => this.download(this.single(refOrField), options?.dest),
|
|
2078
|
+
downloadAll: async (field2, options) => this.downloadAll(field2, options?.dest),
|
|
2079
|
+
read: async (refOrField) => this.read(this.single(refOrField))
|
|
1809
2080
|
};
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2081
|
+
}
|
|
2082
|
+
deps;
|
|
2083
|
+
files;
|
|
2084
|
+
tempDir;
|
|
2085
|
+
/** Apaga a pasta temporária da execução (cópias que o bot não levou para `dest`). Nunca lança. */
|
|
2086
|
+
async cleanup() {
|
|
2087
|
+
const pending = this.tempDir;
|
|
2088
|
+
this.tempDir = void 0;
|
|
2089
|
+
if (!pending) return;
|
|
2090
|
+
try {
|
|
2091
|
+
const { rm } = await import("fs/promises");
|
|
2092
|
+
await rm(await pending, { recursive: true, force: true });
|
|
2093
|
+
} catch {
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
field(field2) {
|
|
2097
|
+
const value = fileField(this.deps.params, field2);
|
|
2098
|
+
if (value === void 0) throw new Error(`campo "${field2}" n\xE3o tem arquivo nos params`);
|
|
2099
|
+
return value;
|
|
2100
|
+
}
|
|
2101
|
+
single(refOrField) {
|
|
2102
|
+
if (typeof refOrField === "string") {
|
|
2103
|
+
const value = this.field(refOrField);
|
|
2104
|
+
if (Array.isArray(value)) {
|
|
2105
|
+
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1831
2106
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
1837
|
-
this.context = {
|
|
1838
|
-
runId,
|
|
1839
|
-
params,
|
|
1840
|
-
log,
|
|
1841
|
-
items,
|
|
1842
|
-
progress,
|
|
1843
|
-
files: this.fileStore.files,
|
|
1844
|
-
transaction: tx,
|
|
1845
|
-
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1846
|
-
signal,
|
|
1847
|
-
throwIfCanceled: checkCanceled,
|
|
1848
|
-
sleep: sleep2,
|
|
1849
|
-
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1850
|
-
};
|
|
2107
|
+
return value;
|
|
2108
|
+
}
|
|
2109
|
+
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
2110
|
+
return refOrField;
|
|
1851
2111
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
dead = false;
|
|
1859
|
-
chain = Promise.resolve();
|
|
1860
|
-
pendingUpdates = 0;
|
|
1861
|
-
timer;
|
|
1862
|
-
watchTimer;
|
|
1863
|
-
abortController = new AbortController();
|
|
1864
|
-
flushIntervalMs;
|
|
1865
|
-
cancelCheckIntervalMs;
|
|
1866
|
-
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1867
|
-
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1868
|
-
// envio preservada e sem lotes sobrepostos.
|
|
1869
|
-
itemsBuffer = [];
|
|
1870
|
-
itemsChain = Promise.resolve();
|
|
1871
|
-
itemsTimer;
|
|
1872
|
-
itemSeq = 0;
|
|
1873
|
-
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1874
|
-
itemsUnsupported = false;
|
|
1875
|
-
/**
|
|
1876
|
-
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
1877
|
-
* degrada com um aviso só.
|
|
1878
|
-
*/
|
|
1879
|
-
itemsQuotaExceeded = false;
|
|
1880
|
-
/**
|
|
1881
|
-
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
1882
|
-
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
1883
|
-
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
1884
|
-
*/
|
|
1885
|
-
itemsStorageFull = false;
|
|
1886
|
-
/**
|
|
1887
|
-
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1888
|
-
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1889
|
-
*/
|
|
1890
|
-
itemsClosed = false;
|
|
1891
|
-
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1892
|
-
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1893
|
-
pendingAttachments = /* @__PURE__ */ new Set();
|
|
1894
|
-
attachmentCount = 0;
|
|
1895
|
-
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
1896
|
-
uploadQueue = [];
|
|
1897
|
-
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1898
|
-
attachmentsUnsupported = false;
|
|
1899
|
-
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
1900
|
-
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
1901
|
-
progressCurrent = 0;
|
|
1902
|
-
progressTotal = null;
|
|
1903
|
-
/** Snapshot ainda não entregue ao servidor. */
|
|
1904
|
-
progressDirty = false;
|
|
1905
|
-
progressTimer;
|
|
1906
|
-
progressWarned = false;
|
|
1907
|
-
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1908
|
-
fileStore;
|
|
1909
|
-
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
1910
|
-
cleanupFiles() {
|
|
1911
|
-
return this.fileStore.cleanup();
|
|
2112
|
+
async downloadAll(field2, dest) {
|
|
2113
|
+
const value = this.field(field2);
|
|
2114
|
+
const refs = Array.isArray(value) ? value : [value];
|
|
2115
|
+
const paths = [];
|
|
2116
|
+
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
2117
|
+
return paths;
|
|
1912
2118
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
2119
|
+
async read(ref) {
|
|
2120
|
+
const { readFile, rm } = await import("fs/promises");
|
|
2121
|
+
const { dirname } = await import("path");
|
|
2122
|
+
const path = await this.download(ref, void 0);
|
|
2123
|
+
try {
|
|
2124
|
+
return await readFile(path);
|
|
2125
|
+
} finally {
|
|
2126
|
+
await rm(dirname(path), { recursive: true, force: true });
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
async download(ref, dest, destIsDir = false) {
|
|
2130
|
+
this.checkCanceled();
|
|
2131
|
+
const target = await this.resolveTarget(ref, dest, destIsDir);
|
|
2132
|
+
if (ref.storage === "local") await this.copyLocal(ref, target);
|
|
2133
|
+
else await this.downloadS3(ref, target);
|
|
2134
|
+
return target;
|
|
1915
2135
|
}
|
|
1916
|
-
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
1917
2136
|
checkCanceled() {
|
|
1918
|
-
if (this.
|
|
2137
|
+
if (this.deps.signal.aborted) throw runCanceledError();
|
|
1919
2138
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2139
|
+
runTempDir() {
|
|
2140
|
+
this.tempDir ??= (async () => {
|
|
2141
|
+
const { mkdtemp } = await import("fs/promises");
|
|
2142
|
+
const { tmpdir } = await import("os");
|
|
2143
|
+
const { join } = await import("path");
|
|
2144
|
+
return mkdtemp(join(tmpdir(), "hobots-files-"));
|
|
2145
|
+
})();
|
|
2146
|
+
return this.tempDir;
|
|
1924
2147
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2148
|
+
async resolveTarget(ref, dest, destIsDir) {
|
|
2149
|
+
const { mkdir, mkdtemp, stat } = await import("fs/promises");
|
|
2150
|
+
const { dirname, join, resolve: resolve2 } = await import("path");
|
|
2151
|
+
const name = safeFileName(ref.name, "arquivo");
|
|
2152
|
+
if (dest === void 0) {
|
|
2153
|
+
return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
|
|
2154
|
+
}
|
|
2155
|
+
const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
|
|
2156
|
+
if (isDir) {
|
|
2157
|
+
await mkdir(dest, { recursive: true });
|
|
2158
|
+
return resolve2(dest, name);
|
|
2159
|
+
}
|
|
2160
|
+
await mkdir(dirname(resolve2(dest)), { recursive: true });
|
|
2161
|
+
return resolve2(dest);
|
|
2162
|
+
}
|
|
2163
|
+
/** storage 's3': URL pré-assinada → GET em streaming → confere o tamanho → só então DELETE. */
|
|
2164
|
+
async downloadS3(ref, target) {
|
|
2165
|
+
const { api, runId, signal } = this.deps;
|
|
2166
|
+
let expected;
|
|
2167
|
+
let url;
|
|
2168
|
+
try {
|
|
2169
|
+
const info = await api.requestFileDownload(runId, ref.id, signal);
|
|
2170
|
+
expected = info.size >= 0 ? info.size : ref.size;
|
|
2171
|
+
url = info.url;
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
throw this.translate(error, ref);
|
|
2174
|
+
}
|
|
2175
|
+
const { open, rename, rm } = await import("fs/promises");
|
|
2176
|
+
const part = `${target}.part`;
|
|
2177
|
+
const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
|
|
2178
|
+
const controller = new AbortController();
|
|
2179
|
+
let timedOut = false;
|
|
2180
|
+
let timer;
|
|
2181
|
+
const arm = () => {
|
|
2182
|
+
clearTimeout(timer);
|
|
2183
|
+
timer = setTimeout(() => {
|
|
2184
|
+
timedOut = true;
|
|
2185
|
+
controller.abort();
|
|
2186
|
+
}, idleTimeoutMs);
|
|
2187
|
+
};
|
|
2188
|
+
const onCancel = () => controller.abort();
|
|
2189
|
+
signal.addEventListener("abort", onCancel, { once: true });
|
|
2190
|
+
try {
|
|
2191
|
+
arm();
|
|
2192
|
+
const response = await api.fetchFile(url, controller.signal);
|
|
2193
|
+
const handle = await open(part, "w");
|
|
2194
|
+
let written = 0;
|
|
2195
|
+
try {
|
|
2196
|
+
const reader = response.body?.getReader();
|
|
2197
|
+
const stopReading = () => void reader?.cancel().catch(() => void 0);
|
|
2198
|
+
controller.signal.addEventListener("abort", stopReading, { once: true });
|
|
2199
|
+
for (; ; ) {
|
|
2200
|
+
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
2201
|
+
if (chunk.done) break;
|
|
2202
|
+
arm();
|
|
2203
|
+
written += chunk.value.byteLength;
|
|
2204
|
+
if (written > expected) {
|
|
2205
|
+
stopReading();
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
await handle.write(chunk.value);
|
|
2209
|
+
}
|
|
2210
|
+
} finally {
|
|
2211
|
+
await handle.close();
|
|
2212
|
+
}
|
|
2213
|
+
if (written !== expected) {
|
|
2214
|
+
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
2215
|
+
}
|
|
2216
|
+
await rename(part, target);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
await rm(part, { force: true });
|
|
2219
|
+
if (signal.aborted) throw runCanceledError();
|
|
2220
|
+
if (timedOut) {
|
|
2221
|
+
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
2222
|
+
}
|
|
2223
|
+
throw this.translate(error, ref);
|
|
2224
|
+
} finally {
|
|
2225
|
+
clearTimeout(timer);
|
|
2226
|
+
signal.removeEventListener("abort", onCancel);
|
|
2227
|
+
}
|
|
2228
|
+
try {
|
|
2229
|
+
await api.confirmFileDownload(runId, ref.id);
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
1941
2232
|
}
|
|
1942
|
-
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
1943
|
-
for (const resume of waiting) resume();
|
|
1944
2233
|
}
|
|
1945
2234
|
/**
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
* então preserva os logs mais recentes, os mais valiosos na morte do processo.
|
|
2235
|
+
* storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
|
|
2236
|
+
* origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
|
|
1949
2237
|
*/
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2238
|
+
async copyLocal(ref, target) {
|
|
2239
|
+
const direct = localRunPath(ref);
|
|
2240
|
+
if (direct !== void 0) {
|
|
2241
|
+
await this.copyFromPath(ref, direct, target);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
2245
|
+
if (!base) throw unavailableLocal(ref);
|
|
2246
|
+
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
2247
|
+
const { join, resolve: resolve2, sep } = await import("path");
|
|
2248
|
+
const root = resolve2(base);
|
|
2249
|
+
const folder = resolve2(root, ref.id);
|
|
2250
|
+
if (!isInsideDir(root, folder, sep) || !UUID_RE.test(ref.id)) {
|
|
2251
|
+
throw new Error(`id de arquivo local inv\xE1lido: ${JSON.stringify(ref.id)}`);
|
|
2252
|
+
}
|
|
2253
|
+
let names;
|
|
2254
|
+
try {
|
|
2255
|
+
names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
|
|
2258
|
+
throw error;
|
|
2259
|
+
}
|
|
2260
|
+
if (names.length === 0) throw unavailableLocal(ref);
|
|
2261
|
+
if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
|
|
2262
|
+
this.checkCanceled();
|
|
2263
|
+
const part = `${target}.part`;
|
|
2264
|
+
try {
|
|
2265
|
+
await copyFile(join(folder, names[0]), part);
|
|
2266
|
+
const { size } = await stat(part);
|
|
2267
|
+
if (size !== ref.size) {
|
|
2268
|
+
throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
|
|
2269
|
+
}
|
|
2270
|
+
await rename(part, target);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
await rm(part, { force: true });
|
|
2273
|
+
throw error;
|
|
2274
|
+
}
|
|
2275
|
+
await rm(folder, { recursive: true, force: true });
|
|
1953
2276
|
}
|
|
1954
2277
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
* intacta e o handler livre da latência do upload.
|
|
2278
|
+
* Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
|
|
2279
|
+
* Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
|
|
1958
2280
|
*/
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
const
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
this.
|
|
2281
|
+
async copyFromPath(ref, path, target) {
|
|
2282
|
+
const { copyFile, rename, rm, stat } = await import("fs/promises");
|
|
2283
|
+
const { resolve: resolve2 } = await import("path");
|
|
2284
|
+
const source = resolve2(path);
|
|
2285
|
+
const info = await stat(source).catch(() => void 0);
|
|
2286
|
+
if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
|
|
2287
|
+
this.checkCanceled();
|
|
2288
|
+
const part = `${target}.part`;
|
|
2289
|
+
try {
|
|
2290
|
+
await copyFile(source, part);
|
|
2291
|
+
await rename(part, target);
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
await rm(part, { force: true });
|
|
2294
|
+
throw error;
|
|
2295
|
+
}
|
|
1966
2296
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
if (
|
|
1970
|
-
|
|
1971
|
-
this.buffer.push({
|
|
1972
|
-
ts,
|
|
1973
|
-
level,
|
|
1974
|
-
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
1975
|
-
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
1976
|
-
});
|
|
1977
|
-
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
1978
|
-
this.flushLogs();
|
|
1979
|
-
} else {
|
|
1980
|
-
this.scheduleFlush();
|
|
2297
|
+
translate(error, ref) {
|
|
2298
|
+
if (this.deps.signal.aborted) return runCanceledError();
|
|
2299
|
+
if (error instanceof TasksApiError && error.status === 410) {
|
|
2300
|
+
return new TasksApiError(`arquivo "${ref.name}" j\xE1 baixado ou expirado`, 410);
|
|
1981
2301
|
}
|
|
1982
|
-
|
|
2302
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2303
|
+
return new TasksApiError(`arquivo "${ref.name}" n\xE3o encontrado nesta execu\xE7\xE3o`, 404);
|
|
2304
|
+
}
|
|
2305
|
+
return error;
|
|
1983
2306
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
if (
|
|
2019
|
-
|
|
2020
|
-
await Promise.race([
|
|
2021
|
-
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2022
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2023
|
-
]);
|
|
2024
|
-
}
|
|
2025
|
-
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
2026
|
-
async reportRunning() {
|
|
2027
|
-
this.enqueue({ status: "running" });
|
|
2028
|
-
await this.chain;
|
|
2029
|
-
if (!this.dead) this.startCancelWatch();
|
|
2030
|
-
return !this.dead;
|
|
2031
|
-
}
|
|
2032
|
-
reportSuccess() {
|
|
2033
|
-
this.stopCancelWatch();
|
|
2034
|
-
this.clearFlushTimer();
|
|
2035
|
-
this.forceFinalProgress();
|
|
2036
|
-
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
2037
|
-
}
|
|
2038
|
-
reportFailure(error) {
|
|
2039
|
-
this.stopCancelWatch();
|
|
2040
|
-
this.clearFlushTimer();
|
|
2041
|
-
this.forceFinalProgress();
|
|
2042
|
-
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
2046
|
-
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
2047
|
-
*/
|
|
2048
|
-
takeLogsForTerminal() {
|
|
2049
|
-
let logs = this.takeLogs();
|
|
2050
|
-
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
2051
|
-
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
2052
|
-
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
2053
|
-
}
|
|
2054
|
-
return logs;
|
|
2055
|
-
}
|
|
2056
|
-
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
2057
|
-
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
2058
|
-
setProgress(current, total) {
|
|
2059
|
-
if (this.dead) return;
|
|
2060
|
-
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
2061
|
-
if (!this.progressWarned) {
|
|
2062
|
-
this.progressWarned = true;
|
|
2063
|
-
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
2064
|
-
}
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
// src/tasks/context.ts
|
|
2310
|
+
var LOG_LEVEL_COLOR = {
|
|
2311
|
+
debug: "gray",
|
|
2312
|
+
info: "cyan",
|
|
2313
|
+
warning: "yellow",
|
|
2314
|
+
error: "red",
|
|
2315
|
+
success: "green"
|
|
2316
|
+
};
|
|
2317
|
+
function joinLogArgs(message, rest) {
|
|
2318
|
+
if (rest.length === 0) return safeString(message);
|
|
2319
|
+
return [message, ...rest].map(safeString).join(" ");
|
|
2320
|
+
}
|
|
2321
|
+
function splitLogArgs(message, rest) {
|
|
2322
|
+
const args = [message, ...rest];
|
|
2323
|
+
const last = args[args.length - 1];
|
|
2324
|
+
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
2325
|
+
const text = args.slice(0, -1);
|
|
2326
|
+
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
2327
|
+
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
2328
|
+
}
|
|
2329
|
+
function printRunLog(level, message) {
|
|
2330
|
+
const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
2331
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
2332
|
+
console[method2](line, message);
|
|
2333
|
+
}
|
|
2334
|
+
var PROGRESS_LINE_PREFIX = "[hobots:progress]";
|
|
2335
|
+
var PROGRESS_PRINT_THROTTLE_MS = 250;
|
|
2336
|
+
function printRunProgress(current, total) {
|
|
2337
|
+
console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
|
|
2338
|
+
}
|
|
2339
|
+
function cancelableSleep(ms, signal) {
|
|
2340
|
+
return new Promise((resolve2, reject) => {
|
|
2341
|
+
if (signal.aborted) {
|
|
2342
|
+
reject(runCanceledError());
|
|
2065
2343
|
return;
|
|
2066
2344
|
}
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
takeProgress() {
|
|
2100
|
-
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
2101
|
-
this.progressDirty = false;
|
|
2102
|
-
return { current: this.progressCurrent, total: this.progressTotal };
|
|
2103
|
-
}
|
|
2104
|
-
/**
|
|
2105
|
-
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
2106
|
-
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
2107
|
-
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
2108
|
-
*/
|
|
2109
|
-
startCancelWatch() {
|
|
2110
|
-
if (this.watchTimer) return;
|
|
2111
|
-
this.watchTimer = setInterval(() => {
|
|
2112
|
-
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2113
|
-
this.enqueue({ status: "running" });
|
|
2114
|
-
}, this.cancelCheckIntervalMs);
|
|
2115
|
-
unrefTimer(this.watchTimer);
|
|
2116
|
-
}
|
|
2117
|
-
stopCancelWatch() {
|
|
2118
|
-
if (this.watchTimer) {
|
|
2119
|
-
clearInterval(this.watchTimer);
|
|
2120
|
-
this.watchTimer = void 0;
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
2124
|
-
finalize() {
|
|
2125
|
-
return this.chain;
|
|
2126
|
-
}
|
|
2127
|
-
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
2128
|
-
/**
|
|
2129
|
-
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2130
|
-
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2131
|
-
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2132
|
-
*/
|
|
2133
|
-
addItem(status, message, payload, id, image3) {
|
|
2134
|
-
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2135
|
-
let scrubbedPayload;
|
|
2136
|
-
if (payload) {
|
|
2137
|
-
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2138
|
-
let serializedLength;
|
|
2139
|
-
try {
|
|
2140
|
-
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2141
|
-
} catch {
|
|
2142
|
-
}
|
|
2143
|
-
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2144
|
-
this.logger.warn(
|
|
2145
|
-
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2146
|
-
);
|
|
2147
|
-
scrubbedPayload = void 0;
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
const ts = nowISO();
|
|
2151
|
-
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2152
|
-
this.itemsBuffer.push({
|
|
2153
|
-
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2154
|
-
seq: this.itemSeq++,
|
|
2155
|
-
status,
|
|
2156
|
-
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2157
|
-
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2158
|
-
ts,
|
|
2159
|
-
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2160
|
-
});
|
|
2161
|
-
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2162
|
-
this.flushItems();
|
|
2163
|
-
} else {
|
|
2164
|
-
this.scheduleItemsFlush();
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
scheduleItemsFlush() {
|
|
2168
|
-
if (this.itemsTimer) return;
|
|
2169
|
-
this.itemsTimer = setTimeout(() => {
|
|
2170
|
-
this.itemsTimer = void 0;
|
|
2171
|
-
this.flushItems();
|
|
2172
|
-
}, this.flushIntervalMs);
|
|
2173
|
-
unrefTimer(this.itemsTimer);
|
|
2174
|
-
}
|
|
2175
|
-
clearItemsTimer() {
|
|
2176
|
-
if (this.itemsTimer) {
|
|
2177
|
-
clearTimeout(this.itemsTimer);
|
|
2178
|
-
this.itemsTimer = void 0;
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
2182
|
-
flushItems() {
|
|
2183
|
-
this.clearItemsTimer();
|
|
2184
|
-
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2185
|
-
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2186
|
-
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2187
|
-
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2188
|
-
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
async sendItems(batch) {
|
|
2192
|
-
if (this.itemsUnsupported) return;
|
|
2193
|
-
try {
|
|
2194
|
-
await this.api.createItems(this.runId, { items: batch });
|
|
2195
|
-
this.itemsStorageFull = false;
|
|
2196
|
-
} catch (error) {
|
|
2197
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
2198
|
-
this.itemsUnsupported = true;
|
|
2199
|
-
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
if (error instanceof TasksApiError && error.status === 429) {
|
|
2203
|
-
if (!this.itemsQuotaExceeded) {
|
|
2204
|
-
this.itemsQuotaExceeded = true;
|
|
2205
|
-
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
2206
|
-
}
|
|
2207
|
-
return;
|
|
2208
|
-
}
|
|
2209
|
-
if (error instanceof TasksApiError && error.status === 402) {
|
|
2210
|
-
if (!this.itemsStorageFull) {
|
|
2211
|
-
this.itemsStorageFull = true;
|
|
2212
|
-
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2213
|
-
}
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
if (error instanceof TasksApiError) {
|
|
2217
|
-
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
2218
|
-
this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
|
|
2219
|
-
return;
|
|
2220
|
-
}
|
|
2221
|
-
this.itemsBuffer.unshift(...batch);
|
|
2222
|
-
this.scheduleItemsFlush();
|
|
2223
|
-
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
/**
|
|
2227
|
-
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
2228
|
-
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
2229
|
-
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2230
|
-
*/
|
|
2231
|
-
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2232
|
-
this.flushItems();
|
|
2233
|
-
await Promise.race([
|
|
2234
|
-
this.itemsChain,
|
|
2235
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2236
|
-
]);
|
|
2237
|
-
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2238
|
-
this.flushItems();
|
|
2239
|
-
await Promise.race([
|
|
2240
|
-
this.itemsChain,
|
|
2241
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2242
|
-
]);
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2246
|
-
/**
|
|
2247
|
-
* Pendura uma imagem num evento da run (log, item ou step): faz as checagens que não
|
|
2248
|
-
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2249
|
-
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2250
|
-
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2251
|
-
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2252
|
-
* também cai aqui via `log()`.
|
|
2253
|
-
*/
|
|
2254
|
-
queueAttachment(imageIn, ts) {
|
|
2255
|
-
if (this.dead) return void 0;
|
|
2256
|
-
const image3 = toImageAttachment(imageIn);
|
|
2257
|
-
if (this.attachmentsUnsupported) return void 0;
|
|
2258
|
-
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
2259
|
-
if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
|
|
2260
|
-
this.attachmentCount++;
|
|
2261
|
-
this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
2262
|
-
}
|
|
2263
|
-
return void 0;
|
|
2264
|
-
}
|
|
2265
|
-
this.attachmentCount++;
|
|
2266
|
-
const id = uuid4();
|
|
2267
|
-
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
2268
|
-
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
2269
|
-
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
2270
|
-
this.pendingAttachments.add(work);
|
|
2271
|
-
void work.finally(() => this.pendingAttachments.delete(work));
|
|
2272
|
-
return id;
|
|
2345
|
+
const onAbort = () => {
|
|
2346
|
+
clearTimeout(timer);
|
|
2347
|
+
reject(runCanceledError());
|
|
2348
|
+
};
|
|
2349
|
+
const timer = setTimeout(() => {
|
|
2350
|
+
signal.removeEventListener("abort", onAbort);
|
|
2351
|
+
resolve2();
|
|
2352
|
+
}, ms);
|
|
2353
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
2357
|
+
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
2358
|
+
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
2359
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
2360
|
+
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
2361
|
+
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
2362
|
+
var FLUSH_AT_BUFFERED = 20;
|
|
2363
|
+
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
2364
|
+
var MAX_ITEMS_DONE_PER_PAGE = WIRE_LIMITS.MAX_ITEMS_DONE_PER_PAGE;
|
|
2365
|
+
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
2366
|
+
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
2367
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
2368
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
2369
|
+
var PROGRESS_FLUSH_MS = 1e3;
|
|
2370
|
+
function scrubPayloadValue(value, depth) {
|
|
2371
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
2372
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
2373
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
2374
|
+
const out = {};
|
|
2375
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2376
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
2273
2377
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
this.log(
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
)
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2378
|
+
return out;
|
|
2379
|
+
}
|
|
2380
|
+
var RunReporter = class {
|
|
2381
|
+
constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
2382
|
+
this.api = api;
|
|
2383
|
+
this.runId = runId;
|
|
2384
|
+
this.logger = logger;
|
|
2385
|
+
this.tx = tx;
|
|
2386
|
+
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
2387
|
+
this.cancelCheckIntervalMs = Math.min(
|
|
2388
|
+
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
2389
|
+
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
2390
|
+
);
|
|
2391
|
+
const logAt = (level) => (message, ...rest) => {
|
|
2392
|
+
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
2393
|
+
this.log(level, text, image3);
|
|
2394
|
+
};
|
|
2395
|
+
const log = {
|
|
2396
|
+
debug: logAt("debug"),
|
|
2397
|
+
info: logAt("info"),
|
|
2398
|
+
warn: logAt("warning"),
|
|
2399
|
+
error: logAt("error"),
|
|
2400
|
+
success: logAt("success")
|
|
2401
|
+
};
|
|
2402
|
+
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
2403
|
+
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
2404
|
+
const checkCanceled = () => this.checkCanceled();
|
|
2405
|
+
tx.setStepGuard(checkCanceled);
|
|
2406
|
+
const eachSink = {
|
|
2407
|
+
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
2408
|
+
startStep: (op, description) => tx.start(op, description),
|
|
2409
|
+
stepScope: (step, fn) => tx.scope(step, fn),
|
|
2410
|
+
declareTotal: (total) => {
|
|
2411
|
+
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
2412
|
+
},
|
|
2413
|
+
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
2414
|
+
checkCanceled,
|
|
2415
|
+
isAborted: () => this.abortController.signal.aborted,
|
|
2416
|
+
logInfo: (message) => this.log("info", message),
|
|
2417
|
+
logWarning: (message) => this.log("warning", message),
|
|
2418
|
+
logError: (message) => this.log("error", message),
|
|
2419
|
+
sleep: (ms) => cancelableSleep(ms, this.abortController.signal),
|
|
2420
|
+
doneItemIds: () => this.listDoneItemIds(),
|
|
2421
|
+
flush: () => this.flush()
|
|
2422
|
+
};
|
|
2423
|
+
const items = {
|
|
2424
|
+
add: (item) => {
|
|
2425
|
+
checkCanceled();
|
|
2426
|
+
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
2427
|
+
},
|
|
2428
|
+
succeeded: (payload, options) => {
|
|
2429
|
+
checkCanceled();
|
|
2430
|
+
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
2431
|
+
},
|
|
2432
|
+
failed: (message, payload, options) => {
|
|
2433
|
+
checkCanceled();
|
|
2434
|
+
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
2435
|
+
},
|
|
2436
|
+
occurrence: (message, payload, options) => {
|
|
2437
|
+
checkCanceled();
|
|
2438
|
+
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
2439
|
+
},
|
|
2440
|
+
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
2441
|
+
};
|
|
2442
|
+
const progress = Object.defineProperties(
|
|
2443
|
+
{
|
|
2444
|
+
total: (total) => {
|
|
2445
|
+
checkCanceled();
|
|
2446
|
+
this.setProgress(this.progressCurrent, total);
|
|
2308
2447
|
},
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2448
|
+
advance: (count = 1) => {
|
|
2449
|
+
checkCanceled();
|
|
2450
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
2451
|
+
},
|
|
2452
|
+
set: (current, total) => {
|
|
2453
|
+
checkCanceled();
|
|
2454
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
2455
|
+
}
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
current: { enumerable: true, get: () => this.progressCurrent },
|
|
2459
|
+
snapshot: {
|
|
2460
|
+
enumerable: true,
|
|
2461
|
+
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
2462
|
+
}
|
|
2324
2463
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2464
|
+
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
2465
|
+
);
|
|
2466
|
+
const signal = this.abortController.signal;
|
|
2467
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
2468
|
+
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
2469
|
+
this.context = {
|
|
2470
|
+
runId,
|
|
2471
|
+
params,
|
|
2472
|
+
log,
|
|
2473
|
+
items,
|
|
2474
|
+
progress,
|
|
2475
|
+
files: this.fileStore.files,
|
|
2476
|
+
step: async (name, fn, options) => {
|
|
2477
|
+
const root = tx.currentStep() === void 0;
|
|
2478
|
+
try {
|
|
2479
|
+
return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
|
|
2480
|
+
} finally {
|
|
2481
|
+
if (options?.flush !== false && root && !signal.aborted) await this.flush();
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
2485
|
+
signal,
|
|
2486
|
+
throwIfCanceled: checkCanceled,
|
|
2487
|
+
sleep: sleep2,
|
|
2488
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
2344
2489
|
};
|
|
2345
|
-
if (this.uploadSlots > 0) {
|
|
2346
|
-
this.uploadSlots--;
|
|
2347
|
-
return Promise.resolve(release);
|
|
2348
|
-
}
|
|
2349
|
-
return new Promise((resolve2) => {
|
|
2350
|
-
this.uploadQueue.push(() => resolve2(release));
|
|
2351
|
-
});
|
|
2352
2490
|
}
|
|
2491
|
+
api;
|
|
2492
|
+
runId;
|
|
2493
|
+
logger;
|
|
2494
|
+
tx;
|
|
2495
|
+
context;
|
|
2496
|
+
buffer = [];
|
|
2497
|
+
dead = false;
|
|
2498
|
+
chain = Promise.resolve();
|
|
2499
|
+
pendingUpdates = 0;
|
|
2500
|
+
timer;
|
|
2501
|
+
watchTimer;
|
|
2502
|
+
abortController = new AbortController();
|
|
2503
|
+
flushIntervalMs;
|
|
2504
|
+
cancelCheckIntervalMs;
|
|
2505
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
2506
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
2507
|
+
// envio preservada e sem lotes sobrepostos.
|
|
2508
|
+
itemsBuffer = [];
|
|
2509
|
+
itemsChain = Promise.resolve();
|
|
2510
|
+
itemsTimer;
|
|
2511
|
+
itemSeq = 0;
|
|
2512
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
2513
|
+
itemsUnsupported = false;
|
|
2353
2514
|
/**
|
|
2354
|
-
*
|
|
2355
|
-
*
|
|
2515
|
+
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
2516
|
+
* degrada com um aviso só.
|
|
2356
2517
|
*/
|
|
2357
|
-
|
|
2358
|
-
if (this.pendingAttachments.size === 0) return;
|
|
2359
|
-
await Promise.race([
|
|
2360
|
-
Promise.allSettled([...this.pendingAttachments]),
|
|
2361
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2362
|
-
]);
|
|
2363
|
-
}
|
|
2364
|
-
enqueue(updateIn) {
|
|
2365
|
-
if (this.dead) {
|
|
2366
|
-
this.salvageLogs(updateIn.logs);
|
|
2367
|
-
return;
|
|
2368
|
-
}
|
|
2369
|
-
this.clearProgressTimer();
|
|
2370
|
-
const progress = this.takeProgress();
|
|
2371
|
-
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
2372
|
-
this.pendingUpdates++;
|
|
2373
|
-
this.chain = this.chain.then(async () => {
|
|
2374
|
-
if (this.dead) {
|
|
2375
|
-
this.salvageLogs(update.logs);
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
2379
|
-
try {
|
|
2380
|
-
await this.api.updateRun(this.runId, payload);
|
|
2381
|
-
} catch (error) {
|
|
2382
|
-
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
2383
|
-
this.dead = true;
|
|
2384
|
-
this.stopCancelWatch();
|
|
2385
|
-
this.abort();
|
|
2386
|
-
const pending = this.buffer.splice(0, this.buffer.length);
|
|
2387
|
-
this.salvageLogs(pending);
|
|
2388
|
-
this.logger.warn(
|
|
2389
|
-
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
2390
|
-
);
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
if (update.logs && update.logs.length > 0) {
|
|
2394
|
-
this.buffer.unshift(...update.logs);
|
|
2395
|
-
this.scheduleFlush();
|
|
2396
|
-
}
|
|
2397
|
-
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
2398
|
-
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
2399
|
-
}
|
|
2400
|
-
}).finally(() => {
|
|
2401
|
-
this.pendingUpdates--;
|
|
2402
|
-
});
|
|
2403
|
-
}
|
|
2518
|
+
itemsQuotaExceeded = false;
|
|
2404
2519
|
/**
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
* o
|
|
2520
|
+
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
2521
|
+
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
2522
|
+
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
2408
2523
|
*/
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2439
|
-
function stepMessage(text) {
|
|
2440
|
-
return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
2441
|
-
}
|
|
2442
|
-
function stepId() {
|
|
2443
|
-
return uuid4().slice(0, 16);
|
|
2444
|
-
}
|
|
2445
|
-
function stepLabel(op, description) {
|
|
2446
|
-
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
2447
|
-
}
|
|
2448
|
-
function levelForStatus(status) {
|
|
2449
|
-
switch (status) {
|
|
2450
|
-
case "warning":
|
|
2451
|
-
return "warning";
|
|
2452
|
-
case "error":
|
|
2453
|
-
return "error";
|
|
2454
|
-
case "cancelled":
|
|
2455
|
-
return "warning";
|
|
2456
|
-
default:
|
|
2457
|
-
return "success";
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
function colorForStatus(status) {
|
|
2461
|
-
switch (status) {
|
|
2462
|
-
case "warning":
|
|
2463
|
-
return "yellow";
|
|
2464
|
-
case "error":
|
|
2465
|
-
return "red";
|
|
2466
|
-
case "cancelled":
|
|
2467
|
-
return "gray";
|
|
2468
|
-
default:
|
|
2469
|
-
return "green";
|
|
2524
|
+
itemsStorageFull = false;
|
|
2525
|
+
/**
|
|
2526
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
2527
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
2528
|
+
*/
|
|
2529
|
+
itemsClosed = false;
|
|
2530
|
+
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
2531
|
+
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
2532
|
+
pendingAttachments = /* @__PURE__ */ new Set();
|
|
2533
|
+
attachmentCount = 0;
|
|
2534
|
+
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
2535
|
+
uploadQueue = [];
|
|
2536
|
+
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
2537
|
+
attachmentsUnsupported = false;
|
|
2538
|
+
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
2539
|
+
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
2540
|
+
progressCurrent = 0;
|
|
2541
|
+
progressTotal = null;
|
|
2542
|
+
/** Snapshot ainda não entregue ao servidor. */
|
|
2543
|
+
progressDirty = false;
|
|
2544
|
+
progressTimer;
|
|
2545
|
+
progressWarned = false;
|
|
2546
|
+
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
2547
|
+
progressLastPrint = 0;
|
|
2548
|
+
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
2549
|
+
fileStore;
|
|
2550
|
+
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
2551
|
+
cleanupFiles() {
|
|
2552
|
+
return this.fileStore.cleanup();
|
|
2470
2553
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
this.
|
|
2477
|
-
|
|
2554
|
+
get isDead() {
|
|
2555
|
+
return this.dead;
|
|
2556
|
+
}
|
|
2557
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
2558
|
+
checkCanceled() {
|
|
2559
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
2560
|
+
}
|
|
2561
|
+
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
2562
|
+
abort() {
|
|
2563
|
+
this.stopCancelWatch();
|
|
2564
|
+
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
2478
2565
|
}
|
|
2479
|
-
transaction;
|
|
2480
|
-
parentStepId;
|
|
2481
|
-
op;
|
|
2482
|
-
description;
|
|
2483
|
-
stepId = stepId();
|
|
2484
|
-
startTimestamp = nowISO();
|
|
2485
|
-
status = "ok";
|
|
2486
|
-
endTimestamp;
|
|
2487
|
-
data;
|
|
2488
2566
|
/**
|
|
2489
|
-
*
|
|
2490
|
-
*
|
|
2491
|
-
*
|
|
2567
|
+
* Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
|
|
2568
|
+
* Usado no exit path — depois que o agente reportou AgentTerminated por fora
|
|
2569
|
+
* da fila, um handler zumbi não pode reenviar status por cima.
|
|
2492
2570
|
*/
|
|
2493
|
-
|
|
2494
|
-
this.
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2571
|
+
kill() {
|
|
2572
|
+
this.abort();
|
|
2573
|
+
this.dead = true;
|
|
2574
|
+
this.clearFlushTimer();
|
|
2575
|
+
this.clearProgressTimer();
|
|
2576
|
+
this.buffer = [];
|
|
2577
|
+
this.itemsClosed = true;
|
|
2578
|
+
this.clearItemsTimer();
|
|
2579
|
+
if (this.itemsBuffer.length > 0) {
|
|
2580
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
2581
|
+
this.itemsBuffer = [];
|
|
2582
|
+
}
|
|
2583
|
+
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
2584
|
+
for (const resume of waiting) resume();
|
|
2498
2585
|
}
|
|
2499
2586
|
/**
|
|
2500
|
-
*
|
|
2501
|
-
*
|
|
2502
|
-
*
|
|
2503
|
-
* Numa step já fechada não faz nada.
|
|
2587
|
+
* Drena o buffer pendente para o PATCH único do exit path (chame ANTES de kill,
|
|
2588
|
+
* que descarta o buffer). Capado em um chunk — o exit path não tem retry nem fila,
|
|
2589
|
+
* então preserva os logs mais recentes, os mais valiosos na morte do processo.
|
|
2504
2590
|
*/
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
this.
|
|
2508
|
-
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
2509
|
-
const raw = message ?? (error instanceof Error ? error.message || error.name : String(error));
|
|
2510
|
-
this.setData("error", { ...type ? { type } : {}, message: stepMessage(raw) });
|
|
2511
|
-
if (options?.image !== void 0) this.attachImage(options.image);
|
|
2512
|
-
this.close();
|
|
2513
|
-
return this;
|
|
2591
|
+
drainLogs() {
|
|
2592
|
+
this.clearFlushTimer();
|
|
2593
|
+
return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
|
|
2514
2594
|
}
|
|
2515
2595
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2596
|
+
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
2597
|
+
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
2598
|
+
* intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
|
|
2599
|
+
* imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
|
|
2519
2600
|
*/
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2601
|
+
log(level, message, image3) {
|
|
2602
|
+
if (this.dead) return void 0;
|
|
2603
|
+
const scrubbed = scrubSecrets(safeString(message));
|
|
2604
|
+
const ts = nowISO();
|
|
2605
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2606
|
+
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
2607
|
+
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2608
|
+
return attachmentId;
|
|
2526
2609
|
}
|
|
2527
|
-
/**
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2610
|
+
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
2611
|
+
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
2612
|
+
if (this.dead) return ts;
|
|
2613
|
+
const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
|
|
2614
|
+
this.buffer.push({
|
|
2615
|
+
ts,
|
|
2616
|
+
level,
|
|
2617
|
+
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
2618
|
+
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
2619
|
+
});
|
|
2620
|
+
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
2621
|
+
this.flushLogs();
|
|
2622
|
+
} else {
|
|
2623
|
+
this.scheduleFlush();
|
|
2624
|
+
}
|
|
2625
|
+
return ts;
|
|
2532
2626
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2627
|
+
scheduleFlush() {
|
|
2628
|
+
if (this.timer) return;
|
|
2629
|
+
this.timer = setTimeout(() => {
|
|
2630
|
+
this.timer = void 0;
|
|
2631
|
+
this.flushLogs();
|
|
2632
|
+
}, this.flushIntervalMs);
|
|
2633
|
+
unrefTimer(this.timer);
|
|
2536
2634
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2635
|
+
clearFlushTimer() {
|
|
2636
|
+
if (this.timer) {
|
|
2637
|
+
clearTimeout(this.timer);
|
|
2638
|
+
this.timer = void 0;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
takeLogs() {
|
|
2642
|
+
return this.buffer.splice(0, this.buffer.length);
|
|
2643
|
+
}
|
|
2644
|
+
/** Flush incremental: envia o buffer como status 'running' (em chunks). */
|
|
2645
|
+
flushLogs() {
|
|
2646
|
+
this.clearFlushTimer();
|
|
2647
|
+
if (this.dead || this.buffer.length === 0) return;
|
|
2648
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
2649
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
2650
|
+
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
2651
|
+
}
|
|
2540
2652
|
}
|
|
2541
2653
|
/**
|
|
2542
|
-
*
|
|
2543
|
-
*
|
|
2654
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
|
|
2655
|
+
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2656
|
+
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2544
2657
|
*/
|
|
2545
|
-
|
|
2546
|
-
this.
|
|
2547
|
-
|
|
2658
|
+
async flush(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2659
|
+
this.tx.flush();
|
|
2660
|
+
this.flushItems();
|
|
2661
|
+
if (this.progressDirty) this.flushProgress();
|
|
2662
|
+
else this.flushLogs();
|
|
2663
|
+
await Promise.race([
|
|
2664
|
+
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2665
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2666
|
+
]);
|
|
2548
2667
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
this.
|
|
2552
|
-
this.
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
stepLabel(this.op, this.description),
|
|
2556
|
-
this.startTimestamp,
|
|
2557
|
-
this.endTimestamp,
|
|
2558
|
-
this.status,
|
|
2559
|
-
this.reason()
|
|
2560
|
-
);
|
|
2668
|
+
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
2669
|
+
async reportRunning() {
|
|
2670
|
+
this.enqueue({ status: "running" });
|
|
2671
|
+
await this.chain;
|
|
2672
|
+
if (!this.dead) this.startCancelWatch();
|
|
2673
|
+
return !this.dead;
|
|
2561
2674
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
description: this.description,
|
|
2568
|
-
status: this.status,
|
|
2569
|
-
start_timestamp: this.startTimestamp,
|
|
2570
|
-
timestamp: this.endTimestamp ?? nowISO(),
|
|
2571
|
-
data: this.data
|
|
2572
|
-
};
|
|
2675
|
+
reportSuccess() {
|
|
2676
|
+
this.stopCancelWatch();
|
|
2677
|
+
this.clearFlushTimer();
|
|
2678
|
+
this.forceFinalProgress();
|
|
2679
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
2573
2680
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
this.
|
|
2578
|
-
this.
|
|
2579
|
-
this.op = op;
|
|
2580
|
-
this.runId = runId;
|
|
2581
|
-
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2681
|
+
reportFailure(error) {
|
|
2682
|
+
this.stopCancelWatch();
|
|
2683
|
+
this.clearFlushTimer();
|
|
2684
|
+
this.forceFinalProgress();
|
|
2685
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
2582
2686
|
}
|
|
2583
|
-
client;
|
|
2584
|
-
name;
|
|
2585
|
-
op;
|
|
2586
|
-
runId;
|
|
2587
|
-
eventId = uuid4();
|
|
2588
|
-
stepId = stepId();
|
|
2589
|
-
startTimestamp = nowISO();
|
|
2590
|
-
status = "ok";
|
|
2591
|
-
steps = [];
|
|
2592
|
-
tags = {};
|
|
2593
|
-
finished = false;
|
|
2594
|
-
lifecycleSink;
|
|
2595
|
-
attachmentSink;
|
|
2596
|
-
warnedNoAttachmentSink = false;
|
|
2597
|
-
// instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
|
|
2598
|
-
stepGuard;
|
|
2599
|
-
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
2600
|
-
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
2601
|
-
lifecycleBacklog = [];
|
|
2602
2687
|
/**
|
|
2603
|
-
*
|
|
2604
|
-
*
|
|
2605
|
-
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
2606
|
-
* escreve no stdout do processo do usuário sem permissão.
|
|
2688
|
+
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
2689
|
+
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
2607
2690
|
*/
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
console.log(`${tag} ${label}${suffix}`);
|
|
2691
|
+
takeLogsForTerminal() {
|
|
2692
|
+
let logs = this.takeLogs();
|
|
2693
|
+
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
2694
|
+
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
2695
|
+
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
2614
2696
|
}
|
|
2615
|
-
|
|
2616
|
-
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
2617
|
-
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
2618
|
-
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
2697
|
+
return logs;
|
|
2619
2698
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2699
|
+
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
2700
|
+
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
2701
|
+
setProgress(current, total) {
|
|
2702
|
+
if (this.dead) return;
|
|
2703
|
+
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
2704
|
+
if (!this.progressWarned) {
|
|
2705
|
+
this.progressWarned = true;
|
|
2706
|
+
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
2707
|
+
}
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
2711
|
+
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
2712
|
+
const totalChanged = total !== this.progressTotal;
|
|
2713
|
+
if (next === this.progressCurrent && !totalChanged) return;
|
|
2714
|
+
this.progressCurrent = next;
|
|
2715
|
+
this.progressTotal = total;
|
|
2716
|
+
if (total == null) return;
|
|
2717
|
+
const now = Date.now();
|
|
2718
|
+
if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
|
|
2719
|
+
this.progressLastPrint = now;
|
|
2720
|
+
printRunProgress(next, total);
|
|
2721
|
+
}
|
|
2722
|
+
this.progressDirty = true;
|
|
2723
|
+
if (this.progressTimer) return;
|
|
2724
|
+
this.progressTimer = setTimeout(() => {
|
|
2725
|
+
this.progressTimer = void 0;
|
|
2726
|
+
this.flushProgress();
|
|
2727
|
+
}, PROGRESS_FLUSH_MS);
|
|
2728
|
+
unrefTimer(this.progressTimer);
|
|
2626
2729
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
this.
|
|
2730
|
+
flushProgress() {
|
|
2731
|
+
this.clearProgressTimer();
|
|
2732
|
+
if (this.dead || !this.progressDirty) return;
|
|
2733
|
+
if (this.buffer.length > 0) this.flushLogs();
|
|
2734
|
+
else this.enqueue({ status: "running" });
|
|
2630
2735
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
this.warnedNoAttachmentSink = true;
|
|
2636
|
-
console.warn("[hobots] imagem de step descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
2736
|
+
clearProgressTimer() {
|
|
2737
|
+
if (this.progressTimer) {
|
|
2738
|
+
clearTimeout(this.progressTimer);
|
|
2739
|
+
this.progressTimer = void 0;
|
|
2637
2740
|
}
|
|
2638
|
-
|
|
2741
|
+
}
|
|
2742
|
+
/** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
|
|
2743
|
+
forceFinalProgress() {
|
|
2744
|
+
this.clearProgressTimer();
|
|
2745
|
+
this.progressDirty = this.progressTotal != null;
|
|
2746
|
+
}
|
|
2747
|
+
/** Consome o snapshot pendente para pegar carona no próximo PATCH. */
|
|
2748
|
+
takeProgress() {
|
|
2749
|
+
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
2750
|
+
this.progressDirty = false;
|
|
2751
|
+
return { current: this.progressCurrent, total: this.progressTotal };
|
|
2639
2752
|
}
|
|
2640
2753
|
/**
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
2754
|
+
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
2755
|
+
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
2756
|
+
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
2643
2757
|
*/
|
|
2644
|
-
|
|
2645
|
-
this.
|
|
2758
|
+
startCancelWatch() {
|
|
2759
|
+
if (this.watchTimer) return;
|
|
2760
|
+
this.watchTimer = setInterval(() => {
|
|
2761
|
+
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2762
|
+
this.enqueue({ status: "running" });
|
|
2763
|
+
}, this.cancelCheckIntervalMs);
|
|
2764
|
+
unrefTimer(this.watchTimer);
|
|
2646
2765
|
}
|
|
2647
|
-
|
|
2648
|
-
this.
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
return step;
|
|
2766
|
+
stopCancelWatch() {
|
|
2767
|
+
if (this.watchTimer) {
|
|
2768
|
+
clearInterval(this.watchTimer);
|
|
2769
|
+
this.watchTimer = void 0;
|
|
2770
|
+
}
|
|
2653
2771
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
|
|
2656
|
-
return this.
|
|
2772
|
+
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
2773
|
+
finalize() {
|
|
2774
|
+
return this.chain;
|
|
2657
2775
|
}
|
|
2776
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
2658
2777
|
/**
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2778
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2779
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2780
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2661
2781
|
*/
|
|
2662
|
-
|
|
2663
|
-
|
|
2782
|
+
addItem(status, message, payload, id, image3) {
|
|
2783
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2784
|
+
let scrubbedPayload;
|
|
2785
|
+
if (payload) {
|
|
2786
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2787
|
+
let serializedLength;
|
|
2788
|
+
try {
|
|
2789
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2793
|
+
this.logger.warn(
|
|
2794
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2795
|
+
);
|
|
2796
|
+
scrubbedPayload = void 0;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
const ts = nowISO();
|
|
2800
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2801
|
+
this.itemsBuffer.push({
|
|
2802
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2803
|
+
seq: this.itemSeq++,
|
|
2804
|
+
status,
|
|
2805
|
+
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2806
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2807
|
+
ts,
|
|
2808
|
+
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2809
|
+
});
|
|
2810
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2811
|
+
this.flushItems();
|
|
2812
|
+
} else {
|
|
2813
|
+
this.scheduleItemsFlush();
|
|
2814
|
+
}
|
|
2664
2815
|
}
|
|
2665
2816
|
/**
|
|
2666
|
-
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2817
|
+
* Ids já concluídos (`succeeded`/`occurrence`) neste trabalho — a run e suas reexecuções.
|
|
2818
|
+
* Pagina até o fim. undefined quando a consulta falha (servidor antigo, rede): o `each`
|
|
2819
|
+
* avisa e processa tudo, nunca trava a run por isso.
|
|
2669
2820
|
*/
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2821
|
+
async listDoneItemIds() {
|
|
2822
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2823
|
+
let cursor = null;
|
|
2824
|
+
try {
|
|
2825
|
+
do {
|
|
2826
|
+
const page = await this.api.listDoneItems(this.runId, {
|
|
2827
|
+
limit: MAX_ITEMS_DONE_PER_PAGE,
|
|
2828
|
+
...cursor ? { cursor } : {}
|
|
2829
|
+
});
|
|
2830
|
+
for (const id of page.ids) ids.add(id);
|
|
2831
|
+
cursor = page.next;
|
|
2832
|
+
} while (cursor);
|
|
2833
|
+
return ids;
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
this.log("warning", `n\xE3o foi poss\xEDvel consultar os itens j\xE1 conclu\xEDdos deste trabalho \u2014 todos ser\xE3o processados (${thrownMessage(error)})`);
|
|
2836
|
+
return void 0;
|
|
2677
2837
|
}
|
|
2678
|
-
return void 0;
|
|
2679
2838
|
}
|
|
2680
|
-
|
|
2681
|
-
this.
|
|
2682
|
-
|
|
2839
|
+
scheduleItemsFlush() {
|
|
2840
|
+
if (this.itemsTimer) return;
|
|
2841
|
+
this.itemsTimer = setTimeout(() => {
|
|
2842
|
+
this.itemsTimer = void 0;
|
|
2843
|
+
this.flushItems();
|
|
2844
|
+
}, this.flushIntervalMs);
|
|
2845
|
+
unrefTimer(this.itemsTimer);
|
|
2683
2846
|
}
|
|
2684
|
-
|
|
2685
|
-
this.
|
|
2686
|
-
|
|
2847
|
+
clearItemsTimer() {
|
|
2848
|
+
if (this.itemsTimer) {
|
|
2849
|
+
clearTimeout(this.itemsTimer);
|
|
2850
|
+
this.itemsTimer = void 0;
|
|
2851
|
+
}
|
|
2687
2852
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2853
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
2854
|
+
flushItems() {
|
|
2855
|
+
this.clearItemsTimer();
|
|
2856
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2857
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2858
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2859
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2860
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
async sendItems(batch) {
|
|
2864
|
+
if (this.itemsUnsupported) return;
|
|
2865
|
+
try {
|
|
2866
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
2867
|
+
this.itemsStorageFull = false;
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2870
|
+
this.itemsUnsupported = true;
|
|
2871
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
2875
|
+
if (!this.itemsQuotaExceeded) {
|
|
2876
|
+
this.itemsQuotaExceeded = true;
|
|
2877
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
2878
|
+
}
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (error instanceof TasksApiError && error.status === 402) {
|
|
2882
|
+
if (!this.itemsStorageFull) {
|
|
2883
|
+
this.itemsStorageFull = true;
|
|
2884
|
+
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2885
|
+
}
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (error instanceof TasksApiError) {
|
|
2889
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
2890
|
+
this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
this.itemsBuffer.unshift(...batch);
|
|
2894
|
+
this.scheduleItemsFlush();
|
|
2895
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
2896
|
+
}
|
|
2691
2897
|
}
|
|
2692
2898
|
/**
|
|
2693
|
-
*
|
|
2694
|
-
*
|
|
2695
|
-
*
|
|
2696
|
-
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2697
|
-
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2698
|
-
* Devolve quantas steps foram enviadas.
|
|
2899
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
2900
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
2901
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2699
2902
|
*/
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2903
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2904
|
+
this.flushItems();
|
|
2905
|
+
await Promise.race([
|
|
2906
|
+
this.itemsChain,
|
|
2907
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2908
|
+
]);
|
|
2909
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2910
|
+
this.flushItems();
|
|
2911
|
+
await Promise.race([
|
|
2912
|
+
this.itemsChain,
|
|
2913
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2914
|
+
]);
|
|
2705
2915
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2916
|
+
}
|
|
2917
|
+
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2918
|
+
/**
|
|
2919
|
+
* Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
|
|
2920
|
+
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2921
|
+
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2922
|
+
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2923
|
+
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2924
|
+
* também cai aqui via `log()`.
|
|
2925
|
+
*/
|
|
2926
|
+
queueAttachment(imageIn, ts) {
|
|
2927
|
+
if (this.dead) return void 0;
|
|
2928
|
+
const image3 = toImageAttachment(imageIn);
|
|
2929
|
+
if (this.attachmentsUnsupported) return void 0;
|
|
2930
|
+
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
2931
|
+
if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
|
|
2932
|
+
this.attachmentCount++;
|
|
2933
|
+
this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
2715
2934
|
}
|
|
2935
|
+
return void 0;
|
|
2716
2936
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2937
|
+
this.attachmentCount++;
|
|
2938
|
+
const id = uuid4();
|
|
2939
|
+
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
2940
|
+
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
2941
|
+
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
2942
|
+
this.pendingAttachments.add(work);
|
|
2943
|
+
void work.finally(() => this.pendingAttachments.delete(work));
|
|
2944
|
+
return id;
|
|
2945
|
+
}
|
|
2946
|
+
/** Retorna o motivo da falha, ou undefined em sucesso. */
|
|
2947
|
+
async uploadAttachment(id, image3, meta) {
|
|
2948
|
+
let bytes;
|
|
2949
|
+
try {
|
|
2950
|
+
bytes = await toBytes(image3);
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
|
|
2953
|
+
return "read-failed";
|
|
2954
|
+
}
|
|
2955
|
+
const contentType = sniffContentType(bytes) ?? meta.contentType;
|
|
2956
|
+
if (!contentType) {
|
|
2957
|
+
this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
|
|
2958
|
+
return "unsupported-format";
|
|
2959
|
+
}
|
|
2960
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
2961
|
+
this.log(
|
|
2962
|
+
"warning",
|
|
2963
|
+
`imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
|
|
2964
|
+
);
|
|
2965
|
+
return "too-large";
|
|
2966
|
+
}
|
|
2967
|
+
const dimensions = readPngDimensions(bytes);
|
|
2968
|
+
const release = await this.acquireUploadSlot();
|
|
2969
|
+
try {
|
|
2970
|
+
if (this.abortController.signal.aborted || this.dead) return "canceled";
|
|
2971
|
+
const created = await this.api.createAttachment(
|
|
2972
|
+
this.runId,
|
|
2973
|
+
{
|
|
2974
|
+
id,
|
|
2975
|
+
content_type: contentType,
|
|
2976
|
+
size_bytes: bytes.byteLength,
|
|
2977
|
+
log_ts: meta.logTs,
|
|
2978
|
+
...meta.caption ? { caption: meta.caption } : {},
|
|
2979
|
+
...dimensions ?? {}
|
|
2980
|
+
},
|
|
2981
|
+
this.abortController.signal
|
|
2982
|
+
);
|
|
2983
|
+
await this.api.uploadAttachment(created.upload_url, created.headers, bytes, this.abortController.signal);
|
|
2984
|
+
await this.api.confirmAttachment(this.runId, id, { status: "uploaded", size_bytes: bytes.byteLength, ...dimensions ?? {} }).catch(() => {
|
|
2985
|
+
});
|
|
2986
|
+
return void 0;
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
if (this.isUnsupported(error)) {
|
|
2989
|
+
this.attachmentsUnsupported = true;
|
|
2990
|
+
this.log("warning", "anexos n\xE3o est\xE3o habilitados nesta instala\xE7\xE3o do Hobots \u2014 imagem n\xE3o enviada");
|
|
2991
|
+
return "unsupported";
|
|
2992
|
+
}
|
|
2993
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
2994
|
+
this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
|
|
2995
|
+
return "quota";
|
|
2725
2996
|
}
|
|
2997
|
+
if (this.abortController.signal.aborted) return "canceled";
|
|
2998
|
+
this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
|
|
2999
|
+
void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
|
|
3000
|
+
});
|
|
3001
|
+
return "upload-failed";
|
|
3002
|
+
} finally {
|
|
3003
|
+
release();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
/** 404 = API anterior ao recurso; 501/503 = instalação sem S3 configurado. */
|
|
3007
|
+
isUnsupported(error) {
|
|
3008
|
+
return error instanceof TasksApiError && (error.status === 404 || error.status === 501 || error.status === 503);
|
|
3009
|
+
}
|
|
3010
|
+
/** Semáforo simples: no máximo MAX_CONCURRENT_UPLOADS binários em voo por run. */
|
|
3011
|
+
acquireUploadSlot() {
|
|
3012
|
+
const release = () => {
|
|
3013
|
+
const next = this.uploadQueue.shift();
|
|
3014
|
+
if (next) next();
|
|
3015
|
+
else this.uploadSlots++;
|
|
3016
|
+
};
|
|
3017
|
+
if (this.uploadSlots > 0) {
|
|
3018
|
+
this.uploadSlots--;
|
|
3019
|
+
return Promise.resolve(release);
|
|
2726
3020
|
}
|
|
2727
|
-
return
|
|
3021
|
+
return new Promise((resolve2) => {
|
|
3022
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
3023
|
+
});
|
|
2728
3024
|
}
|
|
2729
|
-
/**
|
|
2730
|
-
|
|
2731
|
-
|
|
3025
|
+
/**
|
|
3026
|
+
* Espera os uploads pendentes antes do PATCH terminal, para a run não terminar
|
|
3027
|
+
* exibindo miniaturas em "enviando…". Capado: um upload lento não segura a run.
|
|
3028
|
+
*/
|
|
3029
|
+
async finalizeAttachments(timeoutMs = ATTACHMENT_DRAIN_MS) {
|
|
3030
|
+
if (this.pendingAttachments.size === 0) return;
|
|
3031
|
+
await Promise.race([
|
|
3032
|
+
Promise.allSettled([...this.pendingAttachments]),
|
|
3033
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
3034
|
+
]);
|
|
2732
3035
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3036
|
+
enqueue(updateIn) {
|
|
3037
|
+
if (this.dead) {
|
|
3038
|
+
this.salvageLogs(updateIn.logs);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
this.clearProgressTimer();
|
|
3042
|
+
const progress = this.takeProgress();
|
|
3043
|
+
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
3044
|
+
this.pendingUpdates++;
|
|
3045
|
+
this.chain = this.chain.then(async () => {
|
|
3046
|
+
if (this.dead) {
|
|
3047
|
+
this.salvageLogs(update.logs);
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
3051
|
+
try {
|
|
3052
|
+
await this.api.updateRun(this.runId, payload);
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
3055
|
+
this.dead = true;
|
|
3056
|
+
this.stopCancelWatch();
|
|
3057
|
+
this.abort();
|
|
3058
|
+
const pending = this.buffer.splice(0, this.buffer.length);
|
|
3059
|
+
this.salvageLogs(pending);
|
|
3060
|
+
this.logger.warn(
|
|
3061
|
+
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
3062
|
+
);
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
if (update.logs && update.logs.length > 0) {
|
|
3066
|
+
this.buffer.unshift(...update.logs);
|
|
3067
|
+
this.scheduleFlush();
|
|
3068
|
+
}
|
|
3069
|
+
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
3070
|
+
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
3071
|
+
}
|
|
3072
|
+
}).finally(() => {
|
|
3073
|
+
this.pendingUpdates--;
|
|
3074
|
+
});
|
|
2745
3075
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
this.
|
|
2755
|
-
|
|
2756
|
-
endTimestamp,
|
|
2757
|
-
this.status
|
|
2758
|
-
);
|
|
2759
|
-
if (!this.client) return;
|
|
2760
|
-
const steps = this.steps;
|
|
2761
|
-
let first = 0;
|
|
2762
|
-
while (steps.length - first > MAX_STEPS) {
|
|
2763
|
-
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
2764
|
-
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2765
|
-
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
2766
|
-
first += MAX_STEPS;
|
|
3076
|
+
/**
|
|
3077
|
+
* Best-effort de log tardio: envia logs para uma run já terminal, fora da fila.
|
|
3078
|
+
* O servidor anexa os logs mesmo respondendo 409; erros são engolidos — este é
|
|
3079
|
+
* o último recurso, não um canal confiável.
|
|
3080
|
+
*/
|
|
3081
|
+
salvageLogs(logs) {
|
|
3082
|
+
if (!logs || logs.length === 0) return;
|
|
3083
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
3084
|
+
void this.api.updateRunOnce(this.runId, { status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) }).catch(() => {
|
|
3085
|
+
});
|
|
2767
3086
|
}
|
|
2768
|
-
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
2769
3087
|
}
|
|
2770
3088
|
};
|
|
2771
3089
|
|
|
@@ -2774,7 +3092,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
|
2774
3092
|
async function executeRun(input) {
|
|
2775
3093
|
const { run, handler, api, client, logger, tracker } = input;
|
|
2776
3094
|
const options = handler.options;
|
|
2777
|
-
const tx = new Transaction(client, "run", "hobots.run", run.id);
|
|
3095
|
+
const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
|
|
2778
3096
|
const reporter = new RunReporter(
|
|
2779
3097
|
api,
|
|
2780
3098
|
run.id,
|
|
@@ -3014,42 +3332,26 @@ var TaskAgent = class {
|
|
|
3014
3332
|
}
|
|
3015
3333
|
};
|
|
3016
3334
|
|
|
3017
|
-
// src/tasks/
|
|
3018
|
-
var
|
|
3019
|
-
|
|
3020
|
-
if (!(error instanceof TasksApiError)) return true;
|
|
3021
|
-
return error.status >= 500 || error.status === 429;
|
|
3022
|
-
}
|
|
3023
|
-
function describeRejection(runId, error) {
|
|
3024
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
3025
|
-
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3026
|
-
}
|
|
3027
|
-
if (error instanceof TasksApiError && error.status === 409) {
|
|
3028
|
-
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3029
|
-
}
|
|
3030
|
-
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3031
|
-
}
|
|
3032
|
-
var AttachedRunner = class {
|
|
3033
|
-
constructor(runId, handler, deps) {
|
|
3034
|
-
this.runId = runId;
|
|
3335
|
+
// src/tasks/single-run.ts
|
|
3336
|
+
var SingleRunRunner = class {
|
|
3337
|
+
constructor(handler, deps) {
|
|
3035
3338
|
this.handler = handler;
|
|
3036
3339
|
this.deps = deps;
|
|
3037
3340
|
this.done = new Promise((resolve2) => {
|
|
3038
3341
|
this.resolveDone = resolve2;
|
|
3039
3342
|
});
|
|
3040
3343
|
}
|
|
3041
|
-
runId;
|
|
3042
3344
|
handler;
|
|
3043
3345
|
deps;
|
|
3044
3346
|
abort = new AbortController();
|
|
3045
|
-
/** Resolve quando a run terminou (ou não pôde
|
|
3347
|
+
/** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
|
|
3046
3348
|
done;
|
|
3047
3349
|
resolveDone;
|
|
3048
3350
|
async run() {
|
|
3049
3351
|
try {
|
|
3050
|
-
const run = await this.
|
|
3352
|
+
const run = await this.openRun();
|
|
3051
3353
|
if (!run) return;
|
|
3052
|
-
this.deps.logger.debug(
|
|
3354
|
+
this.deps.logger.debug(this.opened(run));
|
|
3053
3355
|
await executeRun({
|
|
3054
3356
|
run,
|
|
3055
3357
|
handler: this.handler,
|
|
@@ -3064,11 +3366,37 @@ var AttachedRunner = class {
|
|
|
3064
3366
|
this.resolveDone();
|
|
3065
3367
|
}
|
|
3066
3368
|
}
|
|
3067
|
-
/** Desiste de
|
|
3369
|
+
/** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
|
|
3068
3370
|
stop() {
|
|
3069
3371
|
this.abort.abort();
|
|
3070
3372
|
}
|
|
3071
|
-
|
|
3373
|
+
};
|
|
3374
|
+
|
|
3375
|
+
// src/tasks/attached.ts
|
|
3376
|
+
var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
3377
|
+
function isTransient(error) {
|
|
3378
|
+
if (!(error instanceof TasksApiError)) return true;
|
|
3379
|
+
return error.status >= 500 || error.status === 429;
|
|
3380
|
+
}
|
|
3381
|
+
function describeRejection(runId, error) {
|
|
3382
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
3383
|
+
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3384
|
+
}
|
|
3385
|
+
if (error instanceof TasksApiError && error.status === 409) {
|
|
3386
|
+
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3387
|
+
}
|
|
3388
|
+
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3389
|
+
}
|
|
3390
|
+
var AttachedRunner = class extends SingleRunRunner {
|
|
3391
|
+
constructor(runId, handler, deps) {
|
|
3392
|
+
super(handler, deps);
|
|
3393
|
+
this.runId = runId;
|
|
3394
|
+
}
|
|
3395
|
+
runId;
|
|
3396
|
+
opened(run) {
|
|
3397
|
+
return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
|
|
3398
|
+
}
|
|
3399
|
+
async openRun() {
|
|
3072
3400
|
for (let attempt = 0; ; attempt++) {
|
|
3073
3401
|
if (this.abort.signal.aborted) return void 0;
|
|
3074
3402
|
try {
|
|
@@ -3131,46 +3459,16 @@ function normalizeItemColumns(input, logger) {
|
|
|
3131
3459
|
}
|
|
3132
3460
|
|
|
3133
3461
|
// src/tasks/local.ts
|
|
3134
|
-
var LocalRunner = class {
|
|
3462
|
+
var LocalRunner = class extends SingleRunRunner {
|
|
3463
|
+
backoff;
|
|
3135
3464
|
constructor(handler, deps) {
|
|
3136
|
-
|
|
3137
|
-
this.deps = deps;
|
|
3465
|
+
super(handler, deps);
|
|
3138
3466
|
this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
|
|
3139
|
-
this.done = new Promise((resolve2) => {
|
|
3140
|
-
this.resolveDone = resolve2;
|
|
3141
|
-
});
|
|
3142
|
-
}
|
|
3143
|
-
handler;
|
|
3144
|
-
deps;
|
|
3145
|
-
abort = new AbortController();
|
|
3146
|
-
backoff;
|
|
3147
|
-
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
3148
|
-
done;
|
|
3149
|
-
resolveDone;
|
|
3150
|
-
async run() {
|
|
3151
|
-
try {
|
|
3152
|
-
const run = await this.createRun();
|
|
3153
|
-
if (!run) return;
|
|
3154
|
-
this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
|
|
3155
|
-
await executeRun({
|
|
3156
|
-
run,
|
|
3157
|
-
handler: this.handler,
|
|
3158
|
-
api: this.deps.api,
|
|
3159
|
-
client: this.deps.client,
|
|
3160
|
-
logger: this.deps.logger,
|
|
3161
|
-
tracker: this.deps.tracker
|
|
3162
|
-
});
|
|
3163
|
-
} catch (error) {
|
|
3164
|
-
this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
|
|
3165
|
-
} finally {
|
|
3166
|
-
this.resolveDone();
|
|
3167
|
-
}
|
|
3168
3467
|
}
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
this.abort.abort();
|
|
3468
|
+
opened(run) {
|
|
3469
|
+
return `run ${run.id} aberta (modo start)`;
|
|
3172
3470
|
}
|
|
3173
|
-
async
|
|
3471
|
+
async openRun() {
|
|
3174
3472
|
for (; ; ) {
|
|
3175
3473
|
if (this.abort.signal.aborted) return void 0;
|
|
3176
3474
|
try {
|
|
@@ -3183,6 +3481,133 @@ var LocalRunner = class {
|
|
|
3183
3481
|
}
|
|
3184
3482
|
};
|
|
3185
3483
|
|
|
3484
|
+
// src/tasks/local-transport.ts
|
|
3485
|
+
var LOCAL_RUN_ID = "local";
|
|
3486
|
+
var ITEM_LINE_PREFIX = "[hobots:item]";
|
|
3487
|
+
var SUMMARY_LINE_PREFIX = "[hobots:summary]";
|
|
3488
|
+
var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
|
|
3489
|
+
function unavailable(name) {
|
|
3490
|
+
return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
|
|
3491
|
+
}
|
|
3492
|
+
function cell(value) {
|
|
3493
|
+
return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
|
|
3494
|
+
}
|
|
3495
|
+
var LocalRunTransport = class {
|
|
3496
|
+
constructor(params = {}, out = (line) => void process.stdout.write(`${line}
|
|
3497
|
+
`)) {
|
|
3498
|
+
this.params = params;
|
|
3499
|
+
this.out = out;
|
|
3500
|
+
}
|
|
3501
|
+
params;
|
|
3502
|
+
out;
|
|
3503
|
+
counts = { succeeded: 0, occurrence: 0, failed: 0 };
|
|
3504
|
+
warnedAttachments = false;
|
|
3505
|
+
/** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
|
|
3506
|
+
terminalStatus;
|
|
3507
|
+
async updateRun(_runId, update) {
|
|
3508
|
+
if (update.status === "running") return;
|
|
3509
|
+
this.terminalStatus = update.status;
|
|
3510
|
+
if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
|
|
3511
|
+
printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
|
|
3512
|
+
if (update.error.stack) console.error(update.error.stack);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
async updateRunOnce() {
|
|
3516
|
+
}
|
|
3517
|
+
async createItems(_runId, request) {
|
|
3518
|
+
for (const item of request.items) {
|
|
3519
|
+
this.counts[item.status]++;
|
|
3520
|
+
this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
|
|
3521
|
+
}
|
|
3522
|
+
return { accepted: request.items.length, duplicates: 0 };
|
|
3523
|
+
}
|
|
3524
|
+
// modo local não tem trabalho anterior: nada a pular
|
|
3525
|
+
async listDoneItems() {
|
|
3526
|
+
return { ids: [], next: null };
|
|
3527
|
+
}
|
|
3528
|
+
/** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
|
|
3529
|
+
printSummary() {
|
|
3530
|
+
const { succeeded, occurrence, failed } = this.counts;
|
|
3531
|
+
this.out(
|
|
3532
|
+
`${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
async createAttachment(_runId, request) {
|
|
3536
|
+
if (!this.warnedAttachments) {
|
|
3537
|
+
this.warnedAttachments = true;
|
|
3538
|
+
printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
|
|
3539
|
+
}
|
|
3540
|
+
return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
|
|
3541
|
+
}
|
|
3542
|
+
async uploadAttachment() {
|
|
3543
|
+
}
|
|
3544
|
+
async confirmAttachment() {
|
|
3545
|
+
}
|
|
3546
|
+
/** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
|
|
3547
|
+
fileName(fileId) {
|
|
3548
|
+
return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
|
|
3549
|
+
}
|
|
3550
|
+
async requestFileDownload(_runId, fileId) {
|
|
3551
|
+
throw unavailable(this.fileName(fileId));
|
|
3552
|
+
}
|
|
3553
|
+
async fetchFile(url) {
|
|
3554
|
+
throw unavailable(url);
|
|
3555
|
+
}
|
|
3556
|
+
async confirmFileDownload(_runId, fileId) {
|
|
3557
|
+
throw unavailable(this.fileName(fileId));
|
|
3558
|
+
}
|
|
3559
|
+
};
|
|
3560
|
+
|
|
3561
|
+
// src/tasks/local-run.ts
|
|
3562
|
+
var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
|
|
3563
|
+
var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
|
|
3564
|
+
var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
|
|
3565
|
+
var LocalRunRunner = class extends SingleRunRunner {
|
|
3566
|
+
constructor(params, handler, deps) {
|
|
3567
|
+
super(handler, deps);
|
|
3568
|
+
this.params = params;
|
|
3569
|
+
}
|
|
3570
|
+
params;
|
|
3571
|
+
canceled = false;
|
|
3572
|
+
startedAt = Date.now();
|
|
3573
|
+
finished = false;
|
|
3574
|
+
run() {
|
|
3575
|
+
this.startedAt = Date.now();
|
|
3576
|
+
return super.run();
|
|
3577
|
+
}
|
|
3578
|
+
opened() {
|
|
3579
|
+
return "run local iniciada (HOBOTS_LOCAL_RUN)";
|
|
3580
|
+
}
|
|
3581
|
+
async openRun() {
|
|
3582
|
+
if (this.abort.signal.aborted) return void 0;
|
|
3583
|
+
return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
|
|
3584
|
+
}
|
|
3585
|
+
/** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
|
|
3586
|
+
cancel() {
|
|
3587
|
+
this.canceled = true;
|
|
3588
|
+
this.deps.tracker.singleActive()?.abort();
|
|
3589
|
+
}
|
|
3590
|
+
/** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
|
|
3591
|
+
get outcome() {
|
|
3592
|
+
if (this.canceled) return "canceled";
|
|
3593
|
+
return this.deps.api.terminalStatus ?? "failed";
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
|
|
3597
|
+
* desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
|
|
3598
|
+
* esgota com o handler ainda rodando.
|
|
3599
|
+
*/
|
|
3600
|
+
finish() {
|
|
3601
|
+
const outcome = this.outcome;
|
|
3602
|
+
if (this.finished) return outcome;
|
|
3603
|
+
this.finished = true;
|
|
3604
|
+
this.deps.api.printSummary();
|
|
3605
|
+
const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
|
|
3606
|
+
printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
|
|
3607
|
+
return outcome;
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
|
|
3186
3611
|
// src/tasks/registry.ts
|
|
3187
3612
|
var RunTracker = class {
|
|
3188
3613
|
constructor(api) {
|
|
@@ -3298,26 +3723,10 @@ function hostname() {
|
|
|
3298
3723
|
if (!isNode()) return void 0;
|
|
3299
3724
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
3300
3725
|
}
|
|
3301
|
-
function
|
|
3302
|
-
|
|
3303
|
-
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3304
|
-
const logger = makeLogger(options.debug ?? false);
|
|
3305
|
-
const environment = resolveEnvironmentOption(options.environment);
|
|
3306
|
-
const client = new Client(
|
|
3307
|
-
{
|
|
3308
|
-
environment,
|
|
3309
|
-
release: options.release,
|
|
3310
|
-
tags: options.tags,
|
|
3311
|
-
debug: options.debug,
|
|
3312
|
-
captureRunErrors: options.captureUnhandled !== false
|
|
3313
|
-
},
|
|
3314
|
-
config
|
|
3315
|
-
);
|
|
3316
|
-
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3317
|
-
const rt = {
|
|
3726
|
+
function baseRuntime(options, handler, logger, api) {
|
|
3727
|
+
return {
|
|
3318
3728
|
options: { ...options, clientSecret: void 0 },
|
|
3319
|
-
|
|
3320
|
-
client,
|
|
3729
|
+
client: void 0,
|
|
3321
3730
|
logger,
|
|
3322
3731
|
api,
|
|
3323
3732
|
handler,
|
|
@@ -3327,19 +3736,54 @@ function createRuntime(options, handler) {
|
|
|
3327
3736
|
agent: void 0,
|
|
3328
3737
|
local: void 0,
|
|
3329
3738
|
attached: void 0,
|
|
3739
|
+
localRun: void 0,
|
|
3330
3740
|
runId: void 0,
|
|
3331
3741
|
instanceId: resolveInstanceId(options.instanceId),
|
|
3332
|
-
environment,
|
|
3742
|
+
environment: void 0,
|
|
3333
3743
|
pollIntervalMs: void 0,
|
|
3334
3744
|
booted: false,
|
|
3335
3745
|
bootScheduled: false,
|
|
3336
3746
|
bootPromise: void 0,
|
|
3337
|
-
uninstallGlobalHandlers:
|
|
3747
|
+
uninstallGlobalHandlers: void 0,
|
|
3338
3748
|
uninstallShutdown: void 0
|
|
3339
3749
|
};
|
|
3750
|
+
}
|
|
3751
|
+
function createRuntime(options, handler, logger) {
|
|
3752
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3753
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
3754
|
+
const client = new Client(
|
|
3755
|
+
{
|
|
3756
|
+
environment,
|
|
3757
|
+
release: options.release,
|
|
3758
|
+
tags: options.tags,
|
|
3759
|
+
debug: options.debug,
|
|
3760
|
+
captureRunErrors: options.captureUnhandled !== false
|
|
3761
|
+
},
|
|
3762
|
+
config
|
|
3763
|
+
);
|
|
3764
|
+
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3765
|
+
const rt = baseRuntime(options, handler, logger, api);
|
|
3766
|
+
rt.client = client;
|
|
3767
|
+
rt.environment = environment;
|
|
3768
|
+
rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
|
|
3340
3769
|
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
3341
3770
|
return rt;
|
|
3342
3771
|
}
|
|
3772
|
+
function createLocalRunRuntime(options, handler, logger) {
|
|
3773
|
+
let params;
|
|
3774
|
+
try {
|
|
3775
|
+
params = resolveLocalRunParams();
|
|
3776
|
+
} catch (error) {
|
|
3777
|
+
printRunLog("error", error instanceof Error ? error.message : String(error));
|
|
3778
|
+
process.exit(2);
|
|
3779
|
+
throw error;
|
|
3780
|
+
}
|
|
3781
|
+
const transport = new LocalRunTransport(params);
|
|
3782
|
+
const rt = baseRuntime(options, handler, logger, transport);
|
|
3783
|
+
rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
|
|
3784
|
+
printRunLog("info", LOCAL_RUN_BANNER);
|
|
3785
|
+
return rt;
|
|
3786
|
+
}
|
|
3343
3787
|
function validateHandler(handler, fn) {
|
|
3344
3788
|
if (typeof handler !== "function") {
|
|
3345
3789
|
throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
|
|
@@ -3382,9 +3826,12 @@ function instanceInfo(rt) {
|
|
|
3382
3826
|
machine: rt.sampler?.sample()
|
|
3383
3827
|
};
|
|
3384
3828
|
}
|
|
3829
|
+
function platformApi(rt) {
|
|
3830
|
+
return rt.api;
|
|
3831
|
+
}
|
|
3385
3832
|
function ensureAgent(rt) {
|
|
3386
3833
|
rt.agent ??= new TaskAgent({
|
|
3387
|
-
api: rt
|
|
3834
|
+
api: platformApi(rt),
|
|
3388
3835
|
client: rt.client,
|
|
3389
3836
|
logger: rt.logger,
|
|
3390
3837
|
handler: rt.handler,
|
|
@@ -3396,7 +3843,7 @@ function ensureAgent(rt) {
|
|
|
3396
3843
|
}
|
|
3397
3844
|
function launchLocal(rt) {
|
|
3398
3845
|
const runner = new LocalRunner(rt.handler, {
|
|
3399
|
-
api: rt
|
|
3846
|
+
api: platformApi(rt),
|
|
3400
3847
|
client: rt.client,
|
|
3401
3848
|
logger: rt.logger,
|
|
3402
3849
|
tracker: rt.tracker,
|
|
@@ -3407,7 +3854,7 @@ function launchLocal(rt) {
|
|
|
3407
3854
|
}
|
|
3408
3855
|
function launchAttached(rt, runId) {
|
|
3409
3856
|
const runner = new AttachedRunner(runId, rt.handler, {
|
|
3410
|
-
api: rt
|
|
3857
|
+
api: platformApi(rt),
|
|
3411
3858
|
client: rt.client,
|
|
3412
3859
|
logger: rt.logger,
|
|
3413
3860
|
tracker: rt.tracker,
|
|
@@ -3419,7 +3866,43 @@ function launchAttached(rt, runId) {
|
|
|
3419
3866
|
if (runtime === rt) void close();
|
|
3420
3867
|
});
|
|
3421
3868
|
}
|
|
3869
|
+
function launchLocalRun(rt, runner) {
|
|
3870
|
+
let finished = false;
|
|
3871
|
+
const finish = () => {
|
|
3872
|
+
if (finished) return;
|
|
3873
|
+
finished = true;
|
|
3874
|
+
rt.uninstallShutdown?.();
|
|
3875
|
+
rt.uninstallShutdown = void 0;
|
|
3876
|
+
const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
|
|
3877
|
+
if (runtime === rt) void close(0);
|
|
3878
|
+
process.stdout.write("", () => process.exit(code));
|
|
3879
|
+
};
|
|
3880
|
+
if (rt.options.installExitHandlers !== false) {
|
|
3881
|
+
let signaled = false;
|
|
3882
|
+
const onSignal = (signal) => {
|
|
3883
|
+
if (externalListenerCount(signal) > 0 || signaled) return;
|
|
3884
|
+
signaled = true;
|
|
3885
|
+
runner.cancel();
|
|
3886
|
+
void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
|
|
3887
|
+
};
|
|
3888
|
+
const onInt = markInternal(() => onSignal("SIGINT"));
|
|
3889
|
+
const onTerm = markInternal(() => onSignal("SIGTERM"));
|
|
3890
|
+
process.on("SIGINT", onInt);
|
|
3891
|
+
process.on("SIGTERM", onTerm);
|
|
3892
|
+
rt.uninstallShutdown = () => {
|
|
3893
|
+
process.off("SIGINT", onInt);
|
|
3894
|
+
process.off("SIGTERM", onTerm);
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
void runner.run();
|
|
3898
|
+
void runner.done.then(finish);
|
|
3899
|
+
}
|
|
3422
3900
|
async function boot(rt) {
|
|
3901
|
+
if (rt.localRun) {
|
|
3902
|
+
rt.booted = true;
|
|
3903
|
+
launchLocalRun(rt, rt.localRun);
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3423
3906
|
if (!rt.instanceId) {
|
|
3424
3907
|
const id = await fallbackInstanceId();
|
|
3425
3908
|
if (runtime !== rt) return;
|
|
@@ -3456,9 +3939,12 @@ function startAgent(fn, mode, handler, options) {
|
|
|
3456
3939
|
}
|
|
3457
3940
|
const logger = makeLogger(options.debug ?? false);
|
|
3458
3941
|
const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
if (mode === "remote")
|
|
3942
|
+
loadDotenv();
|
|
3943
|
+
const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
|
|
3944
|
+
if (!rt.localRun && mode === "remote") {
|
|
3945
|
+
if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
|
|
3946
|
+
rt.runId = resolveRunId(options.runId);
|
|
3947
|
+
}
|
|
3462
3948
|
runtime = rt;
|
|
3463
3949
|
scheduleBoot(rt);
|
|
3464
3950
|
}
|
|
@@ -3481,10 +3967,10 @@ function forwardAttachment(rt, attachment, message, type) {
|
|
|
3481
3967
|
const reporter = currentReporter();
|
|
3482
3968
|
if (!reporter) {
|
|
3483
3969
|
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
|
|
3484
|
-
return;
|
|
3970
|
+
return void 0;
|
|
3485
3971
|
}
|
|
3486
3972
|
const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
|
|
3487
|
-
reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3973
|
+
return reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3488
3974
|
}
|
|
3489
3975
|
function captureException(error, hint) {
|
|
3490
3976
|
const rt = runtime;
|
|
@@ -3495,13 +3981,19 @@ function captureException(error, hint) {
|
|
|
3495
3981
|
}
|
|
3496
3982
|
return void 0;
|
|
3497
3983
|
}
|
|
3984
|
+
let attachmentId;
|
|
3498
3985
|
if (hint?.attachment) {
|
|
3499
3986
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
3500
|
-
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3987
|
+
attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3988
|
+
}
|
|
3989
|
+
if (!rt.client) {
|
|
3990
|
+
rt.logger.debug("captureException ignorado no modo local (sem Central)");
|
|
3991
|
+
return void 0;
|
|
3501
3992
|
}
|
|
3502
3993
|
const runId = currentReporter()?.runId;
|
|
3503
3994
|
const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
|
|
3504
|
-
|
|
3995
|
+
const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
|
|
3996
|
+
return rt.client.captureException(error, withRun, contexts);
|
|
3505
3997
|
}
|
|
3506
3998
|
async function close(timeoutMs = 3e4) {
|
|
3507
3999
|
const rt = runtime;
|
|
@@ -3511,6 +4003,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3511
4003
|
if (rt.agent) await rt.agent.stop(0);
|
|
3512
4004
|
rt.local?.stop();
|
|
3513
4005
|
rt.attached?.stop();
|
|
4006
|
+
rt.localRun?.stop();
|
|
3514
4007
|
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
3515
4008
|
if (!idle) {
|
|
3516
4009
|
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
@@ -3519,7 +4012,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3519
4012
|
rt.heartbeat?.stop();
|
|
3520
4013
|
rt.uninstallShutdown?.();
|
|
3521
4014
|
rt.uninstallGlobalHandlers?.();
|
|
3522
|
-
const flushed = await rt.client.close(timeoutMs);
|
|
4015
|
+
const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
|
|
3523
4016
|
return idle && flushed;
|
|
3524
4017
|
}
|
|
3525
4018
|
|
|
@@ -3548,11 +4041,19 @@ var noop = () => void 0;
|
|
|
3548
4041
|
var fallbackEachSink = {
|
|
3549
4042
|
report: noop,
|
|
3550
4043
|
startStep: () => void 0,
|
|
4044
|
+
stepScope: (_step, fn) => fn(),
|
|
3551
4045
|
declareTotal: noop,
|
|
3552
4046
|
advance: noop,
|
|
3553
4047
|
checkCanceled: noop,
|
|
3554
4048
|
isAborted: () => false,
|
|
3555
|
-
|
|
4049
|
+
/* v8 ignore start -- fora de uma run não há consulta de concluídos, logo nunca há item pulado para anunciar */
|
|
4050
|
+
logInfo: (message) => printRunLog("info", message),
|
|
4051
|
+
/* v8 ignore stop */
|
|
4052
|
+
logWarning: (message) => printRunLog("warning", message),
|
|
4053
|
+
logError: (message) => printRunLog("error", message),
|
|
4054
|
+
sleep,
|
|
4055
|
+
doneItemIds: () => Promise.resolve(void 0),
|
|
4056
|
+
flush: () => Promise.resolve()
|
|
3556
4057
|
};
|
|
3557
4058
|
var fallbackItems = {
|
|
3558
4059
|
add: noop,
|
|
@@ -3571,7 +4072,6 @@ var fallbackFiles = {
|
|
|
3571
4072
|
read: outsideRunFiles
|
|
3572
4073
|
};
|
|
3573
4074
|
var neverAborts = new AbortController().signal;
|
|
3574
|
-
var detachedTransaction;
|
|
3575
4075
|
function resolve(member) {
|
|
3576
4076
|
const reporter = currentReporter();
|
|
3577
4077
|
if (reporter) return reporter.context;
|
|
@@ -3592,8 +4092,12 @@ var hobots = Object.defineProperties({}, {
|
|
|
3592
4092
|
items: getter("items", () => fallbackItems),
|
|
3593
4093
|
progress: getter("progress", () => fallbackProgress),
|
|
3594
4094
|
files: getter("files", () => fallbackFiles),
|
|
3595
|
-
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
3596
4095
|
signal: getter("signal", () => neverAborts),
|
|
4096
|
+
step: method((name, fn, options) => {
|
|
4097
|
+
const ctx = resolve("step");
|
|
4098
|
+
if (ctx) return ctx.step(name, fn, options);
|
|
4099
|
+
return new Transaction(void 0, "hobots.detached").step(name, fn, options);
|
|
4100
|
+
}),
|
|
3597
4101
|
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
3598
4102
|
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
3599
4103
|
flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
|
|
@@ -3631,6 +4135,7 @@ var isFileRef2 = hobots.isFileRef;
|
|
|
3631
4135
|
loadDotenv();
|
|
3632
4136
|
var src_default = hobots;
|
|
3633
4137
|
export {
|
|
4138
|
+
Occurrence,
|
|
3634
4139
|
captureException2 as captureException,
|
|
3635
4140
|
close2 as close,
|
|
3636
4141
|
src_default as default,
|