hobots 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/index.cjs +307 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -4
- package/dist/index.d.ts +33 -4
- package/dist/index.js +307 -7
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +41 -1
- package/dist/protocol.d.ts +41 -1
- package/docs/api-reference.md +21 -1
- package/docs/concepts.md +17 -0
- package/docs/getting-started.md +1 -0
- package/docs/protocol.md +60 -3
- package/docs/tasks.md +75 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData } from './protocol.cjs';
|
|
2
|
-
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
|
|
1
|
+
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.cjs';
|
|
2
|
+
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.cjs';
|
|
3
|
+
|
|
4
|
+
/** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
|
|
5
|
+
type AttachInput = Uint8Array | ArrayBuffer | string;
|
|
3
6
|
|
|
4
7
|
/** API oficial da Hobots — o SDK sempre fala com ela em produção. */
|
|
5
8
|
declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
@@ -224,17 +227,43 @@ interface InitOptions {
|
|
|
224
227
|
/** Itens por envelope antes de flush imediato. Default: 10 */
|
|
225
228
|
maxBatchSize?: number;
|
|
226
229
|
}
|
|
230
|
+
/** Imagem enviada junto de um capture* — com legenda e content-type opcionais. */
|
|
231
|
+
interface CaptureAttachment {
|
|
232
|
+
/** Bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node) */
|
|
233
|
+
image: AttachInput;
|
|
234
|
+
/** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
|
|
235
|
+
caption?: string;
|
|
236
|
+
/** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
|
|
237
|
+
contentType?: RunAttachmentContentType;
|
|
238
|
+
}
|
|
227
239
|
interface CaptureHint {
|
|
228
240
|
level?: Severity;
|
|
229
241
|
tags?: Record<string, string>;
|
|
230
242
|
extra?: Record<string, unknown>;
|
|
231
243
|
fingerprint?: string[];
|
|
244
|
+
/**
|
|
245
|
+
* Print da tela enviado junto da captura. Dentro de uma execução de task, a imagem
|
|
246
|
+
* vira anexo da linha de log da execução que espelha o evento — miniatura na aba
|
|
247
|
+
* "logs do agente". Fora de uma execução não há onde pendurar a imagem: o SDK
|
|
248
|
+
* descarta com um aviso. O upload roda em segundo plano e **nunca bloqueia nem lança**.
|
|
249
|
+
*
|
|
250
|
+
* PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
|
|
251
|
+
*
|
|
252
|
+
* ```ts
|
|
253
|
+
* Hobots.captureException(error, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
attachment?: AttachInput | CaptureAttachment;
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
/** Inicializa o SDK. Chame uma vez, o mais cedo possível no programa. */
|
|
235
260
|
declare function init(options: InitOptions): void;
|
|
236
261
|
declare function getClient(): Client | undefined;
|
|
237
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado).
|
|
264
|
+
* `hint.attachment` envia um print junto — dentro de uma execução de task, ele aparece
|
|
265
|
+
* como miniatura na linha de log da run correspondente ao erro.
|
|
266
|
+
*/
|
|
238
267
|
declare function captureException(error: unknown, hint?: CaptureHint): string | undefined;
|
|
239
268
|
declare function captureMessage(message: string, level?: Severity, hint?: CaptureHint): string | undefined;
|
|
240
269
|
/** Registra um breadcrumb — anexado aos próximos eventos como trilha do que aconteceu antes. */
|
|
@@ -267,4 +296,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
|
|
|
267
296
|
|
|
268
297
|
declare function scrubSecrets(text: string): string;
|
|
269
298
|
|
|
270
|
-
export { Breadcrumb, type BreadcrumbInput, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
|
299
|
+
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData } from './protocol.js';
|
|
2
|
-
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
|
|
1
|
+
import { Breadcrumb, EventUser, Severity, EventPayload, TransactionPayload, HeartbeatPayload, StepStatus, RunLogLevel, StepData, RunAttachmentContentType } from './protocol.js';
|
|
2
|
+
export { AgentInfo, Envelope, EnvelopeItem, ExceptionData, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, StackFrame } from './protocol.js';
|
|
3
|
+
|
|
4
|
+
/** Aceita tudo que o handler pode ter em mãos: bytes ou um caminho de arquivo (só Node). */
|
|
5
|
+
type AttachInput = Uint8Array | ArrayBuffer | string;
|
|
3
6
|
|
|
4
7
|
/** API oficial da Hobots — o SDK sempre fala com ela em produção. */
|
|
5
8
|
declare const DEFAULT_BASE_URL = "https://api2.hobots.app";
|
|
@@ -224,17 +227,43 @@ interface InitOptions {
|
|
|
224
227
|
/** Itens por envelope antes de flush imediato. Default: 10 */
|
|
225
228
|
maxBatchSize?: number;
|
|
226
229
|
}
|
|
230
|
+
/** Imagem enviada junto de um capture* — com legenda e content-type opcionais. */
|
|
231
|
+
interface CaptureAttachment {
|
|
232
|
+
/** Bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node) */
|
|
233
|
+
image: AttachInput;
|
|
234
|
+
/** Legenda curta, exibida sob a miniatura no app. Passa pelo scrub de segredos. */
|
|
235
|
+
caption?: string;
|
|
236
|
+
/** Informe quando os magic bytes não bastarem (o SDK sempre prefere o que detectar). */
|
|
237
|
+
contentType?: RunAttachmentContentType;
|
|
238
|
+
}
|
|
227
239
|
interface CaptureHint {
|
|
228
240
|
level?: Severity;
|
|
229
241
|
tags?: Record<string, string>;
|
|
230
242
|
extra?: Record<string, unknown>;
|
|
231
243
|
fingerprint?: string[];
|
|
244
|
+
/**
|
|
245
|
+
* Print da tela enviado junto da captura. Dentro de uma execução de task, a imagem
|
|
246
|
+
* vira anexo da linha de log da execução que espelha o evento — miniatura na aba
|
|
247
|
+
* "logs do agente". Fora de uma execução não há onde pendurar a imagem: o SDK
|
|
248
|
+
* descarta com um aviso. O upload roda em segundo plano e **nunca bloqueia nem lança**.
|
|
249
|
+
*
|
|
250
|
+
* PNG, JPEG ou WEBP, até 5 MB, no máximo 20 anexos por execução.
|
|
251
|
+
*
|
|
252
|
+
* ```ts
|
|
253
|
+
* Hobots.captureException(error, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
attachment?: AttachInput | CaptureAttachment;
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
/** Inicializa o SDK. Chame uma vez, o mais cedo possível no programa. */
|
|
235
260
|
declare function init(options: InitOptions): void;
|
|
236
261
|
declare function getClient(): Client | undefined;
|
|
237
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* Captura uma exceção e retorna o event_id (undefined se o SDK não foi inicializado).
|
|
264
|
+
* `hint.attachment` envia um print junto — dentro de uma execução de task, ele aparece
|
|
265
|
+
* como miniatura na linha de log da run correspondente ao erro.
|
|
266
|
+
*/
|
|
238
267
|
declare function captureException(error: unknown, hint?: CaptureHint): string | undefined;
|
|
239
268
|
declare function captureMessage(message: string, level?: Severity, hint?: CaptureHint): string | undefined;
|
|
240
269
|
/** Registra um breadcrumb — anexado aos próximos eventos como trilha do que aconteceu antes. */
|
|
@@ -267,4 +296,4 @@ declare function close(timeoutMs?: number): Promise<boolean>;
|
|
|
267
296
|
|
|
268
297
|
declare function scrubSecrets(text: string): string;
|
|
269
298
|
|
|
270
|
-
export { Breadcrumb, type BreadcrumbInput, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
|
299
|
+
export { type AttachInput, Breadcrumb, type BreadcrumbInput, type CaptureAttachment, type CaptureHint, Client, DEFAULT_BASE_URL, EventPayload, EventUser, type HeartbeatOptions, HeartbeatPayload, type InitOptions, RunAttachmentContentType, type RunContext, RunLogLevel, Scope, Severity, Step, StepData, type TaskHandler, type TasksOptions, Transaction, TransactionPayload, addBreadcrumb, captureException, captureMessage, close, flush, getClient, init, register, scrubSecrets, setContext, setExtra, setTag, setTags, setUser, start, startTransaction, stop, withScope };
|
package/dist/index.js
CHANGED
|
@@ -103,7 +103,7 @@ function scrubSecrets(text) {
|
|
|
103
103
|
|
|
104
104
|
// src/utils.ts
|
|
105
105
|
var SDK_NAME = "hobots";
|
|
106
|
-
var SDK_VERSION = true ? "1.
|
|
106
|
+
var SDK_VERSION = true ? "1.9.0" : "0.0.0-dev";
|
|
107
107
|
function isNode() {
|
|
108
108
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
109
109
|
}
|
|
@@ -270,6 +270,7 @@ var REQUEST_TIMEOUT_MS = 1e4;
|
|
|
270
270
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
271
271
|
var DEFAULT_MAX_BATCH_SIZE = 10;
|
|
272
272
|
var DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
273
|
+
var MAX_ITEMS_PER_ENVELOPE = 50;
|
|
273
274
|
var Transport = class {
|
|
274
275
|
endpoint;
|
|
275
276
|
key;
|
|
@@ -319,7 +320,15 @@ var Transport = class {
|
|
|
319
320
|
this.timer = void 0;
|
|
320
321
|
}
|
|
321
322
|
if (this.buffer.length === 0) return Promise.resolve();
|
|
322
|
-
const
|
|
323
|
+
const taken = this.buffer.splice(0, this.buffer.length);
|
|
324
|
+
const sends = [];
|
|
325
|
+
for (let offset = 0; offset < taken.length; offset += MAX_ITEMS_PER_ENVELOPE) {
|
|
326
|
+
sends.push(this.send(taken.slice(offset, offset + MAX_ITEMS_PER_ENVELOPE)));
|
|
327
|
+
}
|
|
328
|
+
return Promise.all(sends).then(() => void 0);
|
|
329
|
+
}
|
|
330
|
+
/** Registra o envio em `this.pending` para que `flush()` aguarde todos os envelopes. */
|
|
331
|
+
send(items) {
|
|
323
332
|
const envelope = {
|
|
324
333
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
325
334
|
sent_at: nowISO(),
|
|
@@ -351,7 +360,7 @@ var Transport = class {
|
|
|
351
360
|
this.logger.debug(`envelope enviado (${envelope.items.length} itens)`);
|
|
352
361
|
return;
|
|
353
362
|
}
|
|
354
|
-
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado`);
|
|
363
|
+
this.logger.error(`envelope rejeitado (HTTP ${response.status}), descartado (${envelope.items.length} itens)`);
|
|
355
364
|
}
|
|
356
365
|
/** Envia o que estiver na fila e aguarda envios em andamento (até timeoutMs). */
|
|
357
366
|
async flush(timeoutMs = 5e3) {
|
|
@@ -824,6 +833,8 @@ var Transaction = class {
|
|
|
824
833
|
// src/tasks/api.ts
|
|
825
834
|
var POLL_TIMEOUT_MS = 3e4;
|
|
826
835
|
var UPDATE_TIMEOUT_MS = 1e4;
|
|
836
|
+
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
837
|
+
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
827
838
|
var TasksApiError = class extends Error {
|
|
828
839
|
constructor(message, status) {
|
|
829
840
|
super(message);
|
|
@@ -918,8 +929,123 @@ var TasksApi = class {
|
|
|
918
929
|
signal: AbortSignal.timeout(timeoutMs)
|
|
919
930
|
});
|
|
920
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Registra o anexo e obtém a URL presignada de upload. 404/501/503 significam
|
|
934
|
+
* "esta instalação não tem anexos" — o RunReporter degrada em silêncio a partir daí.
|
|
935
|
+
*/
|
|
936
|
+
async createAttachment(runId, request, signal) {
|
|
937
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPDATE_TIMEOUT_MS, signal);
|
|
938
|
+
let response;
|
|
939
|
+
try {
|
|
940
|
+
response = await fetchWithRetry(
|
|
941
|
+
() => fetch(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments`, {
|
|
942
|
+
method: "POST",
|
|
943
|
+
headers: this.headers(),
|
|
944
|
+
body: JSON.stringify(request),
|
|
945
|
+
signal: timeoutSignal
|
|
946
|
+
}),
|
|
947
|
+
{ logger: this.logger, label: `anexo da run ${runId}` }
|
|
948
|
+
);
|
|
949
|
+
} finally {
|
|
950
|
+
cleanup();
|
|
951
|
+
}
|
|
952
|
+
if (!response.ok) {
|
|
953
|
+
throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
954
|
+
}
|
|
955
|
+
const body = await response.json();
|
|
956
|
+
if (typeof body?.upload_url !== "string") {
|
|
957
|
+
throw new TasksApiError("resposta inv\xE1lida do anexo (upload_url ausente)", 0);
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
id: body.id ?? request.id,
|
|
961
|
+
upload_url: body.upload_url,
|
|
962
|
+
headers: body.headers ?? { "content-type": request.content_type },
|
|
963
|
+
expires_in: body.expires_in ?? 900
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* PUT do binário direto no S3. Deliberadamente sem `authorization`: a autorização
|
|
968
|
+
* está na assinatura da URL, e mandar o client secret para fora da API seria vazamento.
|
|
969
|
+
*/
|
|
970
|
+
async uploadAttachment(url, headers, body, signal) {
|
|
971
|
+
const { signal: timeoutSignal, cleanup } = withTimeoutSignal(UPLOAD_TIMEOUT_MS, signal);
|
|
972
|
+
let response;
|
|
973
|
+
try {
|
|
974
|
+
response = await fetchWithRetry(
|
|
975
|
+
() => fetch(url, {
|
|
976
|
+
method: "PUT",
|
|
977
|
+
headers,
|
|
978
|
+
// BodyInit não aceita Uint8Array<ArrayBufferLike> em todos os lib.dom; o buffer é o mesmo
|
|
979
|
+
body,
|
|
980
|
+
signal: timeoutSignal
|
|
981
|
+
}),
|
|
982
|
+
{ delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
|
|
983
|
+
);
|
|
984
|
+
} finally {
|
|
985
|
+
cleanup();
|
|
986
|
+
}
|
|
987
|
+
if (!response.ok) {
|
|
988
|
+
throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/** Marca o anexo como enviado (ou falho). Best-effort — a leitura não depende disso. */
|
|
992
|
+
async confirmAttachment(runId, attachmentId, request) {
|
|
993
|
+
const response = await fetch(
|
|
994
|
+
`${this.baseUrl}/runs/${encodeURIComponent(runId)}/attachments/${encodeURIComponent(attachmentId)}`,
|
|
995
|
+
{
|
|
996
|
+
method: "PATCH",
|
|
997
|
+
headers: this.headers(),
|
|
998
|
+
body: JSON.stringify(request),
|
|
999
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
if (!response.ok) {
|
|
1003
|
+
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
921
1006
|
};
|
|
922
1007
|
|
|
1008
|
+
// src/tasks/attachments.ts
|
|
1009
|
+
var MAX_ATTACHMENT_BYTES = 5242880;
|
|
1010
|
+
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
1011
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
1012
|
+
var MAX_CAPTION_CHARS = 200;
|
|
1013
|
+
var ATTACHMENT_DRAIN_MS = 15e3;
|
|
1014
|
+
async function readImageFile(path) {
|
|
1015
|
+
if (!isNode()) {
|
|
1016
|
+
throw new Error("caminho de arquivo s\xF3 \xE9 suportado em Node");
|
|
1017
|
+
}
|
|
1018
|
+
const { readFile } = await import("fs/promises");
|
|
1019
|
+
return new Uint8Array(await readFile(path));
|
|
1020
|
+
}
|
|
1021
|
+
function startsWith(bytes, signature, offset = 0) {
|
|
1022
|
+
if (bytes.length < offset + signature.length) return false;
|
|
1023
|
+
return signature.every((byte, index) => bytes[offset + index] === byte);
|
|
1024
|
+
}
|
|
1025
|
+
var PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1026
|
+
var JPEG_MAGIC = [255, 216, 255];
|
|
1027
|
+
var RIFF_MAGIC = [82, 73, 70, 70];
|
|
1028
|
+
var WEBP_MAGIC = [87, 69, 66, 80];
|
|
1029
|
+
function sniffContentType(bytes) {
|
|
1030
|
+
if (startsWith(bytes, PNG_MAGIC)) return "image/png";
|
|
1031
|
+
if (startsWith(bytes, JPEG_MAGIC)) return "image/jpeg";
|
|
1032
|
+
if (startsWith(bytes, RIFF_MAGIC) && startsWith(bytes, WEBP_MAGIC, 8)) return "image/webp";
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
function readPngDimensions(bytes) {
|
|
1036
|
+
if (!startsWith(bytes, PNG_MAGIC) || bytes.length < 24) return void 0;
|
|
1037
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1038
|
+
const width = view.getUint32(16);
|
|
1039
|
+
const height = view.getUint32(20);
|
|
1040
|
+
if (width === 0 || height === 0) return void 0;
|
|
1041
|
+
return { width, height };
|
|
1042
|
+
}
|
|
1043
|
+
async function toBytes(input) {
|
|
1044
|
+
if (typeof input === "string") return readImageFile(input);
|
|
1045
|
+
if (input instanceof Uint8Array) return input;
|
|
1046
|
+
return new Uint8Array(input);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
923
1049
|
// src/tasks/context.ts
|
|
924
1050
|
var LOG_LEVEL_COLOR = {
|
|
925
1051
|
debug: "gray",
|
|
@@ -1001,6 +1127,14 @@ var RunReporter = class {
|
|
|
1001
1127
|
abortController = new AbortController();
|
|
1002
1128
|
flushIntervalMs;
|
|
1003
1129
|
cancelCheckIntervalMs;
|
|
1130
|
+
// Uploads de anexo vivem FORA de `this.chain`: a cadeia é um FIFO puro de
|
|
1131
|
+
// status/logs e não pode ficar bloqueada esperando um binário subir.
|
|
1132
|
+
pendingAttachments = /* @__PURE__ */ new Set();
|
|
1133
|
+
attachmentCount = 0;
|
|
1134
|
+
uploadSlots = MAX_CONCURRENT_UPLOADS;
|
|
1135
|
+
uploadQueue = [];
|
|
1136
|
+
/** Uma vez que a instalação recusa anexos, para de tentar (e de avisar) nesta run. */
|
|
1137
|
+
attachmentsUnsupported = false;
|
|
1004
1138
|
get isDead() {
|
|
1005
1139
|
return this.dead;
|
|
1006
1140
|
}
|
|
@@ -1019,6 +1153,8 @@ var RunReporter = class {
|
|
|
1019
1153
|
this.dead = true;
|
|
1020
1154
|
this.clearFlushTimer();
|
|
1021
1155
|
this.buffer = [];
|
|
1156
|
+
const waiting = this.uploadQueue.splice(0, this.uploadQueue.length);
|
|
1157
|
+
for (const resume of waiting) resume();
|
|
1022
1158
|
}
|
|
1023
1159
|
/**
|
|
1024
1160
|
* Drena o buffer pendente para o PATCH único do exit path (chame ANTES de kill,
|
|
@@ -1036,15 +1172,22 @@ var RunReporter = class {
|
|
|
1036
1172
|
this.push(level, scrubbed, true);
|
|
1037
1173
|
}
|
|
1038
1174
|
/** Bufferiza uma entrada de log sem eco no terminal (cap, truncate, flush). */
|
|
1039
|
-
push(level, message, alreadyScrubbed = false) {
|
|
1040
|
-
|
|
1175
|
+
push(level, message, alreadyScrubbed = false, attachments) {
|
|
1176
|
+
const ts = nowISO();
|
|
1177
|
+
if (this.dead) return ts;
|
|
1041
1178
|
const scrubbed = alreadyScrubbed ? message : scrubSecrets(message);
|
|
1042
|
-
this.buffer.push({
|
|
1179
|
+
this.buffer.push({
|
|
1180
|
+
ts,
|
|
1181
|
+
level,
|
|
1182
|
+
message: truncate(scrubbed, MAX_LOG_MESSAGE_CHARS),
|
|
1183
|
+
...attachments && attachments.length > 0 ? { attachments } : {}
|
|
1184
|
+
});
|
|
1043
1185
|
if (this.buffer.length >= FLUSH_AT_BUFFERED) {
|
|
1044
1186
|
this.flushLogs();
|
|
1045
1187
|
} else {
|
|
1046
1188
|
this.scheduleFlush();
|
|
1047
1189
|
}
|
|
1190
|
+
return ts;
|
|
1048
1191
|
}
|
|
1049
1192
|
scheduleFlush() {
|
|
1050
1193
|
if (this.timer) return;
|
|
@@ -1112,6 +1255,131 @@ var RunReporter = class {
|
|
|
1112
1255
|
finalize() {
|
|
1113
1256
|
return this.chain;
|
|
1114
1257
|
}
|
|
1258
|
+
// ── Anexos ───────────────────────────────────────────────────────────────────
|
|
1259
|
+
/**
|
|
1260
|
+
* Registra a linha de log com o id do anexo (síncrono, na ordem certa) e dispara
|
|
1261
|
+
* o upload em segundo plano. Nunca lança: todo caminho de erro vira `{ ok: false }`
|
|
1262
|
+
* com um aviso no log da run. Chega ao handler pelos capture* do SDK — o hint
|
|
1263
|
+
* `attachment` de Hobots.captureException/captureMessage cai aqui via run-context-store.
|
|
1264
|
+
*/
|
|
1265
|
+
async attach(image, options = {}) {
|
|
1266
|
+
const id = uuid4();
|
|
1267
|
+
const level = options.level ?? "info";
|
|
1268
|
+
const caption = options.caption ? truncate(scrubSecrets(options.caption), MAX_CAPTION_CHARS) : void 0;
|
|
1269
|
+
const message = options.message ?? options.caption ?? "captura de tela";
|
|
1270
|
+
if (this.dead) return { id, ok: false, reason: "canceled" };
|
|
1271
|
+
if (this.attachmentsUnsupported) {
|
|
1272
|
+
this.log(level, message);
|
|
1273
|
+
return { id, ok: false, reason: "unsupported" };
|
|
1274
|
+
}
|
|
1275
|
+
if (this.attachmentCount >= MAX_ATTACHMENTS_PER_RUN) {
|
|
1276
|
+
this.log(level, message);
|
|
1277
|
+
this.log("warning", `anexo descartado: limite de ${MAX_ATTACHMENTS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
1278
|
+
return { id, ok: false, reason: "quota" };
|
|
1279
|
+
}
|
|
1280
|
+
this.attachmentCount++;
|
|
1281
|
+
const scrubbed = scrubSecrets(message);
|
|
1282
|
+
printRunLog(this.context.task, level, `${scrubbed} [imagem]`);
|
|
1283
|
+
const logTs = this.push(level, scrubbed, true, [id]);
|
|
1284
|
+
let settle;
|
|
1285
|
+
const result = new Promise((resolve) => {
|
|
1286
|
+
settle = resolve;
|
|
1287
|
+
});
|
|
1288
|
+
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" }));
|
|
1289
|
+
this.pendingAttachments.add(work);
|
|
1290
|
+
void work.finally(() => this.pendingAttachments.delete(work));
|
|
1291
|
+
return result;
|
|
1292
|
+
}
|
|
1293
|
+
/** Retorna o motivo da falha, ou undefined em sucesso. */
|
|
1294
|
+
async uploadAttachment(id, image, meta) {
|
|
1295
|
+
let bytes;
|
|
1296
|
+
try {
|
|
1297
|
+
bytes = await toBytes(image);
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
this.log("warning", `anexo n\xE3o p\xF4de ser lido: ${String(error)}`);
|
|
1300
|
+
return "read-failed";
|
|
1301
|
+
}
|
|
1302
|
+
const contentType = sniffContentType(bytes) ?? meta.contentType;
|
|
1303
|
+
if (!contentType) {
|
|
1304
|
+
this.log("warning", "anexo descartado: formato n\xE3o reconhecido (aceitos: PNG, JPEG, WEBP)");
|
|
1305
|
+
return "unsupported-format";
|
|
1306
|
+
}
|
|
1307
|
+
if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
1308
|
+
this.log(
|
|
1309
|
+
"warning",
|
|
1310
|
+
`anexo descartado: ${Math.round(bytes.byteLength / 1024)} KB excede o limite de ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB`
|
|
1311
|
+
);
|
|
1312
|
+
return "too-large";
|
|
1313
|
+
}
|
|
1314
|
+
const dimensions = readPngDimensions(bytes);
|
|
1315
|
+
const release = await this.acquireUploadSlot();
|
|
1316
|
+
try {
|
|
1317
|
+
if (this.abortController.signal.aborted || this.dead) return "canceled";
|
|
1318
|
+
const created = await this.api.createAttachment(
|
|
1319
|
+
this.runId,
|
|
1320
|
+
{
|
|
1321
|
+
id,
|
|
1322
|
+
content_type: contentType,
|
|
1323
|
+
size_bytes: bytes.byteLength,
|
|
1324
|
+
log_ts: meta.logTs,
|
|
1325
|
+
...meta.caption ? { caption: meta.caption } : {},
|
|
1326
|
+
...dimensions ?? {}
|
|
1327
|
+
},
|
|
1328
|
+
this.abortController.signal
|
|
1329
|
+
);
|
|
1330
|
+
await this.api.uploadAttachment(created.upload_url, created.headers, bytes, this.abortController.signal);
|
|
1331
|
+
await this.api.confirmAttachment(this.runId, id, { status: "uploaded", size_bytes: bytes.byteLength, ...dimensions ?? {} }).catch(() => {
|
|
1332
|
+
});
|
|
1333
|
+
return void 0;
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
if (this.isUnsupported(error)) {
|
|
1336
|
+
this.attachmentsUnsupported = true;
|
|
1337
|
+
this.log("warning", "anexos n\xE3o est\xE3o habilitados nesta instala\xE7\xE3o do Hobots \u2014 imagem n\xE3o enviada");
|
|
1338
|
+
return "unsupported";
|
|
1339
|
+
}
|
|
1340
|
+
if (error instanceof TasksApiError && error.status === 429) {
|
|
1341
|
+
this.log("warning", "anexo descartado: limite de anexos por execu\xE7\xE3o atingido no servidor");
|
|
1342
|
+
return "quota";
|
|
1343
|
+
}
|
|
1344
|
+
if (this.abortController.signal.aborted) return "canceled";
|
|
1345
|
+
this.log("warning", `falha ao enviar o anexo: ${String(error)}`);
|
|
1346
|
+
void this.api.confirmAttachment(this.runId, id, { status: "failed" }).catch(() => {
|
|
1347
|
+
});
|
|
1348
|
+
return "upload-failed";
|
|
1349
|
+
} finally {
|
|
1350
|
+
release();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/** 404 = API anterior ao recurso; 501/503 = instalação sem S3 configurado. */
|
|
1354
|
+
isUnsupported(error) {
|
|
1355
|
+
return error instanceof TasksApiError && (error.status === 404 || error.status === 501 || error.status === 503);
|
|
1356
|
+
}
|
|
1357
|
+
/** Semáforo simples: no máximo MAX_CONCURRENT_UPLOADS binários em voo por run. */
|
|
1358
|
+
acquireUploadSlot() {
|
|
1359
|
+
const release = () => {
|
|
1360
|
+
const next = this.uploadQueue.shift();
|
|
1361
|
+
if (next) next();
|
|
1362
|
+
else this.uploadSlots++;
|
|
1363
|
+
};
|
|
1364
|
+
if (this.uploadSlots > 0) {
|
|
1365
|
+
this.uploadSlots--;
|
|
1366
|
+
return Promise.resolve(release);
|
|
1367
|
+
}
|
|
1368
|
+
return new Promise((resolve) => {
|
|
1369
|
+
this.uploadQueue.push(() => resolve(release));
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Espera os uploads pendentes antes do PATCH terminal, para a run não terminar
|
|
1374
|
+
* exibindo miniaturas em "enviando…". Capado: um upload lento não segura a run.
|
|
1375
|
+
*/
|
|
1376
|
+
async finalizeAttachments(timeoutMs = ATTACHMENT_DRAIN_MS) {
|
|
1377
|
+
if (this.pendingAttachments.size === 0) return;
|
|
1378
|
+
await Promise.race([
|
|
1379
|
+
Promise.allSettled([...this.pendingAttachments]),
|
|
1380
|
+
new Promise((resolve) => unrefTimer(setTimeout(resolve, timeoutMs)))
|
|
1381
|
+
]);
|
|
1382
|
+
}
|
|
1115
1383
|
enqueue(update) {
|
|
1116
1384
|
if (this.dead) {
|
|
1117
1385
|
this.salvageLogs(update.logs);
|
|
@@ -1179,6 +1447,9 @@ async function runWithRunContext(store, fn) {
|
|
|
1179
1447
|
function currentRunTransaction() {
|
|
1180
1448
|
return storage?.getStore()?.tx;
|
|
1181
1449
|
}
|
|
1450
|
+
function currentRunAttach() {
|
|
1451
|
+
return storage?.getStore()?.attach;
|
|
1452
|
+
}
|
|
1182
1453
|
|
|
1183
1454
|
// src/tasks/shutdown.ts
|
|
1184
1455
|
var WATCHED_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
@@ -1420,12 +1691,17 @@ var TaskAgent = class {
|
|
|
1420
1691
|
}
|
|
1421
1692
|
this.logger.debug(`executando run ${run.id} (${run.task})`);
|
|
1422
1693
|
try {
|
|
1423
|
-
await runWithRunContext(
|
|
1694
|
+
await runWithRunContext(
|
|
1695
|
+
{ runId: run.id, tx, attach: (image, options) => reporter.attach(image, options) },
|
|
1696
|
+
() => this.invokeWithTimeout(handler, run, reporter)
|
|
1697
|
+
);
|
|
1698
|
+
await reporter.finalizeAttachments();
|
|
1424
1699
|
tx.setStatus("ok");
|
|
1425
1700
|
tx.finish();
|
|
1426
1701
|
reporter.reportSuccess();
|
|
1427
1702
|
this.logger.debug(`run ${run.id} conclu\xEDda`);
|
|
1428
1703
|
} catch (error) {
|
|
1704
|
+
await reporter.finalizeAttachments();
|
|
1429
1705
|
tx.setStatus("error");
|
|
1430
1706
|
tx.finish();
|
|
1431
1707
|
reporter.reportFailure(toRunError(error));
|
|
@@ -1508,10 +1784,34 @@ function init(options) {
|
|
|
1508
1784
|
function getClient() {
|
|
1509
1785
|
return client;
|
|
1510
1786
|
}
|
|
1787
|
+
function toRunLogLevel(level) {
|
|
1788
|
+
return level === "fatal" ? "error" : level;
|
|
1789
|
+
}
|
|
1790
|
+
function forwardAttachment(attachment, message, level) {
|
|
1791
|
+
const attach = currentRunAttach();
|
|
1792
|
+
if (!attach) {
|
|
1793
|
+
client?.logger.warn("attachment descartado \u2014 s\xF3 \xE9 suportado dentro de uma execu\xE7\xE3o de task (register/start)");
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const withMeta = typeof attachment === "object" && "image" in attachment ? attachment : { image: attachment };
|
|
1797
|
+
void attach(withMeta.image, {
|
|
1798
|
+
message,
|
|
1799
|
+
level: toRunLogLevel(level),
|
|
1800
|
+
caption: withMeta.caption,
|
|
1801
|
+
contentType: withMeta.contentType
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1511
1804
|
function captureException(error, hint) {
|
|
1805
|
+
if (hint?.attachment) {
|
|
1806
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
1807
|
+
forwardAttachment(hint.attachment, message, hint.level ?? "error");
|
|
1808
|
+
}
|
|
1512
1809
|
return client?.captureException(error, hint, scope);
|
|
1513
1810
|
}
|
|
1514
1811
|
function captureMessage(message, level = "info", hint) {
|
|
1812
|
+
if (hint?.attachment) {
|
|
1813
|
+
forwardAttachment(hint.attachment, message, hint.level ?? level);
|
|
1814
|
+
}
|
|
1515
1815
|
return client?.captureMessage(message, level, hint, scope);
|
|
1516
1816
|
}
|
|
1517
1817
|
function addBreadcrumb(crumb) {
|