hobots 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{chunk-HKCDMUA3.js → chunk-6YTOYIZM.js} +2 -1
- package/dist/chunk-6YTOYIZM.js.map +1 -0
- package/dist/index.cjs +1655 -1466
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -8
- package/dist/index.d.ts +62 -8
- package/dist/index.js +1655 -1467
- 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 +20 -1
- package/dist/protocol.d.ts +20 -1
- package/dist/protocol.js +1 -1
- package/package.json +1 -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 ? "5.
|
|
7
|
+
var SDK_VERSION = true ? "5.2.0" : "0.0.0-dev";
|
|
8
8
|
function isNode() {
|
|
9
9
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
10
10
|
}
|
|
@@ -1099,6 +1099,36 @@ var TasksApi = class {
|
|
|
1099
1099
|
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
1100
1100
|
};
|
|
1101
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
|
+
}
|
|
1102
1132
|
/**
|
|
1103
1133
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
1104
1134
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1285,6 +1315,20 @@ var Backoff = class {
|
|
|
1285
1315
|
};
|
|
1286
1316
|
|
|
1287
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
|
+
}
|
|
1288
1332
|
function runCanceledError() {
|
|
1289
1333
|
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1290
1334
|
}
|
|
@@ -1301,1617 +1345,1750 @@ function isCancellation(error) {
|
|
|
1301
1345
|
return error instanceof Error && (error.name === "RunCanceled" || error.name === "AbortError");
|
|
1302
1346
|
}
|
|
1303
1347
|
|
|
1304
|
-
// src/tasks/
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
var
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
this.
|
|
1315
|
-
this.
|
|
1316
|
-
|
|
1317
|
-
index;
|
|
1318
|
-
id;
|
|
1319
|
-
step;
|
|
1320
|
-
basePayload;
|
|
1321
|
-
sink;
|
|
1322
|
-
reported = false;
|
|
1323
|
-
status;
|
|
1324
|
-
succeeded(payload, options) {
|
|
1325
|
-
this.record("succeeded", void 0, payload, options?.image);
|
|
1326
|
-
}
|
|
1327
|
-
failed(message, payload, options) {
|
|
1328
|
-
this.record("failed", message, payload, options?.image);
|
|
1329
|
-
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
1330
|
-
}
|
|
1331
|
-
occurrence(message, payload, options) {
|
|
1332
|
-
this.record("occurrence", message, payload, options?.image);
|
|
1333
|
-
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
1334
|
-
}
|
|
1335
|
-
record(status, message, payload, image3) {
|
|
1336
|
-
this.reported = true;
|
|
1337
|
-
this.status = status;
|
|
1338
|
-
const merged = { ...this.basePayload ?? {}, ...payload ?? {} };
|
|
1339
|
-
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;
|
|
1340
1361
|
}
|
|
1362
|
+
input;
|
|
1363
|
+
caption;
|
|
1364
|
+
contentType;
|
|
1365
|
+
[IMAGE_BRAND] = true;
|
|
1341
1366
|
};
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
if (raw === null || raw === void 0) return void 0;
|
|
1345
|
-
const text = safeString(raw);
|
|
1346
|
-
return text.length > 0 ? text : void 0;
|
|
1367
|
+
function image(input, options = {}) {
|
|
1368
|
+
return new ImageAttachment(input, options.caption, options.contentType);
|
|
1347
1369
|
}
|
|
1348
|
-
function
|
|
1349
|
-
|
|
1350
|
-
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;
|
|
1351
1372
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
sink.checkCanceled();
|
|
1359
|
-
const id = resolveId(options, element, index);
|
|
1360
|
-
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
1361
|
-
const step = sink.startStep(options.op ?? "item", description);
|
|
1362
|
-
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1363
|
-
try {
|
|
1364
|
-
await sink.stepScope(step, async () => handler(element, handle, index));
|
|
1365
|
-
if (!handle.reported) handle.succeeded();
|
|
1366
|
-
} catch (error) {
|
|
1367
|
-
if (isTerminalError(error) || sink.isAborted()) {
|
|
1368
|
-
step?._setStatus("cancelled")._close();
|
|
1369
|
-
throw error;
|
|
1370
|
-
}
|
|
1371
|
-
const message = thrownMessage(error);
|
|
1372
|
-
if (!handle.reported) handle.failed(message);
|
|
1373
|
-
step?._failed(error);
|
|
1374
|
-
sink.logError(`item ${description} falhou: ${message}`);
|
|
1375
|
-
}
|
|
1376
|
-
step?._close();
|
|
1377
|
-
if (options.progress !== false) sink.advance();
|
|
1378
|
-
result.total += 1;
|
|
1379
|
-
result[handle.status ?? "succeeded"] += 1;
|
|
1380
|
-
index += 1;
|
|
1381
|
-
if (options.flush !== false) await sink.flush();
|
|
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");
|
|
1382
1379
|
}
|
|
1383
|
-
|
|
1380
|
+
const { readFile } = await import("fs/promises");
|
|
1381
|
+
return new Uint8Array(await readFile(path));
|
|
1384
1382
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
1389
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1390
|
-
function isFileRef(value) {
|
|
1391
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1392
|
-
const ref = value;
|
|
1393
|
-
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);
|
|
1394
1386
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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";
|
|
1400
1396
|
return void 0;
|
|
1401
1397
|
}
|
|
1402
|
-
function
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
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 };
|
|
1409
1406
|
}
|
|
1410
|
-
function
|
|
1411
|
-
|
|
1412
|
-
|
|
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);
|
|
1413
1411
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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;
|
|
1417
1424
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1425
|
+
async function runWithRunContext(store, fn) {
|
|
1426
|
+
await ensureStorage();
|
|
1427
|
+
return storage ? storage.run(store, fn) : fn();
|
|
1420
1428
|
}
|
|
1421
|
-
function
|
|
1422
|
-
|
|
1423
|
-
const path = ref.path;
|
|
1424
|
-
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
1429
|
+
function currentRunReporter() {
|
|
1430
|
+
return storage?.getStore()?.reporter;
|
|
1425
1431
|
}
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1432
|
+
async function runWithStepScope(scope, fn) {
|
|
1433
|
+
await ensureStorage();
|
|
1434
|
+
return stepStorage ? stepStorage.run(scope, fn) : fn();
|
|
1428
1435
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
this.tempDir = void 0;
|
|
1448
|
-
if (!pending) return;
|
|
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();
|
|
1449
1454
|
try {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
return value;
|
|
1459
|
-
}
|
|
1460
|
-
single(refOrField) {
|
|
1461
|
-
if (typeof refOrField === "string") {
|
|
1462
|
-
const value = this.field(refOrField);
|
|
1463
|
-
if (Array.isArray(value)) {
|
|
1464
|
-
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1465
|
-
}
|
|
1466
|
-
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);
|
|
1467
1463
|
}
|
|
1468
|
-
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
1469
|
-
return refOrField;
|
|
1470
|
-
}
|
|
1471
|
-
async downloadAll(field, dest) {
|
|
1472
|
-
const value = this.field(field);
|
|
1473
|
-
const refs = Array.isArray(value) ? value : [value];
|
|
1474
|
-
const paths = [];
|
|
1475
|
-
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
1476
|
-
return paths;
|
|
1477
1464
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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";
|
|
1487
1499
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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";
|
|
1494
1511
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
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;
|
|
1497
1519
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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;
|
|
1506
1534
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const name = safeFileName(ref.name, "arquivo");
|
|
1511
|
-
if (dest === void 0) {
|
|
1512
|
-
return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
|
|
1513
|
-
}
|
|
1514
|
-
const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
|
|
1515
|
-
if (isDir) {
|
|
1516
|
-
await mkdir(dest, { recursive: true });
|
|
1517
|
-
return resolve2(dest, name);
|
|
1518
|
-
}
|
|
1519
|
-
await mkdir(dirname(resolve2(dest)), { recursive: true });
|
|
1520
|
-
return resolve2(dest);
|
|
1535
|
+
image(image3) {
|
|
1536
|
+
this.attachImage(image3);
|
|
1537
|
+
return this;
|
|
1521
1538
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
|
|
1537
|
-
const controller = new AbortController();
|
|
1538
|
-
let timedOut = false;
|
|
1539
|
-
let timer;
|
|
1540
|
-
const arm = () => {
|
|
1541
|
-
clearTimeout(timer);
|
|
1542
|
-
timer = setTimeout(() => {
|
|
1543
|
-
timedOut = true;
|
|
1544
|
-
controller.abort();
|
|
1545
|
-
}, idleTimeoutMs);
|
|
1546
|
-
};
|
|
1547
|
-
const onCancel = () => controller.abort();
|
|
1548
|
-
signal.addEventListener("abort", onCancel, { once: true });
|
|
1549
|
-
try {
|
|
1550
|
-
arm();
|
|
1551
|
-
const response = await api.fetchFile(url, controller.signal);
|
|
1552
|
-
const handle = await open(part, "w");
|
|
1553
|
-
let written = 0;
|
|
1554
|
-
try {
|
|
1555
|
-
const reader = response.body?.getReader();
|
|
1556
|
-
const stopReading = () => void reader?.cancel().catch(() => void 0);
|
|
1557
|
-
controller.signal.addEventListener("abort", stopReading, { once: true });
|
|
1558
|
-
for (; ; ) {
|
|
1559
|
-
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
1560
|
-
if (chunk.done) break;
|
|
1561
|
-
arm();
|
|
1562
|
-
written += chunk.value.byteLength;
|
|
1563
|
-
if (written > expected) {
|
|
1564
|
-
stopReading();
|
|
1565
|
-
break;
|
|
1566
|
-
}
|
|
1567
|
-
await handle.write(chunk.value);
|
|
1568
|
-
}
|
|
1569
|
-
} finally {
|
|
1570
|
-
await handle.close();
|
|
1571
|
-
}
|
|
1572
|
-
if (written !== expected) {
|
|
1573
|
-
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
1574
|
-
}
|
|
1575
|
-
await rename(part, target);
|
|
1576
|
-
} catch (error) {
|
|
1577
|
-
await rm(part, { force: true });
|
|
1578
|
-
if (signal.aborted) throw runCanceledError();
|
|
1579
|
-
if (timedOut) {
|
|
1580
|
-
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
1581
|
-
}
|
|
1582
|
-
throw this.translate(error, ref);
|
|
1583
|
-
} finally {
|
|
1584
|
-
clearTimeout(timer);
|
|
1585
|
-
signal.removeEventListener("abort", onCancel);
|
|
1586
|
-
}
|
|
1587
|
-
try {
|
|
1588
|
-
await api.confirmFileDownload(runId, ref.id);
|
|
1589
|
-
} catch (error) {
|
|
1590
|
-
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
1591
|
-
}
|
|
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;
|
|
1592
1553
|
}
|
|
1593
1554
|
/**
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
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.
|
|
1596
1557
|
*/
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
1615
|
-
} catch (error) {
|
|
1616
|
-
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
|
|
1617
|
-
throw error;
|
|
1618
|
-
}
|
|
1619
|
-
if (names.length === 0) throw unavailableLocal(ref);
|
|
1620
|
-
if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
|
|
1621
|
-
this.checkCanceled();
|
|
1622
|
-
const part = `${target}.part`;
|
|
1623
|
-
try {
|
|
1624
|
-
await copyFile(join(folder, names[0]), part);
|
|
1625
|
-
const { size } = await stat(part);
|
|
1626
|
-
if (size !== ref.size) {
|
|
1627
|
-
throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
|
|
1628
|
-
}
|
|
1629
|
-
await rename(part, target);
|
|
1630
|
-
} catch (error) {
|
|
1631
|
-
await rm(part, { force: true });
|
|
1632
|
-
throw error;
|
|
1633
|
-
}
|
|
1634
|
-
await rm(folder, { recursive: true, force: true });
|
|
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;
|
|
1635
1575
|
}
|
|
1636
1576
|
/**
|
|
1637
|
-
*
|
|
1638
|
-
*
|
|
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.
|
|
1639
1580
|
*/
|
|
1640
|
-
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
this.checkCanceled();
|
|
1647
|
-
const part = `${target}.part`;
|
|
1648
|
-
try {
|
|
1649
|
-
await copyFile(source, part);
|
|
1650
|
-
await rename(part, target);
|
|
1651
|
-
} catch (error) {
|
|
1652
|
-
await rm(part, { force: true });
|
|
1653
|
-
throw error;
|
|
1654
|
-
}
|
|
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]);
|
|
1655
1587
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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}`);
|
|
1660
1664
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
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]);
|
|
1669
|
+
}
|
|
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");
|
|
1663
1687
|
}
|
|
1664
|
-
return
|
|
1688
|
+
return void 0;
|
|
1665
1689
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
if (
|
|
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;
|
|
1681
1731
|
try {
|
|
1682
|
-
|
|
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
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
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)}`);
|
|
1753
|
+
}
|
|
1754
|
+
step._close();
|
|
1755
|
+
return result;
|
|
1683
1756
|
} catch (error) {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
options.onRetry?.(error, attempt, delay);
|
|
1689
|
-
await wait(delay);
|
|
1757
|
+
this.noteAttempts(step, attempts);
|
|
1758
|
+
if (isTerminalError(error)) step._setStatus("cancelled")._close();
|
|
1759
|
+
else step._failed(error);
|
|
1760
|
+
throw error;
|
|
1690
1761
|
}
|
|
1691
1762
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
// src/tasks/attachments.ts
|
|
1695
|
-
var MAX_ATTACHMENT_BYTES = WIRE_LIMITS.MAX_ATTACHMENT_BYTES;
|
|
1696
|
-
var MAX_ATTACHMENTS_PER_RUN = WIRE_LIMITS.MAX_ATTACHMENTS_PER_RUN;
|
|
1697
|
-
var MAX_ATTACHMENTS_PER_LOG_ENTRY = WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY;
|
|
1698
|
-
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1699
|
-
var MAX_CAPTION_CHARS = WIRE_LIMITS.MAX_CAPTION_CHARS;
|
|
1700
|
-
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1701
|
-
var IMAGE_BRAND = /* @__PURE__ */ Symbol.for("hobots.image");
|
|
1702
|
-
var ImageAttachment = class {
|
|
1703
|
-
constructor(input, caption, contentType) {
|
|
1704
|
-
this.input = input;
|
|
1705
|
-
this.caption = caption;
|
|
1706
|
-
this.contentType = contentType;
|
|
1763
|
+
noteAttempts(step, attempts) {
|
|
1764
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
1707
1765
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
};
|
|
1713
|
-
function image(input, options = {}) {
|
|
1714
|
-
return new ImageAttachment(input, options.caption, options.contentType);
|
|
1715
|
-
}
|
|
1716
|
-
function isImageAttachment(value) {
|
|
1717
|
-
return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
|
|
1718
|
-
}
|
|
1719
|
-
function toImageAttachment(value) {
|
|
1720
|
-
return isImageAttachment(value) ? value : new ImageAttachment(value);
|
|
1721
|
-
}
|
|
1722
|
-
async function readImageFile(path) {
|
|
1723
|
-
if (!isNode()) {
|
|
1724
|
-
throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
|
|
1725
|
-
}
|
|
1726
|
-
const { readFile } = await import("fs/promises");
|
|
1727
|
-
return new Uint8Array(await readFile(path));
|
|
1728
|
-
}
|
|
1729
|
-
function startsWith(bytes, signature, offset = 0) {
|
|
1730
|
-
if (bytes.length < offset + signature.length) return false;
|
|
1731
|
-
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1732
|
-
}
|
|
1733
|
-
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1734
|
-
var MAX_PNG_DIMENSION = WIRE_LIMITS.MAX_IMAGE_DIMENSION_PX;
|
|
1735
|
-
var JPEG_MAGIC = [255, 216, 255];
|
|
1736
|
-
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1737
|
-
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
1738
|
-
function sniffContentType(bytes) {
|
|
1739
|
-
if (startsWith(bytes, PNG_MAGIC)) return "image/png";
|
|
1740
|
-
if (startsWith(bytes, JPEG_MAGIC)) return "image/jpeg";
|
|
1741
|
-
if (startsWith(bytes, RIFF_MAGIC) && startsWith(bytes, WEBP_MAGIC, 8)) return "image/webp";
|
|
1742
|
-
return void 0;
|
|
1743
|
-
}
|
|
1744
|
-
function readPngDimensions(bytes) {
|
|
1745
|
-
if (!startsWith(bytes, PNG_MAGIC) || bytes.length < 24) return void 0;
|
|
1746
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1747
|
-
const width = view.getUint32(16);
|
|
1748
|
-
const height = view.getUint32(20);
|
|
1749
|
-
if (width === 0 || height === 0) return void 0;
|
|
1750
|
-
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1751
|
-
return { width, height };
|
|
1752
|
-
}
|
|
1753
|
-
async function toBytes(input) {
|
|
1754
|
-
if (typeof input === "string") return readImageFile(input);
|
|
1755
|
-
if (input instanceof Uint8Array) return input;
|
|
1756
|
-
return new Uint8Array(input);
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// src/tasks/context.ts
|
|
1760
|
-
var LOG_LEVEL_COLOR = {
|
|
1761
|
-
debug: "gray",
|
|
1762
|
-
info: "cyan",
|
|
1763
|
-
warning: "yellow",
|
|
1764
|
-
error: "red",
|
|
1765
|
-
success: "green"
|
|
1766
|
-
};
|
|
1767
|
-
function joinLogArgs(message, rest) {
|
|
1768
|
-
if (rest.length === 0) return safeString(message);
|
|
1769
|
-
return [message, ...rest].map(safeString).join(" ");
|
|
1770
|
-
}
|
|
1771
|
-
function splitLogArgs(message, rest) {
|
|
1772
|
-
const args = [message, ...rest];
|
|
1773
|
-
const last = args[args.length - 1];
|
|
1774
|
-
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
1775
|
-
const text = args.slice(0, -1);
|
|
1776
|
-
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
1777
|
-
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
1778
|
-
}
|
|
1779
|
-
function printRunLog(level, message) {
|
|
1780
|
-
const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
1781
|
-
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1782
|
-
console[method2](line, message);
|
|
1783
|
-
}
|
|
1784
|
-
var PROGRESS_LINE_PREFIX = "[hobots:progress]";
|
|
1785
|
-
var PROGRESS_PRINT_THROTTLE_MS = 250;
|
|
1786
|
-
function printRunProgress(current, total) {
|
|
1787
|
-
console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
|
|
1788
|
-
}
|
|
1789
|
-
function cancelableSleep(ms, signal) {
|
|
1790
|
-
return new Promise((resolve2, reject) => {
|
|
1791
|
-
if (signal.aborted) {
|
|
1792
|
-
reject(runCanceledError());
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
const onAbort = () => {
|
|
1796
|
-
clearTimeout(timer);
|
|
1797
|
-
reject(runCanceledError());
|
|
1798
|
-
};
|
|
1799
|
-
const timer = setTimeout(() => {
|
|
1800
|
-
signal.removeEventListener("abort", onAbort);
|
|
1801
|
-
resolve2();
|
|
1802
|
-
}, ms);
|
|
1803
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
1807
|
-
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
1808
|
-
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
1809
|
-
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
1810
|
-
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
1811
|
-
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
1812
|
-
var FLUSH_AT_BUFFERED = 20;
|
|
1813
|
-
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
1814
|
-
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
1815
|
-
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
1816
|
-
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1817
|
-
var ITEMS_DRAIN_MS = 15e3;
|
|
1818
|
-
var PROGRESS_FLUSH_MS = 1e3;
|
|
1819
|
-
function scrubPayloadValue(value, depth) {
|
|
1820
|
-
if (typeof value === "string") return scrubSecrets(value);
|
|
1821
|
-
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1822
|
-
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1823
|
-
const out = {};
|
|
1824
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1825
|
-
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1826
|
-
}
|
|
1827
|
-
return out;
|
|
1828
|
-
}
|
|
1829
|
-
var RunReporter = class {
|
|
1830
|
-
constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
1831
|
-
this.api = api;
|
|
1832
|
-
this.runId = runId;
|
|
1833
|
-
this.logger = logger;
|
|
1834
|
-
this.tx = tx;
|
|
1835
|
-
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
1836
|
-
this.cancelCheckIntervalMs = Math.min(
|
|
1837
|
-
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
1838
|
-
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
1839
|
-
);
|
|
1840
|
-
const logAt = (level) => (message, ...rest) => {
|
|
1841
|
-
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
1842
|
-
this.log(level, text, image3);
|
|
1843
|
-
};
|
|
1844
|
-
const log = {
|
|
1845
|
-
debug: logAt("debug"),
|
|
1846
|
-
info: logAt("info"),
|
|
1847
|
-
warn: logAt("warning"),
|
|
1848
|
-
error: logAt("error"),
|
|
1849
|
-
success: logAt("success")
|
|
1850
|
-
};
|
|
1851
|
-
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1852
|
-
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
1853
|
-
const checkCanceled = () => this.checkCanceled();
|
|
1854
|
-
tx.setStepGuard(checkCanceled);
|
|
1855
|
-
const eachSink = {
|
|
1856
|
-
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
1857
|
-
startStep: (op, description) => tx.start(op, description),
|
|
1858
|
-
stepScope: (step, fn) => tx.scope(step, fn),
|
|
1859
|
-
declareTotal: (total) => {
|
|
1860
|
-
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
1861
|
-
},
|
|
1862
|
-
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
1863
|
-
checkCanceled,
|
|
1864
|
-
isAborted: () => this.abortController.signal.aborted,
|
|
1865
|
-
logError: (message) => this.log("error", message),
|
|
1866
|
-
flush: () => this.flush()
|
|
1867
|
-
};
|
|
1868
|
-
const items = {
|
|
1869
|
-
add: (item) => {
|
|
1870
|
-
checkCanceled();
|
|
1871
|
-
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
1872
|
-
},
|
|
1873
|
-
succeeded: (payload, options) => {
|
|
1874
|
-
checkCanceled();
|
|
1875
|
-
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
1876
|
-
},
|
|
1877
|
-
failed: (message, payload, options) => {
|
|
1878
|
-
checkCanceled();
|
|
1879
|
-
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
1880
|
-
},
|
|
1881
|
-
occurrence: (message, payload, options) => {
|
|
1882
|
-
checkCanceled();
|
|
1883
|
-
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
1884
|
-
},
|
|
1885
|
-
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
1886
|
-
};
|
|
1887
|
-
const progress = Object.defineProperties(
|
|
1888
|
-
{
|
|
1889
|
-
total: (total) => {
|
|
1890
|
-
checkCanceled();
|
|
1891
|
-
this.setProgress(this.progressCurrent, total);
|
|
1892
|
-
},
|
|
1893
|
-
advance: (count = 1) => {
|
|
1894
|
-
checkCanceled();
|
|
1895
|
-
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
1896
|
-
},
|
|
1897
|
-
set: (current, total) => {
|
|
1898
|
-
checkCanceled();
|
|
1899
|
-
this.setProgress(current, total ?? this.progressTotal);
|
|
1900
|
-
}
|
|
1901
|
-
},
|
|
1902
|
-
{
|
|
1903
|
-
current: { enumerable: true, get: () => this.progressCurrent },
|
|
1904
|
-
snapshot: {
|
|
1905
|
-
enumerable: true,
|
|
1906
|
-
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1910
|
-
);
|
|
1911
|
-
const signal = this.abortController.signal;
|
|
1912
|
-
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
1913
|
-
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
1914
|
-
this.context = {
|
|
1915
|
-
runId,
|
|
1916
|
-
params,
|
|
1917
|
-
log,
|
|
1918
|
-
items,
|
|
1919
|
-
progress,
|
|
1920
|
-
files: this.fileStore.files,
|
|
1921
|
-
step: async (name, fn, options) => {
|
|
1922
|
-
const root = tx.currentStep() === void 0;
|
|
1923
|
-
try {
|
|
1924
|
-
return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
|
|
1925
|
-
} finally {
|
|
1926
|
-
if (options?.flush !== false && root && !signal.aborted) await this.flush();
|
|
1927
|
-
}
|
|
1928
|
-
},
|
|
1929
|
-
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1930
|
-
signal,
|
|
1931
|
-
throwIfCanceled: checkCanceled,
|
|
1932
|
-
sleep: sleep2,
|
|
1933
|
-
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1934
|
-
};
|
|
1935
|
-
}
|
|
1936
|
-
api;
|
|
1937
|
-
runId;
|
|
1938
|
-
logger;
|
|
1939
|
-
tx;
|
|
1940
|
-
context;
|
|
1941
|
-
buffer = [];
|
|
1942
|
-
dead = false;
|
|
1943
|
-
chain = Promise.resolve();
|
|
1944
|
-
pendingUpdates = 0;
|
|
1945
|
-
timer;
|
|
1946
|
-
watchTimer;
|
|
1947
|
-
abortController = new AbortController();
|
|
1948
|
-
flushIntervalMs;
|
|
1949
|
-
cancelCheckIntervalMs;
|
|
1950
|
-
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1951
|
-
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1952
|
-
// envio preservada e sem lotes sobrepostos.
|
|
1953
|
-
itemsBuffer = [];
|
|
1954
|
-
itemsChain = Promise.resolve();
|
|
1955
|
-
itemsTimer;
|
|
1956
|
-
itemSeq = 0;
|
|
1957
|
-
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1958
|
-
itemsUnsupported = false;
|
|
1959
|
-
/**
|
|
1960
|
-
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
1961
|
-
* degrada com um aviso só.
|
|
1962
|
-
*/
|
|
1963
|
-
itemsQuotaExceeded = false;
|
|
1964
|
-
/**
|
|
1965
|
-
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
1966
|
-
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
1967
|
-
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
1968
|
-
*/
|
|
1969
|
-
itemsStorageFull = false;
|
|
1970
|
-
/**
|
|
1971
|
-
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1972
|
-
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1973
|
-
*/
|
|
1974
|
-
itemsClosed = false;
|
|
1975
|
-
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1976
|
-
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1977
|
-
pendingAttachments = /* @__PURE__ */ new Set();
|
|
1978
|
-
attachmentCount = 0;
|
|
1979
|
-
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
1980
|
-
uploadQueue = [];
|
|
1981
|
-
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1982
|
-
attachmentsUnsupported = false;
|
|
1983
|
-
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
1984
|
-
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
1985
|
-
progressCurrent = 0;
|
|
1986
|
-
progressTotal = null;
|
|
1987
|
-
/** Snapshot ainda não entregue ao servidor. */
|
|
1988
|
-
progressDirty = false;
|
|
1989
|
-
progressTimer;
|
|
1990
|
-
progressWarned = false;
|
|
1991
|
-
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
1992
|
-
progressLastPrint = 0;
|
|
1993
|
-
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1994
|
-
fileStore;
|
|
1995
|
-
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
1996
|
-
cleanupFiles() {
|
|
1997
|
-
return this.fileStore.cleanup();
|
|
1998
|
-
}
|
|
1999
|
-
get isDead() {
|
|
2000
|
-
return this.dead;
|
|
2001
|
-
}
|
|
2002
|
-
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
2003
|
-
checkCanceled() {
|
|
2004
|
-
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
2005
|
-
}
|
|
2006
|
-
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
2007
|
-
abort() {
|
|
2008
|
-
this.stopCancelWatch();
|
|
2009
|
-
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
1766
|
+
// ── Metadados ─────────────────────────────────────────────────────────────
|
|
1767
|
+
setStatus(status) {
|
|
1768
|
+
this.status = status;
|
|
1769
|
+
return this;
|
|
2010
1770
|
}
|
|
1771
|
+
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
2011
1772
|
/**
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
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()`).
|
|
2015
1779
|
*/
|
|
2016
|
-
|
|
2017
|
-
this.
|
|
2018
|
-
|
|
2019
|
-
this.
|
|
2020
|
-
|
|
2021
|
-
this.buffer = [];
|
|
2022
|
-
this.itemsClosed = true;
|
|
2023
|
-
this.clearItemsTimer();
|
|
2024
|
-
if (this.itemsBuffer.length > 0) {
|
|
2025
|
-
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
2026
|
-
this.itemsBuffer = [];
|
|
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);
|
|
2027
1785
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
|
|
2039
|
-
}
|
|
2040
|
-
/**
|
|
2041
|
-
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
2042
|
-
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
2043
|
-
* intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
|
|
2044
|
-
* imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
|
|
2045
|
-
*/
|
|
2046
|
-
log(level, message, image3) {
|
|
2047
|
-
if (this.dead) return void 0;
|
|
2048
|
-
const scrubbed = scrubSecrets(safeString(message));
|
|
2049
|
-
const ts = nowISO();
|
|
2050
|
-
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2051
|
-
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
2052
|
-
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2053
|
-
return attachmentId;
|
|
2054
|
-
}
|
|
2055
|
-
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
2056
|
-
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
2057
|
-
if (this.dead) return ts;
|
|
2058
|
-
const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
|
|
2059
|
-
this.buffer.push({
|
|
2060
|
-
ts,
|
|
2061
|
-
level,
|
|
2062
|
-
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
2063
|
-
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
2064
|
-
});
|
|
2065
|
-
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
2066
|
-
this.flushLogs();
|
|
2067
|
-
} else {
|
|
2068
|
-
this.scheduleFlush();
|
|
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);
|
|
1795
|
+
}
|
|
2069
1796
|
}
|
|
2070
|
-
|
|
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));
|
|
1804
|
+
}
|
|
1805
|
+
return batch.length;
|
|
2071
1806
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
this.
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
+
};
|
|
2079
1824
|
}
|
|
2080
|
-
|
|
2081
|
-
if (this.
|
|
2082
|
-
|
|
2083
|
-
|
|
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;
|
|
2084
1846
|
}
|
|
1847
|
+
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
2085
1848
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
1849
|
+
};
|
|
1850
|
+
|
|
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);
|
|
1858
|
+
}
|
|
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();
|
|
2097
1866
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2111
|
-
]);
|
|
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
|
+
accumulated = {};
|
|
1877
|
+
data(payload) {
|
|
1878
|
+
Object.assign(this.accumulated, payload);
|
|
2112
1879
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
this.enqueue({ status: "running" });
|
|
2116
|
-
await this.chain;
|
|
2117
|
-
if (!this.dead) this.startCancelWatch();
|
|
2118
|
-
return !this.dead;
|
|
1880
|
+
succeeded(payload, options) {
|
|
1881
|
+
this.record("succeeded", void 0, payload, options?.image);
|
|
2119
1882
|
}
|
|
2120
|
-
|
|
2121
|
-
this.
|
|
2122
|
-
this.
|
|
2123
|
-
this.forceFinalProgress();
|
|
2124
|
-
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
1883
|
+
failed(message, payload, options) {
|
|
1884
|
+
this.record("failed", message, payload, options?.image);
|
|
1885
|
+
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
2125
1886
|
}
|
|
2126
|
-
|
|
2127
|
-
this.
|
|
2128
|
-
this.
|
|
2129
|
-
this.forceFinalProgress();
|
|
2130
|
-
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
1887
|
+
occurrence(message, payload, options) {
|
|
1888
|
+
this.record("occurrence", message, payload, options?.image);
|
|
1889
|
+
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
2131
1890
|
}
|
|
2132
|
-
/**
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
this.
|
|
2140
|
-
|
|
1891
|
+
/** Nova tentativa: o desfecho da anterior não vale (o id é o mesmo, o último reporte vence). */
|
|
1892
|
+
_nextAttempt(attempt) {
|
|
1893
|
+
this.attempt = attempt;
|
|
1894
|
+
this.reported = false;
|
|
1895
|
+
this.status = void 0;
|
|
1896
|
+
this.accumulated = {};
|
|
1897
|
+
if (this.step && (this.step.status === "error" || this.step.status === "warning")) {
|
|
1898
|
+
this.step._setStatus("ok");
|
|
1899
|
+
this.step._deleteData("message");
|
|
2141
1900
|
}
|
|
2142
|
-
return logs;
|
|
2143
1901
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
1902
|
+
record(status, message, payload, image3) {
|
|
1903
|
+
this.reported = true;
|
|
1904
|
+
this.status = status;
|
|
1905
|
+
const merged = { ...this.basePayload ?? {}, ...this.accumulated, ...payload ?? {} };
|
|
1906
|
+
this.sink.report(status, message, Object.keys(merged).length > 0 ? merged : void 0, this.id, image3);
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
function field(element, name) {
|
|
1910
|
+
if (typeof element !== "object" || element === null) return { present: false, value: void 0 };
|
|
1911
|
+
const record = element;
|
|
1912
|
+
return { present: name in record, value: record[name] };
|
|
1913
|
+
}
|
|
1914
|
+
function resolveId(spec, element, index, explicit) {
|
|
1915
|
+
if (spec === null) return void 0;
|
|
1916
|
+
let raw;
|
|
1917
|
+
if (typeof spec === "function") {
|
|
1918
|
+
raw = spec(element, index);
|
|
1919
|
+
} else {
|
|
1920
|
+
const fields = spec === void 0 ? [DEFAULT_ITEM_ID_FIELD] : typeof spec === "string" ? [spec] : spec;
|
|
1921
|
+
const parts = [];
|
|
1922
|
+
for (const name of fields) {
|
|
1923
|
+
const { present, value } = field(element, name);
|
|
1924
|
+
if (!present || value === null || value === void 0) {
|
|
1925
|
+
if (explicit) {
|
|
1926
|
+
throw new Error(
|
|
1927
|
+
`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`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
return void 0;
|
|
2152
1931
|
}
|
|
2153
|
-
|
|
1932
|
+
parts.push(safeString(value));
|
|
2154
1933
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
1934
|
+
raw = parts.join(ID_SEPARATOR);
|
|
1935
|
+
}
|
|
1936
|
+
if (raw === null || raw === void 0) return void 0;
|
|
1937
|
+
const text = safeString(raw);
|
|
1938
|
+
return text.length > 0 ? text : void 0;
|
|
1939
|
+
}
|
|
1940
|
+
function knownLength(list) {
|
|
1941
|
+
const length = list?.length;
|
|
1942
|
+
return typeof length === "number" && Number.isInteger(length) && length >= 1 ? length : void 0;
|
|
1943
|
+
}
|
|
1944
|
+
function skippedMessage(count) {
|
|
1945
|
+
if (count === 1) return "1 item pulado: j\xE1 conclu\xEDdo neste trabalho numa execu\xE7\xE3o anterior";
|
|
1946
|
+
return `${count} itens pulados: j\xE1 conclu\xEDdos neste trabalho numa execu\xE7\xE3o anterior`;
|
|
1947
|
+
}
|
|
1948
|
+
async function eachItem(list, handler, options, sink) {
|
|
1949
|
+
const result = { total: 0, succeeded: 0, failed: 0, occurrence: 0, skipped: 0 };
|
|
1950
|
+
const requested = options.retries ?? DEFAULT_EACH_RETRIES;
|
|
1951
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_EACH_RETRIES;
|
|
1952
|
+
const explicitId = options.id !== void 0;
|
|
1953
|
+
const length = options.progress === false ? void 0 : knownLength(list);
|
|
1954
|
+
if (length !== void 0) sink.declareTotal(length);
|
|
1955
|
+
const done = options.skipDone === false ? void 0 : await sink.doneItemIds();
|
|
1956
|
+
let index = 0;
|
|
1957
|
+
for await (const element of list) {
|
|
1958
|
+
sink.checkCanceled();
|
|
1959
|
+
const id = resolveId(options.id, element, index, explicitId);
|
|
1960
|
+
if (done?.size && id !== void 0 && done.has(id)) {
|
|
1961
|
+
result.total += 1;
|
|
1962
|
+
result.skipped += 1;
|
|
1963
|
+
if (options.progress !== false) sink.advance();
|
|
1964
|
+
index += 1;
|
|
1965
|
+
continue;
|
|
2166
1966
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
1967
|
+
const description = options.description?.(element, index) ?? id ?? `#${index + 1}`;
|
|
1968
|
+
const step = sink.startStep(options.op ?? "item", description);
|
|
1969
|
+
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1970
|
+
const current = index;
|
|
1971
|
+
let attempts = 0;
|
|
1972
|
+
let lastFailure;
|
|
1973
|
+
try {
|
|
1974
|
+
await retryWithBackoff(
|
|
1975
|
+
(attempt) => {
|
|
1976
|
+
attempts = attempt;
|
|
1977
|
+
if (attempt > 1) handle._nextAttempt(attempt);
|
|
1978
|
+
return sink.stepScope(step, async () => handler(element, handle, current));
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
retries: totalRetries,
|
|
1982
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
1983
|
+
// sem nova tentativa: ocorrência (regra de negócio), desfecho já registrado pelo
|
|
1984
|
+
// handler (ele decidiu) ou run abortada (o erro é efeito do cancelamento)
|
|
1985
|
+
shouldRetry: (error) => !isOccurrence(error) && !handle.reported && !sink.isAborted(),
|
|
1986
|
+
wait: (ms) => sink.sleep(ms),
|
|
1987
|
+
onRetry: (error, attempt, delayMs) => {
|
|
1988
|
+
lastFailure = { error };
|
|
1989
|
+
sink.logWarning(
|
|
1990
|
+
`item ${description}: tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
);
|
|
1995
|
+
if (!handle.reported) handle.succeeded();
|
|
1996
|
+
if (step) {
|
|
1997
|
+
noteAttempts(step, attempts);
|
|
1998
|
+
if (lastFailure && step.status === "ok") {
|
|
1999
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (isTerminalError(error) || sink.isAborted()) {
|
|
2004
|
+
step?._setStatus("cancelled")._close();
|
|
2005
|
+
throw error;
|
|
2006
|
+
}
|
|
2007
|
+
if (step) noteAttempts(step, attempts);
|
|
2008
|
+
if (isOccurrence(error)) {
|
|
2009
|
+
const occurrence = error;
|
|
2010
|
+
if (!handle.reported) handle.occurrence(occurrence.message, occurrence.payload, { image: occurrence.image });
|
|
2011
|
+
} else {
|
|
2012
|
+
const message = thrownMessage(error);
|
|
2013
|
+
if (!handle.reported) handle.failed(message);
|
|
2014
|
+
step?._failed(error);
|
|
2015
|
+
sink.logError(`item ${description} falhou: ${message}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
step?._close();
|
|
2019
|
+
if (options.progress !== false) sink.advance();
|
|
2020
|
+
result.total += 1;
|
|
2021
|
+
result[handle.status ?? "succeeded"] += 1;
|
|
2022
|
+
index += 1;
|
|
2023
|
+
if (options.flush !== false) await sink.flush();
|
|
2174
2024
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2025
|
+
if (result.skipped > 0) sink.logInfo(skippedMessage(result.skipped));
|
|
2026
|
+
return result;
|
|
2027
|
+
}
|
|
2028
|
+
function noteAttempts(step, attempts) {
|
|
2029
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/tasks/files.ts
|
|
2033
|
+
var LOCAL_FILES_DIR_ENV = "HOBOTS_LOCAL_FILES_DIR";
|
|
2034
|
+
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
2035
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2036
|
+
function isFileRef(value) {
|
|
2037
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2038
|
+
const ref = value;
|
|
2039
|
+
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
2040
|
+
}
|
|
2041
|
+
function fileField(params, field2) {
|
|
2042
|
+
if (!Object.prototype.hasOwnProperty.call(params, field2)) return void 0;
|
|
2043
|
+
const value = params[field2];
|
|
2044
|
+
if (isFileRef(value)) return value;
|
|
2045
|
+
if (Array.isArray(value) && value.every(isFileRef)) return value;
|
|
2046
|
+
return void 0;
|
|
2047
|
+
}
|
|
2048
|
+
function collectFileRefs(params) {
|
|
2049
|
+
const refs = [];
|
|
2050
|
+
for (const field2 of Object.keys(params)) {
|
|
2051
|
+
const value = fileField(params, field2);
|
|
2052
|
+
if (value !== void 0) refs.push(...Array.isArray(value) ? value : [value]);
|
|
2180
2053
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2054
|
+
return refs;
|
|
2055
|
+
}
|
|
2056
|
+
function safeFileName(name, fallback) {
|
|
2057
|
+
const cleaned = name.replace(/^.*[\\/]/, "").replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
|
|
2058
|
+
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
2059
|
+
}
|
|
2060
|
+
function isInsideDir(root, candidate, sep) {
|
|
2061
|
+
const base = root.endsWith(sep) ? root : root + sep;
|
|
2062
|
+
return candidate.startsWith(base) && candidate.length > base.length;
|
|
2063
|
+
}
|
|
2064
|
+
function unavailableLocal(ref) {
|
|
2065
|
+
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
2066
|
+
}
|
|
2067
|
+
function localRunPath(ref) {
|
|
2068
|
+
if (!isLocalRun()) return void 0;
|
|
2069
|
+
const path = ref.path;
|
|
2070
|
+
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
2071
|
+
}
|
|
2072
|
+
function errorCode(error) {
|
|
2073
|
+
return error?.code;
|
|
2074
|
+
}
|
|
2075
|
+
var RunFileStore = class {
|
|
2076
|
+
constructor(deps) {
|
|
2077
|
+
this.deps = deps;
|
|
2078
|
+
this.files = {
|
|
2079
|
+
list: () => collectFileRefs(deps.params),
|
|
2080
|
+
get: (field2) => fileField(deps.params, field2),
|
|
2081
|
+
// async: erro de uso (campo sem arquivo, valor inválido) chega como Promise rejeitada, nunca síncrono
|
|
2082
|
+
download: async (refOrField, options) => this.download(this.single(refOrField), options?.dest),
|
|
2083
|
+
downloadAll: async (field2, options) => this.downloadAll(field2, options?.dest),
|
|
2084
|
+
read: async (refOrField) => this.read(this.single(refOrField))
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
deps;
|
|
2088
|
+
files;
|
|
2089
|
+
tempDir;
|
|
2090
|
+
/** Apaga a pasta temporária da execução (cópias que o bot não levou para `dest`). Nunca lança. */
|
|
2091
|
+
async cleanup() {
|
|
2092
|
+
const pending = this.tempDir;
|
|
2093
|
+
this.tempDir = void 0;
|
|
2094
|
+
if (!pending) return;
|
|
2095
|
+
try {
|
|
2096
|
+
const { rm } = await import("fs/promises");
|
|
2097
|
+
await rm(await pending, { recursive: true, force: true });
|
|
2098
|
+
} catch {
|
|
2185
2099
|
}
|
|
2186
2100
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2101
|
+
field(field2) {
|
|
2102
|
+
const value = fileField(this.deps.params, field2);
|
|
2103
|
+
if (value === void 0) throw new Error(`campo "${field2}" n\xE3o tem arquivo nos params`);
|
|
2104
|
+
return value;
|
|
2191
2105
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2106
|
+
single(refOrField) {
|
|
2107
|
+
if (typeof refOrField === "string") {
|
|
2108
|
+
const value = this.field(refOrField);
|
|
2109
|
+
if (Array.isArray(value)) {
|
|
2110
|
+
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
2111
|
+
}
|
|
2112
|
+
return value;
|
|
2113
|
+
}
|
|
2114
|
+
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
2115
|
+
return refOrField;
|
|
2197
2116
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
if (this.watchTimer) return;
|
|
2205
|
-
this.watchTimer = setInterval(() => {
|
|
2206
|
-
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2207
|
-
this.enqueue({ status: "running" });
|
|
2208
|
-
}, this.cancelCheckIntervalMs);
|
|
2209
|
-
unrefTimer(this.watchTimer);
|
|
2117
|
+
async downloadAll(field2, dest) {
|
|
2118
|
+
const value = this.field(field2);
|
|
2119
|
+
const refs = Array.isArray(value) ? value : [value];
|
|
2120
|
+
const paths = [];
|
|
2121
|
+
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
2122
|
+
return paths;
|
|
2210
2123
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2124
|
+
async read(ref) {
|
|
2125
|
+
const { readFile, rm } = await import("fs/promises");
|
|
2126
|
+
const { dirname } = await import("path");
|
|
2127
|
+
const path = await this.download(ref, void 0);
|
|
2128
|
+
try {
|
|
2129
|
+
return await readFile(path);
|
|
2130
|
+
} finally {
|
|
2131
|
+
await rm(dirname(path), { recursive: true, force: true });
|
|
2215
2132
|
}
|
|
2216
2133
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2134
|
+
async download(ref, dest, destIsDir = false) {
|
|
2135
|
+
this.checkCanceled();
|
|
2136
|
+
const target = await this.resolveTarget(ref, dest, destIsDir);
|
|
2137
|
+
if (ref.storage === "local") await this.copyLocal(ref, target);
|
|
2138
|
+
else await this.downloadS3(ref, target);
|
|
2139
|
+
return target;
|
|
2220
2140
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2224
|
-
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2225
|
-
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2226
|
-
*/
|
|
2227
|
-
addItem(status, message, payload, id, image3) {
|
|
2228
|
-
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2229
|
-
let scrubbedPayload;
|
|
2230
|
-
if (payload) {
|
|
2231
|
-
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2232
|
-
let serializedLength;
|
|
2233
|
-
try {
|
|
2234
|
-
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2235
|
-
} catch {
|
|
2236
|
-
}
|
|
2237
|
-
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2238
|
-
this.logger.warn(
|
|
2239
|
-
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2240
|
-
);
|
|
2241
|
-
scrubbedPayload = void 0;
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
const ts = nowISO();
|
|
2245
|
-
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2246
|
-
this.itemsBuffer.push({
|
|
2247
|
-
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2248
|
-
seq: this.itemSeq++,
|
|
2249
|
-
status,
|
|
2250
|
-
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2251
|
-
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2252
|
-
ts,
|
|
2253
|
-
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2254
|
-
});
|
|
2255
|
-
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2256
|
-
this.flushItems();
|
|
2257
|
-
} else {
|
|
2258
|
-
this.scheduleItemsFlush();
|
|
2259
|
-
}
|
|
2141
|
+
checkCanceled() {
|
|
2142
|
+
if (this.deps.signal.aborted) throw runCanceledError();
|
|
2260
2143
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2144
|
+
runTempDir() {
|
|
2145
|
+
this.tempDir ??= (async () => {
|
|
2146
|
+
const { mkdtemp } = await import("fs/promises");
|
|
2147
|
+
const { tmpdir } = await import("os");
|
|
2148
|
+
const { join } = await import("path");
|
|
2149
|
+
return mkdtemp(join(tmpdir(), "hobots-files-"));
|
|
2150
|
+
})();
|
|
2151
|
+
return this.tempDir;
|
|
2268
2152
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2153
|
+
async resolveTarget(ref, dest, destIsDir) {
|
|
2154
|
+
const { mkdir, mkdtemp, stat } = await import("fs/promises");
|
|
2155
|
+
const { dirname, join, resolve: resolve2 } = await import("path");
|
|
2156
|
+
const name = safeFileName(ref.name, "arquivo");
|
|
2157
|
+
if (dest === void 0) {
|
|
2158
|
+
return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
|
|
2273
2159
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2279
|
-
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2280
|
-
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2281
|
-
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2282
|
-
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2160
|
+
const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
|
|
2161
|
+
if (isDir) {
|
|
2162
|
+
await mkdir(dest, { recursive: true });
|
|
2163
|
+
return resolve2(dest, name);
|
|
2283
2164
|
}
|
|
2165
|
+
await mkdir(dirname(resolve2(dest)), { recursive: true });
|
|
2166
|
+
return resolve2(dest);
|
|
2284
2167
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2168
|
+
/** storage 's3': URL pré-assinada → GET em streaming → confere o tamanho → só então DELETE. */
|
|
2169
|
+
async downloadS3(ref, target) {
|
|
2170
|
+
const { api, runId, signal } = this.deps;
|
|
2171
|
+
let expected;
|
|
2172
|
+
let url;
|
|
2287
2173
|
try {
|
|
2288
|
-
await
|
|
2289
|
-
|
|
2174
|
+
const info = await api.requestFileDownload(runId, ref.id, signal);
|
|
2175
|
+
expected = info.size >= 0 ? info.size : ref.size;
|
|
2176
|
+
url = info.url;
|
|
2290
2177
|
} catch (error) {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2178
|
+
throw this.translate(error, ref);
|
|
2179
|
+
}
|
|
2180
|
+
const { open, rename, rm } = await import("fs/promises");
|
|
2181
|
+
const part = `${target}.part`;
|
|
2182
|
+
const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
|
|
2183
|
+
const controller = new AbortController();
|
|
2184
|
+
let timedOut = false;
|
|
2185
|
+
let timer;
|
|
2186
|
+
const arm = () => {
|
|
2187
|
+
clearTimeout(timer);
|
|
2188
|
+
timer = setTimeout(() => {
|
|
2189
|
+
timedOut = true;
|
|
2190
|
+
controller.abort();
|
|
2191
|
+
}, idleTimeoutMs);
|
|
2192
|
+
};
|
|
2193
|
+
const onCancel = () => controller.abort();
|
|
2194
|
+
signal.addEventListener("abort", onCancel, { once: true });
|
|
2195
|
+
try {
|
|
2196
|
+
arm();
|
|
2197
|
+
const response = await api.fetchFile(url, controller.signal);
|
|
2198
|
+
const handle = await open(part, "w");
|
|
2199
|
+
let written = 0;
|
|
2200
|
+
try {
|
|
2201
|
+
const reader = response.body?.getReader();
|
|
2202
|
+
const stopReading = () => void reader?.cancel().catch(() => void 0);
|
|
2203
|
+
controller.signal.addEventListener("abort", stopReading, { once: true });
|
|
2204
|
+
for (; ; ) {
|
|
2205
|
+
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
2206
|
+
if (chunk.done) break;
|
|
2207
|
+
arm();
|
|
2208
|
+
written += chunk.value.byteLength;
|
|
2209
|
+
if (written > expected) {
|
|
2210
|
+
stopReading();
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
await handle.write(chunk.value);
|
|
2300
2214
|
}
|
|
2301
|
-
|
|
2215
|
+
} finally {
|
|
2216
|
+
await handle.close();
|
|
2302
2217
|
}
|
|
2303
|
-
if (
|
|
2304
|
-
|
|
2305
|
-
this.itemsStorageFull = true;
|
|
2306
|
-
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2307
|
-
}
|
|
2308
|
-
return;
|
|
2218
|
+
if (written !== expected) {
|
|
2219
|
+
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
2309
2220
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2221
|
+
await rename(part, target);
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
await rm(part, { force: true });
|
|
2224
|
+
if (signal.aborted) throw runCanceledError();
|
|
2225
|
+
if (timedOut) {
|
|
2226
|
+
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
2314
2227
|
}
|
|
2315
|
-
this.
|
|
2316
|
-
|
|
2317
|
-
|
|
2228
|
+
throw this.translate(error, ref);
|
|
2229
|
+
} finally {
|
|
2230
|
+
clearTimeout(timer);
|
|
2231
|
+
signal.removeEventListener("abort", onCancel);
|
|
2318
2232
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2324
|
-
*/
|
|
2325
|
-
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2326
|
-
this.flushItems();
|
|
2327
|
-
await Promise.race([
|
|
2328
|
-
this.itemsChain,
|
|
2329
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2330
|
-
]);
|
|
2331
|
-
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2332
|
-
this.flushItems();
|
|
2333
|
-
await Promise.race([
|
|
2334
|
-
this.itemsChain,
|
|
2335
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2336
|
-
]);
|
|
2233
|
+
try {
|
|
2234
|
+
await api.confirmFileDownload(runId, ref.id);
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
2337
2237
|
}
|
|
2338
2238
|
}
|
|
2339
|
-
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2340
2239
|
/**
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2344
|
-
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2345
|
-
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2346
|
-
* também cai aqui via `log()`.
|
|
2240
|
+
* storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
|
|
2241
|
+
* origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
|
|
2347
2242
|
*/
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2243
|
+
async copyLocal(ref, target) {
|
|
2244
|
+
const direct = localRunPath(ref);
|
|
2245
|
+
if (direct !== void 0) {
|
|
2246
|
+
await this.copyFromPath(ref, direct, target);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
2250
|
+
if (!base) throw unavailableLocal(ref);
|
|
2251
|
+
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
2252
|
+
const { join, resolve: resolve2, sep } = await import("path");
|
|
2253
|
+
const root = resolve2(base);
|
|
2254
|
+
const folder = resolve2(root, ref.id);
|
|
2255
|
+
if (!isInsideDir(root, folder, sep) || !UUID_RE.test(ref.id)) {
|
|
2256
|
+
throw new Error(`id de arquivo local inv\xE1lido: ${JSON.stringify(ref.id)}`);
|
|
2257
|
+
}
|
|
2258
|
+
let names;
|
|
2259
|
+
try {
|
|
2260
|
+
names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
|
|
2263
|
+
throw error;
|
|
2264
|
+
}
|
|
2265
|
+
if (names.length === 0) throw unavailableLocal(ref);
|
|
2266
|
+
if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
|
|
2267
|
+
this.checkCanceled();
|
|
2268
|
+
const part = `${target}.part`;
|
|
2269
|
+
try {
|
|
2270
|
+
await copyFile(join(folder, names[0]), part);
|
|
2271
|
+
const { size } = await stat(part);
|
|
2272
|
+
if (size !== ref.size) {
|
|
2273
|
+
throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
|
|
2356
2274
|
}
|
|
2357
|
-
|
|
2275
|
+
await rename(part, target);
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
await rm(part, { force: true });
|
|
2278
|
+
throw error;
|
|
2358
2279
|
}
|
|
2359
|
-
|
|
2360
|
-
const id = uuid4();
|
|
2361
|
-
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
2362
|
-
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
2363
|
-
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
2364
|
-
this.pendingAttachments.add(work);
|
|
2365
|
-
void work.finally(() => this.pendingAttachments.delete(work));
|
|
2366
|
-
return id;
|
|
2280
|
+
await rm(folder, { recursive: true, force: true });
|
|
2367
2281
|
}
|
|
2368
|
-
/**
|
|
2369
|
-
|
|
2370
|
-
|
|
2282
|
+
/**
|
|
2283
|
+
* Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
|
|
2284
|
+
* Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
|
|
2285
|
+
*/
|
|
2286
|
+
async copyFromPath(ref, path, target) {
|
|
2287
|
+
const { copyFile, rename, rm, stat } = await import("fs/promises");
|
|
2288
|
+
const { resolve: resolve2 } = await import("path");
|
|
2289
|
+
const source = resolve2(path);
|
|
2290
|
+
const info = await stat(source).catch(() => void 0);
|
|
2291
|
+
if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
|
|
2292
|
+
this.checkCanceled();
|
|
2293
|
+
const part = `${target}.part`;
|
|
2371
2294
|
try {
|
|
2372
|
-
|
|
2295
|
+
await copyFile(source, part);
|
|
2296
|
+
await rename(part, target);
|
|
2373
2297
|
} catch (error) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2298
|
+
await rm(part, { force: true });
|
|
2299
|
+
throw error;
|
|
2376
2300
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2301
|
+
}
|
|
2302
|
+
translate(error, ref) {
|
|
2303
|
+
if (this.deps.signal.aborted) return runCanceledError();
|
|
2304
|
+
if (error instanceof TasksApiError && error.status === 410) {
|
|
2305
|
+
return new TasksApiError(`arquivo "${ref.name}" j\xE1 baixado ou expirado`, 410);
|
|
2381
2306
|
}
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2384
|
-
"warning",
|
|
2385
|
-
`imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
|
|
2386
|
-
);
|
|
2387
|
-
return "too-large";
|
|
2307
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2308
|
+
return new TasksApiError(`arquivo "${ref.name}" n\xE3o encontrado nesta execu\xE7\xE3o`, 404);
|
|
2388
2309
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2310
|
+
return error;
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
// src/tasks/context.ts
|
|
2315
|
+
var LOG_LEVEL_COLOR = {
|
|
2316
|
+
debug: "gray",
|
|
2317
|
+
info: "cyan",
|
|
2318
|
+
warning: "yellow",
|
|
2319
|
+
error: "red",
|
|
2320
|
+
success: "green"
|
|
2321
|
+
};
|
|
2322
|
+
function joinLogArgs(message, rest) {
|
|
2323
|
+
if (rest.length === 0) return safeString(message);
|
|
2324
|
+
return [message, ...rest].map(safeString).join(" ");
|
|
2325
|
+
}
|
|
2326
|
+
function splitLogArgs(message, rest) {
|
|
2327
|
+
const args = [message, ...rest];
|
|
2328
|
+
const last = args[args.length - 1];
|
|
2329
|
+
if (!isImageAttachment(last)) return { text: joinLogArgs(message, rest) };
|
|
2330
|
+
const text = args.slice(0, -1);
|
|
2331
|
+
if (text.length === 0) return { text: last.caption !== void 0 ? safeString(last.caption) : "captura de tela", image: last };
|
|
2332
|
+
return { text: joinLogArgs(text[0], text.slice(1)), image: last };
|
|
2333
|
+
}
|
|
2334
|
+
function printRunLog(level, message) {
|
|
2335
|
+
const line = colorize(`[hobots] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
2336
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
2337
|
+
console[method2](line, message);
|
|
2338
|
+
}
|
|
2339
|
+
var PROGRESS_LINE_PREFIX = "[hobots:progress]";
|
|
2340
|
+
var PROGRESS_PRINT_THROTTLE_MS = 250;
|
|
2341
|
+
function printRunProgress(current, total) {
|
|
2342
|
+
console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
|
|
2343
|
+
}
|
|
2344
|
+
function cancelableSleep(ms, signal) {
|
|
2345
|
+
return new Promise((resolve2, reject) => {
|
|
2346
|
+
if (signal.aborted) {
|
|
2347
|
+
reject(runCanceledError());
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
const onAbort = () => {
|
|
2351
|
+
clearTimeout(timer);
|
|
2352
|
+
reject(runCanceledError());
|
|
2353
|
+
};
|
|
2354
|
+
const timer = setTimeout(() => {
|
|
2355
|
+
signal.removeEventListener("abort", onAbort);
|
|
2356
|
+
resolve2();
|
|
2357
|
+
}, ms);
|
|
2358
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
var DEFAULT_LOG_FLUSH_INTERVAL_MS = 2e3;
|
|
2362
|
+
var DEFAULT_CANCEL_CHECK_INTERVAL_MS = 5e3;
|
|
2363
|
+
var MIN_CANCEL_CHECK_INTERVAL_MS = 1e3;
|
|
2364
|
+
var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
2365
|
+
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
2366
|
+
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
2367
|
+
var FLUSH_AT_BUFFERED = 20;
|
|
2368
|
+
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
2369
|
+
var MAX_ITEMS_DONE_PER_PAGE = WIRE_LIMITS.MAX_ITEMS_DONE_PER_PAGE;
|
|
2370
|
+
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
2371
|
+
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
2372
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
2373
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
2374
|
+
var PROGRESS_FLUSH_MS = 1e3;
|
|
2375
|
+
function scrubPayloadValue(value, depth) {
|
|
2376
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
2377
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
2378
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
2379
|
+
const out = {};
|
|
2380
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2381
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
2382
|
+
}
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
2385
|
+
var RunReporter = class {
|
|
2386
|
+
constructor(api, runId, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
2387
|
+
this.api = api;
|
|
2388
|
+
this.runId = runId;
|
|
2389
|
+
this.logger = logger;
|
|
2390
|
+
this.tx = tx;
|
|
2391
|
+
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
2392
|
+
this.cancelCheckIntervalMs = Math.min(
|
|
2393
|
+
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
2394
|
+
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
2395
|
+
);
|
|
2396
|
+
const logAt = (level) => (message, ...rest) => {
|
|
2397
|
+
const { text, image: image3 } = splitLogArgs(message, rest);
|
|
2398
|
+
this.log(level, text, image3);
|
|
2399
|
+
};
|
|
2400
|
+
const log = {
|
|
2401
|
+
debug: logAt("debug"),
|
|
2402
|
+
info: logAt("info"),
|
|
2403
|
+
warn: logAt("warning"),
|
|
2404
|
+
error: logAt("error"),
|
|
2405
|
+
success: logAt("success")
|
|
2406
|
+
};
|
|
2407
|
+
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
2408
|
+
tx.setAttachmentSink((image3, ts) => this.queueAttachment(image3, ts));
|
|
2409
|
+
const checkCanceled = () => this.checkCanceled();
|
|
2410
|
+
tx.setStepGuard(checkCanceled);
|
|
2411
|
+
const eachSink = {
|
|
2412
|
+
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
2413
|
+
startStep: (op, description) => tx.start(op, description),
|
|
2414
|
+
stepScope: (step, fn) => tx.scope(step, fn),
|
|
2415
|
+
declareTotal: (total) => {
|
|
2416
|
+
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
2417
|
+
},
|
|
2418
|
+
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
2419
|
+
checkCanceled,
|
|
2420
|
+
isAborted: () => this.abortController.signal.aborted,
|
|
2421
|
+
logInfo: (message) => this.log("info", message),
|
|
2422
|
+
logWarning: (message) => this.log("warning", message),
|
|
2423
|
+
logError: (message) => this.log("error", message),
|
|
2424
|
+
sleep: (ms) => cancelableSleep(ms, this.abortController.signal),
|
|
2425
|
+
doneItemIds: () => this.listDoneItemIds(),
|
|
2426
|
+
flush: () => this.flush()
|
|
2427
|
+
};
|
|
2428
|
+
const items = {
|
|
2429
|
+
add: (item) => {
|
|
2430
|
+
checkCanceled();
|
|
2431
|
+
this.addItem(item.status, item.message, item.payload, item.id, item.image);
|
|
2432
|
+
},
|
|
2433
|
+
succeeded: (payload, options) => {
|
|
2434
|
+
checkCanceled();
|
|
2435
|
+
this.addItem("succeeded", void 0, payload, options?.id, options?.image);
|
|
2436
|
+
},
|
|
2437
|
+
failed: (message, payload, options) => {
|
|
2438
|
+
checkCanceled();
|
|
2439
|
+
this.addItem("failed", message, payload, options?.id, options?.image);
|
|
2440
|
+
},
|
|
2441
|
+
occurrence: (message, payload, options) => {
|
|
2442
|
+
checkCanceled();
|
|
2443
|
+
this.addItem("occurrence", message, payload, options?.id, options?.image);
|
|
2444
|
+
},
|
|
2445
|
+
each: (list, handler, options = {}) => eachItem(list, handler, options, eachSink)
|
|
2446
|
+
};
|
|
2447
|
+
const progress = Object.defineProperties(
|
|
2448
|
+
{
|
|
2449
|
+
total: (total) => {
|
|
2450
|
+
checkCanceled();
|
|
2451
|
+
this.setProgress(this.progressCurrent, total);
|
|
2452
|
+
},
|
|
2453
|
+
advance: (count = 1) => {
|
|
2454
|
+
checkCanceled();
|
|
2455
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
2402
2456
|
},
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2415
|
-
if (error instanceof TasksApiError && error.status === 429) {
|
|
2416
|
-
this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
|
|
2417
|
-
return "quota";
|
|
2457
|
+
set: (current, total) => {
|
|
2458
|
+
checkCanceled();
|
|
2459
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
current: { enumerable: true, get: () => this.progressCurrent },
|
|
2464
|
+
snapshot: {
|
|
2465
|
+
enumerable: true,
|
|
2466
|
+
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
2467
|
+
}
|
|
2418
2468
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2469
|
+
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
2470
|
+
);
|
|
2471
|
+
const signal = this.abortController.signal;
|
|
2472
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
2473
|
+
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
2474
|
+
this.context = {
|
|
2475
|
+
runId,
|
|
2476
|
+
params,
|
|
2477
|
+
log,
|
|
2478
|
+
items,
|
|
2479
|
+
progress,
|
|
2480
|
+
files: this.fileStore.files,
|
|
2481
|
+
step: async (name, fn, options) => {
|
|
2482
|
+
const root = tx.currentStep() === void 0;
|
|
2483
|
+
try {
|
|
2484
|
+
return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
|
|
2485
|
+
} finally {
|
|
2486
|
+
if (options?.flush !== false && root && !signal.aborted) await this.flush();
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
2490
|
+
signal,
|
|
2491
|
+
throwIfCanceled: checkCanceled,
|
|
2492
|
+
sleep: sleep2,
|
|
2493
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
2438
2494
|
};
|
|
2439
|
-
if (this.uploadSlots > 0) {
|
|
2440
|
-
this.uploadSlots--;
|
|
2441
|
-
return Promise.resolve(release);
|
|
2442
|
-
}
|
|
2443
|
-
return new Promise((resolve2) => {
|
|
2444
|
-
this.uploadQueue.push(() => resolve2(release));
|
|
2445
|
-
});
|
|
2446
2495
|
}
|
|
2496
|
+
api;
|
|
2497
|
+
runId;
|
|
2498
|
+
logger;
|
|
2499
|
+
tx;
|
|
2500
|
+
context;
|
|
2501
|
+
buffer = [];
|
|
2502
|
+
dead = false;
|
|
2503
|
+
chain = Promise.resolve();
|
|
2504
|
+
pendingUpdates = 0;
|
|
2505
|
+
timer;
|
|
2506
|
+
watchTimer;
|
|
2507
|
+
abortController = new AbortController();
|
|
2508
|
+
flushIntervalMs;
|
|
2509
|
+
cancelCheckIntervalMs;
|
|
2510
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
2511
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
2512
|
+
// envio preservada e sem lotes sobrepostos.
|
|
2513
|
+
itemsBuffer = [];
|
|
2514
|
+
itemsChain = Promise.resolve();
|
|
2515
|
+
itemsTimer;
|
|
2516
|
+
itemSeq = 0;
|
|
2517
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
2518
|
+
itemsUnsupported = false;
|
|
2447
2519
|
/**
|
|
2448
|
-
*
|
|
2449
|
-
*
|
|
2520
|
+
* Cota de itens por run do servidor (429, legado — hoje a api não tem teto de itens) —
|
|
2521
|
+
* degrada com um aviso só.
|
|
2450
2522
|
*/
|
|
2451
|
-
|
|
2452
|
-
if (this.pendingAttachments.size === 0) return;
|
|
2453
|
-
await Promise.race([
|
|
2454
|
-
Promise.allSettled([...this.pendingAttachments]),
|
|
2455
|
-
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2456
|
-
]);
|
|
2457
|
-
}
|
|
2458
|
-
enqueue(updateIn) {
|
|
2459
|
-
if (this.dead) {
|
|
2460
|
-
this.salvageLogs(updateIn.logs);
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
2463
|
-
this.clearProgressTimer();
|
|
2464
|
-
const progress = this.takeProgress();
|
|
2465
|
-
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
2466
|
-
this.pendingUpdates++;
|
|
2467
|
-
this.chain = this.chain.then(async () => {
|
|
2468
|
-
if (this.dead) {
|
|
2469
|
-
this.salvageLogs(update.logs);
|
|
2470
|
-
return;
|
|
2471
|
-
}
|
|
2472
|
-
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
2473
|
-
try {
|
|
2474
|
-
await this.api.updateRun(this.runId, payload);
|
|
2475
|
-
} catch (error) {
|
|
2476
|
-
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
2477
|
-
this.dead = true;
|
|
2478
|
-
this.stopCancelWatch();
|
|
2479
|
-
this.abort();
|
|
2480
|
-
const pending = this.buffer.splice(0, this.buffer.length);
|
|
2481
|
-
this.salvageLogs(pending);
|
|
2482
|
-
this.logger.warn(
|
|
2483
|
-
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
2484
|
-
);
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2487
|
-
if (update.logs && update.logs.length > 0) {
|
|
2488
|
-
this.buffer.unshift(...update.logs);
|
|
2489
|
-
this.scheduleFlush();
|
|
2490
|
-
}
|
|
2491
|
-
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
2492
|
-
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
2493
|
-
}
|
|
2494
|
-
}).finally(() => {
|
|
2495
|
-
this.pendingUpdates--;
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2523
|
+
itemsQuotaExceeded = false;
|
|
2498
2524
|
/**
|
|
2499
|
-
*
|
|
2500
|
-
*
|
|
2501
|
-
* o
|
|
2525
|
+
* Armazenamento do plano cheio (402 `storage_limit`): o lote é descartado e o aviso sai
|
|
2526
|
+
* uma vez por episódio. Os envios continuam — num agente `start()` o espaço pode ser
|
|
2527
|
+
* liberado (ou o plano trocado) sem reiniciar; um lote aceito rearma o aviso.
|
|
2502
2528
|
*/
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2529
|
+
itemsStorageFull = false;
|
|
2530
|
+
/**
|
|
2531
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
2532
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
2533
|
+
*/
|
|
2534
|
+
itemsClosed = false;
|
|
2535
|
+
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
2536
|
+
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
2537
|
+
pendingAttachments = /* @__PURE__ */ new Set();
|
|
2538
|
+
attachmentCount = 0;
|
|
2539
|
+
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
2540
|
+
uploadQueue = [];
|
|
2541
|
+
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
2542
|
+
attachmentsUnsupported = false;
|
|
2543
|
+
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
2544
|
+
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
2545
|
+
progressCurrent = 0;
|
|
2546
|
+
progressTotal = null;
|
|
2547
|
+
/** Snapshot ainda não entregue ao servidor. */
|
|
2548
|
+
progressDirty = false;
|
|
2549
|
+
progressTimer;
|
|
2550
|
+
progressWarned = false;
|
|
2551
|
+
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
2552
|
+
progressLastPrint = 0;
|
|
2553
|
+
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
2554
|
+
fileStore;
|
|
2555
|
+
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
2556
|
+
cleanupFiles() {
|
|
2557
|
+
return this.fileStore.cleanup();
|
|
2509
2558
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
// src/tasks/run-context-store.ts
|
|
2513
|
-
var storage;
|
|
2514
|
-
var stepStorage;
|
|
2515
|
-
var initPromise;
|
|
2516
|
-
async function ensureStorage() {
|
|
2517
|
-
if (storage || !isNode()) return;
|
|
2518
|
-
initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
|
|
2519
|
-
storage = new ALS();
|
|
2520
|
-
stepStorage = new ALS();
|
|
2521
|
-
});
|
|
2522
|
-
await initPromise;
|
|
2523
|
-
}
|
|
2524
|
-
async function runWithRunContext(store, fn) {
|
|
2525
|
-
await ensureStorage();
|
|
2526
|
-
return storage ? storage.run(store, fn) : fn();
|
|
2527
|
-
}
|
|
2528
|
-
function currentRunReporter() {
|
|
2529
|
-
return storage?.getStore()?.reporter;
|
|
2530
|
-
}
|
|
2531
|
-
async function runWithStepScope(scope, fn) {
|
|
2532
|
-
await ensureStorage();
|
|
2533
|
-
return stepStorage ? stepStorage.run(scope, fn) : fn();
|
|
2534
|
-
}
|
|
2535
|
-
function currentStep(transaction) {
|
|
2536
|
-
const scope = stepStorage?.getStore();
|
|
2537
|
-
return scope?.transaction === transaction ? scope.step : void 0;
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
// src/transaction.ts
|
|
2541
|
-
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
2542
|
-
var DEFAULT_STEP_RETRIES = 1;
|
|
2543
|
-
var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
|
|
2544
|
-
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2545
|
-
var MAX_BACKLOG = 100;
|
|
2546
|
-
function stepMessage(text) {
|
|
2547
|
-
return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
2548
|
-
}
|
|
2549
|
-
function stepId() {
|
|
2550
|
-
return uuid4().slice(0, 16);
|
|
2551
|
-
}
|
|
2552
|
-
function stepLabel(op, description) {
|
|
2553
|
-
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
2554
|
-
}
|
|
2555
|
-
function errorText(error) {
|
|
2556
|
-
if (error instanceof Error) return error.message || error.name;
|
|
2557
|
-
return safeString(error);
|
|
2558
|
-
}
|
|
2559
|
-
function errorReason(error) {
|
|
2560
|
-
return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
|
|
2561
|
-
}
|
|
2562
|
-
function levelForStatus(status) {
|
|
2563
|
-
switch (status) {
|
|
2564
|
-
case "warning":
|
|
2565
|
-
return "warning";
|
|
2566
|
-
case "error":
|
|
2567
|
-
return "error";
|
|
2568
|
-
case "cancelled":
|
|
2569
|
-
return "warning";
|
|
2570
|
-
default:
|
|
2571
|
-
return "success";
|
|
2559
|
+
get isDead() {
|
|
2560
|
+
return this.dead;
|
|
2572
2561
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
case "warning":
|
|
2577
|
-
return "yellow";
|
|
2578
|
-
case "error":
|
|
2579
|
-
return "red";
|
|
2580
|
-
case "cancelled":
|
|
2581
|
-
return "gray";
|
|
2582
|
-
default:
|
|
2583
|
-
return "green";
|
|
2562
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
2563
|
+
checkCanceled() {
|
|
2564
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
2584
2565
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
this.
|
|
2589
|
-
this.parentStepId = parentStepId;
|
|
2590
|
-
this.op = op;
|
|
2591
|
-
this.description = description;
|
|
2566
|
+
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
2567
|
+
abort() {
|
|
2568
|
+
this.stopCancelWatch();
|
|
2569
|
+
if (!this.abortController.signal.aborted) this.abortController.abort();
|
|
2592
2570
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2571
|
+
/**
|
|
2572
|
+
* Mata o reporter de vez: aborta o signal e suprime todo reporte futuro.
|
|
2573
|
+
* Usado no exit path — depois que o agente reportou AgentTerminated por fora
|
|
2574
|
+
* da fila, um handler zumbi não pode reenviar status por cima.
|
|
2575
|
+
*/
|
|
2576
|
+
kill() {
|
|
2577
|
+
this.abort();
|
|
2578
|
+
this.dead = true;
|
|
2579
|
+
this.clearFlushTimer();
|
|
2580
|
+
this.clearProgressTimer();
|
|
2581
|
+
this.buffer = [];
|
|
2582
|
+
this.itemsClosed = true;
|
|
2583
|
+
this.clearItemsTimer();
|
|
2584
|
+
if (this.itemsBuffer.length > 0) {
|
|
2585
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
2586
|
+
this.itemsBuffer = [];
|
|
2587
|
+
}
|
|
2588
|
+
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
2589
|
+
for (const resume of waiting) resume();
|
|
2607
2590
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2591
|
+
/**
|
|
2592
|
+
* Drena o buffer pendente para o PATCH único do exit path (chame ANTES de kill,
|
|
2593
|
+
* que descarta o buffer). Capado em um chunk — o exit path não tem retry nem fila,
|
|
2594
|
+
* então preserva os logs mais recentes, os mais valiosos na morte do processo.
|
|
2595
|
+
*/
|
|
2596
|
+
drainLogs() {
|
|
2597
|
+
this.clearFlushTimer();
|
|
2598
|
+
return this.takeLogs().slice(-MAX_LOGS_PER_PATCH);
|
|
2611
2599
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2600
|
+
/**
|
|
2601
|
+
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
2602
|
+
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
2603
|
+
* intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
|
|
2604
|
+
* imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
|
|
2605
|
+
*/
|
|
2606
|
+
log(level, message, image3) {
|
|
2607
|
+
if (this.dead) return void 0;
|
|
2608
|
+
const scrubbed = scrubSecrets(safeString(message));
|
|
2609
|
+
const ts = nowISO();
|
|
2610
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2611
|
+
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
2612
|
+
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2613
|
+
return attachmentId;
|
|
2616
2614
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2615
|
+
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
2616
|
+
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
2617
|
+
if (this.dead) return ts;
|
|
2618
|
+
const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
|
|
2619
|
+
this.buffer.push({
|
|
2620
|
+
ts,
|
|
2621
|
+
level,
|
|
2622
|
+
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
2623
|
+
...attachments && attachments.length > 0 ? { attachments: attachments.slice(0, WIRE_LIMITS.MAX_ATTACHMENTS_PER_LOG_ENTRY) } : {}
|
|
2624
|
+
});
|
|
2625
|
+
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
2626
|
+
this.flushLogs();
|
|
2627
|
+
} else {
|
|
2628
|
+
this.scheduleFlush();
|
|
2629
|
+
}
|
|
2630
|
+
return ts;
|
|
2621
2631
|
}
|
|
2622
|
-
|
|
2623
|
-
this.
|
|
2624
|
-
|
|
2625
|
-
|
|
2632
|
+
scheduleFlush() {
|
|
2633
|
+
if (this.timer) return;
|
|
2634
|
+
this.timer = setTimeout(() => {
|
|
2635
|
+
this.timer = void 0;
|
|
2636
|
+
this.flushLogs();
|
|
2637
|
+
}, this.flushIntervalMs);
|
|
2638
|
+
unrefTimer(this.timer);
|
|
2639
|
+
}
|
|
2640
|
+
clearFlushTimer() {
|
|
2641
|
+
if (this.timer) {
|
|
2642
|
+
clearTimeout(this.timer);
|
|
2643
|
+
this.timer = void 0;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
takeLogs() {
|
|
2647
|
+
return this.buffer.splice(0, this.buffer.length);
|
|
2648
|
+
}
|
|
2649
|
+
/** Flush incremental: envia o buffer como status 'running' (em chunks). */
|
|
2650
|
+
flushLogs() {
|
|
2651
|
+
this.clearFlushTimer();
|
|
2652
|
+
if (this.dead || this.buffer.length === 0) return;
|
|
2653
|
+
const logs = this.buffer.splice(0, this.buffer.length);
|
|
2654
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
2655
|
+
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
2656
|
+
}
|
|
2626
2657
|
}
|
|
2627
2658
|
/**
|
|
2628
|
-
*
|
|
2629
|
-
*
|
|
2659
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
|
|
2660
|
+
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2661
|
+
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2630
2662
|
*/
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
this.
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2663
|
+
async flush(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2664
|
+
this.tx.flush();
|
|
2665
|
+
this.flushItems();
|
|
2666
|
+
if (this.progressDirty) this.flushProgress();
|
|
2667
|
+
else this.flushLogs();
|
|
2668
|
+
await Promise.race([
|
|
2669
|
+
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2670
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2671
|
+
]);
|
|
2639
2672
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2673
|
+
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
2674
|
+
async reportRunning() {
|
|
2675
|
+
this.enqueue({ status: "running" });
|
|
2676
|
+
await this.chain;
|
|
2677
|
+
if (!this.dead) this.startCancelWatch();
|
|
2678
|
+
return !this.dead;
|
|
2679
|
+
}
|
|
2680
|
+
reportSuccess() {
|
|
2681
|
+
this.stopCancelWatch();
|
|
2682
|
+
this.clearFlushTimer();
|
|
2683
|
+
this.forceFinalProgress();
|
|
2684
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
2685
|
+
}
|
|
2686
|
+
reportFailure(error) {
|
|
2687
|
+
this.stopCancelWatch();
|
|
2688
|
+
this.clearFlushTimer();
|
|
2689
|
+
this.forceFinalProgress();
|
|
2690
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
2642
2691
|
}
|
|
2643
2692
|
/**
|
|
2644
|
-
*
|
|
2645
|
-
*
|
|
2646
|
-
* run não há onde pendurar a imagem — descarta com aviso único.
|
|
2693
|
+
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
2694
|
+
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
2647
2695
|
*/
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2696
|
+
takeLogsForTerminal() {
|
|
2697
|
+
let logs = this.takeLogs();
|
|
2698
|
+
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
2699
|
+
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
2700
|
+
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
2701
|
+
}
|
|
2702
|
+
return logs;
|
|
2654
2703
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2704
|
+
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
2705
|
+
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
2706
|
+
setProgress(current, total) {
|
|
2707
|
+
if (this.dead) return;
|
|
2708
|
+
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
2709
|
+
if (!this.progressWarned) {
|
|
2710
|
+
this.progressWarned = true;
|
|
2711
|
+
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
2712
|
+
}
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
2716
|
+
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
2717
|
+
const totalChanged = total !== this.progressTotal;
|
|
2718
|
+
if (next === this.progressCurrent && !totalChanged) return;
|
|
2719
|
+
this.progressCurrent = next;
|
|
2720
|
+
this.progressTotal = total;
|
|
2721
|
+
if (total == null) return;
|
|
2722
|
+
const now = Date.now();
|
|
2723
|
+
if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
|
|
2724
|
+
this.progressLastPrint = now;
|
|
2725
|
+
printRunProgress(next, total);
|
|
2726
|
+
}
|
|
2727
|
+
this.progressDirty = true;
|
|
2728
|
+
if (this.progressTimer) return;
|
|
2729
|
+
this.progressTimer = setTimeout(() => {
|
|
2730
|
+
this.progressTimer = void 0;
|
|
2731
|
+
this.flushProgress();
|
|
2732
|
+
}, PROGRESS_FLUSH_MS);
|
|
2733
|
+
unrefTimer(this.progressTimer);
|
|
2660
2734
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if (this.
|
|
2664
|
-
this.
|
|
2665
|
-
this.
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
this.
|
|
2670
|
-
this.
|
|
2671
|
-
|
|
2672
|
-
this.reason()
|
|
2673
|
-
);
|
|
2735
|
+
flushProgress() {
|
|
2736
|
+
this.clearProgressTimer();
|
|
2737
|
+
if (this.dead || !this.progressDirty) return;
|
|
2738
|
+
if (this.buffer.length > 0) this.flushLogs();
|
|
2739
|
+
else this.enqueue({ status: "running" });
|
|
2740
|
+
}
|
|
2741
|
+
clearProgressTimer() {
|
|
2742
|
+
if (this.progressTimer) {
|
|
2743
|
+
clearTimeout(this.progressTimer);
|
|
2744
|
+
this.progressTimer = void 0;
|
|
2745
|
+
}
|
|
2674
2746
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
|
|
2680
|
-
...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
|
|
2681
|
-
status: this.status,
|
|
2682
|
-
start_timestamp: this.startTimestamp,
|
|
2683
|
-
timestamp: this.endTimestamp ?? nowISO(),
|
|
2684
|
-
...this._data ? { data: this._data } : {}
|
|
2685
|
-
};
|
|
2747
|
+
/** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
|
|
2748
|
+
forceFinalProgress() {
|
|
2749
|
+
this.clearProgressTimer();
|
|
2750
|
+
this.progressDirty = this.progressTotal != null;
|
|
2686
2751
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
this.
|
|
2691
|
-
this.
|
|
2692
|
-
this.op = op;
|
|
2693
|
-
this.runId = runId;
|
|
2694
|
-
this.echo = options.echo ?? false;
|
|
2695
|
-
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2752
|
+
/** Consome o snapshot pendente para pegar carona no próximo PATCH. */
|
|
2753
|
+
takeProgress() {
|
|
2754
|
+
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
2755
|
+
this.progressDirty = false;
|
|
2756
|
+
return { current: this.progressCurrent, total: this.progressTotal };
|
|
2696
2757
|
}
|
|
2697
|
-
client;
|
|
2698
|
-
name;
|
|
2699
|
-
op;
|
|
2700
|
-
runId;
|
|
2701
|
-
eventId = uuid4();
|
|
2702
|
-
stepId = stepId();
|
|
2703
|
-
startTimestamp = nowISO();
|
|
2704
|
-
status = "ok";
|
|
2705
|
-
steps = [];
|
|
2706
|
-
finished = false;
|
|
2707
|
-
lifecycleSink;
|
|
2708
|
-
attachmentSink;
|
|
2709
|
-
warnedNoAttachmentSink = false;
|
|
2710
|
-
// instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
|
|
2711
|
-
stepGuard;
|
|
2712
|
-
// modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
|
|
2713
|
-
echo;
|
|
2714
|
-
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
2715
|
-
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
2716
|
-
lifecycleBacklog = [];
|
|
2717
|
-
// ── Lifecycle / eco ───────────────────────────────────────────────────────
|
|
2718
2758
|
/**
|
|
2719
|
-
*
|
|
2720
|
-
*
|
|
2721
|
-
*
|
|
2722
|
-
* etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
|
|
2759
|
+
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
2760
|
+
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
2761
|
+
* mata o reporter e dispara ctx.signal, mesmo em handlers que nunca logam.
|
|
2723
2762
|
*/
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
}
|
|
2732
|
-
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
2733
|
-
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
2734
|
-
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
2735
|
-
else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
|
|
2763
|
+
startCancelWatch() {
|
|
2764
|
+
if (this.watchTimer) return;
|
|
2765
|
+
this.watchTimer = setInterval(() => {
|
|
2766
|
+
if (this.dead || this.pendingUpdates > 0 || this.buffer.length > 0) return;
|
|
2767
|
+
this.enqueue({ status: "running" });
|
|
2768
|
+
}, this.cancelCheckIntervalMs);
|
|
2769
|
+
unrefTimer(this.watchTimer);
|
|
2736
2770
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
for (const [level, message] of backlog) sink(level, message);
|
|
2771
|
+
stopCancelWatch() {
|
|
2772
|
+
if (this.watchTimer) {
|
|
2773
|
+
clearInterval(this.watchTimer);
|
|
2774
|
+
this.watchTimer = void 0;
|
|
2775
|
+
}
|
|
2743
2776
|
}
|
|
2744
|
-
/**
|
|
2745
|
-
|
|
2746
|
-
this.
|
|
2777
|
+
/** Aguarda a fila de PATCHes esvaziar (chame depois do terminal). */
|
|
2778
|
+
finalize() {
|
|
2779
|
+
return this.chain;
|
|
2747
2780
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2781
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
2782
|
+
/**
|
|
2783
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
2784
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
2785
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
2786
|
+
*/
|
|
2787
|
+
addItem(status, message, payload, id, image3) {
|
|
2788
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
2789
|
+
let scrubbedPayload;
|
|
2790
|
+
if (payload) {
|
|
2791
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
2792
|
+
let serializedLength;
|
|
2793
|
+
try {
|
|
2794
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
2795
|
+
} catch {
|
|
2796
|
+
}
|
|
2797
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
2798
|
+
this.logger.warn(
|
|
2799
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
2800
|
+
);
|
|
2801
|
+
scrubbedPayload = void 0;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const ts = nowISO();
|
|
2805
|
+
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2806
|
+
this.itemsBuffer.push({
|
|
2807
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
2808
|
+
seq: this.itemSeq++,
|
|
2809
|
+
status,
|
|
2810
|
+
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
2811
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
2812
|
+
ts,
|
|
2813
|
+
...attachmentId ? { attachments: [attachmentId] } : {}
|
|
2814
|
+
});
|
|
2815
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
2816
|
+
this.flushItems();
|
|
2817
|
+
} else {
|
|
2818
|
+
this.scheduleItemsFlush();
|
|
2754
2819
|
}
|
|
2755
|
-
return void 0;
|
|
2756
2820
|
}
|
|
2757
|
-
// ── Etapas ────────────────────────────────────────────────────────────────
|
|
2758
2821
|
/**
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2822
|
+
* Ids já concluídos (`succeeded`/`occurrence`) neste trabalho — a run e suas reexecuções.
|
|
2823
|
+
* Pagina até o fim. undefined quando a consulta falha (servidor antigo, rede): o `each`
|
|
2824
|
+
* avisa e processa tudo, nunca trava a run por isso.
|
|
2761
2825
|
*/
|
|
2762
|
-
|
|
2763
|
-
|
|
2826
|
+
async listDoneItemIds() {
|
|
2827
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2828
|
+
let cursor = null;
|
|
2829
|
+
try {
|
|
2830
|
+
do {
|
|
2831
|
+
const page = await this.api.listDoneItems(this.runId, {
|
|
2832
|
+
limit: MAX_ITEMS_DONE_PER_PAGE,
|
|
2833
|
+
...cursor ? { cursor } : {}
|
|
2834
|
+
});
|
|
2835
|
+
for (const id of page.ids) ids.add(id);
|
|
2836
|
+
cursor = page.next;
|
|
2837
|
+
} while (cursor);
|
|
2838
|
+
return ids;
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
this.log("warning", `n\xE3o foi poss\xEDvel consultar os itens j\xE1 conclu\xEDdos deste trabalho \u2014 todos ser\xE3o processados (${thrownMessage(error)})`);
|
|
2841
|
+
return void 0;
|
|
2842
|
+
}
|
|
2764
2843
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2844
|
+
scheduleItemsFlush() {
|
|
2845
|
+
if (this.itemsTimer) return;
|
|
2846
|
+
this.itemsTimer = setTimeout(() => {
|
|
2847
|
+
this.itemsTimer = void 0;
|
|
2848
|
+
this.flushItems();
|
|
2849
|
+
}, this.flushIntervalMs);
|
|
2850
|
+
unrefTimer(this.itemsTimer);
|
|
2768
2851
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2852
|
+
clearItemsTimer() {
|
|
2853
|
+
if (this.itemsTimer) {
|
|
2854
|
+
clearTimeout(this.itemsTimer);
|
|
2855
|
+
this.itemsTimer = void 0;
|
|
2856
|
+
}
|
|
2773
2857
|
}
|
|
2774
|
-
/**
|
|
2775
|
-
|
|
2776
|
-
this.
|
|
2777
|
-
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2858
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
2859
|
+
flushItems() {
|
|
2860
|
+
this.clearItemsTimer();
|
|
2861
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
2862
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
2863
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
2864
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
2865
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
async sendItems(batch) {
|
|
2869
|
+
if (this.itemsUnsupported) return;
|
|
2870
|
+
try {
|
|
2871
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
2872
|
+
this.itemsStorageFull = false;
|
|
2873
|
+
} catch (error) {
|
|
2874
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
2875
|
+
this.itemsUnsupported = true;
|
|
2876
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
2880
|
+
if (!this.itemsQuotaExceeded) {
|
|
2881
|
+
this.itemsQuotaExceeded = true;
|
|
2882
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
2883
|
+
}
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (error instanceof TasksApiError && error.status === 402) {
|
|
2887
|
+
if (!this.itemsStorageFull) {
|
|
2888
|
+
this.itemsStorageFull = true;
|
|
2889
|
+
this.log("warning", "itens descartados: armazenamento do plano cheio \u2014 libere espa\xE7o ou fa\xE7a upgrade em Plano e cobran\xE7a");
|
|
2890
|
+
}
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (error instanceof TasksApiError) {
|
|
2894
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
2895
|
+
this.log("warning", `${batch.length} item(ns) descartado(s): envio rejeitado pelo servidor (HTTP ${error.status})`);
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
this.itemsBuffer.unshift(...batch);
|
|
2899
|
+
this.scheduleItemsFlush();
|
|
2900
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
2905
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
2906
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
2907
|
+
*/
|
|
2908
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2909
|
+
this.flushItems();
|
|
2910
|
+
await Promise.race([
|
|
2911
|
+
this.itemsChain,
|
|
2912
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2913
|
+
]);
|
|
2914
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
2915
|
+
this.flushItems();
|
|
2916
|
+
await Promise.race([
|
|
2917
|
+
this.itemsChain,
|
|
2918
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2919
|
+
]);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2923
|
+
/**
|
|
2924
|
+
* Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
|
|
2925
|
+
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2926
|
+
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2927
|
+
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
2928
|
+
* caminho de erro do upload vira aviso. O hint `attachment` de `hobots.captureException`
|
|
2929
|
+
* também cai aqui via `log()`.
|
|
2930
|
+
*/
|
|
2931
|
+
queueAttachment(imageIn, ts) {
|
|
2932
|
+
if (this.dead) return void 0;
|
|
2933
|
+
const image3 = toImageAttachment(imageIn);
|
|
2934
|
+
if (this.attachmentsUnsupported) return void 0;
|
|
2935
|
+
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
2936
|
+
if (this.attachmentCount === MAX_ATTACHMENTS_PER_RUN) {
|
|
2937
|
+
this.attachmentCount++;
|
|
2938
|
+
this.log("warning", `imagem descartada: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
2939
|
+
}
|
|
2940
|
+
return void 0;
|
|
2941
|
+
}
|
|
2942
|
+
this.attachmentCount++;
|
|
2943
|
+
const id = uuid4();
|
|
2944
|
+
const rawCaption = image3.caption !== void 0 ? safeString(image3.caption) : void 0;
|
|
2945
|
+
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
2946
|
+
const work = this.uploadAttachment(id, image3.input, { caption, logTs: ts, contentType: image3.contentType }).then(() => void 0).catch(() => void 0);
|
|
2947
|
+
this.pendingAttachments.add(work);
|
|
2948
|
+
void work.finally(() => this.pendingAttachments.delete(work));
|
|
2949
|
+
return id;
|
|
2782
2950
|
}
|
|
2783
|
-
/**
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
* Resolve com o retorno de `fn`.
|
|
2787
|
-
*
|
|
2788
|
-
* Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
|
|
2789
|
-
* mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
|
|
2790
|
-
* sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
|
|
2791
|
-
*/
|
|
2792
|
-
async step(name, fn, options = {}) {
|
|
2793
|
-
const requested = options.retries ?? DEFAULT_STEP_RETRIES;
|
|
2794
|
-
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
|
|
2795
|
-
const step = this.start(void 0, safeString(name));
|
|
2796
|
-
let attempts = 0;
|
|
2797
|
-
let lastFailure;
|
|
2951
|
+
/** Retorna o motivo da falha, ou undefined em sucesso. */
|
|
2952
|
+
async uploadAttachment(id, image3, meta) {
|
|
2953
|
+
let bytes;
|
|
2798
2954
|
try {
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2955
|
+
bytes = await toBytes(image3);
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
this.log("warning", `imagem n\xE3o p\xF4de ser lida: ${String(error)}`);
|
|
2958
|
+
return "read-failed";
|
|
2959
|
+
}
|
|
2960
|
+
const contentType = sniffContentType(bytes) ?? meta.contentType;
|
|
2961
|
+
if (!contentType) {
|
|
2962
|
+
this.log("warning", "imagem descartada: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
|
|
2963
|
+
return "unsupported-format";
|
|
2964
|
+
}
|
|
2965
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
2966
|
+
this.log(
|
|
2967
|
+
"warning",
|
|
2968
|
+
`imagem descartada: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
|
|
2969
|
+
);
|
|
2970
|
+
return "too-large";
|
|
2971
|
+
}
|
|
2972
|
+
const dimensions = readPngDimensions(bytes);
|
|
2973
|
+
const release = await this.acquireUploadSlot();
|
|
2974
|
+
try {
|
|
2975
|
+
if (this.abortController.signal.aborted || this.dead) return "canceled";
|
|
2976
|
+
const created = await this.api.createAttachment(
|
|
2977
|
+
this.runId,
|
|
2804
2978
|
{
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
);
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2979
|
+
id,
|
|
2980
|
+
content_type: contentType,
|
|
2981
|
+
size_bytes: bytes.byteLength,
|
|
2982
|
+
log_ts: meta.logTs,
|
|
2983
|
+
...meta.caption ? { caption: meta.caption } : {},
|
|
2984
|
+
...dimensions ?? {}
|
|
2985
|
+
},
|
|
2986
|
+
this.abortController.signal
|
|
2816
2987
|
);
|
|
2817
|
-
this.
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
step._close();
|
|
2822
|
-
return result;
|
|
2988
|
+
await this.api.uploadAttachment(created.upload_url, created.headers, bytes, this.abortController.signal);
|
|
2989
|
+
await this.api.confirmAttachment(this.runId, id, { status: "uploaded", size_bytes: bytes.byteLength, ...dimensions ?? {} }).catch(() => {
|
|
2990
|
+
});
|
|
2991
|
+
return void 0;
|
|
2823
2992
|
} catch (error) {
|
|
2824
|
-
this.
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2993
|
+
if (this.isUnsupported(error)) {
|
|
2994
|
+
this.attachmentsUnsupported = true;
|
|
2995
|
+
this.log("warning", "anexos n\xE3o est\xE3o habilitados nesta instala\xE7\xE3o do Hobots \u2014 imagem n\xE3o enviada");
|
|
2996
|
+
return "unsupported";
|
|
2997
|
+
}
|
|
2998
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
2999
|
+
this.log("warning", "imagem descartada: limite de anexos por execu\xE7\xE3o atingido no servidor");
|
|
3000
|
+
return "quota";
|
|
3001
|
+
}
|
|
3002
|
+
if (this.abortController.signal.aborted) return "canceled";
|
|
3003
|
+
this.log("warning", `falha ao enviar a imagem: ${String(error)}`);
|
|
3004
|
+
void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
|
|
3005
|
+
});
|
|
3006
|
+
return "upload-failed";
|
|
3007
|
+
} finally {
|
|
3008
|
+
release();
|
|
2828
3009
|
}
|
|
2829
3010
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
3011
|
+
/** 404 = API anterior ao recurso; 501/503 = instalação sem S3 configurado. */
|
|
3012
|
+
isUnsupported(error) {
|
|
3013
|
+
return error instanceof TasksApiError && (error.status === 404 || error.status === 501 || error.status === 503);
|
|
2832
3014
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3015
|
+
/** Semáforo simples: no máximo MAX_CONCURRENT_UPLOADS binários em voo por run. */
|
|
3016
|
+
acquireUploadSlot() {
|
|
3017
|
+
const release = () => {
|
|
3018
|
+
const next = this.uploadQueue.shift();
|
|
3019
|
+
if (next) next();
|
|
3020
|
+
else this.uploadSlots++;
|
|
3021
|
+
};
|
|
3022
|
+
if (this.uploadSlots > 0) {
|
|
3023
|
+
this.uploadSlots--;
|
|
3024
|
+
return Promise.resolve(release);
|
|
3025
|
+
}
|
|
3026
|
+
return new Promise((resolve2) => {
|
|
3027
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
3028
|
+
});
|
|
2837
3029
|
}
|
|
2838
|
-
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
2839
3030
|
/**
|
|
2840
|
-
*
|
|
2841
|
-
*
|
|
2842
|
-
* `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
|
|
2843
|
-
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2844
|
-
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2845
|
-
* Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
|
|
3031
|
+
* Espera os uploads pendentes antes do PATCH terminal, para a run não terminar
|
|
3032
|
+
* exibindo miniaturas em "enviando…". Capado: um upload lento não segura a run.
|
|
2846
3033
|
*/
|
|
2847
|
-
|
|
2848
|
-
if (
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
3034
|
+
async finalizeAttachments(timeoutMs = ATTACHMENT_DRAIN_MS) {
|
|
3035
|
+
if (this.pendingAttachments.size === 0) return;
|
|
3036
|
+
await Promise.race([
|
|
3037
|
+
Promise.allSettled([...this.pendingAttachments]),
|
|
3038
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
3039
|
+
]);
|
|
3040
|
+
}
|
|
3041
|
+
enqueue(updateIn) {
|
|
3042
|
+
if (this.dead) {
|
|
3043
|
+
this.salvageLogs(updateIn.logs);
|
|
3044
|
+
return;
|
|
2852
3045
|
}
|
|
2853
|
-
|
|
2854
|
-
const
|
|
2855
|
-
const
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
remaining.push(step);
|
|
3046
|
+
this.clearProgressTimer();
|
|
3047
|
+
const progress = this.takeProgress();
|
|
3048
|
+
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
3049
|
+
this.pendingUpdates++;
|
|
3050
|
+
this.chain = this.chain.then(async () => {
|
|
3051
|
+
if (this.dead) {
|
|
3052
|
+
this.salvageLogs(update.logs);
|
|
3053
|
+
return;
|
|
2862
3054
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
tags: void 0,
|
|
2889
|
-
run_id: this.runId
|
|
2890
|
-
};
|
|
3055
|
+
const payload = update.logs && update.logs.length === 0 ? { ...update, logs: void 0 } : update;
|
|
3056
|
+
try {
|
|
3057
|
+
await this.api.updateRun(this.runId, payload);
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
if (error instanceof TasksApiError && (error.status === 409 || error.status === 404)) {
|
|
3060
|
+
this.dead = true;
|
|
3061
|
+
this.stopCancelWatch();
|
|
3062
|
+
this.abort();
|
|
3063
|
+
const pending = this.buffer.splice(0, this.buffer.length);
|
|
3064
|
+
this.salvageLogs(pending);
|
|
3065
|
+
this.logger.warn(
|
|
3066
|
+
`run ${this.runId} cancelada/finalizada no servidor \u2014 status suprimido` + (pending.length > 0 ? `, ${pending.length} log(s) enviados como log tardio` : "")
|
|
3067
|
+
);
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
if (update.logs && update.logs.length > 0) {
|
|
3071
|
+
this.buffer.unshift(...update.logs);
|
|
3072
|
+
this.scheduleFlush();
|
|
3073
|
+
}
|
|
3074
|
+
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
3075
|
+
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
3076
|
+
}
|
|
3077
|
+
}).finally(() => {
|
|
3078
|
+
this.pendingUpdates--;
|
|
3079
|
+
});
|
|
2891
3080
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
this.
|
|
2901
|
-
|
|
2902
|
-
endTimestamp,
|
|
2903
|
-
this.status
|
|
2904
|
-
);
|
|
2905
|
-
if (!this.client) return;
|
|
2906
|
-
const steps = this.steps;
|
|
2907
|
-
let first = 0;
|
|
2908
|
-
while (steps.length - first > MAX_STEPS) {
|
|
2909
|
-
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
2910
|
-
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2911
|
-
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
2912
|
-
first += MAX_STEPS;
|
|
3081
|
+
/**
|
|
3082
|
+
* Best-effort de log tardio: envia logs para uma run já terminal, fora da fila.
|
|
3083
|
+
* O servidor anexa os logs mesmo respondendo 409; erros são engolidos — este é
|
|
3084
|
+
* o último recurso, não um canal confiável.
|
|
3085
|
+
*/
|
|
3086
|
+
salvageLogs(logs) {
|
|
3087
|
+
if (!logs || logs.length === 0) return;
|
|
3088
|
+
for (let i = 0; i < logs.length; i += MAX_LOGS_PER_PATCH) {
|
|
3089
|
+
void this.api.updateRunOnce(this.runId, { status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) }).catch(() => {
|
|
3090
|
+
});
|
|
2913
3091
|
}
|
|
2914
|
-
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
2915
3092
|
}
|
|
2916
3093
|
};
|
|
2917
3094
|
|
|
@@ -3349,6 +3526,10 @@ var LocalRunTransport = class {
|
|
|
3349
3526
|
}
|
|
3350
3527
|
return { accepted: request.items.length, duplicates: 0 };
|
|
3351
3528
|
}
|
|
3529
|
+
// modo local não tem trabalho anterior: nada a pular
|
|
3530
|
+
async listDoneItems() {
|
|
3531
|
+
return { ids: [], next: null };
|
|
3532
|
+
}
|
|
3352
3533
|
/** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
|
|
3353
3534
|
printSummary() {
|
|
3354
3535
|
const { succeeded, occurrence, failed } = this.counts;
|
|
@@ -3870,7 +4051,13 @@ var fallbackEachSink = {
|
|
|
3870
4051
|
advance: noop,
|
|
3871
4052
|
checkCanceled: noop,
|
|
3872
4053
|
isAborted: () => false,
|
|
4054
|
+
/* v8 ignore start -- fora de uma run não há consulta de concluídos, logo nunca há item pulado para anunciar */
|
|
4055
|
+
logInfo: (message) => printRunLog("info", message),
|
|
4056
|
+
/* v8 ignore stop */
|
|
4057
|
+
logWarning: (message) => printRunLog("warning", message),
|
|
3873
4058
|
logError: (message) => printRunLog("error", message),
|
|
4059
|
+
sleep,
|
|
4060
|
+
doneItemIds: () => Promise.resolve(void 0),
|
|
3874
4061
|
flush: () => Promise.resolve()
|
|
3875
4062
|
};
|
|
3876
4063
|
var fallbackItems = {
|
|
@@ -3953,6 +4140,7 @@ var isFileRef2 = hobots.isFileRef;
|
|
|
3953
4140
|
loadDotenv();
|
|
3954
4141
|
var src_default = hobots;
|
|
3955
4142
|
export {
|
|
4143
|
+
Occurrence,
|
|
3956
4144
|
captureException2 as captureException,
|
|
3957
4145
|
close2 as close,
|
|
3958
4146
|
src_default as default,
|