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