zerochat 7.3.0__py3-none-any.whl
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.
- zerochat-7.3.0.dist-info/METADATA +70 -0
- zerochat-7.3.0.dist-info/RECORD +130 -0
- zerochat-7.3.0.dist-info/WHEEL +5 -0
- zerochat-7.3.0.dist-info/entry_points.txt +2 -0
- zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
- zerochat-7.3.0.dist-info/top_level.txt +2 -0
- zerochat.py +2004 -0
- zerochat_runtime/__init__.py +1 -0
- zerochat_runtime/assets/css/base.css +199 -0
- zerochat_runtime/assets/css/components/composer.css +1258 -0
- zerochat_runtime/assets/css/components/debug.css +777 -0
- zerochat_runtime/assets/css/components/header.css +97 -0
- zerochat_runtime/assets/css/components/icons.css +39 -0
- zerochat_runtime/assets/css/components/markdown.css +685 -0
- zerochat_runtime/assets/css/components/messages.css +473 -0
- zerochat_runtime/assets/css/components/modals.css +1319 -0
- zerochat_runtime/assets/css/components/sidebar.css +622 -0
- zerochat_runtime/assets/css/components/tools.css +1223 -0
- zerochat_runtime/assets/css/layout.css +171 -0
- zerochat_runtime/assets/css/print.css +120 -0
- zerochat_runtime/assets/css/styles.css +18 -0
- zerochat_runtime/assets/css/theme-overrides.css +555 -0
- zerochat_runtime/assets/css/tokens.css +165 -0
- zerochat_runtime/assets/help/architecture.html +247 -0
- zerochat_runtime/assets/help/composer.html +134 -0
- zerochat_runtime/assets/help/conversations.html +105 -0
- zerochat_runtime/assets/help/debug.html +108 -0
- zerochat_runtime/assets/help/en/architecture.html +247 -0
- zerochat_runtime/assets/help/en/composer.html +134 -0
- zerochat_runtime/assets/help/en/conversations.html +104 -0
- zerochat_runtime/assets/help/en/debug.html +105 -0
- zerochat_runtime/assets/help/en/gemini-free.html +39 -0
- zerochat_runtime/assets/help/en/index.html +191 -0
- zerochat_runtime/assets/help/en/learning.html +71 -0
- zerochat_runtime/assets/help/en/mcp.html +143 -0
- zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/en/profiles.html +173 -0
- zerochat_runtime/assets/help/en/rag.html +130 -0
- zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
- zerochat_runtime/assets/help/en/tools-agent.html +133 -0
- zerochat_runtime/assets/help/en/webllm.html +355 -0
- zerochat_runtime/assets/help/gemini-free.html +39 -0
- zerochat_runtime/assets/help/help.css +604 -0
- zerochat_runtime/assets/help/help.js +51 -0
- zerochat_runtime/assets/help/index.html +194 -0
- zerochat_runtime/assets/help/learning.html +71 -0
- zerochat_runtime/assets/help/mcp.html +143 -0
- zerochat_runtime/assets/help/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/profiles.html +174 -0
- zerochat_runtime/assets/help/rag.html +130 -0
- zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
- zerochat_runtime/assets/help/tools-agent.html +133 -0
- zerochat_runtime/assets/help/webllm.html +359 -0
- zerochat_runtime/assets/js/agent-core.js +1553 -0
- zerochat_runtime/assets/js/api.js +844 -0
- zerochat_runtime/assets/js/app.js +2558 -0
- zerochat_runtime/assets/js/attachments.js +220 -0
- zerochat_runtime/assets/js/charts.js +338 -0
- zerochat_runtime/assets/js/chat-engine.js +566 -0
- zerochat_runtime/assets/js/config-store.js +207 -0
- zerochat_runtime/assets/js/context-manager.js +584 -0
- zerochat_runtime/assets/js/conversation-service.js +484 -0
- zerochat_runtime/assets/js/cookies.js +688 -0
- zerochat_runtime/assets/js/data-reset-service.js +136 -0
- zerochat_runtime/assets/js/debug.js +508 -0
- zerochat_runtime/assets/js/defaults.js +16 -0
- zerochat_runtime/assets/js/export.js +179 -0
- zerochat_runtime/assets/js/file-parser.js +2088 -0
- zerochat_runtime/assets/js/generation-controller.js +417 -0
- zerochat_runtime/assets/js/i18n.js +1483 -0
- zerochat_runtime/assets/js/icons.js +156 -0
- zerochat_runtime/assets/js/ingestionEngine.js +436 -0
- zerochat_runtime/assets/js/markdown.js +588 -0
- zerochat_runtime/assets/js/mcp.js +1271 -0
- zerochat_runtime/assets/js/message-turns.js +62 -0
- zerochat_runtime/assets/js/profile-backup.js +118 -0
- zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
- zerochat_runtime/assets/js/profile-repository.js +213 -0
- zerochat_runtime/assets/js/providers-webllm.js +486 -0
- zerochat_runtime/assets/js/providers.js +1560 -0
- zerochat_runtime/assets/js/rag-index.js +295 -0
- zerochat_runtime/assets/js/rag-service.js +530 -0
- zerochat_runtime/assets/js/rag-ui.js +997 -0
- zerochat_runtime/assets/js/ragStorage.js +676 -0
- zerochat_runtime/assets/js/sandbox.js +449 -0
- zerochat_runtime/assets/js/state.js +704 -0
- zerochat_runtime/assets/js/storage-db.js +144 -0
- zerochat_runtime/assets/js/tool-cards.js +330 -0
- zerochat_runtime/assets/js/tool-security.js +653 -0
- zerochat_runtime/assets/js/tools/README.md +50 -0
- zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
- zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
- zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
- zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
- zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
- zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
- zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
- zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
- zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
- zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
- zerochat_runtime/assets/js/ui-composer.js +319 -0
- zerochat_runtime/assets/js/ui-conversation.js +655 -0
- zerochat_runtime/assets/js/ui-dialogs.js +132 -0
- zerochat_runtime/assets/js/ui-generation-status.js +119 -0
- zerochat_runtime/assets/js/ui-inspector.js +804 -0
- zerochat_runtime/assets/js/ui-mcp.js +604 -0
- zerochat_runtime/assets/js/ui-profiles.js +824 -0
- zerochat_runtime/assets/js/ui-reasoning.js +146 -0
- zerochat_runtime/assets/js/ui-settings.js +825 -0
- zerochat_runtime/assets/js/ui-shell.js +215 -0
- zerochat_runtime/assets/js/ui-sidebar.js +429 -0
- zerochat_runtime/assets/js/ui-telemetry.js +404 -0
- zerochat_runtime/assets/js/ui-transfer.js +262 -0
- zerochat_runtime/assets/js/utils.js +216 -0
- zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
- zerochat_runtime/assets/js/web-browser.js +569 -0
- zerochat_runtime/assets/js/web-search.js +519 -0
- zerochat_runtime/assets/manifest.webmanifest +20 -0
- zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
- zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
- zerochat_runtime/assets/services/lsp/installer.json +9 -0
- zerochat_runtime/assets/services/lsp/service.json +22 -0
- zerochat_runtime/assets/services/memory/installer.json +9 -0
- zerochat_runtime/assets/services/memory/service.json +24 -0
- zerochat_runtime/assets/services/playwright/installer.json +10 -0
- zerochat_runtime/assets/services/playwright/service.json +39 -0
- zerochat_runtime/assets/sw.js +164 -0
- zerochat_runtime/assets/zerochat.html +671 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Módulo de Consulta, Extracción Web y Documentos PDF Desacoplado (ChatWebBrowser) para ZeroChat.
|
|
3
|
+
*
|
|
4
|
+
* ==============================================================================================
|
|
5
|
+
* DOCUMENTACIÓN DEL MODELO DE SEGURIDAD Y LIMITACIONES DEL NAVEGADOR
|
|
6
|
+
* ==============================================================================================
|
|
7
|
+
* 1. Política del Mismo Origen (Same-Origin Policy - SOP) y CORS:
|
|
8
|
+
* - Los navegadores web prohíben por defecto que el código JavaScript del cliente acceda
|
|
9
|
+
* directamente al contenido de orígenes web de terceros mediante fetch() o XMLHttpRequest
|
|
10
|
+
* a menos que el servidor de destino emita cabeceras 'Access-Control-Allow-Origin'.
|
|
11
|
+
* - Para permitir la lectura universal de artículos y páginas web públicas solicitadas por
|
|
12
|
+
* el modelo agéntico, ZeroChat utiliza estrategias desacopladas:
|
|
13
|
+
* a) Gateway Reader para LLMs (ej: Jina Reader / r.jina.ai): Procesa y devuelve Markdown limpio.
|
|
14
|
+
* b) Petición Directa (DirectFetch): Utilizada para servidores con CORS habilitado o endpoints locales.
|
|
15
|
+
* c) Proxy CORS (AllOrigins): Fallback de contingencia para obtener el HTML crudo.
|
|
16
|
+
*
|
|
17
|
+
* 2. Mitigación de SSRF (Server-Side Request Forgery) en el Cliente Web:
|
|
18
|
+
* - Los agentes LLM pueden recibir o generar URLs arbitrarias. ZeroChat aplica validación estricta
|
|
19
|
+
* antes de despachar cualquier petición:
|
|
20
|
+
* a) Protocolos: Exclusivamente http:// y https://. Se rechazan esquemas como file://, data://,
|
|
21
|
+
* javascript://, vbscript://, blob://, etc.
|
|
22
|
+
* b) Metadatos Cloud: Se bloquean de forma terminante accesos a endpoints de metadatos de
|
|
23
|
+
* proveedores cloud (AWS / GCP / Azure / DigitalOcean / Alibaba): 169.254.169.254,
|
|
24
|
+
* metadata.google.internal, instance-data, 100.100.100.200.
|
|
25
|
+
* c) Evasiones de IP: Detección y normalización de notaciones decimales, octales o hexadecimales
|
|
26
|
+
* (ej: 2130706433, 0x7f000001) para evitar bypasses de direcciones loopback o locales.
|
|
27
|
+
* d) Control de Longitud y Timeout: Truncado estricto (MAX_CONTENT_LENGTH) y AbortController
|
|
28
|
+
* para evitar consumo excesivo de memoria, DoS o desbordamiento de contexto en el LLM.
|
|
29
|
+
* ==============================================================================================
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
(function (root, factory) {
|
|
33
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
34
|
+
module.exports = factory();
|
|
35
|
+
} else {
|
|
36
|
+
root.ChatWebBrowser = factory();
|
|
37
|
+
}
|
|
38
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
const MAX_CONTENT_LENGTH = 60000;
|
|
42
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
43
|
+
|
|
44
|
+
function getFileParser() {
|
|
45
|
+
if (typeof window !== 'undefined' && window.ChatFileParser) {
|
|
46
|
+
return window.ChatFileParser;
|
|
47
|
+
}
|
|
48
|
+
if (typeof require !== 'undefined') {
|
|
49
|
+
try {
|
|
50
|
+
return require('./file-parser.js');
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getUtils() {
|
|
57
|
+
if (typeof window !== 'undefined' && window.ChatUtils) return window.ChatUtils;
|
|
58
|
+
if (typeof require !== 'undefined') {
|
|
59
|
+
try { return require('./utils.js'); } catch (e) {}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Realiza un fetch con timeout controlado mediante AbortController.
|
|
66
|
+
*/
|
|
67
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
68
|
+
const Utils = getUtils();
|
|
69
|
+
if (Utils && typeof Utils.fetchWithTimeout === 'function') {
|
|
70
|
+
return Utils.fetchWithTimeout(url, options, timeoutMs);
|
|
71
|
+
}
|
|
72
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
73
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return await fetch(url, {
|
|
77
|
+
...options,
|
|
78
|
+
signal: controller ? controller.signal : undefined
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
if (timer) clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Analiza y normaliza posibles evasiones numéricas (decimal, octal, hex) y rangos privados/locales de direcciones IP.
|
|
87
|
+
*/
|
|
88
|
+
function isDangerousIpAddress(hostname, allowLocal = false) {
|
|
89
|
+
if (!hostname) return false;
|
|
90
|
+
const cleanHost = hostname.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
91
|
+
|
|
92
|
+
// Cloud Metadata Endpoints (Siempre bloqueados de forma terminante)
|
|
93
|
+
if (cleanHost === '169.254.169.254' || cleanHost === 'metadata.google.internal' || cleanHost === 'instance-data' || cleanHost === '100.100.100.200') {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Comprobar si es un número decimal entero representando una IP (ej: 2130706433 = 127.0.0.1 o 2852039166 = 169.254.169.254)
|
|
98
|
+
if (/^\d+$/.test(cleanHost)) {
|
|
99
|
+
const num = parseInt(cleanHost, 10);
|
|
100
|
+
if (!isNaN(num) && num >= 0 && num <= 4294967295) {
|
|
101
|
+
const b1 = (num >>> 24) & 255;
|
|
102
|
+
const b2 = (num >>> 16) & 255;
|
|
103
|
+
if (b1 === 127 || (b1 === 169 && b2 === 254) || b1 === 10 || (b1 === 172 && b2 >= 16 && b2 <= 31) || (b1 === 192 && b2 === 168) || b1 === 0) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Comprobar si es notación hexadecimal (ej: 0x7f000001)
|
|
110
|
+
if (/^0x[0-9a-fA-F]+$/.test(cleanHost)) {
|
|
111
|
+
const num = parseInt(cleanHost, 16);
|
|
112
|
+
if (!isNaN(num) && num >= 0 && num <= 4294967295) {
|
|
113
|
+
const b1 = (num >>> 24) & 255;
|
|
114
|
+
const b2 = (num >>> 16) & 255;
|
|
115
|
+
if (b1 === 127 || (b1 === 169 && b2 === 254) || b1 === 10 || (b1 === 172 && b2 >= 16 && b2 <= 31) || (b1 === 192 && b2 === 168) || b1 === 0) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Si no se permiten recursos locales/internos, bloquear loopback y rangos privados
|
|
122
|
+
if (!allowLocal) {
|
|
123
|
+
if (cleanHost === 'localhost' || cleanHost === '0.0.0.0' || cleanHost === '127.0.0.1' || cleanHost.startsWith('127.') || cleanHost === '::1' || cleanHost === '0:0:0:0:0:0:0:1') {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (cleanHost.startsWith('192.168.') || cleanHost.startsWith('10.') || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(cleanHost) || cleanHost.startsWith('169.254.')) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Valida la URL de destino para prevenir SSRF en servicios de metadatos o esquemas no seguros.
|
|
136
|
+
*/
|
|
137
|
+
function validateUrlForFetch(rawUrl, options = {}) {
|
|
138
|
+
if (!rawUrl || typeof rawUrl !== 'string') {
|
|
139
|
+
return { valid: false, error: 'No se proporcionó una URL válida.' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let url = rawUrl.trim();
|
|
143
|
+
const schemeMatch = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
144
|
+
if (schemeMatch) {
|
|
145
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
146
|
+
if (scheme !== 'http' && scheme !== 'https') {
|
|
147
|
+
return { valid: false, error: `Protocolo no permitido: ${scheme}:. Solo se admiten http:// y https://.` };
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
url = 'https://' + url;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const parsed = new URL(url);
|
|
155
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
156
|
+
return { valid: false, error: `Protocolo no permitido: ${parsed.protocol}. Solo se admiten http:// y https://.` };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
160
|
+
const allowLocal = options.allowLocal === true;
|
|
161
|
+
if (isDangerousIpAddress(hostname, allowLocal)) {
|
|
162
|
+
return { valid: false, error: 'Acceso bloqueado: intento de acceso a servicios internos, recursos locales o de metadatos de infraestructura (SSRF protection).' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { valid: true, url: parsed.toString(), hostname: hostname };
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return { valid: false, error: 'Formato de URL inválido.' };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extrae el texto legible y estructurado de un documento HTML local.
|
|
173
|
+
*/
|
|
174
|
+
function extractReadableTextFromHtml(htmlString, targetUrl) {
|
|
175
|
+
try {
|
|
176
|
+
if (typeof DOMParser !== 'undefined') {
|
|
177
|
+
const parser = new DOMParser();
|
|
178
|
+
const doc = parser.parseFromString(htmlString, 'text/html');
|
|
179
|
+
|
|
180
|
+
const elementsToRemove = doc.querySelectorAll('script, style, noscript, iframe, svg, canvas, nav, footer, form');
|
|
181
|
+
elementsToRemove.forEach(el => el.remove());
|
|
182
|
+
|
|
183
|
+
const title = doc.querySelector('title') ? doc.querySelector('title').innerText.trim() : '';
|
|
184
|
+
const metaDesc = doc.querySelector('meta[name="description"]') ? doc.querySelector('meta[name="description"]').getAttribute('content') : '';
|
|
185
|
+
|
|
186
|
+
const mainElement = doc.querySelector('main, article, #content, .content, #main, .main-content') || doc.body;
|
|
187
|
+
let text = mainElement ? (mainElement.innerText || mainElement.textContent || '') : '';
|
|
188
|
+
|
|
189
|
+
text = text.replace(/\r\n/g, '\n')
|
|
190
|
+
.replace(/[ \t]+/g, ' ')
|
|
191
|
+
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|
192
|
+
.trim();
|
|
193
|
+
|
|
194
|
+
let headerInfo = `[Título: ${title || 'Sin título'}]\n[URL: ${targetUrl}]\n`;
|
|
195
|
+
if (metaDesc) headerInfo += `[Descripción: ${metaDesc}]\n`;
|
|
196
|
+
headerInfo += '\n--- Contenido de la página ---\n';
|
|
197
|
+
|
|
198
|
+
const fullOutput = headerInfo + text;
|
|
199
|
+
if (fullOutput.length > MAX_CONTENT_LENGTH) {
|
|
200
|
+
return fullOutput.slice(0, MAX_CONTENT_LENGTH) + '\n\n[... Contenido truncado por longitud ...]';
|
|
201
|
+
}
|
|
202
|
+
return fullOutput;
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {}
|
|
205
|
+
|
|
206
|
+
// Fallback simple basado en expresiones regulares para Node.js o entornos sin DOMParser
|
|
207
|
+
const cleanText = htmlString
|
|
208
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
209
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
|
210
|
+
.replace(/<[^>]+>/g, ' ')
|
|
211
|
+
.replace(/[ \t]+/g, ' ')
|
|
212
|
+
.replace(/\n\s*\n+/g, '\n\n')
|
|
213
|
+
.trim();
|
|
214
|
+
|
|
215
|
+
return cleanText.slice(0, MAX_CONTENT_LENGTH);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Interfaz Base para estrategias de extracción de contenido (FetchStrategy).
|
|
220
|
+
*/
|
|
221
|
+
class BaseFetchStrategy {
|
|
222
|
+
constructor(options = {}) {
|
|
223
|
+
this.name = options.name || 'base';
|
|
224
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async canHandle(url, context = {}) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async fetch(url, context = {}) {
|
|
232
|
+
throw new Error(`El método fetch() debe implementarse en ${this.constructor.name}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Estrategia de lectura optimizada para LLM mediante Gateway Reader (Jina Reader).
|
|
238
|
+
*/
|
|
239
|
+
class ReaderGatewayStrategy extends BaseFetchStrategy {
|
|
240
|
+
constructor(options = {}) {
|
|
241
|
+
super({ name: 'ReaderGateway', timeoutMs: 12000, ...options });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async canHandle(url, context = {}) {
|
|
245
|
+
// No aplicar gateway a hosts locales
|
|
246
|
+
return !context.isLocalUrl;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async fetch(url, context = {}) {
|
|
250
|
+
const readerUrl = `https://r.jina.ai/${url}`;
|
|
251
|
+
const res = await fetchWithTimeout(readerUrl, {
|
|
252
|
+
headers: {
|
|
253
|
+
'Accept': 'text/plain',
|
|
254
|
+
'X-Return-Format': 'markdown',
|
|
255
|
+
'X-No-Cache': 'true'
|
|
256
|
+
}
|
|
257
|
+
}, this.timeoutMs);
|
|
258
|
+
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
throw new Error(`Reader Gateway respondió con HTTP ${res.status}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let content = await res.text();
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
content: content,
|
|
267
|
+
byteSize: content.length,
|
|
268
|
+
status: 200,
|
|
269
|
+
strategy: this.name
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Estrategia de descarga y extracción de documentos PDF locales/remotos.
|
|
276
|
+
*/
|
|
277
|
+
class LocalPdfStrategy extends BaseFetchStrategy {
|
|
278
|
+
constructor(options = {}) {
|
|
279
|
+
super({ name: 'LocalPdfExtractor', timeoutMs: 10000, ...options });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async canHandle(url, context = {}) {
|
|
283
|
+
return context.isPdfUrl || false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async fetch(url, context = {}) {
|
|
287
|
+
const FileParser = getFileParser();
|
|
288
|
+
if (!FileParser || !FileParser.extractTextFromPdf) {
|
|
289
|
+
throw new Error('El módulo ChatFileParser no está disponible para procesar PDFs.');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const res = await fetchWithTimeout(url, {
|
|
293
|
+
headers: { 'Accept': 'application/pdf,*/*' }
|
|
294
|
+
}, this.timeoutMs);
|
|
295
|
+
|
|
296
|
+
if (!res.ok) {
|
|
297
|
+
throw new Error(`Error descargando PDF (HTTP ${res.status})`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
301
|
+
const extractedText = await FileParser.extractTextFromPdf(arrayBuffer);
|
|
302
|
+
const fileName = url.split('/').pop().split('?')[0] || 'documento.pdf';
|
|
303
|
+
const sizeStr = FileParser.formatBytes ? FileParser.formatBytes(arrayBuffer.byteLength) : arrayBuffer.byteLength + ' B';
|
|
304
|
+
|
|
305
|
+
const content = `[Documento PDF: ${fileName}]\n[URL: ${url}]\n[Tamaño: ${sizeStr}]\n\n--- Contenido extraído del PDF ---\n\n${extractedText}`;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
content: content,
|
|
310
|
+
byteSize: arrayBuffer.byteLength,
|
|
311
|
+
status: 200,
|
|
312
|
+
isPdf: true,
|
|
313
|
+
strategy: this.name
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Estrategia de acceso mediante Proxy CORS (AllOrigins).
|
|
320
|
+
*/
|
|
321
|
+
class CorsProxyStrategy extends BaseFetchStrategy {
|
|
322
|
+
constructor(options = {}) {
|
|
323
|
+
super({ name: 'CorsProxy', timeoutMs: 8000, ...options });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async canHandle(url, context = {}) {
|
|
327
|
+
return !context.isLocalUrl;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async fetch(url, context = {}) {
|
|
331
|
+
const allOriginsUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
|
|
332
|
+
const res = await fetchWithTimeout(allOriginsUrl, {}, this.timeoutMs);
|
|
333
|
+
|
|
334
|
+
if (!res.ok) {
|
|
335
|
+
throw new Error(`Proxy CORS respondió con HTTP ${res.status}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
const rawHtml = data.contents || '';
|
|
340
|
+
if (!rawHtml) {
|
|
341
|
+
throw new Error('Proxy CORS no devolvió contenido.');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const content = extractReadableTextFromHtml(rawHtml, url);
|
|
345
|
+
return {
|
|
346
|
+
success: true,
|
|
347
|
+
content: content,
|
|
348
|
+
byteSize: rawHtml.length,
|
|
349
|
+
status: data.status?.http_code || 200,
|
|
350
|
+
strategy: this.name
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Estrategia de petición HTTP directa estándar.
|
|
357
|
+
*/
|
|
358
|
+
class DirectFetchStrategy extends BaseFetchStrategy {
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
super({ name: 'DirectFetch', timeoutMs: 6000, ...options });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async canHandle(url, context = {}) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async fetch(url, context = {}) {
|
|
368
|
+
const FileParser = getFileParser();
|
|
369
|
+
const res = await fetchWithTimeout(url, {
|
|
370
|
+
headers: { 'Accept': 'text/html,application/xhtml+xml,application/json,application/pdf,text/plain;q=0.9,*/*;q=0.8' }
|
|
371
|
+
}, this.timeoutMs);
|
|
372
|
+
|
|
373
|
+
const contentType = (res.headers.get('content-type') || '').toLowerCase();
|
|
374
|
+
const isPdf = context.isPdfUrl || contentType.includes('application/pdf');
|
|
375
|
+
|
|
376
|
+
let content = '';
|
|
377
|
+
let byteSize = 0;
|
|
378
|
+
|
|
379
|
+
if (isPdf && FileParser && FileParser.extractTextFromPdf) {
|
|
380
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
381
|
+
byteSize = arrayBuffer.byteLength;
|
|
382
|
+
const extractedText = await FileParser.extractTextFromPdf(arrayBuffer);
|
|
383
|
+
const fileName = url.split('/').pop().split('?')[0] || 'documento.pdf';
|
|
384
|
+
content = `[Documento PDF: ${fileName}]\n[URL: ${url}]\n\n--- Contenido extraído del PDF ---\n\n${extractedText}`;
|
|
385
|
+
} else {
|
|
386
|
+
const rawText = await res.text();
|
|
387
|
+
byteSize = rawText.length;
|
|
388
|
+
if (rawText.includes('<html') || rawText.includes('<!DOCTYPE')) {
|
|
389
|
+
content = extractReadableTextFromHtml(rawText, url);
|
|
390
|
+
} else {
|
|
391
|
+
content = rawText.slice(0, MAX_CONTENT_LENGTH);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
success: res.ok,
|
|
397
|
+
content: content,
|
|
398
|
+
byteSize: byteSize,
|
|
399
|
+
status: res.status,
|
|
400
|
+
isPdf: isPdf,
|
|
401
|
+
strategy: this.name,
|
|
402
|
+
error: res.ok ? undefined : `HTTP ${res.status}: ${res.statusText}`
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Orquestador de estrategias de lectura web (PageFetcherPipeline).
|
|
409
|
+
*/
|
|
410
|
+
class PageFetcherPipeline {
|
|
411
|
+
constructor() {
|
|
412
|
+
this.strategies = [
|
|
413
|
+
new ReaderGatewayStrategy(),
|
|
414
|
+
new LocalPdfStrategy(),
|
|
415
|
+
new DirectFetchStrategy(),
|
|
416
|
+
new CorsProxyStrategy()
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Registra o añade una estrategia al pipeline.
|
|
422
|
+
*/
|
|
423
|
+
addStrategy(strategy, unshift = false) {
|
|
424
|
+
if (unshift) {
|
|
425
|
+
this.strategies.unshift(strategy);
|
|
426
|
+
} else {
|
|
427
|
+
this.strategies.push(strategy);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Ejecuta las estrategias en orden hasta obtener una respuesta exitosa.
|
|
433
|
+
*/
|
|
434
|
+
async fetch(rawUrl, options = {}) {
|
|
435
|
+
const validation = validateUrlForFetch(rawUrl, options);
|
|
436
|
+
if (!validation.valid) {
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
url: rawUrl || '',
|
|
440
|
+
content: '',
|
|
441
|
+
elapsedMs: 0,
|
|
442
|
+
error: validation.error
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const url = validation.url;
|
|
447
|
+
const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
448
|
+
const isLocalUrl = /^(localhost|127\.0\.0\.1|192\.168\.|10\.|0\.0\.0\.0)/i.test(validation.hostname);
|
|
449
|
+
const isPdfUrl = /\.pdf(\?|#|$)/i.test(url);
|
|
450
|
+
|
|
451
|
+
const context = {
|
|
452
|
+
isLocalUrl,
|
|
453
|
+
isPdfUrl,
|
|
454
|
+
hostname: validation.hostname,
|
|
455
|
+
...options
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
let lastError = null;
|
|
459
|
+
|
|
460
|
+
// Si es un host local, intentar DirectFetch prioritariamente
|
|
461
|
+
const executionList = isLocalUrl
|
|
462
|
+
? [new DirectFetchStrategy({ timeoutMs: 5000 })]
|
|
463
|
+
: this.strategies;
|
|
464
|
+
|
|
465
|
+
for (const strategy of executionList) {
|
|
466
|
+
try {
|
|
467
|
+
const can = await strategy.canHandle(url, context);
|
|
468
|
+
if (!can) continue;
|
|
469
|
+
|
|
470
|
+
const result = await strategy.fetch(url, context);
|
|
471
|
+
if (result && result.success) {
|
|
472
|
+
let content = result.content || '';
|
|
473
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
474
|
+
content = content.slice(0, MAX_CONTENT_LENGTH) + '\n\n[... Contenido truncado por límite de tamaño ...]';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
478
|
+
return {
|
|
479
|
+
success: true,
|
|
480
|
+
url: url,
|
|
481
|
+
status: result.status || 200,
|
|
482
|
+
content: content,
|
|
483
|
+
byteSize: result.byteSize || content.length,
|
|
484
|
+
isPdf: result.isPdf || isPdfUrl,
|
|
485
|
+
elapsedMs: parseFloat((endTime - startTime).toFixed(2)),
|
|
486
|
+
strategy: result.strategy || strategy.name
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
lastError = err;
|
|
491
|
+
console.warn(`Estrategia ${strategy.name} falló para ${url}:`, err.message);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
url: url,
|
|
499
|
+
content: '',
|
|
500
|
+
elapsedMs: parseFloat((endTime - startTime).toFixed(2)),
|
|
501
|
+
error: `No se pudo acceder a la página web o descargar el documento (${lastError ? lastError.message : 'Error de conexión o bloqueo de red'}).`
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const pipeline = new PageFetcherPipeline();
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Consulta una página web o descarga un documento PDF y devuelve el contenido legible.
|
|
510
|
+
*/
|
|
511
|
+
async function fetchPage(rawUrl, options = {}) {
|
|
512
|
+
return pipeline.fetch(rawUrl, options);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Definiciones estándar de herramientas (Tool/Function Calling) para OpenAI y LLMs compatibles.
|
|
517
|
+
*/
|
|
518
|
+
const WEB_TOOL_DEFINITION = {
|
|
519
|
+
type: 'function',
|
|
520
|
+
function: {
|
|
521
|
+
name: 'fetch_web_page',
|
|
522
|
+
description: 'Descarga y lee el texto y contenido de una página web o artículo a partir de su URL (ej: "https://es.wikipedia.org/wiki/Sol" o "https://nodejs.org/en/about").',
|
|
523
|
+
parameters: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
properties: {
|
|
526
|
+
url: {
|
|
527
|
+
type: 'string',
|
|
528
|
+
description: 'URL de la página web a consultar.'
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
required: ['url']
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const PDF_TOOL_DEFINITION = {
|
|
537
|
+
type: 'function',
|
|
538
|
+
function: {
|
|
539
|
+
name: 'download_pdf',
|
|
540
|
+
description: 'Descarga un archivo o documento PDF desde una URL web y extrae todo su texto legible para analizarlo e integrarlo en el contexto de la conversación (ej: "https://arxiv.org/pdf/2310.06825.pdf" o "https://ejemplo.com/informe.pdf").',
|
|
541
|
+
parameters: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {
|
|
544
|
+
url: {
|
|
545
|
+
type: 'string',
|
|
546
|
+
description: 'URL directa del documento PDF a descargar y extraer.'
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
required: ['url']
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
fetchPage,
|
|
556
|
+
downloadPdf: fetchPage,
|
|
557
|
+
validateUrlForFetch,
|
|
558
|
+
extractReadableTextFromHtml,
|
|
559
|
+
BaseFetchStrategy,
|
|
560
|
+
ReaderGatewayStrategy,
|
|
561
|
+
LocalPdfStrategy,
|
|
562
|
+
DirectFetchStrategy,
|
|
563
|
+
CorsProxyStrategy,
|
|
564
|
+
PageFetcherPipeline,
|
|
565
|
+
pipeline,
|
|
566
|
+
WEB_TOOL_DEFINITION,
|
|
567
|
+
PDF_TOOL_DEFINITION
|
|
568
|
+
};
|
|
569
|
+
});
|