hobots 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/index.cjs +83 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +83 -3
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +13 -1
- package/dist/protocol.d.ts +13 -1
- package/docs/api-reference.md +10 -0
- package/docs/protocol.md +3 -1
- package/docs/tasks.md +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -261,6 +261,7 @@ interface RunContext {
|
|
|
261
261
|
task: string;
|
|
262
262
|
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
263
263
|
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
264
|
+
progress: RunProgressReporter; // ctx.progress.total/advance/set — barra + tempo restante no app
|
|
264
265
|
tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
|
|
265
266
|
signal: AbortSignal;
|
|
266
267
|
throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
|
|
@@ -271,6 +272,7 @@ interface RunContext {
|
|
|
271
272
|
- **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
|
|
272
273
|
- **`ctx.log.<level>(message)`** — níveis `debug`, `info`, `warn`, `error` e `success`; logs enviados **incrementalmente** ao app (PATCHes com throttle), visíveis na solicitação quase em tempo real; também impressos no terminal local, coloridos por nível.
|
|
273
274
|
- **`ctx.items`** — registra os **itens que a execução processou/gerou** (a entrega do bot: notas fiscais baixadas, boletos emitidos…), cada um com status `succeeded`, `failed` (falha técnica) ou `occurrence` (ocorrência de negócio, ex.: "nota nº 10 não encontrada") e um `payload` de atributos livres. Aparecem na aba **itens** da solicitação, com totalizadores; as colunas exibidas são configuradas por formulário no app. Guia: [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
275
|
+
- **`ctx.progress`** — **opcional**: declare quantos itens a run vai processar (`ctx.progress.total(n)`) e avance a cada item (`ctx.progress.advance()`); o app mostra barra, percentual e tempo restante estimado na solicitação. Independente de `ctx.items`. Guia: [Progresso](./docs/tasks.md#progresso-da-execução-ctxprogress).
|
|
274
276
|
- **`ctx.tx`** — a transaction de performance desta run, já correlacionada e finalizada pelo SDK. Use `ctx.tx.startChild(op, description)` para medir as etapas — elas viram o waterfall da execução no app. `startTransaction()` chamado dentro do handler retorna essa mesma transaction. Conceito: [ctx.tx](./docs/concepts.md#transaction-de-run-ctxtx).
|
|
275
277
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
276
278
|
- **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
|
|
@@ -299,6 +301,23 @@ Hobots.register('baixar-notas', async (params, ctx) => {
|
|
|
299
301
|
|
|
300
302
|
Chamadas síncronas e bufferizadas (lotes de até 500; dreno antes do reporte final). O `id` opcional é a identidade de negócio do item na run — repetido, é ignorado pelo servidor (registro único, retry idempotente). Limites: 10.000 itens/run, payload 16 KB, mensagem 2.000 caracteres. Detalhes em [Itens de execução](./docs/tasks.md#itens-de-execução-ctxitems).
|
|
301
303
|
|
|
304
|
+
### Progresso (`ctx.progress`)
|
|
305
|
+
|
|
306
|
+
Quando a run percorre uma lista conhecida, declare o total e avance a cada item — a solicitação no app ganha barra de progresso, percentual (`10/100 · 10%`) e **tempo restante estimado**:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
Hobots.register('baixar-notas', async (params, ctx) => {
|
|
310
|
+
const notas = await listarNotas(params);
|
|
311
|
+
ctx.progress.total(notas.length);
|
|
312
|
+
for (const nota of notas) {
|
|
313
|
+
await baixar(nota);
|
|
314
|
+
ctx.progress.advance();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Opcional e independente de `ctx.items` (use um, outro ou ambos). Sem `total()` nada é enviado. Chamadas síncronas e baratas: o snapshot pega carona nos PATCHes de log e, no pior caso, sai 1 vez por segundo; o PATCH terminal sempre leva o valor final. `ctx.progress.set(current, total?)` define um valor absoluto; `ctx.progress.current` e `ctx.progress.snapshot` leem o estado. Detalhes em [Progresso da execução](./docs/tasks.md#progresso-da-execução-ctxprogress).
|
|
320
|
+
|
|
302
321
|
### Anexos (screenshots)
|
|
303
322
|
|
|
304
323
|
Texto de log conta o que o robô fez; um print conta **como estava a tela**. Os métodos de captura (`Hobots.captureException` e `Hobots.captureMessage`) aceitam um `attachment` no hint: dentro de uma execução de task, a imagem vira anexo da linha de log da run que espelha a captura — no app ela aparece como miniatura logo abaixo dessa linha, clicável para ampliar.
|
package/dist/index.cjs
CHANGED
|
@@ -161,7 +161,7 @@ function scrubSecrets(text) {
|
|
|
161
161
|
|
|
162
162
|
// src/utils.ts
|
|
163
163
|
var SDK_NAME = "hobots";
|
|
164
|
-
var SDK_VERSION = true ? "2.0
|
|
164
|
+
var SDK_VERSION = true ? "2.2.0" : "0.0.0-dev";
|
|
165
165
|
function isNode() {
|
|
166
166
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
167
167
|
}
|
|
@@ -1201,6 +1201,7 @@ var MAX_ITEM_MESSAGE_CHARS = 2e3;
|
|
|
1201
1201
|
var MAX_ITEM_PAYLOAD_BYTES = 16384;
|
|
1202
1202
|
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1203
1203
|
var ITEMS_DRAIN_MS = 15e3;
|
|
1204
|
+
var PROGRESS_FLUSH_MS = 1e3;
|
|
1204
1205
|
function scrubPayloadValue(value, depth) {
|
|
1205
1206
|
if (typeof value === "string") return scrubSecrets(value);
|
|
1206
1207
|
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
@@ -1235,12 +1236,28 @@ var RunReporter = class {
|
|
|
1235
1236
|
failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
|
|
1236
1237
|
occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
|
|
1237
1238
|
};
|
|
1239
|
+
const progress = Object.defineProperties(
|
|
1240
|
+
{
|
|
1241
|
+
total: (total) => this.setProgress(this.progressCurrent, total),
|
|
1242
|
+
advance: (count = 1) => this.setProgress(this.progressCurrent + count, this.progressTotal),
|
|
1243
|
+
set: (current, total) => this.setProgress(current, total ?? this.progressTotal)
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
current: { enumerable: true, get: () => this.progressCurrent },
|
|
1247
|
+
snapshot: {
|
|
1248
|
+
enumerable: true,
|
|
1249
|
+
get: () => this.progressTotal == null ? null : { current: this.progressCurrent, total: this.progressTotal }
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1253
|
+
);
|
|
1238
1254
|
const signal = this.abortController.signal;
|
|
1239
1255
|
this.context = {
|
|
1240
1256
|
runId,
|
|
1241
1257
|
task,
|
|
1242
1258
|
log,
|
|
1243
1259
|
items,
|
|
1260
|
+
progress,
|
|
1244
1261
|
tx,
|
|
1245
1262
|
signal,
|
|
1246
1263
|
throwIfCanceled: () => {
|
|
@@ -1284,6 +1301,14 @@ var RunReporter = class {
|
|
|
1284
1301
|
uploadQueue = [];
|
|
1285
1302
|
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1286
1303
|
attachmentsUnsupported = false;
|
|
1304
|
+
// Progresso (ctx.progress): snapshot absoluto, enviado com throttle via piggyback
|
|
1305
|
+
// em `enqueue()` — nunca gera tráfego próprio enquanto logs/heartbeat já fluem.
|
|
1306
|
+
progressCurrent = 0;
|
|
1307
|
+
progressTotal = null;
|
|
1308
|
+
/** Snapshot ainda não entregue ao servidor. */
|
|
1309
|
+
progressDirty = false;
|
|
1310
|
+
progressTimer;
|
|
1311
|
+
progressWarned = false;
|
|
1287
1312
|
get isDead() {
|
|
1288
1313
|
return this.dead;
|
|
1289
1314
|
}
|
|
@@ -1301,6 +1326,7 @@ var RunReporter = class {
|
|
|
1301
1326
|
this.abort();
|
|
1302
1327
|
this.dead = true;
|
|
1303
1328
|
this.clearFlushTimer();
|
|
1329
|
+
this.clearProgressTimer();
|
|
1304
1330
|
this.buffer = [];
|
|
1305
1331
|
this.itemsClosed = true;
|
|
1306
1332
|
this.clearItemsTimer();
|
|
@@ -1380,13 +1406,63 @@ var RunReporter = class {
|
|
|
1380
1406
|
reportSuccess() {
|
|
1381
1407
|
this.stopCancelWatch();
|
|
1382
1408
|
this.clearFlushTimer();
|
|
1409
|
+
this.forceFinalProgress();
|
|
1383
1410
|
this.enqueue({ status: "succeeded", logs: this.takeLogs() });
|
|
1384
1411
|
}
|
|
1385
1412
|
reportFailure(error) {
|
|
1386
1413
|
this.stopCancelWatch();
|
|
1387
1414
|
this.clearFlushTimer();
|
|
1415
|
+
this.forceFinalProgress();
|
|
1388
1416
|
this.enqueue({ status: "failed", error, logs: this.takeLogs() });
|
|
1389
1417
|
}
|
|
1418
|
+
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
1419
|
+
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
1420
|
+
setProgress(current, total) {
|
|
1421
|
+
if (this.dead) return;
|
|
1422
|
+
if (total != null && (!Number.isInteger(total) || total < 1)) {
|
|
1423
|
+
if (!this.progressWarned) {
|
|
1424
|
+
this.progressWarned = true;
|
|
1425
|
+
this.logger.warn(`ctx.progress: total inv\xE1lido (${String(total)}) \u2014 precisa ser inteiro >= 1; ignorado`);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const safeCurrent = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : this.progressCurrent;
|
|
1430
|
+
const next = total == null ? safeCurrent : Math.min(safeCurrent, total);
|
|
1431
|
+
if (next === this.progressCurrent && total === this.progressTotal) return;
|
|
1432
|
+
this.progressCurrent = next;
|
|
1433
|
+
this.progressTotal = total;
|
|
1434
|
+
if (total == null) return;
|
|
1435
|
+
this.progressDirty = true;
|
|
1436
|
+
if (this.progressTimer) return;
|
|
1437
|
+
this.progressTimer = setTimeout(() => {
|
|
1438
|
+
this.progressTimer = void 0;
|
|
1439
|
+
this.flushProgress();
|
|
1440
|
+
}, PROGRESS_FLUSH_MS);
|
|
1441
|
+
unrefTimer(this.progressTimer);
|
|
1442
|
+
}
|
|
1443
|
+
flushProgress() {
|
|
1444
|
+
this.clearProgressTimer();
|
|
1445
|
+
if (this.dead || !this.progressDirty) return;
|
|
1446
|
+
if (this.buffer.length > 0) this.flushLogs();
|
|
1447
|
+
else this.enqueue({ status: "running" });
|
|
1448
|
+
}
|
|
1449
|
+
clearProgressTimer() {
|
|
1450
|
+
if (this.progressTimer) {
|
|
1451
|
+
clearTimeout(this.progressTimer);
|
|
1452
|
+
this.progressTimer = void 0;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/** O PATCH terminal sempre leva o snapshot final (o app mostra "87/100" numa run que falhou). */
|
|
1456
|
+
forceFinalProgress() {
|
|
1457
|
+
this.clearProgressTimer();
|
|
1458
|
+
this.progressDirty = this.progressTotal != null;
|
|
1459
|
+
}
|
|
1460
|
+
/** Consome o snapshot pendente para pegar carona no próximo PATCH. */
|
|
1461
|
+
takeProgress() {
|
|
1462
|
+
if (!this.progressDirty || this.progressTotal == null) return void 0;
|
|
1463
|
+
this.progressDirty = false;
|
|
1464
|
+
return { current: this.progressCurrent, total: this.progressTotal };
|
|
1465
|
+
}
|
|
1390
1466
|
/**
|
|
1391
1467
|
* Heartbeat de cancelamento: enquanto o handler roda, um PATCH 'running' vazio
|
|
1392
1468
|
* periódico faz o servidor responder 409 se a run foi cancelada no app — o que
|
|
@@ -1649,11 +1725,14 @@ var RunReporter = class {
|
|
|
1649
1725
|
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1650
1726
|
]);
|
|
1651
1727
|
}
|
|
1652
|
-
enqueue(
|
|
1728
|
+
enqueue(updateIn) {
|
|
1653
1729
|
if (this.dead) {
|
|
1654
|
-
this.salvageLogs(
|
|
1730
|
+
this.salvageLogs(updateIn.logs);
|
|
1655
1731
|
return;
|
|
1656
1732
|
}
|
|
1733
|
+
this.clearProgressTimer();
|
|
1734
|
+
const progress = this.takeProgress();
|
|
1735
|
+
const update = progress ? { ...updateIn, progress } : updateIn;
|
|
1657
1736
|
this.pendingUpdates++;
|
|
1658
1737
|
this.chain = this.chain.then(async () => {
|
|
1659
1738
|
if (this.dead) {
|
|
@@ -1679,6 +1758,7 @@ var RunReporter = class {
|
|
|
1679
1758
|
this.buffer.unshift(...update.logs);
|
|
1680
1759
|
this.scheduleFlush();
|
|
1681
1760
|
}
|
|
1761
|
+
if (update.progress && this.progressTotal != null) this.progressDirty = true;
|
|
1682
1762
|
this.logger.error(`update da run ${this.runId} perdido:`, error);
|
|
1683
1763
|
}
|
|
1684
1764
|
}).finally(() => {
|