@johpaz/hive-sdk 0.4.3 → 0.4.4
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 +1 -1
- package/package.json +5 -5
- package/packages/cli/templates/hive-app/package.json +3 -0
- package/packages/core/src/config/loader.ts +2 -1
- package/packages/core/src/mcp/transports/sse.ts +11 -5
- package/packages/core/src/scheduler/CronScheduler.ts +4 -2
- package/packages/core/src/scheduler/cron/job.ts +2 -1
- package/packages/core/src/scheduler/cron/zoned-time.ts +2 -1
- package/packages/core/src/tools/office/office-leer-pdf.ts +93 -44
- package/packages/core/src/tools/office/office-leer-xlsx.ts +36 -10
- package/packages/core/src/tools/office/security-limits.ts +28 -0
- package/packages/core/src/utils/port.ts +33 -0
- package/packages/core/src/voice/index.ts +2 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"LICENSE"
|
|
81
81
|
],
|
|
82
82
|
"engines": {
|
|
83
|
-
"bun": ">=1.4.
|
|
83
|
+
"bun": ">=1.4.2"
|
|
84
84
|
},
|
|
85
85
|
"workspaces": [
|
|
86
86
|
"packages/core",
|
|
@@ -115,15 +115,15 @@
|
|
|
115
115
|
"mammoth": "^1.12.0",
|
|
116
116
|
"ollama": "^0.6.3",
|
|
117
117
|
"openai": "^6.18.0",
|
|
118
|
-
"pdfjs-dist": "^
|
|
118
|
+
"pdfjs-dist": "^6.3.289",
|
|
119
119
|
"pptxgenjs": "^4.0.1",
|
|
120
120
|
"qrcode-terminal": "^0.12.0",
|
|
121
121
|
"toon-format-parser": "^1.1.0",
|
|
122
|
-
"xlsx": "
|
|
122
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
123
123
|
"zod": "^4.4.3"
|
|
124
124
|
},
|
|
125
125
|
"devDependencies": {
|
|
126
|
-
"@types/bun": "^1.
|
|
126
|
+
"@types/bun": "^1.4.1",
|
|
127
127
|
"@types/jsonwebtoken": "^9.0.10",
|
|
128
128
|
"typescript": "6.0.2"
|
|
129
129
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePort } from "../utils/port.ts";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
import { mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
@@ -407,7 +408,7 @@ function buildDefaultConfig(): Config {
|
|
|
407
408
|
return {
|
|
408
409
|
gateway: {
|
|
409
410
|
host: process.env.HIVE_HOST || "127.0.0.1",
|
|
410
|
-
port:
|
|
411
|
+
port: resolvePort(process.env.HIVE_PORT, 18790),
|
|
411
412
|
pidFile: path.join(hiveDir, "gateway.pid"),
|
|
412
413
|
authToken: process.env.HIVE_AUTH_TOKEN || undefined,
|
|
413
414
|
tools: {
|
|
@@ -96,11 +96,17 @@ export class SSETransport implements Transport {
|
|
|
96
96
|
this.sessionId = sessionId;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// Track cookies for session affinity (important for n8n/proxies)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// Track cookies for session affinity (important for n8n/proxies).
|
|
100
|
+
//
|
|
101
|
+
// `getSetCookie()` y no `get("set-cookie")`: Set-Cookie es la única cabecera
|
|
102
|
+
// que puede repetirse sin combinarse, y `get()` devuelve las repeticiones
|
|
103
|
+
// unidas con ", " (Bun 1.4 lo alineó con la spec de Fetch). Partir eso por
|
|
104
|
+
// coma rompe cualquier cookie cuyo valor lleve una —`Expires=Wed, 09 Jun
|
|
105
|
+
// 2027 10:18:14 GMT` es el caso de todos los días— y dejaba fragmentos como
|
|
106
|
+
// "09 Jun 2027 10:18:14 GMT" haciéndose pasar por cookies.
|
|
107
|
+
const setCookies = response.headers.getSetCookie();
|
|
108
|
+
if (setCookies.length > 0) {
|
|
109
|
+
const newCookies = setCookies.map(c => c.split(";")[0].trim());
|
|
104
110
|
this.cookies = [...new Set([...this.cookies, ...newCookies])];
|
|
105
111
|
}
|
|
106
112
|
}
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* El motor de cron es propio (`./cron`), sin dependencias: sólo `setTimeout` e
|
|
8
8
|
* `Intl` del runtime. Antes era `croner`.
|
|
9
9
|
*
|
|
10
|
-
* `Bun.cron()` no sirve como reemplazo —
|
|
10
|
+
* `Bun.cron()` no sirve como reemplazo —reevaluado contra el runtime 1.4.2—:
|
|
11
11
|
* acepta sólo 5 campos y rechaza el sexto, no admite una fecha ISO como patrón
|
|
12
|
-
* (que es como se agendan los jobs `one_shot`),
|
|
12
|
+
* (que es como se agendan los jobs `one_shot`), no toma una zona por job (usa la
|
|
13
|
+
* local del proceso desde 1.4; antes era UTC, y ese cambio silencioso es
|
|
14
|
+
* justamente por qué no conviene delegarle la conversión), y su
|
|
13
15
|
* handle no expone la próxima corrida, que es de donde sale `next_run_at` y con
|
|
14
16
|
* lo que se detectan las corridas perdidas al arrancar. Tampoco tiene
|
|
15
17
|
* equivalente de `protect`, `maxRuns`, `interval`, `startAt`/`stopAt` ni
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* habría sido migrar la base para no ganar nada.
|
|
9
9
|
*
|
|
10
10
|
* No usa `Bun.cron()`: ese sólo acepta 5 campos, no admite una fecha ISO como
|
|
11
|
-
* patrón —que es como se agendan los jobs `one_shot`—,
|
|
11
|
+
* patrón —que es como se agendan los jobs `one_shot`—, no toma una zona por job
|
|
12
|
+
* (usa la local del proceso desde Bun 1.4; antes era UTC) y
|
|
12
13
|
* su handle no expone la próxima corrida, que es de donde sale `next_run_at` y
|
|
13
14
|
* con lo que el scheduler detecta las corridas perdidas al arrancar. Lo que sí
|
|
14
15
|
* se usa de Bun es el runtime pelado: `setTimeout` e `Intl`.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Reloj de pared ↔ instante, en una zona horaria IANA.
|
|
3
3
|
*
|
|
4
4
|
* Es la parte difícil de un cron con zona horaria y la razón por la que no
|
|
5
|
-
* alcanza con `Bun.cron.parse()`, que
|
|
5
|
+
* alcanza con `Bun.cron.parse()`, que resuelve en una sola zona —UTC hasta Bun
|
|
6
|
+
* 1.3, la local del proceso desde 1.4— y nunca en la del job. "Todos los días a
|
|
6
7
|
* las 9" significa las 9 **del reloj de la pared en Bogotá**, y ese instante se
|
|
7
8
|
* corre una hora dos veces al año en las zonas con horario de verano. Calcular
|
|
8
9
|
* el offset una sola vez y sumarlo produce un cron que se desfasa un día al año
|
|
@@ -10,6 +10,14 @@ import type { Tool } from "../types.ts";
|
|
|
10
10
|
import { logger } from "../../utils/logger.ts";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
assertBeforeDeadline,
|
|
16
|
+
MAX_PDF_INPUT_BYTES,
|
|
17
|
+
MAX_PDF_PAGES_PER_REQUEST,
|
|
18
|
+
OFFICE_PROCESSING_TIMEOUT_MS,
|
|
19
|
+
validateOfficeInput,
|
|
20
|
+
} from "./security-limits.ts";
|
|
13
21
|
|
|
14
22
|
const log = logger.child("office-leer-pdf");
|
|
15
23
|
|
|
@@ -48,61 +56,102 @@ export const officeLeerPdfTool: Tool = {
|
|
|
48
56
|
return { ok: false, error: `Archivo no encontrado: ${rutaAbsoluta}` };
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
const inputError = validateOfficeInput(
|
|
60
|
+
rutaAbsoluta,
|
|
61
|
+
MAX_PDF_INPUT_BYTES,
|
|
62
|
+
"El PDF",
|
|
63
|
+
);
|
|
64
|
+
if (inputError) return { ok: false, error: inputError };
|
|
65
|
+
|
|
51
66
|
const buffer = fs.readFileSync(rutaAbsoluta);
|
|
52
67
|
const uint8Array = new Uint8Array(buffer);
|
|
53
68
|
|
|
54
|
-
//
|
|
69
|
+
// La build legacy conserva compatibilidad con Bun. PDF.js configura su
|
|
70
|
+
// propio fake worker en Node/Bun; borrar workerSrc rompe PDF.js 6+.
|
|
55
71
|
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs" as any).catch(
|
|
56
72
|
() => import("pdfjs-dist" as any)
|
|
57
73
|
);
|
|
58
74
|
|
|
59
|
-
// Desactivar worker para entorno Node/Bun
|
|
60
75
|
const lib = pdfjsLib.default ?? pdfjsLib;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
const pdfjsPackageUrl = import.meta.resolve("pdfjs-dist/package.json");
|
|
77
|
+
const loadingTask = lib.getDocument({
|
|
78
|
+
data: uint8Array,
|
|
79
|
+
enableScripting: false,
|
|
80
|
+
isEvalSupported: false,
|
|
81
|
+
// Bun's process.getBuiltinModule("fs/promises") expects a filesystem
|
|
82
|
+
// path here; a file:// URL string is not accepted.
|
|
83
|
+
standardFontDataUrl: fileURLToPath(
|
|
84
|
+
new URL("./standard_fonts/", pdfjsPackageUrl),
|
|
85
|
+
),
|
|
86
|
+
});
|
|
67
87
|
|
|
68
|
-
// Metadata
|
|
69
|
-
let titulo: string | undefined;
|
|
70
88
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
const deadline = Date.now() + OFFICE_PROCESSING_TIMEOUT_MS;
|
|
90
|
+
const doc = await loadingTask.promise;
|
|
91
|
+
const totalPaginas = doc.numPages;
|
|
92
|
+
|
|
93
|
+
// Metadata
|
|
94
|
+
let titulo: string | undefined;
|
|
95
|
+
try {
|
|
96
|
+
const meta = await doc.getMetadata();
|
|
97
|
+
titulo = (meta?.info as any)?.Title ?? undefined;
|
|
98
|
+
} catch {
|
|
99
|
+
// metadata opcional
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const inicio = paginaInicio;
|
|
103
|
+
if (!Number.isInteger(inicio) || inicio > totalPaginas) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: `La página inicial debe estar entre 1 y ${totalPaginas}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (paginaFin !== undefined && (!Number.isInteger(paginaFin) || paginaFin < inicio)) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: "La página final debe ser un entero mayor o igual a la página inicial",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fin = paginaFin ? Math.min(paginaFin, totalPaginas) : totalPaginas;
|
|
117
|
+
const paginasSolicitadas = fin - inicio + 1;
|
|
118
|
+
if (paginasSolicitadas > MAX_PDF_PAGES_PER_REQUEST) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: `Se pueden leer como máximo ${MAX_PDF_PAGES_PER_REQUEST} páginas por solicitud`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const textosPorPagina: Array<{ pagina: number; texto: string }> = [];
|
|
126
|
+
|
|
127
|
+
for (let i = inicio; i <= fin; i++) {
|
|
128
|
+
assertBeforeDeadline(deadline, "La lectura del PDF");
|
|
129
|
+
const pagina = await doc.getPage(i);
|
|
130
|
+
const contenido = await pagina.getTextContent();
|
|
131
|
+
const texto = (contenido.items as any[])
|
|
132
|
+
.map((item: any) => item.str ?? "")
|
|
133
|
+
.join(" ")
|
|
134
|
+
.replace(/\s+/g, " ")
|
|
135
|
+
.trim();
|
|
136
|
+
textosPorPagina.push({ pagina: i, texto });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const textoCompleto = textosPorPagina.map((p) => p.texto).join("\n\n");
|
|
140
|
+
|
|
141
|
+
log.info(`PDF leído: ${totalPaginas} páginas, ${textoCompleto.length} caracteres`);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
ruta: rutaAbsoluta,
|
|
146
|
+
totalPaginas,
|
|
147
|
+
paginasLeidas: paginasSolicitadas,
|
|
148
|
+
titulo,
|
|
149
|
+
texto: textoCompleto,
|
|
150
|
+
paginas: textosPorPagina,
|
|
151
|
+
};
|
|
152
|
+
} finally {
|
|
153
|
+
await loadingTask.destroy();
|
|
75
154
|
}
|
|
76
|
-
|
|
77
|
-
const inicio = paginaInicio;
|
|
78
|
-
const fin = paginaFin ? Math.min(paginaFin, totalPaginas) : totalPaginas;
|
|
79
|
-
|
|
80
|
-
const textosPorPagina: Array<{ pagina: number; texto: string }> = [];
|
|
81
|
-
|
|
82
|
-
for (let i = inicio; i <= fin; i++) {
|
|
83
|
-
const pagina = await doc.getPage(i);
|
|
84
|
-
const contenido = await pagina.getTextContent();
|
|
85
|
-
const texto = (contenido.items as any[])
|
|
86
|
-
.map((item: any) => item.str ?? "")
|
|
87
|
-
.join(" ")
|
|
88
|
-
.replace(/\s+/g, " ")
|
|
89
|
-
.trim();
|
|
90
|
-
textosPorPagina.push({ pagina: i, texto });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const textoCompleto = textosPorPagina.map((p) => p.texto).join("\n\n");
|
|
94
|
-
|
|
95
|
-
log.info(`PDF leído: ${totalPaginas} páginas, ${textoCompleto.length} caracteres`);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ok: true,
|
|
99
|
-
ruta: rutaAbsoluta,
|
|
100
|
-
totalPaginas,
|
|
101
|
-
paginasLeidas: fin - inicio + 1,
|
|
102
|
-
titulo,
|
|
103
|
-
texto: textoCompleto,
|
|
104
|
-
paginas: textosPorPagina,
|
|
105
|
-
};
|
|
106
155
|
} catch (error) {
|
|
107
156
|
log.error(`Error leyendo PDF: ${(error as Error).message}`);
|
|
108
157
|
return {
|
|
@@ -11,6 +11,14 @@ import { logger } from "../../utils/logger.ts";
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { cargarXlsx } from "./xlsx-loader.ts";
|
|
14
|
+
import {
|
|
15
|
+
assertBeforeDeadline,
|
|
16
|
+
MAX_XLSX_INPUT_BYTES,
|
|
17
|
+
MAX_XLSX_ROWS_PER_SHEET,
|
|
18
|
+
MAX_XLSX_SHEETS,
|
|
19
|
+
OFFICE_PROCESSING_TIMEOUT_MS,
|
|
20
|
+
validateOfficeInput,
|
|
21
|
+
} from "./security-limits.ts";
|
|
14
22
|
|
|
15
23
|
const log = logger.child("office-leer-xlsx");
|
|
16
24
|
|
|
@@ -56,32 +64,43 @@ export const officeLeerXlsxTool: Tool = {
|
|
|
56
64
|
return { ok: false, error: `Archivo no encontrado: ${rutaAbsoluta}` };
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
const inputError = validateOfficeInput(
|
|
68
|
+
rutaAbsoluta,
|
|
69
|
+
MAX_XLSX_INPUT_BYTES,
|
|
70
|
+
"El XLSX",
|
|
71
|
+
);
|
|
72
|
+
if (inputError) return { ok: false, error: inputError };
|
|
73
|
+
|
|
59
74
|
const XLSX = await cargarXlsx();
|
|
60
75
|
const buffer = fs.readFileSync(rutaAbsoluta);
|
|
61
|
-
const
|
|
76
|
+
const deadline = Date.now() + OFFICE_PROCESSING_TIMEOUT_MS;
|
|
77
|
+
const workbook = XLSX.read(buffer, {
|
|
78
|
+
type: "buffer",
|
|
79
|
+
sheetRows: MAX_XLSX_ROWS_PER_SHEET + 2,
|
|
80
|
+
sheets: hojaFiltro,
|
|
81
|
+
});
|
|
62
82
|
|
|
63
83
|
const nombresHojas = hojaFiltro
|
|
64
84
|
? [hojaFiltro]
|
|
65
85
|
: workbook.SheetNames;
|
|
66
86
|
|
|
87
|
+
if (nombresHojas.length > MAX_XLSX_SHEETS) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: `El XLSX contiene más de ${MAX_XLSX_SHEETS} hojas`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
const hojas: Record<string, any[]> = {};
|
|
68
95
|
|
|
69
96
|
for (const nombreHoja of nombresHojas) {
|
|
97
|
+
assertBeforeDeadline(deadline, "La lectura del XLSX");
|
|
70
98
|
const hoja = workbook.Sheets[nombreHoja];
|
|
71
99
|
if (!hoja) {
|
|
72
100
|
log.warn(`Hoja '${nombreHoja}' no encontrada en el archivo`);
|
|
73
101
|
continue;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
const opciones: any = {
|
|
77
|
-
header: incluirEncabezados ? 1 : 1,
|
|
78
|
-
defval: "",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (rango) {
|
|
82
|
-
opciones.range = rango;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
104
|
if (incluirEncabezados) {
|
|
86
105
|
// La primera fila se usa como encabezados
|
|
87
106
|
hojas[nombreHoja] = XLSX.utils.sheet_to_json(hoja, {
|
|
@@ -96,6 +115,13 @@ export const officeLeerXlsxTool: Tool = {
|
|
|
96
115
|
range: rango,
|
|
97
116
|
});
|
|
98
117
|
}
|
|
118
|
+
|
|
119
|
+
if (hojas[nombreHoja].length > MAX_XLSX_ROWS_PER_SHEET) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: `La hoja '${nombreHoja}' supera el máximo de 10.000 filas`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
const totalFilas = Object.values(hojas).reduce(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const MAX_PDF_INPUT_BYTES = 25 * 1024 * 1024;
|
|
4
|
+
export const MAX_XLSX_INPUT_BYTES = 15 * 1024 * 1024;
|
|
5
|
+
export const MAX_PDF_PAGES_PER_REQUEST = 200;
|
|
6
|
+
export const MAX_XLSX_SHEETS = 50;
|
|
7
|
+
export const MAX_XLSX_ROWS_PER_SHEET = 10_000;
|
|
8
|
+
export const OFFICE_PROCESSING_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
export function validateOfficeInput(
|
|
11
|
+
filePath: string,
|
|
12
|
+
maxBytes: number,
|
|
13
|
+
label: string,
|
|
14
|
+
): string | null {
|
|
15
|
+
const stat = statSync(filePath);
|
|
16
|
+
if (!stat.isFile()) return `${label} debe ser un archivo regular`;
|
|
17
|
+
if (stat.size > maxBytes) {
|
|
18
|
+
const maxMiB = maxBytes / (1024 * 1024);
|
|
19
|
+
return `${label} excede el límite de ${maxMiB} MiB`;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertBeforeDeadline(deadline: number, label: string): void {
|
|
25
|
+
if (Date.now() > deadline) {
|
|
26
|
+
throw new Error(`${label} excedió el límite de procesamiento de 30 segundos`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolución de puertos desde el entorno.
|
|
3
|
+
*
|
|
4
|
+
* Bun 1.4 endureció `Bun.serve`: un puerto fuera de `[0, 65535]` —o `NaN`, que
|
|
5
|
+
* es lo que devuelve `parseInt("no-es-un-numero")`— ahora lanza `RangeError` en
|
|
6
|
+
* vez de recortar el valor. Un `HIVE_PORT` mal escrito dejó de degradar y pasó a
|
|
7
|
+
* tumbar el arranque con una excepción sin capturar:
|
|
8
|
+
*
|
|
9
|
+
* RangeError: The value of "options.port" is out of range.
|
|
10
|
+
* It must be an integer. Received NaN
|
|
11
|
+
*
|
|
12
|
+
* Este helper vuelve a la degradación explícita: avisa y sigue con el puerto por
|
|
13
|
+
* defecto, que para una variable de entorno mal tipeada es el comportamiento
|
|
14
|
+
* útil. El `0` se deja pasar a propósito: `Bun.serve` lo interpreta como "asigná
|
|
15
|
+
* un puerto libre" y hay código que se apoya en eso.
|
|
16
|
+
*
|
|
17
|
+
* Avisa con `console.warn` y no con el logger del proyecto a propósito: esto lo
|
|
18
|
+
* usa `config/loader.ts`, y el logger importa `config/loader.ts` para saber
|
|
19
|
+
* dónde escribir. Importarlo acá cierra el ciclo y rompe con "Cannot access
|
|
20
|
+
* 'logger' before initialization". Además, cuando se resuelve un puerto el
|
|
21
|
+
* logger todavía no está configurado.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export function resolvePort(raw: unknown, fallback: number): number {
|
|
25
|
+
if (raw === undefined || raw === null || raw === "") return fallback
|
|
26
|
+
|
|
27
|
+
const n = typeof raw === "number" ? raw : Number(String(raw).trim())
|
|
28
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
29
|
+
console.warn(`[config] Puerto inválido ${JSON.stringify(raw)}; se usa ${fallback}`)
|
|
30
|
+
return fallback
|
|
31
|
+
}
|
|
32
|
+
return n
|
|
33
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePort } from "../utils/port.ts";
|
|
1
2
|
import { col } from "../storage/hive.ts";
|
|
2
3
|
import type { ChannelDoc, ModelDoc } from "../storage/collections.ts";
|
|
3
4
|
import { loadProviderApiKey } from "../storage/crypto.ts";
|
|
@@ -281,7 +282,7 @@ class VoiceService {
|
|
|
281
282
|
|
|
282
283
|
private async speakWithPiper(text: string, voiceId?: string): Promise<AudioOutput> {
|
|
283
284
|
const cleanText = cleanTextForTTS(text);
|
|
284
|
-
const port =
|
|
285
|
+
const port = resolvePort(process.env.TTS_PORT, 5500);
|
|
285
286
|
const res = await fetch(`http://localhost:${port}/tts`, {
|
|
286
287
|
method: "POST",
|
|
287
288
|
headers: { "Content-Type": "application/json" },
|