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