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.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/utils.ts
|
|
6
6
|
var SDK_NAME = "hobots";
|
|
7
|
-
var SDK_VERSION = true ? "
|
|
7
|
+
var SDK_VERSION = true ? "5.0.0" : "0.0.0-dev";
|
|
8
8
|
function isNode() {
|
|
9
9
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
10
10
|
}
|
|
@@ -57,8 +57,13 @@ function abortableSleep(ms, signal) {
|
|
|
57
57
|
function jitter(baseMs) {
|
|
58
58
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
59
59
|
}
|
|
60
|
+
var TRUNCATION_SUFFIX = "\u2026 [truncado]";
|
|
60
61
|
function truncate(value, max) {
|
|
61
|
-
return value.length > max ? `${value.slice(0, max)}
|
|
62
|
+
return value.length > max ? `${value.slice(0, max)}${TRUNCATION_SUFFIX}` : value;
|
|
63
|
+
}
|
|
64
|
+
function clip(value, max) {
|
|
65
|
+
if (value.length <= max) return value;
|
|
66
|
+
return `${value.slice(0, Math.max(0, max - TRUNCATION_SUFFIX.length))}${TRUNCATION_SUFFIX}`;
|
|
62
67
|
}
|
|
63
68
|
function safeString(value) {
|
|
64
69
|
if (typeof value === "string") return value;
|
|
@@ -156,6 +161,8 @@ function colorize(text, color) {
|
|
|
156
161
|
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
157
162
|
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
158
163
|
var RUN_ID_ENV = "HOBOTS_RUN_ID";
|
|
164
|
+
var LOCAL_RUN_ENV = "HOBOTS_LOCAL_RUN";
|
|
165
|
+
var RUN_PARAMS_ENV = "HOBOTS_RUN_PARAMS";
|
|
159
166
|
var dotenvLoaded = false;
|
|
160
167
|
function loadDotenv() {
|
|
161
168
|
if (dotenvLoaded) return;
|
|
@@ -181,6 +188,24 @@ function resolveRunId(option) {
|
|
|
181
188
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
182
189
|
return process.env[RUN_ID_ENV]?.trim() || void 0;
|
|
183
190
|
}
|
|
191
|
+
function isLocalRun() {
|
|
192
|
+
if (typeof process === "undefined" || !process.env) return false;
|
|
193
|
+
return process.env[LOCAL_RUN_ENV]?.trim() === "1";
|
|
194
|
+
}
|
|
195
|
+
function resolveLocalRunParams() {
|
|
196
|
+
const raw = process.env[RUN_PARAMS_ENV]?.trim();
|
|
197
|
+
if (!raw) return {};
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(raw);
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: n\xE3o \xE9 JSON v\xE1lido`);
|
|
203
|
+
}
|
|
204
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
205
|
+
throw new Error(`${RUN_PARAMS_ENV} inv\xE1lido: esperado um objeto JSON`);
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
184
209
|
function resolveEnvironmentOption(option) {
|
|
185
210
|
const fromOption = option?.trim();
|
|
186
211
|
if (fromOption) return fromOption;
|
|
@@ -394,9 +419,18 @@ var MAX_FINGERPRINT_ITEMS = 10;
|
|
|
394
419
|
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
395
420
|
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
396
421
|
var MAX_STEPS = 200;
|
|
422
|
+
var MAX_STEP_DESCRIPTION_CHARS = 2048;
|
|
423
|
+
var MAX_STEP_OP_CHARS = 255;
|
|
397
424
|
function clipTags(tags) {
|
|
398
425
|
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
399
426
|
}
|
|
427
|
+
function clipStep(step) {
|
|
428
|
+
return {
|
|
429
|
+
...step,
|
|
430
|
+
...step.op ? { op: clip(step.op, MAX_STEP_OP_CHARS) } : {},
|
|
431
|
+
...step.description ? { description: clip(step.description, MAX_STEP_DESCRIPTION_CHARS) } : {}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
400
434
|
function clipFingerprint(fingerprint) {
|
|
401
435
|
if (fingerprint === void 0) return void 0;
|
|
402
436
|
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
@@ -455,12 +489,13 @@ var Client = class {
|
|
|
455
489
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
456
490
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
457
491
|
}
|
|
458
|
-
|
|
492
|
+
/** `contexts` é interno ao SDK (ex.: `contexts.run` com o print do captureException) — o evento ainda pode sobrescrever. */
|
|
493
|
+
captureException(error, hint, contexts) {
|
|
459
494
|
if (typeof error === "object" && error !== null) captured.add(error);
|
|
460
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
495
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint, contexts);
|
|
461
496
|
}
|
|
462
|
-
captureEvent(event, hint) {
|
|
463
|
-
const final = this.prepareEvent(event, hint);
|
|
497
|
+
captureEvent(event, hint, contexts) {
|
|
498
|
+
const final = this.prepareEvent(contexts ? { ...event, contexts: { ...contexts, ...event.contexts } } : event, hint);
|
|
464
499
|
this.transport.enqueue({ type: "event", payload: final });
|
|
465
500
|
return final.event_id;
|
|
466
501
|
}
|
|
@@ -483,14 +518,15 @@ var Client = class {
|
|
|
483
518
|
sendTransaction(payload) {
|
|
484
519
|
let steps = payload.steps;
|
|
485
520
|
if (steps.length > MAX_STEPS) {
|
|
486
|
-
this.logger.warn(`transaction com ${steps.length}
|
|
521
|
+
this.logger.warn(`transaction com ${steps.length} etapas \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
487
522
|
steps = steps.slice(0, MAX_STEPS);
|
|
488
523
|
}
|
|
524
|
+
steps = steps.map(clipStep);
|
|
489
525
|
this.transport.enqueue({
|
|
490
526
|
type: "transaction",
|
|
491
527
|
payload: {
|
|
492
528
|
...payload,
|
|
493
|
-
name:
|
|
529
|
+
name: clip(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
494
530
|
steps,
|
|
495
531
|
release: payload.release ?? this.options.release,
|
|
496
532
|
environment: this.resolveEnvironment(payload.environment),
|
|
@@ -1290,11 +1326,11 @@ var ItemHandle = class {
|
|
|
1290
1326
|
}
|
|
1291
1327
|
failed(message, payload, options) {
|
|
1292
1328
|
this.record("failed", message, payload, options?.image);
|
|
1293
|
-
if (this.step && this.step.status !== "error") this.step.
|
|
1329
|
+
if (this.step && this.step.status !== "error") this.step._setStatus("error", message);
|
|
1294
1330
|
}
|
|
1295
1331
|
occurrence(message, payload, options) {
|
|
1296
1332
|
this.record("occurrence", message, payload, options?.image);
|
|
1297
|
-
if (this.step && this.step.status === "ok") this.step.
|
|
1333
|
+
if (this.step && this.step.status === "ok") this.step._setStatus("warning", message);
|
|
1298
1334
|
}
|
|
1299
1335
|
record(status, message, payload, image3) {
|
|
1300
1336
|
this.reported = true;
|
|
@@ -1325,23 +1361,24 @@ async function eachItem(list, handler, options, sink) {
|
|
|
1325
1361
|
const step = sink.startStep(options.op ?? "item", description);
|
|
1326
1362
|
const handle = new ItemHandle(index, id, step, options.payload?.(element, index), sink);
|
|
1327
1363
|
try {
|
|
1328
|
-
await handler(element, handle, index);
|
|
1364
|
+
await sink.stepScope(step, async () => handler(element, handle, index));
|
|
1329
1365
|
if (!handle.reported) handle.succeeded();
|
|
1330
1366
|
} catch (error) {
|
|
1331
1367
|
if (isTerminalError(error) || sink.isAborted()) {
|
|
1332
|
-
step?.
|
|
1368
|
+
step?._setStatus("cancelled")._close();
|
|
1333
1369
|
throw error;
|
|
1334
1370
|
}
|
|
1335
1371
|
const message = thrownMessage(error);
|
|
1336
1372
|
if (!handle.reported) handle.failed(message);
|
|
1337
|
-
step?.
|
|
1373
|
+
step?._failed(error);
|
|
1338
1374
|
sink.logError(`item ${description} falhou: ${message}`);
|
|
1339
1375
|
}
|
|
1340
|
-
step?.
|
|
1376
|
+
step?._close();
|
|
1341
1377
|
if (options.progress !== false) sink.advance();
|
|
1342
1378
|
result.total += 1;
|
|
1343
1379
|
result[handle.status ?? "succeeded"] += 1;
|
|
1344
1380
|
index += 1;
|
|
1381
|
+
if (options.flush !== false) await sink.flush();
|
|
1345
1382
|
}
|
|
1346
1383
|
return result;
|
|
1347
1384
|
}
|
|
@@ -1381,6 +1418,11 @@ function isInsideDir(root, candidate, sep) {
|
|
|
1381
1418
|
function unavailableLocal(ref) {
|
|
1382
1419
|
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
1383
1420
|
}
|
|
1421
|
+
function localRunPath(ref) {
|
|
1422
|
+
if (!isLocalRun()) return void 0;
|
|
1423
|
+
const path = ref.path;
|
|
1424
|
+
return typeof path === "string" && path.trim() !== "" ? path : void 0;
|
|
1425
|
+
}
|
|
1384
1426
|
function errorCode(error) {
|
|
1385
1427
|
return error?.code;
|
|
1386
1428
|
}
|
|
@@ -1548,8 +1590,16 @@ var RunFileStore = class {
|
|
|
1548
1590
|
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
1549
1591
|
}
|
|
1550
1592
|
}
|
|
1551
|
-
/**
|
|
1593
|
+
/**
|
|
1594
|
+
* storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de
|
|
1595
|
+
* origem. No modo local, um FileRef com `path` copia direto desse caminho, sem apagar a origem.
|
|
1596
|
+
*/
|
|
1552
1597
|
async copyLocal(ref, target) {
|
|
1598
|
+
const direct = localRunPath(ref);
|
|
1599
|
+
if (direct !== void 0) {
|
|
1600
|
+
await this.copyFromPath(ref, direct, target);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1553
1603
|
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
1554
1604
|
if (!base) throw unavailableLocal(ref);
|
|
1555
1605
|
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
@@ -1583,6 +1633,26 @@ var RunFileStore = class {
|
|
|
1583
1633
|
}
|
|
1584
1634
|
await rm(folder, { recursive: true, force: true });
|
|
1585
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Modo local: cópia do arquivo apontado por `path` (teste roda várias vezes — a fonte fica).
|
|
1638
|
+
* Sem conferência de tamanho: o `size` do FileRef é informativo aqui.
|
|
1639
|
+
*/
|
|
1640
|
+
async copyFromPath(ref, path, target) {
|
|
1641
|
+
const { copyFile, rename, rm, stat } = await import("fs/promises");
|
|
1642
|
+
const { resolve: resolve2 } = await import("path");
|
|
1643
|
+
const source = resolve2(path);
|
|
1644
|
+
const info = await stat(source).catch(() => void 0);
|
|
1645
|
+
if (!info?.isFile()) throw new Error(`arquivo "${ref.name}" n\xE3o encontrado em ${path}`);
|
|
1646
|
+
this.checkCanceled();
|
|
1647
|
+
const part = `${target}.part`;
|
|
1648
|
+
try {
|
|
1649
|
+
await copyFile(source, part);
|
|
1650
|
+
await rename(part, target);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
await rm(part, { force: true });
|
|
1653
|
+
throw error;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1586
1656
|
translate(error, ref) {
|
|
1587
1657
|
if (this.deps.signal.aborted) return runCanceledError();
|
|
1588
1658
|
if (error instanceof TasksApiError && error.status === 410) {
|
|
@@ -1711,6 +1781,11 @@ function printRunLog(level, message) {
|
|
|
1711
1781
|
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1712
1782
|
console[method2](line, message);
|
|
1713
1783
|
}
|
|
1784
|
+
var PROGRESS_LINE_PREFIX = "[hobots:progress]";
|
|
1785
|
+
var PROGRESS_PRINT_THROTTLE_MS = 250;
|
|
1786
|
+
function printRunProgress(current, total) {
|
|
1787
|
+
console.log(`${PROGRESS_LINE_PREFIX} ${current}/${total}`);
|
|
1788
|
+
}
|
|
1714
1789
|
function cancelableSleep(ms, signal) {
|
|
1715
1790
|
return new Promise((resolve2, reject) => {
|
|
1716
1791
|
if (signal.aborted) {
|
|
@@ -1780,13 +1855,15 @@ var RunReporter = class {
|
|
|
1780
1855
|
const eachSink = {
|
|
1781
1856
|
report: (status, message, payload, id, image3) => this.addItem(status, message, payload, id, image3),
|
|
1782
1857
|
startStep: (op, description) => tx.start(op, description),
|
|
1858
|
+
stepScope: (step, fn) => tx.scope(step, fn),
|
|
1783
1859
|
declareTotal: (total) => {
|
|
1784
1860
|
if (this.progressTotal == null) this.setProgress(this.progressCurrent, total);
|
|
1785
1861
|
},
|
|
1786
1862
|
advance: () => this.setProgress(this.progressCurrent + 1, this.progressTotal),
|
|
1787
1863
|
checkCanceled,
|
|
1788
1864
|
isAborted: () => this.abortController.signal.aborted,
|
|
1789
|
-
logError: (message) => this.log("error", message)
|
|
1865
|
+
logError: (message) => this.log("error", message),
|
|
1866
|
+
flush: () => this.flush()
|
|
1790
1867
|
};
|
|
1791
1868
|
const items = {
|
|
1792
1869
|
add: (item) => {
|
|
@@ -1841,7 +1918,14 @@ var RunReporter = class {
|
|
|
1841
1918
|
items,
|
|
1842
1919
|
progress,
|
|
1843
1920
|
files: this.fileStore.files,
|
|
1844
|
-
|
|
1921
|
+
step: async (name, fn, options) => {
|
|
1922
|
+
const root = tx.currentStep() === void 0;
|
|
1923
|
+
try {
|
|
1924
|
+
return await tx.step(name, fn, { ...options, signal, wait: sleep2, warn: (message) => this.log("warning", message) });
|
|
1925
|
+
} finally {
|
|
1926
|
+
if (options?.flush !== false && root && !signal.aborted) await this.flush();
|
|
1927
|
+
}
|
|
1928
|
+
},
|
|
1845
1929
|
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1846
1930
|
signal,
|
|
1847
1931
|
throwIfCanceled: checkCanceled,
|
|
@@ -1904,6 +1988,8 @@ var RunReporter = class {
|
|
|
1904
1988
|
progressDirty = false;
|
|
1905
1989
|
progressTimer;
|
|
1906
1990
|
progressWarned = false;
|
|
1991
|
+
/** Relógio do último eco `[hobots:progress]` no terminal (throttle do eco, não do wire). */
|
|
1992
|
+
progressLastPrint = 0;
|
|
1907
1993
|
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1908
1994
|
fileStore;
|
|
1909
1995
|
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
@@ -1954,15 +2040,17 @@ var RunReporter = class {
|
|
|
1954
2040
|
/**
|
|
1955
2041
|
* Linha de log da run com eco no terminal. `image` (opcional) vira o anexo da linha:
|
|
1956
2042
|
* o id entra na entrada ANTES de qualquer I/O — é isso que mantém a ordem dos logs
|
|
1957
|
-
* intacta e o handler livre da latência do upload.
|
|
2043
|
+
* intacta e o handler livre da latência do upload. Devolve o id do anexo (quando houve
|
|
2044
|
+
* imagem e ela não foi descartada) — o `captureException` o leva ao evento da issue.
|
|
1958
2045
|
*/
|
|
1959
2046
|
log(level, message, image3) {
|
|
1960
|
-
if (this.dead) return;
|
|
2047
|
+
if (this.dead) return void 0;
|
|
1961
2048
|
const scrubbed = scrubSecrets(safeString(message));
|
|
1962
2049
|
const ts = nowISO();
|
|
1963
2050
|
const attachmentId = image3 === void 0 ? void 0 : this.queueAttachment(image3, ts);
|
|
1964
2051
|
printRunLog(level, attachmentId ? `${scrubbed} [imagem]` : scrubbed);
|
|
1965
2052
|
this.push(level, scrubbed, true, attachmentId ? [attachmentId] : void 0, ts);
|
|
2053
|
+
return attachmentId;
|
|
1966
2054
|
}
|
|
1967
2055
|
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
1968
2056
|
push(level, message, alreadyScrubbed = false, attachments, ts = nowISO()) {
|
|
@@ -2008,7 +2096,7 @@ var RunReporter = class {
|
|
|
2008
2096
|
}
|
|
2009
2097
|
}
|
|
2010
2098
|
/**
|
|
2011
|
-
* `hobots.flush()`: o bot decide quando entregar o que já fez —
|
|
2099
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — etapas raiz finalizadas
|
|
2012
2100
|
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2013
2101
|
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2014
2102
|
*/
|
|
@@ -2066,10 +2154,16 @@ var RunReporter = class {
|
|
|
2066
2154
|
}
|
|
2067
2155
|
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
2068
2156
|
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
2069
|
-
|
|
2157
|
+
const totalChanged = total !== this.progressTotal;
|
|
2158
|
+
if (next === this.progressCurrent && !totalChanged) return;
|
|
2070
2159
|
this.progressCurrent = next;
|
|
2071
2160
|
this.progressTotal = total;
|
|
2072
2161
|
if (total == null) return;
|
|
2162
|
+
const now = Date.now();
|
|
2163
|
+
if (totalChanged || next >= total || now - this.progressLastPrint >= PROGRESS_PRINT_THROTTLE_MS) {
|
|
2164
|
+
this.progressLastPrint = now;
|
|
2165
|
+
printRunProgress(next, total);
|
|
2166
|
+
}
|
|
2073
2167
|
this.progressDirty = true;
|
|
2074
2168
|
if (this.progressTimer) return;
|
|
2075
2169
|
this.progressTimer = setTimeout(() => {
|
|
@@ -2244,7 +2338,7 @@ var RunReporter = class {
|
|
|
2244
2338
|
}
|
|
2245
2339
|
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
2246
2340
|
/**
|
|
2247
|
-
* Pendura uma imagem num evento da run (log, item ou
|
|
2341
|
+
* Pendura uma imagem num evento da run (log, item ou etapa): faz as checagens que não
|
|
2248
2342
|
* dependem de I/O (reporter morto, instalação sem anexos, cota), reserva o id e dispara
|
|
2249
2343
|
* o upload em segundo plano. Devolve o id — já referenciável pelo evento — ou undefined
|
|
2250
2344
|
* quando a imagem foi descartada (sempre com aviso no log da run). Nunca lança: todo
|
|
@@ -2417,11 +2511,13 @@ var RunReporter = class {
|
|
|
2417
2511
|
|
|
2418
2512
|
// src/tasks/run-context-store.ts
|
|
2419
2513
|
var storage;
|
|
2514
|
+
var stepStorage;
|
|
2420
2515
|
var initPromise;
|
|
2421
2516
|
async function ensureStorage() {
|
|
2422
2517
|
if (storage || !isNode()) return;
|
|
2423
2518
|
initPromise ??= import("async_hooks").then(({ AsyncLocalStorage: ALS }) => {
|
|
2424
2519
|
storage = new ALS();
|
|
2520
|
+
stepStorage = new ALS();
|
|
2425
2521
|
});
|
|
2426
2522
|
await initPromise;
|
|
2427
2523
|
}
|
|
@@ -2432,12 +2528,23 @@ async function runWithRunContext(store, fn) {
|
|
|
2432
2528
|
function currentRunReporter() {
|
|
2433
2529
|
return storage?.getStore()?.reporter;
|
|
2434
2530
|
}
|
|
2531
|
+
async function runWithStepScope(scope, fn) {
|
|
2532
|
+
await ensureStorage();
|
|
2533
|
+
return stepStorage ? stepStorage.run(scope, fn) : fn();
|
|
2534
|
+
}
|
|
2535
|
+
function currentStep(transaction) {
|
|
2536
|
+
const scope = stepStorage?.getStore();
|
|
2537
|
+
return scope?.transaction === transaction ? scope.step : void 0;
|
|
2538
|
+
}
|
|
2435
2539
|
|
|
2436
2540
|
// src/transaction.ts
|
|
2437
2541
|
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
2542
|
+
var DEFAULT_STEP_RETRIES = 1;
|
|
2543
|
+
var DEFAULT_STEP_RETRY_DELAY_MS = 5e3;
|
|
2438
2544
|
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2545
|
+
var MAX_BACKLOG = 100;
|
|
2439
2546
|
function stepMessage(text) {
|
|
2440
|
-
return
|
|
2547
|
+
return clip(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
2441
2548
|
}
|
|
2442
2549
|
function stepId() {
|
|
2443
2550
|
return uuid4().slice(0, 16);
|
|
@@ -2445,6 +2552,13 @@ function stepId() {
|
|
|
2445
2552
|
function stepLabel(op, description) {
|
|
2446
2553
|
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
2447
2554
|
}
|
|
2555
|
+
function errorText(error) {
|
|
2556
|
+
if (error instanceof Error) return error.message || error.name;
|
|
2557
|
+
return safeString(error);
|
|
2558
|
+
}
|
|
2559
|
+
function errorReason(error) {
|
|
2560
|
+
return error instanceof Error ? `${error.name}: ${errorText(error)}` : errorText(error);
|
|
2561
|
+
}
|
|
2448
2562
|
function levelForStatus(status) {
|
|
2449
2563
|
switch (status) {
|
|
2450
2564
|
case "warning":
|
|
@@ -2484,69 +2598,68 @@ var Step = class {
|
|
|
2484
2598
|
startTimestamp = nowISO();
|
|
2485
2599
|
status = "ok";
|
|
2486
2600
|
endTimestamp;
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
* `options.image` pendura um print do estado da tela em `data.attachments`.
|
|
2492
|
-
*/
|
|
2493
|
-
setStatus(status, message, options) {
|
|
2494
|
-
this.status = status;
|
|
2495
|
-
if (message) this.setData("message", stepMessage(message));
|
|
2601
|
+
_data;
|
|
2602
|
+
// ── API do bot ────────────────────────────────────────────────────────────
|
|
2603
|
+
warning(message, options) {
|
|
2604
|
+
if (this.status !== "error") this._setStatus("warning", message);
|
|
2496
2605
|
if (options?.image !== void 0) this.attachImage(options.image);
|
|
2497
2606
|
return this;
|
|
2498
2607
|
}
|
|
2608
|
+
image(image3) {
|
|
2609
|
+
this.attachImage(image3);
|
|
2610
|
+
return this;
|
|
2611
|
+
}
|
|
2612
|
+
data(key, value) {
|
|
2613
|
+
if (key === "attachments") return this;
|
|
2614
|
+
this._setData(key, value);
|
|
2615
|
+
return this;
|
|
2616
|
+
}
|
|
2617
|
+
// ── Internos (chamados pelo SDK) ──────────────────────────────────────────
|
|
2618
|
+
/** `data` da etapa como vai no wire (undefined quando vazio). */
|
|
2619
|
+
get payload() {
|
|
2620
|
+
return this._data;
|
|
2621
|
+
}
|
|
2622
|
+
_setStatus(status, message) {
|
|
2623
|
+
this.status = status;
|
|
2624
|
+
if (message) this._setData("message", stepMessage(safeString(message)));
|
|
2625
|
+
return this;
|
|
2626
|
+
}
|
|
2499
2627
|
/**
|
|
2500
|
-
* Marca
|
|
2501
|
-
*
|
|
2502
|
-
* `options.image` pendura um print do estado da tela em `data.attachments`.
|
|
2503
|
-
* Numa step já fechada não faz nada.
|
|
2628
|
+
* Marca `error` guardando `{ type, message }` em `data.error` e fecha a etapa. Aceita qualquer
|
|
2629
|
+
* valor lançado; `message` sobrescreve a mensagem do erro. Numa etapa já fechada não faz nada.
|
|
2504
2630
|
*/
|
|
2505
|
-
|
|
2631
|
+
_failed(error, message) {
|
|
2506
2632
|
if (this.endTimestamp) return this;
|
|
2507
2633
|
this.status = "error";
|
|
2508
2634
|
const type = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Error" : void 0;
|
|
2509
|
-
const raw = message ?? (error
|
|
2510
|
-
this.
|
|
2511
|
-
|
|
2512
|
-
this.close();
|
|
2635
|
+
const raw = message ?? errorText(error);
|
|
2636
|
+
this._setData("error", { ...type ? { type } : {}, message: stepMessage(safeString(raw)) });
|
|
2637
|
+
this._close();
|
|
2513
2638
|
return this;
|
|
2514
2639
|
}
|
|
2640
|
+
_setData(key, value) {
|
|
2641
|
+
this._data = { ...this._data, [key]: value };
|
|
2642
|
+
}
|
|
2515
2643
|
/**
|
|
2516
2644
|
* Dispara o upload pela run (sink do RunReporter) e referencia o anexo em
|
|
2517
2645
|
* `data.attachments` (chave reservada; teto igual ao das linhas de log). Fora de uma
|
|
2518
2646
|
* run não há onde pendurar a imagem — descarta com aviso único.
|
|
2519
2647
|
*/
|
|
2520
2648
|
attachImage(image3) {
|
|
2521
|
-
const current = Array.isArray(this.
|
|
2649
|
+
const current = Array.isArray(this._data?.attachments) ? this._data.attachments : [];
|
|
2522
2650
|
if (current.length >= MAX_ATTACHMENTS_PER_LOG_ENTRY) return;
|
|
2523
2651
|
const id = this.transaction.attachImage(image3);
|
|
2524
2652
|
if (!id) return;
|
|
2525
|
-
this.
|
|
2653
|
+
this._setData("attachments", [...current, id]);
|
|
2526
2654
|
}
|
|
2527
|
-
/** Motivo registrado
|
|
2655
|
+
/** Motivo registrado (warning/erro) — usado no log de FINISH. */
|
|
2528
2656
|
reason() {
|
|
2529
|
-
const error = this.
|
|
2530
|
-
const text = typeof error?.message === "string" ? error.message : this.
|
|
2657
|
+
const error = this._data?.error;
|
|
2658
|
+
const text = typeof error?.message === "string" ? error.message : this._data?.message;
|
|
2531
2659
|
return typeof text === "string" && text ? text : void 0;
|
|
2532
2660
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
return this;
|
|
2536
|
-
}
|
|
2537
|
-
/** Step aninhada (filha desta step). */
|
|
2538
|
-
start(op, description) {
|
|
2539
|
-
return this.transaction.createStep(this.stepId, op, description);
|
|
2540
|
-
}
|
|
2541
|
-
/**
|
|
2542
|
-
* Fecha a step. O status fica o que estiver (`ok` por padrão, ou o de `setStatus`).
|
|
2543
|
-
* Idempotente; a transaction fecha no fim da run o que ficou aberto.
|
|
2544
|
-
*/
|
|
2545
|
-
succeeded() {
|
|
2546
|
-
this.close();
|
|
2547
|
-
return this;
|
|
2548
|
-
}
|
|
2549
|
-
close() {
|
|
2661
|
+
/** Fecha a etapa mantendo o status que estiver. Idempotente. */
|
|
2662
|
+
_close() {
|
|
2550
2663
|
if (this.endTimestamp) return;
|
|
2551
2664
|
this.endTimestamp = nowISO();
|
|
2552
2665
|
this.transaction.emit(
|
|
@@ -2562,22 +2675,23 @@ var Step = class {
|
|
|
2562
2675
|
toJSON() {
|
|
2563
2676
|
return {
|
|
2564
2677
|
step_id: this.stepId,
|
|
2565
|
-
parent_step_id: this.parentStepId,
|
|
2566
|
-
op: this.op,
|
|
2567
|
-
description: this.description,
|
|
2678
|
+
...this.parentStepId ? { parent_step_id: this.parentStepId } : {},
|
|
2679
|
+
...this.op ? { op: clip(this.op, MAX_STEP_OP_CHARS) } : {},
|
|
2680
|
+
...this.description ? { description: clip(this.description, MAX_STEP_DESCRIPTION_CHARS) } : {},
|
|
2568
2681
|
status: this.status,
|
|
2569
2682
|
start_timestamp: this.startTimestamp,
|
|
2570
2683
|
timestamp: this.endTimestamp ?? nowISO(),
|
|
2571
|
-
data: this.
|
|
2684
|
+
...this._data ? { data: this._data } : {}
|
|
2572
2685
|
};
|
|
2573
2686
|
}
|
|
2574
2687
|
};
|
|
2575
2688
|
var Transaction = class {
|
|
2576
|
-
constructor(client, name, op, runId) {
|
|
2689
|
+
constructor(client, name, op, runId, options = {}) {
|
|
2577
2690
|
this.client = client;
|
|
2578
2691
|
this.name = name;
|
|
2579
2692
|
this.op = op;
|
|
2580
2693
|
this.runId = runId;
|
|
2694
|
+
this.echo = options.echo ?? false;
|
|
2581
2695
|
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2582
2696
|
}
|
|
2583
2697
|
client;
|
|
@@ -2589,33 +2703,36 @@ var Transaction = class {
|
|
|
2589
2703
|
startTimestamp = nowISO();
|
|
2590
2704
|
status = "ok";
|
|
2591
2705
|
steps = [];
|
|
2592
|
-
tags = {};
|
|
2593
2706
|
finished = false;
|
|
2594
2707
|
lifecycleSink;
|
|
2595
2708
|
attachmentSink;
|
|
2596
2709
|
warnedNoAttachmentSink = false;
|
|
2597
|
-
// instalado pelo RunReporter: abrir uma
|
|
2710
|
+
// instalado pelo RunReporter: abrir uma etapa é um ponto de parada cooperativo (lança RunCanceled)
|
|
2598
2711
|
stepGuard;
|
|
2712
|
+
// modo local (sem client): o FINISH de cada etapa sai no terminal, como `[hobots:item]`
|
|
2713
|
+
echo;
|
|
2599
2714
|
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
2600
2715
|
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
2601
2716
|
lifecycleBacklog = [];
|
|
2717
|
+
// ── Lifecycle / eco ───────────────────────────────────────────────────────
|
|
2602
2718
|
/**
|
|
2603
|
-
* Registra o início/fim de uma
|
|
2604
|
-
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2719
|
+
* Registra o início/fim de uma etapa ou da transaction: espelha a linha no sink de
|
|
2720
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos logs da
|
|
2721
|
+
* run no app), e ecoa no terminal com `debug: true` — ou, no modo local, só o FINISH das
|
|
2722
|
+
* etapas (`[hobots:step] FINISH …`), para o Studio e o dev verem.
|
|
2607
2723
|
*/
|
|
2608
2724
|
emit(kind, phase, label, startTs, endTs, status, reason) {
|
|
2609
2725
|
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
2610
2726
|
const suffix = (durationMs != null ? ` (${durationMs}ms)` : "") + (reason ? `: ${reason}` : "");
|
|
2611
|
-
|
|
2727
|
+
const localFinish = this.echo && kind === "step" && phase === "finish";
|
|
2728
|
+
if (this.client?.debug || localFinish) {
|
|
2612
2729
|
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
2613
2730
|
console.log(`${tag} ${label}${suffix}`);
|
|
2614
2731
|
}
|
|
2615
2732
|
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
2616
2733
|
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
2617
2734
|
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
2618
|
-
else if (this.lifecycleBacklog.length <
|
|
2735
|
+
else if (this.lifecycleBacklog.length < MAX_BACKLOG) this.lifecycleBacklog.push([level, message]);
|
|
2619
2736
|
}
|
|
2620
2737
|
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
2621
2738
|
setLifecycleSink(sink) {
|
|
@@ -2624,81 +2741,111 @@ var Transaction = class {
|
|
|
2624
2741
|
this.lifecycleBacklog = [];
|
|
2625
2742
|
for (const [level, message] of backlog) sink(level, message);
|
|
2626
2743
|
}
|
|
2627
|
-
/** Conecta o pipeline de anexos da run — é o que dá imagem às
|
|
2744
|
+
/** Conecta o pipeline de anexos da run — é o que dá imagem às etapas. */
|
|
2628
2745
|
setAttachmentSink(sink) {
|
|
2629
2746
|
this.attachmentSink = sink;
|
|
2630
2747
|
}
|
|
2631
|
-
/** Imagem de uma
|
|
2748
|
+
/** Imagem de uma etapa → id do anexo pela run; sem run, descarta com aviso único. */
|
|
2632
2749
|
attachImage(image3) {
|
|
2633
2750
|
if (this.attachmentSink) return this.attachmentSink(image3, nowISO());
|
|
2634
2751
|
if (!this.warnedNoAttachmentSink) {
|
|
2635
2752
|
this.warnedNoAttachmentSink = true;
|
|
2636
|
-
console.warn("[hobots] imagem de
|
|
2753
|
+
console.warn("[hobots] imagem de etapa descartada \u2014 s\xF3 \xE9 suportada dentro de uma execu\xE7\xE3o");
|
|
2637
2754
|
}
|
|
2638
2755
|
return void 0;
|
|
2639
2756
|
}
|
|
2757
|
+
// ── Etapas ────────────────────────────────────────────────────────────────
|
|
2640
2758
|
/**
|
|
2641
|
-
* Conecta a checagem de cancelamento chamada antes de cada
|
|
2642
|
-
*
|
|
2759
|
+
* Conecta a checagem de cancelamento chamada antes de cada etapa nova (inclusive antes de
|
|
2760
|
+
* cada nova tentativa) — é assim que `hobots.step()` para uma run cancelada.
|
|
2643
2761
|
*/
|
|
2644
2762
|
setStepGuard(guard) {
|
|
2645
2763
|
this.stepGuard = guard;
|
|
2646
2764
|
}
|
|
2647
|
-
|
|
2765
|
+
/** A etapa aberta por `step()` (ou `items.each`) no contexto async atual, se houver. */
|
|
2766
|
+
currentStep() {
|
|
2767
|
+
return currentStep(this);
|
|
2768
|
+
}
|
|
2769
|
+
/** Faz de `step` a etapa corrente durante `fn` (filhas aninham nela). Sem etapa, só roda `fn`. */
|
|
2770
|
+
scope(step, fn) {
|
|
2771
|
+
if (!step) return fn();
|
|
2772
|
+
return runWithStepScope({ transaction: this, step }, fn);
|
|
2773
|
+
}
|
|
2774
|
+
/** Abre uma etapa sob a corrente (ou direta da transaction). Interno: o bot usa `step()`. */
|
|
2775
|
+
start(op, description) {
|
|
2648
2776
|
this.stepGuard?.();
|
|
2649
|
-
const
|
|
2777
|
+
const parent = this.currentStep();
|
|
2778
|
+
const step = new Step(this, parent ? parent.stepId : this.stepId, op, description);
|
|
2650
2779
|
this.steps.push(step);
|
|
2651
2780
|
this.emit("step", "start", stepLabel(op, description));
|
|
2652
2781
|
return step;
|
|
2653
2782
|
}
|
|
2654
|
-
/** Step direta da transaction. `op` é o nome usado por `succeeded(name)`/`failed(error, name)`. */
|
|
2655
|
-
start(op, description) {
|
|
2656
|
-
return this.createStep(this.stepId, op, description);
|
|
2657
|
-
}
|
|
2658
2783
|
/**
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2784
|
+
* Registra a etapa `name` executando `fn(etapa)` até dar certo ou esgotar `retries`
|
|
2785
|
+
* tentativas extras (default 1), esperando `retryDelayMs` entre elas (default 5 000).
|
|
2786
|
+
* Resolve com o retorno de `fn`.
|
|
2787
|
+
*
|
|
2788
|
+
* Uma etapa só no waterfall, cobrindo todas as tentativas: `data.attempts` quando houve
|
|
2789
|
+
* mais de uma; `warning` com o motivo quando a última deu certo; `error` (e a exceção
|
|
2790
|
+
* sobe) quando esgotou; `cancelled` em cancelamento/timeout, que nunca repetem.
|
|
2661
2791
|
*/
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2792
|
+
async step(name, fn, options = {}) {
|
|
2793
|
+
const requested = options.retries ?? DEFAULT_STEP_RETRIES;
|
|
2794
|
+
const totalRetries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_STEP_RETRIES;
|
|
2795
|
+
const step = this.start(void 0, safeString(name));
|
|
2796
|
+
let attempts = 0;
|
|
2797
|
+
let lastFailure;
|
|
2798
|
+
try {
|
|
2799
|
+
const result = await retryWithBackoff(
|
|
2800
|
+
(attempt) => {
|
|
2801
|
+
attempts = attempt;
|
|
2802
|
+
return this.scope(step, async () => fn(step));
|
|
2803
|
+
},
|
|
2804
|
+
{
|
|
2805
|
+
retries: totalRetries,
|
|
2806
|
+
delayMs: (attempt) => resolveDelay(options.retryDelayMs, attempt, DEFAULT_STEP_RETRY_DELAY_MS),
|
|
2807
|
+
signal: options.signal,
|
|
2808
|
+
wait: options.wait,
|
|
2809
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2810
|
+
lastFailure = { error };
|
|
2811
|
+
options.warn?.(
|
|
2812
|
+
`etapa "${step.description}": tentativa ${attempt}/${totalRetries + 1} falhou: ${truncate(errorReason(error), 500)} \u2014 nova tentativa em ${delayMs}ms`
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
);
|
|
2817
|
+
this.noteAttempts(step, attempts);
|
|
2818
|
+
if (lastFailure && step.status === "ok") {
|
|
2819
|
+
step._setStatus("warning", `tentativa ${attempts - 1}/${totalRetries + 1} falhou: ${errorReason(lastFailure.error)}`);
|
|
2820
|
+
}
|
|
2821
|
+
step._close();
|
|
2822
|
+
return result;
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
this.noteAttempts(step, attempts);
|
|
2825
|
+
if (isTerminalError(error)) step._setStatus("cancelled")._close();
|
|
2826
|
+
else step._failed(error);
|
|
2827
|
+
throw error;
|
|
2677
2828
|
}
|
|
2678
|
-
return void 0;
|
|
2679
2829
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
return this;
|
|
2830
|
+
noteAttempts(step, attempts) {
|
|
2831
|
+
if (attempts > 1) step._setData("attempts", attempts);
|
|
2683
2832
|
}
|
|
2833
|
+
// ── Metadados ─────────────────────────────────────────────────────────────
|
|
2684
2834
|
setStatus(status) {
|
|
2685
2835
|
this.status = status;
|
|
2686
2836
|
return this;
|
|
2687
2837
|
}
|
|
2688
|
-
|
|
2689
|
-
this.name = name;
|
|
2690
|
-
return this;
|
|
2691
|
-
}
|
|
2838
|
+
// ── Encerramento ──────────────────────────────────────────────────────────
|
|
2692
2839
|
/**
|
|
2693
|
-
* Envia já as
|
|
2840
|
+
* Envia já as etapas raiz finalizadas (e todas as descendentes, fechando as que
|
|
2694
2841
|
* ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
|
|
2695
|
-
* `event_id` novo, até 200
|
|
2842
|
+
* `event_id` novo, até 200 etapas cada) e as tira da memória. Etapas raiz ainda abertas
|
|
2696
2843
|
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2697
2844
|
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2698
|
-
* Devolve quantas
|
|
2845
|
+
* Devolve quantas etapas foram enviadas (0 sem client, sem nada terminado ou depois do `finish()`).
|
|
2699
2846
|
*/
|
|
2700
2847
|
flush() {
|
|
2701
|
-
if (this.finished) return 0;
|
|
2848
|
+
if (!this.client || this.finished) return 0;
|
|
2702
2849
|
const ready = /* @__PURE__ */ new Set();
|
|
2703
2850
|
for (const step of this.steps) {
|
|
2704
2851
|
if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
|
|
@@ -2714,15 +2861,13 @@ var Transaction = class {
|
|
|
2714
2861
|
remaining.push(step);
|
|
2715
2862
|
}
|
|
2716
2863
|
}
|
|
2717
|
-
for (const step of batch) step.
|
|
2864
|
+
for (const step of batch) step._close();
|
|
2718
2865
|
this.steps.splice(0, this.steps.length, ...remaining);
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
2725
|
-
}
|
|
2866
|
+
for (let i = 0; i < batch.length; i += MAX_STEPS) {
|
|
2867
|
+
const chunk = batch.slice(i, i + MAX_STEPS);
|
|
2868
|
+
const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
|
|
2869
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2870
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
2726
2871
|
}
|
|
2727
2872
|
return batch.length;
|
|
2728
2873
|
}
|
|
@@ -2739,14 +2884,15 @@ var Transaction = class {
|
|
|
2739
2884
|
start_timestamp: startTimestamp,
|
|
2740
2885
|
timestamp,
|
|
2741
2886
|
steps: steps.map((step) => step.toJSON()),
|
|
2742
|
-
|
|
2887
|
+
// sem tags próprias: o client mescla as do processo
|
|
2888
|
+
tags: void 0,
|
|
2743
2889
|
run_id: this.runId
|
|
2744
2890
|
};
|
|
2745
2891
|
}
|
|
2746
2892
|
finish() {
|
|
2747
2893
|
if (this.finished) return;
|
|
2748
2894
|
this.finished = true;
|
|
2749
|
-
for (const step of this.steps) step.
|
|
2895
|
+
for (const step of this.steps) step._close();
|
|
2750
2896
|
const endTimestamp = nowISO();
|
|
2751
2897
|
this.emit(
|
|
2752
2898
|
"transaction",
|
|
@@ -2774,7 +2920,7 @@ var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
|
2774
2920
|
async function executeRun(input) {
|
|
2775
2921
|
const { run, handler, api, client, logger, tracker } = input;
|
|
2776
2922
|
const options = handler.options;
|
|
2777
|
-
const tx = new Transaction(client, "run", "hobots.run", run.id);
|
|
2923
|
+
const tx = new Transaction(client, "run", "hobots.run", run.id, { echo: !client });
|
|
2778
2924
|
const reporter = new RunReporter(
|
|
2779
2925
|
api,
|
|
2780
2926
|
run.id,
|
|
@@ -3014,42 +3160,26 @@ var TaskAgent = class {
|
|
|
3014
3160
|
}
|
|
3015
3161
|
};
|
|
3016
3162
|
|
|
3017
|
-
// src/tasks/
|
|
3018
|
-
var
|
|
3019
|
-
|
|
3020
|
-
if (!(error instanceof TasksApiError)) return true;
|
|
3021
|
-
return error.status >= 500 || error.status === 429;
|
|
3022
|
-
}
|
|
3023
|
-
function describeRejection(runId, error) {
|
|
3024
|
-
if (error instanceof TasksApiError && error.status === 404) {
|
|
3025
|
-
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3026
|
-
}
|
|
3027
|
-
if (error instanceof TasksApiError && error.status === 409) {
|
|
3028
|
-
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3029
|
-
}
|
|
3030
|
-
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3031
|
-
}
|
|
3032
|
-
var AttachedRunner = class {
|
|
3033
|
-
constructor(runId, handler, deps) {
|
|
3034
|
-
this.runId = runId;
|
|
3163
|
+
// src/tasks/single-run.ts
|
|
3164
|
+
var SingleRunRunner = class {
|
|
3165
|
+
constructor(handler, deps) {
|
|
3035
3166
|
this.handler = handler;
|
|
3036
3167
|
this.deps = deps;
|
|
3037
3168
|
this.done = new Promise((resolve2) => {
|
|
3038
3169
|
this.resolveDone = resolve2;
|
|
3039
3170
|
});
|
|
3040
3171
|
}
|
|
3041
|
-
runId;
|
|
3042
3172
|
handler;
|
|
3043
3173
|
deps;
|
|
3044
3174
|
abort = new AbortController();
|
|
3045
|
-
/** Resolve quando a run terminou (ou não pôde
|
|
3175
|
+
/** Resolve quando a run terminou (ou não pôde começar, ou o runner foi parado antes). */
|
|
3046
3176
|
done;
|
|
3047
3177
|
resolveDone;
|
|
3048
3178
|
async run() {
|
|
3049
3179
|
try {
|
|
3050
|
-
const run = await this.
|
|
3180
|
+
const run = await this.openRun();
|
|
3051
3181
|
if (!run) return;
|
|
3052
|
-
this.deps.logger.debug(
|
|
3182
|
+
this.deps.logger.debug(this.opened(run));
|
|
3053
3183
|
await executeRun({
|
|
3054
3184
|
run,
|
|
3055
3185
|
handler: this.handler,
|
|
@@ -3064,11 +3194,37 @@ var AttachedRunner = class {
|
|
|
3064
3194
|
this.resolveDone();
|
|
3065
3195
|
}
|
|
3066
3196
|
}
|
|
3067
|
-
/** Desiste de
|
|
3197
|
+
/** Desiste de obter a run (close() antes dela existir). Uma run já em execução segue pelo tracker. */
|
|
3068
3198
|
stop() {
|
|
3069
3199
|
this.abort.abort();
|
|
3070
3200
|
}
|
|
3071
|
-
|
|
3201
|
+
};
|
|
3202
|
+
|
|
3203
|
+
// src/tasks/attached.ts
|
|
3204
|
+
var START_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
3205
|
+
function isTransient(error) {
|
|
3206
|
+
if (!(error instanceof TasksApiError)) return true;
|
|
3207
|
+
return error.status >= 500 || error.status === 429;
|
|
3208
|
+
}
|
|
3209
|
+
function describeRejection(runId, error) {
|
|
3210
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
3211
|
+
return `run ${runId} n\xE3o existe na plataforma (404) \u2014 nada a executar`;
|
|
3212
|
+
}
|
|
3213
|
+
if (error instanceof TasksApiError && error.status === 409) {
|
|
3214
|
+
return `run ${runId} n\xE3o pode ser iniciada (409: cancelada, j\xE1 iniciada ou de outra inst\xE2ncia) \u2014 nada a executar`;
|
|
3215
|
+
}
|
|
3216
|
+
return `n\xE3o foi poss\xEDvel iniciar a run ${runId} (${String(error)}) \u2014 nada a executar`;
|
|
3217
|
+
}
|
|
3218
|
+
var AttachedRunner = class extends SingleRunRunner {
|
|
3219
|
+
constructor(runId, handler, deps) {
|
|
3220
|
+
super(handler, deps);
|
|
3221
|
+
this.runId = runId;
|
|
3222
|
+
}
|
|
3223
|
+
runId;
|
|
3224
|
+
opened(run) {
|
|
3225
|
+
return `run ${run.id} iniciada (HOBOTS_RUN_ID)`;
|
|
3226
|
+
}
|
|
3227
|
+
async openRun() {
|
|
3072
3228
|
for (let attempt = 0; ; attempt++) {
|
|
3073
3229
|
if (this.abort.signal.aborted) return void 0;
|
|
3074
3230
|
try {
|
|
@@ -3131,46 +3287,16 @@ function normalizeItemColumns(input, logger) {
|
|
|
3131
3287
|
}
|
|
3132
3288
|
|
|
3133
3289
|
// src/tasks/local.ts
|
|
3134
|
-
var LocalRunner = class {
|
|
3290
|
+
var LocalRunner = class extends SingleRunRunner {
|
|
3291
|
+
backoff;
|
|
3135
3292
|
constructor(handler, deps) {
|
|
3136
|
-
|
|
3137
|
-
this.deps = deps;
|
|
3293
|
+
super(handler, deps);
|
|
3138
3294
|
this.backoff = new Backoff(deps.logger, "cria\xE7\xE3o da run");
|
|
3139
|
-
this.done = new Promise((resolve2) => {
|
|
3140
|
-
this.resolveDone = resolve2;
|
|
3141
|
-
});
|
|
3142
3295
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
abort = new AbortController();
|
|
3146
|
-
backoff;
|
|
3147
|
-
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
3148
|
-
done;
|
|
3149
|
-
resolveDone;
|
|
3150
|
-
async run() {
|
|
3151
|
-
try {
|
|
3152
|
-
const run = await this.createRun();
|
|
3153
|
-
if (!run) return;
|
|
3154
|
-
this.deps.logger.debug(`run ${run.id} aberta (modo start)`);
|
|
3155
|
-
await executeRun({
|
|
3156
|
-
run,
|
|
3157
|
-
handler: this.handler,
|
|
3158
|
-
api: this.deps.api,
|
|
3159
|
-
client: this.deps.client,
|
|
3160
|
-
logger: this.deps.logger,
|
|
3161
|
-
tracker: this.deps.tracker
|
|
3162
|
-
});
|
|
3163
|
-
} catch (error) {
|
|
3164
|
-
this.deps.logger.error("erro inesperado na execu\xE7\xE3o do agente:", error);
|
|
3165
|
-
} finally {
|
|
3166
|
-
this.resolveDone();
|
|
3167
|
-
}
|
|
3296
|
+
opened(run) {
|
|
3297
|
+
return `run ${run.id} aberta (modo start)`;
|
|
3168
3298
|
}
|
|
3169
|
-
|
|
3170
|
-
stop() {
|
|
3171
|
-
this.abort.abort();
|
|
3172
|
-
}
|
|
3173
|
-
async createRun() {
|
|
3299
|
+
async openRun() {
|
|
3174
3300
|
for (; ; ) {
|
|
3175
3301
|
if (this.abort.signal.aborted) return void 0;
|
|
3176
3302
|
try {
|
|
@@ -3183,6 +3309,129 @@ var LocalRunner = class {
|
|
|
3183
3309
|
}
|
|
3184
3310
|
};
|
|
3185
3311
|
|
|
3312
|
+
// src/tasks/local-transport.ts
|
|
3313
|
+
var LOCAL_RUN_ID = "local";
|
|
3314
|
+
var ITEM_LINE_PREFIX = "[hobots:item]";
|
|
3315
|
+
var SUMMARY_LINE_PREFIX = "[hobots:summary]";
|
|
3316
|
+
var ATTACHMENTS_DISCARDED_WARNING = "imagens e anexos n\xE3o s\xE3o enviados no modo local (descartados)";
|
|
3317
|
+
function unavailable(name) {
|
|
3318
|
+
return new Error(`arquivo "${name}" enviado pela Central n\xE3o est\xE1 dispon\xEDvel no modo local`);
|
|
3319
|
+
}
|
|
3320
|
+
function cell(value) {
|
|
3321
|
+
return (value ?? "").replace(/\r\n|[\t\r\n]/g, " ");
|
|
3322
|
+
}
|
|
3323
|
+
var LocalRunTransport = class {
|
|
3324
|
+
constructor(params = {}, out = (line) => void process.stdout.write(`${line}
|
|
3325
|
+
`)) {
|
|
3326
|
+
this.params = params;
|
|
3327
|
+
this.out = out;
|
|
3328
|
+
}
|
|
3329
|
+
params;
|
|
3330
|
+
out;
|
|
3331
|
+
counts = { succeeded: 0, occurrence: 0, failed: 0 };
|
|
3332
|
+
warnedAttachments = false;
|
|
3333
|
+
/** Status terminal que o pipeline reportou — `undefined` enquanto a run corre (ou se ela nem reportou). */
|
|
3334
|
+
terminalStatus;
|
|
3335
|
+
async updateRun(_runId, update) {
|
|
3336
|
+
if (update.status === "running") return;
|
|
3337
|
+
this.terminalStatus = update.status;
|
|
3338
|
+
if (update.status === "failed" && update.error && update.error.type !== "RunCanceled") {
|
|
3339
|
+
printRunLog("error", `execu\xE7\xE3o falhou \u2014 ${update.error.type ?? "Error"}: ${update.error.message}`);
|
|
3340
|
+
if (update.error.stack) console.error(update.error.stack);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
async updateRunOnce() {
|
|
3344
|
+
}
|
|
3345
|
+
async createItems(_runId, request) {
|
|
3346
|
+
for (const item of request.items) {
|
|
3347
|
+
this.counts[item.status]++;
|
|
3348
|
+
this.out(`${ITEM_LINE_PREFIX} ${item.status} ${cell(item.id)} ${cell(item.message)}`);
|
|
3349
|
+
}
|
|
3350
|
+
return { accepted: request.items.length, duplicates: 0 };
|
|
3351
|
+
}
|
|
3352
|
+
/** Totais por status — chame uma vez, no fim da run, antes da linha de conclusão. */
|
|
3353
|
+
printSummary() {
|
|
3354
|
+
const { succeeded, occurrence, failed } = this.counts;
|
|
3355
|
+
this.out(
|
|
3356
|
+
`${SUMMARY_LINE_PREFIX} total=${succeeded + occurrence + failed} ok=${succeeded} avisos=${occurrence} falhas=${failed} pulados=0`
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
async createAttachment(_runId, request) {
|
|
3360
|
+
if (!this.warnedAttachments) {
|
|
3361
|
+
this.warnedAttachments = true;
|
|
3362
|
+
printRunLog("warning", ATTACHMENTS_DISCARDED_WARNING);
|
|
3363
|
+
}
|
|
3364
|
+
return { id: request.id, upload_url: "", headers: {}, expires_in: 0 };
|
|
3365
|
+
}
|
|
3366
|
+
async uploadAttachment() {
|
|
3367
|
+
}
|
|
3368
|
+
async confirmAttachment() {
|
|
3369
|
+
}
|
|
3370
|
+
/** Nome do arquivo pelo id (nos params); sem ele, o próprio id. */
|
|
3371
|
+
fileName(fileId) {
|
|
3372
|
+
return collectFileRefs(this.params).find((ref) => ref.id === fileId)?.name ?? fileId;
|
|
3373
|
+
}
|
|
3374
|
+
async requestFileDownload(_runId, fileId) {
|
|
3375
|
+
throw unavailable(this.fileName(fileId));
|
|
3376
|
+
}
|
|
3377
|
+
async fetchFile(url) {
|
|
3378
|
+
throw unavailable(url);
|
|
3379
|
+
}
|
|
3380
|
+
async confirmFileDownload(_runId, fileId) {
|
|
3381
|
+
throw unavailable(this.fileName(fileId));
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
// src/tasks/local-run.ts
|
|
3386
|
+
var LOCAL_RUN_EXIT_CODES = { succeeded: 0, failed: 1, canceled: 130 };
|
|
3387
|
+
var LOCAL_RUN_CANCEL_GRACE_MS = 4e3;
|
|
3388
|
+
var LOCAL_RUN_BANNER = "modo local \u2014 execu\xE7\xE3o sem a Central; params de HOBOTS_RUN_PARAMS";
|
|
3389
|
+
var LocalRunRunner = class extends SingleRunRunner {
|
|
3390
|
+
constructor(params, handler, deps) {
|
|
3391
|
+
super(handler, deps);
|
|
3392
|
+
this.params = params;
|
|
3393
|
+
}
|
|
3394
|
+
params;
|
|
3395
|
+
canceled = false;
|
|
3396
|
+
startedAt = Date.now();
|
|
3397
|
+
finished = false;
|
|
3398
|
+
run() {
|
|
3399
|
+
this.startedAt = Date.now();
|
|
3400
|
+
return super.run();
|
|
3401
|
+
}
|
|
3402
|
+
opened() {
|
|
3403
|
+
return "run local iniciada (HOBOTS_LOCAL_RUN)";
|
|
3404
|
+
}
|
|
3405
|
+
async openRun() {
|
|
3406
|
+
if (this.abort.signal.aborted) return void 0;
|
|
3407
|
+
return { id: LOCAL_RUN_ID, params: this.params, form: null, source: "local", created_at: nowISO() };
|
|
3408
|
+
}
|
|
3409
|
+
/** SIGINT/SIGTERM: dispara o `ctx.signal` da run em voo — o handler para no próximo ponto de parada. */
|
|
3410
|
+
cancel() {
|
|
3411
|
+
this.canceled = true;
|
|
3412
|
+
this.deps.tracker.singleActive()?.abort();
|
|
3413
|
+
}
|
|
3414
|
+
/** Desfecho da run: cancelada pelo sinal, senão o status terminal reportado (sem reporte = falha). */
|
|
3415
|
+
get outcome() {
|
|
3416
|
+
if (this.canceled) return "canceled";
|
|
3417
|
+
return this.deps.api.terminalStatus ?? "failed";
|
|
3418
|
+
}
|
|
3419
|
+
/**
|
|
3420
|
+
* Fecha o relato no terminal — `[hobots:summary]` e a linha de conclusão — e devolve o
|
|
3421
|
+
* desfecho. Idempotente: o runtime chama ao fim da run ou quando a graça do cancelamento
|
|
3422
|
+
* esgota com o handler ainda rodando.
|
|
3423
|
+
*/
|
|
3424
|
+
finish() {
|
|
3425
|
+
const outcome = this.outcome;
|
|
3426
|
+
if (this.finished) return outcome;
|
|
3427
|
+
this.finished = true;
|
|
3428
|
+
this.deps.api.printSummary();
|
|
3429
|
+
const seconds = Math.round((Date.now() - this.startedAt) / 1e3);
|
|
3430
|
+
printRunLog("info", `execu\xE7\xE3o local conclu\xEDda (${outcome}) em ${seconds}s`);
|
|
3431
|
+
return outcome;
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
|
|
3186
3435
|
// src/tasks/registry.ts
|
|
3187
3436
|
var RunTracker = class {
|
|
3188
3437
|
constructor(api) {
|
|
@@ -3298,26 +3547,10 @@ function hostname() {
|
|
|
3298
3547
|
if (!isNode()) return void 0;
|
|
3299
3548
|
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
3300
3549
|
}
|
|
3301
|
-
function
|
|
3302
|
-
|
|
3303
|
-
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3304
|
-
const logger = makeLogger(options.debug ?? false);
|
|
3305
|
-
const environment = resolveEnvironmentOption(options.environment);
|
|
3306
|
-
const client = new Client(
|
|
3307
|
-
{
|
|
3308
|
-
environment,
|
|
3309
|
-
release: options.release,
|
|
3310
|
-
tags: options.tags,
|
|
3311
|
-
debug: options.debug,
|
|
3312
|
-
captureRunErrors: options.captureUnhandled !== false
|
|
3313
|
-
},
|
|
3314
|
-
config
|
|
3315
|
-
);
|
|
3316
|
-
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3317
|
-
const rt = {
|
|
3550
|
+
function baseRuntime(options, handler, logger, api) {
|
|
3551
|
+
return {
|
|
3318
3552
|
options: { ...options, clientSecret: void 0 },
|
|
3319
|
-
|
|
3320
|
-
client,
|
|
3553
|
+
client: void 0,
|
|
3321
3554
|
logger,
|
|
3322
3555
|
api,
|
|
3323
3556
|
handler,
|
|
@@ -3327,19 +3560,54 @@ function createRuntime(options, handler) {
|
|
|
3327
3560
|
agent: void 0,
|
|
3328
3561
|
local: void 0,
|
|
3329
3562
|
attached: void 0,
|
|
3563
|
+
localRun: void 0,
|
|
3330
3564
|
runId: void 0,
|
|
3331
3565
|
instanceId: resolveInstanceId(options.instanceId),
|
|
3332
|
-
environment,
|
|
3566
|
+
environment: void 0,
|
|
3333
3567
|
pollIntervalMs: void 0,
|
|
3334
3568
|
booted: false,
|
|
3335
3569
|
bootScheduled: false,
|
|
3336
3570
|
bootPromise: void 0,
|
|
3337
|
-
uninstallGlobalHandlers:
|
|
3571
|
+
uninstallGlobalHandlers: void 0,
|
|
3338
3572
|
uninstallShutdown: void 0
|
|
3339
3573
|
};
|
|
3574
|
+
}
|
|
3575
|
+
function createRuntime(options, handler, logger) {
|
|
3576
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
3577
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
3578
|
+
const client = new Client(
|
|
3579
|
+
{
|
|
3580
|
+
environment,
|
|
3581
|
+
release: options.release,
|
|
3582
|
+
tags: options.tags,
|
|
3583
|
+
debug: options.debug,
|
|
3584
|
+
captureRunErrors: options.captureUnhandled !== false
|
|
3585
|
+
},
|
|
3586
|
+
config
|
|
3587
|
+
);
|
|
3588
|
+
const api = new TasksApi({ baseUrl: config.runsBaseUrl, key: config.key, logger });
|
|
3589
|
+
const rt = baseRuntime(options, handler, logger, api);
|
|
3590
|
+
rt.client = client;
|
|
3591
|
+
rt.environment = environment;
|
|
3592
|
+
rt.uninstallGlobalHandlers = options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0;
|
|
3340
3593
|
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
3341
3594
|
return rt;
|
|
3342
3595
|
}
|
|
3596
|
+
function createLocalRunRuntime(options, handler, logger) {
|
|
3597
|
+
let params;
|
|
3598
|
+
try {
|
|
3599
|
+
params = resolveLocalRunParams();
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
printRunLog("error", error instanceof Error ? error.message : String(error));
|
|
3602
|
+
process.exit(2);
|
|
3603
|
+
throw error;
|
|
3604
|
+
}
|
|
3605
|
+
const transport = new LocalRunTransport(params);
|
|
3606
|
+
const rt = baseRuntime(options, handler, logger, transport);
|
|
3607
|
+
rt.localRun = new LocalRunRunner(params, handler, { api: transport, client: void 0, logger, tracker: rt.tracker });
|
|
3608
|
+
printRunLog("info", LOCAL_RUN_BANNER);
|
|
3609
|
+
return rt;
|
|
3610
|
+
}
|
|
3343
3611
|
function validateHandler(handler, fn) {
|
|
3344
3612
|
if (typeof handler !== "function") {
|
|
3345
3613
|
throw new Error(`[hobots] ${fn}(): o handler precisa ser uma fun\xE7\xE3o \u2014 ${fn}(async (params) => { ... })`);
|
|
@@ -3382,9 +3650,12 @@ function instanceInfo(rt) {
|
|
|
3382
3650
|
machine: rt.sampler?.sample()
|
|
3383
3651
|
};
|
|
3384
3652
|
}
|
|
3653
|
+
function platformApi(rt) {
|
|
3654
|
+
return rt.api;
|
|
3655
|
+
}
|
|
3385
3656
|
function ensureAgent(rt) {
|
|
3386
3657
|
rt.agent ??= new TaskAgent({
|
|
3387
|
-
api: rt
|
|
3658
|
+
api: platformApi(rt),
|
|
3388
3659
|
client: rt.client,
|
|
3389
3660
|
logger: rt.logger,
|
|
3390
3661
|
handler: rt.handler,
|
|
@@ -3396,7 +3667,7 @@ function ensureAgent(rt) {
|
|
|
3396
3667
|
}
|
|
3397
3668
|
function launchLocal(rt) {
|
|
3398
3669
|
const runner = new LocalRunner(rt.handler, {
|
|
3399
|
-
api: rt
|
|
3670
|
+
api: platformApi(rt),
|
|
3400
3671
|
client: rt.client,
|
|
3401
3672
|
logger: rt.logger,
|
|
3402
3673
|
tracker: rt.tracker,
|
|
@@ -3407,7 +3678,7 @@ function launchLocal(rt) {
|
|
|
3407
3678
|
}
|
|
3408
3679
|
function launchAttached(rt, runId) {
|
|
3409
3680
|
const runner = new AttachedRunner(runId, rt.handler, {
|
|
3410
|
-
api: rt
|
|
3681
|
+
api: platformApi(rt),
|
|
3411
3682
|
client: rt.client,
|
|
3412
3683
|
logger: rt.logger,
|
|
3413
3684
|
tracker: rt.tracker,
|
|
@@ -3419,7 +3690,43 @@ function launchAttached(rt, runId) {
|
|
|
3419
3690
|
if (runtime === rt) void close();
|
|
3420
3691
|
});
|
|
3421
3692
|
}
|
|
3693
|
+
function launchLocalRun(rt, runner) {
|
|
3694
|
+
let finished = false;
|
|
3695
|
+
const finish = () => {
|
|
3696
|
+
if (finished) return;
|
|
3697
|
+
finished = true;
|
|
3698
|
+
rt.uninstallShutdown?.();
|
|
3699
|
+
rt.uninstallShutdown = void 0;
|
|
3700
|
+
const code = LOCAL_RUN_EXIT_CODES[runner.finish()];
|
|
3701
|
+
if (runtime === rt) void close(0);
|
|
3702
|
+
process.stdout.write("", () => process.exit(code));
|
|
3703
|
+
};
|
|
3704
|
+
if (rt.options.installExitHandlers !== false) {
|
|
3705
|
+
let signaled = false;
|
|
3706
|
+
const onSignal = (signal) => {
|
|
3707
|
+
if (externalListenerCount(signal) > 0 || signaled) return;
|
|
3708
|
+
signaled = true;
|
|
3709
|
+
runner.cancel();
|
|
3710
|
+
void Promise.race([runner.done, sleep(LOCAL_RUN_CANCEL_GRACE_MS)]).then(finish);
|
|
3711
|
+
};
|
|
3712
|
+
const onInt = markInternal(() => onSignal("SIGINT"));
|
|
3713
|
+
const onTerm = markInternal(() => onSignal("SIGTERM"));
|
|
3714
|
+
process.on("SIGINT", onInt);
|
|
3715
|
+
process.on("SIGTERM", onTerm);
|
|
3716
|
+
rt.uninstallShutdown = () => {
|
|
3717
|
+
process.off("SIGINT", onInt);
|
|
3718
|
+
process.off("SIGTERM", onTerm);
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
void runner.run();
|
|
3722
|
+
void runner.done.then(finish);
|
|
3723
|
+
}
|
|
3422
3724
|
async function boot(rt) {
|
|
3725
|
+
if (rt.localRun) {
|
|
3726
|
+
rt.booted = true;
|
|
3727
|
+
launchLocalRun(rt, rt.localRun);
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3423
3730
|
if (!rt.instanceId) {
|
|
3424
3731
|
const id = await fallbackInstanceId();
|
|
3425
3732
|
if (runtime !== rt) return;
|
|
@@ -3456,9 +3763,12 @@ function startAgent(fn, mode, handler, options) {
|
|
|
3456
3763
|
}
|
|
3457
3764
|
const logger = makeLogger(options.debug ?? false);
|
|
3458
3765
|
const agentHandler = { mode, handler, options: resolveRunOptions(options, mode, logger) };
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
if (mode === "remote")
|
|
3766
|
+
loadDotenv();
|
|
3767
|
+
const rt = isLocalRun() ? createLocalRunRuntime(options, agentHandler, logger) : createRuntime(options, agentHandler, logger);
|
|
3768
|
+
if (!rt.localRun && mode === "remote") {
|
|
3769
|
+
if (options.pollIntervalMs !== void 0) rt.pollIntervalMs = options.pollIntervalMs;
|
|
3770
|
+
rt.runId = resolveRunId(options.runId);
|
|
3771
|
+
}
|
|
3462
3772
|
runtime = rt;
|
|
3463
3773
|
scheduleBoot(rt);
|
|
3464
3774
|
}
|
|
@@ -3481,10 +3791,10 @@ function forwardAttachment(rt, attachment, message, type) {
|
|
|
3481
3791
|
const reporter = currentReporter();
|
|
3482
3792
|
if (!reporter) {
|
|
3483
3793
|
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o");
|
|
3484
|
-
return;
|
|
3794
|
+
return void 0;
|
|
3485
3795
|
}
|
|
3486
3796
|
const withMeta = typeof attachment === "object" && !isImageAttachment(attachment) && "image" in attachment ? image(attachment.image, { caption: attachment.caption, contentType: attachment.contentType }) : attachment;
|
|
3487
|
-
reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3797
|
+
return reporter.log(toRunLogLevel(type), message, withMeta);
|
|
3488
3798
|
}
|
|
3489
3799
|
function captureException(error, hint) {
|
|
3490
3800
|
const rt = runtime;
|
|
@@ -3495,13 +3805,19 @@ function captureException(error, hint) {
|
|
|
3495
3805
|
}
|
|
3496
3806
|
return void 0;
|
|
3497
3807
|
}
|
|
3808
|
+
let attachmentId;
|
|
3498
3809
|
if (hint?.attachment) {
|
|
3499
3810
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
3500
|
-
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3811
|
+
attachmentId = forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
3812
|
+
}
|
|
3813
|
+
if (!rt.client) {
|
|
3814
|
+
rt.logger.debug("captureException ignorado no modo local (sem Central)");
|
|
3815
|
+
return void 0;
|
|
3501
3816
|
}
|
|
3502
3817
|
const runId = currentReporter()?.runId;
|
|
3503
3818
|
const withRun = runId ? { ...hint, tags: { run_id: runId, ...hint?.tags } } : hint;
|
|
3504
|
-
|
|
3819
|
+
const contexts = runId && attachmentId ? { run: { id: runId, attachments: [attachmentId] } } : void 0;
|
|
3820
|
+
return rt.client.captureException(error, withRun, contexts);
|
|
3505
3821
|
}
|
|
3506
3822
|
async function close(timeoutMs = 3e4) {
|
|
3507
3823
|
const rt = runtime;
|
|
@@ -3511,6 +3827,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3511
3827
|
if (rt.agent) await rt.agent.stop(0);
|
|
3512
3828
|
rt.local?.stop();
|
|
3513
3829
|
rt.attached?.stop();
|
|
3830
|
+
rt.localRun?.stop();
|
|
3514
3831
|
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
3515
3832
|
if (!idle) {
|
|
3516
3833
|
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
@@ -3519,7 +3836,7 @@ async function close(timeoutMs = 3e4) {
|
|
|
3519
3836
|
rt.heartbeat?.stop();
|
|
3520
3837
|
rt.uninstallShutdown?.();
|
|
3521
3838
|
rt.uninstallGlobalHandlers?.();
|
|
3522
|
-
const flushed = await rt.client.close(timeoutMs);
|
|
3839
|
+
const flushed = rt.client ? await rt.client.close(timeoutMs) : true;
|
|
3523
3840
|
return idle && flushed;
|
|
3524
3841
|
}
|
|
3525
3842
|
|
|
@@ -3548,11 +3865,13 @@ var noop = () => void 0;
|
|
|
3548
3865
|
var fallbackEachSink = {
|
|
3549
3866
|
report: noop,
|
|
3550
3867
|
startStep: () => void 0,
|
|
3868
|
+
stepScope: (_step, fn) => fn(),
|
|
3551
3869
|
declareTotal: noop,
|
|
3552
3870
|
advance: noop,
|
|
3553
3871
|
checkCanceled: noop,
|
|
3554
3872
|
isAborted: () => false,
|
|
3555
|
-
logError: (message) => printRunLog("error", message)
|
|
3873
|
+
logError: (message) => printRunLog("error", message),
|
|
3874
|
+
flush: () => Promise.resolve()
|
|
3556
3875
|
};
|
|
3557
3876
|
var fallbackItems = {
|
|
3558
3877
|
add: noop,
|
|
@@ -3571,7 +3890,6 @@ var fallbackFiles = {
|
|
|
3571
3890
|
read: outsideRunFiles
|
|
3572
3891
|
};
|
|
3573
3892
|
var neverAborts = new AbortController().signal;
|
|
3574
|
-
var detachedTransaction;
|
|
3575
3893
|
function resolve(member) {
|
|
3576
3894
|
const reporter = currentReporter();
|
|
3577
3895
|
if (reporter) return reporter.context;
|
|
@@ -3592,8 +3910,12 @@ var hobots = Object.defineProperties({}, {
|
|
|
3592
3910
|
items: getter("items", () => fallbackItems),
|
|
3593
3911
|
progress: getter("progress", () => fallbackProgress),
|
|
3594
3912
|
files: getter("files", () => fallbackFiles),
|
|
3595
|
-
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
3596
3913
|
signal: getter("signal", () => neverAborts),
|
|
3914
|
+
step: method((name, fn, options) => {
|
|
3915
|
+
const ctx = resolve("step");
|
|
3916
|
+
if (ctx) return ctx.step(name, fn, options);
|
|
3917
|
+
return new Transaction(void 0, "hobots.detached").step(name, fn, options);
|
|
3918
|
+
}),
|
|
3597
3919
|
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
3598
3920
|
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
3599
3921
|
flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
|