hobots 3.0.0 → 3.2.1
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 +9 -3
- package/dist/index.cjs +177 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -14
- package/dist/index.d.ts +37 -14
- package/dist/index.js +177 -66
- 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 +3 -2
- package/docs/protocol.md +4 -1
- package/docs/tasks.md +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ CommonJS: `const { hobots } = require('hobots')`.
|
|
|
27
27
|
```bash
|
|
28
28
|
export HOBOTS_CLIENT_SECRET=hb_<64 hex> # Configurações → Agentes
|
|
29
29
|
export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — default: hostname da máquina
|
|
30
|
+
export HOBOTS_ENVIRONMENT=staging # opcional — default: production
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
```js
|
|
@@ -65,6 +66,7 @@ O objeto **`hobots`** é global: `hobots.log`, `hobots.items`, `hobots.progress`
|
|
|
65
66
|
| --- | --- | --- |
|
|
66
67
|
| `HOBOTS_CLIENT_SECRET` | `clientSecret` | O **Client Secret do agente** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. Viaja só no header `Authorization: Bearer`, nunca na URL. Ausente ou inválido → `register()`/`start()` lança na hora, citando a env (sem ecoar o valor). |
|
|
67
68
|
| `HOBOTS_INSTANCE_ID` | `instanceId` | Identidade **estável** desta instância do robô (`vm-cliente-01`). Sem ela, o hostname. Heartbeat e tasks convergem nela: **um agente só** no painel. |
|
|
69
|
+
| `HOBOTS_ENVIRONMENT` | `environment` | Ambiente lógico do robô (`development` \| `testing` \| `staging` \| `production`). Sem ela, `production`. |
|
|
68
70
|
| `HOBOTS_API_URL` | — | Só em desenvolvimento: aponta o SDK para outro host (`http://localhost:3001`). Em produção é sempre `https://api2.hobots.app`. |
|
|
69
71
|
|
|
70
72
|
A opção passada em `register()`/`start()` tem precedência sobre a env — útil para copiar e colar o snippet do app.
|
|
@@ -91,8 +93,8 @@ Os dois podem coexistir no mesmo processo: um monitor sempre ligado + tasks sob
|
|
|
91
93
|
|
|
92
94
|
| Membro | O que faz |
|
|
93
95
|
| --- | --- |
|
|
94
|
-
| `hobots.log.debug/info/warn/error/success(msg)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). **Nunca lança** — pode ir em `finally`. |
|
|
95
|
-
| `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app). Bufferizado, em lotes. |
|
|
96
|
+
| `hobots.log.debug/info/warn/error/success(msg, ...rest)` | Log da execução, enviado incrementalmente ao app (e ecoado no terminal). Aceita vários argumentos como `console.log` — `hobots.log.info('nota', numero, 'ok')` — cada um vira texto, unidos por espaço. **Nunca lança** — pode ir em `finally`. |
|
|
97
|
+
| `hobots.items.succeeded(payload?, { id }?)` · `.failed(msg, payload?, …)` · `.occurrence(msg, payload?, …)` · `.add({...})` | **A entrega do robô**: cada item processado, com status e atributos (viram colunas no app — declare-as com a opção `items: { columns: [...] }` do `register`/`start`). Bufferizado, em lotes. |
|
|
96
98
|
| `hobots.progress.total(n)` · `.advance(k = 1)` · `.set(atual, total?)` · `.current` · `.snapshot` | Barra, percentual e tempo restante na solicitação. Sem `total()` nada é enviado. |
|
|
97
99
|
| `hobots.transaction.startChild(op, descrição)` | Uma **step** no waterfall de performance da execução. A transaction da execução já existe e é finalizada pelo SDK. |
|
|
98
100
|
| `hobots.attach(imagem, { caption?, level? })` | Pendura um **print** (PNG/JPEG/WEBP, até 5 MB, 20 por execução) na linha de log. Nunca lança nem bloqueia. |
|
|
@@ -143,7 +145,7 @@ hobots.start(task, handler, options?: StartOptions);
|
|
|
143
145
|
| --- | --- | --- |
|
|
144
146
|
| `clientSecret` | env `HOBOTS_CLIENT_SECRET` | Client Secret do agente |
|
|
145
147
|
| `instanceId` | env `HOBOTS_INSTANCE_ID` → hostname | Identidade da instância |
|
|
146
|
-
| `environment` | `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
|
|
148
|
+
| `environment` | env `HOBOTS_ENVIRONMENT` → `'production'` | `development` \| `testing` \| `staging` \| `production` — decide quem é alertado |
|
|
147
149
|
| `release` | — | Versão do programa, ex. `robo-nfe@1.4.2` |
|
|
148
150
|
| `tags` | — | Tags de todos os eventos |
|
|
149
151
|
| `debug` | `false` | Loga a atividade do SDK |
|
|
@@ -176,6 +178,10 @@ Sem handler próprio de sinal, um `SIGINT`/`SIGTERM` (ou erro fatal) com execuç
|
|
|
176
178
|
|
|
177
179
|
---
|
|
178
180
|
|
|
181
|
+
## Changelog resumido
|
|
182
|
+
|
|
183
|
+
- **3.1.0** — `hobots.log.*` aceita vários argumentos (como `console.log`); `retries` inválido (`NaN`) cai no default em vez de tentar para sempre; 429 de itens e 429/501/503 de anexo são definitivos (sem esperar a ladder — `quota`/`unsupported` passam a funcionar de fato); timeout **por requisição** nos anexos; truncamento nos limites do servidor (tag 512, fingerprint 10×255, nome de transaction 500, 200 steps, `exception.value` 8192, `filename` 1024); PNG acima de 20 000 px não declara dimensões; PATCH terminal nunca passa de 100 logs.
|
|
184
|
+
|
|
179
185
|
## Documentação
|
|
180
186
|
|
|
181
187
|
- [Getting started](./docs/getting-started.md) — instalação, envs, primeira task nos dois modos.
|
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
42
42
|
|
|
43
43
|
// src/stacktrace.ts
|
|
44
44
|
var MAX_FRAMES = 50;
|
|
45
|
+
var MAX_FILENAME_CHARS = 1024;
|
|
45
46
|
var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
|
|
46
47
|
var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
|
|
47
48
|
function isInApp(filename) {
|
|
@@ -64,7 +65,7 @@ function parseStacktrace(stack) {
|
|
|
64
65
|
const filename = loc ? loc[1] : location;
|
|
65
66
|
frames.push({
|
|
66
67
|
function: fn || "<anonymous>",
|
|
67
|
-
filename: filename
|
|
68
|
+
filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
|
|
68
69
|
lineno: loc?.[2] ? Number(loc[2]) : void 0,
|
|
69
70
|
colno: loc?.[3] ? Number(loc[3]) : void 0,
|
|
70
71
|
in_app: isInApp(filename)
|
|
@@ -94,7 +95,7 @@ function scrubSecrets(text) {
|
|
|
94
95
|
|
|
95
96
|
// src/utils.ts
|
|
96
97
|
var SDK_NAME = "hobots";
|
|
97
|
-
var SDK_VERSION = true ? "3.
|
|
98
|
+
var SDK_VERSION = true ? "3.2.0" : "0.0.0-dev";
|
|
98
99
|
function isNode() {
|
|
99
100
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
100
101
|
}
|
|
@@ -150,9 +151,19 @@ function jitter(baseMs) {
|
|
|
150
151
|
function truncate(value, max) {
|
|
151
152
|
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
152
153
|
}
|
|
154
|
+
function safeString(value) {
|
|
155
|
+
if (typeof value === "string") return value;
|
|
156
|
+
try {
|
|
157
|
+
const json = JSON.stringify(value);
|
|
158
|
+
if (json && json !== "{}") return json;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
return String(value);
|
|
162
|
+
}
|
|
153
163
|
var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
154
164
|
async function fetchWithRetry(send, options) {
|
|
155
165
|
const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
|
|
166
|
+
const noRetry = options.noRetryStatuses ?? [];
|
|
156
167
|
for (let attempt = 0; ; attempt++) {
|
|
157
168
|
let retriable;
|
|
158
169
|
let serverDelay;
|
|
@@ -160,6 +171,7 @@ async function fetchWithRetry(send, options) {
|
|
|
160
171
|
const response = await send();
|
|
161
172
|
if (response.ok) return response;
|
|
162
173
|
if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
|
|
174
|
+
if (noRetry.includes(response.status)) return response;
|
|
163
175
|
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
164
176
|
retriable = new Error(`HTTP ${response.status}`);
|
|
165
177
|
} catch (error) {
|
|
@@ -232,26 +244,18 @@ function colorize(text, color) {
|
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
// src/eventbuilder.ts
|
|
235
|
-
|
|
236
|
-
if (typeof value === "string") return value;
|
|
237
|
-
try {
|
|
238
|
-
const json = JSON.stringify(value);
|
|
239
|
-
if (json && json !== "{}") return json;
|
|
240
|
-
} catch {
|
|
241
|
-
}
|
|
242
|
-
return String(value);
|
|
243
|
-
}
|
|
247
|
+
var MAX_EXCEPTION_VALUE_CHARS = 8192;
|
|
244
248
|
function eventFromException(error, type = "error") {
|
|
245
249
|
let exceptionType;
|
|
246
250
|
let value;
|
|
247
251
|
let frames = [];
|
|
248
252
|
if (error instanceof Error) {
|
|
249
253
|
exceptionType = error.name || "Error";
|
|
250
|
-
value = scrubSecrets(error.message);
|
|
254
|
+
value = truncate(scrubSecrets(error.message), MAX_EXCEPTION_VALUE_CHARS);
|
|
251
255
|
frames = parseStacktrace(error.stack);
|
|
252
256
|
} else {
|
|
253
257
|
exceptionType = "NonError";
|
|
254
|
-
value = scrubSecrets(safeString(error));
|
|
258
|
+
value = truncate(scrubSecrets(safeString(error)), MAX_EXCEPTION_VALUE_CHARS);
|
|
255
259
|
}
|
|
256
260
|
return {
|
|
257
261
|
event_id: uuid4(),
|
|
@@ -377,6 +381,18 @@ var Transport = class {
|
|
|
377
381
|
};
|
|
378
382
|
|
|
379
383
|
// src/client.ts
|
|
384
|
+
var MAX_TAG_VALUE_CHARS = 512;
|
|
385
|
+
var MAX_FINGERPRINT_ITEMS = 10;
|
|
386
|
+
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
387
|
+
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
388
|
+
var MAX_STEPS = 200;
|
|
389
|
+
function clipTags(tags) {
|
|
390
|
+
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
391
|
+
}
|
|
392
|
+
function clipFingerprint(fingerprint) {
|
|
393
|
+
if (fingerprint === void 0) return void 0;
|
|
394
|
+
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
395
|
+
}
|
|
380
396
|
function runtimeContexts() {
|
|
381
397
|
if (isNode()) {
|
|
382
398
|
return {
|
|
@@ -445,20 +461,27 @@ var Client = class {
|
|
|
445
461
|
type: hint?.type ?? event.type ?? "error",
|
|
446
462
|
release: event.release ?? this.options.release,
|
|
447
463
|
environment: this.resolveEnvironment(event.environment),
|
|
448
|
-
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
464
|
+
tags: Object.keys(tags).length > 0 ? clipTags(tags) : void 0,
|
|
449
465
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
450
466
|
contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
|
|
451
|
-
fingerprint: hint?.fingerprint ?? event.fingerprint
|
|
467
|
+
fingerprint: clipFingerprint(hint?.fingerprint ?? event.fingerprint)
|
|
452
468
|
};
|
|
453
469
|
}
|
|
454
470
|
sendTransaction(payload) {
|
|
471
|
+
let steps = payload.steps;
|
|
472
|
+
if (steps.length > MAX_STEPS) {
|
|
473
|
+
this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
474
|
+
steps = steps.slice(0, MAX_STEPS);
|
|
475
|
+
}
|
|
455
476
|
this.transport.enqueue({
|
|
456
477
|
type: "transaction",
|
|
457
478
|
payload: {
|
|
458
479
|
...payload,
|
|
480
|
+
name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
481
|
+
steps,
|
|
459
482
|
release: payload.release ?? this.options.release,
|
|
460
483
|
environment: this.resolveEnvironment(payload.environment),
|
|
461
|
-
tags: { ...this.options.tags, ...payload.tags }
|
|
484
|
+
tags: clipTags({ ...this.options.tags, ...payload.tags })
|
|
462
485
|
}
|
|
463
486
|
});
|
|
464
487
|
}
|
|
@@ -551,12 +574,19 @@ function resolveConfig(options = {}, prefix = "[hobots]") {
|
|
|
551
574
|
|
|
552
575
|
// src/env.ts
|
|
553
576
|
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
577
|
+
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
554
578
|
function resolveInstanceId(option) {
|
|
555
579
|
const fromOption = option?.trim();
|
|
556
580
|
if (fromOption) return fromOption;
|
|
557
581
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
558
582
|
return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
|
|
559
583
|
}
|
|
584
|
+
function resolveEnvironmentOption(option) {
|
|
585
|
+
const fromOption = option?.trim();
|
|
586
|
+
if (fromOption) return fromOption;
|
|
587
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
588
|
+
return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
|
|
589
|
+
}
|
|
560
590
|
async function fallbackInstanceId() {
|
|
561
591
|
if (!isNode()) return "browser";
|
|
562
592
|
try {
|
|
@@ -635,6 +665,11 @@ function externalListenerCount(event) {
|
|
|
635
665
|
var emergencyTasks = [];
|
|
636
666
|
function addEmergencyTask(task) {
|
|
637
667
|
emergencyTasks.push(task);
|
|
668
|
+
const forget = () => {
|
|
669
|
+
const index = emergencyTasks.indexOf(task);
|
|
670
|
+
if (index !== -1) emergencyTasks.splice(index, 1);
|
|
671
|
+
};
|
|
672
|
+
task.then(forget, forget);
|
|
638
673
|
}
|
|
639
674
|
async function awaitEmergencyTasks(capMs) {
|
|
640
675
|
if (emergencyTasks.length === 0) return;
|
|
@@ -829,6 +864,8 @@ var MachineSampler = class {
|
|
|
829
864
|
// src/tasks/api.ts
|
|
830
865
|
var POLL_TIMEOUT_MS = 3e4;
|
|
831
866
|
var UPDATE_TIMEOUT_MS = 1e4;
|
|
867
|
+
var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
|
|
868
|
+
var ITEMS_TERMINAL_STATUSES = [429];
|
|
832
869
|
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
833
870
|
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
834
871
|
var TasksApiError = class extends Error {
|
|
@@ -838,6 +875,14 @@ var TasksApiError = class extends Error {
|
|
|
838
875
|
this.name = "TasksApiError";
|
|
839
876
|
}
|
|
840
877
|
};
|
|
878
|
+
async function withRequestTimeout(timeoutMs, external, send) {
|
|
879
|
+
const { signal, cleanup } = withTimeoutSignal(timeoutMs, external);
|
|
880
|
+
try {
|
|
881
|
+
return await send(signal);
|
|
882
|
+
} finally {
|
|
883
|
+
cleanup();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
841
886
|
var TasksApi = class {
|
|
842
887
|
baseUrl;
|
|
843
888
|
key;
|
|
@@ -974,12 +1019,9 @@ var TasksApi = class {
|
|
|
974
1019
|
body: JSON.stringify(request),
|
|
975
1020
|
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
976
1021
|
}),
|
|
977
|
-
{ logger: this.logger, label: `itens da run ${runId}
|
|
1022
|
+
{ logger: this.logger, label: `itens da run ${runId}`, noRetryStatuses: ITEMS_TERMINAL_STATUSES }
|
|
978
1023
|
);
|
|
979
1024
|
} catch (error) {
|
|
980
|
-
if (error instanceof Error && error.message === "HTTP 429") {
|
|
981
|
-
throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
|
|
982
|
-
}
|
|
983
1025
|
throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
984
1026
|
}
|
|
985
1027
|
if (!response.ok) {
|
|
@@ -996,21 +1038,19 @@ var TasksApi = class {
|
|
|
996
1038
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
997
1039
|
*/
|
|
998
1040
|
async createAttachment(runId, request, signal) {
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
|
|
1041
|
+
const response = await fetchWithRetry(
|
|
1042
|
+
() => withRequestTimeout(
|
|
1043
|
+
UPDATE_TIMEOUT_MS,
|
|
1044
|
+
signal,
|
|
1045
|
+
(timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
|
|
1004
1046
|
method: "POST",
|
|
1005
1047
|
headers: this.headers(),
|
|
1006
1048
|
body: JSON.stringify(request),
|
|
1007
1049
|
signal: timeoutSignal
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
cleanup();
|
|
1013
|
-
}
|
|
1050
|
+
})
|
|
1051
|
+
),
|
|
1052
|
+
{ logger: this.logger, label: `anexo da run ${runId}`, noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES }
|
|
1053
|
+
);
|
|
1014
1054
|
if (!response.ok) {
|
|
1015
1055
|
throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1016
1056
|
}
|
|
@@ -1030,22 +1070,20 @@ var TasksApi = class {
|
|
|
1030
1070
|
* está na assinatura da URL, e mandar o client secret para fora da API seria vazamento.
|
|
1031
1071
|
*/
|
|
1032
1072
|
async uploadAttachment(url, headers, body, signal) {
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
() => fetch(url, {
|
|
1073
|
+
const response = await fetchWithRetry(
|
|
1074
|
+
() => withRequestTimeout(
|
|
1075
|
+
UPLOAD_TIMEOUT_MS,
|
|
1076
|
+
signal,
|
|
1077
|
+
(timeoutSignal) => fetch(url, {
|
|
1038
1078
|
method: "PUT",
|
|
1039
1079
|
headers,
|
|
1040
1080
|
// BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
|
|
1041
1081
|
body,
|
|
1042
1082
|
signal: timeoutSignal
|
|
1043
|
-
})
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
cleanup();
|
|
1048
|
-
}
|
|
1083
|
+
})
|
|
1084
|
+
),
|
|
1085
|
+
{ delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
|
|
1086
|
+
);
|
|
1049
1087
|
if (!response.ok) {
|
|
1050
1088
|
throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
|
|
1051
1089
|
}
|
|
@@ -1083,10 +1121,12 @@ var Backoff = class {
|
|
|
1083
1121
|
this.authFailures = 0;
|
|
1084
1122
|
}
|
|
1085
1123
|
next(error) {
|
|
1086
|
-
if (error instanceof TasksApiError && error.status === 401) {
|
|
1124
|
+
if (error instanceof TasksApiError && (error.status === 401 || error.status === 402)) {
|
|
1087
1125
|
this.authFailures++;
|
|
1088
1126
|
if (this.authFailures === 1 || this.authFailures % 10 === 0) {
|
|
1089
|
-
this.logger.error(
|
|
1127
|
+
this.logger.error(
|
|
1128
|
+
error.status === 402 ? "agente fora do limite do plano (402) \u2014 fa\xE7a upgrade em Plano e cobran\xE7a para reativ\xE1-lo" : "chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes"
|
|
1129
|
+
);
|
|
1090
1130
|
}
|
|
1091
1131
|
return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
|
|
1092
1132
|
}
|
|
@@ -1120,7 +1160,8 @@ function resolveDelay(delay, attempt, fallbackMs) {
|
|
|
1120
1160
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1121
1161
|
}
|
|
1122
1162
|
async function retryWithBackoff(fn, options = {}) {
|
|
1123
|
-
const
|
|
1163
|
+
const requested = options.retries ?? DEFAULT_RETRIES;
|
|
1164
|
+
const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
|
|
1124
1165
|
const wait = options.wait ?? sleep;
|
|
1125
1166
|
for (let attempt = 1; ; attempt++) {
|
|
1126
1167
|
if (options.signal?.aborted) throw runCanceledError();
|
|
@@ -1155,6 +1196,7 @@ function startsWith(bytes, signature, offset = 0) {
|
|
|
1155
1196
|
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1156
1197
|
}
|
|
1157
1198
|
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1199
|
+
var MAX_PNG_DIMENSION = 2e4;
|
|
1158
1200
|
var JPEG_MAGIC = [255, 216, 255];
|
|
1159
1201
|
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1160
1202
|
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
@@ -1170,6 +1212,7 @@ function readPngDimensions(bytes) {
|
|
|
1170
1212
|
const width = view.getUint32(16);
|
|
1171
1213
|
const height = view.getUint32(20);
|
|
1172
1214
|
if (width === 0 || height === 0) return void 0;
|
|
1215
|
+
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1173
1216
|
return { width, height };
|
|
1174
1217
|
}
|
|
1175
1218
|
async function toBytes(input) {
|
|
@@ -1186,6 +1229,10 @@ var LOG_LEVEL_COLOR = {
|
|
|
1186
1229
|
error: "red",
|
|
1187
1230
|
success: "green"
|
|
1188
1231
|
};
|
|
1232
|
+
function joinLogArgs(message, rest) {
|
|
1233
|
+
if (rest.length === 0) return safeString(message);
|
|
1234
|
+
return [message, ...rest].map(safeString).join(" ");
|
|
1235
|
+
}
|
|
1189
1236
|
function printRunLog(task, level, message) {
|
|
1190
1237
|
const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
1191
1238
|
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
@@ -1243,11 +1290,11 @@ var RunReporter = class {
|
|
|
1243
1290
|
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
1244
1291
|
);
|
|
1245
1292
|
const log = {
|
|
1246
|
-
debug: (message) => this.log("debug", message),
|
|
1247
|
-
info: (message) => this.log("info", message),
|
|
1248
|
-
warn: (message) => this.log("warning", message),
|
|
1249
|
-
error: (message) => this.log("error", message),
|
|
1250
|
-
success: (message) => this.log("success", message)
|
|
1293
|
+
debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
|
|
1294
|
+
info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
|
|
1295
|
+
warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
|
|
1296
|
+
error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
|
|
1297
|
+
success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
|
|
1251
1298
|
};
|
|
1252
1299
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1253
1300
|
const checkCanceled = () => this.checkCanceled();
|
|
@@ -1397,7 +1444,7 @@ var RunReporter = class {
|
|
|
1397
1444
|
}
|
|
1398
1445
|
log(level, message) {
|
|
1399
1446
|
if (this.dead) return;
|
|
1400
|
-
const scrubbed = scrubSecrets(message);
|
|
1447
|
+
const scrubbed = scrubSecrets(safeString(message));
|
|
1401
1448
|
printRunLog(this.context.task, level, scrubbed);
|
|
1402
1449
|
this.push(level, scrubbed, true);
|
|
1403
1450
|
}
|
|
@@ -1456,13 +1503,25 @@ var RunReporter = class {
|
|
|
1456
1503
|
this.stopCancelWatch();
|
|
1457
1504
|
this.clearFlushTimer();
|
|
1458
1505
|
this.forceFinalProgress();
|
|
1459
|
-
this.enqueue({ status: "succeeded", logs: this.
|
|
1506
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
1460
1507
|
}
|
|
1461
1508
|
reportFailure(error) {
|
|
1462
1509
|
this.stopCancelWatch();
|
|
1463
1510
|
this.clearFlushTimer();
|
|
1464
1511
|
this.forceFinalProgress();
|
|
1465
|
-
this.enqueue({ status: "failed", error, logs: this.
|
|
1512
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
1516
|
+
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
1517
|
+
*/
|
|
1518
|
+
takeLogsForTerminal() {
|
|
1519
|
+
let logs = this.takeLogs();
|
|
1520
|
+
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
1521
|
+
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
1522
|
+
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
1523
|
+
}
|
|
1524
|
+
return logs;
|
|
1466
1525
|
}
|
|
1467
1526
|
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
1468
1527
|
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
@@ -1570,7 +1629,7 @@ var RunReporter = class {
|
|
|
1570
1629
|
id: id ? truncate(String(id), 255) : uuid4(),
|
|
1571
1630
|
seq: this.itemSeq++,
|
|
1572
1631
|
status,
|
|
1573
|
-
...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1632
|
+
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1574
1633
|
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
1575
1634
|
ts: nowISO()
|
|
1576
1635
|
});
|
|
@@ -1659,8 +1718,10 @@ var RunReporter = class {
|
|
|
1659
1718
|
async attach(image, options = {}) {
|
|
1660
1719
|
const id = uuid4();
|
|
1661
1720
|
const level = options.level ?? "info";
|
|
1662
|
-
const
|
|
1663
|
-
const
|
|
1721
|
+
const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
|
|
1722
|
+
const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
|
|
1723
|
+
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
1724
|
+
const message = rawMessage ?? rawCaption ?? "captura de tela";
|
|
1664
1725
|
if (this.dead) return { id, ok: false, reason: "canceled" };
|
|
1665
1726
|
if (this.attachmentsUnsupported) {
|
|
1666
1727
|
this.log(level, message);
|
|
@@ -2280,6 +2341,42 @@ var TaskAgent = class {
|
|
|
2280
2341
|
}
|
|
2281
2342
|
};
|
|
2282
2343
|
|
|
2344
|
+
// src/tasks/item-columns.ts
|
|
2345
|
+
var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
|
|
2346
|
+
var MAX_COLUMNS = 12;
|
|
2347
|
+
var MAX_LABEL_CHARS = 60;
|
|
2348
|
+
function normalizeItemColumns(slug, input, logger) {
|
|
2349
|
+
if (input === void 0 || input === null) return void 0;
|
|
2350
|
+
const columns = input.columns;
|
|
2351
|
+
if (typeof input !== "object" || !Array.isArray(columns)) {
|
|
2352
|
+
logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
|
|
2353
|
+
return void 0;
|
|
2354
|
+
}
|
|
2355
|
+
const result = [];
|
|
2356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2357
|
+
for (const entry of columns) {
|
|
2358
|
+
const rawKey = typeof entry === "string" ? entry : entry?.key;
|
|
2359
|
+
const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
|
|
2360
|
+
const key = typeof rawKey === "string" ? rawKey.trim() : "";
|
|
2361
|
+
if (!KEY_RE.test(key)) {
|
|
2362
|
+
logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
if (seen.has(key)) {
|
|
2366
|
+
logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
if (result.length >= MAX_COLUMNS) {
|
|
2370
|
+
logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
seen.add(key);
|
|
2374
|
+
const label = typeof rawLabel === "string" ? rawLabel.trim().slice(0, MAX_LABEL_CHARS) : "";
|
|
2375
|
+
result.push(label ? { key, label } : { key });
|
|
2376
|
+
}
|
|
2377
|
+
return result.length > 0 ? result : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2283
2380
|
// src/tasks/local.ts
|
|
2284
2381
|
var LocalRunner = class {
|
|
2285
2382
|
constructor(task, deps) {
|
|
@@ -2358,6 +2455,15 @@ var TaskRegistry = class {
|
|
|
2358
2455
|
allSlugs() {
|
|
2359
2456
|
return [...this.tasks.keys()];
|
|
2360
2457
|
}
|
|
2458
|
+
/** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
|
|
2459
|
+
itemDeclarations() {
|
|
2460
|
+
let result;
|
|
2461
|
+
for (const task of this.tasks.values()) {
|
|
2462
|
+
if (!task.options.itemColumns) continue;
|
|
2463
|
+
(result ??= {})[task.slug] = { attributes: task.options.itemColumns };
|
|
2464
|
+
}
|
|
2465
|
+
return result;
|
|
2466
|
+
}
|
|
2361
2467
|
clear() {
|
|
2362
2468
|
this.tasks.clear();
|
|
2363
2469
|
}
|
|
@@ -2511,8 +2617,9 @@ function ensureRuntime(options) {
|
|
|
2511
2617
|
}
|
|
2512
2618
|
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
2513
2619
|
const logger = makeLogger(options.debug ?? false);
|
|
2620
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
2514
2621
|
const client = new Client(
|
|
2515
|
-
{ environment
|
|
2622
|
+
{ environment, release: options.release, tags: options.tags, debug: options.debug },
|
|
2516
2623
|
config
|
|
2517
2624
|
);
|
|
2518
2625
|
const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
|
|
@@ -2529,6 +2636,7 @@ function ensureRuntime(options) {
|
|
|
2529
2636
|
agent: void 0,
|
|
2530
2637
|
locals: /* @__PURE__ */ new Map(),
|
|
2531
2638
|
instanceId: resolveInstanceId(options.instanceId),
|
|
2639
|
+
environment,
|
|
2532
2640
|
pollIntervalMs: void 0,
|
|
2533
2641
|
booted: false,
|
|
2534
2642
|
bootScheduled: false,
|
|
@@ -2553,7 +2661,7 @@ function clampConcurrency(value) {
|
|
|
2553
2661
|
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
2554
2662
|
return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
|
|
2555
2663
|
}
|
|
2556
|
-
function resolveTaskOptions(options, mode) {
|
|
2664
|
+
function resolveTaskOptions(slug, options, mode, logger) {
|
|
2557
2665
|
const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
|
|
2558
2666
|
return {
|
|
2559
2667
|
retries,
|
|
@@ -2562,7 +2670,8 @@ function resolveTaskOptions(options, mode) {
|
|
|
2562
2670
|
concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
|
|
2563
2671
|
logFlushIntervalMs: options.logFlushIntervalMs,
|
|
2564
2672
|
cancelCheckIntervalMs: options.cancelCheckIntervalMs,
|
|
2565
|
-
cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
|
|
2673
|
+
cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
|
|
2674
|
+
itemColumns: normalizeItemColumns(slug, options.items, logger)
|
|
2566
2675
|
};
|
|
2567
2676
|
}
|
|
2568
2677
|
function instanceInfo(rt) {
|
|
@@ -2571,9 +2680,11 @@ function instanceInfo(rt) {
|
|
|
2571
2680
|
version: SDK_VERSION,
|
|
2572
2681
|
host: hostname(),
|
|
2573
2682
|
// ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
|
|
2574
|
-
environment: rt.
|
|
2683
|
+
environment: rt.environment,
|
|
2575
2684
|
// TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
|
|
2576
2685
|
tasks: rt.registry.allSlugs(),
|
|
2686
|
+
// colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
|
|
2687
|
+
task_items: rt.registry.itemDeclarations(),
|
|
2577
2688
|
// o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
|
|
2578
2689
|
// poll; clampado à faixa aceita pelo schema (1s..15min)
|
|
2579
2690
|
poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
|
|
@@ -2642,7 +2753,7 @@ function addTask(fn, mode, task, handler, options) {
|
|
|
2642
2753
|
rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
|
|
2643
2754
|
}
|
|
2644
2755
|
}
|
|
2645
|
-
const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
|
|
2756
|
+
const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
|
|
2646
2757
|
rt.registry.add(registered);
|
|
2647
2758
|
if (!rt.booted) {
|
|
2648
2759
|
scheduleBoot(rt);
|
|
@@ -2731,11 +2842,11 @@ function warnOutsideRun(member) {
|
|
|
2731
2842
|
}
|
|
2732
2843
|
var FALLBACK_TASK = "hobots";
|
|
2733
2844
|
var fallbackLog = {
|
|
2734
|
-
debug: (message) => printRunLog(FALLBACK_TASK, "debug", message),
|
|
2735
|
-
info: (message) => printRunLog(FALLBACK_TASK, "info", message),
|
|
2736
|
-
warn: (message) => printRunLog(FALLBACK_TASK, "warning", message),
|
|
2737
|
-
error: (message) => printRunLog(FALLBACK_TASK, "error", message),
|
|
2738
|
-
success: (message) => printRunLog(FALLBACK_TASK, "success", message)
|
|
2845
|
+
debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
|
|
2846
|
+
info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
|
|
2847
|
+
warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
|
|
2848
|
+
error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
|
|
2849
|
+
success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
|
|
2739
2850
|
};
|
|
2740
2851
|
var noop = () => void 0;
|
|
2741
2852
|
var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
|