@panal/sdk 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/idiomas.d.ts +69 -0
- package/dist/idiomas.js +88 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/niveles.d.ts +102 -0
- package/dist/niveles.js +210 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +35 -1
- package/package.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — la ficha de un agente en el idioma de quien la lee.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* EL PROBLEMA
|
|
6
|
+
*
|
|
7
|
+
* El marketplace habla diez idiomas. Los AGENTES no: su nombre, su descripción
|
|
8
|
+
* y sus niveles son texto libre que escribió una persona en el suyo, y ese
|
|
9
|
+
* texto sale igual en las diez versiones del escaparate. Hoy en mainnet hay
|
|
10
|
+
* fichas que enseñan las dos cosas a la vez —la descripción en inglés y los
|
|
11
|
+
* niveles en español, en la misma tarjeta—, porque una la escribió el registro
|
|
12
|
+
* y los otros el código del bot.
|
|
13
|
+
*
|
|
14
|
+
* QUIÉN TRADUCE
|
|
15
|
+
*
|
|
16
|
+
* El propio agente. No hay un traductor de Panal en medio, por dos razones que
|
|
17
|
+
* no son de gusto:
|
|
18
|
+
*
|
|
19
|
+
* - Un servicio central que traduzca todas las fichas es un servidor que hay
|
|
20
|
+
* que pagar, mantener y del que pasa a depender el escaparate. Panal no
|
|
21
|
+
* tiene ninguno: el catálogo se puede reconstruir leyendo la cadena.
|
|
22
|
+
* - El agente YA es un modelo. Traducir cuatro frases suyas es la llamada más
|
|
23
|
+
* barata que va a hacer en su vida, y la paga quien se beneficia.
|
|
24
|
+
*
|
|
25
|
+
* Se pide con `?lang=`, y lo que vuelve es la MISMA ficha con los campos de
|
|
26
|
+
* texto traducidos. Así ningún lector cambia: `leerNiveles`, `leerX402` y todo
|
|
27
|
+
* lo demás siguen leyendo los mismos campos.
|
|
28
|
+
*
|
|
29
|
+
* LO QUE NO SE TRADUCE
|
|
30
|
+
*
|
|
31
|
+
* El nombre del agente. «LexPanal» no significa nada en francés y traducirlo
|
|
32
|
+
* sería inventarle otro nombre a alguien, además de romper toda referencia
|
|
33
|
+
* escrita a él. Se traduce lo que es una frase: la descripción y el nombre y la
|
|
34
|
+
* descripción de cada nivel, que sí describen —«Un archivo», «El repositorio»—
|
|
35
|
+
* y que hoy son lo único que un francés no puede leer.
|
|
36
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Los idiomas del marketplace, en códigos ISO 639-1.
|
|
40
|
+
*
|
|
41
|
+
* Es la MISMA lista que `src/i18n/locales` de la web. Si allí entra uno nuevo,
|
|
42
|
+
* entra aquí, o los agentes no sabrán que se lo pueden pedir.
|
|
43
|
+
*/
|
|
44
|
+
export declare const IDIOMAS: readonly ["ar", "bn", "en", "es", "fr", "hi", "pt", "ru", "ur", "zh"];
|
|
45
|
+
export type Idioma = (typeof IDIOMAS)[number];
|
|
46
|
+
/**
|
|
47
|
+
* `'fr-CA'` → `'fr'`, `'klingon'` → `null`.
|
|
48
|
+
*
|
|
49
|
+
* El navegador dice `es-419` y `zh-Hans`, no `es` y `zh`. Quedarse con la
|
|
50
|
+
* primera parte es lo que hace que un mexicano y un argentino vean lo mismo en
|
|
51
|
+
* vez de caer los dos al inglés.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizarIdioma(v: unknown): Idioma | null;
|
|
54
|
+
/**
|
|
55
|
+
* La URL de la ficha de un agente en un idioma.
|
|
56
|
+
*
|
|
57
|
+
* Sin idioma reconocible se pide la ficha de siempre, SIN parámetro: un agente
|
|
58
|
+
* viejo que no sepa de esto contesta igual, y uno nuevo no gasta una traducción
|
|
59
|
+
* en un código que no existe.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fichaEnIdioma(botUrl: string, idioma: unknown): string;
|
|
62
|
+
/**
|
|
63
|
+
* Cómo se llama cada idioma EN ese idioma.
|
|
64
|
+
*
|
|
65
|
+
* Para pedirle una traducción a un modelo, que entiende mucho mejor «français»
|
|
66
|
+
* que «fr». Y de paso es la lista que enseñaría un selector, si algún día hace
|
|
67
|
+
* falta uno aquí.
|
|
68
|
+
*/
|
|
69
|
+
export declare const NOMBRE_IDIOMA: Record<Idioma, string>;
|
package/dist/idiomas.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — la ficha de un agente en el idioma de quien la lee.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* EL PROBLEMA
|
|
6
|
+
*
|
|
7
|
+
* El marketplace habla diez idiomas. Los AGENTES no: su nombre, su descripción
|
|
8
|
+
* y sus niveles son texto libre que escribió una persona en el suyo, y ese
|
|
9
|
+
* texto sale igual en las diez versiones del escaparate. Hoy en mainnet hay
|
|
10
|
+
* fichas que enseñan las dos cosas a la vez —la descripción en inglés y los
|
|
11
|
+
* niveles en español, en la misma tarjeta—, porque una la escribió el registro
|
|
12
|
+
* y los otros el código del bot.
|
|
13
|
+
*
|
|
14
|
+
* QUIÉN TRADUCE
|
|
15
|
+
*
|
|
16
|
+
* El propio agente. No hay un traductor de Panal en medio, por dos razones que
|
|
17
|
+
* no son de gusto:
|
|
18
|
+
*
|
|
19
|
+
* - Un servicio central que traduzca todas las fichas es un servidor que hay
|
|
20
|
+
* que pagar, mantener y del que pasa a depender el escaparate. Panal no
|
|
21
|
+
* tiene ninguno: el catálogo se puede reconstruir leyendo la cadena.
|
|
22
|
+
* - El agente YA es un modelo. Traducir cuatro frases suyas es la llamada más
|
|
23
|
+
* barata que va a hacer en su vida, y la paga quien se beneficia.
|
|
24
|
+
*
|
|
25
|
+
* Se pide con `?lang=`, y lo que vuelve es la MISMA ficha con los campos de
|
|
26
|
+
* texto traducidos. Así ningún lector cambia: `leerNiveles`, `leerX402` y todo
|
|
27
|
+
* lo demás siguen leyendo los mismos campos.
|
|
28
|
+
*
|
|
29
|
+
* LO QUE NO SE TRADUCE
|
|
30
|
+
*
|
|
31
|
+
* El nombre del agente. «LexPanal» no significa nada en francés y traducirlo
|
|
32
|
+
* sería inventarle otro nombre a alguien, además de romper toda referencia
|
|
33
|
+
* escrita a él. Se traduce lo que es una frase: la descripción y el nombre y la
|
|
34
|
+
* descripción de cada nivel, que sí describen —«Un archivo», «El repositorio»—
|
|
35
|
+
* y que hoy son lo único que un francés no puede leer.
|
|
36
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Los idiomas del marketplace, en códigos ISO 639-1.
|
|
40
|
+
*
|
|
41
|
+
* Es la MISMA lista que `src/i18n/locales` de la web. Si allí entra uno nuevo,
|
|
42
|
+
* entra aquí, o los agentes no sabrán que se lo pueden pedir.
|
|
43
|
+
*/
|
|
44
|
+
export const IDIOMAS = ['ar', 'bn', 'en', 'es', 'fr', 'hi', 'pt', 'ru', 'ur', 'zh'];
|
|
45
|
+
/**
|
|
46
|
+
* `'fr-CA'` → `'fr'`, `'klingon'` → `null`.
|
|
47
|
+
*
|
|
48
|
+
* El navegador dice `es-419` y `zh-Hans`, no `es` y `zh`. Quedarse con la
|
|
49
|
+
* primera parte es lo que hace que un mexicano y un argentino vean lo mismo en
|
|
50
|
+
* vez de caer los dos al inglés.
|
|
51
|
+
*/
|
|
52
|
+
export function normalizarIdioma(v) {
|
|
53
|
+
if (typeof v !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
const base = v.trim().toLowerCase().split(/[-_]/)[0];
|
|
56
|
+
return IDIOMAS.includes(base ?? '') ? base : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* La URL de la ficha de un agente en un idioma.
|
|
60
|
+
*
|
|
61
|
+
* Sin idioma reconocible se pide la ficha de siempre, SIN parámetro: un agente
|
|
62
|
+
* viejo que no sepa de esto contesta igual, y uno nuevo no gasta una traducción
|
|
63
|
+
* en un código que no existe.
|
|
64
|
+
*/
|
|
65
|
+
export function fichaEnIdioma(botUrl, idioma) {
|
|
66
|
+
const base = `${botUrl.replace(/\/+$/, '')}/agent.json`;
|
|
67
|
+
const lang = normalizarIdioma(idioma);
|
|
68
|
+
return lang ? `${base}?lang=${lang}` : base;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Cómo se llama cada idioma EN ese idioma.
|
|
72
|
+
*
|
|
73
|
+
* Para pedirle una traducción a un modelo, que entiende mucho mejor «français»
|
|
74
|
+
* que «fr». Y de paso es la lista que enseñaría un selector, si algún día hace
|
|
75
|
+
* falta uno aquí.
|
|
76
|
+
*/
|
|
77
|
+
export const NOMBRE_IDIOMA = {
|
|
78
|
+
ar: 'العربية (Arabic)',
|
|
79
|
+
bn: 'বাংলা (Bengali)',
|
|
80
|
+
en: 'English',
|
|
81
|
+
es: 'español (Spanish)',
|
|
82
|
+
fr: 'français (French)',
|
|
83
|
+
hi: 'हिन्दी (Hindi)',
|
|
84
|
+
pt: 'português (Portuguese)',
|
|
85
|
+
ru: 'русский (Russian)',
|
|
86
|
+
ur: 'اردو (Urdu)',
|
|
87
|
+
zh: '中文 (Chinese, simplified)',
|
|
88
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { PanalClient, createPanalClient } from './client.js';
|
|
|
23
23
|
export type { PanalClientOptions, HireParams, HireResult } from './client.js';
|
|
24
24
|
export { monad, monadTestnet, addressesFor, chainFor, MAINNET_ADDRESSES, TESTNET_ADDRESSES, NATIVE_CURRENCY, FEE_BPS, AUTO_RELEASE_SECONDS, DISPUTE_TIMEOUT_SECONDS, } from './chains.js';
|
|
25
25
|
export type { PanalNetwork, PanalAddresses } from './chains.js';
|
|
26
|
-
export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, } from './types.js';
|
|
26
|
+
export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, MAX_LOGO_DATA, isEmbeddedLogo, } from './types.js';
|
|
27
27
|
export type { Agent, AgentLinkKey, AgentLinks, AgentMetadata, Task } from './types.js';
|
|
28
28
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
29
29
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
@@ -40,3 +40,6 @@ export type { CallEnvelope } from './envelope.js';
|
|
|
40
40
|
export type { UrlGuardOptions } from './net.js';
|
|
41
41
|
export { leerDireccion, leerMaxBriefChars, leerNiveles, leerX402, nivelPara } from './agent-card.js';
|
|
42
42
|
export type { AgentCard, FichaGetResult, FichaNivel, FichaPostBrief, FichaX402, Nivel } from './agent-card.js';
|
|
43
|
+
export { componerNivel, esTokenDeNivel, leerNivelDeSegmento, leerNivelesDeMetadata, NIVELES_EDITABLES, precioAWei, weiAPrecio, } from './niveles.js';
|
|
44
|
+
export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
|
|
45
|
+
export type { Idioma } from './idiomas.js';
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export { PanalClient, createPanalClient } from './client.js';
|
|
23
23
|
export { monad, monadTestnet, addressesFor, chainFor, MAINNET_ADDRESSES, TESTNET_ADDRESSES, NATIVE_CURRENCY, FEE_BPS, AUTO_RELEASE_SECONDS, DISPUTE_TIMEOUT_SECONDS, } from './chains.js';
|
|
24
|
-
export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, } from './types.js';
|
|
24
|
+
export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink, parseAgentMetadata, AGENT_LINK_KEYS, MAX_LOGO_DATA, isEmbeddedLogo, } from './types.js';
|
|
25
25
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
26
26
|
// x402: pagar a otro agente por una consulta, sin escrow y sin humano.
|
|
27
27
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
@@ -40,3 +40,9 @@ export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhau
|
|
|
40
40
|
// La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
|
|
41
41
|
// antiguo. En agent-card.ts está por qué llegó a haber dos.
|
|
42
42
|
export { leerDireccion, leerMaxBriefChars, leerNiveles, leerX402, nivelPara } from './agent-card.js';
|
|
43
|
+
// Los niveles escritos en el metadataURI on-chain. Mismo tipo `Nivel` que los
|
|
44
|
+
// de la ficha; en niveles.ts está por qué viven en los dos sitios.
|
|
45
|
+
export { componerNivel, esTokenDeNivel, leerNivelDeSegmento, leerNivelesDeMetadata, NIVELES_EDITABLES, precioAWei, weiAPrecio, } from './niveles.js';
|
|
46
|
+
// La ficha en el idioma de quien la lee. En idiomas.ts está por qué traduce el
|
|
47
|
+
// propio agente y no un servicio de Panal.
|
|
48
|
+
export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — los niveles de un agente dentro del `metadataURI` on-chain.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* POR QUÉ AQUÍ Y NO SOLO EN LA FICHA
|
|
6
|
+
*
|
|
7
|
+
* Los niveles nacieron en la ficha que sirve el bot (`tiers`, ver
|
|
8
|
+
* `agent-card.ts`), y allí siguen: es donde un agente declara los topes de
|
|
9
|
+
* caracteres que su modelo puede masticar. Pero vivir SOLO ahí tiene dos
|
|
10
|
+
* consecuencias que se notan:
|
|
11
|
+
*
|
|
12
|
+
* - Para cambiar un precio hay que tocar el código del agente y reiniciarlo.
|
|
13
|
+
* Quien no programa no puede tocar lo que cobra.
|
|
14
|
+
* - Si el bot está caído, sus niveles no existen. El escaparate enseña un
|
|
15
|
+
* precio suelto de un agente que en realidad vende tres cosas distintas.
|
|
16
|
+
*
|
|
17
|
+
* En el `metadataURI` los escribe el registro, los guarda la cadena y los lee
|
|
18
|
+
* cualquiera sin preguntarle a nadie. El precio —que es lo único que hay que
|
|
19
|
+
* bloquear de verdad— deja de depender de que un servidor conteste.
|
|
20
|
+
*
|
|
21
|
+
* EL FORMATO
|
|
22
|
+
*
|
|
23
|
+
* nivel:<precio>|<nombre>|<descripción>|<brief>|<adjunto>|<adjuntos>
|
|
24
|
+
*
|
|
25
|
+
* Un segmento por nivel, entre los demás de la ficha:
|
|
26
|
+
*
|
|
27
|
+
* Lint · Revisa código · solidity · bot:https://… · nivel:0.03|Un archivo|…
|
|
28
|
+
*
|
|
29
|
+
* El precio va en unidades enteras («0.03»), no en wei. En wei son diecisiete
|
|
30
|
+
* dígitos por nivel y esta cadena se escribe en la cadena de bloques: lo que
|
|
31
|
+
* ocupa se paga. Los tres topes son opcionales y los vacíos del final no se
|
|
32
|
+
* escriben, así que un nivel sin topes son tres campos y punto.
|
|
33
|
+
*
|
|
34
|
+
* POR QUÉ SE VALIDA TAN DURO
|
|
35
|
+
*
|
|
36
|
+
* La regla la fija `marca.ts` y es la misma: UN TOKEN SOLO CUENTA SI SU VALOR
|
|
37
|
+
* VALE. La descripción de un agente es texto libre y alguien va a escribir
|
|
38
|
+
* «nivel: depende del encargo» dentro de ella; si bastara con ver dos puntos,
|
|
39
|
+
* esa frase se convertiría en un nivel fantasma y además desaparecería de la
|
|
40
|
+
* descripción. Exigiendo un número decimal, una barra y un nombre no vacío, la
|
|
41
|
+
* frase se queda donde estaba.
|
|
42
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
43
|
+
*/
|
|
44
|
+
import type { Nivel } from './agent-card.js';
|
|
45
|
+
/**
|
|
46
|
+
* Cuántos ofrece el formulario.
|
|
47
|
+
*
|
|
48
|
+
* Tres es lo que la gente entiende de un vistazo —pequeño, mediano, grande— y
|
|
49
|
+
* lo que ya publican los agentes que los usan. El LECTOR admite hasta ocho
|
|
50
|
+
* porque una ficha la escribe cualquiera y recortar lo que ya está escrito
|
|
51
|
+
* sería tirar un nivel que su dueño creía publicado.
|
|
52
|
+
*/
|
|
53
|
+
export declare const NIVELES_EDITABLES = 3;
|
|
54
|
+
/**
|
|
55
|
+
* `'0.03'` → `30000000000000000n`.
|
|
56
|
+
*
|
|
57
|
+
* A mano y no con `parseUnits` porque este módulo no importa viem: lo lee el
|
|
58
|
+
* bot de cada agente, que depende de viem a propósito solo en su cliente. La
|
|
59
|
+
* cuenta es rellenar de ceros a la derecha, que es exactamente lo que hace
|
|
60
|
+
* `parseUnits` sin coma flotante por medio.
|
|
61
|
+
*/
|
|
62
|
+
export declare function precioAWei(precio: string): bigint | null;
|
|
63
|
+
/**
|
|
64
|
+
* `30000000000000000n` → `'0.03'`.
|
|
65
|
+
*
|
|
66
|
+
* Sin ceros de relleno al final: `'0.030000000000000000'` es el mismo número
|
|
67
|
+
* y ocupa quince caracteres más en la cadena de bloques.
|
|
68
|
+
*/
|
|
69
|
+
export declare function weiAPrecio(wei: bigint): string;
|
|
70
|
+
/**
|
|
71
|
+
* `nivel:0.03|Un archivo|Un fichero suelto` → el nivel, o `null` si no lo es.
|
|
72
|
+
*
|
|
73
|
+
* Devolver `null` es lo normal: por aquí pasan TODOS los segmentos de la
|
|
74
|
+
* ficha, incluida la descripción libre del agente.
|
|
75
|
+
*/
|
|
76
|
+
export declare function leerNivelDeSegmento(segmento: string): Nivel | null;
|
|
77
|
+
/** true si este segmento es un nivel. Para que los lectores de ficha lo aparten. */
|
|
78
|
+
export declare function esTokenDeNivel(segmento: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Los niveles que un agente publica en la cadena, de menor a mayor precio.
|
|
81
|
+
*
|
|
82
|
+
* `[]` significa que no publica ninguno, y eso hay que tratarlo como se
|
|
83
|
+
* trataba antes de que esto existiera: el agente tiene UN precio, el del
|
|
84
|
+
* registro. Nadie debe fabricarle niveles a partir de él.
|
|
85
|
+
*/
|
|
86
|
+
export declare function leerNivelesDeMetadata(metadataURI: string | null | undefined): Nivel[];
|
|
87
|
+
/**
|
|
88
|
+
* El nivel → su segmento, o `null` si no se puede escribir.
|
|
89
|
+
*
|
|
90
|
+
* `null` en vez de arreglarlo por su cuenta: un «·» dentro del nombre partiría
|
|
91
|
+
* la ficha en dos y un «|» correría los campos, así que la salida silenciosa
|
|
92
|
+
* sería un nivel que dice algo distinto de lo que su dueño escribió. Quien
|
|
93
|
+
* llama tiene que enseñar el error, no firmar una ficha que no reconoce.
|
|
94
|
+
*/
|
|
95
|
+
export declare function componerNivel(nivel: {
|
|
96
|
+
name: string;
|
|
97
|
+
description?: string | null;
|
|
98
|
+
precio: string;
|
|
99
|
+
maxBriefChars?: number | null;
|
|
100
|
+
maxAttachChars?: number | null;
|
|
101
|
+
maxAttachCharsTotal?: number | null;
|
|
102
|
+
}): string | null;
|
package/dist/niveles.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — los niveles de un agente dentro del `metadataURI` on-chain.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* POR QUÉ AQUÍ Y NO SOLO EN LA FICHA
|
|
6
|
+
*
|
|
7
|
+
* Los niveles nacieron en la ficha que sirve el bot (`tiers`, ver
|
|
8
|
+
* `agent-card.ts`), y allí siguen: es donde un agente declara los topes de
|
|
9
|
+
* caracteres que su modelo puede masticar. Pero vivir SOLO ahí tiene dos
|
|
10
|
+
* consecuencias que se notan:
|
|
11
|
+
*
|
|
12
|
+
* - Para cambiar un precio hay que tocar el código del agente y reiniciarlo.
|
|
13
|
+
* Quien no programa no puede tocar lo que cobra.
|
|
14
|
+
* - Si el bot está caído, sus niveles no existen. El escaparate enseña un
|
|
15
|
+
* precio suelto de un agente que en realidad vende tres cosas distintas.
|
|
16
|
+
*
|
|
17
|
+
* En el `metadataURI` los escribe el registro, los guarda la cadena y los lee
|
|
18
|
+
* cualquiera sin preguntarle a nadie. El precio —que es lo único que hay que
|
|
19
|
+
* bloquear de verdad— deja de depender de que un servidor conteste.
|
|
20
|
+
*
|
|
21
|
+
* EL FORMATO
|
|
22
|
+
*
|
|
23
|
+
* nivel:<precio>|<nombre>|<descripción>|<brief>|<adjunto>|<adjuntos>
|
|
24
|
+
*
|
|
25
|
+
* Un segmento por nivel, entre los demás de la ficha:
|
|
26
|
+
*
|
|
27
|
+
* Lint · Revisa código · solidity · bot:https://… · nivel:0.03|Un archivo|…
|
|
28
|
+
*
|
|
29
|
+
* El precio va en unidades enteras («0.03»), no en wei. En wei son diecisiete
|
|
30
|
+
* dígitos por nivel y esta cadena se escribe en la cadena de bloques: lo que
|
|
31
|
+
* ocupa se paga. Los tres topes son opcionales y los vacíos del final no se
|
|
32
|
+
* escriben, así que un nivel sin topes son tres campos y punto.
|
|
33
|
+
*
|
|
34
|
+
* POR QUÉ SE VALIDA TAN DURO
|
|
35
|
+
*
|
|
36
|
+
* La regla la fija `marca.ts` y es la misma: UN TOKEN SOLO CUENTA SI SU VALOR
|
|
37
|
+
* VALE. La descripción de un agente es texto libre y alguien va a escribir
|
|
38
|
+
* «nivel: depende del encargo» dentro de ella; si bastara con ver dos puntos,
|
|
39
|
+
* esa frase se convertiría en un nivel fantasma y además desaparecería de la
|
|
40
|
+
* descripción. Exigiendo un número decimal, una barra y un nombre no vacío, la
|
|
41
|
+
* frase se queda donde estaba.
|
|
42
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Decimales de las dos monedas del registro: MON nativo y $PANAL.
|
|
46
|
+
*
|
|
47
|
+
* Está fijo a propósito. `PanalRegistryV2` solo admite esas dos y las dos
|
|
48
|
+
* tienen dieciocho, así que el precio de un nivel se puede escribir en
|
|
49
|
+
* unidades enteras sin arrastrar de qué moneda se trata. El día que el
|
|
50
|
+
* registro admita un token con otros decimales, esto se rompe de la única
|
|
51
|
+
* forma aceptable: aquí, en un sitio, y no repartido por seis lectores.
|
|
52
|
+
*/
|
|
53
|
+
const DECIMALES = 18;
|
|
54
|
+
/** Cuántos niveles se leen de una ficha ajena. El mismo tope que `leerNiveles`. */
|
|
55
|
+
const MAX_NIVELES = 8;
|
|
56
|
+
/**
|
|
57
|
+
* Cuántos ofrece el formulario.
|
|
58
|
+
*
|
|
59
|
+
* Tres es lo que la gente entiende de un vistazo —pequeño, mediano, grande— y
|
|
60
|
+
* lo que ya publican los agentes que los usan. El LECTOR admite hasta ocho
|
|
61
|
+
* porque una ficha la escribe cualquiera y recortar lo que ya está escrito
|
|
62
|
+
* sería tirar un nivel que su dueño creía publicado.
|
|
63
|
+
*/
|
|
64
|
+
export const NIVELES_EDITABLES = 3;
|
|
65
|
+
/** Tope de cada texto. Los mismos que aplica `leerNiveles` a la ficha del bot. */
|
|
66
|
+
const MAX_NOMBRE = 60;
|
|
67
|
+
const MAX_DESCRIPCION = 200;
|
|
68
|
+
/** Un decimal positivo con hasta dieciocho cifras detrás de la coma. */
|
|
69
|
+
const PRECIO = /^\d{1,12}(\.\d{1,18})?$/;
|
|
70
|
+
/**
|
|
71
|
+
* `'0.03'` → `30000000000000000n`.
|
|
72
|
+
*
|
|
73
|
+
* A mano y no con `parseUnits` porque este módulo no importa viem: lo lee el
|
|
74
|
+
* bot de cada agente, que depende de viem a propósito solo en su cliente. La
|
|
75
|
+
* cuenta es rellenar de ceros a la derecha, que es exactamente lo que hace
|
|
76
|
+
* `parseUnits` sin coma flotante por medio.
|
|
77
|
+
*/
|
|
78
|
+
export function precioAWei(precio) {
|
|
79
|
+
const s = precio.trim();
|
|
80
|
+
if (!PRECIO.test(s))
|
|
81
|
+
return null;
|
|
82
|
+
const [entera, decimal = ''] = s.split('.');
|
|
83
|
+
const wei = BigInt(entera + decimal.padEnd(DECIMALES, '0').slice(0, DECIMALES));
|
|
84
|
+
return wei > 0n ? wei : null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `30000000000000000n` → `'0.03'`.
|
|
88
|
+
*
|
|
89
|
+
* Sin ceros de relleno al final: `'0.030000000000000000'` es el mismo número
|
|
90
|
+
* y ocupa quince caracteres más en la cadena de bloques.
|
|
91
|
+
*/
|
|
92
|
+
export function weiAPrecio(wei) {
|
|
93
|
+
const s = wei.toString().padStart(DECIMALES + 1, '0');
|
|
94
|
+
const entera = s.slice(0, -DECIMALES);
|
|
95
|
+
const decimal = s.slice(-DECIMALES).replace(/0+$/, '');
|
|
96
|
+
return decimal ? `${entera}.${decimal}` : entera;
|
|
97
|
+
}
|
|
98
|
+
/** Un entero positivo, o `null`. Misma regla que los topes de la ficha. */
|
|
99
|
+
function tope(v) {
|
|
100
|
+
if (v === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
const s = v.trim();
|
|
103
|
+
if (!/^\d{1,9}$/.test(s))
|
|
104
|
+
return null;
|
|
105
|
+
const n = Number(s);
|
|
106
|
+
return n > 0 ? n : null;
|
|
107
|
+
}
|
|
108
|
+
/** Texto de un desconocido: espacios colapsados y recortado, porque va a un escaparate. */
|
|
109
|
+
function letrero(v, max) {
|
|
110
|
+
if (v === undefined)
|
|
111
|
+
return null;
|
|
112
|
+
const limpio = v.replace(/\s+/g, ' ').trim().slice(0, max);
|
|
113
|
+
return limpio || null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `nivel:0.03|Un archivo|Un fichero suelto` → el nivel, o `null` si no lo es.
|
|
117
|
+
*
|
|
118
|
+
* Devolver `null` es lo normal: por aquí pasan TODOS los segmentos de la
|
|
119
|
+
* ficha, incluida la descripción libre del agente.
|
|
120
|
+
*/
|
|
121
|
+
export function leerNivelDeSegmento(segmento) {
|
|
122
|
+
const i = segmento.indexOf(':');
|
|
123
|
+
if (i <= 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (segmento.slice(0, i).trim().toLowerCase() !== 'nivel')
|
|
126
|
+
return null;
|
|
127
|
+
const campos = segmento.slice(i + 1).split('|');
|
|
128
|
+
// Sin nombre no hay nivel: el cliente elige por el nombre, y un botón vacío
|
|
129
|
+
// con un precio al lado no es una oferta, es un acertijo.
|
|
130
|
+
if (campos.length < 2)
|
|
131
|
+
return null;
|
|
132
|
+
const wei = precioAWei(campos[0] ?? '');
|
|
133
|
+
if (wei === null)
|
|
134
|
+
return null;
|
|
135
|
+
const name = letrero(campos[1], MAX_NOMBRE);
|
|
136
|
+
if (!name)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
description: letrero(campos[2], MAX_DESCRIPCION),
|
|
141
|
+
wei,
|
|
142
|
+
maxBriefChars: tope(campos[3]),
|
|
143
|
+
maxAttachChars: tope(campos[4]),
|
|
144
|
+
maxAttachCharsTotal: tope(campos[5]),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** true si este segmento es un nivel. Para que los lectores de ficha lo aparten. */
|
|
148
|
+
export function esTokenDeNivel(segmento) {
|
|
149
|
+
return leerNivelDeSegmento(segmento) !== null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Los niveles que un agente publica en la cadena, de menor a mayor precio.
|
|
153
|
+
*
|
|
154
|
+
* `[]` significa que no publica ninguno, y eso hay que tratarlo como se
|
|
155
|
+
* trataba antes de que esto existiera: el agente tiene UN precio, el del
|
|
156
|
+
* registro. Nadie debe fabricarle niveles a partir de él.
|
|
157
|
+
*/
|
|
158
|
+
export function leerNivelesDeMetadata(metadataURI) {
|
|
159
|
+
if (!metadataURI)
|
|
160
|
+
return [];
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const segmento of metadataURI.split('·')) {
|
|
163
|
+
if (out.length >= MAX_NIVELES)
|
|
164
|
+
break;
|
|
165
|
+
const nivel = leerNivelDeSegmento(segmento.trim());
|
|
166
|
+
// Un nivel mal escrito se cae de la lista en vez de tumbarla entera: un
|
|
167
|
+
// campo roto no puede dejar sin comprar los niveles buenos de al lado.
|
|
168
|
+
if (nivel)
|
|
169
|
+
out.push(nivel);
|
|
170
|
+
}
|
|
171
|
+
// De menor a mayor, que es como se enseñan y lo que `nivelPara` necesita
|
|
172
|
+
// para quedarse con el último que entra en lo pagado.
|
|
173
|
+
return out.sort((a, b) => (a.wei < b.wei ? -1 : a.wei > b.wei ? 1 : 0));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* El nivel → su segmento, o `null` si no se puede escribir.
|
|
177
|
+
*
|
|
178
|
+
* `null` en vez de arreglarlo por su cuenta: un «·» dentro del nombre partiría
|
|
179
|
+
* la ficha en dos y un «|» correría los campos, así que la salida silenciosa
|
|
180
|
+
* sería un nivel que dice algo distinto de lo que su dueño escribió. Quien
|
|
181
|
+
* llama tiene que enseñar el error, no firmar una ficha que no reconoce.
|
|
182
|
+
*/
|
|
183
|
+
export function componerNivel(nivel) {
|
|
184
|
+
if (precioAWei(nivel.precio) === null)
|
|
185
|
+
return null;
|
|
186
|
+
const name = nivel.name.replace(/\s+/g, ' ').trim();
|
|
187
|
+
const description = (nivel.description ?? '').replace(/\s+/g, ' ').trim();
|
|
188
|
+
if (!name || name.length > MAX_NOMBRE)
|
|
189
|
+
return null;
|
|
190
|
+
if (description.length > MAX_DESCRIPCION)
|
|
191
|
+
return null;
|
|
192
|
+
if (/[·|]/.test(name) || /[·|]/.test(description))
|
|
193
|
+
return null;
|
|
194
|
+
const campos = [
|
|
195
|
+
nivel.precio.trim(),
|
|
196
|
+
name,
|
|
197
|
+
description,
|
|
198
|
+
entero(nivel.maxBriefChars),
|
|
199
|
+
entero(nivel.maxAttachChars),
|
|
200
|
+
entero(nivel.maxAttachCharsTotal),
|
|
201
|
+
];
|
|
202
|
+
// Los vacíos del FINAL se van; los de en medio no pueden irse o el siguiente
|
|
203
|
+
// campo ocuparía el sitio del que falta.
|
|
204
|
+
while (campos.length > 2 && campos[campos.length - 1] === '')
|
|
205
|
+
campos.pop();
|
|
206
|
+
return `nivel:${campos.join('|')}`;
|
|
207
|
+
}
|
|
208
|
+
function entero(v) {
|
|
209
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0 ? String(v) : '';
|
|
210
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -58,6 +58,19 @@ export declare const AGENT_LINK_KEYS: readonly ["logo", "web", "github", "x", "t
|
|
|
58
58
|
export type AgentLinkKey = (typeof AGENT_LINK_KEYS)[number];
|
|
59
59
|
/** Cadena vacía = el agente no lo publicó. */
|
|
60
60
|
export type AgentLinks = Record<AgentLinkKey, string>;
|
|
61
|
+
/**
|
|
62
|
+
* Tope del logo cuando la imagen viaja DENTRO de la ficha, en caracteres.
|
|
63
|
+
*
|
|
64
|
+
* El logo tiene dos formas: una URL https —la imagen vive en el dominio del
|
|
65
|
+
* agente— o la imagen misma en base64. La segunda existe porque la primera
|
|
66
|
+
* pide un sitio donde alojar un archivo, que es justo lo que no tiene quien se
|
|
67
|
+
* registra desde la web. La referencia sigue siendo `src/lib/marca.ts` del
|
|
68
|
+
* marketplace: aquí solo hay que LEER igual, o un logo incrustado saldría
|
|
69
|
+
* recortado a 120 caracteres y con eso no se pinta nada.
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_LOGO_DATA = 5000;
|
|
72
|
+
/** ¿Es una imagen incrustada válida en un token `logo:`? */
|
|
73
|
+
export declare function isEmbeddedLogo(value: string): boolean;
|
|
61
74
|
/**
|
|
62
75
|
* Deja un valor de marca como se guarda, o vacío si no sirve.
|
|
63
76
|
*
|
package/dist/types.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* así que registrar un agente con la forma equivocada era fácil y el error solo
|
|
8
8
|
* se veía al mirar la ficha en el marketplace.
|
|
9
9
|
*/
|
|
10
|
+
import { esTokenDeNivel } from './niveles.js';
|
|
10
11
|
/** Estados de una tarea en el escrow, en el mismo orden que el enum de Solidity. */
|
|
11
12
|
export var TaskStatus;
|
|
12
13
|
(function (TaskStatus) {
|
|
@@ -26,6 +27,29 @@ export const TASK_STATUS_LABEL = {
|
|
|
26
27
|
/** Las claves de marca que entiende el marketplace, en el orden en que se pintan. */
|
|
27
28
|
export const AGENT_LINK_KEYS = ['logo', 'web', 'github', 'x', 'telegram'];
|
|
28
29
|
const NO_LINKS = { logo: '', web: '', github: '', x: '', telegram: '' };
|
|
30
|
+
/**
|
|
31
|
+
* Tope del logo cuando la imagen viaja DENTRO de la ficha, en caracteres.
|
|
32
|
+
*
|
|
33
|
+
* El logo tiene dos formas: una URL https —la imagen vive en el dominio del
|
|
34
|
+
* agente— o la imagen misma en base64. La segunda existe porque la primera
|
|
35
|
+
* pide un sitio donde alojar un archivo, que es justo lo que no tiene quien se
|
|
36
|
+
* registra desde la web. La referencia sigue siendo `src/lib/marca.ts` del
|
|
37
|
+
* marketplace: aquí solo hay que LEER igual, o un logo incrustado saldría
|
|
38
|
+
* recortado a 120 caracteres y con eso no se pinta nada.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_LOGO_DATA = 5000;
|
|
41
|
+
/**
|
|
42
|
+
* `data:image/webp;base64,…`. SVG queda fuera a propósito: es un documento con
|
|
43
|
+
* `<script>` dentro, y esta cadena la pinta cualquiera, no solo un `<img>`.
|
|
44
|
+
*/
|
|
45
|
+
const EMBEDDED_LOGO = /^data:image\/(png|webp|jpeg|gif);base64,([A-Za-z0-9+/]+={0,2})$/;
|
|
46
|
+
/** ¿Es una imagen incrustada válida en un token `logo:`? */
|
|
47
|
+
export function isEmbeddedLogo(value) {
|
|
48
|
+
if (value.length > MAX_LOGO_DATA)
|
|
49
|
+
return false;
|
|
50
|
+
const b64 = EMBEDDED_LOGO.exec(value)?.[2];
|
|
51
|
+
return b64 !== undefined && b64.length >= 64 && b64.length % 4 === 0;
|
|
52
|
+
}
|
|
29
53
|
/**
|
|
30
54
|
* Deja un valor de marca como se guarda, o vacío si no sirve.
|
|
31
55
|
*
|
|
@@ -34,7 +58,12 @@ const NO_LINKS = { logo: '', web: '', github: '', x: '', telegram: '' };
|
|
|
34
58
|
* `src/lib/marca.ts` del marketplace: si cambias una, cambia la otra.
|
|
35
59
|
*/
|
|
36
60
|
export function normalizeAgentLink(key, raw) {
|
|
37
|
-
const
|
|
61
|
+
const whole = raw.trim();
|
|
62
|
+
// Antes de recortar: una imagen incrustada mide miles de caracteres y el
|
|
63
|
+
// recorte la dejaría en un `data:` a medias — ocupa, se guarda y no se ve.
|
|
64
|
+
if (key === 'logo' && whole.startsWith('data:'))
|
|
65
|
+
return isEmbeddedLogo(whole) ? whole : '';
|
|
66
|
+
const value = whole.slice(0, 120);
|
|
38
67
|
if (!value)
|
|
39
68
|
return '';
|
|
40
69
|
// Un espacio o un `·` por dentro invalida en vez de borrarse: borrarlos
|
|
@@ -136,6 +165,11 @@ export function parseAgentMetadata(metadataURI) {
|
|
|
136
165
|
links[link[0]] = link[1];
|
|
137
166
|
continue;
|
|
138
167
|
}
|
|
168
|
+
// Los niveles tampoco son texto de la ficha: sin apartarlos, los tres
|
|
169
|
+
// `nivel:…` de un agente saldrían anunciados como skills suyas. Los lee
|
|
170
|
+
// `leerNivelesDeMetadata`; aquí solo hace falta reconocerlos.
|
|
171
|
+
if (esTokenDeNivel(seg))
|
|
172
|
+
continue;
|
|
139
173
|
rest.push(seg);
|
|
140
174
|
}
|
|
141
175
|
// Los campos que falten quedan vacíos en vez de desplazar a los siguientes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panal/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json",
|
|
47
47
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
-
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts",
|
|
48
|
+
"test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts",
|
|
49
49
|
"test:nombres": "tsx test/nombres.test.ts"
|
|
50
50
|
}
|
|
51
51
|
}
|