hobots 4.5.0 → 5.0.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 +14 -11
- package/dist/cli.cjs +250 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +544 -222
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -250
- package/dist/index.d.ts +75 -250
- package/dist/index.js +544 -222
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +5 -2
- package/dist/protocol.d.ts +5 -2
- package/docs/README.md +2 -2
- package/docs/api-reference.md +21 -7
- package/docs/concepts.md +11 -11
- package/docs/getting-started.md +1 -1
- package/docs/protocol.md +5 -5
- package/docs/runs.md +97 -23
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -44,7 +44,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
44
44
|
|
|
45
45
|
// src/utils.ts
|
|
46
46
|
var SDK_NAME = "hobots";
|
|
47
|
-
var SDK_VERSION = true ? "
|
|
47
|
+
var SDK_VERSION = true ? "5.0.0" : "0.0.0-dev";
|
|
48
48
|
function isNode() {
|
|
49
49
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
50
50
|
}
|
|
@@ -97,8 +97,13 @@ function abortableSleep(ms, signal) {
|
|
|
97
97
|
function jitter(baseMs) {
|
|
98
98
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
99
99
|
}
|
|
100
|
+
var TRUNCATION_SUFFIX = "\u2026 [truncado]";
|
|
100
101
|
function truncate(value, max) {
|
|
101
|
-
return value.length > max ? `${value.slice(0, max)}
|
|
102
|
+
return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
|
|
103
|
+
}
|
|
104
|
+
function clip(value, max) {
|
|
105
|
+
if (value.length <= max) return value;
|
|
106
|
+
return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
|
|
102
107
|
}
|
|
103
108
|
function safeString(value) {
|
|
104
109
|
if (typeof value === "string") return value;
|
|
@@ -196,6 +201,8 @@ function colorize(text, color) {
|
|
|
196
201
|
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
197
202
|
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
198
203
|
var RUN_ID_ENV = "HOBOTS_RUN_ID";
|
|
204
|
+
var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
|
|
205
|
+
var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
|
|
199
206
|
var dotenvLoaded = false;
|
|
200
207
|
function loadDotenv() {
|
|
201
208
|
if (dotenvLoaded) return;
|
|
@@ -221,6 +228,24 @@ function resolveRunId(option) {
|
|
|
221
228
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
222
229
|
return process.env[RUN_ID_ENV]?.trim() || void 0;
|
|
223
230
|
}
|
|
231
|
+
function isLocalRun() {
|
|
232
|
+
if (typeof process === "undefined" || !process.env) return false;
|
|
233
|
+
return process.env[LOCAL_RUN_ENV]?.trim() === "1";
|
|
234
|
+
}
|
|
235
|
+
function resolveLocalRunParams() {
|
|
236
|
+
const raw = process.env[RUN_PARAMS_ENV]?.trim();
|
|
237
|
+
if (!raw) return {};
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = JSON.parse(raw);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
|
|
243
|
+
}
|
|
244
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
245
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
|
|
246
|
+
}
|
|
247
|
+
return parsed;
|
|
248
|
+
}
|
|
224
249
|
function resolveEnvironmentOption(option) {
|
|
225
250
|
const fromOption = option?.trim();
|
|
226
251
|
if (fromOption) return fromOption;
|
|
@@ -460,9 +485,18 @@ var MAX_FINGERPRINT_ITEMS = 10;
|
|
|
460
485
|
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
461
486
|
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
462
487
|
var MAX_STEPS = 200;
|
|
488
|
+
var MAX_STEP_DESCRIPTION_CHARS = 2048;
|
|
489
|
+
var MAX_STEP_OP_CHARS = 255;
|
|
463
490
|
function clipTags(tags) {
|
|
464
491
|
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
465
492
|
}
|
|
493
|
+
function clipStep(step) {
|
|
494
|
+
return {
|
|
495
|
+
...step,
|
|
496
|
+
...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
|
|
497
|
+
...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
466
500
|
function clipFingerprint(fingerprint) {
|
|
467
501
|
if (fingerprint === void 0) return void 0;
|
|
468
502
|
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
@@ -521,12 +555,13 @@ var Client = class {
|
|
|
521
555
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
522
556
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
523
557
|
}
|
|
524
|
-
|
|
558
|
+
/** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
|
|
559
|
+
captureException(error, hint, contexts) {
|
|
525
560
|
if (typeof error === "object" && error !== null) captured.add(error);
|
|
526
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
561
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
|
|
527
562
|
}
|
|
528
|
-
captureEvent(event, hint) {
|
|
529
|
-
const final = this.prepareEvent(event, hint);
|
|
563
|
+
captureEvent(event, hint, contexts) {
|
|
564
|
+
const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
|
|
530
565
|
this.transport.enqueue({ type: "event", payload: final });
|
|
531
566
|
return final.event_id;
|
|
532
567
|
}
|
|
@@ -549,14 +584,15 @@ var Client = class {
|
|
|
549
584
|
sendTransaction(payload) {
|
|
550
585
|
let steps = payload.steps;
|
|
551
586
|
if (steps.length > MAX_STEPS) {
|
|
552
|
-
this.logger.warn(`transaction com ${steps.length}
|
|
587
|
+
this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
553
588
|
steps = steps.slice(0, MAX_STEPS);
|
|
554
589
|
}
|
|
590
|
+
steps = steps.map(clipStep);
|
|
555
591
|
this.transport.enqueue({
|
|
556
592
|
type: "transaction",
|
|
557
593
|
payload: {
|
|
558
594
|
...payload,
|
|
559
|
-
name:
|
|
595
|
+
name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
560
596
|
steps,
|
|
561
597
|
release: payload.release ?? this.options.release,
|
|
562
598
|
environment: this.resolveEnvironment(payload.environment),
|
|
@@ -1356,11 +1392,11 @@ var ItemHandle = class {
|
|
|
1356
1392
|
}
|
|
1357
1393
|
failed(message, payload, options) {
|
|
1358
1394
|
this.record("failed", message, payload, options?.image);
|
|
1359
|
-
if (this.step && this.step.status !== "error") this.step.
|
|
1395
|
+
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
1360
1396
|
}
|
|
1361
1397
|
occurrence(message, payload, options) {
|
|
1362
1398
|
this.record("occurrence", message, payload, options?.image);
|
|
1363
|
-
if (this.step && this.step.status === "ok") this.step.
|
|
1399
|
+
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
1364
1400
|
}
|
|
1365
1401
|
record(status, message, payload, image3) {
|
|
1366
1402
|
this.reported = true;
|
|
@@ -1391,23 +1427,24 @@ async function eachItem(list, handler, options, sink) {
|
|
|
1391
1427
|
const step = sink.startStep(options.op ?? "item", description);
|
|
1392
1428
|
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1393
1429
|
try {
|
|
1394
|
-
await handler(element, handle, index);
|
|
1430
|
+
await sink.stepScope(step, async () => handler(element, handle, index));
|
|
1395
1431
|
if (!handle.reported) handle.succeeded();
|
|
1396
1432
|
} catch (error) {
|
|
1397
1433
|
if (isTerminalError(error) || sink.isAborted()) {
|
|
1398
|
-
step?.
|
|
1434
|
+
step?._setStatus("cancelled")._close();
|
|
1399
1435
|
throw error;
|
|
1400
1436
|
}
|
|
1401
1437
|
const message = thrownMessage(error);
|
|
1402
1438
|
if (!handle.reported) handle.failed(message);
|
|
1403
|
-
step?.
|
|
1439
|
+
step?._failed(error);
|
|
1404
1440
|
sink.logError(`item ${description} falhou: ${message}`);
|
|
1405
1441
|
}
|
|
1406
|
-
step?.
|
|
1442
|
+
step?._close();
|
|
1407
1443
|
if (options.progress !== false) sink.advance();
|
|
1408
1444
|
result.total += 1;
|
|
1409
1445
|
result[handle.status ?? "succeeded"] += 1;
|
|
1410
1446
|
index += 1;
|
|
1447
|
+
if (options.flush !== false) await sink.flush();
|
|
1411
1448
|
}
|
|
1412
1449
|
return result;
|
|
1413
1450
|
}
|
|
@@ -1447,6 +1484,11 @@ function isInsideDir(root, candidate, sep) {
|
|
|
1447
1484
|
function unavailableLocal(ref) {
|
|
1448
1485
|
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
1449
1486
|
}
|
|
1487
|
+
function localRunPath(ref) {
|
|
1488
|
+
if (!isLocalRun()) return void 0;
|
|
1489
|
+
const path = ref.path;
|
|
1490
|
+
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
1491
|
+
}
|
|
1450
1492
|
function errorCode(error) {
|
|
1451
1493
|
return error?.code;
|
|
1452
1494
|
}
|
|
@@ -1614,8 +1656,16 @@ var RunFileStore = class {
|
|
|
1614
1656
|
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
1615
1657
|
}
|
|
1616
1658
|
}
|
|
1617
|
-
/**
|
|
1659
|
+
/**
|
|
1660
|
+
* storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
|
|
1661
|
+
* origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
|
|
1662
|
+
*/
|
|
1618
1663
|
async copyLocal(ref, target) {
|
|
1664
|
+
const direct = localRunPath(ref);
|
|
1665
|
+
if (direct !== void 0) {
|
|
1666
|
+
await this.copyFromPath(ref, direct, target);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1619
1669
|
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
1620
1670
|
if (!base) throw unavailableLocal(ref);
|
|
1621
1671
|
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
@@ -1649,6 +1699,26 @@ var RunFileStore = class {
|
|
|
1649
1699
|
}
|
|
1650
1700
|
await rm(folder, { recursive: true, force: true });
|
|
1651
1701
|
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
|
|
1704
|
+
* Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
|
|
1705
|
+
*/
|
|
1706
|
+
async copyFromPath(ref, path, target) {
|
|
1707
|
+
const { copyFile, rename, rm, stat } = await import("fs/promises");
|
|
1708
|
+
const { resolve: resolve2 } = await import("path");
|
|
1709
|
+
const source = resolve2(path);
|
|
1710
|
+
const info = await stat(source).catch(() => void 0);
|
|
1711
|
+
if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
|
|
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
|
+
}
|
|
1721
|
+
}
|
|
1652
1722
|
translate(error, ref) {
|
|
1653
1723
|
if (this.deps.signal.aborted) return runCanceledError();
|
|
1654
1724
|
if (error instanceof TasksApiError && error.status === 410) {
|
|
@@ -1777,6 +1847,11 @@ function printRunLog(level, message) {
|
|
|
1777
1847
|
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1778
1848
|
console[method2](line, message);
|
|
1779
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
|
+
}
|
|
1780
1855
|
function cancelableSleep(ms, signal) {
|
|
1781
1856
|
return new Promise((resolve2, reject) => {
|
|
1782
1857
|
if (signal.aborted) {
|
|
@@ -1846,13 +1921,15 @@ var RunReporter = class {
|
|
|
1846
1921
|
const eachSink = {
|
|
1847
1922
|
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
1848
1923
|
startStep: (op, description) => tx.start(op, description),
|
|
1924
|
+
stepScope: (step, fn) => tx.scope(step, fn),
|
|
1849
1925
|
declareTotal: (total) => {
|
|
1850
1926
|
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
1851
1927
|
},
|
|
1852
1928
|
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
1853
1929
|
checkCanceled,
|
|
1854
1930
|
isAborted: () => this.abortController.signal.aborted,
|
|
1855
|
-
logError: (message) => this.log("error", message)
|
|
1931
|
+
logError: (message) => this.log("error", message),
|
|
1932
|
+
flush: () => this.flush()
|
|
1856
1933
|
};
|
|
1857
1934
|
const items = {
|
|
1858
1935
|
add: (item) => {
|
|
@@ -1907,7 +1984,14 @@ var RunReporter = class {
|
|
|
1907
1984
|
items,
|
|
1908
1985
|
progress,
|
|
1909
1986
|
files: this.fileStore.files,
|
|
1910
|
-
|
|
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
|
+
},
|
|
1911
1995
|
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1912
1996
|
signal,
|
|
1913
1997
|
throwIfCanceled: checkCanceled,
|
|
@@ -1970,6 +2054,8 @@ var RunReporter = class {
|
|
|
1970
2054
|
progressDirty = false;
|
|
1971
2055
|
progressTimer;
|
|
1972
2056
|
progressWarned = false;
|
|
2057
|
+
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
2058
|
+
progressLastPrint = 0;
|
|
1973
2059
|
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1974
2060
|
fileStore;
|
|
1975
2061
|
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
@@ -2020,15 +2106,17 @@ var RunReporter = class {
|
|
|
2020
2106
|
/**
|
|
2021
2107
|
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
2022
2108
|
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
2023
|
-
* intacta e o handler livre da latência do upload.
|
|
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.
|
|
2024
2111
|
*/
|
|
2025
2112
|
log(level, message, image3) {
|
|
2026
|
-
if (this.dead) return;
|
|
2113
|
+
if (this.dead) return void 0;
|
|
2027
2114
|
const scrubbed = scrubSecrets(safeString(message));
|
|
2028
2115
|
const ts = nowISO();
|
|
2029
2116
|
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
2030
2117
|
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
2031
2118
|
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2119
|
+
return attachmentId;
|
|
2032
2120
|
}
|
|
2033
2121
|
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
2034
2122
|
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
@@ -2074,7 +2162,7 @@ var RunReporter = class {
|
|
|
2074
2162
|
}
|
|
2075
2163
|
}
|
|
2076
2164
|
/**
|
|
2077
|
-
* `hobots.flush()`: o bot decide quando entregar o que já fez —
|
|
2165
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
|
|
2078
2166
|
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2079
2167
|
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2080
2168
|
*/
|
|
@@ -2132,10 +2220,16 @@ var RunReporter = class {
|
|
|
2132
2220
|
}
|
|
2133
2221
|
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
2134
2222
|
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
2135
|
-
|
|
2223
|
+
const totalChanged = total !== this.progressTotal;
|
|
2224
|
+
if (next === this.progressCurrent && !totalChanged) return;
|
|
2136
2225
|
this.progressCurrent = next;
|
|
2137
2226
|
this.progressTotal = total;
|
|
2138
2227
|
if (total == null) return;
|
|
2228
|
+
const now = Date.now();
|
|
2229
|
+
if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
|
|
2230
|
+
this.progressLastPrint = now;
|
|
2231
|
+
printRunProgress(next, total);
|
|
2232
|
+
}
|
|
2139
2233
|
this.progressDirty = true;
|
|
2140
2234
|
if (this.progressTimer) return;
|
|
2141
2235
|
this.progressTimer = setTimeout(() => {
|
|
@@ -2310,7 +2404,7 @@ var RunReporter = class {
|
|
|
2310
2404
|
}
|
|
2311
2405
|
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2312
2406
|
/**
|
|
2313
|
-
* Pendura uma imagem num evento da run (log, item ou
|
|
2407
|
+
* Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
|
|
2314
2408
|
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2315
2409
|
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2316
2410
|
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
@@ -2483,11 +2577,13 @@ var RunReporter = class {
|
|
|
2483
2577
|
|
|
2484
2578
|
// src/tasks/run-context-store.ts
|
|
2485
2579
|
var storage;
|
|
2580
|
+
var stepStorage;
|
|
2486
2581
|
var initPromise;
|
|
2487
2582
|
async function ensureStorage() {
|
|
2488
2583
|
if (storage || !isNode()) return;
|
|
2489
2584
|
initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
|
|
2490
2585
|
storage = new ALS();
|
|
2586
|
+
stepStorage = new ALS();
|
|
2491
2587
|
});
|
|
2492
2588
|
await initPromise;
|
|
2493
2589
|
}
|
|
@@ -2498,12 +2594,23 @@ async function runWithRunContext(store, fn) {
|
|
|
2498
2594
|
function currentRunReporter() {
|
|
2499
2595
|
return storage?.getStore()?.reporter;
|
|
2500
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
|
+
}
|
|
2501
2605
|
|
|
2502
2606
|
// src/transaction.ts
|
|
2503
2607
|
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
2608
|
+
var DEFAULT_STEP_RETRIES = 1;
|
|
2609
|
+
var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
|
|
2504
2610
|
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2611
|
+
var MAX_BACKLOG = 100;
|
|
2505
2612
|
function stepMessage(text) {
|
|
2506
|
-
return
|
|
2613
|
+
return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
2507
2614
|
}
|
|
2508
2615
|
function stepId() {
|
|
2509
2616
|
return uuid4().slice(0, 16);
|
|
@@ -2511,6 +2618,13 @@ function stepId() {
|
|
|
2511
2618
|
function stepLabel(op, description) {
|
|
2512
2619
|
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
2513
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
|
+
}
|
|
2514
2628
|
function levelForStatus(status) {
|
|
2515
2629
|
switch (status) {
|
|
2516
2630
|
case "warning":
|
|
@@ -2550,69 +2664,68 @@ var Step = class {
|
|
|
2550
2664
|
startTimestamp = nowISO();
|
|
2551
2665
|
status = "ok";
|
|
2552
2666
|
endTimestamp;
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
* `options.image` pendura um print do estado da tela em `data.attachments`.
|
|
2558
|
-
*/
|
|
2559
|
-
setStatus(status, message, options) {
|
|
2560
|
-
this.status = status;
|
|
2561
|
-
if (message) this.setData("message", stepMessage(message));
|
|
2667
|
+
_data;
|
|
2668
|
+
// ── API do bot ────────────────────────────────────────────────────────────
|
|
2669
|
+
warning(message, options) {
|
|
2670
|
+
if (this.status !== "error") this._setStatus("warning", message);
|
|
2562
2671
|
if (options?.image !== void 0) this.attachImage(options.image);
|
|
2563
2672
|
return this;
|
|
2564
2673
|
}
|
|
2674
|
+
image(image3) {
|
|
2675
|
+
this.attachImage(image3);
|
|
2676
|
+
return this;
|
|
2677
|
+
}
|
|
2678
|
+
data(key, value) {
|
|
2679
|
+
if (key === "attachments") return this;
|
|
2680
|
+
this._setData(key, value);
|
|
2681
|
+
return this;
|
|
2682
|
+
}
|
|
2683
|
+
// ── Internos (chamados pelo SDK) ──────────────────────────────────────────
|
|
2684
|
+
/** `data` da etapa como vai no wire (undefined quando vazio). */
|
|
2685
|
+
get payload() {
|
|
2686
|
+
return this._data;
|
|
2687
|
+
}
|
|
2688
|
+
_setStatus(status, message) {
|
|
2689
|
+
this.status = status;
|
|
2690
|
+
if (message) this._setData("message", stepMessage(safeString(message)));
|
|
2691
|
+
return this;
|
|
2692
|
+
}
|
|
2565
2693
|
/**
|
|
2566
|
-
* Marca
|
|
2567
|
-
*
|
|
2568
|
-
* `options.image` pendura um print do estado da tela em `data.attachments`.
|
|
2569
|
-
* Numa step já fechada não faz nada.
|
|
2694
|
+
* Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
|
|
2695
|
+
* valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
|
|
2570
2696
|
*/
|
|
2571
|
-
|
|
2697
|
+
_failed(error, message) {
|
|
2572
2698
|
if (this.endTimestamp) return this;
|
|
2573
2699
|
this.status = "error";
|
|
2574
2700
|
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
2575
|
-
const raw = message ?? (error
|
|
2576
|
-
this.
|
|
2577
|
-
|
|
2578
|
-
this.close();
|
|
2701
|
+
const raw = message ?? errorText(error);
|
|
2702
|
+
this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
|
|
2703
|
+
this._close();
|
|
2579
2704
|
return this;
|
|
2580
2705
|
}
|
|
2706
|
+
_setData(key, value) {
|
|
2707
|
+
this._data = { ...this._data, [key]: value };
|
|
2708
|
+
}
|
|
2581
2709
|
/**
|
|
2582
2710
|
* Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
|
|
2583
2711
|
* `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
|
|
2584
2712
|
* run não há onde pendurar a imagem — descarta com aviso único.
|
|
2585
2713
|
*/
|
|
2586
2714
|
attachImage(image3) {
|
|
2587
|
-
const current = Array.isArray(this.
|
|
2715
|
+
const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
|
|
2588
2716
|
if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
|
|
2589
2717
|
const id = this.transaction.attachImage(image3);
|
|
2590
2718
|
if (!id) return;
|
|
2591
|
-
this.
|
|
2719
|
+
this._setData("attachments", [...current, id]);
|
|
2592
2720
|
}
|
|
2593
|
-
/** Motivo registrado
|
|
2721
|
+
/** Motivo registrado (warning/erro) — usado no log de FINISH. */
|
|
2594
2722
|
reason() {
|
|
2595
|
-
const error = this.
|
|
2596
|
-
const text = typeof error?.message === "string" ? error.message : this.
|
|
2723
|
+
const error = this._data?.error;
|
|
2724
|
+
const text = typeof error?.message === "string" ? error.message : this._data?.message;
|
|
2597
2725
|
return typeof text === "string" && text ? text : void 0;
|
|
2598
2726
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
return this;
|
|
2602
|
-
}
|
|
2603
|
-
/** Step aninhada (filha desta step). */
|
|
2604
|
-
start(op, description) {
|
|
2605
|
-
return this.transaction.createStep(this.stepId, op, description);
|
|
2606
|
-
}
|
|
2607
|
-
/**
|
|
2608
|
-
* Fecha a step. O status fica o que estiver (`ok` por padrão, ou o de `setStatus`).
|
|
2609
|
-
* Idempotente; a transaction fecha no fim da run o que ficou aberto.
|
|
2610
|
-
*/
|
|
2611
|
-
succeeded() {
|
|
2612
|
-
this.close();
|
|
2613
|
-
return this;
|
|
2614
|
-
}
|
|
2615
|
-
close() {
|
|
2727
|
+
/** Fecha a etapa mantendo o status que estiver. Idempotente. */
|
|
2728
|
+
_close() {
|
|
2616
2729
|
if (this.endTimestamp) return;
|
|
2617
2730
|
this.endTimestamp = nowISO();
|
|
2618
2731
|
this.transaction.emit(
|
|
@@ -2628,22 +2741,23 @@ var Step = class {
|
|
|
2628
2741
|
toJSON() {
|
|
2629
2742
|
return {
|
|
2630
2743
|
step_id: this.stepId,
|
|
2631
|
-
parent_step_id: this.parentStepId,
|
|
2632
|
-
op: this.op,
|
|
2633
|
-
description: this.description,
|
|
2744
|
+
...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
|
|
2745
|
+
...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
|
|
2746
|
+
...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
|
|
2634
2747
|
status: this.status,
|
|
2635
2748
|
start_timestamp: this.startTimestamp,
|
|
2636
2749
|
timestamp: this.endTimestamp ?? nowISO(),
|
|
2637
|
-
data: this.
|
|
2750
|
+
...this._data ? { data: this._data } : {}
|
|
2638
2751
|
};
|
|
2639
2752
|
}
|
|
2640
2753
|
};
|
|
2641
2754
|
var Transaction = class {
|
|
2642
|
-
constructor(client, name, op, runId) {
|
|
2755
|
+
constructor(client, name, op, runId, options = {}) {
|
|
2643
2756
|
this.client = client;
|
|
2644
2757
|
this.name = name;
|
|
2645
2758
|
this.op = op;
|
|
2646
2759
|
this.runId = runId;
|
|
2760
|
+
this.echo = options.echo ?? false;
|
|
2647
2761
|
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2648
2762
|
}
|
|
2649
2763
|
client;
|
|
@@ -2655,33 +2769,36 @@ var Transaction = class {
|
|
|
2655
2769
|
startTimestamp = nowISO();
|
|
2656
2770
|
status = "ok";
|
|
2657
2771
|
steps = [];
|
|
2658
|
-
tags = {};
|
|
2659
2772
|
finished = false;
|
|
2660
2773
|
lifecycleSink;
|
|
2661
2774
|
attachmentSink;
|
|
2662
2775
|
warnedNoAttachmentSink = false;
|
|
2663
|
-
// instalado pelo RunReporter: abrir uma
|
|
2776
|
+
// instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
|
|
2664
2777
|
stepGuard;
|
|
2778
|
+
// modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
|
|
2779
|
+
echo;
|
|
2665
2780
|
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
2666
2781
|
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
2667
2782
|
lifecycleBacklog = [];
|
|
2783
|
+
// ── Lifecycle / eco ───────────────────────────────────────────────────────
|
|
2668
2784
|
/**
|
|
2669
|
-
* Registra o início/fim de uma
|
|
2670
|
-
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2785
|
+
* Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
|
|
2786
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
|
|
2787
|
+
* run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
|
|
2788
|
+
* etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
|
|
2673
2789
|
*/
|
|
2674
2790
|
emit(kind, phase, label, startTs, endTs, status, reason) {
|
|
2675
2791
|
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
2676
2792
|
const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
|
|
2677
|
-
|
|
2793
|
+
const localFinish = this.echo && kind === "step" && phase === "finish";
|
|
2794
|
+
if (this.client?.debug || localFinish) {
|
|
2678
2795
|
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
2679
2796
|
console.log(`${tag} ${label}${suffix}`);
|
|
2680
2797
|
}
|
|
2681
2798
|
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
2682
2799
|
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
2683
2800
|
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
2684
|
-
else if (this.lifecycleBacklog.length <
|
|
2801
|
+
else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
|
|
2685
2802
|
}
|
|
2686
2803
|
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
2687
2804
|
setLifecycleSink(sink) {
|
|
@@ -2690,81 +2807,111 @@ var Transaction = class {
|
|
|
2690
2807
|
this.lifecycleBacklog = [];
|
|
2691
2808
|
for (const [level, message] of backlog) sink(level, message);
|
|
2692
2809
|
}
|
|
2693
|
-
/** Conecta o pipeline de anexos da run — é o que dá imagem às
|
|
2810
|
+
/** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
|
|
2694
2811
|
setAttachmentSink(sink) {
|
|
2695
2812
|
this.attachmentSink = sink;
|
|
2696
2813
|
}
|
|
2697
|
-
/** Imagem de uma
|
|
2814
|
+
/** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
|
|
2698
2815
|
attachImage(image3) {
|
|
2699
2816
|
if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
|
|
2700
2817
|
if (!this.warnedNoAttachmentSink) {
|
|
2701
2818
|
this.warnedNoAttachmentSink = true;
|
|
2702
|
-
console.warn("[hobots] imagem de
|
|
2819
|
+
console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
2703
2820
|
}
|
|
2704
2821
|
return void 0;
|
|
2705
2822
|
}
|
|
2823
|
+
// ── Etapas ────────────────────────────────────────────────────────────────
|
|
2706
2824
|
/**
|
|
2707
|
-
* Conecta a checagem de cancelamento chamada antes de cada
|
|
2708
|
-
*
|
|
2825
|
+
* Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
|
|
2826
|
+
* cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
|
|
2709
2827
|
*/
|
|
2710
2828
|
setStepGuard(guard) {
|
|
2711
2829
|
this.stepGuard = guard;
|
|
2712
2830
|
}
|
|
2713
|
-
|
|
2831
|
+
/** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
|
|
2832
|
+
currentStep() {
|
|
2833
|
+
return currentStep(this);
|
|
2834
|
+
}
|
|
2835
|
+
/** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
|
|
2836
|
+
scope(step, fn) {
|
|
2837
|
+
if (!step) return fn();
|
|
2838
|
+
return runWithStepScope({ transaction: this, step }, fn);
|
|
2839
|
+
}
|
|
2840
|
+
/** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
|
|
2841
|
+
start(op, description) {
|
|
2714
2842
|
this.stepGuard?.();
|
|
2715
|
-
const
|
|
2843
|
+
const parent = this.currentStep();
|
|
2844
|
+
const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
|
|
2716
2845
|
this.steps.push(step);
|
|
2717
2846
|
this.emit("step", "start", stepLabel(op, description));
|
|
2718
2847
|
return step;
|
|
2719
2848
|
}
|
|
2720
|
-
/** Step direta da transaction. `op` é o nome usado por `succeeded(name)`/`failed(error, name)`. */
|
|
2721
|
-
start(op, description) {
|
|
2722
|
-
return this.createStep(this.stepId, op, description);
|
|
2723
|
-
}
|
|
2724
2849
|
/**
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2850
|
+
* Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
|
|
2851
|
+
* tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
|
|
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.
|
|
2727
2857
|
*/
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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;
|
|
2864
|
+
try {
|
|
2865
|
+
const result = await retryWithBackoff(
|
|
2866
|
+
(attempt) => {
|
|
2867
|
+
attempts = attempt;
|
|
2868
|
+
return this.scope(step, async () => fn(step));
|
|
2869
|
+
},
|
|
2870
|
+
{
|
|
2871
|
+
retries: totalRetries,
|
|
2872
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
2873
|
+
signal: options.signal,
|
|
2874
|
+
wait: options.wait,
|
|
2875
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2876
|
+
lastFailure = { error };
|
|
2877
|
+
options.warn?.(
|
|
2878
|
+
`etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
);
|
|
2883
|
+
this.noteAttempts(step, attempts);
|
|
2884
|
+
if (lastFailure && step.status === "ok") {
|
|
2885
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
2886
|
+
}
|
|
2887
|
+
step._close();
|
|
2888
|
+
return result;
|
|
2889
|
+
} catch (error) {
|
|
2890
|
+
this.noteAttempts(step, attempts);
|
|
2891
|
+
if (isTerminalError(error)) step._setStatus("cancelled")._close();
|
|
2892
|
+
else step._failed(error);
|
|
2893
|
+
throw error;
|
|
2743
2894
|
}
|
|
2744
|
-
return void 0;
|
|
2745
2895
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
return this;
|
|
2896
|
+
noteAttempts(step, attempts) {
|
|
2897
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
2749
2898
|
}
|
|
2899
|
+
// ── Metadados ─────────────────────────────────────────────────────────────
|
|
2750
2900
|
setStatus(status) {
|
|
2751
2901
|
this.status = status;
|
|
2752
2902
|
return this;
|
|
2753
2903
|
}
|
|
2754
|
-
|
|
2755
|
-
this.name = name;
|
|
2756
|
-
return this;
|
|
2757
|
-
}
|
|
2904
|
+
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
2758
2905
|
/**
|
|
2759
|
-
* Envia já as
|
|
2906
|
+
* Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
|
|
2760
2907
|
* ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
|
|
2761
|
-
* `event_id` novo, até 200
|
|
2908
|
+
* `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
|
|
2762
2909
|
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2763
2910
|
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2764
|
-
* Devolve quantas
|
|
2911
|
+
* Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
|
|
2765
2912
|
*/
|
|
2766
2913
|
flush() {
|
|
2767
|
-
if (this.finished) return 0;
|
|
2914
|
+
if (!this.client || this.finished) return 0;
|
|
2768
2915
|
const ready = /* @__PURE__ */ new Set();
|
|
2769
2916
|
for (const step of this.steps) {
|
|
2770
2917
|
if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
|
|
@@ -2780,15 +2927,13 @@ var Transaction = class {
|
|
|
2780
2927
|
remaining.push(step);
|
|
2781
2928
|
}
|
|
2782
2929
|
}
|
|
2783
|
-
for (const step of batch) step.
|
|
2930
|
+
for (const step of batch) step._close();
|
|
2784
2931
|
this.steps.splice(0, this.steps.length, ...remaining);
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
2791
|
-
}
|
|
2932
|
+
for (let i = 0; i < batch.length; i += MAX_STEPS) {
|
|
2933
|
+
const chunk = batch.slice(i, i + MAX_STEPS);
|
|
2934
|
+
const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
|
|
2935
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2936
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
2792
2937
|
}
|
|
2793
2938
|
return batch.length;
|
|
2794
2939
|
}
|
|
@@ -2805,14 +2950,15 @@ var Transaction = class {
|
|
|
2805
2950
|
start_timestamp: startTimestamp,
|
|
2806
2951
|
timestamp,
|
|
2807
2952
|
steps: steps.map((step) => step.toJSON()),
|
|
2808
|
-
|
|
2953
|
+
// sem tags próprias: o client mescla as do processo
|
|
2954
|
+
tags: void 0,
|
|
2809
2955
|
run_id: this.runId
|
|
2810
2956
|
};
|
|
2811
2957
|
}
|
|
2812
2958
|
finish() {
|
|
2813
2959
|
if (this.finished) return;
|
|
2814
2960
|
this.finished = true;
|
|
2815
|
-
for (const step of this.steps) step.
|
|
2961
|
+
for (const step of this.steps) step._close();
|
|
2816
2962
|
const endTimestamp = nowISO();
|
|
2817
2963
|
this.emit(
|
|
2818
2964
|
"transaction",
|
|
@@ -2840,7 +2986,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
|
2840
2986
|
async function executeRun(input) {
|
|
2841
2987
|
const { run, handler, api, client, logger, tracker } = input;
|
|
2842
2988
|
const options = handler.options;
|
|
2843
|
-
const tx = new Transaction(client, "run", "hobots.run", run.id);
|
|
2989
|
+
const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
|
|
2844
2990
|
const reporter = new RunReporter(
|
|
2845
2991
|
api,
|
|
2846
2992
|
run.id,
|
|
@@ -3080,42 +3226,26 @@ var TaskAgent = class {
|
|
|
3080
3226
|
}
|
|
3081
3227
|
};
|
|
3082
3228
|
|
|
3083
|
-
// src/tasks/
|
|
3084
|
-
var
|
|
3085
|
-
|
|
3086
|
-
if (!(error instanceof TasksApiError)) return true;
|
|
3087
|
-
return error.status >= 500 || error.status === 429;
|
|
3088
|
-
}
|
|
3089
|
-
function describeRejection(runId, error) {
|
|
3090
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
3091
|
-
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3092
|
-
}
|
|
3093
|
-
if (error instanceof TasksApiError && error.status === 409) {
|
|
3094
|
-
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3095
|
-
}
|
|
3096
|
-
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3097
|
-
}
|
|
3098
|
-
var AttachedRunner = class {
|
|
3099
|
-
constructor(runId, handler, deps) {
|
|
3100
|
-
this.runId = runId;
|
|
3229
|
+
// src/tasks/single-run.ts
|
|
3230
|
+
var SingleRunRunner = class {
|
|
3231
|
+
constructor(handler, deps) {
|
|
3101
3232
|
this.handler = handler;
|
|
3102
3233
|
this.deps = deps;
|
|
3103
3234
|
this.done = new Promise((resolve2) => {
|
|
3104
3235
|
this.resolveDone = resolve2;
|
|
3105
3236
|
});
|
|
3106
3237
|
}
|
|
3107
|
-
runId;
|
|
3108
3238
|
handler;
|
|
3109
3239
|
deps;
|
|
3110
3240
|
abort = new AbortController();
|
|
3111
|
-
/** Resolve quando a run terminou (ou não pôde
|
|
3241
|
+
/** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
|
|
3112
3242
|
done;
|
|
3113
3243
|
resolveDone;
|
|
3114
3244
|
async run() {
|
|
3115
3245
|
try {
|
|
3116
|
-
const run = await this.
|
|
3246
|
+
const run = await this.openRun();
|
|
3117
3247
|
if (!run) return;
|
|
3118
|
-
this.deps.logger.debug(
|
|
3248
|
+
this.deps.logger.debug(this.opened(run));
|
|
3119
3249
|
await executeRun({
|
|
3120
3250
|
run,
|
|
3121
3251
|
handler: this.handler,
|
|
@@ -3130,11 +3260,37 @@ var AttachedRunner = class {
|
|
|
3130
3260
|
this.resolveDone();
|
|
3131
3261
|
}
|
|
3132
3262
|
}
|
|
3133
|
-
/** Desiste de
|
|
3263
|
+
/** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
|
|
3134
3264
|
stop() {
|
|
3135
3265
|
this.abort.abort();
|
|
3136
3266
|
}
|
|
3137
|
-
|
|
3267
|
+
};
|
|
3268
|
+
|
|
3269
|
+
// src/tasks/attached.ts
|
|
3270
|
+
var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
3271
|
+
function isTransient(error) {
|
|
3272
|
+
if (!(error instanceof TasksApiError)) return true;
|
|
3273
|
+
return error.status >= 500 || error.status === 429;
|
|
3274
|
+
}
|
|
3275
|
+
function describeRejection(runId, error) {
|
|
3276
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
3277
|
+
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3278
|
+
}
|
|
3279
|
+
if (error instanceof TasksApiError && error.status === 409) {
|
|
3280
|
+
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3281
|
+
}
|
|
3282
|
+
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3283
|
+
}
|
|
3284
|
+
var AttachedRunner = class extends SingleRunRunner {
|
|
3285
|
+
constructor(runId, handler, deps) {
|
|
3286
|
+
super(handler, deps);
|
|
3287
|
+
this.runId = runId;
|
|
3288
|
+
}
|
|
3289
|
+
runId;
|
|
3290
|
+
opened(run) {
|
|
3291
|
+
return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
|
|
3292
|
+
}
|
|
3293
|
+
async openRun() {
|
|
3138
3294
|
for (let attempt = 0; ; attempt++) {
|
|
3139
3295
|
if (this.abort.signal.aborted) return void 0;
|
|
3140
3296
|
try {
|
|
@@ -3197,46 +3353,16 @@ function normalizeItemColumns(input, logger) {
|
|
|
3197
3353
|
}
|
|
3198
3354
|
|
|
3199
3355
|
// src/tasks/local.ts
|
|
3200
|
-
var LocalRunner = class {
|
|
3356
|
+
var LocalRunner = class extends SingleRunRunner {
|
|
3357
|
+
backoff;
|
|
3201
3358
|
constructor(handler, deps) {
|
|
3202
|
-
|
|
3203
|
-
this.deps = deps;
|
|
3359
|
+
super(handler, deps);
|
|
3204
3360
|
this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
|
|
3205
|
-
this.done = new Promise((resolve2) => {
|
|
3206
|
-
this.resolveDone = resolve2;
|
|
3207
|
-
});
|
|
3208
3361
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
abort = new AbortController();
|
|
3212
|
-
backoff;
|
|
3213
|
-
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
3214
|
-
done;
|
|
3215
|
-
resolveDone;
|
|
3216
|
-
async run() {
|
|
3217
|
-
try {
|
|
3218
|
-
const run = await this.createRun();
|
|
3219
|
-
if (!run) return;
|
|
3220
|
-
this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
|
|
3221
|
-
await executeRun({
|
|
3222
|
-
run,
|
|
3223
|
-
handler: this.handler,
|
|
3224
|
-
api: this.deps.api,
|
|
3225
|
-
client: this.deps.client,
|
|
3226
|
-
logger: this.deps.logger,
|
|
3227
|
-
tracker: this.deps.tracker
|
|
3228
|
-
});
|
|
3229
|
-
} catch (error) {
|
|
3230
|
-
this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
|
|
3231
|
-
} finally {
|
|
3232
|
-
this.resolveDone();
|
|
3233
|
-
}
|
|
3362
|
+
opened(run) {
|
|
3363
|
+
return `run ${run.id} aberta (modo start)`;
|
|
3234
3364
|
}
|
|
3235
|
-
|
|
3236
|
-
stop() {
|
|
3237
|
-
this.abort.abort();
|
|
3238
|
-
}
|
|
3239
|
-
async createRun() {
|
|
3365
|
+
async openRun() {
|
|
3240
3366
|
for (; ; ) {
|
|
3241
3367
|
if (this.abort.signal.aborted) return void 0;
|
|
3242
3368
|
try {
|
|
@@ -3249,6 +3375,129 @@ var LocalRunner = class {
|
|
|
3249
3375
|
}
|
|
3250
3376
|
};
|
|
3251
3377
|
|
|
3378
|
+
// src/tasks/local-transport.ts
|
|
3379
|
+
var LOCAL_RUN_ID = "local";
|
|
3380
|
+
var ITEM_LINE_PREFIX = "[hobots:item]";
|
|
3381
|
+
var SUMMARY_LINE_PREFIX = "[hobots:summary]";
|
|
3382
|
+
var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
|
|
3383
|
+
function unavailable(name) {
|
|
3384
|
+
return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
|
|
3385
|
+
}
|
|
3386
|
+
function cell(value) {
|
|
3387
|
+
return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
|
|
3388
|
+
}
|
|
3389
|
+
var LocalRunTransport = class {
|
|
3390
|
+
constructor(params = {}, out = (line) => void process.stdout.write(`${line}
|
|
3391
|
+
`)) {
|
|
3392
|
+
this.params = params;
|
|
3393
|
+
this.out = out;
|
|
3394
|
+
}
|
|
3395
|
+
params;
|
|
3396
|
+
out;
|
|
3397
|
+
counts = { succeeded: 0, occurrence: 0, failed: 0 };
|
|
3398
|
+
warnedAttachments = false;
|
|
3399
|
+
/** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
|
|
3400
|
+
terminalStatus;
|
|
3401
|
+
async updateRun(_runId, update) {
|
|
3402
|
+
if (update.status === "running") return;
|
|
3403
|
+
this.terminalStatus = update.status;
|
|
3404
|
+
if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
|
|
3405
|
+
printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
|
|
3406
|
+
if (update.error.stack) console.error(update.error.stack);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
async updateRunOnce() {
|
|
3410
|
+
}
|
|
3411
|
+
async createItems(_runId, request) {
|
|
3412
|
+
for (const item of request.items) {
|
|
3413
|
+
this.counts[item.status]++;
|
|
3414
|
+
this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
|
|
3415
|
+
}
|
|
3416
|
+
return { accepted: request.items.length, duplicates: 0 };
|
|
3417
|
+
}
|
|
3418
|
+
/** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
|
|
3419
|
+
printSummary() {
|
|
3420
|
+
const { succeeded, occurrence, failed } = this.counts;
|
|
3421
|
+
this.out(
|
|
3422
|
+
`${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
async createAttachment(_runId, request) {
|
|
3426
|
+
if (!this.warnedAttachments) {
|
|
3427
|
+
this.warnedAttachments = true;
|
|
3428
|
+
printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
|
|
3429
|
+
}
|
|
3430
|
+
return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
|
|
3431
|
+
}
|
|
3432
|
+
async uploadAttachment() {
|
|
3433
|
+
}
|
|
3434
|
+
async confirmAttachment() {
|
|
3435
|
+
}
|
|
3436
|
+
/** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
|
|
3437
|
+
fileName(fileId) {
|
|
3438
|
+
return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
|
|
3439
|
+
}
|
|
3440
|
+
async requestFileDownload(_runId, fileId) {
|
|
3441
|
+
throw unavailable(this.fileName(fileId));
|
|
3442
|
+
}
|
|
3443
|
+
async fetchFile(url) {
|
|
3444
|
+
throw unavailable(url);
|
|
3445
|
+
}
|
|
3446
|
+
async confirmFileDownload(_runId, fileId) {
|
|
3447
|
+
throw unavailable(this.fileName(fileId));
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3450
|
+
|
|
3451
|
+
// src/tasks/local-run.ts
|
|
3452
|
+
var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
|
|
3453
|
+
var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
|
|
3454
|
+
var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
|
|
3455
|
+
var LocalRunRunner = class extends SingleRunRunner {
|
|
3456
|
+
constructor(params, handler, deps) {
|
|
3457
|
+
super(handler, deps);
|
|
3458
|
+
this.params = params;
|
|
3459
|
+
}
|
|
3460
|
+
params;
|
|
3461
|
+
canceled = false;
|
|
3462
|
+
startedAt = Date.now();
|
|
3463
|
+
finished = false;
|
|
3464
|
+
run() {
|
|
3465
|
+
this.startedAt = Date.now();
|
|
3466
|
+
return super.run();
|
|
3467
|
+
}
|
|
3468
|
+
opened() {
|
|
3469
|
+
return "run local iniciada (HOBOTS_LOCAL_RUN)";
|
|
3470
|
+
}
|
|
3471
|
+
async openRun() {
|
|
3472
|
+
if (this.abort.signal.aborted) return void 0;
|
|
3473
|
+
return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
|
|
3474
|
+
}
|
|
3475
|
+
/** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
|
|
3476
|
+
cancel() {
|
|
3477
|
+
this.canceled = true;
|
|
3478
|
+
this.deps.tracker.singleActive()?.abort();
|
|
3479
|
+
}
|
|
3480
|
+
/** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
|
|
3481
|
+
get outcome() {
|
|
3482
|
+
if (this.canceled) return "canceled";
|
|
3483
|
+
return this.deps.api.terminalStatus ?? "failed";
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
|
|
3487
|
+
* desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
|
|
3488
|
+
* esgota com o handler ainda rodando.
|
|
3489
|
+
*/
|
|
3490
|
+
finish() {
|
|
3491
|
+
const outcome = this.outcome;
|
|
3492
|
+
if (this.finished) return outcome;
|
|
3493
|
+
this.finished = true;
|
|
3494
|
+
this.deps.api.printSummary();
|
|
3495
|
+
const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
|
|
3496
|
+
printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
|
|
3497
|
+
return outcome;
|
|
3498
|
+
}
|
|
3499
|
+
};
|
|
3500
|
+
|
|
3252
3501
|
// src/tasks/registry.ts
|
|
3253
3502
|
var RunTracker = class {
|
|
3254
3503
|
constructor(api) {
|
|
@@ -3364,26 +3613,10 @@ function hostname() {
|
|
|
3364
3613
|
if (!isNode()) return void 0;
|
|
3365
3614
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
3366
3615
|
}
|
|
3367
|
-
function
|
|
3368
|
-
|
|
3369
|
-
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3370
|
-
const logger = makeLogger(options.debug ?? false);
|
|
3371
|
-
const environment = resolveEnvironmentOption(options.environment);
|
|
3372
|
-
const client = new Client(
|
|
3373
|
-
{
|
|
3374
|
-
environment,
|
|
3375
|
-
release: options.release,
|
|
3376
|
-
tags: options.tags,
|
|
3377
|
-
debug: options.debug,
|
|
3378
|
-
captureRunErrors: options.captureUnhandled !== false
|
|
3379
|
-
},
|
|
3380
|
-
config
|
|
3381
|
-
);
|
|
3382
|
-
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3383
|
-
const rt = {
|
|
3616
|
+
function baseRuntime(options, handler, logger, api) {
|
|
3617
|
+
return {
|
|
3384
3618
|
options: { ...options, clientSecret: void 0 },
|
|
3385
|
-
|
|
3386
|
-
client,
|
|
3619
|
+
client: void 0,
|
|
3387
3620
|
logger,
|
|
3388
3621
|
api,
|
|
3389
3622
|
handler,
|
|
@@ -3393,19 +3626,54 @@ function createRuntime(options, handler) {
|
|
|
3393
3626
|
agent: void 0,
|
|
3394
3627
|
local: void 0,
|
|
3395
3628
|
attached: void 0,
|
|
3629
|
+
localRun: void 0,
|
|
3396
3630
|
runId: void 0,
|
|
3397
3631
|
instanceId: resolveInstanceId(options.instanceId),
|
|
3398
|
-
environment,
|
|
3632
|
+
environment: void 0,
|
|
3399
3633
|
pollIntervalMs: void 0,
|
|
3400
3634
|
booted: false,
|
|
3401
3635
|
bootScheduled: false,
|
|
3402
3636
|
bootPromise: void 0,
|
|
3403
|
-
uninstallGlobalHandlers:
|
|
3637
|
+
uninstallGlobalHandlers: void 0,
|
|
3404
3638
|
uninstallShutdown: void 0
|
|
3405
3639
|
};
|
|
3640
|
+
}
|
|
3641
|
+
function createRuntime(options, handler, logger) {
|
|
3642
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3643
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
3644
|
+
const client = new Client(
|
|
3645
|
+
{
|
|
3646
|
+
environment,
|
|
3647
|
+
release: options.release,
|
|
3648
|
+
tags: options.tags,
|
|
3649
|
+
debug: options.debug,
|
|
3650
|
+
captureRunErrors: options.captureUnhandled !== false
|
|
3651
|
+
},
|
|
3652
|
+
config
|
|
3653
|
+
);
|
|
3654
|
+
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3655
|
+
const rt = baseRuntime(options, handler, logger, api);
|
|
3656
|
+
rt.client = client;
|
|
3657
|
+
rt.environment = environment;
|
|
3658
|
+
rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
|
|
3406
3659
|
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
3407
3660
|
return rt;
|
|
3408
3661
|
}
|
|
3662
|
+
function createLocalRunRuntime(options, handler, logger) {
|
|
3663
|
+
let params;
|
|
3664
|
+
try {
|
|
3665
|
+
params = resolveLocalRunParams();
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
printRunLog("error", error instanceof Error ? error.message : String(error));
|
|
3668
|
+
process.exit(2);
|
|
3669
|
+
throw error;
|
|
3670
|
+
}
|
|
3671
|
+
const transport = new LocalRunTransport(params);
|
|
3672
|
+
const rt = baseRuntime(options, handler, logger, transport);
|
|
3673
|
+
rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
|
|
3674
|
+
printRunLog("info", LOCAL_RUN_BANNER);
|
|
3675
|
+
return rt;
|
|
3676
|
+
}
|
|
3409
3677
|
function validateHandler(handler, fn) {
|
|
3410
3678
|
if (typeof handler !== "function") {
|
|
3411
3679
|
throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
|
|
@@ -3448,9 +3716,12 @@ function instanceInfo(rt) {
|
|
|
3448
3716
|
machine: rt.sampler?.sample()
|
|
3449
3717
|
};
|
|
3450
3718
|
}
|
|
3719
|
+
function platformApi(rt) {
|
|
3720
|
+
return rt.api;
|
|
3721
|
+
}
|
|
3451
3722
|
function ensureAgent(rt) {
|
|
3452
3723
|
rt.agent ??= new TaskAgent({
|
|
3453
|
-
api: rt
|
|
3724
|
+
api: platformApi(rt),
|
|
3454
3725
|
client: rt.client,
|
|
3455
3726
|
logger: rt.logger,
|
|
3456
3727
|
handler: rt.handler,
|
|
@@ -3462,7 +3733,7 @@ function ensureAgent(rt) {
|
|
|
3462
3733
|
}
|
|
3463
3734
|
function launchLocal(rt) {
|
|
3464
3735
|
const runner = new LocalRunner(rt.handler, {
|
|
3465
|
-
api: rt
|
|
3736
|
+
api: platformApi(rt),
|
|
3466
3737
|
client: rt.client,
|
|
3467
3738
|
logger: rt.logger,
|
|
3468
3739
|
tracker: rt.tracker,
|
|
@@ -3473,7 +3744,7 @@ function launchLocal(rt) {
|
|
|
3473
3744
|
}
|
|
3474
3745
|
function launchAttached(rt, runId) {
|
|
3475
3746
|
const runner = new AttachedRunner(runId, rt.handler, {
|
|
3476
|
-
api: rt
|
|
3747
|
+
api: platformApi(rt),
|
|
3477
3748
|
client: rt.client,
|
|
3478
3749
|
logger: rt.logger,
|
|
3479
3750
|
tracker: rt.tracker,
|
|
@@ -3485,7 +3756,43 @@ function launchAttached(rt, runId) {
|
|
|
3485
3756
|
if (runtime === rt) void close();
|
|
3486
3757
|
});
|
|
3487
3758
|
}
|
|
3759
|
+
function launchLocalRun(rt, runner) {
|
|
3760
|
+
let finished = false;
|
|
3761
|
+
const finish = () => {
|
|
3762
|
+
if (finished) return;
|
|
3763
|
+
finished = true;
|
|
3764
|
+
rt.uninstallShutdown?.();
|
|
3765
|
+
rt.uninstallShutdown = void 0;
|
|
3766
|
+
const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
|
|
3767
|
+
if (runtime === rt) void close(0);
|
|
3768
|
+
process.stdout.write("", () => process.exit(code));
|
|
3769
|
+
};
|
|
3770
|
+
if (rt.options.installExitHandlers !== false) {
|
|
3771
|
+
let signaled = false;
|
|
3772
|
+
const onSignal = (signal) => {
|
|
3773
|
+
if (externalListenerCount(signal) > 0 || signaled) return;
|
|
3774
|
+
signaled = true;
|
|
3775
|
+
runner.cancel();
|
|
3776
|
+
void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
|
|
3777
|
+
};
|
|
3778
|
+
const onInt = markInternal(() => onSignal("SIGINT"));
|
|
3779
|
+
const onTerm = markInternal(() => onSignal("SIGTERM"));
|
|
3780
|
+
process.on("SIGINT", onInt);
|
|
3781
|
+
process.on("SIGTERM", onTerm);
|
|
3782
|
+
rt.uninstallShutdown = () => {
|
|
3783
|
+
process.off("SIGINT", onInt);
|
|
3784
|
+
process.off("SIGTERM", onTerm);
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
void runner.run();
|
|
3788
|
+
void runner.done.then(finish);
|
|
3789
|
+
}
|
|
3488
3790
|
async function boot(rt) {
|
|
3791
|
+
if (rt.localRun) {
|
|
3792
|
+
rt.booted = true;
|
|
3793
|
+
launchLocalRun(rt, rt.localRun);
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3489
3796
|
if (!rt.instanceId) {
|
|
3490
3797
|
const id = await fallbackInstanceId();
|
|
3491
3798
|
if (runtime !== rt) return;
|
|
@@ -3522,9 +3829,12 @@ function startAgent(fn, mode, handler, options) {
|
|
|
3522
3829
|
}
|
|
3523
3830
|
const logger = makeLogger(options.debug ?? false);
|
|
3524
3831
|
const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
if (mode === "remote")
|
|
3832
|
+
loadDotenv();
|
|
3833
|
+
const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
|
|
3834
|
+
if (!rt.localRun && mode === "remote") {
|
|
3835
|
+
if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
|
|
3836
|
+
rt.runId = resolveRunId(options.runId);
|
|
3837
|
+
}
|
|
3528
3838
|
runtime = rt;
|
|
3529
3839
|
scheduleBoot(rt);
|
|
3530
3840
|
}
|
|
@@ -3547,10 +3857,10 @@ function forwardAttachment(rt, attachment, message, type) {
|
|
|
3547
3857
|
const reporter = currentReporter();
|
|
3548
3858
|
if (!reporter) {
|
|
3549
3859
|
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
|
|
3550
|
-
return;
|
|
3860
|
+
return void 0;
|
|
3551
3861
|
}
|
|
3552
3862
|
const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
|
|
3553
|
-
reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3863
|
+
return reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3554
3864
|
}
|
|
3555
3865
|
function captureException(error, hint) {
|
|
3556
3866
|
const rt = runtime;
|
|
@@ -3561,13 +3871,19 @@ function captureException(error, hint) {
|
|
|
3561
3871
|
}
|
|
3562
3872
|
return void 0;
|
|
3563
3873
|
}
|
|
3874
|
+
let attachmentId;
|
|
3564
3875
|
if (hint?.attachment) {
|
|
3565
3876
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
3566
|
-
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3877
|
+
attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3878
|
+
}
|
|
3879
|
+
if (!rt.client) {
|
|
3880
|
+
rt.logger.debug("captureException ignorado no modo local (sem Central)");
|
|
3881
|
+
return void 0;
|
|
3567
3882
|
}
|
|
3568
3883
|
const runId = currentReporter()?.runId;
|
|
3569
3884
|
const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
|
|
3570
|
-
|
|
3885
|
+
const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
|
|
3886
|
+
return rt.client.captureException(error, withRun, contexts);
|
|
3571
3887
|
}
|
|
3572
3888
|
async function close(timeoutMs = 3e4) {
|
|
3573
3889
|
const rt = runtime;
|
|
@@ -3577,6 +3893,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3577
3893
|
if (rt.agent) await rt.agent.stop(0);
|
|
3578
3894
|
rt.local?.stop();
|
|
3579
3895
|
rt.attached?.stop();
|
|
3896
|
+
rt.localRun?.stop();
|
|
3580
3897
|
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
3581
3898
|
if (!idle) {
|
|
3582
3899
|
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
@@ -3585,7 +3902,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3585
3902
|
rt.heartbeat?.stop();
|
|
3586
3903
|
rt.uninstallShutdown?.();
|
|
3587
3904
|
rt.uninstallGlobalHandlers?.();
|
|
3588
|
-
const flushed = await rt.client.close(timeoutMs);
|
|
3905
|
+
const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
|
|
3589
3906
|
return idle && flushed;
|
|
3590
3907
|
}
|
|
3591
3908
|
|
|
@@ -3614,11 +3931,13 @@ var noop = () => void 0;
|
|
|
3614
3931
|
var fallbackEachSink = {
|
|
3615
3932
|
report: noop,
|
|
3616
3933
|
startStep: () => void 0,
|
|
3934
|
+
stepScope: (_step, fn) => fn(),
|
|
3617
3935
|
declareTotal: noop,
|
|
3618
3936
|
advance: noop,
|
|
3619
3937
|
checkCanceled: noop,
|
|
3620
3938
|
isAborted: () => false,
|
|
3621
|
-
logError: (message) => printRunLog("error", message)
|
|
3939
|
+
logError: (message) => printRunLog("error", message),
|
|
3940
|
+
flush: () => Promise.resolve()
|
|
3622
3941
|
};
|
|
3623
3942
|
var fallbackItems = {
|
|
3624
3943
|
add: noop,
|
|
@@ -3637,7 +3956,6 @@ var fallbackFiles = {
|
|
|
3637
3956
|
read: outsideRunFiles
|
|
3638
3957
|
};
|
|
3639
3958
|
var neverAborts = new AbortController().signal;
|
|
3640
|
-
var detachedTransaction;
|
|
3641
3959
|
function resolve(member) {
|
|
3642
3960
|
const reporter = currentReporter();
|
|
3643
3961
|
if (reporter) return reporter.context;
|
|
@@ -3658,8 +3976,12 @@ var hobots = Object.defineProperties({}, {
|
|
|
3658
3976
|
items: getter("items", () => fallbackItems),
|
|
3659
3977
|
progress: getter("progress", () => fallbackProgress),
|
|
3660
3978
|
files: getter("files", () => fallbackFiles),
|
|
3661
|
-
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
3662
3979
|
signal: getter("signal", () => neverAborts),
|
|
3980
|
+
step: method((name, fn, options) => {
|
|
3981
|
+
const ctx = resolve("step");
|
|
3982
|
+
if (ctx) return ctx.step(name, fn, options);
|
|
3983
|
+
return new Transaction(void 0, "hobots.detached").step(name, fn, options);
|
|
3984
|
+
}),
|
|
3663
3985
|
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
3664
3986
|
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
3665
3987
|
flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
|