hobots 2.4.0 → 3.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 +116 -367
- package/dist/index.cjs +1385 -909
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -225
- package/dist/index.d.ts +252 -225
- package/dist/index.js +1385 -909
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +27 -2
- package/dist/protocol.d.ts +27 -2
- package/docs/README.md +16 -33
- package/docs/api-reference.md +75 -213
- package/docs/concepts.md +60 -393
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +31 -19
- package/docs/tasks.md +102 -247
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/dist/index.js
CHANGED
|
@@ -1,56 +1,6 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
3
|
-
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
4
|
-
function envBaseUrl() {
|
|
5
|
-
if (typeof process === "undefined" || !process.env) return void 0;
|
|
6
|
-
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
7
|
-
}
|
|
8
|
-
function isLoopbackHost(hostname2) {
|
|
9
|
-
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
10
|
-
}
|
|
11
|
-
var warnedInsecureBaseUrl = false;
|
|
12
|
-
function validateBaseUrl(raw, prefix) {
|
|
13
|
-
let url;
|
|
14
|
-
try {
|
|
15
|
-
url = new URL(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
18
|
-
}
|
|
19
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
20
|
-
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
21
|
-
}
|
|
22
|
-
if (url.username || url.password) {
|
|
23
|
-
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
24
|
-
}
|
|
25
|
-
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
26
|
-
warnedInsecureBaseUrl = true;
|
|
27
|
-
console.warn(
|
|
28
|
-
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return raw.replace(/\/+$/, "");
|
|
32
|
-
}
|
|
33
|
-
function resolveConfig(options, prefix = "[hobots]") {
|
|
34
|
-
const key = options.clientSecret?.trim();
|
|
35
|
-
if (!key) {
|
|
36
|
-
throw new Error(`${prefix} clientSecret ausente \u2014 copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`);
|
|
37
|
-
}
|
|
38
|
-
if (!SECRET_PATTERN.test(key)) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`${prefix} clientSecret com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
44
|
-
return {
|
|
45
|
-
key,
|
|
46
|
-
baseUrl,
|
|
47
|
-
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
48
|
-
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
1
|
// src/stacktrace.ts
|
|
53
2
|
var MAX_FRAMES = 50;
|
|
3
|
+
var MAX_FILENAME_CHARS = 1024;
|
|
54
4
|
var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
|
|
55
5
|
var LOCATION_RE = /^(.*?):(\d+):(\d+)$/;
|
|
56
6
|
function isInApp(filename) {
|
|
@@ -73,7 +23,7 @@ function parseStacktrace(stack) {
|
|
|
73
23
|
const filename = loc ? loc[1] : location;
|
|
74
24
|
frames.push({
|
|
75
25
|
function: fn || "<anonymous>",
|
|
76
|
-
filename: filename
|
|
26
|
+
filename: filename ? filename.slice(0, MAX_FILENAME_CHARS) : void 0,
|
|
77
27
|
lineno: loc?.[2] ? Number(loc[2]) : void 0,
|
|
78
28
|
colno: loc?.[3] ? Number(loc[3]) : void 0,
|
|
79
29
|
in_app: isInApp(filename)
|
|
@@ -103,7 +53,7 @@ function scrubSecrets(text) {
|
|
|
103
53
|
|
|
104
54
|
// src/utils.ts
|
|
105
55
|
var SDK_NAME = "hobots";
|
|
106
|
-
var SDK_VERSION = true ? "2.
|
|
56
|
+
var SDK_VERSION = true ? "3.2.0" : "0.0.0-dev";
|
|
107
57
|
function isNode() {
|
|
108
58
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
109
59
|
}
|
|
@@ -131,20 +81,47 @@ function unrefTimer(timer) {
|
|
|
131
81
|
timer?.unref?.();
|
|
132
82
|
}
|
|
133
83
|
function sleep(ms) {
|
|
134
|
-
return new Promise((
|
|
135
|
-
const timer = setTimeout(
|
|
84
|
+
return new Promise((resolve2) => {
|
|
85
|
+
const timer = setTimeout(resolve2, ms);
|
|
136
86
|
unrefTimer(timer);
|
|
137
87
|
});
|
|
138
88
|
}
|
|
89
|
+
function abortableSleep(ms, signal) {
|
|
90
|
+
return new Promise((resolve2) => {
|
|
91
|
+
if (signal?.aborted) {
|
|
92
|
+
resolve2();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve2();
|
|
98
|
+
};
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
resolve2();
|
|
102
|
+
}, ms);
|
|
103
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
139
106
|
function jitter(baseMs) {
|
|
140
107
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
141
108
|
}
|
|
142
109
|
function truncate(value, max) {
|
|
143
110
|
return value.length > max ? `${value.slice(0, max)}\u2026 [truncado]` : value;
|
|
144
111
|
}
|
|
112
|
+
function safeString(value) {
|
|
113
|
+
if (typeof value === "string") return value;
|
|
114
|
+
try {
|
|
115
|
+
const json = JSON.stringify(value);
|
|
116
|
+
if (json && json !== "{}") return json;
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
return String(value);
|
|
120
|
+
}
|
|
145
121
|
var DEFAULT_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
146
122
|
async function fetchWithRetry(send, options) {
|
|
147
123
|
const delays = options.delays ?? DEFAULT_RETRY_DELAYS_MS;
|
|
124
|
+
const noRetry = options.noRetryStatuses ?? [];
|
|
148
125
|
for (let attempt = 0; ; attempt++) {
|
|
149
126
|
let retriable;
|
|
150
127
|
let serverDelay;
|
|
@@ -152,6 +129,7 @@ async function fetchWithRetry(send, options) {
|
|
|
152
129
|
const response = await send();
|
|
153
130
|
if (response.ok) return response;
|
|
154
131
|
if (response.status >= 400 && response.status < 500 && response.status !== 429) return response;
|
|
132
|
+
if (noRetry.includes(response.status)) return response;
|
|
155
133
|
if (response.status === 429) serverDelay = retryAfterMs(response);
|
|
156
134
|
retriable = new Error(`HTTP ${response.status}`);
|
|
157
135
|
} catch (error) {
|
|
@@ -224,26 +202,18 @@ function colorize(text, color) {
|
|
|
224
202
|
}
|
|
225
203
|
|
|
226
204
|
// src/eventbuilder.ts
|
|
227
|
-
|
|
228
|
-
if (typeof value === "string") return value;
|
|
229
|
-
try {
|
|
230
|
-
const json = JSON.stringify(value);
|
|
231
|
-
if (json && json !== "{}") return json;
|
|
232
|
-
} catch {
|
|
233
|
-
}
|
|
234
|
-
return String(value);
|
|
235
|
-
}
|
|
205
|
+
var MAX_EXCEPTION_VALUE_CHARS = 8192;
|
|
236
206
|
function eventFromException(error, type = "error") {
|
|
237
207
|
let exceptionType;
|
|
238
208
|
let value;
|
|
239
209
|
let frames = [];
|
|
240
210
|
if (error instanceof Error) {
|
|
241
211
|
exceptionType = error.name || "Error";
|
|
242
|
-
value = scrubSecrets(error.message);
|
|
212
|
+
value = truncate(scrubSecrets(error.message), MAX_EXCEPTION_VALUE_CHARS);
|
|
243
213
|
frames = parseStacktrace(error.stack);
|
|
244
214
|
} else {
|
|
245
215
|
exceptionType = "NonError";
|
|
246
|
-
value = scrubSecrets(safeString(error));
|
|
216
|
+
value = truncate(scrubSecrets(safeString(error)), MAX_EXCEPTION_VALUE_CHARS);
|
|
247
217
|
}
|
|
248
218
|
return {
|
|
249
219
|
event_id: uuid4(),
|
|
@@ -256,14 +226,6 @@ function eventFromException(error, type = "error") {
|
|
|
256
226
|
}
|
|
257
227
|
};
|
|
258
228
|
}
|
|
259
|
-
function eventFromMessage(message, type = "warning") {
|
|
260
|
-
return {
|
|
261
|
-
event_id: uuid4(),
|
|
262
|
-
timestamp: nowISO(),
|
|
263
|
-
type,
|
|
264
|
-
message: scrubSecrets(message)
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
229
|
|
|
268
230
|
// src/transport.ts
|
|
269
231
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
@@ -377,6 +339,18 @@ var Transport = class {
|
|
|
377
339
|
};
|
|
378
340
|
|
|
379
341
|
// src/client.ts
|
|
342
|
+
var MAX_TAG_VALUE_CHARS = 512;
|
|
343
|
+
var MAX_FINGERPRINT_ITEMS = 10;
|
|
344
|
+
var MAX_FINGERPRINT_ITEM_CHARS = 255;
|
|
345
|
+
var MAX_TRANSACTION_NAME_CHARS = 500;
|
|
346
|
+
var MAX_STEPS = 200;
|
|
347
|
+
function clipTags(tags) {
|
|
348
|
+
return Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, truncate(String(value), MAX_TAG_VALUE_CHARS)]));
|
|
349
|
+
}
|
|
350
|
+
function clipFingerprint(fingerprint) {
|
|
351
|
+
if (fingerprint === void 0) return void 0;
|
|
352
|
+
return fingerprint.slice(0, MAX_FINGERPRINT_ITEMS).map((item) => truncate(String(item), MAX_FINGERPRINT_ITEM_CHARS));
|
|
353
|
+
}
|
|
380
354
|
function runtimeContexts() {
|
|
381
355
|
if (isNode()) {
|
|
382
356
|
return {
|
|
@@ -397,7 +371,7 @@ var Client = class {
|
|
|
397
371
|
#config;
|
|
398
372
|
transport;
|
|
399
373
|
machine;
|
|
400
|
-
constructor(options, config
|
|
374
|
+
constructor(options, config) {
|
|
401
375
|
this.#options = options;
|
|
402
376
|
this.debug = !!options.debug;
|
|
403
377
|
this.logger = makeLogger(this.debug);
|
|
@@ -427,53 +401,45 @@ var Client = class {
|
|
|
427
401
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
428
402
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
429
403
|
}
|
|
430
|
-
captureException(error, hint
|
|
431
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint
|
|
432
|
-
}
|
|
433
|
-
captureMessage(message, type, hint, scope2) {
|
|
434
|
-
return this.captureEvent(eventFromMessage(message, type), hint, scope2);
|
|
404
|
+
captureException(error, hint) {
|
|
405
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
435
406
|
}
|
|
436
|
-
captureEvent(event, hint
|
|
437
|
-
const
|
|
438
|
-
if (sampleRate < 1 && Math.random() >= sampleRate) {
|
|
439
|
-
this.logger.debug("evento descartado por sampleRate");
|
|
440
|
-
return event.event_id;
|
|
441
|
-
}
|
|
442
|
-
const prepared = this.prepareEvent(event, hint, scope2);
|
|
443
|
-
const final = this.options.beforeSend ? this.options.beforeSend(prepared) : prepared;
|
|
444
|
-
if (!final) {
|
|
445
|
-
this.logger.debug("evento descartado por beforeSend");
|
|
446
|
-
return event.event_id;
|
|
447
|
-
}
|
|
407
|
+
captureEvent(event, hint) {
|
|
408
|
+
const final = this.prepareEvent(event, hint);
|
|
448
409
|
this.transport.enqueue({ type: "event", payload: final });
|
|
449
410
|
return final.event_id;
|
|
450
411
|
}
|
|
451
|
-
prepareEvent(event, hint
|
|
452
|
-
const tags = { ...this.options.tags, ...
|
|
453
|
-
const extra = { ...
|
|
412
|
+
prepareEvent(event, hint) {
|
|
413
|
+
const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
|
|
414
|
+
const extra = { ...event.extra, ...hint?.extra };
|
|
454
415
|
const sampled = this.machine?.sample();
|
|
455
416
|
const machine = sampled ? { machine: sampled } : {};
|
|
456
417
|
return {
|
|
457
418
|
...event,
|
|
458
|
-
type: hint?.type ?? event.type ??
|
|
419
|
+
type: hint?.type ?? event.type ?? "error",
|
|
459
420
|
release: event.release ?? this.options.release,
|
|
460
421
|
environment: this.resolveEnvironment(event.environment),
|
|
461
|
-
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
422
|
+
tags: Object.keys(tags).length > 0 ? clipTags(tags) : void 0,
|
|
462
423
|
extra: Object.keys(extra).length > 0 ? extra : void 0,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
breadcrumbs: scope2.breadcrumbs.length > 0 ? [...scope2.breadcrumbs] : event.breadcrumbs,
|
|
466
|
-
fingerprint: hint?.fingerprint ?? event.fingerprint ?? scope2.fingerprint
|
|
424
|
+
contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
|
|
425
|
+
fingerprint: clipFingerprint(hint?.fingerprint ?? event.fingerprint)
|
|
467
426
|
};
|
|
468
427
|
}
|
|
469
428
|
sendTransaction(payload) {
|
|
429
|
+
let steps = payload.steps;
|
|
430
|
+
if (steps.length > MAX_STEPS) {
|
|
431
|
+
this.logger.warn(`transaction com ${steps.length} steps \u2014 enviando as primeiras ${MAX_STEPS}`);
|
|
432
|
+
steps = steps.slice(0, MAX_STEPS);
|
|
433
|
+
}
|
|
470
434
|
this.transport.enqueue({
|
|
471
435
|
type: "transaction",
|
|
472
436
|
payload: {
|
|
473
437
|
...payload,
|
|
438
|
+
name: truncate(payload.name || "(sem nome)", MAX_TRANSACTION_NAME_CHARS),
|
|
439
|
+
steps,
|
|
474
440
|
release: payload.release ?? this.options.release,
|
|
475
441
|
environment: this.resolveEnvironment(payload.environment),
|
|
476
|
-
tags: { ...this.options.tags, ...payload.tags }
|
|
442
|
+
tags: clipTags({ ...this.options.tags, ...payload.tags })
|
|
477
443
|
}
|
|
478
444
|
});
|
|
479
445
|
}
|
|
@@ -489,7 +455,7 @@ var Client = class {
|
|
|
489
455
|
});
|
|
490
456
|
}
|
|
491
457
|
/**
|
|
492
|
-
* Ambiente do item: o dele, senão o
|
|
458
|
+
* Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
|
|
493
459
|
* porque evento, transaction e heartbeat precisam responder igual — o servidor
|
|
494
460
|
* normaliza apelidos legados, então aqui basta repassar.
|
|
495
461
|
*/
|
|
@@ -504,23 +470,110 @@ var Client = class {
|
|
|
504
470
|
}
|
|
505
471
|
};
|
|
506
472
|
|
|
473
|
+
// src/config.ts
|
|
474
|
+
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
475
|
+
var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
|
|
476
|
+
function envClientSecret() {
|
|
477
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
478
|
+
return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
|
|
479
|
+
}
|
|
480
|
+
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
481
|
+
function envBaseUrl() {
|
|
482
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
483
|
+
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
484
|
+
}
|
|
485
|
+
function isLoopbackHost(hostname2) {
|
|
486
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
487
|
+
}
|
|
488
|
+
var warnedInsecureBaseUrl = false;
|
|
489
|
+
function validateBaseUrl(raw, prefix) {
|
|
490
|
+
let url;
|
|
491
|
+
try {
|
|
492
|
+
url = new URL(raw);
|
|
493
|
+
} catch {
|
|
494
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
495
|
+
}
|
|
496
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
497
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
498
|
+
}
|
|
499
|
+
if (url.username || url.password) {
|
|
500
|
+
throw new Error(`${prefix} HOBOTS_API_URL n\xE3o pode conter credenciais na URL \u2014 a autentica\xE7\xE3o \xE9 s\xF3 pelo Client Secret`);
|
|
501
|
+
}
|
|
502
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
503
|
+
warnedInsecureBaseUrl = true;
|
|
504
|
+
console.warn(
|
|
505
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return raw.replace(/\/+$/, "");
|
|
509
|
+
}
|
|
510
|
+
function resolveConfig(options = {}, prefix = "[hobots]") {
|
|
511
|
+
const fromOption = options.clientSecret?.trim();
|
|
512
|
+
const key = fromOption || envClientSecret();
|
|
513
|
+
const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
|
|
514
|
+
if (!key) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`${prefix} Client Secret ausente \u2014 defina a env ${CLIENT_SECRET_ENV} (ou passe clientSecret nas op\xE7\xF5es da task); copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`${prefix} ${origin} com formato inv\xE1lido (${key.length} caracteres) \u2014 esperado "hb_" + 64 hex; copie o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
525
|
+
return {
|
|
526
|
+
key,
|
|
527
|
+
baseUrl,
|
|
528
|
+
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
529
|
+
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/env.ts
|
|
534
|
+
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
535
|
+
var ENVIRONMENT_ENV = "HOBOTS_ENVIRONMENT";
|
|
536
|
+
function resolveInstanceId(option) {
|
|
537
|
+
const fromOption = option?.trim();
|
|
538
|
+
if (fromOption) return fromOption;
|
|
539
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
540
|
+
return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
|
|
541
|
+
}
|
|
542
|
+
function resolveEnvironmentOption(option) {
|
|
543
|
+
const fromOption = option?.trim();
|
|
544
|
+
if (fromOption) return fromOption;
|
|
545
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
546
|
+
return process.env[ENVIRONMENT_ENV]?.trim() || void 0;
|
|
547
|
+
}
|
|
548
|
+
async function fallbackInstanceId() {
|
|
549
|
+
if (!isNode()) return "browser";
|
|
550
|
+
try {
|
|
551
|
+
const os = await import("os");
|
|
552
|
+
return os.hostname() || "unknown-host";
|
|
553
|
+
} catch {
|
|
554
|
+
return "unknown-host";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
507
558
|
// src/heartbeat.ts
|
|
508
559
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
509
560
|
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
510
561
|
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
511
562
|
var Heartbeat = class {
|
|
512
|
-
constructor(
|
|
513
|
-
this.client =
|
|
563
|
+
constructor(client, options = {}, instanceId, sampler) {
|
|
564
|
+
this.client = client;
|
|
514
565
|
this.sampler = sampler;
|
|
515
566
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
516
567
|
this.instanceId = instanceId;
|
|
517
568
|
this.name = options.name ?? instanceId;
|
|
518
569
|
this.metadata = options.metadata;
|
|
570
|
+
this.keepAlive = options.keepAlive ?? false;
|
|
519
571
|
}
|
|
520
572
|
intervalMs;
|
|
521
573
|
instanceId;
|
|
522
574
|
name;
|
|
523
575
|
metadata;
|
|
576
|
+
keepAlive;
|
|
524
577
|
timer;
|
|
525
578
|
start() {
|
|
526
579
|
if (this.timer) return;
|
|
@@ -536,7 +589,7 @@ var Heartbeat = class {
|
|
|
536
589
|
},
|
|
537
590
|
this.intervalMs * (0.9 + Math.random() * 0.2)
|
|
538
591
|
);
|
|
539
|
-
unrefTimer(this.timer);
|
|
592
|
+
if (!this.keepAlive) unrefTimer(this.timer);
|
|
540
593
|
}
|
|
541
594
|
stop() {
|
|
542
595
|
if (this.timer) {
|
|
@@ -570,6 +623,11 @@ function externalListenerCount(event) {
|
|
|
570
623
|
var emergencyTasks = [];
|
|
571
624
|
function addEmergencyTask(task) {
|
|
572
625
|
emergencyTasks.push(task);
|
|
626
|
+
const forget = () => {
|
|
627
|
+
const index = emergencyTasks.indexOf(task);
|
|
628
|
+
if (index !== -1) emergencyTasks.splice(index, 1);
|
|
629
|
+
};
|
|
630
|
+
task.then(forget, forget);
|
|
573
631
|
}
|
|
574
632
|
async function awaitEmergencyTasks(capMs) {
|
|
575
633
|
if (emergencyTasks.length === 0) return;
|
|
@@ -582,13 +640,13 @@ var installed = false;
|
|
|
582
640
|
function hasInstalledGlobalHandlers() {
|
|
583
641
|
return installed;
|
|
584
642
|
}
|
|
585
|
-
function installGlobalHandlers(
|
|
643
|
+
function installGlobalHandlers(client) {
|
|
586
644
|
if (installed) return () => void 0;
|
|
587
645
|
installed = true;
|
|
588
646
|
if (isNode()) {
|
|
589
647
|
const onUncaught = markInternal((error) => {
|
|
590
|
-
|
|
591
|
-
void
|
|
648
|
+
client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
|
|
649
|
+
void client.flush(2e3).then(async () => {
|
|
592
650
|
if (externalListenerCount("uncaughtException") === 0) {
|
|
593
651
|
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
594
652
|
console.error(error);
|
|
@@ -597,11 +655,11 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
597
655
|
});
|
|
598
656
|
});
|
|
599
657
|
const onRejection = markInternal((reason) => {
|
|
600
|
-
|
|
601
|
-
void
|
|
658
|
+
client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
|
|
659
|
+
void client.flush(2e3);
|
|
602
660
|
});
|
|
603
661
|
const onBeforeExit = markInternal(() => {
|
|
604
|
-
void
|
|
662
|
+
void client.flush(2e3);
|
|
605
663
|
});
|
|
606
664
|
process.on("uncaughtException", onUncaught);
|
|
607
665
|
process.on("unhandledRejection", onRejection);
|
|
@@ -615,13 +673,13 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
615
673
|
}
|
|
616
674
|
if (typeof window !== "undefined") {
|
|
617
675
|
const onError = (event) => {
|
|
618
|
-
|
|
676
|
+
client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
|
|
619
677
|
};
|
|
620
678
|
const onRejection = (event) => {
|
|
621
|
-
|
|
679
|
+
client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
|
|
622
680
|
};
|
|
623
681
|
const onPageHide = () => {
|
|
624
|
-
void
|
|
682
|
+
void client.flush(1e3);
|
|
625
683
|
};
|
|
626
684
|
window.addEventListener("error", onError);
|
|
627
685
|
window.addEventListener("unhandledrejection", onRejection);
|
|
@@ -761,313 +819,112 @@ var MachineSampler = class {
|
|
|
761
819
|
}
|
|
762
820
|
};
|
|
763
821
|
|
|
764
|
-
// src/
|
|
765
|
-
var
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
this.
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
setTags(tags) {
|
|
778
|
-
Object.assign(this.tags, tags);
|
|
779
|
-
return this;
|
|
780
|
-
}
|
|
781
|
-
setUser(user) {
|
|
782
|
-
this.user = user ?? void 0;
|
|
783
|
-
return this;
|
|
784
|
-
}
|
|
785
|
-
setContext(name, context) {
|
|
786
|
-
if (context === null) {
|
|
787
|
-
delete this.contexts[name];
|
|
788
|
-
} else {
|
|
789
|
-
this.contexts[name] = context;
|
|
790
|
-
}
|
|
791
|
-
return this;
|
|
792
|
-
}
|
|
793
|
-
setExtra(key, value) {
|
|
794
|
-
this.extra[key] = value;
|
|
795
|
-
return this;
|
|
822
|
+
// src/tasks/api.ts
|
|
823
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
824
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
825
|
+
var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
|
|
826
|
+
var ITEMS_TERMINAL_STATUSES = [429];
|
|
827
|
+
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
828
|
+
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
829
|
+
var TasksApiError = class extends Error {
|
|
830
|
+
constructor(message, status) {
|
|
831
|
+
super(message);
|
|
832
|
+
this.status = status;
|
|
833
|
+
this.name = "TasksApiError";
|
|
796
834
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
835
|
+
};
|
|
836
|
+
async function withRequestTimeout(timeoutMs, external, send) {
|
|
837
|
+
const { signal, cleanup } = withTimeoutSignal(timeoutMs, external);
|
|
838
|
+
try {
|
|
839
|
+
return await send(signal);
|
|
840
|
+
} finally {
|
|
841
|
+
cleanup();
|
|
800
842
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
843
|
+
}
|
|
844
|
+
var TasksApi = class {
|
|
845
|
+
baseUrl;
|
|
846
|
+
key;
|
|
847
|
+
logger;
|
|
848
|
+
constructor(options) {
|
|
849
|
+
this.baseUrl = options.baseUrl;
|
|
850
|
+
this.key = options.key;
|
|
851
|
+
this.logger = options.logger;
|
|
804
852
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
853
|
+
headers() {
|
|
854
|
+
return {
|
|
855
|
+
"content-type": "application/json",
|
|
856
|
+
authorization: `Bearer ${this.key}`
|
|
857
|
+
};
|
|
808
858
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
859
|
+
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
860
|
+
async poll(request, signal) {
|
|
861
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
862
|
+
let response;
|
|
863
|
+
try {
|
|
864
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers: this.headers(),
|
|
867
|
+
body: JSON.stringify(request),
|
|
868
|
+
signal: timeoutSignal
|
|
869
|
+
});
|
|
870
|
+
} finally {
|
|
871
|
+
cleanup();
|
|
813
872
|
}
|
|
814
|
-
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
875
|
+
}
|
|
876
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
815
877
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Abre uma run por conta própria (modo start). Um request só — como no poll, o
|
|
880
|
+
* ladder de espera entre tentativas é decisão do chamador (LocalRunner).
|
|
881
|
+
* 4xx (401 = chave rejeitada) vira TasksApiError com o status.
|
|
882
|
+
*/
|
|
883
|
+
async createRun(request, signal) {
|
|
884
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
|
|
885
|
+
let response;
|
|
886
|
+
try {
|
|
887
|
+
response = await fetch(`${this.baseUrl}/runs`, {
|
|
888
|
+
method: "POST",
|
|
889
|
+
headers: this.headers(),
|
|
890
|
+
body: JSON.stringify(request),
|
|
891
|
+
signal: timeoutSignal
|
|
892
|
+
});
|
|
893
|
+
} finally {
|
|
894
|
+
cleanup();
|
|
895
|
+
}
|
|
896
|
+
if (!response.ok) {
|
|
897
|
+
throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
|
|
898
|
+
}
|
|
899
|
+
const body = await response.json().catch(() => null);
|
|
900
|
+
if (typeof body?.id !== "string" || typeof body.task !== "string") {
|
|
901
|
+
throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
id: body.id,
|
|
905
|
+
task: body.task,
|
|
906
|
+
params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
|
|
907
|
+
form: null,
|
|
908
|
+
source: typeof body.source === "string" ? body.source : "agent",
|
|
909
|
+
created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
910
|
+
};
|
|
819
911
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
function stepLabel(op, description) {
|
|
838
|
-
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
839
|
-
}
|
|
840
|
-
function levelForStatus(status) {
|
|
841
|
-
switch (status) {
|
|
842
|
-
case "warning":
|
|
843
|
-
return "warning";
|
|
844
|
-
case "error":
|
|
845
|
-
return "error";
|
|
846
|
-
case "cancelled":
|
|
847
|
-
return "warning";
|
|
848
|
-
default:
|
|
849
|
-
return "success";
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
function colorForStatus(status) {
|
|
853
|
-
switch (status) {
|
|
854
|
-
case "warning":
|
|
855
|
-
return "yellow";
|
|
856
|
-
case "error":
|
|
857
|
-
return "red";
|
|
858
|
-
case "cancelled":
|
|
859
|
-
return "gray";
|
|
860
|
-
default:
|
|
861
|
-
return "green";
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
var Step = class {
|
|
865
|
-
constructor(transaction, parentStepId, op, description) {
|
|
866
|
-
this.transaction = transaction;
|
|
867
|
-
this.parentStepId = parentStepId;
|
|
868
|
-
this.op = op;
|
|
869
|
-
this.description = description;
|
|
870
|
-
}
|
|
871
|
-
stepId = stepId();
|
|
872
|
-
startTimestamp = nowISO();
|
|
873
|
-
status = "ok";
|
|
874
|
-
endTimestamp;
|
|
875
|
-
data;
|
|
876
|
-
setStatus(status) {
|
|
877
|
-
this.status = status;
|
|
878
|
-
return this;
|
|
879
|
-
}
|
|
880
|
-
setData(key, value) {
|
|
881
|
-
this.data = { ...this.data, [key]: value };
|
|
882
|
-
return this;
|
|
883
|
-
}
|
|
884
|
-
/** Step aninhada (filha desta step). */
|
|
885
|
-
startChild(op, description) {
|
|
886
|
-
return this.transaction.createStep(this.stepId, op, description);
|
|
887
|
-
}
|
|
888
|
-
finish() {
|
|
889
|
-
if (this.endTimestamp) return;
|
|
890
|
-
this.endTimestamp = nowISO();
|
|
891
|
-
this.transaction.emit(
|
|
892
|
-
"step",
|
|
893
|
-
"finish",
|
|
894
|
-
stepLabel(this.op, this.description),
|
|
895
|
-
this.startTimestamp,
|
|
896
|
-
this.endTimestamp,
|
|
897
|
-
this.status
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
toJSON() {
|
|
901
|
-
return {
|
|
902
|
-
step_id: this.stepId,
|
|
903
|
-
parent_step_id: this.parentStepId,
|
|
904
|
-
op: this.op,
|
|
905
|
-
description: this.description,
|
|
906
|
-
status: this.status,
|
|
907
|
-
start_timestamp: this.startTimestamp,
|
|
908
|
-
timestamp: this.endTimestamp ?? nowISO(),
|
|
909
|
-
data: this.data
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
};
|
|
913
|
-
var Transaction = class {
|
|
914
|
-
constructor(client2, name, op, sampled = true, runId) {
|
|
915
|
-
this.client = client2;
|
|
916
|
-
this.name = name;
|
|
917
|
-
this.op = op;
|
|
918
|
-
this.sampled = sampled;
|
|
919
|
-
this.runId = runId;
|
|
920
|
-
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
921
|
-
}
|
|
922
|
-
eventId = uuid4();
|
|
923
|
-
stepId = stepId();
|
|
924
|
-
startTimestamp = nowISO();
|
|
925
|
-
status = "ok";
|
|
926
|
-
steps = [];
|
|
927
|
-
tags = {};
|
|
928
|
-
finished = false;
|
|
929
|
-
lifecycleSink;
|
|
930
|
-
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
931
|
-
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
932
|
-
lifecycleBacklog = [];
|
|
933
|
-
/**
|
|
934
|
-
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
935
|
-
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
936
|
-
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
937
|
-
* escreve no stdout do processo do usuário sem permissão.
|
|
938
|
-
*/
|
|
939
|
-
emit(kind, phase, label, startTs, endTs, status) {
|
|
940
|
-
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
941
|
-
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
942
|
-
if (this.client?.debug) {
|
|
943
|
-
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
944
|
-
console.log(`${tag} ${label}${suffix}`);
|
|
945
|
-
}
|
|
946
|
-
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
947
|
-
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
948
|
-
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
949
|
-
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
950
|
-
}
|
|
951
|
-
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
952
|
-
setLifecycleSink(sink) {
|
|
953
|
-
this.lifecycleSink = sink;
|
|
954
|
-
const backlog = this.lifecycleBacklog;
|
|
955
|
-
this.lifecycleBacklog = [];
|
|
956
|
-
for (const [level, message] of backlog) sink(level, message);
|
|
957
|
-
}
|
|
958
|
-
createStep(parentStepId, op, description) {
|
|
959
|
-
const step = new Step(this, parentStepId, op, description);
|
|
960
|
-
this.steps.push(step);
|
|
961
|
-
this.emit("step", "start", stepLabel(op, description));
|
|
962
|
-
return step;
|
|
963
|
-
}
|
|
964
|
-
/** Step direta da transaction. */
|
|
965
|
-
startChild(op, description) {
|
|
966
|
-
return this.createStep(this.stepId, op, description);
|
|
967
|
-
}
|
|
968
|
-
setTag(key, value) {
|
|
969
|
-
this.tags[key] = value;
|
|
970
|
-
return this;
|
|
971
|
-
}
|
|
972
|
-
setStatus(status) {
|
|
973
|
-
this.status = status;
|
|
974
|
-
return this;
|
|
975
|
-
}
|
|
976
|
-
setName(name) {
|
|
977
|
-
this.name = name;
|
|
978
|
-
return this;
|
|
979
|
-
}
|
|
980
|
-
finish() {
|
|
981
|
-
if (this.finished) return;
|
|
982
|
-
this.finished = true;
|
|
983
|
-
for (const step of this.steps) step.finish();
|
|
984
|
-
const endTimestamp = nowISO();
|
|
985
|
-
this.emit(
|
|
986
|
-
"transaction",
|
|
987
|
-
"finish",
|
|
988
|
-
this.op ? `${this.name} (${this.op})` : this.name,
|
|
989
|
-
this.startTimestamp,
|
|
990
|
-
endTimestamp,
|
|
991
|
-
this.status
|
|
992
|
-
);
|
|
993
|
-
if (!this.sampled || !this.client) return;
|
|
994
|
-
const payload = {
|
|
995
|
-
event_id: this.eventId,
|
|
996
|
-
name: this.name,
|
|
997
|
-
op: this.op,
|
|
998
|
-
status: this.status,
|
|
999
|
-
start_timestamp: this.startTimestamp,
|
|
1000
|
-
timestamp: endTimestamp,
|
|
1001
|
-
steps: this.steps.map((step) => step.toJSON()),
|
|
1002
|
-
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
1003
|
-
run_id: this.runId
|
|
1004
|
-
};
|
|
1005
|
-
this.client.sendTransaction(payload);
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
// src/tasks/api.ts
|
|
1010
|
-
var POLL_TIMEOUT_MS = 3e4;
|
|
1011
|
-
var UPDATE_TIMEOUT_MS = 1e4;
|
|
1012
|
-
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
1013
|
-
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
1014
|
-
var TasksApiError = class extends Error {
|
|
1015
|
-
constructor(message, status) {
|
|
1016
|
-
super(message);
|
|
1017
|
-
this.status = status;
|
|
1018
|
-
this.name = "TasksApiError";
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
var TasksApi = class {
|
|
1022
|
-
baseUrl;
|
|
1023
|
-
key;
|
|
1024
|
-
logger;
|
|
1025
|
-
constructor(options) {
|
|
1026
|
-
this.baseUrl = options.baseUrl;
|
|
1027
|
-
this.key = options.key;
|
|
1028
|
-
this.logger = options.logger;
|
|
1029
|
-
}
|
|
1030
|
-
headers() {
|
|
1031
|
-
return {
|
|
1032
|
-
"content-type": "application/json",
|
|
1033
|
-
authorization: `Bearer ${this.key}`
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
1037
|
-
async poll(request, signal) {
|
|
1038
|
-
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
1039
|
-
let response;
|
|
1040
|
-
try {
|
|
1041
|
-
response = await fetch(`${this.baseUrl}/poll`, {
|
|
1042
|
-
method: "POST",
|
|
1043
|
-
headers: this.headers(),
|
|
1044
|
-
body: JSON.stringify(request),
|
|
1045
|
-
signal: timeoutSignal
|
|
1046
|
-
});
|
|
1047
|
-
} finally {
|
|
1048
|
-
cleanup();
|
|
1049
|
-
}
|
|
1050
|
-
if (!response.ok) {
|
|
1051
|
-
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
1052
|
-
}
|
|
1053
|
-
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
1054
|
-
}
|
|
1055
|
-
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
1056
|
-
parsePollResponse(body, max) {
|
|
1057
|
-
const runs = body?.runs;
|
|
1058
|
-
if (!Array.isArray(runs)) {
|
|
1059
|
-
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
1060
|
-
}
|
|
1061
|
-
const valid = [];
|
|
1062
|
-
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
1063
|
-
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
1064
|
-
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
1065
|
-
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
1066
|
-
}
|
|
1067
|
-
if (valid.length < runs.length) {
|
|
1068
|
-
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
1069
|
-
}
|
|
1070
|
-
return { runs: valid };
|
|
912
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
913
|
+
parsePollResponse(body, max) {
|
|
914
|
+
const runs = body?.runs;
|
|
915
|
+
if (!Array.isArray(runs)) {
|
|
916
|
+
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
917
|
+
}
|
|
918
|
+
const valid = [];
|
|
919
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
920
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
921
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
922
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
923
|
+
}
|
|
924
|
+
if (valid.length < runs.length) {
|
|
925
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
926
|
+
}
|
|
927
|
+
return { runs: valid };
|
|
1071
928
|
}
|
|
1072
929
|
/**
|
|
1073
930
|
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
@@ -1120,12 +977,9 @@ var TasksApi = class {
|
|
|
1120
977
|
body: JSON.stringify(request),
|
|
1121
978
|
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1122
979
|
}),
|
|
1123
|
-
{ logger: this.logger, label: `itens da run ${runId}
|
|
980
|
+
{ logger: this.logger, label: `itens da run ${runId}`, noRetryStatuses: ITEMS_TERMINAL_STATUSES }
|
|
1124
981
|
);
|
|
1125
982
|
} catch (error) {
|
|
1126
|
-
if (error instanceof Error && error.message === "HTTP 429") {
|
|
1127
|
-
throw new TasksApiError("envio de itens rejeitado (HTTP 429)", 429);
|
|
1128
|
-
}
|
|
1129
983
|
throw new Error(`envio de itens da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
1130
984
|
}
|
|
1131
985
|
if (!response.ok) {
|
|
@@ -1142,21 +996,19 @@ var TasksApi = class {
|
|
|
1142
996
|
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
1143
997
|
*/
|
|
1144
998
|
async createAttachment(runId, request, signal) {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
|
|
999
|
+
const response = await fetchWithRetry(
|
|
1000
|
+
() => withRequestTimeout(
|
|
1001
|
+
UPDATE_TIMEOUT_MS,
|
|
1002
|
+
signal,
|
|
1003
|
+
(timeoutSignal) => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
|
|
1150
1004
|
method: "POST",
|
|
1151
1005
|
headers: this.headers(),
|
|
1152
1006
|
body: JSON.stringify(request),
|
|
1153
1007
|
signal: timeoutSignal
|
|
1154
|
-
})
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
cleanup();
|
|
1159
|
-
}
|
|
1008
|
+
})
|
|
1009
|
+
),
|
|
1010
|
+
{ logger: this.logger, label: `anexo da run ${runId}`, noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES }
|
|
1011
|
+
);
|
|
1160
1012
|
if (!response.ok) {
|
|
1161
1013
|
throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1162
1014
|
}
|
|
@@ -1176,22 +1028,20 @@ var TasksApi = class {
|
|
|
1176
1028
|
* está na assinatura da URL, e mandar o client secret para fora da API seria vazamento.
|
|
1177
1029
|
*/
|
|
1178
1030
|
async uploadAttachment(url, headers, body, signal) {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
() => fetch(url, {
|
|
1031
|
+
const response = await fetchWithRetry(
|
|
1032
|
+
() => withRequestTimeout(
|
|
1033
|
+
UPLOAD_TIMEOUT_MS,
|
|
1034
|
+
signal,
|
|
1035
|
+
(timeoutSignal) => fetch(url, {
|
|
1184
1036
|
method: "PUT",
|
|
1185
1037
|
headers,
|
|
1186
1038
|
// BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
|
|
1187
1039
|
body,
|
|
1188
1040
|
signal: timeoutSignal
|
|
1189
|
-
})
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
cleanup();
|
|
1194
|
-
}
|
|
1041
|
+
})
|
|
1042
|
+
),
|
|
1043
|
+
{ delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
|
|
1044
|
+
);
|
|
1195
1045
|
if (!response.ok) {
|
|
1196
1046
|
throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
|
|
1197
1047
|
}
|
|
@@ -1213,14 +1063,87 @@ var TasksApi = class {
|
|
|
1213
1063
|
}
|
|
1214
1064
|
};
|
|
1215
1065
|
|
|
1216
|
-
// src/tasks/
|
|
1217
|
-
var
|
|
1218
|
-
var
|
|
1219
|
-
var
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1066
|
+
// src/tasks/backoff.ts
|
|
1067
|
+
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
1068
|
+
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
1069
|
+
var Backoff = class {
|
|
1070
|
+
constructor(logger, label, floorMs = 0) {
|
|
1071
|
+
this.logger = logger;
|
|
1072
|
+
this.label = label;
|
|
1073
|
+
this.floorMs = floorMs;
|
|
1074
|
+
}
|
|
1075
|
+
failures = 0;
|
|
1076
|
+
authFailures = 0;
|
|
1077
|
+
reset() {
|
|
1078
|
+
this.failures = 0;
|
|
1079
|
+
this.authFailures = 0;
|
|
1080
|
+
}
|
|
1081
|
+
next(error) {
|
|
1082
|
+
if (error instanceof TasksApiError && (error.status === 401 || error.status === 402)) {
|
|
1083
|
+
this.authFailures++;
|
|
1084
|
+
if (this.authFailures === 1 || this.authFailures % 10 === 0) {
|
|
1085
|
+
this.logger.error(
|
|
1086
|
+
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"
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
|
|
1090
|
+
}
|
|
1091
|
+
this.failures++;
|
|
1092
|
+
const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
|
|
1093
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
|
|
1094
|
+
this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1095
|
+
return delay;
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// src/tasks/errors.ts
|
|
1100
|
+
function runCanceledError() {
|
|
1101
|
+
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1102
|
+
}
|
|
1103
|
+
function taskTimeoutError(timeoutMs) {
|
|
1104
|
+
return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
1105
|
+
name: "TaskTimeout"
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
|
|
1109
|
+
function isTerminalError(error) {
|
|
1110
|
+
return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/retry.ts
|
|
1114
|
+
var DEFAULT_RETRIES = 3;
|
|
1115
|
+
var DEFAULT_DELAY_MS = 1e3;
|
|
1116
|
+
function resolveDelay(delay, attempt, fallbackMs) {
|
|
1117
|
+
const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
|
|
1118
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1119
|
+
}
|
|
1120
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
1121
|
+
const requested = options.retries ?? DEFAULT_RETRIES;
|
|
1122
|
+
const retries = Number.isFinite(requested) ? Math.max(0, Math.floor(requested)) : DEFAULT_RETRIES;
|
|
1123
|
+
const wait = options.wait ?? sleep;
|
|
1124
|
+
for (let attempt = 1; ; attempt++) {
|
|
1125
|
+
if (options.signal?.aborted) throw runCanceledError();
|
|
1126
|
+
try {
|
|
1127
|
+
return await fn(attempt);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
const exhausted = attempt > retries;
|
|
1130
|
+
const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
|
|
1131
|
+
if (exhausted || !retriable) throw error;
|
|
1132
|
+
const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
|
|
1133
|
+
options.onRetry?.(error, attempt, delay);
|
|
1134
|
+
await wait(delay);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/tasks/attachments.ts
|
|
1140
|
+
var MAX_ATTACHMENT_BYTES = 5242880;
|
|
1141
|
+
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
1142
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1143
|
+
var MAX_CAPTION_CHARS = 200;
|
|
1144
|
+
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1145
|
+
async function readImageFile(path) {
|
|
1146
|
+
if (!isNode()) {
|
|
1224
1147
|
throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
|
|
1225
1148
|
}
|
|
1226
1149
|
const { readFile } = await import("fs/promises");
|
|
@@ -1231,6 +1154,7 @@ function startsWith(bytes, signature, offset = 0) {
|
|
|
1231
1154
|
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1232
1155
|
}
|
|
1233
1156
|
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1157
|
+
var MAX_PNG_DIMENSION = 2e4;
|
|
1234
1158
|
var JPEG_MAGIC = [255, 216, 255];
|
|
1235
1159
|
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1236
1160
|
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
@@ -1246,6 +1170,7 @@ function readPngDimensions(bytes) {
|
|
|
1246
1170
|
const width = view.getUint32(16);
|
|
1247
1171
|
const height = view.getUint32(20);
|
|
1248
1172
|
if (width === 0 || height === 0) return void 0;
|
|
1173
|
+
if (width > MAX_PNG_DIMENSION || height > MAX_PNG_DIMENSION) return void 0;
|
|
1249
1174
|
return { width, height };
|
|
1250
1175
|
}
|
|
1251
1176
|
async function toBytes(input) {
|
|
@@ -1262,16 +1187,17 @@ var LOG_LEVEL_COLOR = {
|
|
|
1262
1187
|
error: "red",
|
|
1263
1188
|
success: "green"
|
|
1264
1189
|
};
|
|
1190
|
+
function joinLogArgs(message, rest) {
|
|
1191
|
+
if (rest.length === 0) return safeString(message);
|
|
1192
|
+
return [message, ...rest].map(safeString).join(" ");
|
|
1193
|
+
}
|
|
1265
1194
|
function printRunLog(task, level, message) {
|
|
1266
1195
|
const line = colorize(`[hobots:${task}] ${level.toUpperCase()}`, LOG_LEVEL_COLOR[level]);
|
|
1267
|
-
const
|
|
1268
|
-
console[
|
|
1269
|
-
}
|
|
1270
|
-
function runCanceledError() {
|
|
1271
|
-
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1196
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1197
|
+
console[method2](line, message);
|
|
1272
1198
|
}
|
|
1273
1199
|
function cancelableSleep(ms, signal) {
|
|
1274
|
-
return new Promise((
|
|
1200
|
+
return new Promise((resolve2, reject) => {
|
|
1275
1201
|
if (signal.aborted) {
|
|
1276
1202
|
reject(runCanceledError());
|
|
1277
1203
|
return;
|
|
@@ -1282,7 +1208,7 @@ function cancelableSleep(ms, signal) {
|
|
|
1282
1208
|
};
|
|
1283
1209
|
const timer = setTimeout(() => {
|
|
1284
1210
|
signal.removeEventListener("abort", onAbort);
|
|
1285
|
-
|
|
1211
|
+
resolve2();
|
|
1286
1212
|
}, ms);
|
|
1287
1213
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1288
1214
|
});
|
|
@@ -1312,7 +1238,7 @@ function scrubPayloadValue(value, depth) {
|
|
|
1312
1238
|
return out;
|
|
1313
1239
|
}
|
|
1314
1240
|
var RunReporter = class {
|
|
1315
|
-
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1241
|
+
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
1316
1242
|
this.api = api;
|
|
1317
1243
|
this.runId = runId;
|
|
1318
1244
|
this.logger = logger;
|
|
@@ -1322,24 +1248,47 @@ var RunReporter = class {
|
|
|
1322
1248
|
Math.max(MIN_CANCEL_CHECK_INTERVAL_MS, cancelCheckIntervalMs ?? DEFAULT_CANCEL_CHECK_INTERVAL_MS)
|
|
1323
1249
|
);
|
|
1324
1250
|
const log = {
|
|
1325
|
-
debug: (message) => this.log("debug", message),
|
|
1326
|
-
info: (message) => this.log("info", message),
|
|
1327
|
-
warn: (message) => this.log("warning", message),
|
|
1328
|
-
error: (message) => this.log("error", message),
|
|
1329
|
-
success: (message) => this.log("success", message)
|
|
1251
|
+
debug: (message, ...rest) => this.log("debug", joinLogArgs(message, rest)),
|
|
1252
|
+
info: (message, ...rest) => this.log("info", joinLogArgs(message, rest)),
|
|
1253
|
+
warn: (message, ...rest) => this.log("warning", joinLogArgs(message, rest)),
|
|
1254
|
+
error: (message, ...rest) => this.log("error", joinLogArgs(message, rest)),
|
|
1255
|
+
success: (message, ...rest) => this.log("success", joinLogArgs(message, rest))
|
|
1330
1256
|
};
|
|
1331
1257
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1258
|
+
const checkCanceled = () => this.checkCanceled();
|
|
1259
|
+
tx.setStepGuard(checkCanceled);
|
|
1332
1260
|
const items = {
|
|
1333
|
-
add: (item) =>
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1261
|
+
add: (item) => {
|
|
1262
|
+
checkCanceled();
|
|
1263
|
+
this.addItem(item.status, item.message, item.payload, item.id);
|
|
1264
|
+
},
|
|
1265
|
+
succeeded: (payload, options) => {
|
|
1266
|
+
checkCanceled();
|
|
1267
|
+
this.addItem("succeeded", void 0, payload, options?.id);
|
|
1268
|
+
},
|
|
1269
|
+
failed: (message, payload, options) => {
|
|
1270
|
+
checkCanceled();
|
|
1271
|
+
this.addItem("failed", message, payload, options?.id);
|
|
1272
|
+
},
|
|
1273
|
+
occurrence: (message, payload, options) => {
|
|
1274
|
+
checkCanceled();
|
|
1275
|
+
this.addItem("occurrence", message, payload, options?.id);
|
|
1276
|
+
}
|
|
1337
1277
|
};
|
|
1338
1278
|
const progress = Object.defineProperties(
|
|
1339
1279
|
{
|
|
1340
|
-
total: (total) =>
|
|
1341
|
-
|
|
1342
|
-
|
|
1280
|
+
total: (total) => {
|
|
1281
|
+
checkCanceled();
|
|
1282
|
+
this.setProgress(this.progressCurrent, total);
|
|
1283
|
+
},
|
|
1284
|
+
advance: (count = 1) => {
|
|
1285
|
+
checkCanceled();
|
|
1286
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
1287
|
+
},
|
|
1288
|
+
set: (current, total) => {
|
|
1289
|
+
checkCanceled();
|
|
1290
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
1291
|
+
}
|
|
1343
1292
|
},
|
|
1344
1293
|
{
|
|
1345
1294
|
current: { enumerable: true, get: () => this.progressCurrent },
|
|
@@ -1351,18 +1300,20 @@ var RunReporter = class {
|
|
|
1351
1300
|
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1352
1301
|
);
|
|
1353
1302
|
const signal = this.abortController.signal;
|
|
1303
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
1354
1304
|
this.context = {
|
|
1355
1305
|
runId,
|
|
1356
1306
|
task,
|
|
1307
|
+
params,
|
|
1357
1308
|
log,
|
|
1358
1309
|
items,
|
|
1359
1310
|
progress,
|
|
1360
|
-
tx,
|
|
1311
|
+
transaction: tx,
|
|
1361
1312
|
signal,
|
|
1362
|
-
throwIfCanceled:
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1313
|
+
throwIfCanceled: checkCanceled,
|
|
1314
|
+
sleep: sleep2,
|
|
1315
|
+
attach: (image, options) => this.attach(image, options),
|
|
1316
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1366
1317
|
};
|
|
1367
1318
|
}
|
|
1368
1319
|
context;
|
|
@@ -1411,6 +1362,10 @@ var RunReporter = class {
|
|
|
1411
1362
|
get isDead() {
|
|
1412
1363
|
return this.dead;
|
|
1413
1364
|
}
|
|
1365
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
1366
|
+
checkCanceled() {
|
|
1367
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
1368
|
+
}
|
|
1414
1369
|
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
1415
1370
|
abort() {
|
|
1416
1371
|
this.stopCancelWatch();
|
|
@@ -1447,7 +1402,7 @@ var RunReporter = class {
|
|
|
1447
1402
|
}
|
|
1448
1403
|
log(level, message) {
|
|
1449
1404
|
if (this.dead) return;
|
|
1450
|
-
const scrubbed = scrubSecrets(message);
|
|
1405
|
+
const scrubbed = scrubSecrets(safeString(message));
|
|
1451
1406
|
printRunLog(this.context.task, level, scrubbed);
|
|
1452
1407
|
this.push(level, scrubbed, true);
|
|
1453
1408
|
}
|
|
@@ -1506,13 +1461,25 @@ var RunReporter = class {
|
|
|
1506
1461
|
this.stopCancelWatch();
|
|
1507
1462
|
this.clearFlushTimer();
|
|
1508
1463
|
this.forceFinalProgress();
|
|
1509
|
-
this.enqueue({ status: "succeeded", logs: this.
|
|
1464
|
+
this.enqueue({ status: "succeeded", logs: this.takeLogsForTerminal() });
|
|
1510
1465
|
}
|
|
1511
1466
|
reportFailure(error) {
|
|
1512
1467
|
this.stopCancelWatch();
|
|
1513
1468
|
this.clearFlushTimer();
|
|
1514
1469
|
this.forceFinalProgress();
|
|
1515
|
-
this.enqueue({ status: "failed", error, logs: this.
|
|
1470
|
+
this.enqueue({ status: "failed", error, logs: this.takeLogsForTerminal() });
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Esvazia o buffer para o PATCH terminal respeitando o teto de 100 logs por PATCH:
|
|
1474
|
+
* o excedente sai antes, em PATCHes `running` na mesma cadeia (a ordem se mantém).
|
|
1475
|
+
*/
|
|
1476
|
+
takeLogsForTerminal() {
|
|
1477
|
+
let logs = this.takeLogs();
|
|
1478
|
+
while (logs.length > MAX_LOGS_PER_PATCH) {
|
|
1479
|
+
this.enqueue({ status: "running", logs: logs.slice(0, MAX_LOGS_PER_PATCH) });
|
|
1480
|
+
logs = logs.slice(MAX_LOGS_PER_PATCH);
|
|
1481
|
+
}
|
|
1482
|
+
return logs;
|
|
1516
1483
|
}
|
|
1517
1484
|
// ── Progresso ────────────────────────────────────────────────────────────────
|
|
1518
1485
|
/** Valida, clampa e marca o snapshot como pendente; agenda flush só se nada mais vai sair. */
|
|
@@ -1620,7 +1587,7 @@ var RunReporter = class {
|
|
|
1620
1587
|
id: id ? truncate(String(id), 255) : uuid4(),
|
|
1621
1588
|
seq: this.itemSeq++,
|
|
1622
1589
|
status,
|
|
1623
|
-
...message ? { message: truncate(scrubSecrets(message), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1590
|
+
...message ? { message: truncate(scrubSecrets(safeString(message)), MAX_ITEM_MESSAGE_CHARS) } : {},
|
|
1624
1591
|
...scrubbedPayload ? { payload: scrubbedPayload } : {},
|
|
1625
1592
|
ts: nowISO()
|
|
1626
1593
|
});
|
|
@@ -1689,13 +1656,13 @@ var RunReporter = class {
|
|
|
1689
1656
|
this.flushItems();
|
|
1690
1657
|
await Promise.race([
|
|
1691
1658
|
this.itemsChain,
|
|
1692
|
-
new Promise((
|
|
1659
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1693
1660
|
]);
|
|
1694
1661
|
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1695
1662
|
this.flushItems();
|
|
1696
1663
|
await Promise.race([
|
|
1697
1664
|
this.itemsChain,
|
|
1698
|
-
new Promise((
|
|
1665
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1699
1666
|
]);
|
|
1700
1667
|
}
|
|
1701
1668
|
}
|
|
@@ -1703,14 +1670,16 @@ var RunReporter = class {
|
|
|
1703
1670
|
/**
|
|
1704
1671
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
1705
1672
|
* o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
|
|
1706
|
-
* com um aviso no log da run.
|
|
1707
|
-
* `
|
|
1673
|
+
* com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
|
|
1674
|
+
* `hobots.captureException` também cai aqui via run-context-store.
|
|
1708
1675
|
*/
|
|
1709
1676
|
async attach(image, options = {}) {
|
|
1710
1677
|
const id = uuid4();
|
|
1711
1678
|
const level = options.level ?? "info";
|
|
1712
|
-
const
|
|
1713
|
-
const
|
|
1679
|
+
const rawCaption = options.caption !== void 0 ? safeString(options.caption) : void 0;
|
|
1680
|
+
const rawMessage = options.message !== void 0 ? safeString(options.message) : void 0;
|
|
1681
|
+
const caption = rawCaption ? truncate(scrubSecrets(rawCaption), MAX_CAPTION_CHARS) : void 0;
|
|
1682
|
+
const message = rawMessage ?? rawCaption ?? "captura de tela";
|
|
1714
1683
|
if (this.dead) return { id, ok: false, reason: "canceled" };
|
|
1715
1684
|
if (this.attachmentsUnsupported) {
|
|
1716
1685
|
this.log(level, message);
|
|
@@ -1726,8 +1695,8 @@ var RunReporter = class {
|
|
|
1726
1695
|
printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
|
|
1727
1696
|
const logTs = this.push(level, scrubbed, true, [id]);
|
|
1728
1697
|
let settle;
|
|
1729
|
-
const result = new Promise((
|
|
1730
|
-
settle =
|
|
1698
|
+
const result = new Promise((resolve2) => {
|
|
1699
|
+
settle = resolve2;
|
|
1731
1700
|
});
|
|
1732
1701
|
const work = this.uploadAttachment(id, image, { caption, logTs, contentType: options.contentType }).then((reason) => settle(reason ? { id, ok: false, reason } : { id, ok: true })).catch(() => settle({ id, ok: false, reason: "upload-failed" }));
|
|
1733
1702
|
this.pendingAttachments.add(work);
|
|
@@ -1809,8 +1778,8 @@ var RunReporter = class {
|
|
|
1809
1778
|
this.uploadSlots--;
|
|
1810
1779
|
return Promise.resolve(release);
|
|
1811
1780
|
}
|
|
1812
|
-
return new Promise((
|
|
1813
|
-
this.uploadQueue.push(() =>
|
|
1781
|
+
return new Promise((resolve2) => {
|
|
1782
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
1814
1783
|
});
|
|
1815
1784
|
}
|
|
1816
1785
|
/**
|
|
@@ -1821,7 +1790,7 @@ var RunReporter = class {
|
|
|
1821
1790
|
if (this.pendingAttachments.size === 0) return;
|
|
1822
1791
|
await Promise.race([
|
|
1823
1792
|
Promise.allSettled([...this.pendingAttachments]),
|
|
1824
|
-
new Promise((
|
|
1793
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1825
1794
|
]);
|
|
1826
1795
|
}
|
|
1827
1796
|
enqueue(updateIn) {
|
|
@@ -1892,166 +1861,371 @@ async function runWithRunContext(store, fn) {
|
|
|
1892
1861
|
await ensureStorage();
|
|
1893
1862
|
return storage ? storage.run(store, fn) : fn();
|
|
1894
1863
|
}
|
|
1895
|
-
function
|
|
1896
|
-
return storage?.getStore()?.
|
|
1897
|
-
}
|
|
1898
|
-
function currentRunAttach() {
|
|
1899
|
-
return storage?.getStore()?.attach;
|
|
1864
|
+
function currentRunReporter() {
|
|
1865
|
+
return storage?.getStore()?.reporter;
|
|
1900
1866
|
}
|
|
1901
1867
|
|
|
1902
|
-
// src/
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
reraise();
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
const report = agent2.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
1923
|
-
addEmergencyTask(report);
|
|
1924
|
-
void report.finally(reraise);
|
|
1925
|
-
});
|
|
1926
|
-
process.on(signal, handler);
|
|
1927
|
-
signalHandlers.set(signal, handler);
|
|
1868
|
+
// src/transaction.ts
|
|
1869
|
+
function stepId() {
|
|
1870
|
+
return uuid4().slice(0, 16);
|
|
1871
|
+
}
|
|
1872
|
+
function stepLabel(op, description) {
|
|
1873
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
1874
|
+
}
|
|
1875
|
+
function levelForStatus(status) {
|
|
1876
|
+
switch (status) {
|
|
1877
|
+
case "warning":
|
|
1878
|
+
return "warning";
|
|
1879
|
+
case "error":
|
|
1880
|
+
return "error";
|
|
1881
|
+
case "cancelled":
|
|
1882
|
+
return "warning";
|
|
1883
|
+
default:
|
|
1884
|
+
return "success";
|
|
1928
1885
|
}
|
|
1929
|
-
const onFatal = markInternal((error) => {
|
|
1930
|
-
const report = agent2.activeCount > 0 ? agent2.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
|
|
1931
|
-
addEmergencyTask(report);
|
|
1932
|
-
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
1933
|
-
void report.finally(() => {
|
|
1934
|
-
console.error(error);
|
|
1935
|
-
process.exit(1);
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
});
|
|
1939
|
-
process.on("uncaughtException", onFatal);
|
|
1940
|
-
return () => {
|
|
1941
|
-
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
1942
|
-
process.removeListener("uncaughtException", onFatal);
|
|
1943
|
-
};
|
|
1944
1886
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1887
|
+
function colorForStatus(status) {
|
|
1888
|
+
switch (status) {
|
|
1889
|
+
case "warning":
|
|
1890
|
+
return "yellow";
|
|
1891
|
+
case "error":
|
|
1892
|
+
return "red";
|
|
1893
|
+
case "cancelled":
|
|
1894
|
+
return "gray";
|
|
1895
|
+
default:
|
|
1896
|
+
return "green";
|
|
1897
|
+
}
|
|
1956
1898
|
}
|
|
1957
|
-
var
|
|
1958
|
-
constructor(
|
|
1959
|
-
this.
|
|
1960
|
-
this.
|
|
1961
|
-
this.
|
|
1962
|
-
|
|
1963
|
-
this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
|
|
1964
|
-
this.client = options.client;
|
|
1965
|
-
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
1966
|
-
this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, options.concurrency ?? 1));
|
|
1967
|
-
this.instanceId = options.instanceId;
|
|
1968
|
-
this.taskTimeoutMs = options.taskTimeoutMs;
|
|
1969
|
-
this.logFlushIntervalMs = options.logFlushIntervalMs;
|
|
1970
|
-
this.cancelCheckIntervalMs = options.cancelCheckIntervalMs;
|
|
1971
|
-
this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
|
|
1972
|
-
this.logger.debug("agente configurado", { baseUrl: config.tasksBaseUrl });
|
|
1899
|
+
var Step = class {
|
|
1900
|
+
constructor(transaction, parentStepId, op, description) {
|
|
1901
|
+
this.transaction = transaction;
|
|
1902
|
+
this.parentStepId = parentStepId;
|
|
1903
|
+
this.op = op;
|
|
1904
|
+
this.description = description;
|
|
1973
1905
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
cancelCheckIntervalMs;
|
|
1983
|
-
cancelGraceMs;
|
|
1984
|
-
state = "idle";
|
|
1985
|
-
timer;
|
|
1986
|
-
pollAbort;
|
|
1987
|
-
consecutiveFailures = 0;
|
|
1988
|
-
consecutiveAuthFailures = 0;
|
|
1989
|
-
activeRuns = /* @__PURE__ */ new Set();
|
|
1990
|
-
reporters = /* @__PURE__ */ new Set();
|
|
1991
|
-
uninstallShutdown;
|
|
1992
|
-
get activeCount() {
|
|
1993
|
-
return this.activeRuns.size;
|
|
1906
|
+
stepId = stepId();
|
|
1907
|
+
startTimestamp = nowISO();
|
|
1908
|
+
status = "ok";
|
|
1909
|
+
endTimestamp;
|
|
1910
|
+
data;
|
|
1911
|
+
setStatus(status) {
|
|
1912
|
+
this.status = status;
|
|
1913
|
+
return this;
|
|
1994
1914
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
throw new Error("[hobots] start() chamado durante stop() \u2014 aguarde o stop terminar");
|
|
1999
|
-
}
|
|
2000
|
-
if (this.registry.size === 0) {
|
|
2001
|
-
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
2002
|
-
}
|
|
2003
|
-
this.state = "started";
|
|
2004
|
-
if (this.options.installExitHandlers !== false && !this.uninstallShutdown) {
|
|
2005
|
-
this.uninstallShutdown = installShutdownHandlers(this);
|
|
2006
|
-
}
|
|
2007
|
-
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
2008
|
-
this.schedule(0);
|
|
1915
|
+
setData(key, value) {
|
|
1916
|
+
this.data = { ...this.data, [key]: value };
|
|
1917
|
+
return this;
|
|
2009
1918
|
}
|
|
2010
|
-
/**
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
this.
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1919
|
+
/** Step aninhada (filha desta step). */
|
|
1920
|
+
startChild(op, description) {
|
|
1921
|
+
return this.transaction.createStep(this.stepId, op, description);
|
|
1922
|
+
}
|
|
1923
|
+
finish() {
|
|
1924
|
+
if (this.endTimestamp) return;
|
|
1925
|
+
this.endTimestamp = nowISO();
|
|
1926
|
+
this.transaction.emit(
|
|
1927
|
+
"step",
|
|
1928
|
+
"finish",
|
|
1929
|
+
stepLabel(this.op, this.description),
|
|
1930
|
+
this.startTimestamp,
|
|
1931
|
+
this.endTimestamp,
|
|
1932
|
+
this.status
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
toJSON() {
|
|
1936
|
+
return {
|
|
1937
|
+
step_id: this.stepId,
|
|
1938
|
+
parent_step_id: this.parentStepId,
|
|
1939
|
+
op: this.op,
|
|
1940
|
+
description: this.description,
|
|
1941
|
+
status: this.status,
|
|
1942
|
+
start_timestamp: this.startTimestamp,
|
|
1943
|
+
timestamp: this.endTimestamp ?? nowISO(),
|
|
1944
|
+
data: this.data
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
var Transaction = class {
|
|
1949
|
+
constructor(client, name, op, runId) {
|
|
1950
|
+
this.client = client;
|
|
1951
|
+
this.name = name;
|
|
1952
|
+
this.op = op;
|
|
1953
|
+
this.runId = runId;
|
|
1954
|
+
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
2029
1955
|
}
|
|
1956
|
+
eventId = uuid4();
|
|
1957
|
+
stepId = stepId();
|
|
1958
|
+
startTimestamp = nowISO();
|
|
1959
|
+
status = "ok";
|
|
1960
|
+
steps = [];
|
|
1961
|
+
tags = {};
|
|
1962
|
+
finished = false;
|
|
1963
|
+
lifecycleSink;
|
|
1964
|
+
// instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
|
|
1965
|
+
stepGuard;
|
|
1966
|
+
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
1967
|
+
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
1968
|
+
lifecycleBacklog = [];
|
|
2030
1969
|
/**
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2034
|
-
*
|
|
1970
|
+
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
1971
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
1972
|
+
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
1973
|
+
* escreve no stdout do processo do usuário sem permissão.
|
|
2035
1974
|
*/
|
|
2036
|
-
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
pending.push(
|
|
2043
|
-
this.api.updateRunOnce(
|
|
2044
|
-
reporter.context.runId,
|
|
2045
|
-
{ status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
|
|
2046
|
-
timeoutMs
|
|
2047
|
-
).catch(() => {
|
|
2048
|
-
})
|
|
2049
|
-
);
|
|
1975
|
+
emit(kind, phase, label, startTs, endTs, status) {
|
|
1976
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
1977
|
+
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
1978
|
+
if (this.client?.debug) {
|
|
1979
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
1980
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
2050
1981
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1982
|
+
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
1983
|
+
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
1984
|
+
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
1985
|
+
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
1986
|
+
}
|
|
1987
|
+
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
1988
|
+
setLifecycleSink(sink) {
|
|
1989
|
+
this.lifecycleSink = sink;
|
|
1990
|
+
const backlog = this.lifecycleBacklog;
|
|
1991
|
+
this.lifecycleBacklog = [];
|
|
1992
|
+
for (const [level, message] of backlog) sink(level, message);
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
|
|
1996
|
+
* `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
|
|
1997
|
+
*/
|
|
1998
|
+
setStepGuard(guard) {
|
|
1999
|
+
this.stepGuard = guard;
|
|
2000
|
+
}
|
|
2001
|
+
createStep(parentStepId, op, description) {
|
|
2002
|
+
this.stepGuard?.();
|
|
2003
|
+
const step = new Step(this, parentStepId, op, description);
|
|
2004
|
+
this.steps.push(step);
|
|
2005
|
+
this.emit("step", "start", stepLabel(op, description));
|
|
2006
|
+
return step;
|
|
2007
|
+
}
|
|
2008
|
+
/** Step direta da transaction. */
|
|
2009
|
+
startChild(op, description) {
|
|
2010
|
+
return this.createStep(this.stepId, op, description);
|
|
2011
|
+
}
|
|
2012
|
+
setTag(key, value) {
|
|
2013
|
+
this.tags[key] = value;
|
|
2014
|
+
return this;
|
|
2015
|
+
}
|
|
2016
|
+
setStatus(status) {
|
|
2017
|
+
this.status = status;
|
|
2018
|
+
return this;
|
|
2019
|
+
}
|
|
2020
|
+
setName(name) {
|
|
2021
|
+
this.name = name;
|
|
2022
|
+
return this;
|
|
2023
|
+
}
|
|
2024
|
+
finish() {
|
|
2025
|
+
if (this.finished) return;
|
|
2026
|
+
this.finished = true;
|
|
2027
|
+
for (const step of this.steps) step.finish();
|
|
2028
|
+
const endTimestamp = nowISO();
|
|
2029
|
+
this.emit(
|
|
2030
|
+
"transaction",
|
|
2031
|
+
"finish",
|
|
2032
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
2033
|
+
this.startTimestamp,
|
|
2034
|
+
endTimestamp,
|
|
2035
|
+
this.status
|
|
2036
|
+
);
|
|
2037
|
+
if (!this.client) return;
|
|
2038
|
+
const payload = {
|
|
2039
|
+
event_id: this.eventId,
|
|
2040
|
+
name: this.name,
|
|
2041
|
+
op: this.op,
|
|
2042
|
+
status: this.status,
|
|
2043
|
+
start_timestamp: this.startTimestamp,
|
|
2044
|
+
timestamp: endTimestamp,
|
|
2045
|
+
steps: this.steps.map((step) => step.toJSON()),
|
|
2046
|
+
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
2047
|
+
run_id: this.runId
|
|
2048
|
+
};
|
|
2049
|
+
this.client.sendTransaction(payload);
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// src/tasks/runner.ts
|
|
2054
|
+
var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
2055
|
+
async function executeRun(input) {
|
|
2056
|
+
const { run, task, api, client, logger, tracker } = input;
|
|
2057
|
+
const options = task.options;
|
|
2058
|
+
const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
|
|
2059
|
+
const reporter = new RunReporter(
|
|
2060
|
+
api,
|
|
2061
|
+
run.id,
|
|
2062
|
+
run.task,
|
|
2063
|
+
logger,
|
|
2064
|
+
tx,
|
|
2065
|
+
options.logFlushIntervalMs,
|
|
2066
|
+
options.cancelCheckIntervalMs,
|
|
2067
|
+
run.params
|
|
2068
|
+
);
|
|
2069
|
+
tracker.add(reporter);
|
|
2070
|
+
try {
|
|
2071
|
+
const ok = await reporter.reportRunning();
|
|
2072
|
+
if (!ok) {
|
|
2073
|
+
logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
|
|
2074
|
+
tx.setStatus("cancelled");
|
|
2075
|
+
tx.finish();
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
logger.debug(`executando run ${run.id} (${run.task})`);
|
|
2079
|
+
try {
|
|
2080
|
+
await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
|
|
2081
|
+
await reporter.finalizeAttachments();
|
|
2082
|
+
await reporter.finalizeItems();
|
|
2083
|
+
tx.setStatus("ok");
|
|
2084
|
+
tx.finish();
|
|
2085
|
+
reporter.reportSuccess();
|
|
2086
|
+
logger.debug(`run ${run.id} conclu\xEDda`);
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
await reporter.finalizeAttachments();
|
|
2089
|
+
await reporter.finalizeItems();
|
|
2090
|
+
tx.setStatus("error");
|
|
2091
|
+
tx.finish();
|
|
2092
|
+
reporter.reportFailure(toRunError(error));
|
|
2093
|
+
logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
2094
|
+
}
|
|
2095
|
+
await reporter.finalize();
|
|
2096
|
+
} finally {
|
|
2097
|
+
tracker.remove(reporter);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function invokeWithRetry(task, run, reporter, logger) {
|
|
2101
|
+
const { retries, retryDelayMs } = task.options;
|
|
2102
|
+
const total = retries + 1;
|
|
2103
|
+
const ctx = reporter.context;
|
|
2104
|
+
return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
|
|
2105
|
+
retries,
|
|
2106
|
+
delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
|
|
2107
|
+
shouldRetry: () => !reporter.isDead,
|
|
2108
|
+
signal: ctx.signal,
|
|
2109
|
+
wait: (ms) => ctx.sleep(ms),
|
|
2110
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2111
|
+
const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2112
|
+
ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
2117
|
+
var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
|
|
2118
|
+
async function invokeWithTimeout(task, run, reporter, logger) {
|
|
2119
|
+
const { timeoutMs, cancelGraceMs } = task.options;
|
|
2120
|
+
const handler = task.handler;
|
|
2121
|
+
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
2122
|
+
const races = [];
|
|
2123
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2124
|
+
races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
|
|
2125
|
+
}
|
|
2126
|
+
if (cancelGraceMs > 0) {
|
|
2127
|
+
races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
|
|
2128
|
+
}
|
|
2129
|
+
if (races.length === 0) {
|
|
2130
|
+
await invocation;
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const outcome = await Promise.race([invocation, ...races]);
|
|
2134
|
+
if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
|
|
2135
|
+
if (outcome === CANCEL_SENTINEL) {
|
|
2136
|
+
logger.warn(
|
|
2137
|
+
`run ${run.id} cancelada \u2014 handler n\xE3o respondeu a hobots.signal em ${cancelGraceMs}ms, liberando o slot (o handler pode continuar rodando em segundo plano)`
|
|
2138
|
+
);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
reporter.abort();
|
|
2142
|
+
invocation.then(() => logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
|
|
2143
|
+
throw taskTimeoutError(timeoutMs);
|
|
2144
|
+
}
|
|
2145
|
+
function waitForCancelGrace(signal, graceMs) {
|
|
2146
|
+
return new Promise((resolve2) => {
|
|
2147
|
+
if (signal.aborted) {
|
|
2148
|
+
setTimeout(resolve2, graceMs);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
2155
|
+
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
2156
|
+
function toRunError(error) {
|
|
2157
|
+
if (error instanceof Error) {
|
|
2158
|
+
return {
|
|
2159
|
+
type: error.name,
|
|
2160
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
2161
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/tasks/agent.ts
|
|
2168
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
2169
|
+
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
2170
|
+
var MAX_CLAIM_PER_POLL = 5;
|
|
2171
|
+
var TaskAgent = class {
|
|
2172
|
+
constructor(options) {
|
|
2173
|
+
this.options = options;
|
|
2174
|
+
this.logger = options.logger;
|
|
2175
|
+
this.api = options.api;
|
|
2176
|
+
this.client = options.client;
|
|
2177
|
+
this.registry = options.registry;
|
|
2178
|
+
this.tracker = options.tracker;
|
|
2179
|
+
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
2180
|
+
this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
|
|
2181
|
+
}
|
|
2182
|
+
logger;
|
|
2183
|
+
pollIntervalMs;
|
|
2184
|
+
api;
|
|
2185
|
+
client;
|
|
2186
|
+
registry;
|
|
2187
|
+
tracker;
|
|
2188
|
+
backoff;
|
|
2189
|
+
state = "idle";
|
|
2190
|
+
timer;
|
|
2191
|
+
pollAbort;
|
|
2192
|
+
activeRuns = /* @__PURE__ */ new Set();
|
|
2193
|
+
get activeCount() {
|
|
2194
|
+
return this.activeRuns.size;
|
|
2195
|
+
}
|
|
2196
|
+
start() {
|
|
2197
|
+
if (this.state === "started") return;
|
|
2198
|
+
if (this.state === "stopping") {
|
|
2199
|
+
throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
|
|
2200
|
+
}
|
|
2201
|
+
this.state = "started";
|
|
2202
|
+
this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
|
|
2203
|
+
this.schedule(0);
|
|
2204
|
+
}
|
|
2205
|
+
/** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
|
|
2206
|
+
wake() {
|
|
2207
|
+
if (this.state !== "started") return;
|
|
2208
|
+
if (this.timer) {
|
|
2209
|
+
clearTimeout(this.timer);
|
|
2210
|
+
this.timer = void 0;
|
|
2211
|
+
}
|
|
2212
|
+
this.schedule(0);
|
|
2213
|
+
}
|
|
2214
|
+
/** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
|
|
2215
|
+
async stop(timeoutMs = 3e4) {
|
|
2216
|
+
if (this.state === "idle") return true;
|
|
2217
|
+
this.state = "stopping";
|
|
2218
|
+
if (this.timer) {
|
|
2219
|
+
clearTimeout(this.timer);
|
|
2220
|
+
this.timer = void 0;
|
|
2221
|
+
}
|
|
2222
|
+
this.pollAbort?.abort();
|
|
2223
|
+
const all = Promise.allSettled([...this.activeRuns]).then(() => true);
|
|
2224
|
+
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
|
|
2225
|
+
this.state = "idle";
|
|
2226
|
+
return finished;
|
|
2227
|
+
}
|
|
2228
|
+
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
2055
2229
|
schedule(delayMs) {
|
|
2056
2230
|
if (this.state !== "started" || this.timer) return;
|
|
2057
2231
|
this.timer = setTimeout(() => {
|
|
@@ -2059,9 +2233,22 @@ var TaskAgent = class {
|
|
|
2059
2233
|
void this.tick();
|
|
2060
2234
|
}, delayMs);
|
|
2061
2235
|
}
|
|
2236
|
+
/** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
|
|
2237
|
+
freeSlots() {
|
|
2238
|
+
const tasks = [];
|
|
2239
|
+
let max = 0;
|
|
2240
|
+
for (const task of this.registry.remoteTasks()) {
|
|
2241
|
+
const free = task.options.concurrency - this.tracker.activeFor(task.slug);
|
|
2242
|
+
if (free <= 0) continue;
|
|
2243
|
+
tasks.push(task.slug);
|
|
2244
|
+
max += free;
|
|
2245
|
+
}
|
|
2246
|
+
return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
|
|
2247
|
+
}
|
|
2062
2248
|
async tick() {
|
|
2063
2249
|
if (this.state !== "started") return;
|
|
2064
|
-
|
|
2250
|
+
const { tasks, max } = this.freeSlots();
|
|
2251
|
+
if (max === 0) {
|
|
2065
2252
|
this.schedule(this.pollIntervalMs);
|
|
2066
2253
|
return;
|
|
2067
2254
|
}
|
|
@@ -2069,224 +2256,498 @@ var TaskAgent = class {
|
|
|
2069
2256
|
try {
|
|
2070
2257
|
const response = await this.api.poll(
|
|
2071
2258
|
{
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
// ambiente da instância: em agentes poll-only (sem heartbeat) este é o
|
|
2079
|
-
// único canal por onde o servidor descobre onde o robô está rodando
|
|
2080
|
-
environment: this.options.environment,
|
|
2081
|
-
tasks: [...this.registry.keys()],
|
|
2082
|
-
// o servidor calcula a tolerância de offline (3×) a partir disto em
|
|
2083
|
-
// agentes poll-only — sem o campo, um poll lento flaparia offline/online.
|
|
2084
|
-
// Clampado à faixa aceita pelo schema do poll (1s..15min).
|
|
2085
|
-
poll_interval_ms: Math.min(Math.max(this.pollIntervalMs, 1e3), 9e5),
|
|
2086
|
-
// recursos da máquina — alimenta a série da instância em agentes sem heartbeat
|
|
2087
|
-
// (o servidor grava no máx. 1 ponto/30s por instância)
|
|
2088
|
-
machine: this.options.machineSampler?.sample()
|
|
2089
|
-
},
|
|
2090
|
-
max: Math.min(MAX_CONCURRENCY, this.concurrency - this.activeRuns.size),
|
|
2091
|
-
tasks: [...this.registry.keys()]
|
|
2259
|
+
// identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
|
|
2260
|
+
// então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
|
|
2261
|
+
// tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
|
|
2262
|
+
instance: this.options.instanceInfo(),
|
|
2263
|
+
max,
|
|
2264
|
+
tasks
|
|
2092
2265
|
},
|
|
2093
2266
|
this.pollAbort.signal
|
|
2094
2267
|
);
|
|
2095
|
-
this.
|
|
2096
|
-
this.consecutiveAuthFailures = 0;
|
|
2268
|
+
this.backoff.reset();
|
|
2097
2269
|
for (const run of response.runs) {
|
|
2098
|
-
if (this.activeRuns.size >= this.concurrency) break;
|
|
2099
2270
|
this.dispatch(run);
|
|
2100
2271
|
}
|
|
2101
2272
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
2102
2273
|
} catch (error) {
|
|
2103
2274
|
if (this.state !== "started") return;
|
|
2104
|
-
this.schedule(this.
|
|
2275
|
+
this.schedule(this.backoff.next(error));
|
|
2105
2276
|
} finally {
|
|
2106
2277
|
this.pollAbort = void 0;
|
|
2107
2278
|
}
|
|
2108
2279
|
}
|
|
2109
|
-
delayAfterPollError(error) {
|
|
2110
|
-
if (error instanceof TasksApiError && error.status === 401) {
|
|
2111
|
-
this.consecutiveAuthFailures++;
|
|
2112
|
-
if (this.consecutiveAuthFailures === 1 || this.consecutiveAuthFailures % 10 === 0) {
|
|
2113
|
-
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
|
|
2114
|
-
}
|
|
2115
|
-
return Math.max(this.pollIntervalMs, AUTH_FAILURE_INTERVAL_MS);
|
|
2116
|
-
}
|
|
2117
|
-
this.consecutiveFailures++;
|
|
2118
|
-
const ladder = POLL_RETRY_DELAYS_MS[Math.min(this.consecutiveFailures - 1, POLL_RETRY_DELAYS_MS.length - 1)];
|
|
2119
|
-
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.pollIntervalMs, 16e3));
|
|
2120
|
-
this.logger.warn(`poll falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
2121
|
-
return delay;
|
|
2122
|
-
}
|
|
2123
2280
|
dispatch(run) {
|
|
2124
|
-
const promise = this.
|
|
2281
|
+
const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
|
|
2125
2282
|
this.activeRuns.delete(promise);
|
|
2126
2283
|
if (this.state === "started") this.schedule(0);
|
|
2127
2284
|
});
|
|
2128
2285
|
this.activeRuns.add(promise);
|
|
2129
2286
|
}
|
|
2130
|
-
async
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
2287
|
+
async executeClaimed(run) {
|
|
2288
|
+
const task = this.registry.get(run.task);
|
|
2289
|
+
if (!task || task.mode !== "remote") {
|
|
2290
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
|
|
2291
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
|
|
2292
|
+
reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
|
|
2293
|
+
await reporter.finalize();
|
|
2294
|
+
tx.setStatus("error");
|
|
2295
|
+
tx.finish();
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
// src/tasks/item-columns.ts
|
|
2303
|
+
var KEY_RE = /^[a-zA-Z0-9_.-]{1,100}$/;
|
|
2304
|
+
var MAX_COLUMNS = 12;
|
|
2305
|
+
var MAX_LABEL_CHARS = 60;
|
|
2306
|
+
function normalizeItemColumns(slug, input, logger) {
|
|
2307
|
+
if (input === void 0 || input === null) return void 0;
|
|
2308
|
+
const columns = input.columns;
|
|
2309
|
+
if (typeof input !== "object" || !Array.isArray(columns)) {
|
|
2310
|
+
logger.warn(`task "${slug}": items.columns precisa ser uma lista \u2014 declara\xE7\xE3o de colunas ignorada`);
|
|
2311
|
+
return void 0;
|
|
2312
|
+
}
|
|
2313
|
+
const result = [];
|
|
2314
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2315
|
+
for (const entry of columns) {
|
|
2316
|
+
const rawKey = typeof entry === "string" ? entry : entry?.key;
|
|
2317
|
+
const rawLabel = typeof entry === "object" && entry !== null ? entry.label : void 0;
|
|
2318
|
+
const key = typeof rawKey === "string" ? rawKey.trim() : "";
|
|
2319
|
+
if (!KEY_RE.test(key)) {
|
|
2320
|
+
logger.warn(`task "${slug}": coluna de item inv\xE1lida ignorada (${JSON.stringify(rawKey)}) \u2014 use letras, n\xFAmeros, ponto, h\xEDfen e underline`);
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
if (seen.has(key)) {
|
|
2324
|
+
logger.warn(`task "${slug}": coluna de item "${key}" repetida ignorada`);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
if (result.length >= MAX_COLUMNS) {
|
|
2328
|
+
logger.warn(`task "${slug}": no m\xE1ximo ${MAX_COLUMNS} colunas de item \u2014 "${key}" em diante ignoradas`);
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
seen.add(key);
|
|
2332
|
+
const label = typeof rawLabel === "string" ? rawLabel.trim().slice(0, MAX_LABEL_CHARS) : "";
|
|
2333
|
+
result.push(label ? { key, label } : { key });
|
|
2334
|
+
}
|
|
2335
|
+
return result.length > 0 ? result : void 0;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// src/tasks/local.ts
|
|
2339
|
+
var LocalRunner = class {
|
|
2340
|
+
constructor(task, deps) {
|
|
2341
|
+
this.task = task;
|
|
2342
|
+
this.deps = deps;
|
|
2343
|
+
this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
|
|
2344
|
+
this.done = new Promise((resolve2) => {
|
|
2345
|
+
this.resolveDone = resolve2;
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
abort = new AbortController();
|
|
2349
|
+
backoff;
|
|
2350
|
+
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
2351
|
+
done;
|
|
2352
|
+
resolveDone;
|
|
2353
|
+
async run() {
|
|
2134
2354
|
try {
|
|
2135
|
-
const
|
|
2136
|
-
if (!
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2355
|
+
const run = await this.createRun();
|
|
2356
|
+
if (!run) return;
|
|
2357
|
+
this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
|
|
2358
|
+
await executeRun({
|
|
2359
|
+
run,
|
|
2360
|
+
task: this.task,
|
|
2361
|
+
api: this.deps.api,
|
|
2362
|
+
client: this.deps.client,
|
|
2363
|
+
logger: this.deps.logger,
|
|
2364
|
+
tracker: this.deps.tracker
|
|
2365
|
+
});
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
|
|
2368
|
+
} finally {
|
|
2369
|
+
this.resolveDone();
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
|
|
2373
|
+
stop() {
|
|
2374
|
+
this.abort.abort();
|
|
2375
|
+
}
|
|
2376
|
+
async createRun() {
|
|
2377
|
+
for (; ; ) {
|
|
2378
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2154
2379
|
try {
|
|
2155
|
-
await
|
|
2156
|
-
{ runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
|
|
2157
|
-
() => this.invokeWithTimeout(handler, run, reporter)
|
|
2158
|
-
);
|
|
2159
|
-
await reporter.finalizeAttachments();
|
|
2160
|
-
await reporter.finalizeItems();
|
|
2161
|
-
tx.setStatus("ok");
|
|
2162
|
-
tx.finish();
|
|
2163
|
-
reporter.reportSuccess();
|
|
2164
|
-
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
2380
|
+
return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
|
|
2165
2381
|
} catch (error) {
|
|
2166
|
-
|
|
2167
|
-
await
|
|
2168
|
-
tx.setStatus("error");
|
|
2169
|
-
tx.finish();
|
|
2170
|
-
reporter.reportFailure(toRunError(error));
|
|
2171
|
-
this.logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
2382
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2383
|
+
await abortableSleep(this.backoff.next(error), this.abort.signal);
|
|
2172
2384
|
}
|
|
2173
|
-
await reporter.finalize();
|
|
2174
|
-
} finally {
|
|
2175
|
-
this.reporters.delete(reporter);
|
|
2176
2385
|
}
|
|
2177
2386
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/tasks/registry.ts
|
|
2390
|
+
var TaskRegistry = class {
|
|
2391
|
+
tasks = /* @__PURE__ */ new Map();
|
|
2392
|
+
add(task) {
|
|
2393
|
+
if (this.tasks.has(task.slug)) {
|
|
2394
|
+
throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
|
|
2183
2395
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2396
|
+
this.tasks.set(task.slug, task);
|
|
2397
|
+
}
|
|
2398
|
+
get(slug) {
|
|
2399
|
+
return this.tasks.get(slug);
|
|
2400
|
+
}
|
|
2401
|
+
get size() {
|
|
2402
|
+
return this.tasks.size;
|
|
2403
|
+
}
|
|
2404
|
+
/** Tasks que chegam pelo poll (register). */
|
|
2405
|
+
remoteTasks() {
|
|
2406
|
+
return [...this.tasks.values()].filter((task) => task.mode === "remote");
|
|
2407
|
+
}
|
|
2408
|
+
/** Tasks abertas pelo próprio bot (start). */
|
|
2409
|
+
localTasks() {
|
|
2410
|
+
return [...this.tasks.values()].filter((task) => task.mode === "local");
|
|
2411
|
+
}
|
|
2412
|
+
/** Todos os slugs, para a presença da instância (`instance.tasks`). */
|
|
2413
|
+
allSlugs() {
|
|
2414
|
+
return [...this.tasks.keys()];
|
|
2415
|
+
}
|
|
2416
|
+
/** Colunas de itens declaradas no código, por slug (`instance.task_items`); undefined se nenhuma task declarou. */
|
|
2417
|
+
itemDeclarations() {
|
|
2418
|
+
let result;
|
|
2419
|
+
for (const task of this.tasks.values()) {
|
|
2420
|
+
if (!task.options.itemColumns) continue;
|
|
2421
|
+
(result ??= {})[task.slug] = { attributes: task.options.itemColumns };
|
|
2186
2422
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
clear() {
|
|
2426
|
+
this.tasks.clear();
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
var RunTracker = class {
|
|
2430
|
+
constructor(api) {
|
|
2431
|
+
this.api = api;
|
|
2432
|
+
}
|
|
2433
|
+
reporters = /* @__PURE__ */ new Set();
|
|
2434
|
+
waiters = /* @__PURE__ */ new Set();
|
|
2435
|
+
add(reporter) {
|
|
2436
|
+
this.reporters.add(reporter);
|
|
2437
|
+
}
|
|
2438
|
+
remove(reporter) {
|
|
2439
|
+
this.reporters.delete(reporter);
|
|
2440
|
+
if (this.reporters.size === 0) {
|
|
2441
|
+
for (const resolve2 of this.waiters) resolve2();
|
|
2442
|
+
this.waiters.clear();
|
|
2190
2443
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2444
|
+
}
|
|
2445
|
+
get activeCount() {
|
|
2446
|
+
return this.reporters.size;
|
|
2447
|
+
}
|
|
2448
|
+
/** Runs ativas de uma task (concorrência por task no poll). */
|
|
2449
|
+
activeFor(slug) {
|
|
2450
|
+
let count = 0;
|
|
2451
|
+
for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
|
|
2452
|
+
return count;
|
|
2453
|
+
}
|
|
2454
|
+
/** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
|
|
2455
|
+
singleActive() {
|
|
2456
|
+
if (this.reporters.size !== 1) return void 0;
|
|
2457
|
+
return this.reporters.values().next().value;
|
|
2458
|
+
}
|
|
2459
|
+
/** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
|
|
2460
|
+
async waitIdle(timeoutMs) {
|
|
2461
|
+
if (this.reporters.size === 0) return true;
|
|
2462
|
+
const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
|
|
2463
|
+
if (timeoutMs <= 0) return false;
|
|
2464
|
+
return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
2468
|
+
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
2469
|
+
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
2470
|
+
* o sweep server-side é o backstop.
|
|
2471
|
+
*/
|
|
2472
|
+
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
2473
|
+
const pending = [];
|
|
2474
|
+
for (const reporter of this.reporters) {
|
|
2475
|
+
if (reporter.isDead) continue;
|
|
2476
|
+
const logs = reporter.drainLogs();
|
|
2477
|
+
reporter.kill();
|
|
2478
|
+
pending.push(
|
|
2479
|
+
this.api.updateRunOnce(
|
|
2480
|
+
reporter.context.runId,
|
|
2481
|
+
{ status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
|
|
2482
|
+
timeoutMs
|
|
2483
|
+
).catch(() => {
|
|
2484
|
+
})
|
|
2196
2485
|
);
|
|
2197
|
-
return;
|
|
2198
2486
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
invocation.then(() => this.logger.warn(`run ${run.id} terminou depois do timeout de ${timeoutMs}ms`)).catch(() => this.logger.warn(`run ${run.id} falhou depois do timeout de ${timeoutMs}ms (erro descartado)`));
|
|
2202
|
-
throw Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
2203
|
-
name: "TaskTimeout"
|
|
2204
|
-
});
|
|
2487
|
+
if (pending.length === 0) return;
|
|
2488
|
+
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
2205
2489
|
}
|
|
2206
2490
|
};
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2491
|
+
|
|
2492
|
+
// src/tasks/shutdown.ts
|
|
2493
|
+
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
2494
|
+
function installShutdownHandlers(agent) {
|
|
2495
|
+
if (!isNode()) return () => {
|
|
2496
|
+
};
|
|
2497
|
+
let signaled = false;
|
|
2498
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
2499
|
+
for (const signal of WATCHED_SIGNALS) {
|
|
2500
|
+
const handler = markInternal(() => {
|
|
2501
|
+
if (externalListenerCount(signal) > 0) return;
|
|
2502
|
+
if (signaled) return;
|
|
2503
|
+
signaled = true;
|
|
2504
|
+
const reraise = () => {
|
|
2505
|
+
process.removeListener(signal, handler);
|
|
2506
|
+
process.kill(process.pid, signal);
|
|
2507
|
+
};
|
|
2508
|
+
if (agent.activeCount === 0) {
|
|
2509
|
+
reraise();
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
2513
|
+
addEmergencyTask(report);
|
|
2514
|
+
void report.finally(reraise);
|
|
2515
|
+
});
|
|
2516
|
+
process.on(signal, handler);
|
|
2517
|
+
signalHandlers.set(signal, handler);
|
|
2518
|
+
}
|
|
2519
|
+
const onFatal = markInternal((error) => {
|
|
2520
|
+
const report = agent.activeCount > 0 ? agent.reportAgentTerminated("o processo do agente morreu com um erro n\xE3o tratado durante a execu\xE7\xE3o") : Promise.resolve();
|
|
2521
|
+
addEmergencyTask(report);
|
|
2522
|
+
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
2523
|
+
void report.finally(() => {
|
|
2524
|
+
console.error(error);
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
});
|
|
2214
2527
|
}
|
|
2215
|
-
signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
|
|
2216
2528
|
});
|
|
2529
|
+
process.on("uncaughtException", onFatal);
|
|
2530
|
+
return () => {
|
|
2531
|
+
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
2532
|
+
process.removeListener("uncaughtException", onFatal);
|
|
2533
|
+
};
|
|
2217
2534
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2535
|
+
|
|
2536
|
+
// src/runtime.ts
|
|
2537
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2538
|
+
var DEFAULT_RETRIES2 = 1;
|
|
2539
|
+
var DEFAULT_RETRY_DELAY_MS2 = 5e3;
|
|
2540
|
+
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
2541
|
+
var MAX_CONCURRENCY = 5;
|
|
2542
|
+
var MAX_SLUG_CHARS = 255;
|
|
2543
|
+
var PROCESS_LEVEL_KEYS = [
|
|
2544
|
+
"clientSecret",
|
|
2545
|
+
"instanceId",
|
|
2546
|
+
"environment",
|
|
2547
|
+
"release",
|
|
2548
|
+
"tags",
|
|
2549
|
+
"debug",
|
|
2550
|
+
"machineMetrics",
|
|
2551
|
+
"captureUnhandled",
|
|
2552
|
+
"installExitHandlers"
|
|
2553
|
+
];
|
|
2554
|
+
var runtime;
|
|
2555
|
+
var warnedNoRuntime = false;
|
|
2556
|
+
function hostname() {
|
|
2557
|
+
if (!isNode()) return void 0;
|
|
2558
|
+
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
2559
|
+
}
|
|
2560
|
+
function sameValue(a, b) {
|
|
2561
|
+
if (a === b) return true;
|
|
2562
|
+
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
function ensureRuntime(options) {
|
|
2566
|
+
if (runtime) {
|
|
2567
|
+
for (const key of PROCESS_LEVEL_KEYS) {
|
|
2568
|
+
if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
|
|
2569
|
+
runtime.logger.warn(
|
|
2570
|
+
`op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return runtime;
|
|
2575
|
+
}
|
|
2576
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
2577
|
+
const logger = makeLogger(options.debug ?? false);
|
|
2578
|
+
const environment = resolveEnvironmentOption(options.environment);
|
|
2579
|
+
const client = new Client(
|
|
2580
|
+
{ environment, release: options.release, tags: options.tags, debug: options.debug },
|
|
2581
|
+
config
|
|
2582
|
+
);
|
|
2583
|
+
const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
|
|
2584
|
+
runtime = {
|
|
2585
|
+
options: { ...options, clientSecret: void 0 },
|
|
2586
|
+
config,
|
|
2587
|
+
client,
|
|
2588
|
+
logger,
|
|
2589
|
+
api,
|
|
2590
|
+
registry: new TaskRegistry(),
|
|
2591
|
+
tracker: new RunTracker(api),
|
|
2592
|
+
sampler: void 0,
|
|
2593
|
+
heartbeat: void 0,
|
|
2594
|
+
agent: void 0,
|
|
2595
|
+
locals: /* @__PURE__ */ new Map(),
|
|
2596
|
+
instanceId: resolveInstanceId(options.instanceId),
|
|
2597
|
+
environment,
|
|
2598
|
+
pollIntervalMs: void 0,
|
|
2599
|
+
booted: false,
|
|
2600
|
+
bootScheduled: false,
|
|
2601
|
+
bootPromise: void 0,
|
|
2602
|
+
uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
|
|
2603
|
+
uninstallShutdown: void 0
|
|
2604
|
+
};
|
|
2605
|
+
runtime.options.clientSecret = options.clientSecret;
|
|
2606
|
+
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
2607
|
+
return runtime;
|
|
2608
|
+
}
|
|
2609
|
+
function validateTask(slug, handler, fn) {
|
|
2610
|
+
if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
|
|
2611
|
+
throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
|
|
2227
2612
|
}
|
|
2228
|
-
|
|
2613
|
+
if (typeof handler !== "function") {
|
|
2614
|
+
throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
|
|
2615
|
+
}
|
|
2616
|
+
return slug.trim();
|
|
2229
2617
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
)
|
|
2618
|
+
function clampConcurrency(value) {
|
|
2619
|
+
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
2620
|
+
return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
|
|
2621
|
+
}
|
|
2622
|
+
function resolveTaskOptions(slug, options, mode, logger) {
|
|
2623
|
+
const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
|
|
2624
|
+
return {
|
|
2625
|
+
retries,
|
|
2626
|
+
retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
|
|
2627
|
+
timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
|
|
2628
|
+
concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
|
|
2629
|
+
logFlushIntervalMs: options.logFlushIntervalMs,
|
|
2630
|
+
cancelCheckIntervalMs: options.cancelCheckIntervalMs,
|
|
2631
|
+
cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS,
|
|
2632
|
+
itemColumns: normalizeItemColumns(slug, options.items, logger)
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function instanceInfo(rt) {
|
|
2636
|
+
return {
|
|
2637
|
+
instance_id: rt.instanceId ?? "unknown",
|
|
2638
|
+
version: SDK_VERSION,
|
|
2639
|
+
host: hostname(),
|
|
2640
|
+
// ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
|
|
2641
|
+
environment: rt.environment,
|
|
2642
|
+
// TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
|
|
2643
|
+
tasks: rt.registry.allSlugs(),
|
|
2644
|
+
// colunas da aba "itens" declaradas no código — a plataforma usa enquanto o usuário não personalizar
|
|
2645
|
+
task_items: rt.registry.itemDeclarations(),
|
|
2646
|
+
// o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
|
|
2647
|
+
// poll; clampado à faixa aceita pelo schema (1s..15min)
|
|
2648
|
+
poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
|
|
2649
|
+
// recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
|
|
2650
|
+
machine: rt.sampler?.sample()
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
function ensureAgent(rt) {
|
|
2654
|
+
rt.agent ??= new TaskAgent({
|
|
2655
|
+
api: rt.api,
|
|
2656
|
+
client: rt.client,
|
|
2657
|
+
logger: rt.logger,
|
|
2658
|
+
registry: rt.registry,
|
|
2659
|
+
tracker: rt.tracker,
|
|
2660
|
+
instanceInfo: () => instanceInfo(rt),
|
|
2661
|
+
pollIntervalMs: rt.pollIntervalMs
|
|
2662
|
+
});
|
|
2663
|
+
return rt.agent;
|
|
2664
|
+
}
|
|
2665
|
+
function launchLocal(rt, task) {
|
|
2666
|
+
const runner = new LocalRunner(task, {
|
|
2667
|
+
api: rt.api,
|
|
2668
|
+
client: rt.client,
|
|
2669
|
+
logger: rt.logger,
|
|
2670
|
+
tracker: rt.tracker,
|
|
2671
|
+
instanceInfo: () => instanceInfo(rt)
|
|
2672
|
+
});
|
|
2673
|
+
rt.locals.set(task.slug, runner);
|
|
2674
|
+
void runner.run();
|
|
2675
|
+
}
|
|
2676
|
+
async function boot(rt) {
|
|
2677
|
+
if (!rt.instanceId) {
|
|
2678
|
+
const id = await fallbackInstanceId();
|
|
2679
|
+
if (runtime !== rt) return;
|
|
2680
|
+
rt.instanceId = id;
|
|
2681
|
+
rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
|
|
2682
|
+
}
|
|
2683
|
+
if (runtime !== rt) return;
|
|
2684
|
+
rt.booted = true;
|
|
2685
|
+
if (rt.options.machineMetrics !== false) {
|
|
2686
|
+
rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
|
|
2687
|
+
void rt.sampler.start();
|
|
2688
|
+
rt.client.attachMachineSampler(rt.sampler);
|
|
2689
|
+
}
|
|
2690
|
+
rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
|
|
2691
|
+
rt.heartbeat.start();
|
|
2692
|
+
if (rt.options.installExitHandlers !== false) {
|
|
2693
|
+
rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
|
|
2694
|
+
}
|
|
2695
|
+
if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
|
|
2696
|
+
for (const task of rt.registry.localTasks()) launchLocal(rt, task);
|
|
2697
|
+
}
|
|
2698
|
+
function scheduleBoot(rt) {
|
|
2699
|
+
if (rt.bootScheduled) return;
|
|
2700
|
+
rt.bootScheduled = true;
|
|
2701
|
+
queueMicrotask(() => {
|
|
2702
|
+
rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
function addTask(fn, mode, task, handler, options) {
|
|
2706
|
+
const slug = validateTask(task, handler, fn);
|
|
2707
|
+
const rt = ensureRuntime(options);
|
|
2708
|
+
if (mode === "remote" && options.pollIntervalMs !== void 0) {
|
|
2709
|
+
if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
|
|
2710
|
+
else if (rt.pollIntervalMs !== options.pollIntervalMs) {
|
|
2711
|
+
rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
|
|
2712
|
+
}
|
|
2273
2713
|
}
|
|
2274
|
-
|
|
2714
|
+
const registered = { slug, mode, handler, options: resolveTaskOptions(slug, options, mode, rt.logger) };
|
|
2715
|
+
rt.registry.add(registered);
|
|
2716
|
+
if (!rt.booted) {
|
|
2717
|
+
scheduleBoot(rt);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (mode === "remote") {
|
|
2721
|
+
const agent = ensureAgent(rt);
|
|
2722
|
+
agent.start();
|
|
2723
|
+
agent.wake();
|
|
2724
|
+
} else {
|
|
2725
|
+
launchLocal(rt, registered);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
function register(task, handler, options = {}) {
|
|
2729
|
+
addTask("register", "remote", task, handler, options);
|
|
2730
|
+
}
|
|
2731
|
+
function start(task, handler, options = {}) {
|
|
2732
|
+
addTask("start", "local", task, handler, options);
|
|
2275
2733
|
}
|
|
2276
|
-
function
|
|
2277
|
-
return
|
|
2734
|
+
function currentReporter() {
|
|
2735
|
+
return currentRunReporter() ?? runtime?.tracker.singleActive();
|
|
2736
|
+
}
|
|
2737
|
+
function activeLogger() {
|
|
2738
|
+
return runtime?.logger;
|
|
2278
2739
|
}
|
|
2279
2740
|
function toRunLogLevel(type) {
|
|
2280
2741
|
return type === "fatal" ? "error" : type;
|
|
2281
2742
|
}
|
|
2282
|
-
function forwardAttachment(attachment, message, type) {
|
|
2283
|
-
const
|
|
2284
|
-
if (!
|
|
2285
|
-
|
|
2743
|
+
function forwardAttachment(rt, attachment, message, type) {
|
|
2744
|
+
const reporter = currentReporter();
|
|
2745
|
+
if (!reporter) {
|
|
2746
|
+
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
|
|
2286
2747
|
return;
|
|
2287
2748
|
}
|
|
2288
2749
|
const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
|
|
2289
|
-
void attach(withMeta.image, {
|
|
2750
|
+
void reporter.attach(withMeta.image, {
|
|
2290
2751
|
message,
|
|
2291
2752
|
level: toRunLogLevel(type),
|
|
2292
2753
|
caption: withMeta.caption,
|
|
@@ -2294,109 +2755,124 @@ function forwardAttachment(attachment, message, type) {
|
|
|
2294
2755
|
});
|
|
2295
2756
|
}
|
|
2296
2757
|
function captureException(error, hint) {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2758
|
+
const rt = runtime;
|
|
2759
|
+
if (!rt) {
|
|
2760
|
+
if (!warnedNoRuntime) {
|
|
2761
|
+
warnedNoRuntime = true;
|
|
2762
|
+
console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
|
|
2763
|
+
}
|
|
2764
|
+
return void 0;
|
|
2300
2765
|
}
|
|
2301
|
-
return client?.captureException(error, hint, scope);
|
|
2302
|
-
}
|
|
2303
|
-
function captureMessage(message, type = "warning", hint) {
|
|
2304
2766
|
if (hint?.attachment) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
function withScope(callback) {
|
|
2328
|
-
const previous = scope;
|
|
2329
|
-
scope = previous.clone();
|
|
2330
|
-
try {
|
|
2331
|
-
return callback(scope);
|
|
2332
|
-
} finally {
|
|
2333
|
-
scope = previous;
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
function startTransaction(name, op) {
|
|
2337
|
-
const active = currentRunTransaction();
|
|
2338
|
-
if (active) {
|
|
2339
|
-
client?.logger.debug(`startTransaction('${name}') dentro de uma execu\xE7\xE3o \u2014 reutilizando a transaction da run`);
|
|
2340
|
-
return active;
|
|
2341
|
-
}
|
|
2342
|
-
const rate = client?.options.tracesSampleRate ?? 1;
|
|
2343
|
-
const sampled = rate >= 1 || Math.random() < rate;
|
|
2344
|
-
return new Transaction(client, name, op, sampled);
|
|
2345
|
-
}
|
|
2346
|
-
function flush(timeoutMs) {
|
|
2347
|
-
return client ? client.flush(timeoutMs) : Promise.resolve(true);
|
|
2348
|
-
}
|
|
2349
|
-
function register(task, handler) {
|
|
2350
|
-
if (registry.has(task)) {
|
|
2351
|
-
(agent?.logger ?? client?.logger)?.warn(`register('${task}') sobrescreveu um handler existente`);
|
|
2352
|
-
}
|
|
2353
|
-
if (client && !agent && !warnedTasksDisabled) {
|
|
2354
|
-
warnedTasksDisabled = true;
|
|
2355
|
-
client.logger.warn(
|
|
2356
|
-
`register('${task}') com tasks desabilitadas \u2014 passe tasks: true (ou op\xE7\xF5es) no init()`
|
|
2357
|
-
);
|
|
2358
|
-
}
|
|
2359
|
-
registry.set(task, handler);
|
|
2360
|
-
}
|
|
2361
|
-
function start() {
|
|
2362
|
-
if (!agent) {
|
|
2363
|
-
throw new Error("[hobots] tasks n\xE3o configuradas \u2014 passe tasks: true (ou op\xE7\xF5es) no init() antes de start()");
|
|
2364
|
-
}
|
|
2365
|
-
agent.start();
|
|
2366
|
-
}
|
|
2367
|
-
function stop(timeoutMs) {
|
|
2368
|
-
return agent ? agent.stop(timeoutMs) : Promise.resolve(true);
|
|
2767
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2768
|
+
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
2769
|
+
}
|
|
2770
|
+
return rt.client.captureException(error, hint);
|
|
2771
|
+
}
|
|
2772
|
+
async function close(timeoutMs = 3e4) {
|
|
2773
|
+
const rt = runtime;
|
|
2774
|
+
if (!rt) return true;
|
|
2775
|
+
runtime = void 0;
|
|
2776
|
+
await rt.bootPromise;
|
|
2777
|
+
if (rt.agent) await rt.agent.stop(0);
|
|
2778
|
+
for (const local of rt.locals.values()) local.stop();
|
|
2779
|
+
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
2780
|
+
if (!idle) {
|
|
2781
|
+
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
2782
|
+
await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
2783
|
+
}
|
|
2784
|
+
rt.heartbeat?.stop();
|
|
2785
|
+
rt.uninstallShutdown?.();
|
|
2786
|
+
rt.uninstallGlobalHandlers?.();
|
|
2787
|
+
const flushed = await rt.client.close(timeoutMs);
|
|
2788
|
+
return idle && flushed;
|
|
2369
2789
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2790
|
+
|
|
2791
|
+
// src/hobots.ts
|
|
2792
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2793
|
+
function warnOutsideRun(member) {
|
|
2794
|
+
if (warned.has(member)) return;
|
|
2795
|
+
warned.add(member);
|
|
2796
|
+
const message = `hobots.${member} usado fora de uma execu\xE7\xE3o de task \u2014 ignorado (registre a task com register()/start() e use dentro do handler)`;
|
|
2797
|
+
const logger = activeLogger();
|
|
2798
|
+
if (logger) logger.warn(message);
|
|
2799
|
+
else console.warn(`[hobots] ${message}`);
|
|
2800
|
+
}
|
|
2801
|
+
var FALLBACK_TASK = "hobots";
|
|
2802
|
+
var fallbackLog = {
|
|
2803
|
+
debug: (message, ...rest) => printRunLog(FALLBACK_TASK, "debug", joinLogArgs(message, rest)),
|
|
2804
|
+
info: (message, ...rest) => printRunLog(FALLBACK_TASK, "info", joinLogArgs(message, rest)),
|
|
2805
|
+
warn: (message, ...rest) => printRunLog(FALLBACK_TASK, "warning", joinLogArgs(message, rest)),
|
|
2806
|
+
error: (message, ...rest) => printRunLog(FALLBACK_TASK, "error", joinLogArgs(message, rest)),
|
|
2807
|
+
success: (message, ...rest) => printRunLog(FALLBACK_TASK, "success", joinLogArgs(message, rest))
|
|
2808
|
+
};
|
|
2809
|
+
var noop = () => void 0;
|
|
2810
|
+
var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
|
|
2811
|
+
var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
|
|
2812
|
+
var neverAborts = new AbortController().signal;
|
|
2813
|
+
var detachedTransaction;
|
|
2814
|
+
function resolve(member) {
|
|
2815
|
+
const reporter = currentReporter();
|
|
2816
|
+
if (reporter) return reporter.context;
|
|
2817
|
+
warnOutsideRun(member);
|
|
2818
|
+
return void 0;
|
|
2376
2819
|
}
|
|
2820
|
+
function getter(member, fallback) {
|
|
2821
|
+
return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
|
|
2822
|
+
}
|
|
2823
|
+
function method(value) {
|
|
2824
|
+
return { enumerable: true, writable: false, value };
|
|
2825
|
+
}
|
|
2826
|
+
var hobots = Object.defineProperties({}, {
|
|
2827
|
+
// ── contexto da run atual (getters) ─────────────────────────────────────────
|
|
2828
|
+
runId: getter("runId", () => ""),
|
|
2829
|
+
task: getter("task", () => ""),
|
|
2830
|
+
params: getter("params", () => ({})),
|
|
2831
|
+
log: getter("log", () => fallbackLog),
|
|
2832
|
+
items: getter("items", () => fallbackItems),
|
|
2833
|
+
progress: getter("progress", () => fallbackProgress),
|
|
2834
|
+
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
2835
|
+
signal: getter("signal", () => neverAborts),
|
|
2836
|
+
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
2837
|
+
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
2838
|
+
attach: method(
|
|
2839
|
+
(image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
|
|
2840
|
+
),
|
|
2841
|
+
retry: method((fn, options) => {
|
|
2842
|
+
const reporter = currentReporter();
|
|
2843
|
+
return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
|
|
2844
|
+
}),
|
|
2845
|
+
// ── processo ────────────────────────────────────────────────────────────────
|
|
2846
|
+
register: method((task, handler, options) => {
|
|
2847
|
+
register(task, handler, options);
|
|
2848
|
+
installGlobalHobots();
|
|
2849
|
+
}),
|
|
2850
|
+
start: method((task, handler, options) => {
|
|
2851
|
+
start(task, handler, options);
|
|
2852
|
+
installGlobalHobots();
|
|
2853
|
+
}),
|
|
2854
|
+
close: method((timeoutMs) => close(timeoutMs)),
|
|
2855
|
+
captureException: method((error, hint) => captureException(error, hint))
|
|
2856
|
+
});
|
|
2857
|
+
function installGlobalHobots() {
|
|
2858
|
+
if (globalThis.hobots === hobots) return;
|
|
2859
|
+
Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
|
|
2860
|
+
}
|
|
2861
|
+
var register2 = hobots.register;
|
|
2862
|
+
var start2 = hobots.start;
|
|
2863
|
+
var close2 = hobots.close;
|
|
2864
|
+
var captureException2 = hobots.captureException;
|
|
2865
|
+
var retry = hobots.retry;
|
|
2866
|
+
|
|
2867
|
+
// src/index.ts
|
|
2868
|
+
var src_default = hobots;
|
|
2377
2869
|
export {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
captureMessage,
|
|
2386
|
-
close,
|
|
2387
|
-
flush,
|
|
2388
|
-
getClient,
|
|
2389
|
-
init,
|
|
2390
|
-
register,
|
|
2391
|
-
scrubSecrets,
|
|
2392
|
-
setContext,
|
|
2393
|
-
setExtra,
|
|
2394
|
-
setTag,
|
|
2395
|
-
setTags,
|
|
2396
|
-
setUser,
|
|
2397
|
-
start,
|
|
2398
|
-
startTransaction,
|
|
2399
|
-
stop,
|
|
2400
|
-
withScope
|
|
2870
|
+
captureException2 as captureException,
|
|
2871
|
+
close2 as close,
|
|
2872
|
+
src_default as default,
|
|
2873
|
+
hobots,
|
|
2874
|
+
register2 as register,
|
|
2875
|
+
retry,
|
|
2876
|
+
start2 as start
|
|
2401
2877
|
};
|
|
2402
2878
|
//# sourceMappingURL=index.js.map
|