hobots 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/dist/{chunk-5WIIACWC.js → chunk-HKCDMUA3.js} +1 -2
- package/dist/chunk-HKCDMUA3.js.map +1 -0
- package/dist/index.cjs +438 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -3
- package/dist/index.d.ts +64 -3
- package/dist/index.js +439 -31
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +0 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +39 -3
- package/dist/protocol.d.ts +39 -3
- package/dist/protocol.js +1 -1
- package/docs/api-reference.md +4 -1
- package/docs/protocol.md +12 -2
- package/docs/runs.md +60 -2
- package/package.json +1 -1
- package/dist/chunk-5WIIACWC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(src_exports, {
|
|
|
35
35
|
default: () => src_default,
|
|
36
36
|
hobots: () => hobots,
|
|
37
37
|
image: () => image2,
|
|
38
|
+
isFileRef: () => isFileRef2,
|
|
38
39
|
register: () => register2,
|
|
39
40
|
retry: () => retry,
|
|
40
41
|
start: () => start2
|
|
@@ -96,7 +97,7 @@ function scrubSecrets(text) {
|
|
|
96
97
|
|
|
97
98
|
// src/utils.ts
|
|
98
99
|
var SDK_NAME = "hobots";
|
|
99
|
-
var SDK_VERSION = true ? "4.
|
|
100
|
+
var SDK_VERSION = true ? "4.2.0" : "0.0.0-dev";
|
|
100
101
|
function isNode() {
|
|
101
102
|
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
102
103
|
}
|
|
@@ -178,13 +179,13 @@ async function fetchWithRetry(send, options) {
|
|
|
178
179
|
} catch (error) {
|
|
179
180
|
retriable = error;
|
|
180
181
|
}
|
|
182
|
+
const failure = retriable instanceof Error ? retriable : new Error(String(retriable));
|
|
181
183
|
const base = delays[attempt];
|
|
182
|
-
if (base === void 0)
|
|
183
|
-
throw retriable instanceof Error ? retriable : new Error(String(retriable));
|
|
184
|
-
}
|
|
184
|
+
if (base === void 0 || options.signal?.aborted) throw failure;
|
|
185
185
|
const delay = serverDelay ?? jitter(base);
|
|
186
186
|
options.logger.warn(`${options.label}: falha (${String(retriable)}), retry em ${delay}ms`);
|
|
187
|
-
await sleep(delay);
|
|
187
|
+
await (options.signal ? abortableSleep(delay, options.signal) : sleep(delay));
|
|
188
|
+
if (options.signal?.aborted) throw failure;
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
@@ -281,7 +282,6 @@ var protocol_limits_default = {
|
|
|
281
282
|
MAX_LOGS_PER_PATCH: 100,
|
|
282
283
|
MAX_ERROR_MESSAGE_CHARS: 1e4,
|
|
283
284
|
MAX_ERROR_STACK_CHARS: 2e4,
|
|
284
|
-
MAX_ITEMS_PER_RUN: 1e4,
|
|
285
285
|
MAX_ITEMS_PER_REQUEST: 500,
|
|
286
286
|
MAX_ITEM_MESSAGE_CHARS: 2e3,
|
|
287
287
|
MAX_ITEM_PAYLOAD_BYTES: 16384,
|
|
@@ -897,6 +897,7 @@ var ATTACHMENT_TERMINAL_STATUSES = [429, 501, 503];
|
|
|
897
897
|
var ITEMS_TERMINAL_STATUSES = [429];
|
|
898
898
|
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
899
899
|
var UPLOAD_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
900
|
+
var FILE_RETRY_DELAYS_MS = [1e3, 4e3];
|
|
900
901
|
var TasksApiError = class extends Error {
|
|
901
902
|
constructor(message, status) {
|
|
902
903
|
super(message);
|
|
@@ -1084,7 +1085,13 @@ var TasksApi = class {
|
|
|
1084
1085
|
signal: timeoutSignal
|
|
1085
1086
|
})
|
|
1086
1087
|
),
|
|
1087
|
-
{
|
|
1088
|
+
{
|
|
1089
|
+
logger: this.logger,
|
|
1090
|
+
label: `anexo da run ${runId}`,
|
|
1091
|
+
noRetryStatuses: ATTACHMENT_TERMINAL_STATUSES,
|
|
1092
|
+
// run abortada (timeout, 409, kill): o create em voo rejeita por abort e não entra na ladder
|
|
1093
|
+
signal
|
|
1094
|
+
}
|
|
1088
1095
|
);
|
|
1089
1096
|
if (!response.ok) {
|
|
1090
1097
|
throw new TasksApiError(`cria\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
@@ -1118,7 +1125,7 @@ var TasksApi = class {
|
|
|
1118
1125
|
signal: timeoutSignal
|
|
1119
1126
|
})
|
|
1120
1127
|
),
|
|
1121
|
-
{ delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo" }
|
|
1128
|
+
{ delays: UPLOAD_RETRY_DELAYS_MS, logger: this.logger, label: "upload de anexo", signal }
|
|
1122
1129
|
);
|
|
1123
1130
|
if (!response.ok) {
|
|
1124
1131
|
throw new TasksApiError(`upload de anexo rejeitado (HTTP ${response.status})`, response.status);
|
|
@@ -1140,6 +1147,74 @@ var TasksApi = class {
|
|
|
1140
1147
|
throw new TasksApiError(`confirma\xE7\xE3o de anexo rejeitada (HTTP ${response.status})`, response.status);
|
|
1141
1148
|
}
|
|
1142
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Pede a URL pré-assinada de GET de um arquivo do formulário. 404 (não existe nesta run)
|
|
1152
|
+
* e 410 (já baixado/expirado) viram `TasksApiError` com o status — quem chama traduz.
|
|
1153
|
+
*/
|
|
1154
|
+
async requestFileDownload(runId, fileId, signal) {
|
|
1155
|
+
const response = await fetchWithRetry(
|
|
1156
|
+
() => withRequestTimeout(
|
|
1157
|
+
UPDATE_TIMEOUT_MS,
|
|
1158
|
+
signal,
|
|
1159
|
+
(timeoutSignal) => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}/download`, {
|
|
1160
|
+
redirect: "error",
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
headers: this.headers(),
|
|
1163
|
+
signal: timeoutSignal
|
|
1164
|
+
})
|
|
1165
|
+
),
|
|
1166
|
+
{ delays: FILE_RETRY_DELAYS_MS, logger: this.logger, label: `download do arquivo ${fileId}`, signal }
|
|
1167
|
+
);
|
|
1168
|
+
if (!response.ok) {
|
|
1169
|
+
throw new TasksApiError(`download de arquivo rejeitado (HTTP ${response.status})`, response.status);
|
|
1170
|
+
}
|
|
1171
|
+
const body = await response.json().catch(() => null);
|
|
1172
|
+
if (typeof body?.url !== "string") {
|
|
1173
|
+
throw new TasksApiError("resposta inv\xE1lida do download de arquivo (url ausente)", 0);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
url: body.url,
|
|
1177
|
+
expires_in: typeof body.expires_in === "number" ? body.expires_in : 0,
|
|
1178
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
1179
|
+
size: typeof body.size === "number" ? body.size : -1,
|
|
1180
|
+
content_type: typeof body.content_type === "string" ? body.content_type : ""
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* GET do binário direto no S3 — deliberadamente sem `authorization` (a autorização
|
|
1185
|
+
* está na assinatura da URL; mandar o client secret para fora da API seria vazamento).
|
|
1186
|
+
* Devolve a Response com o corpo ainda não lido: quem chama faz o streaming para o disco.
|
|
1187
|
+
*/
|
|
1188
|
+
async fetchFile(url, signal) {
|
|
1189
|
+
const response = await fetchWithRetry(() => fetch(url, { redirect: "error", method: "GET", signal }), {
|
|
1190
|
+
delays: FILE_RETRY_DELAYS_MS,
|
|
1191
|
+
logger: this.logger,
|
|
1192
|
+
label: "download de arquivo",
|
|
1193
|
+
signal
|
|
1194
|
+
});
|
|
1195
|
+
if (!response.ok) {
|
|
1196
|
+
throw new TasksApiError(`download de arquivo falhou (HTTP ${response.status})`, response.status);
|
|
1197
|
+
}
|
|
1198
|
+
return response;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Confirma o download: o servidor apaga o objeto do S3 na hora. Idempotente (204);
|
|
1202
|
+
* 404/410 também significam "não há mais nada a apagar" e contam como confirmado.
|
|
1203
|
+
*/
|
|
1204
|
+
async confirmFileDownload(runId, fileId) {
|
|
1205
|
+
const response = await fetchWithRetry(
|
|
1206
|
+
() => fetch(`${this.baseUrl}/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
|
|
1207
|
+
redirect: "error",
|
|
1208
|
+
method: "DELETE",
|
|
1209
|
+
headers: this.headers(),
|
|
1210
|
+
signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS)
|
|
1211
|
+
}),
|
|
1212
|
+
{ delays: FILE_RETRY_DELAYS_MS, logger: this.logger, label: `confirma\xE7\xE3o do arquivo ${fileId}` }
|
|
1213
|
+
);
|
|
1214
|
+
if (!response.ok && response.status !== 404 && response.status !== 410) {
|
|
1215
|
+
throw new TasksApiError(`confirma\xE7\xE3o de download rejeitada (HTTP ${response.status})`, response.status);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1143
1218
|
};
|
|
1144
1219
|
|
|
1145
1220
|
// src/tasks/backoff.ts
|
|
@@ -1274,6 +1349,255 @@ async function eachItem(list, handler, options, sink) {
|
|
|
1274
1349
|
return result;
|
|
1275
1350
|
}
|
|
1276
1351
|
|
|
1352
|
+
// src/tasks/files.ts
|
|
1353
|
+
var LOCAL_FILES_DIR_ENV = "HOBOTS_LOCAL_FILES_DIR";
|
|
1354
|
+
var FILE_DOWNLOAD_IDLE_TIMEOUT_MS = 6e4;
|
|
1355
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1356
|
+
function isFileRef(value) {
|
|
1357
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1358
|
+
const ref = value;
|
|
1359
|
+
return typeof ref.id === "string" && typeof ref.name === "string" && typeof ref.size === "number" && typeof ref.contentType === "string" && (ref.storage === "s3" || ref.storage === "local");
|
|
1360
|
+
}
|
|
1361
|
+
function fileField(params, field) {
|
|
1362
|
+
if (!Object.prototype.hasOwnProperty.call(params, field)) return void 0;
|
|
1363
|
+
const value = params[field];
|
|
1364
|
+
if (isFileRef(value)) return value;
|
|
1365
|
+
if (Array.isArray(value) && value.every(isFileRef)) return value;
|
|
1366
|
+
return void 0;
|
|
1367
|
+
}
|
|
1368
|
+
function collectFileRefs(params) {
|
|
1369
|
+
const refs = [];
|
|
1370
|
+
for (const field of Object.keys(params)) {
|
|
1371
|
+
const value = fileField(params, field);
|
|
1372
|
+
if (value !== void 0) refs.push(...Array.isArray(value) ? value : [value]);
|
|
1373
|
+
}
|
|
1374
|
+
return refs;
|
|
1375
|
+
}
|
|
1376
|
+
function safeFileName(name, fallback) {
|
|
1377
|
+
const cleaned = name.replace(/^.*[\\/]/, "").replace(/[\u0000-\u001f<>:"|?*]/g, "_").trim();
|
|
1378
|
+
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
1379
|
+
}
|
|
1380
|
+
function isInsideDir(root, candidate, sep) {
|
|
1381
|
+
const base = root.endsWith(sep) ? root : root + sep;
|
|
1382
|
+
return candidate.startsWith(base) && candidate.length > base.length;
|
|
1383
|
+
}
|
|
1384
|
+
function unavailableLocal(ref) {
|
|
1385
|
+
return new Error(`arquivo "${ref.name}" enviado pelo Studio de outro computador ou j\xE1 consumido`);
|
|
1386
|
+
}
|
|
1387
|
+
function errorCode(error) {
|
|
1388
|
+
return error?.code;
|
|
1389
|
+
}
|
|
1390
|
+
var RunFileStore = class {
|
|
1391
|
+
constructor(deps) {
|
|
1392
|
+
this.deps = deps;
|
|
1393
|
+
this.files = {
|
|
1394
|
+
list: () => collectFileRefs(deps.params),
|
|
1395
|
+
get: (field) => fileField(deps.params, field),
|
|
1396
|
+
// async: erro de uso (campo sem arquivo, valor inválido) chega como Promise rejeitada, nunca síncrono
|
|
1397
|
+
download: async (refOrField, options) => this.download(this.single(refOrField), options?.dest),
|
|
1398
|
+
downloadAll: async (field, options) => this.downloadAll(field, options?.dest),
|
|
1399
|
+
read: async (refOrField) => this.read(this.single(refOrField))
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
deps;
|
|
1403
|
+
files;
|
|
1404
|
+
tempDir;
|
|
1405
|
+
/** Apaga a pasta temporária da execução (cópias que o bot não levou para `dest`). Nunca lança. */
|
|
1406
|
+
async cleanup() {
|
|
1407
|
+
const pending = this.tempDir;
|
|
1408
|
+
this.tempDir = void 0;
|
|
1409
|
+
if (!pending) return;
|
|
1410
|
+
try {
|
|
1411
|
+
const { rm } = await import("fs/promises");
|
|
1412
|
+
await rm(await pending, { recursive: true, force: true });
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
field(field) {
|
|
1417
|
+
const value = fileField(this.deps.params, field);
|
|
1418
|
+
if (value === void 0) throw new Error(`campo "${field}" n\xE3o tem arquivo nos params`);
|
|
1419
|
+
return value;
|
|
1420
|
+
}
|
|
1421
|
+
single(refOrField) {
|
|
1422
|
+
if (typeof refOrField === "string") {
|
|
1423
|
+
const value = this.field(refOrField);
|
|
1424
|
+
if (Array.isArray(value)) {
|
|
1425
|
+
throw new Error(`campo "${refOrField}" tem v\xE1rios arquivos \u2014 use downloadAll("${refOrField}") ou passe um item da lista`);
|
|
1426
|
+
}
|
|
1427
|
+
return value;
|
|
1428
|
+
}
|
|
1429
|
+
if (!isFileRef(refOrField)) throw new TypeError("esperado um FileRef ou o nome de um campo de arquivo");
|
|
1430
|
+
return refOrField;
|
|
1431
|
+
}
|
|
1432
|
+
async downloadAll(field, dest) {
|
|
1433
|
+
const value = this.field(field);
|
|
1434
|
+
const refs = Array.isArray(value) ? value : [value];
|
|
1435
|
+
const paths = [];
|
|
1436
|
+
for (const ref of refs) paths.push(await this.download(ref, dest, true));
|
|
1437
|
+
return paths;
|
|
1438
|
+
}
|
|
1439
|
+
async read(ref) {
|
|
1440
|
+
const { readFile, rm } = await import("fs/promises");
|
|
1441
|
+
const { dirname } = await import("path");
|
|
1442
|
+
const path = await this.download(ref, void 0);
|
|
1443
|
+
try {
|
|
1444
|
+
return await readFile(path);
|
|
1445
|
+
} finally {
|
|
1446
|
+
await rm(dirname(path), { recursive: true, force: true });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async download(ref, dest, destIsDir = false) {
|
|
1450
|
+
this.checkCanceled();
|
|
1451
|
+
const target = await this.resolveTarget(ref, dest, destIsDir);
|
|
1452
|
+
if (ref.storage === "local") await this.copyLocal(ref, target);
|
|
1453
|
+
else await this.downloadS3(ref, target);
|
|
1454
|
+
return target;
|
|
1455
|
+
}
|
|
1456
|
+
checkCanceled() {
|
|
1457
|
+
if (this.deps.signal.aborted) throw runCanceledError();
|
|
1458
|
+
}
|
|
1459
|
+
runTempDir() {
|
|
1460
|
+
this.tempDir ??= (async () => {
|
|
1461
|
+
const { mkdtemp } = await import("fs/promises");
|
|
1462
|
+
const { tmpdir } = await import("os");
|
|
1463
|
+
const { join } = await import("path");
|
|
1464
|
+
return mkdtemp(join(tmpdir(), "hobots-files-"));
|
|
1465
|
+
})();
|
|
1466
|
+
return this.tempDir;
|
|
1467
|
+
}
|
|
1468
|
+
async resolveTarget(ref, dest, destIsDir) {
|
|
1469
|
+
const { mkdir, mkdtemp, stat } = await import("fs/promises");
|
|
1470
|
+
const { dirname, join, resolve: resolve2 } = await import("path");
|
|
1471
|
+
const name = safeFileName(ref.name, "arquivo");
|
|
1472
|
+
if (dest === void 0) {
|
|
1473
|
+
return join(await mkdtemp(join(await this.runTempDir(), "f-")), name);
|
|
1474
|
+
}
|
|
1475
|
+
const isDir = destIsDir || dest.endsWith("/") || dest.endsWith("\\") || (await stat(dest).catch(() => void 0))?.isDirectory() === true;
|
|
1476
|
+
if (isDir) {
|
|
1477
|
+
await mkdir(dest, { recursive: true });
|
|
1478
|
+
return resolve2(dest, name);
|
|
1479
|
+
}
|
|
1480
|
+
await mkdir(dirname(resolve2(dest)), { recursive: true });
|
|
1481
|
+
return resolve2(dest);
|
|
1482
|
+
}
|
|
1483
|
+
/** storage 's3': URL pré-assinada → GET em streaming → confere o tamanho → só então DELETE. */
|
|
1484
|
+
async downloadS3(ref, target) {
|
|
1485
|
+
const { api, runId, signal } = this.deps;
|
|
1486
|
+
let expected;
|
|
1487
|
+
let url;
|
|
1488
|
+
try {
|
|
1489
|
+
const info = await api.requestFileDownload(runId, ref.id, signal);
|
|
1490
|
+
expected = info.size >= 0 ? info.size : ref.size;
|
|
1491
|
+
url = info.url;
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
throw this.translate(error, ref);
|
|
1494
|
+
}
|
|
1495
|
+
const { open, rename, rm } = await import("fs/promises");
|
|
1496
|
+
const part = `${target}.part`;
|
|
1497
|
+
const idleTimeoutMs = this.deps.idleTimeoutMs ?? FILE_DOWNLOAD_IDLE_TIMEOUT_MS;
|
|
1498
|
+
const controller = new AbortController();
|
|
1499
|
+
let timedOut = false;
|
|
1500
|
+
let timer;
|
|
1501
|
+
const arm = () => {
|
|
1502
|
+
clearTimeout(timer);
|
|
1503
|
+
timer = setTimeout(() => {
|
|
1504
|
+
timedOut = true;
|
|
1505
|
+
controller.abort();
|
|
1506
|
+
}, idleTimeoutMs);
|
|
1507
|
+
};
|
|
1508
|
+
const onCancel = () => controller.abort();
|
|
1509
|
+
signal.addEventListener("abort", onCancel, { once: true });
|
|
1510
|
+
try {
|
|
1511
|
+
arm();
|
|
1512
|
+
const response = await api.fetchFile(url, controller.signal);
|
|
1513
|
+
const handle = await open(part, "w");
|
|
1514
|
+
let written = 0;
|
|
1515
|
+
try {
|
|
1516
|
+
const reader = response.body?.getReader();
|
|
1517
|
+
const stopReading = () => void reader?.cancel().catch(() => void 0);
|
|
1518
|
+
controller.signal.addEventListener("abort", stopReading, { once: true });
|
|
1519
|
+
for (; ; ) {
|
|
1520
|
+
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
1521
|
+
if (chunk.done) break;
|
|
1522
|
+
arm();
|
|
1523
|
+
written += chunk.value.byteLength;
|
|
1524
|
+
if (written > expected) {
|
|
1525
|
+
stopReading();
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
await handle.write(chunk.value);
|
|
1529
|
+
}
|
|
1530
|
+
} finally {
|
|
1531
|
+
await handle.close();
|
|
1532
|
+
}
|
|
1533
|
+
if (written !== expected) {
|
|
1534
|
+
throw new Error(`tamanho divergente no download de "${ref.name}": recebidos ${written} bytes, esperados ${expected}`);
|
|
1535
|
+
}
|
|
1536
|
+
await rename(part, target);
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
await rm(part, { force: true });
|
|
1539
|
+
if (signal.aborted) throw runCanceledError();
|
|
1540
|
+
if (timedOut) {
|
|
1541
|
+
throw new Error(`download de "${ref.name}" parado h\xE1 ${idleTimeoutMs / 1e3}s \u2014 tente de novo`);
|
|
1542
|
+
}
|
|
1543
|
+
throw this.translate(error, ref);
|
|
1544
|
+
} finally {
|
|
1545
|
+
clearTimeout(timer);
|
|
1546
|
+
signal.removeEventListener("abort", onCancel);
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
await api.confirmFileDownload(runId, ref.id);
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
this.deps.warn(`arquivo "${ref.name}" baixado, mas a confirma\xE7\xE3o ao servidor falhou: ${String(error)}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/** storage 'local': copia o único arquivo de `$HOBOTS_LOCAL_FILES_DIR/<id>/` e apaga a pasta de origem. */
|
|
1555
|
+
async copyLocal(ref, target) {
|
|
1556
|
+
const base = process.env[LOCAL_FILES_DIR_ENV];
|
|
1557
|
+
if (!base) throw unavailableLocal(ref);
|
|
1558
|
+
const { copyFile, readdir, rename, rm, stat } = await import("fs/promises");
|
|
1559
|
+
const { join, resolve: resolve2, sep } = await import("path");
|
|
1560
|
+
const root = resolve2(base);
|
|
1561
|
+
const folder = resolve2(root, ref.id);
|
|
1562
|
+
if (!isInsideDir(root, folder, sep) || !UUID_RE.test(ref.id)) {
|
|
1563
|
+
throw new Error(`id de arquivo local inv\xE1lido: ${JSON.stringify(ref.id)}`);
|
|
1564
|
+
}
|
|
1565
|
+
let names;
|
|
1566
|
+
try {
|
|
1567
|
+
names = (await readdir(folder, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") throw unavailableLocal(ref);
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
if (names.length === 0) throw unavailableLocal(ref);
|
|
1573
|
+
if (names.length > 1) throw new Error(`pasta do arquivo "${ref.name}" tem ${names.length} arquivos \u2014 esperado 1`);
|
|
1574
|
+
this.checkCanceled();
|
|
1575
|
+
const part = `${target}.part`;
|
|
1576
|
+
try {
|
|
1577
|
+
await copyFile(join(folder, names[0]), part);
|
|
1578
|
+
const { size } = await stat(part);
|
|
1579
|
+
if (size !== ref.size) {
|
|
1580
|
+
throw new Error(`tamanho divergente na c\xF3pia de "${ref.name}": copiados ${size} bytes, esperados ${ref.size}`);
|
|
1581
|
+
}
|
|
1582
|
+
await rename(part, target);
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
await rm(part, { force: true });
|
|
1585
|
+
throw error;
|
|
1586
|
+
}
|
|
1587
|
+
await rm(folder, { recursive: true, force: true });
|
|
1588
|
+
}
|
|
1589
|
+
translate(error, ref) {
|
|
1590
|
+
if (this.deps.signal.aborted) return runCanceledError();
|
|
1591
|
+
if (error instanceof TasksApiError && error.status === 410) {
|
|
1592
|
+
return new TasksApiError(`arquivo "${ref.name}" j\xE1 baixado ou expirado`, 410);
|
|
1593
|
+
}
|
|
1594
|
+
if (error instanceof TasksApiError && error.status === 404) {
|
|
1595
|
+
return new TasksApiError(`arquivo "${ref.name}" n\xE3o encontrado nesta execu\xE7\xE3o`, 404);
|
|
1596
|
+
}
|
|
1597
|
+
return error;
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1277
1601
|
// src/retry.ts
|
|
1278
1602
|
var DEFAULT_RETRIES = 3;
|
|
1279
1603
|
var DEFAULT_DELAY_MS = 1e3;
|
|
@@ -1414,7 +1738,6 @@ var MAX_CANCEL_CHECK_INTERVAL_MS = 15e3;
|
|
|
1414
1738
|
var MAX_LOG_MESSAGE_CHARS = WIRE_LIMITS.MAX_LOG_MESSAGE_CHARS;
|
|
1415
1739
|
var MAX_LOGS_PER_PATCH = WIRE_LIMITS.MAX_LOGS_PER_PATCH;
|
|
1416
1740
|
var FLUSH_AT_BUFFERED = 20;
|
|
1417
|
-
var MAX_ITEMS_PER_RUN = WIRE_LIMITS.MAX_ITEMS_PER_RUN;
|
|
1418
1741
|
var MAX_ITEMS_PER_REQUEST = WIRE_LIMITS.MAX_ITEMS_PER_REQUEST;
|
|
1419
1742
|
var MAX_ITEM_MESSAGE_CHARS = WIRE_LIMITS.MAX_ITEM_MESSAGE_CHARS;
|
|
1420
1743
|
var MAX_ITEM_PAYLOAD_BYTES = WIRE_LIMITS.MAX_ITEM_PAYLOAD_BYTES;
|
|
@@ -1436,6 +1759,7 @@ var RunReporter = class {
|
|
|
1436
1759
|
this.api = api;
|
|
1437
1760
|
this.runId = runId;
|
|
1438
1761
|
this.logger = logger;
|
|
1762
|
+
this.tx = tx;
|
|
1439
1763
|
this.flushIntervalMs = flushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
1440
1764
|
this.cancelCheckIntervalMs = Math.min(
|
|
1441
1765
|
MAX_CANCEL_CHECK_INTERVAL_MS,
|
|
@@ -1512,13 +1836,16 @@ var RunReporter = class {
|
|
|
1512
1836
|
);
|
|
1513
1837
|
const signal = this.abortController.signal;
|
|
1514
1838
|
const sleep2 = (ms) => cancelableSleep(ms, signal);
|
|
1839
|
+
this.fileStore = new RunFileStore({ api, runId, params, signal, warn: (message) => this.log("warning", message) });
|
|
1515
1840
|
this.context = {
|
|
1516
1841
|
runId,
|
|
1517
1842
|
params,
|
|
1518
1843
|
log,
|
|
1519
1844
|
items,
|
|
1520
1845
|
progress,
|
|
1846
|
+
files: this.fileStore.files,
|
|
1521
1847
|
transaction: tx,
|
|
1848
|
+
flush: (timeoutMs) => this.flush(timeoutMs),
|
|
1522
1849
|
signal,
|
|
1523
1850
|
throwIfCanceled: checkCanceled,
|
|
1524
1851
|
sleep: sleep2,
|
|
@@ -1528,6 +1855,7 @@ var RunReporter = class {
|
|
|
1528
1855
|
api;
|
|
1529
1856
|
runId;
|
|
1530
1857
|
logger;
|
|
1858
|
+
tx;
|
|
1531
1859
|
context;
|
|
1532
1860
|
buffer = [];
|
|
1533
1861
|
dead = false;
|
|
@@ -1545,7 +1873,6 @@ var RunReporter = class {
|
|
|
1545
1873
|
itemsChain = Promise.resolve();
|
|
1546
1874
|
itemsTimer;
|
|
1547
1875
|
itemSeq = 0;
|
|
1548
|
-
itemCount = 0;
|
|
1549
1876
|
/** Servidor sem o recurso (404) — para de tentar (e de avisar) nesta run. */
|
|
1550
1877
|
itemsUnsupported = false;
|
|
1551
1878
|
/** Cap de itens por run atingido (client-side ou 429) — degrada com um aviso só. */
|
|
@@ -1571,6 +1898,12 @@ var RunReporter = class {
|
|
|
1571
1898
|
progressDirty = false;
|
|
1572
1899
|
progressTimer;
|
|
1573
1900
|
progressWarned = false;
|
|
1901
|
+
/** Arquivos do formulário desta run (`ctx.files`) — a pasta temporária é apagada no fim. */
|
|
1902
|
+
fileStore;
|
|
1903
|
+
/** Apaga as cópias de arquivo que ficaram na pasta temporária da run. Nunca lança. */
|
|
1904
|
+
cleanupFiles() {
|
|
1905
|
+
return this.fileStore.cleanup();
|
|
1906
|
+
}
|
|
1574
1907
|
get isDead() {
|
|
1575
1908
|
return this.dead;
|
|
1576
1909
|
}
|
|
@@ -1668,6 +2001,21 @@ var RunReporter = class {
|
|
|
1668
2001
|
this.enqueue({ status: "running", logs: logs.slice(i, i + MAX_LOGS_PER_PATCH) });
|
|
1669
2002
|
}
|
|
1670
2003
|
}
|
|
2004
|
+
/**
|
|
2005
|
+
* `hobots.flush()`: o bot decide quando entregar o que já fez — steps raiz finalizadas
|
|
2006
|
+
* viram transactions parciais da run, e itens, logs e progresso pendentes saem já, sem
|
|
2007
|
+
* esperar o throttle. Aguarda a entrega com teto. Nunca lança (pode ir num `finally`).
|
|
2008
|
+
*/
|
|
2009
|
+
async flush(timeoutMs = ITEMS_DRAIN_MS) {
|
|
2010
|
+
this.tx.flush();
|
|
2011
|
+
this.flushItems();
|
|
2012
|
+
if (this.progressDirty) this.flushProgress();
|
|
2013
|
+
else this.flushLogs();
|
|
2014
|
+
await Promise.race([
|
|
2015
|
+
Promise.all([this.itemsChain, this.chain, this.tx.drain(timeoutMs)]).catch(() => void 0),
|
|
2016
|
+
new Promise((resolve2) => unrefTimer(setTimeout(resolve2, timeoutMs)))
|
|
2017
|
+
]);
|
|
2018
|
+
}
|
|
1671
2019
|
/** Primeiro PATCH (claimed → running). Retorna false se a run já morreu (cancelada). */
|
|
1672
2020
|
async reportRunning() {
|
|
1673
2021
|
this.enqueue({ status: "running" });
|
|
@@ -1778,14 +2126,6 @@ var RunReporter = class {
|
|
|
1778
2126
|
*/
|
|
1779
2127
|
addItem(status, message, payload, id, image3) {
|
|
1780
2128
|
if (this.itemsClosed || this.itemsUnsupported) return;
|
|
1781
|
-
if (this.itemCount >= MAX_ITEMS_PER_RUN) {
|
|
1782
|
-
if (!this.itemsQuotaExceeded) {
|
|
1783
|
-
this.itemsQuotaExceeded = true;
|
|
1784
|
-
this.log("warning", `itens descartados: limite de ${MAX_ITEMS_PER_RUN} por execu\xE7\xE3o atingido`);
|
|
1785
|
-
}
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
this.itemCount++;
|
|
1789
2129
|
let scrubbedPayload;
|
|
1790
2130
|
if (payload) {
|
|
1791
2131
|
scrubbedPayload = scrubPayloadValue(payload, 0);
|
|
@@ -2080,6 +2420,7 @@ function currentRunReporter() {
|
|
|
2080
2420
|
}
|
|
2081
2421
|
|
|
2082
2422
|
// src/transaction.ts
|
|
2423
|
+
var FLUSH_TRANSACTION_OP = "hobots.run.flush";
|
|
2083
2424
|
var MAX_STEP_MESSAGE_CHARS = 2e3;
|
|
2084
2425
|
function stepMessage(text) {
|
|
2085
2426
|
return truncate(scrubSecrets(text), MAX_STEP_MESSAGE_CHARS);
|
|
@@ -2301,6 +2642,60 @@ var Transaction = class {
|
|
|
2301
2642
|
this.name = name;
|
|
2302
2643
|
return this;
|
|
2303
2644
|
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Envia já as steps raiz finalizadas (e todas as descendentes, fechando as que
|
|
2647
|
+
* ficaram abertas) como transactions parciais da mesma run (`op` hobots.run.flush,
|
|
2648
|
+
* `event_id` novo, até 200 steps cada) e as tira da memória. Steps raiz ainda abertas
|
|
2649
|
+
* ficam para o próximo flush ou para o `finish()`. É assim que um agente sempre
|
|
2650
|
+
* ligado (`start()`), cuja run não termina, entrega as etapas enquanto processa.
|
|
2651
|
+
* Devolve quantas steps foram enviadas.
|
|
2652
|
+
*/
|
|
2653
|
+
flush() {
|
|
2654
|
+
if (this.finished) return 0;
|
|
2655
|
+
const ready = /* @__PURE__ */ new Set();
|
|
2656
|
+
for (const step of this.steps) {
|
|
2657
|
+
if (step.parentStepId === this.stepId && step.endTimestamp) ready.add(step.stepId);
|
|
2658
|
+
}
|
|
2659
|
+
if (ready.size === 0) return 0;
|
|
2660
|
+
const batch = [];
|
|
2661
|
+
const remaining = [];
|
|
2662
|
+
for (const step of this.steps) {
|
|
2663
|
+
if (ready.has(step.stepId) || ready.has(step.parentStepId)) {
|
|
2664
|
+
ready.add(step.stepId);
|
|
2665
|
+
batch.push(step);
|
|
2666
|
+
} else {
|
|
2667
|
+
remaining.push(step);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
for (const step of batch) step.finish();
|
|
2671
|
+
this.steps.splice(0, this.steps.length, ...remaining);
|
|
2672
|
+
if (this.client) {
|
|
2673
|
+
for (let i = 0; i < batch.length; i += MAX_STEPS) {
|
|
2674
|
+
const chunk = batch.slice(i, i + MAX_STEPS);
|
|
2675
|
+
const startTimestamp = chunk.reduce((min, step) => step.startTimestamp < min ? step.startTimestamp : min, chunk[0].startTimestamp);
|
|
2676
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2677
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, startTimestamp, nowISO(), chunk));
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return batch.length;
|
|
2681
|
+
}
|
|
2682
|
+
/** Entrega já (com teto) o que está na fila do transport — as transactions parciais inclusive. */
|
|
2683
|
+
async drain(timeoutMs) {
|
|
2684
|
+
await this.client?.flush(timeoutMs);
|
|
2685
|
+
}
|
|
2686
|
+
payload(eventId, op, status, startTimestamp, timestamp, steps) {
|
|
2687
|
+
return {
|
|
2688
|
+
event_id: eventId,
|
|
2689
|
+
name: this.name,
|
|
2690
|
+
op,
|
|
2691
|
+
status,
|
|
2692
|
+
start_timestamp: startTimestamp,
|
|
2693
|
+
timestamp,
|
|
2694
|
+
steps: steps.map((step) => step.toJSON()),
|
|
2695
|
+
tags: Object.keys(this.tags).length > 0 ? { ...this.tags } : void 0,
|
|
2696
|
+
run_id: this.runId
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2304
2699
|
finish() {
|
|
2305
2700
|
if (this.finished) return;
|
|
2306
2701
|
this.finished = true;
|
|
@@ -2315,18 +2710,15 @@ var Transaction = class {
|
|
|
2315
2710
|
this.status
|
|
2316
2711
|
);
|
|
2317
2712
|
if (!this.client) return;
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
status
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
run_id: this.runId
|
|
2328
|
-
};
|
|
2329
|
-
this.client.sendTransaction(payload);
|
|
2713
|
+
const steps = this.steps;
|
|
2714
|
+
let first = 0;
|
|
2715
|
+
while (steps.length - first > MAX_STEPS) {
|
|
2716
|
+
const chunk = steps.slice(first, first + MAX_STEPS);
|
|
2717
|
+
const status = chunk.some((step) => step.status === "error") ? "error" : "ok";
|
|
2718
|
+
this.client.sendTransaction(this.payload(uuid4(), FLUSH_TRANSACTION_OP, status, chunk[0].startTimestamp, endTimestamp, chunk));
|
|
2719
|
+
first += MAX_STEPS;
|
|
2720
|
+
}
|
|
2721
|
+
this.client.sendTransaction(this.payload(this.eventId, this.op, this.status, this.startTimestamp, endTimestamp, steps.slice(first)));
|
|
2330
2722
|
}
|
|
2331
2723
|
};
|
|
2332
2724
|
|
|
@@ -2374,6 +2766,7 @@ async function executeRun(input) {
|
|
|
2374
2766
|
}
|
|
2375
2767
|
await reporter.finalize();
|
|
2376
2768
|
} finally {
|
|
2769
|
+
await reporter.cleanupFiles();
|
|
2377
2770
|
tracker.remove(reporter);
|
|
2378
2771
|
}
|
|
2379
2772
|
}
|
|
@@ -2829,7 +3222,9 @@ function resolveRunOptions(options, mode, logger) {
|
|
|
2829
3222
|
}
|
|
2830
3223
|
function instanceInfo(rt) {
|
|
2831
3224
|
return {
|
|
3225
|
+
/* v8 ignore start -- defensivo: o boot define o instanceId antes de qualquer poll/criação de run */
|
|
2832
3226
|
instance_id: rt.instanceId ?? "unknown",
|
|
3227
|
+
/* v8 ignore stop */
|
|
2833
3228
|
version: SDK_VERSION,
|
|
2834
3229
|
host: hostname(),
|
|
2835
3230
|
// ambiente da instância: junto do heartbeat, é como o servidor sabe onde o robô roda
|
|
@@ -3006,6 +3401,14 @@ var fallbackItems = {
|
|
|
3006
3401
|
each: (list, handler, options = {}) => eachItem(list, handler, options, fallbackEachSink)
|
|
3007
3402
|
};
|
|
3008
3403
|
var fallbackProgress = { total: noop, advance: noop, set: noop, current: 0, snapshot: null };
|
|
3404
|
+
var outsideRunFiles = () => Promise.reject(new Error("hobots.files s\xF3 baixa arquivos dentro de uma execu\xE7\xE3o (handler do register()/start())"));
|
|
3405
|
+
var fallbackFiles = {
|
|
3406
|
+
list: () => [],
|
|
3407
|
+
get: () => void 0,
|
|
3408
|
+
download: outsideRunFiles,
|
|
3409
|
+
downloadAll: outsideRunFiles,
|
|
3410
|
+
read: outsideRunFiles
|
|
3411
|
+
};
|
|
3009
3412
|
var neverAborts = new AbortController().signal;
|
|
3010
3413
|
var detachedTransaction;
|
|
3011
3414
|
function resolve(member) {
|
|
@@ -3027,10 +3430,12 @@ var hobots = Object.defineProperties({}, {
|
|
|
3027
3430
|
log: getter("log", () => fallbackLog),
|
|
3028
3431
|
items: getter("items", () => fallbackItems),
|
|
3029
3432
|
progress: getter("progress", () => fallbackProgress),
|
|
3433
|
+
files: getter("files", () => fallbackFiles),
|
|
3030
3434
|
transaction: getter("transaction", () => detachedTransaction ??= new Transaction(void 0, "hobots.detached")),
|
|
3031
3435
|
signal: getter("signal", () => neverAborts),
|
|
3032
3436
|
throwIfCanceled: method(() => resolve("throwIfCanceled")?.throwIfCanceled()),
|
|
3033
3437
|
sleep: method((ms) => resolve("sleep")?.sleep(ms) ?? sleep(ms)),
|
|
3438
|
+
flush: method((timeoutMs) => resolve("flush")?.flush(timeoutMs) ?? Promise.resolve()),
|
|
3034
3439
|
retry: method((fn, options) => {
|
|
3035
3440
|
const reporter = currentReporter();
|
|
3036
3441
|
return reporter ? reporter.context.retry(fn, options) : retryWithBackoff(fn, options);
|
|
@@ -3046,7 +3451,8 @@ var hobots = Object.defineProperties({}, {
|
|
|
3046
3451
|
}),
|
|
3047
3452
|
close: method((timeoutMs) => close(timeoutMs)),
|
|
3048
3453
|
captureException: method((error, hint) => captureException(error, hint)),
|
|
3049
|
-
image: method(image)
|
|
3454
|
+
image: method(image),
|
|
3455
|
+
isFileRef: method(isFileRef)
|
|
3050
3456
|
});
|
|
3051
3457
|
function installGlobalHobots() {
|
|
3052
3458
|
if (globalThis.hobots === hobots) return;
|
|
@@ -3058,6 +3464,7 @@ var close2 = hobots.close;
|
|
|
3058
3464
|
var captureException2 = hobots.captureException;
|
|
3059
3465
|
var retry = hobots.retry;
|
|
3060
3466
|
var image2 = hobots.image;
|
|
3467
|
+
var isFileRef2 = hobots.isFileRef;
|
|
3061
3468
|
|
|
3062
3469
|
// src/index.ts
|
|
3063
3470
|
var src_default = hobots;
|