@johpaz/hive-sdk 0.4.0 → 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/storage/seed.ts +66 -16
- 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
|
|
@@ -161,7 +161,11 @@ export const SEED_DATA: SeedData = {
|
|
|
161
161
|
// Generación actual (4.6/4.7/4.8 pasaron a "legacy"). Los IDs sin fecha ya son
|
|
162
162
|
// snapshots fijos, no alias evergreen.
|
|
163
163
|
{ id: "claude-opus-5", providerId: "anthropic", name: "Claude Opus 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 5, outputPer1M: 25 },
|
|
164
|
-
{ id: "claude-sonnet-5", providerId: "anthropic", name: "Claude Sonnet 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M:
|
|
164
|
+
{ id: "claude-sonnet-5", providerId: "anthropic", name: "Claude Sonnet 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
|
|
165
|
+
// Fable 5.1 sucede a Fable 5 en el mismo escalón y al mismo precio: es el
|
|
166
|
+
// modelo más capaz de Anthropic con disponibilidad general. Fable 5 sigue
|
|
167
|
+
// servido, así que quedan los dos.
|
|
168
|
+
{ id: "claude-fable-5-1", providerId: "anthropic", name: "Claude Fable 5.1", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
|
|
165
169
|
{ id: "claude-fable-5", providerId: "anthropic", name: "Claude Fable 5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
|
|
166
170
|
{ id: "claude-haiku-4-5-20251001", providerId: "anthropic", name: "Claude Haiku 4.5", modelType: "llm", contextWindow: 200000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1, outputPer1M: 5 },
|
|
167
171
|
|
|
@@ -182,6 +186,10 @@ export const SEED_DATA: SeedData = {
|
|
|
182
186
|
// Solo la generación 3.x: la familia 2.0 ya está apagada y la 2.5 quedó
|
|
183
187
|
// superada. `gemini-3.5-pro` y `gemini-3.1-flash-lite-preview` se quitaron:
|
|
184
188
|
// el primero no existe en el catálogo y el segundo ya salió de preview.
|
|
189
|
+
// Precio de lanzamiento hasta el 2026-12-31; el 2027-01-01 sube a 1.5/7.5,
|
|
190
|
+
// que es la tarifa con la que ya está sembrado gemini-3.6-flash.
|
|
191
|
+
{ id: "gemini-3.8-flash", providerId: "gemini", name: "Gemini 3.8 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
|
|
192
|
+
{ id: "gemini-3.7-flash", providerId: "gemini", name: "Gemini 3.7 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
|
|
185
193
|
{ id: "gemini-3.6-flash", providerId: "gemini", name: "Gemini 3.6 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 7.5 },
|
|
186
194
|
{ id: "gemini-3.5-flash", providerId: "gemini", name: "Gemini 3.5 Flash", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 9 },
|
|
187
195
|
{ id: "gemini-3.5-flash-lite", providerId: "gemini", name: "Gemini 3.5 Flash Lite", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.3, outputPer1M: 2.5 },
|
|
@@ -214,6 +222,10 @@ export const SEED_DATA: SeedData = {
|
|
|
214
222
|
// 384K de salida máxima y tool calling en ambos.
|
|
215
223
|
{ id: "deepseek-v4-pro", providerId: "deepseek", name: "DeepSeek V4 Pro", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.435, outputPer1M: 0.87 },
|
|
216
224
|
{ id: "deepseek-v4-flash", providerId: "deepseek", name: "DeepSeek V4 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
225
|
+
// Variante experimental de V4 Flash con entrada de imagen; DeepSeek la tarifa
|
|
226
|
+
// igual que la base, por eso repite precio. Las imágenes se facturan como
|
|
227
|
+
// entrada, hasta 384 tokens cada una sin importar la resolución.
|
|
228
|
+
{ id: "deepseek-v4-flash-vision-exp", providerId: "deepseek", name: "DeepSeek V4 Flash Vision (exp)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.28 },
|
|
217
229
|
|
|
218
230
|
// ── Kimi / Moonshot (fuente: platform.kimi.ai/docs/pricing/chat) ──
|
|
219
231
|
// La serie moonshot-v1-* se apaga el 2026-08-31; K2/K2.5 quedaron superadas.
|
|
@@ -223,32 +235,42 @@ export const SEED_DATA: SeedData = {
|
|
|
223
235
|
|
|
224
236
|
// ── OpenRouter (fuente: GET https://openrouter.ai/api/v1/models) ──
|
|
225
237
|
// Solo modelos vivos con `tools` en supported_parameters y publicados desde
|
|
226
|
-
// 2025-07. contextWindow = context_length reportado por el propio catálogo
|
|
238
|
+
// 2025-07. contextWindow = context_length reportado por el propio catálogo,
|
|
239
|
+
// y los precios también salen de ahí — son los de la ruta de OpenRouter, no
|
|
240
|
+
// los del proveedor nativo, y se mueven solos. Revalidado el 2026-09-03.
|
|
227
241
|
// Anthropic
|
|
242
|
+
{ id: "anthropic/claude-fable-5.1", providerId: "openrouter", name: "Claude Fable 5.1 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 10, outputPer1M: 50 },
|
|
228
243
|
{ id: "anthropic/claude-opus-5", providerId: "openrouter", name: "Claude Opus 5 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 5, outputPer1M: 25 },
|
|
229
244
|
{ id: "anthropic/claude-sonnet-5", providerId: "openrouter", name: "Claude Sonnet 5 (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
|
|
230
245
|
// OpenAI — la serie 5.6 se divide en Sol (flagship), Terra (equilibrado) y Luna (económico)
|
|
231
|
-
{ id: "openai/gpt-5.6-sol", providerId: "openrouter", name: "GPT-5.6 Sol (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M:
|
|
232
|
-
{ id: "openai/gpt-5.6-terra", providerId: "openrouter", name: "GPT-5.6 Terra (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M:
|
|
233
|
-
{ id: "openai/gpt-5.6-luna", providerId: "openrouter", name: "GPT-5.6 Luna (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.
|
|
246
|
+
{ id: "openai/gpt-5.6-sol", providerId: "openrouter", name: "GPT-5.6 Sol (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 2, outputPer1M: 10 },
|
|
247
|
+
{ id: "openai/gpt-5.6-terra", providerId: "openrouter", name: "GPT-5.6 Terra (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 2, outputPer1M: 12 },
|
|
248
|
+
{ id: "openai/gpt-5.6-luna", providerId: "openrouter", name: "GPT-5.6 Luna (OR)", modelType: "llm", contextWindow: 1050000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.2, outputPer1M: 1.2 },
|
|
234
249
|
// Google
|
|
235
|
-
{ id: "google/gemini-3.
|
|
250
|
+
{ id: "google/gemini-3.8-flash", providerId: "openrouter", name: "Gemini 3.8 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
|
|
251
|
+
{ id: "google/gemini-3.7-flash", providerId: "openrouter", name: "Gemini 3.7 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
|
|
252
|
+
{ id: "google/gemini-3.6-flash", providerId: "openrouter", name: "Gemini 3.6 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.75, outputPer1M: 3.75 },
|
|
236
253
|
{ id: "google/gemini-3.5-flash", providerId: "openrouter", name: "Gemini 3.5 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.5, outputPer1M: 9 },
|
|
237
254
|
{ id: "google/gemini-3.1-pro-preview", providerId: "openrouter", name: "Gemini 3.1 Pro (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 12 },
|
|
238
255
|
// DeepSeek
|
|
239
|
-
{ id: "deepseek/deepseek-v4-
|
|
240
|
-
{ id: "deepseek/deepseek-v4-
|
|
256
|
+
{ id: "deepseek/deepseek-v4-flash-vision-exp", providerId: "openrouter", name: "DeepSeek V4 Flash Vision exp (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.44, outputPer1M: 1.32 },
|
|
257
|
+
{ id: "deepseek/deepseek-v4-pro", providerId: "openrouter", name: "DeepSeek V4 Pro (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 1.04226, outputPer1M: 2.08452 },
|
|
258
|
+
{ id: "deepseek/deepseek-v4-flash", providerId: "openrouter", name: "DeepSeek V4 Flash (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.088606, outputPer1M: 0.177212 },
|
|
241
259
|
// Kimi
|
|
242
260
|
{ id: "moonshotai/kimi-k3", providerId: "openrouter", name: "Kimi K3 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 3, outputPer1M: 15 },
|
|
243
|
-
{ id: "moonshotai/kimi-k2.7-code", providerId: "openrouter", name: "Kimi K2.7 Code (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.
|
|
261
|
+
{ id: "moonshotai/kimi-k2.7-code", providerId: "openrouter", name: "Kimi K2.7 Code (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 0.66, outputPer1M: 3.4 },
|
|
244
262
|
// MiniMax
|
|
245
263
|
{ id: "minimax/minimax-m3", providerId: "openrouter", name: "MiniMax M3 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.3, outputPer1M: 1.2 },
|
|
246
264
|
// Z.ai / GLM
|
|
247
|
-
{ id: "z-ai/glm-5.
|
|
265
|
+
{ id: "z-ai/glm-5.3", providerId: "openrouter", name: "GLM 5.3 (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 1.4, outputPer1M: 4.4 },
|
|
266
|
+
{ id: "z-ai/glm-5.3-flash", providerId: "openrouter", name: "GLM 5.3 Flash (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
|
|
267
|
+
{ id: "z-ai/glm-5.2", providerId: "openrouter", name: "GLM 5.2 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.966, outputPer1M: 3.036 },
|
|
248
268
|
// Qwen
|
|
249
269
|
{ id: "qwen/qwen3.8-max", providerId: "openrouter", name: "Qwen3.8 Max (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
|
|
270
|
+
{ id: "qwen/qwen3.8-flash", providerId: "openrouter", name: "Qwen3.8 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.15, outputPer1M: 0.47 },
|
|
250
271
|
{ id: "qwen/qwen3.7-flash", providerId: "openrouter", name: "Qwen3.7 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.03, outputPer1M: 0.13 },
|
|
251
272
|
// xAI
|
|
273
|
+
{ id: "x-ai/grok-4.6", providerId: "openrouter", name: "Grok 4.6 (OR)", modelType: "llm", contextWindow: 500000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
|
|
252
274
|
{ id: "x-ai/grok-4.5", providerId: "openrouter", name: "Grok 4.5 (OR)", modelType: "llm", contextWindow: 500000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
|
|
253
275
|
// Mistral
|
|
254
276
|
{ id: "mistralai/mistral-medium-3-5", providerId: "openrouter", name: "Mistral Medium 3.5 (OR)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code"]), inputPer1M: 1.5, outputPer1M: 7.5 },
|
|
@@ -283,9 +305,14 @@ export const SEED_DATA: SeedData = {
|
|
|
283
305
|
{ id: "eleven_v3", providerId: "elevenlabs", name: "Eleven V3", modelType: "tts", contextWindow: 0, capabilities: JSON.stringify(["tts", "speech", "expressive"]) },
|
|
284
306
|
|
|
285
307
|
// ── Qwen (Alibaba DashScope / Model Studio) ──
|
|
286
|
-
// Serie 3.
|
|
287
|
-
// OpenRouter, que enruta a los mismos modelos: el
|
|
288
|
-
// estaba sembrado con 32768 en realidad tiene 262144.
|
|
308
|
+
// Serie 3.8 = generación actual; la 3.7 sigue servida. Los contextWindow salen
|
|
309
|
+
// del catálogo de OpenRouter, que enruta a los mismos modelos: el
|
|
310
|
+
// `qwen3.6-max-preview` que estaba sembrado con 32768 en realidad tiene 262144.
|
|
311
|
+
// Serie 3.8: Max es el buque insignia y Flash el de alto volumen. Precios del
|
|
312
|
+
// endpoint internacional (Singapur), que es el `baseUrl` sembrado — el de
|
|
313
|
+
// China continental cobra entre 60% y 70% menos.
|
|
314
|
+
{ id: "qwen3.8-max", providerId: "qwen", name: "Qwen 3.8 Max", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
|
|
315
|
+
{ id: "qwen3.8-flash", providerId: "qwen", name: "Qwen 3.8 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.14, outputPer1M: 0.42 },
|
|
289
316
|
{ id: "qwen3.7-max", providerId: "qwen", name: "Qwen 3.7 Max", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.475, outputPer1M: 4.425 },
|
|
290
317
|
{ id: "qwen3.7-plus", providerId: "qwen", name: "Qwen 3.7 Plus", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.32, outputPer1M: 1.28 },
|
|
291
318
|
{ id: "qwen3.6-flash", providerId: "qwen", name: "Qwen 3.6 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.1875, outputPer1M: 1.125 },
|
|
@@ -300,10 +327,13 @@ export const SEED_DATA: SeedData = {
|
|
|
300
327
|
// Solo los mejores modelos agénticos (tool calling) del catálogo vivo. NVIDIA
|
|
301
328
|
// retira modelos del endpoint sin avisar y responde 410 Gone al llamarlos, así
|
|
302
329
|
// que esta lista se valida contra /v1/models — no contra la web de build.nvidia.com,
|
|
303
|
-
// que sigue mostrando fichas de modelos ya retirados. Verificado 2026-
|
|
330
|
+
// que sigue mostrando fichas de modelos ya retirados. Verificado 2026-09-03.
|
|
304
331
|
// Nota: Qwen ya no tiene ningún modelo en el catálogo NVIDIA (todos retirados);
|
|
305
332
|
// para Qwen usar el provider `qwen` (DashScope) directamente.
|
|
306
|
-
|
|
333
|
+
// z-ai/glm-5.2 se sacó: NVIDIA lo retiró de /v1/models. Para GLM usar el
|
|
334
|
+
// provider `z-ai` directo, o el enrutado de OpenRouter.
|
|
335
|
+
{ id: "moonshotai/kimi-k3", providerId: "nvidia", name: "Kimi K3 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
336
|
+
{ id: "nvidia/nemotron-3.5-lightning-30b-a3b", providerId: "nvidia", name: "Nemotron 3.5 Lightning 30B", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
307
337
|
{ id: "moonshotai/kimi-k2.6", providerId: "nvidia", name: "Kimi K2.6 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
308
338
|
{ id: "minimaxai/minimax-m3", providerId: "nvidia", name: "MiniMax M3 (NVIDIA)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
309
339
|
{ id: "nvidia/nemotron-3-ultra-550b-a55b", providerId: "nvidia", name: "Nemotron 3 Ultra 550B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
@@ -325,6 +355,7 @@ export const SEED_DATA: SeedData = {
|
|
|
325
355
|
{ id: "Qwen-Ambassador/Qwen3.7-Max", providerId: "modelscope", name: "Qwen3.7 Max (Embajador)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
326
356
|
{ id: "Qwen-Ambassador/Qwen3.7-Plus", providerId: "modelscope", name: "Qwen3.7 Plus (Embajador)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
327
357
|
// Open-weight del mismo endpoint, para cuentas sin permiso de embajador.
|
|
358
|
+
{ id: "Qwen/Qwen3.8-27B", providerId: "modelscope", name: "Qwen3.8 27B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
328
359
|
{ id: "Qwen/Qwen3.5-397B-A17B", providerId: "modelscope", name: "Qwen3.5 397B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
329
360
|
{ id: "Qwen/Qwen3-Next-80B-A3B-Instruct", providerId: "modelscope", name: "Qwen3 Next 80B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
|
|
330
361
|
{ id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", providerId: "modelscope", name: "Qwen3 Coder 30B (ModelScope)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
|
|
@@ -338,11 +369,30 @@ export const SEED_DATA: SeedData = {
|
|
|
338
369
|
{ id: "MiniMax-M2.7-highspeed", providerId: "minimax", name: "MiniMax M2.7 Highspeed", modelType: "llm", contextWindow: 204800, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0.3, outputPer1M: 1.2 },
|
|
339
370
|
|
|
340
371
|
// ── Z.ai / GLM (fuente: docs.z.ai/guides/llm) — OpenAI-compatible endpoint ──
|
|
372
|
+
// GLM-5.3 es sólo texto (código y ciberseguridad); el Flash es el multimodal
|
|
373
|
+
// barato. OJO: 0.075/0.25 del Flash es promoción a mitad de precio hasta el
|
|
374
|
+
// 2026-09-09 (24:00 UTC+8); la tarifa de lista es 0.15/0.50.
|
|
375
|
+
{ id: "glm-5.3", providerId: "z-ai", name: "GLM 5.3", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 1.4, outputPer1M: 4.4 },
|
|
376
|
+
{ id: "glm-5.3-flash", providerId: "z-ai", name: "GLM 5.3 Flash", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
|
|
341
377
|
{ id: "glm-5.2", providerId: "z-ai", name: "GLM 5.2", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.63, outputPer1M: 1.98 },
|
|
342
378
|
{ id: "glm-5.1", providerId: "z-ai", name: "GLM 5.1", modelType: "llm", contextWindow: 204800, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.97, outputPer1M: 3.04 },
|
|
343
379
|
{ id: "glm-5", providerId: "z-ai", name: "GLM 5", modelType: "llm", contextWindow: 200000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.97, outputPer1M: 3.04 },
|
|
344
380
|
|
|
345
|
-
// ── OpenCode Go (fuente: opencode.ai)
|
|
381
|
+
// ── OpenCode Go (fuente: GET https://opencode.ai/zen/go/v1/models) ──
|
|
382
|
+
// Endpoint gratuito y OpenAI-compatible, por eso todo va con precio 0. El
|
|
383
|
+
// contextWindow es conservador a propósito: el listado no publica el ctx
|
|
384
|
+
// servido, y quedarse corto sólo adelanta la compactación mientras que
|
|
385
|
+
// pasarse revienta la llamada. Verificado 2026-09-03.
|
|
386
|
+
{ id: "glm-5.3", providerId: "opencode-go", name: "GLM-5.3", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
387
|
+
{ id: "glm-5.3-flash", providerId: "opencode-go", name: "GLM-5.3 Flash", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
388
|
+
{ id: "glm-5.2", providerId: "opencode-go", name: "GLM-5.2", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
389
|
+
{ id: "kimi-k3", providerId: "opencode-go", name: "Kimi K3", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
390
|
+
{ id: "kimi-k2.7-code", providerId: "opencode-go", name: "Kimi K2.7 Code", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
|
|
391
|
+
{ id: "qwen3.8-max", providerId: "opencode-go", name: "Qwen3.8 Max", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
392
|
+
{ id: "qwen3.8-flash", providerId: "opencode-go", name: "Qwen3.8 Flash", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
393
|
+
{ id: "grok-4.6", providerId: "opencode-go", name: "Grok 4.6", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
394
|
+
{ id: "deepseek-v4-flash-vision-exp", providerId: "opencode-go", name: "DeepSeek V4 Flash Vision (exp)", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
395
|
+
{ id: "hy4-preview", providerId: "opencode-go", name: "Hunyuan 4 Preview", modelType: "llm", contextWindow: 128000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
346
396
|
{ id: "minimax-m3", providerId: "opencode-go", name: "MiniMax M3", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
|
|
347
397
|
{ id: "minimax-m2.7", providerId: "opencode-go", name: "MiniMax M2.7", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
|
|
348
398
|
{ id: "minimax-m2.5", providerId: "opencode-go", name: "MiniMax M2.5", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "function_calling", "streaming"]), inputPer1M: 0, outputPer1M: 0 },
|
|
@@ -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" },
|