hobots 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -369
- package/dist/index.cjs +1217 -852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -214
- package/dist/index.d.ts +218 -214
- package/dist/index.js +1217 -852
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +15 -2
- package/dist/protocol.d.ts +15 -2
- package/docs/README.md +16 -33
- package/docs/api-reference.md +74 -213
- package/docs/concepts.md +60 -393
- package/docs/getting-started.md +38 -84
- package/docs/protocol.md +28 -19
- package/docs/tasks.md +92 -251
- package/package.json +2 -2
- package/docs/telemetry.md +0 -162
package/dist/index.cjs
CHANGED
|
@@ -30,83 +30,16 @@ 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;
|
|
112
45
|
var FRAME_RE = /^\s*at\s+(?:async\s+)?(?:(.+?)\s+\()?(.+?)\)?$/;
|
|
@@ -161,7 +94,7 @@ function scrubSecrets(text) {
|
|
|
161
94
|
|
|
162
95
|
// src/utils.ts
|
|
163
96
|
var SDK_NAME = "hobots";
|
|
164
|
-
var SDK_VERSION = true ? "
|
|
97
|
+
var SDK_VERSION = true ? "3.0.0" : "0.0.0-dev";
|
|
165
98
|
function isNode() {
|
|
166
99
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
167
100
|
}
|
|
@@ -189,11 +122,28 @@ function unrefTimer(timer) {
|
|
|
189
122
|
timer?.unref?.();
|
|
190
123
|
}
|
|
191
124
|
function sleep(ms) {
|
|
192
|
-
return new Promise((
|
|
193
|
-
const timer = setTimeout(
|
|
125
|
+
return new Promise((resolve2) => {
|
|
126
|
+
const timer = setTimeout(resolve2, ms);
|
|
194
127
|
unrefTimer(timer);
|
|
195
128
|
});
|
|
196
129
|
}
|
|
130
|
+
function abortableSleep(ms, signal) {
|
|
131
|
+
return new Promise((resolve2) => {
|
|
132
|
+
if (signal?.aborted) {
|
|
133
|
+
resolve2();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolve2();
|
|
139
|
+
};
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
resolve2();
|
|
143
|
+
}, ms);
|
|
144
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
197
147
|
function jitter(baseMs) {
|
|
198
148
|
return Math.floor(baseMs / 2 + Math.random() * (baseMs / 2));
|
|
199
149
|
}
|
|
@@ -314,14 +264,6 @@ function eventFromException(error, type = "error") {
|
|
|
314
264
|
}
|
|
315
265
|
};
|
|
316
266
|
}
|
|
317
|
-
function eventFromMessage(message, type = "warning") {
|
|
318
|
-
return {
|
|
319
|
-
event_id: uuid4(),
|
|
320
|
-
timestamp: nowISO(),
|
|
321
|
-
type,
|
|
322
|
-
message: scrubSecrets(message)
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
267
|
|
|
326
268
|
// src/transport.ts
|
|
327
269
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
@@ -455,7 +397,7 @@ var Client = class {
|
|
|
455
397
|
#config;
|
|
456
398
|
transport;
|
|
457
399
|
machine;
|
|
458
|
-
constructor(options, config
|
|
400
|
+
constructor(options, config) {
|
|
459
401
|
this.#options = options;
|
|
460
402
|
this.debug = !!options.debug;
|
|
461
403
|
this.logger = makeLogger(this.debug);
|
|
@@ -485,43 +427,28 @@ var Client = class {
|
|
|
485
427
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
486
428
|
return `Client { baseUrl: '${this.#config.baseUrl}' }`;
|
|
487
429
|
}
|
|
488
|
-
captureException(error, hint
|
|
489
|
-
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint
|
|
430
|
+
captureException(error, hint) {
|
|
431
|
+
return this.captureEvent(eventFromException(error, hint?.type ?? "error"), hint);
|
|
490
432
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
captureEvent(event, hint, scope2) {
|
|
495
|
-
const sampleRate = this.options.sampleRate ?? 1;
|
|
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
|
-
}
|
|
433
|
+
captureEvent(event, hint) {
|
|
434
|
+
const final = this.prepareEvent(event, hint);
|
|
506
435
|
this.transport.enqueue({ type: "event", payload: final });
|
|
507
436
|
return final.event_id;
|
|
508
437
|
}
|
|
509
|
-
prepareEvent(event, hint
|
|
510
|
-
const tags = { ...this.options.tags, ...
|
|
511
|
-
const extra = { ...
|
|
438
|
+
prepareEvent(event, hint) {
|
|
439
|
+
const tags = { ...this.options.tags, ...event.tags, ...hint?.tags };
|
|
440
|
+
const extra = { ...event.extra, ...hint?.extra };
|
|
512
441
|
const sampled = this.machine?.sample();
|
|
513
442
|
const machine = sampled ? { machine: sampled } : {};
|
|
514
443
|
return {
|
|
515
444
|
...event,
|
|
516
|
-
type: hint?.type ?? event.type ??
|
|
445
|
+
type: hint?.type ?? event.type ?? "error",
|
|
517
446
|
release: event.release ?? this.options.release,
|
|
518
447
|
environment: this.resolveEnvironment(event.environment),
|
|
519
448
|
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
520
449
|
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
|
|
450
|
+
contexts: { ...runtimeContexts(), ...machine, ...event.contexts },
|
|
451
|
+
fingerprint: hint?.fingerprint ?? event.fingerprint
|
|
525
452
|
};
|
|
526
453
|
}
|
|
527
454
|
sendTransaction(payload) {
|
|
@@ -547,7 +474,7 @@ var Client = class {
|
|
|
547
474
|
});
|
|
548
475
|
}
|
|
549
476
|
/**
|
|
550
|
-
* Ambiente do item: o dele, senão o
|
|
477
|
+
* Ambiente do item: o dele, senão o das opções, senão produção. Centralizado
|
|
551
478
|
* porque evento, transaction e heartbeat precisam responder igual — o servidor
|
|
552
479
|
* normaliza apelidos legados, então aqui basta repassar.
|
|
553
480
|
*/
|
|
@@ -562,23 +489,103 @@ var Client = class {
|
|
|
562
489
|
}
|
|
563
490
|
};
|
|
564
491
|
|
|
492
|
+
// src/config.ts
|
|
493
|
+
var DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
494
|
+
var CLIENT_SECRET_ENV = "HOBOTS_CLIENT_SECRET";
|
|
495
|
+
function envClientSecret() {
|
|
496
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
497
|
+
return process.env[CLIENT_SECRET_ENV]?.trim() || void 0;
|
|
498
|
+
}
|
|
499
|
+
var SECRET_PATTERN = /^hb_[a-f0-9]{64}$/i;
|
|
500
|
+
function envBaseUrl() {
|
|
501
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
502
|
+
return process.env.HOBOTS_API_URL?.trim() || void 0;
|
|
503
|
+
}
|
|
504
|
+
function isLoopbackHost(hostname2) {
|
|
505
|
+
return hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.startsWith("127.") || hostname2 === "[::1]" || hostname2 === "::1" || hostname2 === "0.0.0.0";
|
|
506
|
+
}
|
|
507
|
+
var warnedInsecureBaseUrl = false;
|
|
508
|
+
function validateBaseUrl(raw, prefix) {
|
|
509
|
+
let url;
|
|
510
|
+
try {
|
|
511
|
+
url = new URL(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
throw new Error(`${prefix} HOBOTS_API_URL inv\xE1lida \u2014 esperado URL http(s), ex.: http://localhost:3001`);
|
|
514
|
+
}
|
|
515
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
516
|
+
throw new Error(`${prefix} HOBOTS_API_URL com esquema "${url.protocol}" n\xE3o suportado \u2014 use http(s)`);
|
|
517
|
+
}
|
|
518
|
+
if (url.username || url.password) {
|
|
519
|
+
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`);
|
|
520
|
+
}
|
|
521
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !warnedInsecureBaseUrl) {
|
|
522
|
+
warnedInsecureBaseUrl = true;
|
|
523
|
+
console.warn(
|
|
524
|
+
`${prefix} HOBOTS_API_URL usa http para o host remoto "${url.hostname}" \u2014 o Client Secret trafega sem TLS`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return raw.replace(/\/+$/, "");
|
|
528
|
+
}
|
|
529
|
+
function resolveConfig(options = {}, prefix = "[hobots]") {
|
|
530
|
+
const fromOption = options.clientSecret?.trim();
|
|
531
|
+
const key = fromOption || envClientSecret();
|
|
532
|
+
const origin = fromOption ? "clientSecret" : `env ${CLIENT_SECRET_ENV}`;
|
|
533
|
+
if (!key) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`${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`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (!SECRET_PATTERN.test(key)) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`${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`
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const baseUrl = validateBaseUrl(envBaseUrl() ?? DEFAULT_BASE_URL, prefix);
|
|
544
|
+
return {
|
|
545
|
+
key,
|
|
546
|
+
baseUrl,
|
|
547
|
+
ingestEndpoint: `${baseUrl}/v1/ingest`,
|
|
548
|
+
tasksBaseUrl: `${baseUrl}/v1/tasks`
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/env.ts
|
|
553
|
+
var INSTANCE_ID_ENV = "HOBOTS_INSTANCE_ID";
|
|
554
|
+
function resolveInstanceId(option) {
|
|
555
|
+
const fromOption = option?.trim();
|
|
556
|
+
if (fromOption) return fromOption;
|
|
557
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
558
|
+
return process.env[INSTANCE_ID_ENV]?.trim() || void 0;
|
|
559
|
+
}
|
|
560
|
+
async function fallbackInstanceId() {
|
|
561
|
+
if (!isNode()) return "browser";
|
|
562
|
+
try {
|
|
563
|
+
const os = await import("os");
|
|
564
|
+
return os.hostname() || "unknown-host";
|
|
565
|
+
} catch {
|
|
566
|
+
return "unknown-host";
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
565
570
|
// src/heartbeat.ts
|
|
566
571
|
var DEFAULT_INTERVAL_MS = 3e4;
|
|
567
572
|
var MIN_REPORTED_INTERVAL_MS = 1e3;
|
|
568
573
|
var MAX_REPORTED_INTERVAL_MS = 9e5;
|
|
569
574
|
var Heartbeat = class {
|
|
570
|
-
constructor(
|
|
571
|
-
this.client =
|
|
575
|
+
constructor(client, options = {}, instanceId, sampler) {
|
|
576
|
+
this.client = client;
|
|
572
577
|
this.sampler = sampler;
|
|
573
578
|
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
574
579
|
this.instanceId = instanceId;
|
|
575
580
|
this.name = options.name ?? instanceId;
|
|
576
581
|
this.metadata = options.metadata;
|
|
582
|
+
this.keepAlive = options.keepAlive ?? false;
|
|
577
583
|
}
|
|
578
584
|
intervalMs;
|
|
579
585
|
instanceId;
|
|
580
586
|
name;
|
|
581
587
|
metadata;
|
|
588
|
+
keepAlive;
|
|
582
589
|
timer;
|
|
583
590
|
start() {
|
|
584
591
|
if (this.timer) return;
|
|
@@ -594,7 +601,7 @@ var Heartbeat = class {
|
|
|
594
601
|
},
|
|
595
602
|
this.intervalMs * (0.9 + Math.random() * 0.2)
|
|
596
603
|
);
|
|
597
|
-
unrefTimer(this.timer);
|
|
604
|
+
if (!this.keepAlive) unrefTimer(this.timer);
|
|
598
605
|
}
|
|
599
606
|
stop() {
|
|
600
607
|
if (this.timer) {
|
|
@@ -640,13 +647,13 @@ var installed = false;
|
|
|
640
647
|
function hasInstalledGlobalHandlers() {
|
|
641
648
|
return installed;
|
|
642
649
|
}
|
|
643
|
-
function installGlobalHandlers(
|
|
650
|
+
function installGlobalHandlers(client) {
|
|
644
651
|
if (installed) return () => void 0;
|
|
645
652
|
installed = true;
|
|
646
653
|
if (isNode()) {
|
|
647
654
|
const onUncaught = markInternal((error) => {
|
|
648
|
-
|
|
649
|
-
void
|
|
655
|
+
client.captureException(error, { type: "fatal", tags: { mechanism: "uncaughtException" } });
|
|
656
|
+
void client.flush(2e3).then(async () => {
|
|
650
657
|
if (externalListenerCount("uncaughtException") === 0) {
|
|
651
658
|
await awaitEmergencyTasks(EMERGENCY_REPORT_CAP_MS);
|
|
652
659
|
console.error(error);
|
|
@@ -655,11 +662,11 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
655
662
|
});
|
|
656
663
|
});
|
|
657
664
|
const onRejection = markInternal((reason) => {
|
|
658
|
-
|
|
659
|
-
void
|
|
665
|
+
client.captureException(reason, { type: "error", tags: { mechanism: "unhandledRejection" } });
|
|
666
|
+
void client.flush(2e3);
|
|
660
667
|
});
|
|
661
668
|
const onBeforeExit = markInternal(() => {
|
|
662
|
-
void
|
|
669
|
+
void client.flush(2e3);
|
|
663
670
|
});
|
|
664
671
|
process.on("uncaughtException", onUncaught);
|
|
665
672
|
process.on("unhandledRejection", onRejection);
|
|
@@ -673,13 +680,13 @@ function installGlobalHandlers(client2, getScope) {
|
|
|
673
680
|
}
|
|
674
681
|
if (typeof window !== "undefined") {
|
|
675
682
|
const onError = (event) => {
|
|
676
|
-
|
|
683
|
+
client.captureException(event.error ?? event.message, { tags: { mechanism: "onerror" } });
|
|
677
684
|
};
|
|
678
685
|
const onRejection = (event) => {
|
|
679
|
-
|
|
686
|
+
client.captureException(event.reason, { tags: { mechanism: "onunhandledrejection" } });
|
|
680
687
|
};
|
|
681
688
|
const onPageHide = () => {
|
|
682
|
-
void
|
|
689
|
+
void client.flush(1e3);
|
|
683
690
|
};
|
|
684
691
|
window.addEventListener("error", onError);
|
|
685
692
|
window.addEventListener("unhandledrejection", onRejection);
|
|
@@ -819,349 +826,138 @@ var MachineSampler = class {
|
|
|
819
826
|
}
|
|
820
827
|
};
|
|
821
828
|
|
|
822
|
-
// src/
|
|
823
|
-
var
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.tags[key] = value;
|
|
833
|
-
return this;
|
|
829
|
+
// src/tasks/api.ts
|
|
830
|
+
var POLL_TIMEOUT_MS = 3e4;
|
|
831
|
+
var UPDATE_TIMEOUT_MS = 1e4;
|
|
832
|
+
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
833
|
+
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
834
|
+
var TasksApiError = class extends Error {
|
|
835
|
+
constructor(message, status) {
|
|
836
|
+
super(message);
|
|
837
|
+
this.status = status;
|
|
838
|
+
this.name = "TasksApiError";
|
|
834
839
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
840
|
+
};
|
|
841
|
+
var TasksApi = class {
|
|
842
|
+
baseUrl;
|
|
843
|
+
key;
|
|
844
|
+
logger;
|
|
845
|
+
constructor(options) {
|
|
846
|
+
this.baseUrl = options.baseUrl;
|
|
847
|
+
this.key = options.key;
|
|
848
|
+
this.logger = options.logger;
|
|
838
849
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
850
|
+
headers() {
|
|
851
|
+
return {
|
|
852
|
+
"content-type": "application/json",
|
|
853
|
+
authorization: `Bearer ${this.key}`
|
|
854
|
+
};
|
|
842
855
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
856
|
+
/** Um request só — o loop de polling é quem decide o backoff entre tentativas. */
|
|
857
|
+
async poll(request, signal) {
|
|
858
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(POLL_TIMEOUT_MS, signal);
|
|
859
|
+
let response;
|
|
860
|
+
try {
|
|
861
|
+
response = await fetch(`${this.baseUrl}/poll`, {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: this.headers(),
|
|
864
|
+
body: JSON.stringify(request),
|
|
865
|
+
signal: timeoutSignal
|
|
866
|
+
});
|
|
867
|
+
} finally {
|
|
868
|
+
cleanup();
|
|
848
869
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
this.
|
|
853
|
-
return this;
|
|
854
|
-
}
|
|
855
|
-
setExtras(extras) {
|
|
856
|
-
Object.assign(this.extra, extras);
|
|
857
|
-
return this;
|
|
858
|
-
}
|
|
859
|
-
setType(type) {
|
|
860
|
-
this.type = type;
|
|
861
|
-
return this;
|
|
870
|
+
if (!response.ok) {
|
|
871
|
+
throw new TasksApiError(`poll falhou (HTTP ${response.status})`, response.status);
|
|
872
|
+
}
|
|
873
|
+
return this.parsePollResponse(await response.json(), request.max ?? 1);
|
|
862
874
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
875
|
+
/**
|
|
876
|
+
* Abre uma run por conta própria (modo start). Um request só — como no poll, o
|
|
877
|
+
* ladder de espera entre tentativas é decisão do chamador (LocalRunner).
|
|
878
|
+
* 4xx (401 = chave rejeitada) vira TasksApiError com o status.
|
|
879
|
+
*/
|
|
880
|
+
async createRun(request, signal) {
|
|
881
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
|
|
882
|
+
let response;
|
|
883
|
+
try {
|
|
884
|
+
response = await fetch(`${this.baseUrl}/runs`, {
|
|
885
|
+
method: "POST",
|
|
886
|
+
headers: this.headers(),
|
|
887
|
+
body: JSON.stringify(request),
|
|
888
|
+
signal: timeoutSignal
|
|
889
|
+
});
|
|
890
|
+
} finally {
|
|
891
|
+
cleanup();
|
|
892
|
+
}
|
|
893
|
+
if (!response.ok) {
|
|
894
|
+
throw new TasksApiError(`cria\xE7\xE3o da run falhou (HTTP ${response.status})`, response.status);
|
|
895
|
+
}
|
|
896
|
+
const body = await response.json().catch(() => null);
|
|
897
|
+
if (typeof body?.id !== "string" || typeof body.task !== "string") {
|
|
898
|
+
throw new TasksApiError("resposta inv\xE1lida da cria\xE7\xE3o de run (id/task ausentes)", 0);
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
id: body.id,
|
|
902
|
+
task: body.task,
|
|
903
|
+
params: body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {},
|
|
904
|
+
form: null,
|
|
905
|
+
source: typeof body.source === "string" ? body.source : "agent",
|
|
906
|
+
created_at: typeof body.created_at === "string" ? body.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
907
|
+
};
|
|
866
908
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
909
|
+
/** Nunca confia no shape da resposta: runs malformadas são descartadas e o `max` é respeitado. */
|
|
910
|
+
parsePollResponse(body, max) {
|
|
911
|
+
const runs = body?.runs;
|
|
912
|
+
if (!Array.isArray(runs)) {
|
|
913
|
+
throw new TasksApiError("resposta inv\xE1lida do poll (campo runs ausente)", 0);
|
|
871
914
|
}
|
|
872
|
-
|
|
915
|
+
const valid = [];
|
|
916
|
+
for (const run of runs.slice(0, Math.max(0, max))) {
|
|
917
|
+
if (typeof run?.id !== "string" || typeof run.task !== "string") continue;
|
|
918
|
+
const params = run.params && typeof run.params === "object" && !Array.isArray(run.params) ? run.params : {};
|
|
919
|
+
valid.push({ ...run, id: run.id, task: run.task, params });
|
|
920
|
+
}
|
|
921
|
+
if (valid.length < runs.length) {
|
|
922
|
+
this.logger.warn(`poll retornou ${runs.length - valid.length} run(s) inv\xE1lida(s)/excedente(s) \u2014 descartadas`);
|
|
923
|
+
}
|
|
924
|
+
return { runs: valid };
|
|
873
925
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
926
|
+
/**
|
|
927
|
+
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
928
|
+
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
929
|
+
*/
|
|
930
|
+
async updateRun(runId, update) {
|
|
931
|
+
let response;
|
|
932
|
+
try {
|
|
933
|
+
response = await fetchWithRetry(
|
|
934
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
935
|
+
method: "PATCH",
|
|
936
|
+
headers: this.headers(),
|
|
937
|
+
body: JSON.stringify(update),
|
|
938
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
939
|
+
}),
|
|
940
|
+
{ logger: this.logger, label: `update da run ${runId}` }
|
|
941
|
+
);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
944
|
+
}
|
|
945
|
+
if (!response.ok) {
|
|
946
|
+
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
947
|
+
}
|
|
877
948
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
// src/transaction.ts
|
|
892
|
-
function stepId() {
|
|
893
|
-
return uuid4().slice(0, 16);
|
|
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 };
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* PATCH de status/logs com retry em falha de rede/5xx/429.
|
|
1132
|
-
* 4xx é definitivo e vira TasksApiError imediato (409 = run cancelada/transição inválida).
|
|
1133
|
-
*/
|
|
1134
|
-
async updateRun(runId, update) {
|
|
1135
|
-
let response;
|
|
1136
|
-
try {
|
|
1137
|
-
response = await fetchWithRetry(
|
|
1138
|
-
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
1139
|
-
method: "PATCH",
|
|
1140
|
-
headers: this.headers(),
|
|
1141
|
-
body: JSON.stringify(update),
|
|
1142
|
-
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1143
|
-
}),
|
|
1144
|
-
{ logger: this.logger, label: `update da run ${runId}` }
|
|
1145
|
-
);
|
|
1146
|
-
} catch (error) {
|
|
1147
|
-
throw new Error(`update da run ${runId} descartado ap\xF3s retries: ${String(error)}`);
|
|
1148
|
-
}
|
|
1149
|
-
if (!response.ok) {
|
|
1150
|
-
throw new TasksApiError(`update rejeitado (HTTP ${response.status})`, response.status);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
/**
|
|
1154
|
-
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
1155
|
-
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
1156
|
-
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
1157
|
-
*/
|
|
1158
|
-
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
1159
|
-
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
1160
|
-
method: "PATCH",
|
|
1161
|
-
headers: this.headers(),
|
|
1162
|
-
body: JSON.stringify(update),
|
|
1163
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
1164
|
-
});
|
|
949
|
+
/**
|
|
950
|
+
* Um único PATCH com timeout curto — exit path (o processo está morrendo, o retry
|
|
951
|
+
* ladder do updateRun é proibido aqui). Status HTTP é ignorado: 409/404 significa
|
|
952
|
+
* que a run já foi cancelada/finalizada, o que é aceitável nesse caminho.
|
|
953
|
+
*/
|
|
954
|
+
async updateRunOnce(runId, update, timeoutMs = 2e3) {
|
|
955
|
+
await fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
956
|
+
method: "PATCH",
|
|
957
|
+
headers: this.headers(),
|
|
958
|
+
body: JSON.stringify(update),
|
|
959
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
960
|
+
});
|
|
1165
961
|
}
|
|
1166
962
|
/**
|
|
1167
963
|
* Envia um lote de itens de execução. Retry em falha de rede/5xx/429-transporte;
|
|
@@ -1264,13 +1060,83 @@ var TasksApi = class {
|
|
|
1264
1060
|
body: JSON.stringify(request),
|
|
1265
1061
|
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1266
1062
|
}
|
|
1267
|
-
);
|
|
1268
|
-
if (!response.ok) {
|
|
1269
|
-
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1063
|
+
);
|
|
1064
|
+
if (!response.ok) {
|
|
1065
|
+
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// src/tasks/backoff.ts
|
|
1071
|
+
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
1072
|
+
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
1073
|
+
var Backoff = class {
|
|
1074
|
+
constructor(logger, label, floorMs = 0) {
|
|
1075
|
+
this.logger = logger;
|
|
1076
|
+
this.label = label;
|
|
1077
|
+
this.floorMs = floorMs;
|
|
1078
|
+
}
|
|
1079
|
+
failures = 0;
|
|
1080
|
+
authFailures = 0;
|
|
1081
|
+
reset() {
|
|
1082
|
+
this.failures = 0;
|
|
1083
|
+
this.authFailures = 0;
|
|
1084
|
+
}
|
|
1085
|
+
next(error) {
|
|
1086
|
+
if (error instanceof TasksApiError && error.status === 401) {
|
|
1087
|
+
this.authFailures++;
|
|
1088
|
+
if (this.authFailures === 1 || this.authFailures % 10 === 0) {
|
|
1089
|
+
this.logger.error("chave rejeitada (401) \u2014 verifique o Client Secret do agente em Configura\xE7\xF5es \u2192 Agentes");
|
|
1090
|
+
}
|
|
1091
|
+
return Math.max(this.floorMs, AUTH_FAILURE_INTERVAL_MS);
|
|
1270
1092
|
}
|
|
1093
|
+
this.failures++;
|
|
1094
|
+
const ladder = RETRY_DELAYS_MS[Math.min(this.failures - 1, RETRY_DELAYS_MS.length - 1)];
|
|
1095
|
+
const delay = Math.min(Math.max(jitter(ladder), 0), Math.max(this.floorMs, 16e3));
|
|
1096
|
+
this.logger.warn(`${this.label} falhou (${String(error)}), pr\xF3xima tentativa em ${delay}ms`);
|
|
1097
|
+
return delay;
|
|
1271
1098
|
}
|
|
1272
1099
|
};
|
|
1273
1100
|
|
|
1101
|
+
// src/tasks/errors.ts
|
|
1102
|
+
function runCanceledError() {
|
|
1103
|
+
return Object.assign(new Error("execu\xE7\xE3o cancelada"), { name: "RunCanceled" });
|
|
1104
|
+
}
|
|
1105
|
+
function taskTimeoutError(timeoutMs) {
|
|
1106
|
+
return Object.assign(new Error(`timeout ap\xF3s ${timeoutMs}ms (o handler pode continuar em background)`), {
|
|
1107
|
+
name: "TaskTimeout"
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
var TERMINAL_ERROR_NAMES = /* @__PURE__ */ new Set(["RunCanceled", "AbortError", "TaskTimeout"]);
|
|
1111
|
+
function isTerminalError(error) {
|
|
1112
|
+
return error instanceof Error && TERMINAL_ERROR_NAMES.has(error.name);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/retry.ts
|
|
1116
|
+
var DEFAULT_RETRIES = 3;
|
|
1117
|
+
var DEFAULT_DELAY_MS = 1e3;
|
|
1118
|
+
function resolveDelay(delay, attempt, fallbackMs) {
|
|
1119
|
+
const value = typeof delay === "function" ? delay(attempt) : delay ?? fallbackMs;
|
|
1120
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1121
|
+
}
|
|
1122
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
1123
|
+
const retries = Math.max(0, Math.floor(options.retries ?? DEFAULT_RETRIES));
|
|
1124
|
+
const wait = options.wait ?? sleep;
|
|
1125
|
+
for (let attempt = 1; ; attempt++) {
|
|
1126
|
+
if (options.signal?.aborted) throw runCanceledError();
|
|
1127
|
+
try {
|
|
1128
|
+
return await fn(attempt);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
const exhausted = attempt > retries;
|
|
1131
|
+
const retriable = !isTerminalError(error) && !options.signal?.aborted && (options.shouldRetry?.(error) ?? true);
|
|
1132
|
+
if (exhausted || !retriable) throw error;
|
|
1133
|
+
const delay = resolveDelay(options.delayMs, attempt, DEFAULT_DELAY_MS);
|
|
1134
|
+
options.onRetry?.(error, attempt, delay);
|
|
1135
|
+
await wait(delay);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1274
1140
|
// src/tasks/attachments.ts
|
|
1275
1141
|
var MAX_ATTACHMENT_BYTES = 5242880;
|
|
1276
1142
|
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
@@ -1322,14 +1188,11 @@ var LOG_LEVEL_COLOR = {
|
|
|
1322
1188
|
};
|
|
1323
1189
|
function printRunLog(task, level, message) {
|
|
1324
1190
|
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" });
|
|
1191
|
+
const method2 = level === "warning" ? "warn" : level === "success" ? "info" : level;
|
|
1192
|
+
console[method2](line, message);
|
|
1330
1193
|
}
|
|
1331
1194
|
function cancelableSleep(ms, signal) {
|
|
1332
|
-
return new Promise((
|
|
1195
|
+
return new Promise((resolve2, reject) => {
|
|
1333
1196
|
if (signal.aborted) {
|
|
1334
1197
|
reject(runCanceledError());
|
|
1335
1198
|
return;
|
|
@@ -1340,7 +1203,7 @@ function cancelableSleep(ms, signal) {
|
|
|
1340
1203
|
};
|
|
1341
1204
|
const timer = setTimeout(() => {
|
|
1342
1205
|
signal.removeEventListener("abort", onAbort);
|
|
1343
|
-
|
|
1206
|
+
resolve2();
|
|
1344
1207
|
}, ms);
|
|
1345
1208
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1346
1209
|
});
|
|
@@ -1370,7 +1233,7 @@ function scrubPayloadValue(value, depth) {
|
|
|
1370
1233
|
return out;
|
|
1371
1234
|
}
|
|
1372
1235
|
var RunReporter = class {
|
|
1373
|
-
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs) {
|
|
1236
|
+
constructor(api, runId, task, logger, tx, flushIntervalMs, cancelCheckIntervalMs, params = {}) {
|
|
1374
1237
|
this.api = api;
|
|
1375
1238
|
this.runId = runId;
|
|
1376
1239
|
this.logger = logger;
|
|
@@ -1387,17 +1250,40 @@ var RunReporter = class {
|
|
|
1387
1250
|
success: (message) => this.log("success", message)
|
|
1388
1251
|
};
|
|
1389
1252
|
tx.setLifecycleSink((level, message) => this.push(level, message));
|
|
1253
|
+
const checkCanceled = () => this.checkCanceled();
|
|
1254
|
+
tx.setStepGuard(checkCanceled);
|
|
1390
1255
|
const items = {
|
|
1391
|
-
add: (item) =>
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1256
|
+
add: (item) => {
|
|
1257
|
+
checkCanceled();
|
|
1258
|
+
this.addItem(item.status, item.message, item.payload, item.id);
|
|
1259
|
+
},
|
|
1260
|
+
succeeded: (payload, options) => {
|
|
1261
|
+
checkCanceled();
|
|
1262
|
+
this.addItem("succeeded", void 0, payload, options?.id);
|
|
1263
|
+
},
|
|
1264
|
+
failed: (message, payload, options) => {
|
|
1265
|
+
checkCanceled();
|
|
1266
|
+
this.addItem("failed", message, payload, options?.id);
|
|
1267
|
+
},
|
|
1268
|
+
occurrence: (message, payload, options) => {
|
|
1269
|
+
checkCanceled();
|
|
1270
|
+
this.addItem("occurrence", message, payload, options?.id);
|
|
1271
|
+
}
|
|
1395
1272
|
};
|
|
1396
1273
|
const progress = Object.defineProperties(
|
|
1397
1274
|
{
|
|
1398
|
-
total: (total) =>
|
|
1399
|
-
|
|
1400
|
-
|
|
1275
|
+
total: (total) => {
|
|
1276
|
+
checkCanceled();
|
|
1277
|
+
this.setProgress(this.progressCurrent, total);
|
|
1278
|
+
},
|
|
1279
|
+
advance: (count = 1) => {
|
|
1280
|
+
checkCanceled();
|
|
1281
|
+
this.setProgress(this.progressCurrent + count, this.progressTotal);
|
|
1282
|
+
},
|
|
1283
|
+
set: (current, total) => {
|
|
1284
|
+
checkCanceled();
|
|
1285
|
+
this.setProgress(current, total ?? this.progressTotal);
|
|
1286
|
+
}
|
|
1401
1287
|
},
|
|
1402
1288
|
{
|
|
1403
1289
|
current: { enumerable: true, get: () => this.progressCurrent },
|
|
@@ -1409,18 +1295,20 @@ var RunReporter = class {
|
|
|
1409
1295
|
// defineProperties não tipa os getters adicionados — a interface é a fonte de verdade
|
|
1410
1296
|
);
|
|
1411
1297
|
const signal = this.abortController.signal;
|
|
1298
|
+
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
1412
1299
|
this.context = {
|
|
1413
1300
|
runId,
|
|
1414
1301
|
task,
|
|
1302
|
+
params,
|
|
1415
1303
|
log,
|
|
1416
1304
|
items,
|
|
1417
1305
|
progress,
|
|
1418
|
-
tx,
|
|
1306
|
+
transaction: tx,
|
|
1419
1307
|
signal,
|
|
1420
|
-
throwIfCanceled:
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1308
|
+
throwIfCanceled: checkCanceled,
|
|
1309
|
+
sleep: sleep2,
|
|
1310
|
+
attach: (image, options) => this.attach(image, options),
|
|
1311
|
+
retry: (fn, options) => retryWithBackoff(fn, { ...options, signal, wait: sleep2 })
|
|
1424
1312
|
};
|
|
1425
1313
|
}
|
|
1426
1314
|
context;
|
|
@@ -1469,6 +1357,10 @@ var RunReporter = class {
|
|
|
1469
1357
|
get isDead() {
|
|
1470
1358
|
return this.dead;
|
|
1471
1359
|
}
|
|
1360
|
+
/** Lança `RunCanceled` se o signal desta run já disparou. */
|
|
1361
|
+
checkCanceled() {
|
|
1362
|
+
if (this.abortController.signal.aborted) throw runCanceledError();
|
|
1363
|
+
}
|
|
1472
1364
|
/** Aborta o ctx.signal sem matar o reporter (timeout/shutdown ainda reportam o terminal). */
|
|
1473
1365
|
abort() {
|
|
1474
1366
|
this.stopCancelWatch();
|
|
@@ -1747,13 +1639,13 @@ var RunReporter = class {
|
|
|
1747
1639
|
this.flushItems();
|
|
1748
1640
|
await Promise.race([
|
|
1749
1641
|
this.itemsChain,
|
|
1750
|
-
new Promise((
|
|
1642
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1751
1643
|
]);
|
|
1752
1644
|
if (this.itemsBuffer.length > 0 && !this.itemsUnsupported && !this.itemsQuotaExceeded) {
|
|
1753
1645
|
this.flushItems();
|
|
1754
1646
|
await Promise.race([
|
|
1755
1647
|
this.itemsChain,
|
|
1756
|
-
new Promise((
|
|
1648
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1757
1649
|
]);
|
|
1758
1650
|
}
|
|
1759
1651
|
}
|
|
@@ -1761,8 +1653,8 @@ var RunReporter = class {
|
|
|
1761
1653
|
/**
|
|
1762
1654
|
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
1763
1655
|
* o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
|
|
1764
|
-
* com um aviso no log da run.
|
|
1765
|
-
* `
|
|
1656
|
+
* com um aviso no log da run. É o `hobots.attach()`; o hint `attachment` de
|
|
1657
|
+
* `hobots.captureException` também cai aqui via run-context-store.
|
|
1766
1658
|
*/
|
|
1767
1659
|
async attach(image, options = {}) {
|
|
1768
1660
|
const id = uuid4();
|
|
@@ -1784,8 +1676,8 @@ var RunReporter = class {
|
|
|
1784
1676
|
printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
|
|
1785
1677
|
const logTs = this.push(level, scrubbed, true, [id]);
|
|
1786
1678
|
let settle;
|
|
1787
|
-
const result = new Promise((
|
|
1788
|
-
settle =
|
|
1679
|
+
const result = new Promise((resolve2) => {
|
|
1680
|
+
settle = resolve2;
|
|
1789
1681
|
});
|
|
1790
1682
|
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
1683
|
this.pendingAttachments.add(work);
|
|
@@ -1867,8 +1759,8 @@ var RunReporter = class {
|
|
|
1867
1759
|
this.uploadSlots--;
|
|
1868
1760
|
return Promise.resolve(release);
|
|
1869
1761
|
}
|
|
1870
|
-
return new Promise((
|
|
1871
|
-
this.uploadQueue.push(() =>
|
|
1762
|
+
return new Promise((resolve2) => {
|
|
1763
|
+
this.uploadQueue.push(() => resolve2(release));
|
|
1872
1764
|
});
|
|
1873
1765
|
}
|
|
1874
1766
|
/**
|
|
@@ -1879,7 +1771,7 @@ var RunReporter = class {
|
|
|
1879
1771
|
if (this.pendingAttachments.size === 0) return;
|
|
1880
1772
|
await Promise.race([
|
|
1881
1773
|
Promise.allSettled([...this.pendingAttachments]),
|
|
1882
|
-
new Promise((
|
|
1774
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
1883
1775
|
]);
|
|
1884
1776
|
}
|
|
1885
1777
|
enqueue(updateIn) {
|
|
@@ -1950,119 +1842,354 @@ async function runWithRunContext(store, fn) {
|
|
|
1950
1842
|
await ensureStorage();
|
|
1951
1843
|
return storage ? storage.run(store, fn) : fn();
|
|
1952
1844
|
}
|
|
1953
|
-
function
|
|
1954
|
-
return storage?.getStore()?.
|
|
1845
|
+
function currentRunReporter() {
|
|
1846
|
+
return storage?.getStore()?.reporter;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/transaction.ts
|
|
1850
|
+
function stepId() {
|
|
1851
|
+
return uuid4().slice(0, 16);
|
|
1852
|
+
}
|
|
1853
|
+
function stepLabel(op, description) {
|
|
1854
|
+
return [op, description].filter(Boolean).join(" \u2014 ") || "(sem nome)";
|
|
1855
|
+
}
|
|
1856
|
+
function levelForStatus(status) {
|
|
1857
|
+
switch (status) {
|
|
1858
|
+
case "warning":
|
|
1859
|
+
return "warning";
|
|
1860
|
+
case "error":
|
|
1861
|
+
return "error";
|
|
1862
|
+
case "cancelled":
|
|
1863
|
+
return "warning";
|
|
1864
|
+
default:
|
|
1865
|
+
return "success";
|
|
1866
|
+
}
|
|
1955
1867
|
}
|
|
1956
|
-
function
|
|
1957
|
-
|
|
1868
|
+
function colorForStatus(status) {
|
|
1869
|
+
switch (status) {
|
|
1870
|
+
case "warning":
|
|
1871
|
+
return "yellow";
|
|
1872
|
+
case "error":
|
|
1873
|
+
return "red";
|
|
1874
|
+
case "cancelled":
|
|
1875
|
+
return "gray";
|
|
1876
|
+
default:
|
|
1877
|
+
return "green";
|
|
1878
|
+
}
|
|
1958
1879
|
}
|
|
1880
|
+
var Step = class {
|
|
1881
|
+
constructor(transaction, parentStepId, op, description) {
|
|
1882
|
+
this.transaction = transaction;
|
|
1883
|
+
this.parentStepId = parentStepId;
|
|
1884
|
+
this.op = op;
|
|
1885
|
+
this.description = description;
|
|
1886
|
+
}
|
|
1887
|
+
stepId = stepId();
|
|
1888
|
+
startTimestamp = nowISO();
|
|
1889
|
+
status = "ok";
|
|
1890
|
+
endTimestamp;
|
|
1891
|
+
data;
|
|
1892
|
+
setStatus(status) {
|
|
1893
|
+
this.status = status;
|
|
1894
|
+
return this;
|
|
1895
|
+
}
|
|
1896
|
+
setData(key, value) {
|
|
1897
|
+
this.data = { ...this.data, [key]: value };
|
|
1898
|
+
return this;
|
|
1899
|
+
}
|
|
1900
|
+
/** Step aninhada (filha desta step). */
|
|
1901
|
+
startChild(op, description) {
|
|
1902
|
+
return this.transaction.createStep(this.stepId, op, description);
|
|
1903
|
+
}
|
|
1904
|
+
finish() {
|
|
1905
|
+
if (this.endTimestamp) return;
|
|
1906
|
+
this.endTimestamp = nowISO();
|
|
1907
|
+
this.transaction.emit(
|
|
1908
|
+
"step",
|
|
1909
|
+
"finish",
|
|
1910
|
+
stepLabel(this.op, this.description),
|
|
1911
|
+
this.startTimestamp,
|
|
1912
|
+
this.endTimestamp,
|
|
1913
|
+
this.status
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
toJSON() {
|
|
1917
|
+
return {
|
|
1918
|
+
step_id: this.stepId,
|
|
1919
|
+
parent_step_id: this.parentStepId,
|
|
1920
|
+
op: this.op,
|
|
1921
|
+
description: this.description,
|
|
1922
|
+
status: this.status,
|
|
1923
|
+
start_timestamp: this.startTimestamp,
|
|
1924
|
+
timestamp: this.endTimestamp ?? nowISO(),
|
|
1925
|
+
data: this.data
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
var Transaction = class {
|
|
1930
|
+
constructor(client, name, op, runId) {
|
|
1931
|
+
this.client = client;
|
|
1932
|
+
this.name = name;
|
|
1933
|
+
this.op = op;
|
|
1934
|
+
this.runId = runId;
|
|
1935
|
+
this.emit("transaction", "start", op ? `${name} (${op})` : name);
|
|
1936
|
+
}
|
|
1937
|
+
eventId = uuid4();
|
|
1938
|
+
stepId = stepId();
|
|
1939
|
+
startTimestamp = nowISO();
|
|
1940
|
+
status = "ok";
|
|
1941
|
+
steps = [];
|
|
1942
|
+
tags = {};
|
|
1943
|
+
finished = false;
|
|
1944
|
+
lifecycleSink;
|
|
1945
|
+
// instalado pelo RunReporter: abrir uma step é um ponto de parada cooperativo (lança RunCanceled)
|
|
1946
|
+
stepGuard;
|
|
1947
|
+
// eventos emitidos antes do sink existir (o START da transaction sai no construtor,
|
|
1948
|
+
// antes do RunReporter conectar) — replay no setLifecycleSink
|
|
1949
|
+
lifecycleBacklog = [];
|
|
1950
|
+
/**
|
|
1951
|
+
* Registra o início/fim de uma step ou transaction: espelha a linha no sink de
|
|
1952
|
+
* lifecycle, se conectado (é assim que os eventos de performance aparecem nos
|
|
1953
|
+
* logs da run no app), e ecoa no terminal apenas com `debug: true` — o SDK não
|
|
1954
|
+
* escreve no stdout do processo do usuário sem permissão.
|
|
1955
|
+
*/
|
|
1956
|
+
emit(kind, phase, label, startTs, endTs, status) {
|
|
1957
|
+
const durationMs = startTs && endTs ? new Date(endTs).getTime() - new Date(startTs).getTime() : void 0;
|
|
1958
|
+
const suffix = durationMs != null ? ` (${durationMs}ms)` : "";
|
|
1959
|
+
if (this.client?.debug) {
|
|
1960
|
+
const tag = colorize(`[hobots:${kind}] ${phase.toUpperCase()}`, phase === "finish" ? colorForStatus(status ?? "ok") : "cyan");
|
|
1961
|
+
console.log(`${tag} ${label}${suffix}`);
|
|
1962
|
+
}
|
|
1963
|
+
const level = phase === "finish" ? levelForStatus(status ?? "ok") : "info";
|
|
1964
|
+
const message = `[${kind}] ${phase.toUpperCase()} ${label}${suffix}`;
|
|
1965
|
+
if (this.lifecycleSink) this.lifecycleSink(level, message);
|
|
1966
|
+
else if (this.lifecycleBacklog.length < 100) this.lifecycleBacklog.push([level, message]);
|
|
1967
|
+
}
|
|
1968
|
+
/** Conecta o espelho de lifecycle (RunReporter) e faz replay do backlog em ordem. */
|
|
1969
|
+
setLifecycleSink(sink) {
|
|
1970
|
+
this.lifecycleSink = sink;
|
|
1971
|
+
const backlog = this.lifecycleBacklog;
|
|
1972
|
+
this.lifecycleBacklog = [];
|
|
1973
|
+
for (const [level, message] of backlog) sink(level, message);
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Conecta a checagem de cancelamento chamada antes de cada step nova — é assim que
|
|
1977
|
+
* `hobots.transaction.startChild()` (e `step.startChild()`) param uma run cancelada.
|
|
1978
|
+
*/
|
|
1979
|
+
setStepGuard(guard) {
|
|
1980
|
+
this.stepGuard = guard;
|
|
1981
|
+
}
|
|
1982
|
+
createStep(parentStepId, op, description) {
|
|
1983
|
+
this.stepGuard?.();
|
|
1984
|
+
const step = new Step(this, parentStepId, op, description);
|
|
1985
|
+
this.steps.push(step);
|
|
1986
|
+
this.emit("step", "start", stepLabel(op, description));
|
|
1987
|
+
return step;
|
|
1988
|
+
}
|
|
1989
|
+
/** Step direta da transaction. */
|
|
1990
|
+
startChild(op, description) {
|
|
1991
|
+
return this.createStep(this.stepId, op, description);
|
|
1992
|
+
}
|
|
1993
|
+
setTag(key, value) {
|
|
1994
|
+
this.tags[key] = value;
|
|
1995
|
+
return this;
|
|
1996
|
+
}
|
|
1997
|
+
setStatus(status) {
|
|
1998
|
+
this.status = status;
|
|
1999
|
+
return this;
|
|
2000
|
+
}
|
|
2001
|
+
setName(name) {
|
|
2002
|
+
this.name = name;
|
|
2003
|
+
return this;
|
|
2004
|
+
}
|
|
2005
|
+
finish() {
|
|
2006
|
+
if (this.finished) return;
|
|
2007
|
+
this.finished = true;
|
|
2008
|
+
for (const step of this.steps) step.finish();
|
|
2009
|
+
const endTimestamp = nowISO();
|
|
2010
|
+
this.emit(
|
|
2011
|
+
"transaction",
|
|
2012
|
+
"finish",
|
|
2013
|
+
this.op ? `${this.name} (${this.op})` : this.name,
|
|
2014
|
+
this.startTimestamp,
|
|
2015
|
+
endTimestamp,
|
|
2016
|
+
this.status
|
|
2017
|
+
);
|
|
2018
|
+
if (!this.client) return;
|
|
2019
|
+
const payload = {
|
|
2020
|
+
event_id: this.eventId,
|
|
2021
|
+
name: this.name,
|
|
2022
|
+
op: this.op,
|
|
2023
|
+
status: this.status,
|
|
2024
|
+
start_timestamp: this.startTimestamp,
|
|
2025
|
+
timestamp: endTimestamp,
|
|
2026
|
+
steps: this.steps.map((step) => step.toJSON()),
|
|
2027
|
+
tags: Object.keys(this.tags).length > 0 ? this.tags : void 0,
|
|
2028
|
+
run_id: this.runId
|
|
2029
|
+
};
|
|
2030
|
+
this.client.sendTransaction(payload);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
1959
2033
|
|
|
1960
|
-
// src/tasks/
|
|
1961
|
-
var
|
|
1962
|
-
function
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2034
|
+
// src/tasks/runner.ts
|
|
2035
|
+
var DEFAULT_RETRY_DELAY_MS = 5e3;
|
|
2036
|
+
async function executeRun(input) {
|
|
2037
|
+
const { run, task, api, client, logger, tracker } = input;
|
|
2038
|
+
const options = task.options;
|
|
2039
|
+
const tx = new Transaction(client, `run:${run.task}`, "hobots.task", run.id);
|
|
2040
|
+
const reporter = new RunReporter(
|
|
2041
|
+
api,
|
|
2042
|
+
run.id,
|
|
2043
|
+
run.task,
|
|
2044
|
+
logger,
|
|
2045
|
+
tx,
|
|
2046
|
+
options.logFlushIntervalMs,
|
|
2047
|
+
options.cancelCheckIntervalMs,
|
|
2048
|
+
run.params
|
|
2049
|
+
);
|
|
2050
|
+
tracker.add(reporter);
|
|
2051
|
+
try {
|
|
2052
|
+
const ok = await reporter.reportRunning();
|
|
2053
|
+
if (!ok) {
|
|
2054
|
+
logger.debug(`run ${run.id} cancelada antes de iniciar \u2014 handler n\xE3o executado`);
|
|
2055
|
+
tx.setStatus("cancelled");
|
|
2056
|
+
tx.finish();
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
logger.debug(`executando run ${run.id} (${run.task})`);
|
|
2060
|
+
try {
|
|
2061
|
+
await runWithRunContext({ reporter }, () => invokeWithRetry(task, run, reporter, logger));
|
|
2062
|
+
await reporter.finalizeAttachments();
|
|
2063
|
+
await reporter.finalizeItems();
|
|
2064
|
+
tx.setStatus("ok");
|
|
2065
|
+
tx.finish();
|
|
2066
|
+
reporter.reportSuccess();
|
|
2067
|
+
logger.debug(`run ${run.id} conclu\xEDda`);
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
await reporter.finalizeAttachments();
|
|
2070
|
+
await reporter.finalizeItems();
|
|
2071
|
+
tx.setStatus("error");
|
|
2072
|
+
tx.finish();
|
|
2073
|
+
reporter.reportFailure(toRunError(error));
|
|
2074
|
+
logger.debug(`run ${run.id} falhou: ${String(error)}`);
|
|
2075
|
+
}
|
|
2076
|
+
await reporter.finalize();
|
|
2077
|
+
} finally {
|
|
2078
|
+
tracker.remove(reporter);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function invokeWithRetry(task, run, reporter, logger) {
|
|
2082
|
+
const { retries, retryDelayMs } = task.options;
|
|
2083
|
+
const total = retries + 1;
|
|
2084
|
+
const ctx = reporter.context;
|
|
2085
|
+
return retryWithBackoff(() => invokeWithTimeout(task, run, reporter, logger), {
|
|
2086
|
+
retries,
|
|
2087
|
+
delayMs: (attempt) => resolveDelay(retryDelayMs, attempt, DEFAULT_RETRY_DELAY_MS),
|
|
2088
|
+
shouldRetry: () => !reporter.isDead,
|
|
2089
|
+
signal: ctx.signal,
|
|
2090
|
+
wait: (ms) => ctx.sleep(ms),
|
|
2091
|
+
onRetry: (error, attempt, delayMs) => {
|
|
2092
|
+
const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2093
|
+
ctx.log.warn(`tentativa ${attempt}/${total} falhou: ${truncate(reason, 500)} \u2014 nova tentativa em ${delayMs}ms`);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
var TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("task-timeout");
|
|
2098
|
+
var CANCEL_SENTINEL = /* @__PURE__ */ Symbol("task-canceled");
|
|
2099
|
+
async function invokeWithTimeout(task, run, reporter, logger) {
|
|
2100
|
+
const { timeoutMs, cancelGraceMs } = task.options;
|
|
2101
|
+
const handler = task.handler;
|
|
2102
|
+
const invocation = Promise.resolve().then(() => handler(run.params, reporter.context));
|
|
2103
|
+
const races = [];
|
|
2104
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2105
|
+
races.push(sleep(timeoutMs).then(() => TIMEOUT_SENTINEL));
|
|
2106
|
+
}
|
|
2107
|
+
if (cancelGraceMs > 0) {
|
|
2108
|
+
races.push(waitForCancelGrace(reporter.context.signal, cancelGraceMs).then(() => CANCEL_SENTINEL));
|
|
2109
|
+
}
|
|
2110
|
+
if (races.length === 0) {
|
|
2111
|
+
await invocation;
|
|
2112
|
+
return;
|
|
1986
2113
|
}
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2114
|
+
const outcome = await Promise.race([invocation, ...races]);
|
|
2115
|
+
if (outcome !== TIMEOUT_SENTINEL && outcome !== CANCEL_SENTINEL) return;
|
|
2116
|
+
if (outcome === CANCEL_SENTINEL) {
|
|
2117
|
+
logger.warn(
|
|
2118
|
+
`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)`
|
|
2119
|
+
);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
reporter.abort();
|
|
2123
|
+
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)`));
|
|
2124
|
+
throw taskTimeoutError(timeoutMs);
|
|
2125
|
+
}
|
|
2126
|
+
function waitForCancelGrace(signal, graceMs) {
|
|
2127
|
+
return new Promise((resolve2) => {
|
|
2128
|
+
if (signal.aborted) {
|
|
2129
|
+
setTimeout(resolve2, graceMs);
|
|
2130
|
+
return;
|
|
1995
2131
|
}
|
|
2132
|
+
signal.addEventListener("abort", () => setTimeout(resolve2, graceMs), { once: true });
|
|
1996
2133
|
});
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2134
|
+
}
|
|
2135
|
+
var MAX_ERROR_MESSAGE_CHARS = 1e4;
|
|
2136
|
+
var MAX_ERROR_STACK_CHARS = 2e4;
|
|
2137
|
+
function toRunError(error) {
|
|
2138
|
+
if (error instanceof Error) {
|
|
2139
|
+
return {
|
|
2140
|
+
type: error.name,
|
|
2141
|
+
message: scrubSecrets(truncate(error.message, MAX_ERROR_MESSAGE_CHARS)),
|
|
2142
|
+
stack: error.stack ? scrubSecrets(truncate(error.stack, MAX_ERROR_STACK_CHARS)) : void 0
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
return { message: scrubSecrets(truncate(String(error), MAX_ERROR_MESSAGE_CHARS)) };
|
|
2002
2146
|
}
|
|
2003
2147
|
|
|
2004
2148
|
// src/tasks/agent.ts
|
|
2005
2149
|
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
2006
2150
|
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
2007
|
-
var
|
|
2008
|
-
var POLL_RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
|
|
2009
|
-
var AUTH_FAILURE_INTERVAL_MS = 3e4;
|
|
2010
|
-
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
2011
|
-
function hostname() {
|
|
2012
|
-
if (!isNode()) return void 0;
|
|
2013
|
-
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
2014
|
-
}
|
|
2151
|
+
var MAX_CLAIM_PER_POLL = 5;
|
|
2015
2152
|
var TaskAgent = class {
|
|
2016
|
-
constructor(options
|
|
2153
|
+
constructor(options) {
|
|
2017
2154
|
this.options = options;
|
|
2018
|
-
this.
|
|
2019
|
-
this.
|
|
2020
|
-
const config = options.config;
|
|
2021
|
-
this.api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger: this.logger });
|
|
2155
|
+
this.logger = options.logger;
|
|
2156
|
+
this.api = options.api;
|
|
2022
2157
|
this.client = options.client;
|
|
2158
|
+
this.registry = options.registry;
|
|
2159
|
+
this.tracker = options.tracker;
|
|
2023
2160
|
this.pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
2024
|
-
this.
|
|
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 });
|
|
2161
|
+
this.backoff = new Backoff(this.logger, "poll", this.pollIntervalMs);
|
|
2031
2162
|
}
|
|
2032
2163
|
logger;
|
|
2164
|
+
pollIntervalMs;
|
|
2033
2165
|
api;
|
|
2034
2166
|
client;
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
taskTimeoutMs;
|
|
2039
|
-
logFlushIntervalMs;
|
|
2040
|
-
cancelCheckIntervalMs;
|
|
2041
|
-
cancelGraceMs;
|
|
2167
|
+
registry;
|
|
2168
|
+
tracker;
|
|
2169
|
+
backoff;
|
|
2042
2170
|
state = "idle";
|
|
2043
2171
|
timer;
|
|
2044
2172
|
pollAbort;
|
|
2045
|
-
consecutiveFailures = 0;
|
|
2046
|
-
consecutiveAuthFailures = 0;
|
|
2047
2173
|
activeRuns = /* @__PURE__ */ new Set();
|
|
2048
|
-
reporters = /* @__PURE__ */ new Set();
|
|
2049
|
-
uninstallShutdown;
|
|
2050
2174
|
get activeCount() {
|
|
2051
2175
|
return this.activeRuns.size;
|
|
2052
2176
|
}
|
|
2053
2177
|
start() {
|
|
2054
2178
|
if (this.state === "started") return;
|
|
2055
2179
|
if (this.state === "stopping") {
|
|
2056
|
-
throw new Error("[hobots] start
|
|
2057
|
-
}
|
|
2058
|
-
if (this.registry.size === 0) {
|
|
2059
|
-
throw new Error("[hobots] nenhuma task registrada \u2014 chame register() antes de start()");
|
|
2180
|
+
throw new Error("[hobots] start do agente durante o stop \u2014 aguarde o stop terminar");
|
|
2060
2181
|
}
|
|
2061
2182
|
this.state = "started";
|
|
2062
|
-
|
|
2063
|
-
|
|
2183
|
+
this.logger.debug(`polling iniciado (tasks: ${this.registry.remoteTasks().map((task) => task.slug).join(", ")})`);
|
|
2184
|
+
this.schedule(0);
|
|
2185
|
+
}
|
|
2186
|
+
/** Uma task registrada depois do boot entra no próximo poll — que é antecipado para já. */
|
|
2187
|
+
wake() {
|
|
2188
|
+
if (this.state !== "started") return;
|
|
2189
|
+
if (this.timer) {
|
|
2190
|
+
clearTimeout(this.timer);
|
|
2191
|
+
this.timer = void 0;
|
|
2064
2192
|
}
|
|
2065
|
-
this.logger.debug(`polling iniciado (tasks: ${[...this.registry.keys()].join(", ")})`);
|
|
2066
2193
|
this.schedule(0);
|
|
2067
2194
|
}
|
|
2068
2195
|
/** Para o polling e aguarda as runs ativas (até timeoutMs). Retorna true se tudo terminou a tempo. */
|
|
@@ -2075,40 +2202,10 @@ var TaskAgent = class {
|
|
|
2075
2202
|
}
|
|
2076
2203
|
this.pollAbort?.abort();
|
|
2077
2204
|
const all = Promise.allSettled([...this.activeRuns]).then(() => true);
|
|
2078
|
-
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) :
|
|
2079
|
-
if (!finished && this.activeRuns.size > 0) {
|
|
2080
|
-
this.logger.warn(`${this.activeRuns.size} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
2081
|
-
await this.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
2082
|
-
}
|
|
2083
|
-
this.uninstallShutdown?.();
|
|
2084
|
-
this.uninstallShutdown = void 0;
|
|
2205
|
+
const finished = timeoutMs > 0 ? await Promise.race([all, sleep(timeoutMs).then(() => false)]) : this.activeRuns.size === 0;
|
|
2085
2206
|
this.state = "idle";
|
|
2086
2207
|
return finished;
|
|
2087
2208
|
}
|
|
2088
|
-
/**
|
|
2089
|
-
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
2090
|
-
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
2091
|
-
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
2092
|
-
* o sweep server-side é o backstop.
|
|
2093
|
-
*/
|
|
2094
|
-
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
2095
|
-
const pending = [];
|
|
2096
|
-
for (const reporter of this.reporters) {
|
|
2097
|
-
if (reporter.isDead) continue;
|
|
2098
|
-
const logs = reporter.drainLogs();
|
|
2099
|
-
reporter.kill();
|
|
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
|
-
);
|
|
2108
|
-
}
|
|
2109
|
-
if (pending.length === 0) return;
|
|
2110
|
-
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
2111
|
-
}
|
|
2112
2209
|
/** Timer intencionalmente NÃO unref'd: o agente é a razão do processo existir. */
|
|
2113
2210
|
schedule(delayMs) {
|
|
2114
2211
|
if (this.state !== "started" || this.timer) return;
|
|
@@ -2117,9 +2214,22 @@ var TaskAgent = class {
|
|
|
2117
2214
|
void this.tick();
|
|
2118
2215
|
}, delayMs);
|
|
2119
2216
|
}
|
|
2217
|
+
/** Slots livres por task remota — a soma vira o `max` do poll e as tasks com slot viram o filtro. */
|
|
2218
|
+
freeSlots() {
|
|
2219
|
+
const tasks = [];
|
|
2220
|
+
let max = 0;
|
|
2221
|
+
for (const task of this.registry.remoteTasks()) {
|
|
2222
|
+
const free = task.options.concurrency - this.tracker.activeFor(task.slug);
|
|
2223
|
+
if (free <= 0) continue;
|
|
2224
|
+
tasks.push(task.slug);
|
|
2225
|
+
max += free;
|
|
2226
|
+
}
|
|
2227
|
+
return { tasks, max: Math.min(MAX_CLAIM_PER_POLL, max) };
|
|
2228
|
+
}
|
|
2120
2229
|
async tick() {
|
|
2121
2230
|
if (this.state !== "started") return;
|
|
2122
|
-
|
|
2231
|
+
const { tasks, max } = this.freeSlots();
|
|
2232
|
+
if (max === 0) {
|
|
2123
2233
|
this.schedule(this.pollIntervalMs);
|
|
2124
2234
|
return;
|
|
2125
2235
|
}
|
|
@@ -2127,224 +2237,448 @@ var TaskAgent = class {
|
|
|
2127
2237
|
try {
|
|
2128
2238
|
const response = await this.api.poll(
|
|
2129
2239
|
{
|
|
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()]
|
|
2240
|
+
// identidade canônica do agente no painel — o mesmo instanceId do heartbeat,
|
|
2241
|
+
// então os dois canais convergem em um agente. `instance.tasks` lista TODAS as
|
|
2242
|
+
// tasks (register + start); o filtro `tasks` abaixo só as que este poll pode rodar.
|
|
2243
|
+
instance: this.options.instanceInfo(),
|
|
2244
|
+
max,
|
|
2245
|
+
tasks
|
|
2150
2246
|
},
|
|
2151
2247
|
this.pollAbort.signal
|
|
2152
2248
|
);
|
|
2153
|
-
this.
|
|
2154
|
-
this.consecutiveAuthFailures = 0;
|
|
2249
|
+
this.backoff.reset();
|
|
2155
2250
|
for (const run of response.runs) {
|
|
2156
|
-
if (this.activeRuns.size >= this.concurrency) break;
|
|
2157
2251
|
this.dispatch(run);
|
|
2158
2252
|
}
|
|
2159
2253
|
this.schedule(response.runs.length > 0 ? 0 : this.pollIntervalMs);
|
|
2160
2254
|
} catch (error) {
|
|
2161
2255
|
if (this.state !== "started") return;
|
|
2162
|
-
this.schedule(this.
|
|
2256
|
+
this.schedule(this.backoff.next(error));
|
|
2163
2257
|
} finally {
|
|
2164
2258
|
this.pollAbort = void 0;
|
|
2165
2259
|
}
|
|
2166
2260
|
}
|
|
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
2261
|
dispatch(run) {
|
|
2182
|
-
const promise = this.
|
|
2262
|
+
const promise = this.executeClaimed(run).catch((error) => this.logger.error(`erro inesperado na run ${run.id}:`, error)).finally(() => {
|
|
2183
2263
|
this.activeRuns.delete(promise);
|
|
2184
2264
|
if (this.state === "started") this.schedule(0);
|
|
2185
2265
|
});
|
|
2186
2266
|
this.activeRuns.add(promise);
|
|
2187
2267
|
}
|
|
2188
|
-
async
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2268
|
+
async executeClaimed(run) {
|
|
2269
|
+
const task = this.registry.get(run.task);
|
|
2270
|
+
if (!task || task.mode !== "remote") {
|
|
2271
|
+
const tx = new Transaction(this.client, `run:${run.task}`, "hobots.task", run.id);
|
|
2272
|
+
const reporter = new RunReporter(this.api, run.id, run.task, this.logger, tx);
|
|
2273
|
+
reporter.reportFailure(toRunError(Object.assign(new Error(`tarefa n\xE3o registrada no agente: ${run.task}`), { name: "HandlerNotFound" })));
|
|
2274
|
+
await reporter.finalize();
|
|
2275
|
+
tx.setStatus("error");
|
|
2276
|
+
tx.finish();
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
await executeRun({ run, task, api: this.api, client: this.client, logger: this.logger, tracker: this.tracker });
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
// src/tasks/local.ts
|
|
2284
|
+
var LocalRunner = class {
|
|
2285
|
+
constructor(task, deps) {
|
|
2286
|
+
this.task = task;
|
|
2287
|
+
this.deps = deps;
|
|
2288
|
+
this.backoff = new Backoff(deps.logger, `cria\xE7\xE3o da run "${task.slug}"`);
|
|
2289
|
+
this.done = new Promise((resolve2) => {
|
|
2290
|
+
this.resolveDone = resolve2;
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
abort = new AbortController();
|
|
2294
|
+
backoff;
|
|
2295
|
+
/** Resolve quando a run terminou (ou o runner foi parado antes de abri-la). */
|
|
2296
|
+
done;
|
|
2297
|
+
resolveDone;
|
|
2298
|
+
async run() {
|
|
2192
2299
|
try {
|
|
2193
|
-
const
|
|
2194
|
-
if (!
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2300
|
+
const run = await this.createRun();
|
|
2301
|
+
if (!run) return;
|
|
2302
|
+
this.deps.logger.debug(`run ${run.id} aberta para "${this.task.slug}" (modo start)`);
|
|
2303
|
+
await executeRun({
|
|
2304
|
+
run,
|
|
2305
|
+
task: this.task,
|
|
2306
|
+
api: this.deps.api,
|
|
2307
|
+
client: this.deps.client,
|
|
2308
|
+
logger: this.deps.logger,
|
|
2309
|
+
tracker: this.deps.tracker
|
|
2310
|
+
});
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
this.deps.logger.error(`erro inesperado na task "${this.task.slug}":`, error);
|
|
2313
|
+
} finally {
|
|
2314
|
+
this.resolveDone();
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/** Desiste de abrir a run (close() antes da plataforma responder). Uma run já aberta segue pelo tracker. */
|
|
2318
|
+
stop() {
|
|
2319
|
+
this.abort.abort();
|
|
2320
|
+
}
|
|
2321
|
+
async createRun() {
|
|
2322
|
+
for (; ; ) {
|
|
2323
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2212
2324
|
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`);
|
|
2325
|
+
return await this.deps.api.createRun({ task: this.task.slug, instance: this.deps.instanceInfo() }, this.abort.signal);
|
|
2223
2326
|
} 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)}`);
|
|
2327
|
+
if (this.abort.signal.aborted) return void 0;
|
|
2328
|
+
await abortableSleep(this.backoff.next(error), this.abort.signal);
|
|
2230
2329
|
}
|
|
2231
|
-
await reporter.finalize();
|
|
2232
|
-
} finally {
|
|
2233
|
-
this.reporters.delete(reporter);
|
|
2234
2330
|
}
|
|
2235
2331
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
if (this.
|
|
2243
|
-
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
// src/tasks/registry.ts
|
|
2335
|
+
var TaskRegistry = class {
|
|
2336
|
+
tasks = /* @__PURE__ */ new Map();
|
|
2337
|
+
add(task) {
|
|
2338
|
+
if (this.tasks.has(task.slug)) {
|
|
2339
|
+
throw new Error(`[hobots] task "${task.slug}" j\xE1 registrada \u2014 cada slug s\xF3 pode ter um handler (register ou start)`);
|
|
2244
2340
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2341
|
+
this.tasks.set(task.slug, task);
|
|
2342
|
+
}
|
|
2343
|
+
get(slug) {
|
|
2344
|
+
return this.tasks.get(slug);
|
|
2345
|
+
}
|
|
2346
|
+
get size() {
|
|
2347
|
+
return this.tasks.size;
|
|
2348
|
+
}
|
|
2349
|
+
/** Tasks que chegam pelo poll (register). */
|
|
2350
|
+
remoteTasks() {
|
|
2351
|
+
return [...this.tasks.values()].filter((task) => task.mode === "remote");
|
|
2352
|
+
}
|
|
2353
|
+
/** Tasks abertas pelo próprio bot (start). */
|
|
2354
|
+
localTasks() {
|
|
2355
|
+
return [...this.tasks.values()].filter((task) => task.mode === "local");
|
|
2356
|
+
}
|
|
2357
|
+
/** Todos os slugs, para a presença da instância (`instance.tasks`). */
|
|
2358
|
+
allSlugs() {
|
|
2359
|
+
return [...this.tasks.keys()];
|
|
2360
|
+
}
|
|
2361
|
+
clear() {
|
|
2362
|
+
this.tasks.clear();
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
var RunTracker = class {
|
|
2366
|
+
constructor(api) {
|
|
2367
|
+
this.api = api;
|
|
2368
|
+
}
|
|
2369
|
+
reporters = /* @__PURE__ */ new Set();
|
|
2370
|
+
waiters = /* @__PURE__ */ new Set();
|
|
2371
|
+
add(reporter) {
|
|
2372
|
+
this.reporters.add(reporter);
|
|
2373
|
+
}
|
|
2374
|
+
remove(reporter) {
|
|
2375
|
+
this.reporters.delete(reporter);
|
|
2376
|
+
if (this.reporters.size === 0) {
|
|
2377
|
+
for (const resolve2 of this.waiters) resolve2();
|
|
2378
|
+
this.waiters.clear();
|
|
2248
2379
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2380
|
+
}
|
|
2381
|
+
get activeCount() {
|
|
2382
|
+
return this.reporters.size;
|
|
2383
|
+
}
|
|
2384
|
+
/** Runs ativas de uma task (concorrência por task no poll). */
|
|
2385
|
+
activeFor(slug) {
|
|
2386
|
+
let count = 0;
|
|
2387
|
+
for (const reporter of this.reporters) if (reporter.context.task === slug) count++;
|
|
2388
|
+
return count;
|
|
2389
|
+
}
|
|
2390
|
+
/** A única run ativa, quando há exatamente uma — o que `hobots.*` usa fora de um contexto async. */
|
|
2391
|
+
singleActive() {
|
|
2392
|
+
if (this.reporters.size !== 1) return void 0;
|
|
2393
|
+
return this.reporters.values().next().value;
|
|
2394
|
+
}
|
|
2395
|
+
/** Resolve quando não houver mais runs ativas (ou após timeoutMs, devolvendo false). */
|
|
2396
|
+
async waitIdle(timeoutMs) {
|
|
2397
|
+
if (this.reporters.size === 0) return true;
|
|
2398
|
+
const idle = new Promise((resolve2) => this.waiters.add(() => resolve2(true)));
|
|
2399
|
+
if (timeoutMs <= 0) return false;
|
|
2400
|
+
return Promise.race([idle, sleep(timeoutMs).then(() => false)]);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Best-effort do exit path: reporta 'failed'/AgentTerminated para as runs ativas
|
|
2404
|
+
* antes do processo morrer, fora da fila do reporter e com timeout curto. Runs já
|
|
2405
|
+
* canceladas no app (reporter morto) são puladas; erros de rede são engolidos —
|
|
2406
|
+
* o sweep server-side é o backstop.
|
|
2407
|
+
*/
|
|
2408
|
+
async reportAgentTerminated(message, timeoutMs = 2500) {
|
|
2409
|
+
const pending = [];
|
|
2410
|
+
for (const reporter of this.reporters) {
|
|
2411
|
+
if (reporter.isDead) continue;
|
|
2412
|
+
const logs = reporter.drainLogs();
|
|
2413
|
+
reporter.kill();
|
|
2414
|
+
pending.push(
|
|
2415
|
+
this.api.updateRunOnce(
|
|
2416
|
+
reporter.context.runId,
|
|
2417
|
+
{ status: "failed", error: { type: "AgentTerminated", message }, logs: logs.length > 0 ? logs : void 0 },
|
|
2418
|
+
timeoutMs
|
|
2419
|
+
).catch(() => {
|
|
2420
|
+
})
|
|
2254
2421
|
);
|
|
2255
|
-
return;
|
|
2256
2422
|
}
|
|
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
|
-
});
|
|
2423
|
+
if (pending.length === 0) return;
|
|
2424
|
+
await Promise.race([Promise.allSettled(pending), sleep(timeoutMs)]);
|
|
2263
2425
|
}
|
|
2264
2426
|
};
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2427
|
+
|
|
2428
|
+
// src/tasks/shutdown.ts
|
|
2429
|
+
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
2430
|
+
function installShutdownHandlers(agent) {
|
|
2431
|
+
if (!isNode()) return () => {
|
|
2432
|
+
};
|
|
2433
|
+
let signaled = false;
|
|
2434
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
2435
|
+
for (const signal of WATCHED_SIGNALS) {
|
|
2436
|
+
const handler = markInternal(() => {
|
|
2437
|
+
if (externalListenerCount(signal) > 0) return;
|
|
2438
|
+
if (signaled) return;
|
|
2439
|
+
signaled = true;
|
|
2440
|
+
const reraise = () => {
|
|
2441
|
+
process.removeListener(signal, handler);
|
|
2442
|
+
process.kill(process.pid, signal);
|
|
2443
|
+
};
|
|
2444
|
+
if (agent.activeCount === 0) {
|
|
2445
|
+
reraise();
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
const report = agent.reportAgentTerminated(`o processo do agente recebeu ${signal} durante a execu\xE7\xE3o`);
|
|
2449
|
+
addEmergencyTask(report);
|
|
2450
|
+
void report.finally(reraise);
|
|
2451
|
+
});
|
|
2452
|
+
process.on(signal, handler);
|
|
2453
|
+
signalHandlers.set(signal, handler);
|
|
2454
|
+
}
|
|
2455
|
+
const onFatal = markInternal((error) => {
|
|
2456
|
+
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();
|
|
2457
|
+
addEmergencyTask(report);
|
|
2458
|
+
if (!hasInstalledGlobalHandlers() && externalListenerCount("uncaughtException") === 0) {
|
|
2459
|
+
void report.finally(() => {
|
|
2460
|
+
console.error(error);
|
|
2461
|
+
process.exit(1);
|
|
2462
|
+
});
|
|
2272
2463
|
}
|
|
2273
|
-
signal.addEventListener("abort", () => setTimeout(resolve, graceMs), { once: true });
|
|
2274
2464
|
});
|
|
2465
|
+
process.on("uncaughtException", onFatal);
|
|
2466
|
+
return () => {
|
|
2467
|
+
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
|
|
2468
|
+
process.removeListener("uncaughtException", onFatal);
|
|
2469
|
+
};
|
|
2275
2470
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2471
|
+
|
|
2472
|
+
// src/runtime.ts
|
|
2473
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2474
|
+
var DEFAULT_RETRIES2 = 1;
|
|
2475
|
+
var DEFAULT_RETRY_DELAY_MS2 = 5e3;
|
|
2476
|
+
var DEFAULT_CANCEL_GRACE_MS = 1e4;
|
|
2477
|
+
var MAX_CONCURRENCY = 5;
|
|
2478
|
+
var MAX_SLUG_CHARS = 255;
|
|
2479
|
+
var PROCESS_LEVEL_KEYS = [
|
|
2480
|
+
"clientSecret",
|
|
2481
|
+
"instanceId",
|
|
2482
|
+
"environment",
|
|
2483
|
+
"release",
|
|
2484
|
+
"tags",
|
|
2485
|
+
"debug",
|
|
2486
|
+
"machineMetrics",
|
|
2487
|
+
"captureUnhandled",
|
|
2488
|
+
"installExitHandlers"
|
|
2489
|
+
];
|
|
2490
|
+
var runtime;
|
|
2491
|
+
var warnedNoRuntime = false;
|
|
2492
|
+
function hostname() {
|
|
2493
|
+
if (!isNode()) return void 0;
|
|
2494
|
+
return process.env.HOSTNAME || process.env.COMPUTERNAME || void 0;
|
|
2495
|
+
}
|
|
2496
|
+
function sameValue(a, b) {
|
|
2497
|
+
if (a === b) return true;
|
|
2498
|
+
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
function ensureRuntime(options) {
|
|
2502
|
+
if (runtime) {
|
|
2503
|
+
for (const key of PROCESS_LEVEL_KEYS) {
|
|
2504
|
+
if (options[key] !== void 0 && !sameValue(options[key], runtime.options[key])) {
|
|
2505
|
+
runtime.logger.warn(
|
|
2506
|
+
`op\xE7\xE3o "${key}" divergente ignorada \u2014 o primeiro register()/start() define as op\xE7\xF5es do processo`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return runtime;
|
|
2511
|
+
}
|
|
2512
|
+
const config = resolveConfig({ clientSecret: options.clientSecret });
|
|
2513
|
+
const logger = makeLogger(options.debug ?? false);
|
|
2514
|
+
const client = new Client(
|
|
2515
|
+
{ environment: options.environment, release: options.release, tags: options.tags, debug: options.debug },
|
|
2516
|
+
config
|
|
2517
|
+
);
|
|
2518
|
+
const api = new TasksApi({ baseUrl: config.tasksBaseUrl, key: config.key, logger });
|
|
2519
|
+
runtime = {
|
|
2520
|
+
options: { ...options, clientSecret: void 0 },
|
|
2521
|
+
config,
|
|
2522
|
+
client,
|
|
2523
|
+
logger,
|
|
2524
|
+
api,
|
|
2525
|
+
registry: new TaskRegistry(),
|
|
2526
|
+
tracker: new RunTracker(api),
|
|
2527
|
+
sampler: void 0,
|
|
2528
|
+
heartbeat: void 0,
|
|
2529
|
+
agent: void 0,
|
|
2530
|
+
locals: /* @__PURE__ */ new Map(),
|
|
2531
|
+
instanceId: resolveInstanceId(options.instanceId),
|
|
2532
|
+
pollIntervalMs: void 0,
|
|
2533
|
+
booted: false,
|
|
2534
|
+
bootScheduled: false,
|
|
2535
|
+
bootPromise: void 0,
|
|
2536
|
+
uninstallGlobalHandlers: options.captureUnhandled !== false ? installGlobalHandlers(client) : void 0,
|
|
2537
|
+
uninstallShutdown: void 0
|
|
2538
|
+
};
|
|
2539
|
+
runtime.options.clientSecret = options.clientSecret;
|
|
2540
|
+
logger.debug("inicializado", { endpoint: config.ingestEndpoint });
|
|
2541
|
+
return runtime;
|
|
2542
|
+
}
|
|
2543
|
+
function validateTask(slug, handler, fn) {
|
|
2544
|
+
if (typeof slug !== "string" || slug.trim().length === 0 || slug.length > MAX_SLUG_CHARS) {
|
|
2545
|
+
throw new Error(`[hobots] ${fn}(): o slug da task precisa ser uma string de 1 a ${MAX_SLUG_CHARS} caracteres`);
|
|
2285
2546
|
}
|
|
2286
|
-
|
|
2547
|
+
if (typeof handler !== "function") {
|
|
2548
|
+
throw new Error(`[hobots] ${fn}('${slug}'): o handler precisa ser uma fun\xE7\xE3o`);
|
|
2549
|
+
}
|
|
2550
|
+
return slug.trim();
|
|
2287
2551
|
}
|
|
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
|
-
|
|
2552
|
+
function clampConcurrency(value) {
|
|
2553
|
+
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
2554
|
+
return Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(value)));
|
|
2555
|
+
}
|
|
2556
|
+
function resolveTaskOptions(options, mode) {
|
|
2557
|
+
const retries = options.retries === void 0 || !Number.isFinite(options.retries) ? DEFAULT_RETRIES2 : Math.max(0, Math.floor(options.retries));
|
|
2558
|
+
return {
|
|
2559
|
+
retries,
|
|
2560
|
+
retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
|
|
2561
|
+
timeoutMs: options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : void 0,
|
|
2562
|
+
concurrency: mode === "remote" ? clampConcurrency(options.concurrency) : 1,
|
|
2563
|
+
logFlushIntervalMs: options.logFlushIntervalMs,
|
|
2564
|
+
cancelCheckIntervalMs: options.cancelCheckIntervalMs,
|
|
2565
|
+
cancelGraceMs: options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function instanceInfo(rt) {
|
|
2569
|
+
return {
|
|
2570
|
+
instance_id: rt.instanceId ?? "unknown",
|
|
2571
|
+
version: SDK_VERSION,
|
|
2572
|
+
host: hostname(),
|
|
2573
|
+
// ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
|
|
2574
|
+
environment: rt.options.environment,
|
|
2575
|
+
// TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
|
|
2576
|
+
tasks: rt.registry.allSlugs(),
|
|
2577
|
+
// o servidor calcula a tolerância de offline (3×) a partir disto em agentes que fazem
|
|
2578
|
+
// poll; clampado à faixa aceita pelo schema (1s..15min)
|
|
2579
|
+
poll_interval_ms: rt.agent ? Math.min(Math.max(rt.agent.pollIntervalMs, 1e3), 9e5) : void 0,
|
|
2580
|
+
// recursos da máquina — alimenta a série da instância (o servidor grava no máx. 1 ponto/30s)
|
|
2581
|
+
machine: rt.sampler?.sample()
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function ensureAgent(rt) {
|
|
2585
|
+
rt.agent ??= new TaskAgent({
|
|
2586
|
+
api: rt.api,
|
|
2587
|
+
client: rt.client,
|
|
2588
|
+
logger: rt.logger,
|
|
2589
|
+
registry: rt.registry,
|
|
2590
|
+
tracker: rt.tracker,
|
|
2591
|
+
instanceInfo: () => instanceInfo(rt),
|
|
2592
|
+
pollIntervalMs: rt.pollIntervalMs
|
|
2593
|
+
});
|
|
2594
|
+
return rt.agent;
|
|
2595
|
+
}
|
|
2596
|
+
function launchLocal(rt, task) {
|
|
2597
|
+
const runner = new LocalRunner(task, {
|
|
2598
|
+
api: rt.api,
|
|
2599
|
+
client: rt.client,
|
|
2600
|
+
logger: rt.logger,
|
|
2601
|
+
tracker: rt.tracker,
|
|
2602
|
+
instanceInfo: () => instanceInfo(rt)
|
|
2603
|
+
});
|
|
2604
|
+
rt.locals.set(task.slug, runner);
|
|
2605
|
+
void runner.run();
|
|
2606
|
+
}
|
|
2607
|
+
async function boot(rt) {
|
|
2608
|
+
if (!rt.instanceId) {
|
|
2609
|
+
const id = await fallbackInstanceId();
|
|
2610
|
+
if (runtime !== rt) return;
|
|
2611
|
+
rt.instanceId = id;
|
|
2612
|
+
rt.logger.debug(`instanceId n\xE3o informado (env ${INSTANCE_ID_ENV}) \u2014 usando o hostname "${id}"`);
|
|
2613
|
+
}
|
|
2614
|
+
if (runtime !== rt) return;
|
|
2615
|
+
rt.booted = true;
|
|
2616
|
+
if (rt.options.machineMetrics !== false) {
|
|
2617
|
+
rt.sampler = new MachineSampler(rt.instanceId, rt.logger);
|
|
2618
|
+
void rt.sampler.start();
|
|
2619
|
+
rt.client.attachMachineSampler(rt.sampler);
|
|
2620
|
+
}
|
|
2621
|
+
rt.heartbeat = new Heartbeat(rt.client, { intervalMs: HEARTBEAT_INTERVAL_MS, keepAlive: true }, rt.instanceId, rt.sampler);
|
|
2622
|
+
rt.heartbeat.start();
|
|
2623
|
+
if (rt.options.installExitHandlers !== false) {
|
|
2624
|
+
rt.uninstallShutdown = installShutdownHandlers(rt.tracker);
|
|
2625
|
+
}
|
|
2626
|
+
if (rt.registry.remoteTasks().length > 0) ensureAgent(rt).start();
|
|
2627
|
+
for (const task of rt.registry.localTasks()) launchLocal(rt, task);
|
|
2628
|
+
}
|
|
2629
|
+
function scheduleBoot(rt) {
|
|
2630
|
+
if (rt.bootScheduled) return;
|
|
2631
|
+
rt.bootScheduled = true;
|
|
2632
|
+
queueMicrotask(() => {
|
|
2633
|
+
rt.bootPromise = boot(rt).catch((error) => rt.logger.error("falha ao iniciar o SDK:", error));
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
function addTask(fn, mode, task, handler, options) {
|
|
2637
|
+
const slug = validateTask(task, handler, fn);
|
|
2638
|
+
const rt = ensureRuntime(options);
|
|
2639
|
+
if (mode === "remote" && options.pollIntervalMs !== void 0) {
|
|
2640
|
+
if (rt.pollIntervalMs === void 0 && !rt.agent) rt.pollIntervalMs = options.pollIntervalMs;
|
|
2641
|
+
else if (rt.pollIntervalMs !== options.pollIntervalMs) {
|
|
2642
|
+
rt.logger.warn('op\xE7\xE3o "pollIntervalMs" divergente ignorada \u2014 o primeiro register() define o intervalo do poll');
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const registered = { slug, mode, handler, options: resolveTaskOptions(options, mode) };
|
|
2646
|
+
rt.registry.add(registered);
|
|
2647
|
+
if (!rt.booted) {
|
|
2648
|
+
scheduleBoot(rt);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (mode === "remote") {
|
|
2652
|
+
const agent = ensureAgent(rt);
|
|
2653
|
+
agent.start();
|
|
2654
|
+
agent.wake();
|
|
2655
|
+
} else {
|
|
2656
|
+
launchLocal(rt, registered);
|
|
2331
2657
|
}
|
|
2332
|
-
client.logger.debug("inicializado", { endpoint: client.config.ingestEndpoint });
|
|
2333
2658
|
}
|
|
2334
|
-
function
|
|
2335
|
-
|
|
2659
|
+
function register(task, handler, options = {}) {
|
|
2660
|
+
addTask("register", "remote", task, handler, options);
|
|
2661
|
+
}
|
|
2662
|
+
function start(task, handler, options = {}) {
|
|
2663
|
+
addTask("start", "local", task, handler, options);
|
|
2664
|
+
}
|
|
2665
|
+
function currentReporter() {
|
|
2666
|
+
return currentRunReporter() ?? runtime?.tracker.singleActive();
|
|
2667
|
+
}
|
|
2668
|
+
function activeLogger() {
|
|
2669
|
+
return runtime?.logger;
|
|
2336
2670
|
}
|
|
2337
2671
|
function toRunLogLevel(type) {
|
|
2338
2672
|
return type === "fatal" ? "error" : type;
|
|
2339
2673
|
}
|
|
2340
|
-
function forwardAttachment(attachment, message, type) {
|
|
2341
|
-
const
|
|
2342
|
-
if (!
|
|
2343
|
-
|
|
2674
|
+
function forwardAttachment(rt, attachment, message, type) {
|
|
2675
|
+
const reporter = currentReporter();
|
|
2676
|
+
if (!reporter) {
|
|
2677
|
+
rt.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task");
|
|
2344
2678
|
return;
|
|
2345
2679
|
}
|
|
2346
2680
|
const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
|
|
2347
|
-
void attach(withMeta.image, {
|
|
2681
|
+
void reporter.attach(withMeta.image, {
|
|
2348
2682
|
message,
|
|
2349
2683
|
level: toRunLogLevel(type),
|
|
2350
2684
|
caption: withMeta.caption,
|
|
@@ -2352,84 +2686,115 @@ function forwardAttachment(attachment, message, type) {
|
|
|
2352
2686
|
});
|
|
2353
2687
|
}
|
|
2354
2688
|
function captureException(error, hint) {
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2689
|
+
const rt = runtime;
|
|
2690
|
+
if (!rt) {
|
|
2691
|
+
if (!warnedNoRuntime) {
|
|
2692
|
+
warnedNoRuntime = true;
|
|
2693
|
+
console.warn("[hobots] captureException antes de registrar uma task (register/start) \u2014 evento descartado");
|
|
2694
|
+
}
|
|
2695
|
+
return void 0;
|
|
2358
2696
|
}
|
|
2359
|
-
return client?.captureException(error, hint, scope);
|
|
2360
|
-
}
|
|
2361
|
-
function captureMessage(message, type = "warning", hint) {
|
|
2362
2697
|
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);
|
|
2698
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2699
|
+
forwardAttachment(rt, hint.attachment, message, hint.type ?? "error");
|
|
2700
|
+
}
|
|
2701
|
+
return rt.client.captureException(error, hint);
|
|
2702
|
+
}
|
|
2703
|
+
async function close(timeoutMs = 3e4) {
|
|
2704
|
+
const rt = runtime;
|
|
2705
|
+
if (!rt) return true;
|
|
2706
|
+
runtime = void 0;
|
|
2707
|
+
await rt.bootPromise;
|
|
2708
|
+
if (rt.agent) await rt.agent.stop(0);
|
|
2709
|
+
for (const local of rt.locals.values()) local.stop();
|
|
2710
|
+
const idle = await rt.tracker.waitIdle(timeoutMs);
|
|
2711
|
+
if (!idle) {
|
|
2712
|
+
rt.logger.warn(`${rt.tracker.activeCount} run(s) ainda em execu\xE7\xE3o ap\xF3s ${timeoutMs}ms \u2014 abandonando a espera`);
|
|
2713
|
+
await rt.tracker.reportAgentTerminated("agente encerrado durante a execu\xE7\xE3o (shutdown excedeu o tempo de espera)");
|
|
2714
|
+
}
|
|
2715
|
+
rt.heartbeat?.stop();
|
|
2716
|
+
rt.uninstallShutdown?.();
|
|
2717
|
+
rt.uninstallGlobalHandlers?.();
|
|
2718
|
+
const flushed = await rt.client.close(timeoutMs);
|
|
2719
|
+
return idle && flushed;
|
|
2427
2720
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2721
|
+
|
|
2722
|
+
// src/hobots.ts
|
|
2723
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2724
|
+
function warnOutsideRun(member) {
|
|
2725
|
+
if (warned.has(member)) return;
|
|
2726
|
+
warned.add(member);
|
|
2727
|
+
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)`;
|
|
2728
|
+
const logger = activeLogger();
|
|
2729
|
+
if (logger) logger.warn(message);
|
|
2730
|
+
else console.warn(`[hobots] ${message}`);
|
|
2731
|
+
}
|
|
2732
|
+
var FALLBACK_TASK = "hobots";
|
|
2733
|
+
var fallbackLog = {
|
|
2734
|
+
debug: (message) => printRunLog(FALLBACK_TASK, "debug", message),
|
|
2735
|
+
info: (message) => printRunLog(FALLBACK_TASK, "info", message),
|
|
2736
|
+
warn: (message) => printRunLog(FALLBACK_TASK, "warning", message),
|
|
2737
|
+
error: (message) => printRunLog(FALLBACK_TASK, "error", message),
|
|
2738
|
+
success: (message) => printRunLog(FALLBACK_TASK, "success", message)
|
|
2739
|
+
};
|
|
2740
|
+
var noop = () => void 0;
|
|
2741
|
+
var fallbackItems = { add: noop, succeeded: noop, failed: noop, occurrence: noop };
|
|
2742
|
+
var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
|
|
2743
|
+
var neverAborts = new AbortController().signal;
|
|
2744
|
+
var detachedTransaction;
|
|
2745
|
+
function resolve(member) {
|
|
2746
|
+
const reporter = currentReporter();
|
|
2747
|
+
if (reporter) return reporter.context;
|
|
2748
|
+
warnOutsideRun(member);
|
|
2749
|
+
return void 0;
|
|
2434
2750
|
}
|
|
2751
|
+
function getter(member, fallback) {
|
|
2752
|
+
return { enumerable: true, get: () => resolve(member)?.[member] ?? fallback() };
|
|
2753
|
+
}
|
|
2754
|
+
function method(value) {
|
|
2755
|
+
return { enumerable: true, writable: false, value };
|
|
2756
|
+
}
|
|
2757
|
+
var hobots = Object.defineProperties({}, {
|
|
2758
|
+
// ── contexto da run atual (getters) ─────────────────────────────────────────
|
|
2759
|
+
runId: getter("runId", () => ""),
|
|
2760
|
+
task: getter("task", () => ""),
|
|
2761
|
+
params: getter("params", () => ({})),
|
|
2762
|
+
log: getter("log", () => fallbackLog),
|
|
2763
|
+
items: getter("items", () => fallbackItems),
|
|
2764
|
+
progress: getter("progress", () => fallbackProgress),
|
|
2765
|
+
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
2766
|
+
signal: getter("signal", () => neverAborts),
|
|
2767
|
+
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
2768
|
+
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
2769
|
+
attach: method(
|
|
2770
|
+
(image, options) => resolve("attach")?.attach(image, options) ?? Promise.resolve({ id: "", ok: false, reason: "canceled" })
|
|
2771
|
+
),
|
|
2772
|
+
retry: method((fn, options) => {
|
|
2773
|
+
const reporter = currentReporter();
|
|
2774
|
+
return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
|
|
2775
|
+
}),
|
|
2776
|
+
// ── processo ────────────────────────────────────────────────────────────────
|
|
2777
|
+
register: method((task, handler, options) => {
|
|
2778
|
+
register(task, handler, options);
|
|
2779
|
+
installGlobalHobots();
|
|
2780
|
+
}),
|
|
2781
|
+
start: method((task, handler, options) => {
|
|
2782
|
+
start(task, handler, options);
|
|
2783
|
+
installGlobalHobots();
|
|
2784
|
+
}),
|
|
2785
|
+
close: method((timeoutMs) => close(timeoutMs)),
|
|
2786
|
+
captureException: method((error, hint) => captureException(error, hint))
|
|
2787
|
+
});
|
|
2788
|
+
function installGlobalHobots() {
|
|
2789
|
+
if (globalThis.hobots === hobots) return;
|
|
2790
|
+
Object.defineProperty(globalThis, "hobots", { value: hobots, configurable: true, writable: true, enumerable: false });
|
|
2791
|
+
}
|
|
2792
|
+
var register2 = hobots.register;
|
|
2793
|
+
var start2 = hobots.start;
|
|
2794
|
+
var close2 = hobots.close;
|
|
2795
|
+
var captureException2 = hobots.captureException;
|
|
2796
|
+
var retry = hobots.retry;
|
|
2797
|
+
|
|
2798
|
+
// src/index.ts
|
|
2799
|
+
var src_default = hobots;
|
|
2435
2800
|
//# sourceMappingURL=index.cjs.map
|