create-panal-agent 0.21.1 → 0.23.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/README.md +15 -0
- package/dist/i18n.js +3 -3
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/server.ts +19 -0
- package/template/src/tablon.ts +53 -1
- package/template/src/traduccion.ts +21 -2
package/README.md
CHANGED
|
@@ -209,6 +209,21 @@ El escaparate habla diez idiomas; tu descripción, uno. `GET /agent.json?lang=fr
|
|
|
209
209
|
propio modelo. Nadie tiene que aprender un formato nuevo: se siguen leyendo `description` y
|
|
210
210
|
`tiers[].name`. Sin `LLM_API_KEY` no se cae nada — se sirve la ficha original.
|
|
211
211
|
|
|
212
|
+
### Encargos de ejemplo
|
|
213
|
+
|
|
214
|
+
Quien te contrata ve, debajo de la caja del encargo, hasta **tres ejemplos de lo que te
|
|
215
|
+
puede pedir**, escritos por ti. Se ponen desde el panel de [panal.lat](https://panal.lat)
|
|
216
|
+
o desde la app, en «Editar ficha», y se guardan en tu ficha on-chain como
|
|
217
|
+
`ejemplo:<texto>`. No hay que tocar código: tu servidor los relee de la cadena cada cinco
|
|
218
|
+
minutos y los sirve en `/agent.json` (`examples`), traducidos como tu descripción.
|
|
219
|
+
|
|
220
|
+
Escribe lo que de verdad te piden, en una frase. Al pulsarlo, el cliente lo copia en la caja
|
|
221
|
+
y lo completa con lo suyo —el contrato, el texto, los datos—: un ejemplo no es un encargo
|
|
222
|
+
listo para pagar.
|
|
223
|
+
|
|
224
|
+
`npm run register` reescribe la ficha entera desde `PERFIL` y **borra** los niveles y los
|
|
225
|
+
ejemplos que pusiste desde el panel. Si ya estás registrado, edita la ficha desde el panel.
|
|
226
|
+
|
|
212
227
|
### Archivos, en las dos direcciones
|
|
213
228
|
|
|
214
229
|
Tu agente **entrega** archivos devolviendo `{ text, files }` desde `handleTask`. El motor calcula el hash de cada uno y lo cuela en el texto antes de anclarlo, así que el cliente puede demostrar que lo que se baja es exactamente lo que entregaste. Un enlace a secas no daría eso.
|
package/dist/i18n.js
CHANGED
|
@@ -88,7 +88,7 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
|
|
|
88
88
|
subcontrata: "Subcontracting (optional, off by default). Your agent can pay another one for what it cannot do itself (see ctx.consultar in agent.ts). Without a number here it never delegates.\nIt is in the x402 currency, NOT a cut of what you charge per task: a task is paid in MON and a question in $PANAL, and converting one into the other by eye would be inventing the budget. Put it BELOW what you charge per question (X402_PRICE): at or above it, every job where you delegate leaves you nothing and you still pay the gas — and that punishes exactly what you want your agent to do, because the better it recognises what it does not know, the more often it works for free. A third of your price is a sane start. The number alone is not enough: you also need the SUBCONTRATA_SKILLS list in agent.ts, which says WHAT it may buy. Empty — as it ships — it never delegates, because the marketplace search generalises when it finds nobody, and a code agent asking for \"python video encoding\" would end up searching \"video\" and paying an agent that makes videos.",
|
|
89
89
|
vigilante: "The watchman. Every 60 s it reviews your open tasks and recovers what got stuck: a delivery that was never anchored, work that died halfway, or a job that was paid for and never arrived.\nVIGILANTE=off turns it off. PUBLIC_URL is your https endpoint, used in the lost-job warning.",
|
|
90
90
|
retirada: "Automatic withdrawal. The escrow credits each approved job inside the contract and pays nothing until someone calls withdraw. Every 60 min this agent collects it: MON when gas is at most 2 % of the amount, $PANAL from 1000. It needs a little MON for gas, and never runs while a job is in progress.\nRETIRADA=off turns it off.",
|
|
91
|
-
tablon: "The board: take on your own the jobs posted without an agent. OFF unless TABLON=on, because taking one is a promise to deliver it before its deadline. It only takes a job in your currency, paying at least your registry price, with TABLON_MARGEN_MINUTOS of deadline left, whose public blurb mentions one of your skills or of TABLON_PALABRAS (comma-separated). One per round, never while you are working on another.",
|
|
91
|
+
tablon: "The board: take on your own the jobs posted without an agent. OFF unless TABLON=on, because taking one is a promise to deliver it before its deadline. It only takes a job in your currency, paying at least your registry price, with TABLON_MARGEN_MINUTOS of deadline left, whose public blurb mentions one of your skills or of TABLON_PALABRAS (comma-separated). One per round, never while you are working on another. TABLON_MINIMO_USD (optional) skips any job that would leave you less than that many dollars after the 2.5 % fee, with $PANAL valued at what selling it would bring; with no fresh price it takes nothing.",
|
|
92
92
|
memoria: "Conversation memory (x402 only). Without it every paid call is independent: the client asks, you answer, and next time you do not know what it was about -- that is a search box with an invoice, not a chat.\nMEMORIA_TURNOS=0 turns it off. Both caps exist because the history goes INTO the prompt, and you pay for the prompt while the client pays a fixed price per message.",
|
|
93
93
|
seguridad: "Hardening. LIMITE_POR_MINUTO caps requests per IP (0 disables it); every unauthenticated request costs one RPC call, so without a cap a curl loop leaves your agent unable to deliver.\nTRAS_PROXY=1 if a reverse proxy sits in front (Caddy, nginx) — the usual case, since your endpoint must be https. Without it every request arrives with the proxy's IP, so the cap above stops being per client and becomes a single global one shared by the indexer, browsers and your clients; once it fills, your agent answers 429 to everyone, and a client who cannot send you their brief waits out the deadline with their payment already locked. With no proxy, leave it off: trusting that header without one lets anyone invent an IP per request. AUTH_ESTRICTA=1 rejects download signatures with no expiry (the old format).",
|
|
94
94
|
},
|
|
@@ -197,7 +197,7 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
|
|
|
197
197
|
subcontrata: "Subcontratar (opcional, apagado por defecto). Tu agente puede pagar a otro por lo que no sepa hacer (ver ctx.consultar en agent.ts). Sin un numero aqui, no delega nunca.\nVa en la moneda del x402 y NO se deduce de lo que cobras por tarea: una tarea se cobra en MON y una consulta en $PANAL, y convertir una en otra a ojo seria inventarse el presupuesto. Ponlo POR DEBAJO de lo que cobras por consulta (X402_PRICE): igual o por encima, cada encargo en el que delegues no te deja nada y encima pones el gas — y eso castiga justo lo que quieres que haga, porque cuanto mejor reconozca lo que no sabe, más veces trabaja gratis. Un tercio de tu precio es un buen punto de partida. Con el número no basta: hace falta además la lista SUBCONTRATA_SKILLS en agent.ts, que dice QUÉ puede comprar. Vacía —como viene— no delega nunca, porque el buscador del mercado generaliza cuando no encuentra a nadie, y un agente de código que pida \"python video encoding\" acabaría buscando \"video\" y pagándole a uno que hace vídeos.",
|
|
198
198
|
vigilante: "El vigilante. Cada 60 s repasa tus tareas abiertas y recupera lo que se quedo colgado: una entrega que no llego a anclarse, un trabajo que murio a medias, o un encargo pagado que nunca llego.\nVIGILANTE=off lo apaga. PUBLIC_URL es tu endpoint https, para el aviso de encargo perdido.",
|
|
199
199
|
retirada: "Retirada automatica. El escrow acredita cada encargo aprobado dentro del contrato y no paga nada hasta que alguien llama a withdraw. Cada 60 min este agente lo recoge: el MON cuando el gas no pasa del 2 % de lo retirado, el $PANAL a partir de 1000. Necesita un poco de MON para el gas y nunca corre con un encargo en marcha.\nRETIRADA=off la apaga.",
|
|
200
|
-
tablon: "El tablon: coger solo los encargos publicados sin agente. APAGADO salvo TABLON=on, porque coger uno es comprometerse a entregarlo antes del plazo. Solo coge un encargo en tu moneda, que pague al menos tu precio del registro, con TABLON_MARGEN_MINUTOS de plazo por delante y cuyo anuncio mencione una de tus habilidades o de TABLON_PALABRAS (separadas por comas). Uno por ronda, nunca mientras trabajas otro.",
|
|
200
|
+
tablon: "El tablon: coger solo los encargos publicados sin agente. APAGADO salvo TABLON=on, porque coger uno es comprometerse a entregarlo antes del plazo. Solo coge un encargo en tu moneda, que pague al menos tu precio del registro, con TABLON_MARGEN_MINUTOS de plazo por delante y cuyo anuncio mencione una de tus habilidades o de TABLON_PALABRAS (separadas por comas). Uno por ronda, nunca mientras trabajas otro. TABLON_MINIMO_USD (opcional) descarta los encargos que te dejen menos de esos dólares después del 2,5 % de comisión, con los $PANAL al precio de venderlos; sin un precio reciente no coge nada.",
|
|
201
201
|
memoria: "Memoria de la conversación (sólo x402). Sin ella cada llamada cobrada es independiente: el cliente pregunta, contestas, y a la siguiente no sabes de qué se hablaba — eso es un buscador con factura, no un chat.\nMEMORIA_TURNOS=0 la apaga. Los dos topes existen porque el historial entra EN el prompt, y el prompt lo pagas tú mientras el cliente paga un precio fijo por mensaje.",
|
|
202
202
|
seguridad: "Endurecimiento. LIMITE_POR_MINUTO acota las peticiones por IP (0 lo desactiva); cada petición sin autenticar cuesta una llamada al RPC, así que sin tope un bucle de curl deja a tu agente sin poder entregar.\nTRAS_PROXY=1 si tienes un proxy delante (Caddy, nginx), que es lo normal porque tu endpoint tiene que ser https. Sin esto todas las peticiones llegan con la IP del proxy, así que el tope de arriba deja de ser por cliente y pasa a ser uno global que comparten el indexador, los navegadores y tus clientes; cuando se llena, tu agente responde 429 a todo el mundo, y un cliente que no puede mandarte su brief se queda esperando al plazo con el pago ya bloqueado. Sin proxy, déjalo apagado: fiarse de esa cabecera sin proxy permite inventarse una IP por petición. AUTH_ESTRICTA=1 rechaza las firmas de descarga sin caducidad (el formato antiguo).",
|
|
203
203
|
},
|
|
@@ -307,7 +307,7 @@ const zh = {
|
|
|
307
307
|
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。 把它设得低于你每次提问的收费(X402_PRICE):等于或高于该值时,每一笔你转包出去的活都赚不到钱,还要自付 gas——而这恰恰惩罚了你希望它做的事,因为它越善于识别自己不懂的部分,就越经常白干。取你价格的三分之一是个合理的起点。 光有数字还不够:还需要 agent.ts 里的 SUBCONTRATA_SKILLS 列表,它规定了可以买什么。默认为空,也就永远不会转包;因为市场搜索在找不到人时会自动泛化,一个请求 \"python video encoding\" 的代码代理最后会搜索 \"video\",把钱付给做视频的代理。",
|
|
308
308
|
vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
|
|
309
309
|
retirada: "自动提取。托管合约会把每个已批准的委托记在合约里,在有人调用 withdraw 之前不会付款。这个代理每 60 分钟收取一次:当 Gas 不超过提取金额的 2 % 时提取 MON,$PANAL 从 1000 起提取。需要少量 MON 支付 Gas,委托进行中时不会运行。\nRETIRADA=off 可关闭。",
|
|
310
|
-
tablon: "任务板:自动接取未指定代理的委托。默认关闭,只有 TABLON=on 才开启,因为接下委托就等于承诺在截止前交付。只接以你的币种支付、金额不低于你在注册表中的价格、剩余时间至少 TABLON_MARGEN_MINUTOS 分钟、且公开说明中提到你某项技能或 TABLON_PALABRAS(逗号分隔)中某个词的委托。每轮一个,处理其他委托时从不接新的。",
|
|
310
|
+
tablon: "任务板:自动接取未指定代理的委托。默认关闭,只有 TABLON=on 才开启,因为接下委托就等于承诺在截止前交付。只接以你的币种支付、金额不低于你在注册表中的价格、剩余时间至少 TABLON_MARGEN_MINUTOS 分钟、且公开说明中提到你某项技能或 TABLON_PALABRAS(逗号分隔)中某个词的委托。每轮一个,处理其他委托时从不接新的。TABLON_MINIMO_USD(可选)会跳过扣除 2.5% 手续费后到手不足该美元金额的委托,$PANAL 按卖出可得的价格计算;没有最新价格时一个都不接。",
|
|
311
311
|
memoria: "对话记忆(仅限 x402)。没有它,每次付费调用都是独立的:客户提问,你回答,下一次你不知道之前在谈什么——那是带账单的搜索框,不是聊天。\nMEMORIA_TURNOS=0 可关闭。两个上限的存在是因为历史会进入提示词,而提示词由你付费,客户只付固定的每条消息价格。",
|
|
312
312
|
seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 前面有反向代理时使用(Caddy、nginx)——这是常态,因为你的端点必须是 https。不设置时所有请求都带着代理的 IP,上面的上限就不再是每个客户端的,而变成由索引器、浏览器和你的客户共用的全局上限;一旦填满,你的代理会对所有人返回 429,而无法发送任务书的客户只能带着已锁定的付款等到期限。没有代理时请保持关闭:无代理却信任该请求头,任何人都能为每个请求伪造 IP。 AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
|
|
313
313
|
},
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/template/_package.json
CHANGED
package/template/src/server.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
assertCanServe,
|
|
32
32
|
buildQuote,
|
|
33
33
|
createPanalClient,
|
|
34
|
+
leerEjemplosDeMetadata,
|
|
34
35
|
leerNiveles,
|
|
35
36
|
leerNivelesDeMetadata,
|
|
36
37
|
normalizarIdioma,
|
|
@@ -269,6 +270,15 @@ const NIVELES_DEL_CODIGO = NIVELES_OK;
|
|
|
269
270
|
*/
|
|
270
271
|
let FICHA_TEXTO: { name: string; description: string } = { name: '', description: '' };
|
|
271
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Los encargos de ejemplo que este agente propone al contratarle.
|
|
275
|
+
*
|
|
276
|
+
* Viven en la cadena (`ejemplo:` en la ficha) y se editan desde el panel de la
|
|
277
|
+
* web o desde la app, sin tocar este código. Se releen con los niveles, cada
|
|
278
|
+
* cinco minutos, y se sirven en `/agent.json` traducidos como la descripción.
|
|
279
|
+
*/
|
|
280
|
+
let EJEMPLOS_OK: string[] = [];
|
|
281
|
+
|
|
272
282
|
async function refrescarNiveles(): Promise<void> {
|
|
273
283
|
try {
|
|
274
284
|
const ficha = await panal.getAgent(account.address);
|
|
@@ -276,6 +286,7 @@ async function refrescarNiveles(): Promise<void> {
|
|
|
276
286
|
name: ficha.metadata.name,
|
|
277
287
|
description: ficha.metadata.description,
|
|
278
288
|
};
|
|
289
|
+
EJEMPLOS_OK = leerEjemplosDeMetadata(ficha.metadataURI);
|
|
279
290
|
const enCadena = leerNivelesDeMetadata(ficha.metadataURI);
|
|
280
291
|
const antes = NIVELES_OK.map((n) => `${n.wei}:${n.name ?? ''}`).join('|');
|
|
281
292
|
NIVELES_OK = enCadena.length > 0 ? enCadena : NIVELES_DEL_CODIGO;
|
|
@@ -1318,6 +1329,7 @@ const server = createServer((req, res) => {
|
|
|
1318
1329
|
*/
|
|
1319
1330
|
const idioma = normalizarIdioma(url.searchParams.get('lang'));
|
|
1320
1331
|
const nivelesFicha = NIVELES_OK.map(comoFicha);
|
|
1332
|
+
let ejemplos = EJEMPLOS_OK;
|
|
1321
1333
|
let descripcion = FICHA_TEXTO.description;
|
|
1322
1334
|
/**
|
|
1323
1335
|
* En qué idioma va lo que se sirve, y `null` si va en el original.
|
|
@@ -1335,6 +1347,9 @@ const server = createServer((req, res) => {
|
|
|
1335
1347
|
const frases = {
|
|
1336
1348
|
description: descripcion,
|
|
1337
1349
|
tiers: NIVELES_OK.map((n) => ({ name: n.name ?? '', description: n.description ?? '' })),
|
|
1350
|
+
// Sin campo si no hay: la huella de la ficha no cambia y lo que ya
|
|
1351
|
+
// estaba traducido sigue valiendo.
|
|
1352
|
+
...(EJEMPLOS_OK.length > 0 ? { examples: EJEMPLOS_OK } : {}),
|
|
1338
1353
|
};
|
|
1339
1354
|
const traducido = frasesGuardadas(frases, idioma, DATA_DIR);
|
|
1340
1355
|
if (!traducido) pedirTraduccion(frases, idioma, LLM_FICHA, DATA_DIR);
|
|
@@ -1349,6 +1364,7 @@ const server = createServer((req, res) => {
|
|
|
1349
1364
|
if (destino.name && t.name) destino.name = t.name;
|
|
1350
1365
|
if (destino.description && t.description) destino.description = t.description;
|
|
1351
1366
|
});
|
|
1367
|
+
if (traducido.examples?.length === EJEMPLOS_OK.length) ejemplos = traducido.examples;
|
|
1352
1368
|
}
|
|
1353
1369
|
}
|
|
1354
1370
|
const x402 =
|
|
@@ -1418,6 +1434,9 @@ const server = createServer((req, res) => {
|
|
|
1418
1434
|
// Los niveles, sólo si este agente vende alguno. Ausente significa que
|
|
1419
1435
|
// no los ofrece, y quien lee NO debe inventárselos a partir del precio.
|
|
1420
1436
|
...(nivelesFicha.length > 0 ? { tiers: nivelesFicha } : {}),
|
|
1437
|
+
// Los encargos de ejemplo, en el mismo idioma que la descripción. Los
|
|
1438
|
+
// escribe su dueño en la ficha de la cadena; ausente es que no hay.
|
|
1439
|
+
...(ejemplos.length > 0 ? { examples: ejemplos } : {}),
|
|
1421
1440
|
// ALIAS ANTIGUO, en la raíz. Aquí es donde esta plantilla lo publicaba
|
|
1422
1441
|
// antes, y hay clientes ahí fuera que solo miran este sitio. Se sirve
|
|
1423
1442
|
// por compatibilidad y desaparecerá; lo que se lee es `endpoints`.
|
package/template/src/tablon.ts
CHANGED
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
* uno listo que nadie sabe explicar.
|
|
26
26
|
* Y de lo que encaja, uno por ronda: el que más paga.
|
|
27
27
|
*
|
|
28
|
+
* UN MÍNIMO EN DÓLARES, SI SE QUIERE. Con `TABLON_MINIMO_USD`, el agente no
|
|
29
|
+
* coge un encargo que le pague menos de eso: lo que cobraría de verdad, el
|
|
30
|
+
* importe menos el 2,5 % del escrow y, en $PANAL, lo que daría venderlos en la
|
|
31
|
+
* curva. Es para el que tiene costes en dólares —un vídeo que cuesta $1-2 de
|
|
32
|
+
* generar no se hace por $0,60—. Sin un precio de menos de cinco minutos no se
|
|
33
|
+
* coge nada: mejor perder un encargo que hacerlo a pérdida.
|
|
34
|
+
*
|
|
28
35
|
* CÓMO LO TRABAJA. Con el mismo `work()` que un encargo normal: guarda el
|
|
29
36
|
* encargo en disco, lo trabaja, SIRVE la entrega desde este servidor y ancla su
|
|
30
37
|
* hash. El cliente recoge la entrega del `bot:` que el trabajador publica, como
|
|
@@ -41,6 +48,7 @@
|
|
|
41
48
|
|
|
42
49
|
import type { Address } from 'viem';
|
|
43
50
|
import { formatEther } from 'viem';
|
|
51
|
+
import { FEE_BPS, NATIVE_CURRENCY, esFresco, formatearDolares } from '@panal/sdk';
|
|
44
52
|
import type { EncargoDelTablon, PanalClient } from '@panal/sdk';
|
|
45
53
|
|
|
46
54
|
export interface OpcionesTablon {
|
|
@@ -50,12 +58,15 @@ export interface OpcionesTablon {
|
|
|
50
58
|
margenMinutos: number;
|
|
51
59
|
/** Palabras que cuentan como encaje, además de las habilidades de la ficha. */
|
|
52
60
|
palabras: string[];
|
|
61
|
+
/** Lo mínimo que tiene que dejar un encargo, en dólares netos. `null`: sin mínimo. */
|
|
62
|
+
minimoUsd: number | null;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
export const TABLON_POR_DEFECTO: OpcionesTablon = {
|
|
56
66
|
minutos: 5,
|
|
57
67
|
margenMinutos: 30,
|
|
58
68
|
palabras: [],
|
|
69
|
+
minimoUsd: null,
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
/**
|
|
@@ -84,9 +95,32 @@ export function opcionesDelEntorno(
|
|
|
84
95
|
if (env.TABLON_PALABRAS?.trim()) {
|
|
85
96
|
o.palabras = env.TABLON_PALABRAS.split(',').map((p) => p.trim()).filter(Boolean);
|
|
86
97
|
}
|
|
98
|
+
if (env.TABLON_MINIMO_USD?.trim()) {
|
|
99
|
+
const n = Number(env.TABLON_MINIMO_USD.replace(',', '.'));
|
|
100
|
+
if (Number.isFinite(n) && n > 0) o.minimoUsd = n;
|
|
101
|
+
else avisar(`[tablon] TABLON_MINIMO_USD="${env.TABLON_MINIMO_USD}" no vale (un número mayor que 0): sin mínimo.`);
|
|
102
|
+
}
|
|
87
103
|
return o;
|
|
88
104
|
}
|
|
89
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Lo que este agente cobraría de verdad por un encargo, en dólares: el importe
|
|
108
|
+
* menos el 2,5 % del escrow y, en $PANAL, lo que daría venderlos en la curva.
|
|
109
|
+
* `null` si no hay un precio de menos de cinco minutos.
|
|
110
|
+
*/
|
|
111
|
+
export async function dolaresNetos(
|
|
112
|
+
panal: Pick<PanalClient, 'precios' | 'valorDeVentaPanal'>,
|
|
113
|
+
e: { amount: bigint; currency: Address },
|
|
114
|
+
): Promise<number | null> {
|
|
115
|
+
const neto = (e.amount * (10_000n - FEE_BPS)) / 10_000n;
|
|
116
|
+
const precios = await panal.precios().catch(() => null);
|
|
117
|
+
if (!esFresco(precios?.mon)) return null;
|
|
118
|
+
// El escrow solo acepta MON y $PANAL: lo que no es MON es $PANAL.
|
|
119
|
+
const enMon = e.currency.toLowerCase() === NATIVE_CURRENCY ? neto : await panal.valorDeVentaPanal(neto).catch(() => null);
|
|
120
|
+
if (enMon === null) return null;
|
|
121
|
+
return Number(formatEther(enMon)) * precios.mon.valor;
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
/** Sin tildes, en minúsculas, y con guiones y barras como espacios. */
|
|
91
125
|
function plano(s: string): string {
|
|
92
126
|
return s
|
|
@@ -173,6 +207,23 @@ export async function repasarTablon(deps: DepsTablon): Promise<string[]> {
|
|
|
173
207
|
const elegido = candidatos(lista, deps, Math.floor(Date.now() / 1000))[0];
|
|
174
208
|
if (!elegido) return hecho;
|
|
175
209
|
|
|
210
|
+
// El que más paga va primero y todos son de la misma moneda: si ese no llega
|
|
211
|
+
// al mínimo, ninguno llega.
|
|
212
|
+
const minimo = deps.opciones.minimoUsd;
|
|
213
|
+
if (minimo !== null) {
|
|
214
|
+
const usd = await dolaresNetos(deps.panal, elegido);
|
|
215
|
+
if (usd === null) {
|
|
216
|
+
anotar(`[tablon] #${elegido.taskId} sin precio fresco en dólares: no se coge nada.`);
|
|
217
|
+
return hecho;
|
|
218
|
+
}
|
|
219
|
+
if (usd < minimo) {
|
|
220
|
+
anotar(
|
|
221
|
+
`[tablon] #${elegido.taskId} deja ${formatearDolares(usd)} y el mínimo es ${formatearDolares(minimo)}: no se coge.`,
|
|
222
|
+
);
|
|
223
|
+
return hecho;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
deps.marcar(true);
|
|
177
228
|
try {
|
|
178
229
|
try {
|
|
@@ -239,7 +290,8 @@ export async function arrancarTablon(
|
|
|
239
290
|
|
|
240
291
|
const completas: DepsTablon = { ...deps, precio, habilidades };
|
|
241
292
|
console.log(
|
|
242
|
-
`Tablón: cada ${deps.opciones.minutos} min · desde ${formatEther(precio.amount)} en su moneda
|
|
293
|
+
`Tablón: cada ${deps.opciones.minutos} min · desde ${formatEther(precio.amount)} en su moneda` +
|
|
294
|
+
`${deps.opciones.minimoUsd !== null ? ` y ${formatearDolares(deps.opciones.minimoUsd)} netos` : ''} · ` +
|
|
243
295
|
`con ${deps.opciones.margenMinutos} min de plazo como mínimo · ${habilidades.length + deps.opciones.palabras.length} palabras.`,
|
|
244
296
|
);
|
|
245
297
|
let enMarcha = false;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Aquí `GET /agent.json?lang=fr` devuelve tu MISMA ficha con las frases en
|
|
14
14
|
* francés. Nadie tiene que aprender un formato nuevo: los lectores siguen
|
|
15
|
-
* mirando `description
|
|
15
|
+
* mirando `description`, `tiers[].name` y `examples`, solo que traducidos.
|
|
16
16
|
*
|
|
17
17
|
* QUÉ CUESTA, QUE ES LA PREGUNTA DE VERDAD
|
|
18
18
|
*
|
|
@@ -65,6 +65,12 @@ export interface Frases {
|
|
|
65
65
|
description: string;
|
|
66
66
|
/** Nombre y descripción de cada nivel, en el orden en que van en la ficha. */
|
|
67
67
|
tiers: { name: string; description: string }[];
|
|
68
|
+
/**
|
|
69
|
+
* Los encargos de ejemplo, en su orden. Sin campo cuando no hay ninguno, y
|
|
70
|
+
* a propósito: así la huella de una ficha sin ejemplos es la de siempre y
|
|
71
|
+
* sus traducciones ya guardadas siguen valiendo.
|
|
72
|
+
*/
|
|
73
|
+
examples?: string[];
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/** Tope de la respuesta que se acepta del modelo, por frase. */
|
|
@@ -144,7 +150,20 @@ function validar(v: unknown, original: Frases | null): Frases | null {
|
|
|
144
150
|
if (typeof name !== 'string' || typeof d !== 'string') return null;
|
|
145
151
|
salida.push({ name: name.trim().slice(0, MAX_FRASE), description: d.trim().slice(0, MAX_FRASE) });
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
// Los ejemplos, con la misma regla que los niveles: los mismos en número o
|
|
154
|
+
// se descarta todo. Si el original no tiene, no se esperan.
|
|
155
|
+
const crudos = (v as Record<string, unknown>).examples;
|
|
156
|
+
let examples: string[] = [];
|
|
157
|
+
if ((original?.examples?.length ?? 0) > 0 || crudos !== undefined) {
|
|
158
|
+
if (!Array.isArray(crudos) || crudos.some((e) => typeof e !== 'string')) return null;
|
|
159
|
+
if (original && crudos.length !== (original.examples?.length ?? 0)) return null;
|
|
160
|
+
examples = (crudos as string[]).map((e) => e.trim().slice(0, MAX_FRASE));
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
description: description.trim().slice(0, MAX_FRASE),
|
|
164
|
+
tiers: salida,
|
|
165
|
+
...(examples.length > 0 ? { examples } : {}),
|
|
166
|
+
};
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
/** El JSON que venga, aunque llegue envuelto en un bloque de markdown. */
|