hobots 1.9.2 → 2.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 +21 -0
- package/dist/index.cjs +230 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -6
- package/dist/index.d.ts +39 -6
- package/dist/index.js +230 -13
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +41 -3
- package/dist/protocol.d.ts +41 -3
- package/docs/api-reference.md +12 -1
- package/docs/concepts.md +2 -2
- package/docs/protocol.md +38 -3
- package/docs/tasks.md +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -260,6 +260,7 @@ interface RunContext {
|
|
|
260
260
|
runId: string;
|
|
261
261
|
task: string;
|
|
262
262
|
log: RunLogger; // ctx.log.debug/info/warn/error/success(message)
|
|
263
|
+
items: RunItems; // ctx.items.succeeded/failed/occurrence — a entrega do bot
|
|
263
264
|
tx: Transaction; // transaction desta run — correlacionada e finalizada pelo SDK
|
|
264
265
|
signal: AbortSignal;
|
|
265
266
|
throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
|
|
@@ -269,6 +270,7 @@ interface RunContext {
|
|
|
269
270
|
|
|
270
271
|
- **`params`** — os dados do formulário configurado no app (JSON validado). Tipável: `Hobots.register<{ lote: number }>(...)`.
|
|
271
272
|
- **`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
|
+
- **`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).
|
|
272
274
|
- **`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).
|
|
273
275
|
- **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**.
|
|
274
276
|
- **`ctx.throwIfCanceled()`** — o ponto de parada de uma linha; lança `RunCanceled` se o `signal` já disparou.
|
|
@@ -278,6 +280,25 @@ interface RunContext {
|
|
|
278
280
|
|
|
279
281
|
> **Dados sensíveis:** `ctx.log` e mensagens/stack de erros **são enviados ao servidor** e ficam visíveis no app. Não inclua senhas, tokens, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
|
|
280
282
|
|
|
283
|
+
### Itens de execução (`ctx.items`)
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
Hobots.register('baixar-notas', async (params, ctx) => {
|
|
287
|
+
for (const nota of await listarNotas(params)) {
|
|
288
|
+
try {
|
|
289
|
+
await baixar(nota);
|
|
290
|
+
ctx.items.succeeded({ numero_nota: nota.numero, cnpj_prestador: nota.cnpj }, { id: nota.numero });
|
|
291
|
+
} catch (erro) {
|
|
292
|
+
ctx.items.failed(String(erro), { numero_nota: nota.numero }, { id: nota.numero });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ocorrência de negócio (não é falha do bot):
|
|
296
|
+
// ctx.items.occurrence('nota nº 10 não encontrada', { numero_nota: '10' }, { id: '10' })
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
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
|
+
|
|
281
302
|
### Anexos (screenshots)
|
|
282
303
|
|
|
283
304
|
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 ? "
|
|
164
|
+
var SDK_VERSION = true ? "2.0.0" : "0.0.0-dev";
|
|
165
165
|
function isNode() {
|
|
166
166
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
167
167
|
}
|
|
@@ -354,7 +354,8 @@ var Transport = class {
|
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
356
|
if (this.buffer.length >= this.maxQueueSize) {
|
|
357
|
-
this.buffer.
|
|
357
|
+
const dropIndex = this.buffer.findIndex((queued) => queued.type !== "heartbeat");
|
|
358
|
+
this.buffer.splice(dropIndex === -1 ? 0 : dropIndex, 1);
|
|
358
359
|
this.logger.warn("fila cheia, item mais antigo descartado");
|
|
359
360
|
}
|
|
360
361
|
this.buffer.push(item);
|
|
@@ -445,6 +446,8 @@ function runtimeContexts() {
|
|
|
445
446
|
}
|
|
446
447
|
var Client = class {
|
|
447
448
|
logger;
|
|
449
|
+
/** Eco de lifecycle (Transaction.emit) e afins só imprimem no terminal com debug ligado. */
|
|
450
|
+
debug;
|
|
448
451
|
// campos privados + getters de protótipo: o clientSecret não aparece em
|
|
449
452
|
// JSON.stringify/serialização enumerável do Client (ex.: um objeto capturado
|
|
450
453
|
// que referencie o client), mas client.options/config seguem funcionando
|
|
@@ -453,7 +456,8 @@ var Client = class {
|
|
|
453
456
|
transport;
|
|
454
457
|
constructor(options, config = resolveConfig(options)) {
|
|
455
458
|
this.#options = options;
|
|
456
|
-
this.
|
|
459
|
+
this.debug = !!options.debug;
|
|
460
|
+
this.logger = makeLogger(this.debug);
|
|
457
461
|
this.#config = config;
|
|
458
462
|
this.transport = new Transport({
|
|
459
463
|
endpoint: this.#config.ingestEndpoint,
|
|
@@ -540,6 +544,8 @@ var Client = class {
|
|
|
540
544
|
|
|
541
545
|
// src/heartbeat.ts
|
|
542
546
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
547
|
+
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
548
|
+
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
543
549
|
var Heartbeat = class {
|
|
544
550
|
constructor(client2, options = {}, instanceId) {
|
|
545
551
|
this.client = client2;
|
|
@@ -556,12 +562,22 @@ var Heartbeat = class {
|
|
|
556
562
|
start() {
|
|
557
563
|
if (this.timer) return;
|
|
558
564
|
this.beat();
|
|
559
|
-
this.
|
|
565
|
+
this.schedule();
|
|
566
|
+
}
|
|
567
|
+
/** Jitter de ±10%: uma frota iniciada junto não sincroniza todos os beats no mesmo segundo. */
|
|
568
|
+
schedule() {
|
|
569
|
+
this.timer = setTimeout(
|
|
570
|
+
() => {
|
|
571
|
+
this.beat();
|
|
572
|
+
this.schedule();
|
|
573
|
+
},
|
|
574
|
+
this.intervalMs * (0.9 + Math.random() * 0.2)
|
|
575
|
+
);
|
|
560
576
|
unrefTimer(this.timer);
|
|
561
577
|
}
|
|
562
578
|
stop() {
|
|
563
579
|
if (this.timer) {
|
|
564
|
-
|
|
580
|
+
clearTimeout(this.timer);
|
|
565
581
|
this.timer = void 0;
|
|
566
582
|
}
|
|
567
583
|
}
|
|
@@ -569,7 +585,7 @@ var Heartbeat = class {
|
|
|
569
585
|
this.client.sendHeartbeat({
|
|
570
586
|
instance_id: this.instanceId,
|
|
571
587
|
name: this.name,
|
|
572
|
-
interval_ms: this.intervalMs,
|
|
588
|
+
interval_ms: Math.min(Math.max(this.intervalMs, MIN_REPORTED_INTERVAL_MS), MAX_REPORTED_INTERVAL_MS),
|
|
573
589
|
timestamp: nowISO(),
|
|
574
590
|
metadata: this.metadata
|
|
575
591
|
});
|
|
@@ -816,15 +832,18 @@ var Transaction = class {
|
|
|
816
832
|
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
817
833
|
lifecycleBacklog = [];
|
|
818
834
|
/**
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
835
|
+
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
836
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
837
|
+
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
838
|
+
* escreve no stdout do processo do usuário sem permissão.
|
|
822
839
|
*/
|
|
823
840
|
emit(kind, phase, label, startTs, endTs, status) {
|
|
824
841
|
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
825
|
-
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
826
842
|
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
827
|
-
|
|
843
|
+
if (this.client?.debug) {
|
|
844
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
845
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
846
|
+
}
|
|
828
847
|
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
829
848
|
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
830
849
|
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
@@ -987,6 +1006,38 @@ var TasksApi = class {
|
|
|
987
1006
|
signal: AbortSignal.timeout(timeoutMs)
|
|
988
1007
|
});
|
|
989
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
|
|
1011
|
+
* 4xx é definitivo (404 = servidor sem o recurso, 429 = cap de itens por run) —
|
|
1012
|
+
* o RunReporter trata por status.
|
|
1013
|
+
*/
|
|
1014
|
+
async createItems(runId, request) {
|
|
1015
|
+
let response;
|
|
1016
|
+
try {
|
|
1017
|
+
response = await fetchWithRetry(
|
|
1018
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/items`, {
|
|
1019
|
+
method: "POST",
|
|
1020
|
+
headers: this.headers(),
|
|
1021
|
+
body: JSON.stringify(request),
|
|
1022
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1023
|
+
}),
|
|
1024
|
+
{ logger: this.logger, label: `itens da run ${runId}` }
|
|
1025
|
+
);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error instanceof Error && error.message === "HTTP 429") {
|
|
1028
|
+
throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
|
|
1029
|
+
}
|
|
1030
|
+
throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (!response.ok) {
|
|
1033
|
+
throw new TasksApiError(`envio de itens rejeitado (HTTP ${response.status})`, response.status);
|
|
1034
|
+
}
|
|
1035
|
+
const body = await response.json().catch(() => null);
|
|
1036
|
+
return {
|
|
1037
|
+
accepted: typeof body?.accepted === "number" ? body.accepted : request.items.length,
|
|
1038
|
+
duplicates: typeof body?.duplicates === "number" ? body.duplicates : 0
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
990
1041
|
/**
|
|
991
1042
|
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
992
1043
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
@@ -1144,6 +1195,22 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
|
1144
1195
|
var MAX_LOG_MESSAGE_CHARS = 1e4;
|
|
1145
1196
|
var MAX_LOGS_PER_PATCH = 100;
|
|
1146
1197
|
var FLUSH_AT_BUFFERED = 20;
|
|
1198
|
+
var MAX_ITEMS_PER_RUN = 1e4;
|
|
1199
|
+
var MAX_ITEMS_PER_REQUEST = 500;
|
|
1200
|
+
var MAX_ITEM_MESSAGE_CHARS = 2e3;
|
|
1201
|
+
var MAX_ITEM_PAYLOAD_BYTES = 16384;
|
|
1202
|
+
var ITEMS_FLUSH_AT_BUFFERED = 100;
|
|
1203
|
+
var ITEMS_DRAIN_MS = 15e3;
|
|
1204
|
+
function scrubPayloadValue(value, depth) {
|
|
1205
|
+
if (typeof value === "string") return scrubSecrets(value);
|
|
1206
|
+
if (depth >= 4 || value === null || typeof value !== "object") return value;
|
|
1207
|
+
if (Array.isArray(value)) return value.map((entry) => scrubPayloadValue(entry, depth + 1));
|
|
1208
|
+
const out = {};
|
|
1209
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1210
|
+
out[key] = scrubPayloadValue(entry, depth + 1);
|
|
1211
|
+
}
|
|
1212
|
+
return out;
|
|
1213
|
+
}
|
|
1147
1214
|
var RunReporter = class {
|
|
1148
1215
|
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1149
1216
|
this.api = api;
|
|
@@ -1162,11 +1229,18 @@ var RunReporter = class {
|
|
|
1162
1229
|
success: (message) => this.log("success", message)
|
|
1163
1230
|
};
|
|
1164
1231
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1232
|
+
const items = {
|
|
1233
|
+
add: (item) => this.addItem(item.status, item.message, item.payload, item.id),
|
|
1234
|
+
succeeded: (payload, options) => this.addItem("succeeded", void 0, payload, options?.id),
|
|
1235
|
+
failed: (message, payload, options) => this.addItem("failed", message, payload, options?.id),
|
|
1236
|
+
occurrence: (message, payload, options) => this.addItem("occurrence", message, payload, options?.id)
|
|
1237
|
+
};
|
|
1165
1238
|
const signal = this.abortController.signal;
|
|
1166
1239
|
this.context = {
|
|
1167
1240
|
runId,
|
|
1168
1241
|
task,
|
|
1169
1242
|
log,
|
|
1243
|
+
items,
|
|
1170
1244
|
tx,
|
|
1171
1245
|
signal,
|
|
1172
1246
|
throwIfCanceled: () => {
|
|
@@ -1185,6 +1259,23 @@ var RunReporter = class {
|
|
|
1185
1259
|
abortController = new AbortController();
|
|
1186
1260
|
flushIntervalMs;
|
|
1187
1261
|
cancelCheckIntervalMs;
|
|
1262
|
+
// Itens de execução vivem FORA de `this.chain` (status/liveness não pode ficar
|
|
1263
|
+
// atrás de milhares de itens), mas têm cadeia própria serializada — ordem de
|
|
1264
|
+
// envio preservada e sem lotes sobrepostos.
|
|
1265
|
+
itemsBuffer = [];
|
|
1266
|
+
itemsChain = Promise.resolve();
|
|
1267
|
+
itemsTimer;
|
|
1268
|
+
itemSeq = 0;
|
|
1269
|
+
itemCount = 0;
|
|
1270
|
+
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1271
|
+
itemsUnsupported = false;
|
|
1272
|
+
/** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
|
|
1273
|
+
itemsQuotaExceeded = false;
|
|
1274
|
+
/**
|
|
1275
|
+
* Só o kill() do exit path fecha os itens — `dead` (run cancelada/finalizada no
|
|
1276
|
+
* servidor) NÃO fecha: o servidor aceita itens de run terminal (trabalho já feito).
|
|
1277
|
+
*/
|
|
1278
|
+
itemsClosed = false;
|
|
1188
1279
|
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1189
1280
|
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1190
1281
|
pendingAttachments = /* @__PURE__ */ new Set();
|
|
@@ -1211,6 +1302,12 @@ var RunReporter = class {
|
|
|
1211
1302
|
this.dead = true;
|
|
1212
1303
|
this.clearFlushTimer();
|
|
1213
1304
|
this.buffer = [];
|
|
1305
|
+
this.itemsClosed = true;
|
|
1306
|
+
this.clearItemsTimer();
|
|
1307
|
+
if (this.itemsBuffer.length > 0) {
|
|
1308
|
+
this.logger.warn(`${this.itemsBuffer.length} item(ns) de execu\xE7\xE3o descartados (processo encerrando)`);
|
|
1309
|
+
this.itemsBuffer = [];
|
|
1310
|
+
}
|
|
1214
1311
|
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
1215
1312
|
for (const resume of waiting) resume();
|
|
1216
1313
|
}
|
|
@@ -1313,6 +1410,120 @@ var RunReporter = class {
|
|
|
1313
1410
|
finalize() {
|
|
1314
1411
|
return this.chain;
|
|
1315
1412
|
}
|
|
1413
|
+
// ── Itens de execução ────────────────────────────────────────────────────────
|
|
1414
|
+
/**
|
|
1415
|
+
* Bufferiza um item (síncrono — a chamada nunca espera rede). Diferente do PATCH
|
|
1416
|
+
* de status, o servidor aceita itens mesmo com a run cancelada (trabalho já feito),
|
|
1417
|
+
* então um reporter morto não descarta: só um kill() de exit path descarta.
|
|
1418
|
+
*/
|
|
1419
|
+
addItem(status, message, payload, id) {
|
|
1420
|
+
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
1421
|
+
if (this.itemCount >= MAX_ITEMS_PER_RUN) {
|
|
1422
|
+
if (!this.itemsQuotaExceeded) {
|
|
1423
|
+
this.itemsQuotaExceeded = true;
|
|
1424
|
+
this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
this.itemCount++;
|
|
1429
|
+
let scrubbedPayload;
|
|
1430
|
+
if (payload) {
|
|
1431
|
+
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
1432
|
+
let serializedLength;
|
|
1433
|
+
try {
|
|
1434
|
+
serializedLength = JSON.stringify(scrubbedPayload).length;
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
if (serializedLength === void 0 || serializedLength > MAX_ITEM_PAYLOAD_BYTES) {
|
|
1438
|
+
this.logger.warn(
|
|
1439
|
+
`payload do item descartado (${serializedLength === void 0 ? "n\xE3o serializ\xE1vel" : `${serializedLength} bytes > ${MAX_ITEM_PAYLOAD_BYTES}`}) \u2014 o item foi mantido`
|
|
1440
|
+
);
|
|
1441
|
+
scrubbedPayload = void 0;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
this.itemsBuffer.push({
|
|
1445
|
+
id: id ? truncate(String(id), 255) : uuid4(),
|
|
1446
|
+
seq: this.itemSeq++,
|
|
1447
|
+
status,
|
|
1448
|
+
...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1449
|
+
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
1450
|
+
ts: nowISO()
|
|
1451
|
+
});
|
|
1452
|
+
if (this.itemsBuffer.length >= ITEMS_FLUSH_AT_BUFFERED) {
|
|
1453
|
+
this.flushItems();
|
|
1454
|
+
} else {
|
|
1455
|
+
this.scheduleItemsFlush();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
scheduleItemsFlush() {
|
|
1459
|
+
if (this.itemsTimer) return;
|
|
1460
|
+
this.itemsTimer = setTimeout(() => {
|
|
1461
|
+
this.itemsTimer = void 0;
|
|
1462
|
+
this.flushItems();
|
|
1463
|
+
}, this.flushIntervalMs);
|
|
1464
|
+
unrefTimer(this.itemsTimer);
|
|
1465
|
+
}
|
|
1466
|
+
clearItemsTimer() {
|
|
1467
|
+
if (this.itemsTimer) {
|
|
1468
|
+
clearTimeout(this.itemsTimer);
|
|
1469
|
+
this.itemsTimer = void 0;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/** Envia o buffer de itens em lotes serializados na cadeia própria. */
|
|
1473
|
+
flushItems() {
|
|
1474
|
+
this.clearItemsTimer();
|
|
1475
|
+
if (this.itemsBuffer.length === 0 || this.itemsUnsupported) return;
|
|
1476
|
+
const items = this.itemsBuffer.splice(0, this.itemsBuffer.length);
|
|
1477
|
+
for (let i = 0; i < items.length; i += MAX_ITEMS_PER_REQUEST) {
|
|
1478
|
+
const batch = items.slice(i, i + MAX_ITEMS_PER_REQUEST);
|
|
1479
|
+
this.itemsChain = this.itemsChain.then(() => this.sendItems(batch));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async sendItems(batch) {
|
|
1483
|
+
if (this.itemsUnsupported) return;
|
|
1484
|
+
try {
|
|
1485
|
+
await this.api.createItems(this.runId, { items: batch });
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
1488
|
+
this.itemsUnsupported = true;
|
|
1489
|
+
this.logger.warn("itens de execu\xE7\xE3o n\xE3o s\xE3o suportados por este servidor Hobots \u2014 descartados");
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
1493
|
+
if (!this.itemsQuotaExceeded) {
|
|
1494
|
+
this.itemsQuotaExceeded = true;
|
|
1495
|
+
this.log("warning", "itens descartados: limite de itens por execu\xE7\xE3o atingido no servidor");
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (error instanceof TasksApiError) {
|
|
1500
|
+
this.logger.error(`lote de ${batch.length} item(ns) rejeitado pelo servidor:`, error);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
this.itemsBuffer.unshift(...batch);
|
|
1504
|
+
this.scheduleItemsFlush();
|
|
1505
|
+
this.logger.error(`lote de ${batch.length} item(ns) n\xE3o enviado (devolvido ao buffer):`, error);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Dreno final de itens antes do PATCH terminal (com teto): garante que os
|
|
1510
|
+
* totalizadores estejam completos quando a run vira succeeded/failed. Como o
|
|
1511
|
+
* servidor aceita itens tardios, perder a corrida aqui não perde dados.
|
|
1512
|
+
*/
|
|
1513
|
+
async finalizeItems(timeoutMs = ITEMS_DRAIN_MS) {
|
|
1514
|
+
this.flushItems();
|
|
1515
|
+
await Promise.race([
|
|
1516
|
+
this.itemsChain,
|
|
1517
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1518
|
+
]);
|
|
1519
|
+
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1520
|
+
this.flushItems();
|
|
1521
|
+
await Promise.race([
|
|
1522
|
+
this.itemsChain,
|
|
1523
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1524
|
+
]);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1316
1527
|
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
1317
1528
|
/**
|
|
1318
1529
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
@@ -1679,13 +1890,17 @@ var TaskAgent = class {
|
|
|
1679
1890
|
try {
|
|
1680
1891
|
const response = await this.api.poll(
|
|
1681
1892
|
{
|
|
1682
|
-
|
|
1893
|
+
instance: {
|
|
1683
1894
|
// identidade canônica do agente no painel — o mesmo instanceId do
|
|
1684
1895
|
// heartbeat da Telemetria, então os dois canais convergem em um agente
|
|
1685
1896
|
instance_id: this.instanceId,
|
|
1686
1897
|
version: SDK_VERSION,
|
|
1687
1898
|
host: hostname(),
|
|
1688
|
-
tasks: [...this.registry.keys()]
|
|
1899
|
+
tasks: [...this.registry.keys()],
|
|
1900
|
+
// o servidor calcula a tolerância de offline (3×) a partir disto em
|
|
1901
|
+
// agentes poll-only — sem o campo, um poll lento flaparia offline/online.
|
|
1902
|
+
// Clampado à faixa aceita pelo schema do poll (1s..15min).
|
|
1903
|
+
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5)
|
|
1689
1904
|
},
|
|
1690
1905
|
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
1691
1906
|
tasks: [...this.registry.keys()]
|
|
@@ -1757,12 +1972,14 @@ var TaskAgent = class {
|
|
|
1757
1972
|
() => this.invokeWithTimeout(handler, run, reporter)
|
|
1758
1973
|
);
|
|
1759
1974
|
await reporter.finalizeAttachments();
|
|
1975
|
+
await reporter.finalizeItems();
|
|
1760
1976
|
tx.setStatus("ok");
|
|
1761
1977
|
tx.finish();
|
|
1762
1978
|
reporter.reportSuccess();
|
|
1763
1979
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
1764
1980
|
} catch (error) {
|
|
1765
1981
|
await reporter.finalizeAttachments();
|
|
1982
|
+
await reporter.finalizeItems();
|
|
1766
1983
|
tx.setStatus("error");
|
|
1767
1984
|
tx.finish();
|
|
1768
1985
|
reporter.reportFailure(toRunError(error));
|