/** * El nodo RSS como AI toolkit: leer feeds, abrir un artículo, encontrar los * feeds de una web y saber cuáles lee el nodo — como herramientas que un AI * Node, un servidor MCP o un agente de voz pueden llamar. * * ## Por qué se rehízo (2026-09-27) * * Ariel: «su AI Toolkit está muy mal hecho, hay que volver a hacerlo bien». * Lo que había era el nodo entero expuesto como UNA herramienta: * * - al activarlo, la página PISABA el feed guardado con `{{payload.feedUrl}}` * (así estaba el único nodo en toolkit de producción); * - su único parámetro era esa plantilla, con la plantilla por descripción, y * con un feed fijo la herramienta no tenía ninguno; * - el modelo no podía pedir cuántas entradas, ni cuáles, ni desde cuándo; * - el dedup entre corridas seguía encendido, así que la SEGUNDA llamada del * modelo devolvía cero entradas aunque el feed estuviera lleno; * - volvía cada entrada con su contenido tres veces (HTML, texto y resumen), * cincuenta por defecto, sin tope de caracteres. * * ## Sus decisiones * * - Los feeds del NODO —uno o varios, en los ajustes del toolkit; nace con el * de su single operation— y el dueño lo puede abrir a cualquier feed * (`toolkitScope: 'any'`), como el repo de GitHub. * - El dedup NUNCA en modo toolkit: cada llamada ve el feed tal cual está, y * la memoria del nodo ni se lee ni se toca. Para «lo nuevo», `since`. * - `read_article` abre cualquier página pública: el fetch ya se niega a las * IPs privadas (`fetchSeguro`), y un agregador enlaza a cualquier dominio. * - Todas las opciones de single operation son parámetros, y en modo * toolkit las decide el LLM, no el nodo. Ariel, el mismo día, viendo las * pestañas de filtros en toolkit: «¿por qué no dejamos que el LLM se * encargue de todo eso? … el punto del AI Toolkit es que el cliente no * tenga que pensar mucho: que le diga al LLM lo que quiere y el LLM * resuelva». Así que los filtros guardados en single operation NO se * aplican en toolkit, y los topes son fijos (abajo), no los del nodo. * * Ninguna nace bloqueada: las cuatro sólo leen. */ import type { CampoDeToolkit } from './argumentos-de-toolkit.js'; export type RssToolkitOperation = 'getFeedItems' | 'readArticle' | 'discoverFeeds' | 'listFeeds'; export interface RssToolkitParameter { name: string; type: 'string' | 'number' | 'boolean'; description: string; required: boolean; } export interface RssToolkitSpec { operation: RssToolkitOperation; label: string; toolName: string; description: string; parameters: RssToolkitParameter[]; destructive?: boolean; /** Nace bloqueada en el nodo; el dueño la desbloquea. Ver `toolkits.ts`. */ bloqueadaPorDefecto?: boolean; } /** Los parámetros que el nodo puede traer resueltos: con un solo feed, el * modelo omite `feed_url` y se lee ése. */ export declare const RSS_TOOLKIT_DEFAULTABLE: readonly ["feed_url"]; /** Cuántas entradas vuelven si el modelo no dice otra cosa. Cada una entra * en su contexto. */ export declare const RSS_TOOLKIT_ENTRADAS_POR_DEFECTO = 20; /** Lo más que puede pedir el modelo en una llamada. Fijo: en toolkit el tope * no lo pone el nodo (2026-09-27). */ export declare const RSS_TOOLKIT_ENTRADAS_MAX = 50; /** Cuánto se espera al feed, como mucho, y por defecto. */ export declare const RSS_TOOLKIT_ESPERA_MAX_MS = 30000; /** Con `full_articles`, cuántas entradas se abren como mucho: cada una es una * descarga y un análisis de 1 a 3 segundos. */ export declare const RSS_TOOLKIT_ARTICULOS_COMPLETOS = 5; /** Cuántos feeds puede tener el nodo en sus ajustes de toolkit. */ export declare const RSS_TOOLKIT_MAX_FEEDS = 20; /** Hasta dónde llega el modelo. `node` (el de fábrica): los feeds del nodo. * `any`: cualquier feed público. */ export declare const RSS_TOOLKIT_ALCANCES: readonly ["node", "any"]; export type RssToolkitAlcance = (typeof RSS_TOOLKIT_ALCANCES)[number]; /** Los formatos que entiende el lector (los mismos que la entidad). XML va * por un solo camino —`rss`, `atom` y `rdf` son lo mismo para él—, pero los * cinco se aceptan porque son los que ya pueden estar guardados. */ export declare const RSS_TOOLKIT_FORMATOS: readonly ["auto", "rss", "atom", "rdf", "json"]; export declare const RSS_TOOLKIT_SPECS: RssToolkitSpec[]; export declare const RSS_TOOLKIT_SPECS_BY_TOOL_NAME: Readonly>; export declare const RSS_TOOLKIT_CAMPOS: Readonly>>>; /** Las opciones de single operation del nodo (con punto, las de `filters`). * El test de paridad exige que cada una sea un parámetro o esté abajo. */ export declare const RSS_OPCIONES_DE_SINGLE_OPERATION: readonly ["feedUrl", "format", "maxItems", "timeoutMs", "filters.keywords", "filters.excludeKeywords", "filters.regex", "filters.authorAllowlist", "filters.minPubDate", "enrichContent", "dedupAcrossRuns"]; /** Las que NO son parámetro, cada una con su porqué. */ export declare const RSS_OPCIONES_FUERA_DEL_TOOLKIT: Readonly>; /** ¿Es una URL http(s) que se pueda pedir? */ export declare function esUrlDeFeed(url: unknown): url is string; /** * Una URL de feed en la forma con la que se comparan dos: sin esquema (http y * https son el mismo feed), sin `www.`, sin fragmento ni barra final, con el * host en minúsculas. La ruta y la query se quedan como están: distinguen * feeds de verdad (`/feed?cat=5`). `''` si no es una URL. */ export declare function feedNormalizado(url: string): string; /** * Los feeds del nodo: los de sus ajustes de toolkit (`toolkitFeeds`) o, si no * tiene, el de su single operation cuando es fijo. Sin repetidos, en su orden. */ export declare function feedsDelNodo(nodo: Readonly> | null | undefined): string[]; /** ¿Está este feed entre los del nodo? */ export declare function feedPermitido(url: string, feeds: readonly string[]): boolean; /** * 🔒 [2026-10-16, mejora #158] Una URL como se le enseña al MODELO: esquema, * host y ruta, y nada más. * * Un feed privado lleva su llave en la query (`?token=…`, `?key=…`) o en * `usuario:contraseña@`, y lo que lee el modelo sale al proveedor, se guarda * en `tools[]` y está al alcance de un texto que le meta instrucciones (un * artículo del propio feed, por ejemplo) y le pida mandarlo por otra * herramienta. Con la ruta el modelo sabe de qué feed se habla; la llave no * le hace falta: el servidor usa la URL guardada entera * (`feedDelNodoPorLoQuePide`). Lo que no es una URL http(s) se queda igual. * * 🔒 [mejora #192] Y en la RUTA, cada trozo con pinta de llave sale como * `***` (con su extensión): hay feeds privados que la llevan ahí * (`/feeds//rss.xml`, `/private/.rss`). Ver * `esTrozoConPintaDeLlave`. */ export declare function urlParaElModelo(valor: string): string; /** * 🔒 [2026-10-16, mejora #193] Los trozos PRIVADOS de una URL: el valor de cada * parámetro de la query, el usuario, la contraseña y cada trozo de la ruta * con pinta de llave. Tal cual vienen en la URL y descodificados, del más * largo al más corto. * * Para quitarlos de un texto que va al modelo (el error de quien leyó el * feed) aunque la URL venga escrita de otra forma (`new URL(…).href`, otra * codificación) o venga sólo la llave. Sólo los de 6 caracteres o más: uno * más corto (`format=json`, `page=2`) no es una llave y cambiarlo * estropearía el texto. */ export declare function trozosPrivadosDeLaUrl(valor: string): string[]; /** * 🔒 [mejora #158] El feed del nodo que pide el modelo, con la URL GUARDADA. * * El modelo sólo ve la recortada (`urlParaElModelo`), así que puede devolver * ésa. Se cambia por la guardada entera cuando: * - casa con un feed del nodo tal cual (`feedPermitido`): la guardada, que * puede llevar `usuario:contraseña@`, que la comparación no mira; * - o no trae query ni credenciales y casa por host y ruta con UN solo feed * del nodo. * Con DOS o más que casen así, `ambiguo`: el error no puede enseñar ninguna * entera. Si no casa con ninguno, lo pedido tal cual (y quien llama decide si * el nodo lo deja leer). */ export declare function feedDelNodoPorLoQuePide(pedido: string, feeds: readonly string[]): { url: string; } | { ambiguo: true; };