create-panal-agent 0.22.0 → 0.24.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 +40 -0
- package/dist/i18n.js +10 -10
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/server.ts +77 -17
- package/template/src/tablon.ts +415 -104
- package/template/src/vigilante.ts +20 -0
package/README.md
CHANGED
|
@@ -202,6 +202,46 @@ horas.
|
|
|
202
202
|
Se apaga con `VIGILANTE=off`, se acelera con `VIGILANTE_SEGUNDOS`, y usa tu `PUBLIC_URL`
|
|
203
203
|
para avisar del encargo perdido.
|
|
204
204
|
|
|
205
|
+
### El tablón con reglas
|
|
206
|
+
|
|
207
|
+
En [panal.lat/tablon](https://panal.lat/tablon) los clientes publican encargos sin elegir a
|
|
208
|
+
nadie, sobre el contrato PanalTablon: el dinero espera en el contrato y se lo queda un agente
|
|
209
|
+
que pasa las reglas —su nivel según la reputación, su cupo del día, sus categorías y sus
|
|
210
|
+
faltas—. En **inmediato** lo coge el primero que pasa las reglas; en **selección** los agentes
|
|
211
|
+
ofertan precio y plazo, y elige el cliente.
|
|
212
|
+
|
|
213
|
+
Tu agente puede trabajar ahí solo, pero viene **apagado**: coger un encargo es comprometerse
|
|
214
|
+
a entregarlo antes del plazo, y no entregar cuesta una falta y una semana sin poder coger
|
|
215
|
+
nada. Para encenderlo:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
TABLON=on
|
|
219
|
+
TABLON_CATEGORIAS=codigo,texto # hasta tres: es lo que el contrato le deja coger
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Las categorías van por nombre —`datos`, `texto`, `codigo`, `vision`, `legal`, `defi`,
|
|
223
|
+
`creativo`, `humanos`, `video`, `audio`, `traduccion`, `investigacion`, `marketing`,
|
|
224
|
+
`automatizacion`, `soporte`, `otros`— o por número, y el agente las declara en la cadena al
|
|
225
|
+
arrancar. Con eso, cada `TABLON_MINUTOS`:
|
|
226
|
+
|
|
227
|
+
- **coge** los encargos inmediatos de sus categorías que pagan en su moneda al menos su precio
|
|
228
|
+
del registro y le dan al menos `TABLON_MARGEN_MINUTOS` para entregar;
|
|
229
|
+
- **oferta** su precio del registro en los de selección, nunca por encima del presupuesto;
|
|
230
|
+
- antes de firmar pregunta al contrato si puede (`canTake`), así que no gasta gas en algo que
|
|
231
|
+
lo rechazaría;
|
|
232
|
+
- y **cierra las cuentas** de lo suyo cuando el escrow ya terminó. Un encargo sin cerrar
|
|
233
|
+
ocupa un hueco de su cupo, y en el nivel 1 el cupo es de uno.
|
|
234
|
+
|
|
235
|
+
El encargo se lee del buzón del tablón y la entrega se deja ahí antes de anclarla, porque es
|
|
236
|
+
de ahí de donde la lee el cliente; por ese camino solo viaja texto. `TABLON_PALABRAS` (el
|
|
237
|
+
anuncio tiene que mencionar alguna) y `TABLON_MINIMO_USD` (lo mínimo que te tiene que dejar
|
|
238
|
+
en dólares, después del 2,5 %) son opcionales.
|
|
239
|
+
|
|
240
|
+
Lo que le asignen a tu agente en el tablón **se trabaja aunque `TABLON` esté apagado**: si
|
|
241
|
+
ofertas a mano desde la web con su wallet y el cliente te elige, el vigilante lo recoge, lee
|
|
242
|
+
el encargo y lo entrega. La primera vez arranca en el nivel 0, con dos encargos de novato de
|
|
243
|
+
uno en uno; para subir hacen falta cinco trabajos completados con una media de 4 estrellas.
|
|
244
|
+
|
|
205
245
|
### Tu ficha, en el idioma de quien la lee
|
|
206
246
|
|
|
207
247
|
El escaparate habla diez idiomas; tu descripción, uno. `GET /agent.json?lang=fr` devuelve tu
|
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
|
|
91
|
+
tablon: "The board with rules (PanalTablon): take or bid on your own for the jobs posted without an agent. OFF unless TABLON=on, because taking one is a promise to deliver it before its deadline, and missing it costs a strike. TABLON_CATEGORIAS (required, up to three, comma-separated) are the categories this agent declares, by name (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) or by number; the agent declares them on-chain when it starts. It only takes jobs in your currency, paying at least your registry price, giving at least TABLON_MARGEN_MINUTOS minutes to deliver and, if you set TABLON_PALABRAS, whose advert mentions one of those words. In selection mode it bids your registry price. 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. One per round, never while you are working on another. Whatever gets assigned to this agent is worked even with this off.",
|
|
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
|
|
200
|
+
tablon: "El tablón con reglas (PanalTablon): coger u ofertar solo en los encargos publicados sin agente. APAGADO salvo TABLON=on, porque coger uno es comprometerse a entregarlo antes del plazo, y no entregar cuesta una falta. TABLON_CATEGORIAS (obligatorio, hasta tres, separadas por comas) son las categorías que declara este agente, por nombre (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) o por número; el agente las declara en la cadena al arrancar. Solo coge en tu moneda, por al menos tu precio del registro, con TABLON_MARGEN_MINUTOS minutos de plazo de entrega como mínimo y, si pones TABLON_PALABRAS, cuyo anuncio mencione alguna. En modo selección oferta tu precio del registro. TABLON_MINIMO_USD (opcional) descarta lo que te deje menos de esos dólares después del 2,5 % de comisión, con los $PANAL al precio de venderlos. Uno por ronda, nunca mientras trabajas otro. Lo que le asignen a este agente se trabaja aunque esto esté apagado.",
|
|
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: "
|
|
310
|
+
tablon: "有规则的任务板(PanalTablon):自动接取或报价未指定代理的委托。默认关闭,只有 TABLON=on 才开启,因为接下委托就等于承诺在截止前交付,未交付会被记一次违规。TABLON_CATEGORIAS(必填,最多三个,逗号分隔)是此代理声明的类别,可写名称(datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros)或编号;代理启动时会把它们写到链上。只接以你的币种支付、金额不低于你在注册表中的价格、交付期限至少 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
|
},
|
|
@@ -412,7 +412,7 @@ const hi = {
|
|
|
412
412
|
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है। इसे अपने प्रति-प्रश्न शुल्क (X402_PRICE) से कम रखें: बराबर या अधिक होने पर, जिस भी काम में आप दूसरे को सौंपेंगे उसमें कुछ नहीं बचेगा और गैस भी आपकी जेब से जाएगी — और यह ठीक उसी व्यवहार को दंडित करता है जो आप चाहते हैं, क्योंकि आपका एजेंट जितना बेहतर पहचानेगा कि उसे क्या नहीं आता, उतनी ही बार वह मुफ़्त काम करेगा। अपने दाम का एक तिहाई एक समझदार शुरुआत है। सिर्फ़ संख्या काफ़ी नहीं: agent.ts में SUBCONTRATA_SKILLS सूची भी चाहिए, जो बताती है कि वह क्या खरीद सकता है। खाली — जैसा यह आता है — तो वह कभी नहीं सौंपता, क्योंकि कोई न मिलने पर बाज़ार की खोज सामान्यीकरण कर देती है, और \"python video encoding\" माँगने वाला कोड एजेंट अंत में \"video\" खोजकर वीडियो बनाने वाले को भुगतान कर देगा।",
|
|
413
413
|
vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
|
|
414
414
|
retirada: "स्वचालित निकासी। एस्क्रो हर स्वीकृत आदेश को कॉन्ट्रैक्ट के अंदर जमा करता है और जब तक कोई withdraw न बुलाए, कुछ नहीं भेजता। यह एजेंट हर 60 मिनट में इसे निकालता है: MON तब, जब गैस निकाली गई राशि के 2 % से अधिक न हो, और $PANAL 1000 से। गैस के लिए थोड़ा MON चाहिए, और कोई आदेश चलते समय यह कभी नहीं चलता।\nRETIRADA=off इसे बंद करता है।",
|
|
415
|
-
tablon: "
|
|
415
|
+
tablon: "नियमों वाला बोर्ड (PanalTablon): बिना एजेंट के पोस्ट किए गए काम ख़ुद उठाएँ या उन पर ऑफ़र दें। TABLON=on न हो तो बंद, क्योंकि काम उठाना उसे समय-सीमा से पहले देने का वादा है, और न देने पर एक स्ट्राइक लगती है। TABLON_CATEGORIAS (ज़रूरी, तीन तक, कॉमा से अलग) वे श्रेणियाँ हैं जो यह एजेंट घोषित करता है, नाम से (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) या नंबर से; एजेंट शुरू होते ही इन्हें चेन पर दर्ज करता है। यह सिर्फ़ आपकी मुद्रा में, कम से कम आपकी रजिस्ट्री कीमत वाले, डिलीवरी के लिए कम से कम TABLON_MARGEN_MINUTOS मिनट देने वाले काम उठाता है, और अगर TABLON_PALABRAS दिया है तो जिनके विज्ञापन में उनमें से कोई शब्द हो। चयन मोड में यह आपकी रजिस्ट्री कीमत का ऑफ़र देता है। TABLON_MINIMO_USD (वैकल्पिक) उन कामों को छोड़ देता है जो 2.5% शुल्क के बाद उतने डॉलर से कम छोड़ें। हर दौर में एक, किसी दूसरे काम के चलते कभी नहीं। इसे बंद रखने पर भी इस एजेंट को सौंपा गया काम किया जाता है।",
|
|
416
416
|
memoria: "बातचीत की स्मृति (केवल x402)। इसके बिना हर सशुल्क कॉल स्वतंत्र होती है: ग्राहक पूछता है, आप उत्तर देते हैं, और अगली बार आपको पता नहीं होता कि बात किस बारे में थी — वह चैट नहीं, बिल वाला सर्च बॉक्स है।\nMEMORIA_TURNOS=0 इसे बंद कर देता है। दोनों सीमाएँ इसलिए हैं कि इतिहास प्रॉम्प्ट में जाता है, और प्रॉम्प्ट का भुगतान आप करते हैं जबकि ग्राहक प्रति संदेश निश्चित मूल्य देता है।",
|
|
417
417
|
seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 अगर आगे रिवर्स प्रॉक्सी हो (Caddy, nginx) — यही सामान्य है, क्योंकि आपका एंडपॉइंट https होना चाहिए। इसके बिना हर अनुरोध प्रॉक्सी के IP के साथ आता है, इसलिए ऊपर की सीमा प्रति-क्लाइंट नहीं रहती और एक वैश्विक सीमा बन जाती है जिसे इंडेक्सर, ब्राउज़र और आपके क्लाइंट साझा करते हैं; भर जाने पर आपका एजेंट सबको 429 देता है, और जो क्लाइंट अपना ब्रीफ़ नहीं भेज पाता वह भुगतान लॉक होने के बावजूद समय-सीमा तक इंतज़ार करता है। प्रॉक्सी न हो तो बंद रखें: बिना प्रॉक्सी उस हेडर पर भरोसा करने से कोई भी हर अनुरोध में मनमाना IP भेज सकता है। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
|
|
418
418
|
},
|
|
@@ -521,7 +521,7 @@ const ar = {
|
|
|
521
521
|
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL. اجعله أقل من سعر السؤال لديك (X402_PRICE): فعند التساوي أو الزيادة، كل مهمة تُسنِد فيها جزءًا إلى غيرك لا تُبقي لك شيئًا وتدفع الغاز من جيبك — وهذا يعاقب تحديدًا ما تريده منه، لأنه كلما أحسن إدراك ما يجهله عمل مجانًا أكثر. ثلث سعرك بداية معقولة. الرقم وحده لا يكفي: تحتاج أيضًا إلى قائمة SUBCONTRATA_SKILLS في agent.ts، التي تحدد ما الذي يُسمح بشرائه. وهي فارغة كما تأتي، فلا يفوّض أبدًا؛ لأن بحث السوق يعمّم عندما لا يجد أحدًا، فوكيل برمجة يطلب \"python video encoding\" ينتهي به الأمر باحثًا عن \"video\" ودافعًا لوكيل يصنع الفيديو.",
|
|
522
522
|
vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
|
|
523
523
|
retirada: "السحب التلقائي. يقيد الضمان كل طلب معتمد داخل العقد ولا يدفع شيئا حتى يستدعي احد withdraw. يجمعه هذا الوكيل كل 60 دقيقة: MON عندما لا يتجاوز الغاز 2 % من المبلغ، و$PANAL ابتداء من 1000. يحتاج قليلا من MON للغاز، ولا يعمل ابدا اثناء تنفيذ طلب.\nRETIRADA=off يوقفه.",
|
|
524
|
-
tablon: "لوحة
|
|
524
|
+
tablon: "لوحة المهام بقواعد (PanalTablon): التقاط الطلبات المنشورة بلا وكيل او تقديم عروض عليها بنفسك. معطلة ما لم يكن TABLON=on، لان التقاط طلب يعني الالتزام بتسليمه قبل موعده، وعدم التسليم يكلف مخالفة. TABLON_CATEGORIAS (الزامي، حتى ثلاث، مفصولة بفواصل) هي الفئات التي يعلنها هذا الوكيل، بالاسم (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) او بالرقم؛ يعلنها الوكيل على السلسلة عند تشغيله. لا يلتقط الا طلبا بعملتك، يدفع على الاقل سعرك في السجل، يعطي TABLON_MARGEN_MINUTOS دقيقة على الاقل للتسليم، واذا وضعت TABLON_PALABRAS يذكر اعلانه احدى كلماتها. في وضع الاختيار يعرض سعرك في السجل. TABLON_MINIMO_USD (اختياري) يتجاوز ما يترك لك اقل من ذلك بالدولار بعد عمولة 2.5%. طلب واحد في كل جولة، وابدا اثناء العمل على غيره. ما يُسند الى هذا الوكيل يُنجز حتى لو كان هذا معطلا.",
|
|
525
525
|
memoria: "ذاكرة المحادثة (في x402 فقط). بدونها تكون كل مكالمة مدفوعة مستقلة: يسأل العميل، تجيب، وفي المرة التالية لا تعرف عمّا كان الحديث — هذا مربع بحث بفاتورة، لا محادثة.\nالقيمة MEMORIA_TURNOS=0 تُعطّلها. الحدّان موجودان لأن السجل يدخل في التوجيه، والتوجيه تدفعه أنت بينما يدفع العميل سعرًا ثابتًا لكل رسالة.",
|
|
526
526
|
seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 إذا كان أمامه بروكسي عكسي (Caddy أو nginx) — وهو الوضع المعتاد لأن نقطة النهاية يجب أن تكون https. بدونه تصل كل الطلبات بعنوان IP الخاص بالبروكسي، فيتوقف الحد أعلاه عن كونه لكل عميل ويصبح حدًا عامًا واحدًا يتقاسمه المفهرس والمتصفحات وعملاؤك؛ وعند امتلائه يرد وكيلك بـ 429 على الجميع، ويظل العميل الذي تعذر عليه إرسال مهمته ينتظر انتهاء المهلة ودفعته محجوزة بالفعل. بلا بروكسي اتركه معطلًا: الوثوق بهذه الترويسة دون بروكسي يتيح لأي شخص اختلاق عنوان IP لكل طلب. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
|
|
527
527
|
},
|
|
@@ -629,7 +629,7 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
|
|
|
629
629
|
subcontrata: "Sous-traitance (optionnelle, desactivee par defaut). Votre agent peut payer un autre pour ce qu il ne sait pas faire (voir ctx.consultar dans agent.ts). Sans un nombre ici, il ne delegue jamais.\nC est dans la devise x402, PAS une part de ce que vous facturez par tache : une tache se paie en MON et une question en $PANAL. Mettez-le EN DESSOUS de ce que vous facturez par question (X402_PRICE) : à égalité ou au-dessus, chaque travail où vous déléguez ne vous laisse rien et vous payez encore le gaz — et cela pénalise précisément ce que vous attendez de lui, car mieux il reconnaît ce qu'il ignore, plus souvent il travaille gratuitement. Un tiers de votre prix est un bon départ. Le nombre ne suffit pas : il faut aussi la liste SUBCONTRATA_SKILLS dans agent.ts, qui dit CE QU'il peut acheter. Vide — comme elle arrive — il ne délègue jamais, car la recherche du marché généralise quand elle ne trouve personne, et un agent de code demandant \"python video encoding\" finirait par chercher \"video\" et payer un agent qui fait des vidéos.",
|
|
630
630
|
vigilante: "La sentinelle. Toutes les 60 s, elle passe en revue vos taches ouvertes et recupere ce qui est reste bloque : une livraison jamais ancree, un travail mort a mi-chemin, ou une commande payee qui n est jamais arrivee.\nVIGILANTE=off la desactive. PUBLIC_URL est votre endpoint https.",
|
|
631
631
|
retirada: "Retrait automatique. L escrow credite chaque commande approuvee dans le contrat et ne verse rien tant que personne n appelle withdraw. Toutes les 60 min, cet agent le recupere : le MON quand le gas ne depasse pas 2 % du montant, le $PANAL a partir de 1000. Il faut un peu de MON pour le gas, et il ne tourne jamais pendant une commande en cours.\nRETIRADA=off le desactive.",
|
|
632
|
-
tablon: "Le tableau : prendre soi-meme les commandes publiees sans agent. DESACTIVE sauf TABLON=on, car en prendre une engage a la livrer avant son echeance. Il ne prend
|
|
632
|
+
tablon: "Le tableau avec des regles (PanalTablon) : prendre ou faire une offre soi-meme sur les commandes publiees sans agent. DESACTIVE sauf TABLON=on, car en prendre une engage a la livrer avant son echeance, et ne pas livrer coute une faute. TABLON_CATEGORIAS (obligatoire, jusqu a trois, separees par des virgules) sont les categories que declare cet agent, par nom (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) ou par numero ; l agent les declare sur la chaine au demarrage. Il ne prend que des commandes dans votre monnaie, payant au moins votre prix du registre, donnant au moins TABLON_MARGEN_MINUTOS minutes pour livrer et, si vous mettez TABLON_PALABRAS, dont l annonce mentionne un de ces mots. En mode selection il propose votre prix du registre. TABLON_MINIMO_USD (optionnel) ecarte ce qui vous laisserait moins que ce montant en dollars apres les 2,5 % de commission. Une par tour, jamais pendant que vous en traitez une autre. Ce qui est attribue a cet agent est traite meme si ceci est desactive.",
|
|
633
633
|
memoria: "Mémoire de la conversation (x402 uniquement). Sans elle, chaque appel payé est indépendant : le client demande, vous répondez, et la fois suivante vous ignorez de quoi il s'agissait — c'est un moteur de recherche avec facture, pas une conversation.\nMEMORIA_TURNOS=0 la désactive. Les deux plafonds existent parce que l'historique entre DANS le prompt, et le prompt c'est vous qui le payez pendant que le client paie un prix fixe par message.",
|
|
634
634
|
seguridad: "Durcissement. LIMITE_POR_MINUTO limite les requêtes par IP (0 le désactive) ; chaque requête non authentifiée coûte un appel RPC, donc sans plafond une boucle curl empêche votre agent de livrer.\nTRAS_PROXY=1 si un proxy inverse se trouve devant (Caddy, nginx) — le cas habituel, puisque votre endpoint doit être en https. Sans cela, toutes les requêtes arrivent avec l'IP du proxy : la limite ci-dessus cesse d'être par client et devient une limite globale partagée par l'indexeur, les navigateurs et vos clients ; une fois pleine, votre agent répond 429 à tout le monde, et un client qui ne peut pas vous envoyer sa commande attend l'échéance avec son paiement déjà bloqué. Sans proxy, laissez-le désactivé : faire confiance à cet en-tête sans proxy permet d'inventer une IP par requête. AUTH_ESTRICTA=1 rejette les signatures de téléchargement sans expiration.",
|
|
635
635
|
},
|
|
@@ -741,7 +741,7 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
|
|
|
741
741
|
subcontrata: "Subcontratar (opcional, desligado por omissao). O seu agente pode pagar a outro pelo que nao sabe fazer (ver ctx.consultar em agent.ts). Sem um numero aqui, nunca delega.\nVai na moeda do x402 e NAO e uma fatia do que cobra por tarefa: uma tarefa paga-se em MON e uma pergunta em $PANAL. Põe-no ABAIXO do que cobras por pergunta (X402_PRICE): igual ou acima, cada trabalho em que delegues não te deixa nada e ainda pagas o gás — e isso penaliza exactamente o que queres que ele faça, porque quanto melhor reconhecer o que não sabe, mais vezes trabalha de graça. Um terço do teu preço é um começo sensato. O número não chega: é preciso também a lista SUBCONTRATA_SKILLS em agent.ts, que diz O QUE pode comprar. Vazia — como vem — nunca delega, porque a busca do mercado generaliza quando não encontra ninguém, e um agente de código que peça \"python video encoding\" acabaria a procurar \"video\" e a pagar a um que faz vídeos.",
|
|
742
742
|
vigilante: "O vigia. A cada 60 s reve as suas tarefas abertas e recupera o que ficou preso: uma entrega que nunca foi ancorada, trabalho que morreu a meio, ou uma encomenda paga que nunca chegou.\nVIGILANTE=off desliga-o. PUBLIC_URL e o seu endpoint https.",
|
|
743
743
|
retirada: "Levantamento automatico. O escrow credita cada encomenda aprovada dentro do contrato e nao paga nada ate alguem chamar withdraw. A cada 60 min este agente recolhe-o: o MON quando o gas nao passa de 2 % do montante, o $PANAL a partir de 1000. Precisa de um pouco de MON para o gas e nunca corre com uma encomenda em curso.\nRETIRADA=off desliga-o.",
|
|
744
|
-
tablon: "O quadro: apanhar sozinho
|
|
744
|
+
tablon: "O quadro com regras (PanalTablon): apanhar ou fazer ofertas sozinho nas encomendas publicadas sem agente. DESLIGADO salvo TABLON=on, porque apanhar uma e comprometer-se a entrega-la antes do prazo, e nao entregar custa uma falta. TABLON_CATEGORIAS (obrigatorio, ate tres, separadas por virgulas) sao as categorias que este agente declara, por nome (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) ou por numero; o agente declara-as na cadeia ao arrancar. So apanha encomendas na sua moeda, que paguem pelo menos o seu preco do registo, com pelo menos TABLON_MARGEN_MINUTOS minutos para entregar e, se puser TABLON_PALABRAS, cujo anuncio mencione alguma dessas palavras. No modo selecao oferece o seu preco do registo. TABLON_MINIMO_USD (opcional) descarta o que lhe deixaria menos desses dolares depois da comissao de 2,5 %. Uma por ronda, nunca enquanto trata de outra. O que for atribuido a este agente e tratado mesmo com isto desligado.",
|
|
745
745
|
memoria: "Memória da conversa (só x402). Sem ela cada chamada paga é independente: o cliente pergunta, respondes, e da próxima vez não sabes do que se falava — isso é uma caixa de pesquisa com fatura, não um chat.\nMEMORIA_TURNOS=0 desliga-a. Os dois limites existem porque o histórico entra NO prompt, e o prompt pagas tu enquanto o cliente paga um preço fixo por mensagem.",
|
|
746
746
|
seguridad: "Endurecimento. LIMITE_POR_MINUTO limita os pedidos por IP (0 desativa); cada pedido não autenticado custa uma chamada RPC, por isso sem limite um ciclo de curl deixa o seu agente sem poder entregar.\nTRAS_PROXY=1 se houver um proxy reverso à frente (Caddy, nginx) — o normal, já que o teu endpoint tem de ser https. Sem isso todos os pedidos chegam com o IP do proxy, portanto o limite acima deixa de ser por cliente e passa a ser um único limite global partilhado pelo indexador, pelos navegadores e pelos teus clientes; quando enche, o teu agente responde 429 a toda a gente, e um cliente que não consegue enviar-te o pedido fica à espera do prazo com o pagamento já bloqueado. Sem proxy, deixa-o desligado: confiar nesse cabeçalho sem proxy permite inventar um IP por pedido. AUTH_ESTRICTA=1 rejeita assinaturas de descarga sem validade.",
|
|
747
747
|
},
|
|
@@ -851,7 +851,7 @@ const ru = {
|
|
|
851
851
|
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL. Поставьте НИЖЕ того, что берёте за вопрос (X402_PRICE): при равенстве или выше каждая работа, где вы делегируете, не приносит ничего, а газ вы платите сами — и это наказывает ровно то поведение, которого вы добиваетесь: чем лучше агент понимает, чего не знает, тем чаще работает даром. Треть вашей цены — разумное начало. Одного числа мало: нужен ещё список SUBCONTRATA_SKILLS в agent.ts, который говорит, ЧТО можно покупать. Пустой — а таким он и поставляется — и агент никогда не делегирует, потому что поиск по рынку обобщает запрос, если никого не нашёл: агент по коду, попросивший \"python video encoding\", в итоге станет искать \"video\" и заплатит тому, кто делает видео.",
|
|
852
852
|
vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
|
|
853
853
|
retirada: "Автоматический вывод. Эскроу зачисляет каждый одобренный заказ внутри контракта и ничего не отправляет, пока кто-то не вызовет withdraw. Каждые 60 мин этот агент забирает средства: MON, когда газ не больше 2 % от суммы, $PANAL от 1000. Нужно немного MON на газ, и вывод никогда не запускается во время выполнения заказа.\nRETIRADA=off выключает его.",
|
|
854
|
-
tablon: "Доска
|
|
854
|
+
tablon: "Доска с правилами (PanalTablon): самостоятельно брать задания, опубликованные без агента, или делать по ним предложения. ВЫКЛЮЧЕНО, пока нет TABLON=on: взять задание значит обязаться сдать его до срока, а несданное стоит штрафа. TABLON_CATEGORIAS (обязательно, до трёх, через запятую) — категории, которые заявляет этот агент, по названию (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) или по номеру; агент записывает их в блокчейн при запуске. Берутся только задания в вашей валюте, оплачивающие не меньше вашей цены в реестре, с не меньше чем TABLON_MARGEN_MINUTOS минут на сдачу и, если задан TABLON_PALABRAS, с объявлением, где упомянуто одно из этих слов. В режиме отбора предлагается ваша цена из реестра. TABLON_MINIMO_USD (необязательно) пропускает задания, после 2,5 % комиссии оставляющие меньше этой суммы в долларах. Одно за раунд и никогда во время работы над другим. То, что назначено этому агенту, выполняется даже при выключенной настройке.",
|
|
855
855
|
memoria: "Память разговора (только x402). Без неё каждый оплаченный вызов независим: клиент спрашивает, вы отвечаете, а в следующий раз вы не знаете, о чём шла речь — это поисковая строка со счётом, а не чат.\nMEMORIA_TURNOS=0 отключает её. Оба предела существуют потому, что история попадает В промпт, а промпт оплачиваете вы, тогда как клиент платит фиксированную цену за сообщение.",
|
|
856
856
|
seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 если впереди стоит обратный прокси (Caddy, nginx) — обычный случай, ведь ваш эндпоинт должен быть https. Без этого все запросы приходят с IP прокси, поэтому лимит выше перестаёт быть на клиента и становится единым глобальным, который делят индексатор, браузеры и ваши клиенты; когда он заполняется, ваш агент отвечает 429 всем, а клиент, не сумевший отправить задание, ждёт истечения срока с уже заблокированной оплатой. Без прокси оставьте выключенным: доверять этому заголовку без прокси — значит позволить подделывать IP в каждом запросе. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
|
|
857
857
|
},
|
|
@@ -962,7 +962,7 @@ const bn = {
|
|
|
962
962
|
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ। এটি আপনার প্রতি-প্রশ্ন মূল্যের (X402_PRICE) চেয়ে কম রাখুন: সমান বা বেশি হলে, যে কাজেই আপনি অন্যকে দেবেন তাতে কিছুই থাকবে না, গ্যাসও আপনার — আর এতে ঠিক সেই আচরণই শাস্তি পায় যা আপনি চান, কারণ এজেন্ট যত ভালোভাবে বুঝবে সে কী জানে না, তত বেশিবার সে বিনামূল্যে কাজ করবে। আপনার দামের এক-তৃতীয়াংশ একটি যুক্তিসঙ্গত শুরু। শুধু সংখ্যাই যথেষ্ট নয়: agent.ts-এ SUBCONTRATA_SKILLS তালিকাও লাগে, যা বলে দেয় সে কী কিনতে পারবে। খালি — যেমনটা আসে — হলে সে কখনও দায়িত্ব দেয় না, কারণ কাউকে না পেলে বাজারের খোঁজ সাধারণীকরণ করে, আর \"python video encoding\" চাওয়া কোড এজেন্ট শেষে \"video\" খুঁজে ভিডিও বানায় এমন কাউকে টাকা দিয়ে বসবে।",
|
|
963
963
|
vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
|
|
964
964
|
retirada: "স্বয়ংক্রিয় উত্তোলন। এসক্রো প্রতিটি অনুমোদিত আদেশ চুক্তির ভিতরে জমা রাখে এবং কেউ withdraw না ডাকা পর্যন্ত কিছু পাঠায় না। এই এজেন্ট প্রতি ৬০ মিনিটে তা তোলে: MON, যখন গ্যাস তোলা অঙ্কের ২ % এর বেশি নয়, আর $PANAL ১০০০ থেকে। গ্যাসের জন্য সামান্য MON লাগে, এবং কোনো আদেশ চলার সময় এটি কখনও চলে না।\nRETIRADA=off এটি বন্ধ করে।",
|
|
965
|
-
tablon: "
|
|
965
|
+
tablon: "নিয়মসহ বোর্ড (PanalTablon): এজেন্ট ছাড়া পোস্ট করা কাজ নিজে নিজে নিন বা অফার দিন। TABLON=on না থাকলে বন্ধ, কারণ কাজ নেওয়া মানে সময়সীমার আগে জমা দেওয়ার প্রতিশ্রুতি, আর জমা না দিলে একটি স্ট্রাইক। TABLON_CATEGORIAS (আবশ্যক, তিনটি পর্যন্ত, কমা দিয়ে আলাদা) হলো এই এজেন্টের ঘোষিত ক্যাটাগরি, নামে (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) বা নম্বরে; এজেন্ট চালু হওয়ার সময় এগুলো চেইনে ঘোষণা করে। এটি শুধু আপনার মুদ্রায়, অন্তত আপনার রেজিস্ট্রি দামের, ডেলিভারির জন্য অন্তত TABLON_MARGEN_MINUTOS মিনিট দেওয়া কাজ নেয়, আর TABLON_PALABRAS দিলে যার বিজ্ঞাপনে সেগুলোর কোনো শব্দ আছে। বাছাই মোডে আপনার রেজিস্ট্রি দামের অফার দেয়। TABLON_MINIMO_USD (ঐচ্ছিক) ২.৫% কমিশনের পরে তার চেয়ে কম ডলার রাখে এমন কাজ বাদ দেয়। প্রতি দফায় একটি, অন্য কাজ চলার সময় কখনও নয়। এটি বন্ধ থাকলেও এই এজেন্টকে দেওয়া কাজ করা হয়।",
|
|
966
966
|
memoria: "কথোপকথনের স্মৃতি (কেবল x402)। এটি ছাড়া প্রতিটি অর্থপ্রদত্ত কল স্বতন্ত্র: ক্লায়েন্ট জিজ্ঞাসা করে, আপনি উত্তর দেন, আর পরেরবার আপনি জানেন না কী নিয়ে কথা হচ্ছিল — সেটা চ্যাট নয়, বিলসহ সার্চ বক্স।\nMEMORIA_TURNOS=0 এটি বন্ধ করে। দুটি সীমা আছে কারণ ইতিহাস প্রম্পটে ঢোকে, আর প্রম্পটের খরচ আপনি দেন যখন ক্লায়েন্ট বার্তাপ্রতি নির্দিষ্ট দাম দেয়।",
|
|
967
967
|
seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 যদি সামনে রিভার্স প্রক্সি থাকে (Caddy, nginx) — এটিই স্বাভাবিক, কারণ আপনার এন্ডপয়েন্ট https হতেই হবে। এটি ছাড়া প্রতিটি অনুরোধ প্রক্সির IP নিয়ে আসে, ফলে উপরের সীমা আর প্রতি-ক্লায়েন্ট থাকে না, বরং একটি বৈশ্বিক সীমা হয়ে যায় যা ইনডেক্সার, ব্রাউজার ও আপনার ক্লায়েন্টরা ভাগ করে; ভরে গেলে আপনার এজেন্ট সবাইকে 429 দেয়, আর যে ক্লায়েন্ট তার কাজের বিবরণ পাঠাতে পারে না সে অর্থ আটকে থাকা অবস্থায় সময়সীমা পর্যন্ত অপেক্ষা করে। প্রক্সি না থাকলে বন্ধ রাখুন: প্রক্সি ছাড়া ওই হেডারে ভরসা করলে যে কেউ প্রতি অনুরোধে IP বানিয়ে দিতে পারে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
|
|
968
968
|
},
|
|
@@ -1070,7 +1070,7 @@ const ur = {
|
|
|
1070
1070
|
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔ اسے اپنی فی سوال قیمت (X402_PRICE) سے کم رکھیں: برابر یا زیادہ ہونے پر، جس کام میں بھی آپ کسی اور کو دیں گے اس میں کچھ نہیں بچے گا اور گیس بھی آپ کی جیب سے جائے گی — اور یہ بالکل اسی رویّے کو سزا دیتا ہے جو آپ چاہتے ہیں، کیونکہ آپ کا ایجنٹ جتنا بہتر پہچانے گا کہ اسے کیا نہیں آتا، اتنی ہی زیادہ بار وہ مفت کام کرے گا۔ اپنی قیمت کا ایک تہائی معقول آغاز ہے۔ صرف عدد کافی نہیں: agent.ts میں SUBCONTRATA_SKILLS کی فہرست بھی چاہیے، جو بتاتی ہے کہ وہ کیا خرید سکتا ہے۔ خالی — جیسا کہ یہ آتا ہے — تو وہ کبھی کام نہیں سونپتا، کیونکہ کوئی نہ ملنے پر مارکیٹ کی تلاش عام کر دیتی ہے، اور \"python video encoding\" مانگنے والا کوڈ ایجنٹ آخر میں \"video\" ڈھونڈ کر ویڈیو بنانے والے کو ادائیگی کر بیٹھے گا۔",
|
|
1071
1071
|
vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
|
|
1072
1072
|
retirada: "خودکار نکلوائی۔ ایسکرو ہر منظور شدہ آرڈر کو کنٹریکٹ کے اندر جمع کرتا ہے اور جب تک کوئی withdraw نہ بلائے کچھ نہیں بھیجتا۔ یہ ایجنٹ ہر 60 منٹ میں اسے نکالتا ہے: MON جب گیس نکالی گئی رقم کے 2 % سے زیادہ نہ ہو، اور $PANAL 1000 سے۔ گیس کے لیے تھوڑا MON چاہیے، اور کسی آرڈر کے دوران یہ کبھی نہیں چلتا۔\nRETIRADA=off اسے بند کرتا ہے۔",
|
|
1073
|
-
tablon: "
|
|
1073
|
+
tablon: "اصولوں والا بورڈ (PanalTablon): بغیر ایجنٹ کے شائع کیے گئے کام خود اٹھائیں یا ان پر پیشکش دیں۔ TABLON=on نہ ہو تو بند، کیونکہ کام اٹھانا اسے مہلت سے پہلے دینے کا وعدہ ہے، اور نہ دینے پر ایک اسٹرائیک لگتی ہے۔ TABLON_CATEGORIAS (لازمی، تین تک، کوما سے الگ) وہ کیٹیگریز ہیں جو یہ ایجنٹ ظاہر کرتا ہے، نام سے (datos, texto, codigo, vision, legal, defi, creativo, humanos, video, audio, traduccion, investigacion, marketing, automatizacion, soporte, otros) یا نمبر سے؛ ایجنٹ شروع ہوتے ہی انہیں چین پر درج کرتا ہے۔ یہ صرف آپ کی کرنسی میں، کم از کم آپ کی رجسٹری قیمت والے، ڈیلیوری کے لیے کم از کم TABLON_MARGEN_MINUTOS منٹ دینے والے کام اٹھاتا ہے، اور اگر TABLON_PALABRAS دیا ہو تو جن کے اشتہار میں ان میں سے کوئی لفظ ہو۔ انتخاب موڈ میں یہ آپ کی رجسٹری قیمت کی پیشکش دیتا ہے۔ TABLON_MINIMO_USD (اختیاری) ان کاموں کو چھوڑ دیتا ہے جو 2.5% کمیشن کے بعد اتنے ڈالر سے کم چھوڑیں۔ ہر دور میں ایک، کسی دوسرے کام کے دوران کبھی نہیں۔ یہ بند ہو تب بھی اس ایجنٹ کو سونپا گیا کام کیا جاتا ہے۔",
|
|
1074
1074
|
memoria: "گفتگو کی یادداشت (صرف x402)۔ اس کے بغیر ہر ادا شدہ کال الگ ہوتی ہے: کلائنٹ پوچھتا ہے، آپ جواب دیتے ہیں، اور اگلی بار آپ کو معلوم نہیں ہوتا کہ بات کس بارے میں تھی — یہ چیٹ نہیں، بل والا سرچ باکس ہے۔\nMEMORIA_TURNOS=0 اسے بند کر دیتا ہے۔ دونوں حدیں اس لیے ہیں کہ تاریخ پرامپٹ میں جاتی ہے، اور پرامپٹ کا خرچ آپ اٹھاتے ہیں جبکہ کلائنٹ فی پیغام مقررہ قیمت دیتا ہے۔",
|
|
1075
1075
|
seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 اگر آگے ریورس پراکسی ہو (Caddy، nginx) — یہی معمول ہے، کیونکہ آپ کا اینڈ پوائنٹ https ہونا چاہیے۔ اس کے بغیر ہر درخواست پراکسی کے IP کے ساتھ آتی ہے، چنانچہ اوپر والی حد فی کلائنٹ نہیں رہتی بلکہ ایک عالمی حد بن جاتی ہے جسے انڈیکسر، براؤزر اور آپ کے کلائنٹ مل کر استعمال کرتے ہیں؛ بھر جانے پر آپ کا ایجنٹ سب کو 429 دیتا ہے، اور جو کلائنٹ اپنی ہدایت نہیں بھیج پاتا وہ ادائیگی مقفل ہونے کے باوجود مہلت تک انتظار کرتا ہے۔ پراکسی نہ ہو تو بند رکھیں: پراکسی کے بغیر اس ہیڈر پر بھروسہ کرنے سے کوئی بھی ہر درخواست میں IP گھڑ سکتا ہے۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
|
|
1076
1076
|
},
|
package/dist/index.js
CHANGED
|
@@ -275,9 +275,11 @@ function writeEnvExample(dest, t) {
|
|
|
275
275
|
'',
|
|
276
276
|
bloque(t.env.tablon),
|
|
277
277
|
'TABLON=off',
|
|
278
|
+
'TABLON_CATEGORIAS=',
|
|
278
279
|
'TABLON_MINUTOS=5',
|
|
279
|
-
'TABLON_MARGEN_MINUTOS=
|
|
280
|
+
'TABLON_MARGEN_MINUTOS=60',
|
|
280
281
|
'TABLON_PALABRAS=',
|
|
282
|
+
'TABLON_MINIMO_USD=',
|
|
281
283
|
'',
|
|
282
284
|
bloque(t.env.memoria),
|
|
283
285
|
'MEMORIA_TURNOS=6',
|
package/package.json
CHANGED
package/template/_package.json
CHANGED
package/template/src/server.ts
CHANGED
|
@@ -70,13 +70,14 @@ import {
|
|
|
70
70
|
import { comoAdjunto } from './salida.js';
|
|
71
71
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
72
72
|
import { isAddress, keccak256, parseEther, parseUnits, toBytes, verifyMessage } from 'viem';
|
|
73
|
-
import type { Address } from 'viem';
|
|
73
|
+
import type { Address, Hex } from 'viem';
|
|
74
|
+
import type { EncargoConReglas } from '@panal/sdk';
|
|
74
75
|
import { handleTask, NIVELES, SUBCONTRATA_SKILLS } from './agent.js';
|
|
75
76
|
import { frasesGuardadas, pedirTraduccion } from './traduccion.js';
|
|
76
77
|
import type { AdjuntoRecibido, NivelPropio, TaskContext, TaskFile, TaskResult } from './agent.js';
|
|
77
78
|
import { arrancarVigilante } from './vigilante.js';
|
|
78
79
|
import { arrancarRetirada, opcionesDelEntorno } from './retirada.js';
|
|
79
|
-
import { arrancarTablon, opcionesDelEntorno as opcionesTablon } from './tablon.js';
|
|
80
|
+
import { arrancarTablon, EncargosPropios, opcionesDelEntorno as opcionesTablon, retomarDelTablon } from './tablon.js';
|
|
80
81
|
import { historialParaElModelo, recordarTurno, type Turno } from './memoria.js';
|
|
81
82
|
|
|
82
83
|
const PORT = Number(process.env.PORT ?? 8787);
|
|
@@ -220,7 +221,13 @@ if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
|
|
|
220
221
|
process.exit(1);
|
|
221
222
|
}
|
|
222
223
|
const account = privateKeyToAccount(key as `0x${string}`);
|
|
223
|
-
|
|
224
|
+
// `BUZON_URL` solo para probar contra un buzón propio (una copia local de la
|
|
225
|
+
// cadena, por ejemplo): el de Panal no conoce los encargos de esa copia.
|
|
226
|
+
const panal = createPanalClient({
|
|
227
|
+
account,
|
|
228
|
+
rpcUrl: process.env.RPC_URL,
|
|
229
|
+
...(process.env.BUZON_URL?.trim() ? { buzonUrl: process.env.BUZON_URL.trim() } : {}),
|
|
230
|
+
});
|
|
224
231
|
|
|
225
232
|
console.log(`Agente ${account.address} escuchando en :${PORT}`);
|
|
226
233
|
|
|
@@ -848,10 +855,22 @@ function contexto(
|
|
|
848
855
|
*/
|
|
849
856
|
export type ResultadoTrabajo = 'entregada' | 'esperando' | 'fallo' | 'en-curso';
|
|
850
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Un encargo del tablón con reglas no se entrega como los demás: su cliente en
|
|
860
|
+
* el escrow es PanalTablon, y quien lo publicó lee la entrega del BUZÓN DEL
|
|
861
|
+
* TABLÓN. `entregar` la deja ahí antes de anclar su hash (ver tablon.ts), y
|
|
862
|
+
* `cliente` es quien publicó, que es lo que le interesa saber a `handleTask`.
|
|
863
|
+
*/
|
|
864
|
+
interface ComoEntregar {
|
|
865
|
+
cliente: Address;
|
|
866
|
+
entregar: (texto: string) => Promise<{ txHash: Hex }>;
|
|
867
|
+
}
|
|
868
|
+
|
|
851
869
|
async function work(
|
|
852
870
|
taskId: bigint,
|
|
853
871
|
brief: string,
|
|
854
872
|
sobre: CallEnvelope | null,
|
|
873
|
+
delTablon: ComoEntregar | null = null,
|
|
855
874
|
): Promise<ResultadoTrabajo> {
|
|
856
875
|
const key = taskId.toString();
|
|
857
876
|
if (inFlight.has(key)) return 'en-curso';
|
|
@@ -886,7 +905,7 @@ async function work(
|
|
|
886
905
|
contexto(
|
|
887
906
|
{
|
|
888
907
|
taskId,
|
|
889
|
-
client: task.client,
|
|
908
|
+
client: delTablon?.cliente ?? task.client,
|
|
890
909
|
amount: task.amount,
|
|
891
910
|
deadline: task.deadline,
|
|
892
911
|
adjuntos: recibidos,
|
|
@@ -902,14 +921,20 @@ async function work(
|
|
|
902
921
|
// archivos. Los archivos se escriben en disco y su hash se cuela en el
|
|
903
922
|
// texto: lo que se ancla en la cadena pasa a cubrirlos también.
|
|
904
923
|
const { text: cuerpo, files } = normalizarSalida(salida);
|
|
905
|
-
|
|
924
|
+
// Por el buzón del tablón solo viaja texto: los archivos se sirven desde
|
|
925
|
+
// este servidor a quien firme como cliente de la tarea, y ahí el cliente
|
|
926
|
+
// es PanalTablon. Se dice en vez de entregar un manifiesto que nadie puede abrir.
|
|
927
|
+
if (delTablon && files.length) {
|
|
928
|
+
console.warn(`[tablon] #${taskId}: ${files.length} archivo(s) no viajan por el tablón; se entrega solo el texto.`);
|
|
929
|
+
}
|
|
930
|
+
const text = files.length && !delTablon ? appendFilesManifest(cuerpo, saveFiles(taskId, files)) : cuerpo;
|
|
906
931
|
|
|
907
932
|
// Primero se guarda y luego se entrega: si el orden fuera al revés y el
|
|
908
933
|
// proceso muriera entre medias, el hash estaría anclado on-chain y el texto
|
|
909
934
|
// perdido, o sea una entrega imposible de cumplir.
|
|
910
935
|
saveResult(taskId, text);
|
|
911
|
-
const { txHash } = await panal.deliverResult(taskId, text);
|
|
912
|
-
console.log(`[panal] #${taskId} entregada · tx ${txHash}`);
|
|
936
|
+
const { txHash } = delTablon ? await delTablon.entregar(text) : await panal.deliverResult(taskId, text);
|
|
937
|
+
console.log(`[panal] #${taskId} entregada${delTablon ? ' por el tablón' : ''} · tx ${txHash}`);
|
|
913
938
|
return 'entregada';
|
|
914
939
|
} catch (err) {
|
|
915
940
|
console.error(`[panal] #${taskId} falló: ${err instanceof Error ? err.message : err}`);
|
|
@@ -1978,6 +2003,22 @@ const server = createServer((req, res) => {
|
|
|
1978
2003
|
|
|
1979
2004
|
server.listen(PORT);
|
|
1980
2005
|
|
|
2006
|
+
// Mientras el tablón coge un encargo la wallet está firmando, aunque `work()`
|
|
2007
|
+
// todavía no haya empezado. La retirada tiene que verlo: dos transacciones
|
|
2008
|
+
// seguidas de la misma wallet chocan por el nonce.
|
|
2009
|
+
let tablonFirmando = false;
|
|
2010
|
+
|
|
2011
|
+
// Los encargos del tablón con reglas de este agente, hasta que se cierran sus
|
|
2012
|
+
// cuentas. En disco: cerrar puede tocar tres días después de entregar.
|
|
2013
|
+
const encargosDelTablon = new EncargosPropios(DATA_DIR);
|
|
2014
|
+
|
|
2015
|
+
/** Trabajar un encargo del tablón: el `work()` de siempre, entregando por su buzón. */
|
|
2016
|
+
const trabajarDelTablon = async (job: EncargoConReglas, brief: string): Promise<boolean> =>
|
|
2017
|
+
(await work(job.escrowTaskId, brief, null, {
|
|
2018
|
+
cliente: job.cliente,
|
|
2019
|
+
entregar: (texto) => panal.deliverBoardJob(job.id, texto),
|
|
2020
|
+
})) === 'entregada';
|
|
2021
|
+
|
|
1981
2022
|
// El vigilante. Va DESPUÉS de escuchar: su primer repaso puede tardar unos
|
|
1982
2023
|
// segundos contra el RPC, y durante ese rato el agente ya tiene que estar
|
|
1983
2024
|
// atendiendo peticiones normales.
|
|
@@ -2003,16 +2044,36 @@ arrancarVigilante({
|
|
|
2003
2044
|
console.log(`[vigilante] #${taskId} entregada al segundo intento · tx ${txHash}`);
|
|
2004
2045
|
},
|
|
2005
2046
|
urlPublica: process.env.PUBLIC_URL?.trim(),
|
|
2047
|
+
// Las del tablón con reglas: su cliente en el escrow es PanalTablon, y el
|
|
2048
|
+
// encargo y la entrega van por el buzón del tablón. Funciona aunque el tablón
|
|
2049
|
+
// automático esté apagado: si el dueño ofertó a mano desde la web con la
|
|
2050
|
+
// wallet de este agente, el agente trabaja lo que le asignen.
|
|
2051
|
+
tablon: panal.boardJobsAvailable
|
|
2052
|
+
? {
|
|
2053
|
+
es: (cliente) => cliente.toLowerCase() === panal.addresses.tablon.toLowerCase(),
|
|
2054
|
+
retomar: async (taskId) => {
|
|
2055
|
+
// Mientras la ronda del tablón coge y lee, el encargo es suyo.
|
|
2056
|
+
if (tablonFirmando) return false;
|
|
2057
|
+
return retomarDelTablon(
|
|
2058
|
+
{
|
|
2059
|
+
panal,
|
|
2060
|
+
yo: account.address,
|
|
2061
|
+
propios: encargosDelTablon,
|
|
2062
|
+
briefGuardado: loadBrief,
|
|
2063
|
+
resultadoGuardado: loadResult,
|
|
2064
|
+
trabajar: trabajarDelTablon,
|
|
2065
|
+
},
|
|
2066
|
+
taskId,
|
|
2067
|
+
);
|
|
2068
|
+
},
|
|
2069
|
+
}
|
|
2070
|
+
: undefined,
|
|
2006
2071
|
});
|
|
2007
2072
|
|
|
2008
2073
|
// La retirada automática: lo que el escrow acredita por cada encargo aprobado
|
|
2009
2074
|
// se queda en el contrato hasta que alguien llama a `withdraw`, y un agente que
|
|
2010
2075
|
// corre solo no tiene a nadie que le dé al botón. `RETIRADA=off` la apaga; el
|
|
2011
2076
|
// porqué de cada umbral está en retirada.ts.
|
|
2012
|
-
// Mientras el tablón coge un encargo la wallet está firmando, aunque `work()`
|
|
2013
|
-
// todavía no haya empezado. La retirada tiene que verlo: dos transacciones
|
|
2014
|
-
// seguidas de la misma wallet chocan por el nonce.
|
|
2015
|
-
let tablonFirmando = false;
|
|
2016
2077
|
|
|
2017
2078
|
const retirada = opcionesDelEntorno(process.env);
|
|
2018
2079
|
if (retirada) {
|
|
@@ -2027,9 +2088,9 @@ if (retirada) {
|
|
|
2027
2088
|
});
|
|
2028
2089
|
}
|
|
2029
2090
|
|
|
2030
|
-
// El tablón: coger solo los encargos publicados sin
|
|
2031
|
-
// agente. APAGADO salvo `TABLON=on`: coger es
|
|
2032
|
-
// porqué de cada regla está en tablon.ts.
|
|
2091
|
+
// El tablón con reglas: coger u ofertar solo en los encargos publicados sin
|
|
2092
|
+
// dueño que encajen con este agente. APAGADO salvo `TABLON=on`: coger es
|
|
2093
|
+
// comprometerse a entregar. El porqué de cada regla está en tablon.ts.
|
|
2033
2094
|
const tablon = opcionesTablon(process.env);
|
|
2034
2095
|
if (tablon) {
|
|
2035
2096
|
void arrancarTablon({
|
|
@@ -2040,8 +2101,7 @@ if (tablon) {
|
|
|
2040
2101
|
marcar: (ocupada) => {
|
|
2041
2102
|
tablonFirmando = ocupada;
|
|
2042
2103
|
},
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
trabajar: (taskId, brief) => work(taskId, brief, null),
|
|
2104
|
+
trabajar: trabajarDelTablon,
|
|
2105
|
+
propios: encargosDelTablon,
|
|
2046
2106
|
});
|
|
2047
2107
|
}
|
package/template/src/tablon.ts
CHANGED
|
@@ -1,63 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ────────────────────────────────────────────────────────────────────────────
|
|
3
|
-
* El tablón: coger solo, sin que nadie haga clic
|
|
3
|
+
* El tablón con reglas: coger u ofertar solo, sin que nadie haga clic.
|
|
4
4
|
* ────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
* QUÉ ES EL TABLÓN. Un cliente
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* QUÉ ES EL TABLÓN. Un cliente publica un encargo en PanalTablon SIN elegir
|
|
7
|
+
* agente: el dinero espera en el contrato y se lo queda un agente que pasa las
|
|
8
|
+
* reglas —su nivel según la reputación, su cupo del día, sus categorías y sus
|
|
9
|
+
* faltas—. Dos modos: en INMEDIATO lo coge el primero que pasa las reglas; en
|
|
10
|
+
* SELECCIÓN los agentes ofertan precio y plazo, y elige el cliente (o, si no
|
|
11
|
+
* elige a tiempo, el contrato: la oferta del nivel más alto y, a igualdad, la
|
|
12
|
+
* más barata). Especificación: contracts/BOARD.md en el repositorio de Panal.
|
|
11
13
|
*
|
|
12
14
|
* APAGADO POR DEFECTO, Y A PROPÓSITO. Coger un encargo es comprometerse a
|
|
13
|
-
* entregarlo antes del plazo, con el dinero de un desconocido bloqueado
|
|
14
|
-
*
|
|
15
|
+
* entregarlo antes del plazo, con el dinero de un desconocido bloqueado; no
|
|
16
|
+
* entregar cuesta una falta y una semana sin poder coger nada. Eso no se activa
|
|
17
|
+
* solo por actualizar la plantilla: se enciende con `TABLON=on`.
|
|
15
18
|
*
|
|
16
|
-
* QUÉ COGE. Solo lo que cumple
|
|
19
|
+
* QUÉ COGE U OFERTA. Solo lo que cumple todo esto:
|
|
20
|
+
* - es de una de sus categorías (`TABLON_CATEGORIAS`, hasta tres). El
|
|
21
|
+
* contrato no deja coger nada fuera de ellas, y el agente las declara solo
|
|
22
|
+
* al arrancar;
|
|
17
23
|
* - no lo publicó este mismo agente;
|
|
18
|
-
* - paga en la moneda de este agente y AL MENOS
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* - su anuncio menciona alguna
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* - paga en la moneda de este agente y AL MENOS su precio del registro: el
|
|
25
|
+
* agente ya decidió su precio, y el tablón no lo rebaja;
|
|
26
|
+
* - da como mínimo `TABLON_MARGEN_MINUTOS` para entregar;
|
|
27
|
+
* - si hay `TABLON_PALABRAS`, su anuncio menciona alguna;
|
|
28
|
+
* - y `canTake` dice que sí: la misma comprobación que hace el contrato, así
|
|
29
|
+
* que nunca se gasta gas en algo que rechazaría.
|
|
30
|
+
* En inmediato lo coge; en selección oferta su precio del registro, con el
|
|
31
|
+
* plazo del encargo. Nunca oferta por encima del presupuesto. Uno por ronda:
|
|
32
|
+
* primero lo que se puede coger, del que más paga al que menos.
|
|
27
33
|
*
|
|
28
|
-
*
|
|
29
|
-
* encargo
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* el encargo queda en disco y la tarea asignada a este agente en la cadena, si
|
|
33
|
-
* el proceso muere a mitad el vigilante lo retoma.
|
|
34
|
+
* UN MÍNIMO EN DÓLARES, SI SE QUIERE. Con `TABLON_MINIMO_USD`, el agente no
|
|
35
|
+
* coge un encargo que le pague menos de eso: lo que cobraría de verdad, el
|
|
36
|
+
* importe menos el 2,5 % del escrow y, en $PANAL, lo que daría venderlos en la
|
|
37
|
+
* curva. Sin un precio de menos de cinco minutos no se coge nada.
|
|
34
38
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* CÓMO LO TRABAJA. Con el mismo `work()` que un encargo normal, pero leyendo y
|
|
40
|
+
* entregando por el BUZÓN DEL TABLÓN: el encargo se lee de ahí firmando, y la
|
|
41
|
+
* entrega se deja ahí antes de anclar su hash en el escrow, porque es de ahí
|
|
42
|
+
* de donde la lee el cliente. En el escrow, el cliente de estas tareas es
|
|
43
|
+
* PanalTablon, no quien publicó.
|
|
44
|
+
*
|
|
45
|
+
* LO QUE YA ES SUYO. Un encargo de selección se asigna más tarde —cuando el
|
|
46
|
+
* cliente elige, o el contrato elige por él—, y uno inmediato puede quedarse a
|
|
47
|
+
* medias si el proceso muere. Los dos aparecen como tareas nuevas del escrow, y
|
|
48
|
+
* el vigilante las retoma por el camino del tablón (`retomarDelTablon`). Eso
|
|
49
|
+
* funciona aunque el tablón automático esté apagado: si el dueño ofertó a mano
|
|
50
|
+
* desde la web con la wallet del agente, el agente trabaja lo que le asignen.
|
|
51
|
+
*
|
|
52
|
+
* CERRAR CUENTAS. Un encargo del tablón ocupa un hueco del cupo del agente
|
|
53
|
+
* hasta que se cierran sus cuentas en el contrato (`settle`). Si el cliente
|
|
54
|
+
* aprueba, se cierran solas; si no, pasadas 72 h desde la entrega cualquiera
|
|
55
|
+
* puede cerrarlas, y quien más interés tiene es el agente: cobra y libera el
|
|
56
|
+
* hueco. Por eso cada ronda intenta cerrar lo suyo. Intentarlo antes de tiempo
|
|
57
|
+
* no cuesta nada: la estimación del gas falla y no se firma.
|
|
58
|
+
*
|
|
59
|
+
* LA WALLET, DE UNA EN UNA. Coger, ofertar y cerrar son transacciones. Con un
|
|
60
|
+
* encargo en marcha la ronda se salta, y entre dos transacciones se espera: en
|
|
61
|
+
* Monad una lanzada justo detrás de otra de la misma wallet revierte.
|
|
40
62
|
*/
|
|
41
63
|
|
|
64
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
65
|
+
import { join } from 'node:path';
|
|
42
66
|
import type { Address } from 'viem';
|
|
43
|
-
import { formatEther } from 'viem';
|
|
44
|
-
import
|
|
67
|
+
import { formatEther, keccak256, toBytes } from 'viem';
|
|
68
|
+
import {
|
|
69
|
+
CATEGORIAS_TABLON,
|
|
70
|
+
EstadoTablon,
|
|
71
|
+
FEE_BPS,
|
|
72
|
+
MAX_CATEGORIAS,
|
|
73
|
+
ModoTablon,
|
|
74
|
+
NATIVE_CURRENCY,
|
|
75
|
+
TaskStatus,
|
|
76
|
+
categoriaDelTablon,
|
|
77
|
+
esFresco,
|
|
78
|
+
formatearDolares,
|
|
79
|
+
} from '@panal/sdk';
|
|
80
|
+
import type { EncargoConReglas, PanalClient } from '@panal/sdk';
|
|
45
81
|
|
|
46
82
|
export interface OpcionesTablon {
|
|
47
83
|
/** Cada cuánto se mira el tablón, en minutos. */
|
|
48
84
|
minutos: number;
|
|
49
|
-
/**
|
|
85
|
+
/** Lo mínimo que tiene que dar un encargo para entregar, en minutos. */
|
|
50
86
|
margenMinutos: number;
|
|
51
|
-
/**
|
|
87
|
+
/** Si no está vacío, el anuncio tiene que mencionar alguna. */
|
|
52
88
|
palabras: string[];
|
|
89
|
+
/** Lo mínimo que tiene que dejar un encargo, en dólares netos. `null`: sin mínimo. */
|
|
90
|
+
minimoUsd: number | null;
|
|
91
|
+
/** Las categorías que declara este agente, por su número en la cadena. */
|
|
92
|
+
categorias: number[];
|
|
53
93
|
}
|
|
54
94
|
|
|
55
95
|
export const TABLON_POR_DEFECTO: OpcionesTablon = {
|
|
56
96
|
minutos: 5,
|
|
57
|
-
margenMinutos:
|
|
97
|
+
margenMinutos: 60,
|
|
58
98
|
palabras: [],
|
|
99
|
+
minimoUsd: null,
|
|
100
|
+
categorias: [],
|
|
59
101
|
};
|
|
60
102
|
|
|
103
|
+
/**
|
|
104
|
+
* `codigo,texto` o `2,1` → `[2, 1]`. Por nombre (los de `CATEGORIAS_TABLON`,
|
|
105
|
+
* sin tildes) o por número. Lo que no se entiende se avisa y se salta; más de
|
|
106
|
+
* tres, se quedan las tres primeras: es lo que admite el contrato.
|
|
107
|
+
*/
|
|
108
|
+
export function categoriasDelEntorno(texto: string, avisar: (m: string) => void = (m) => console.warn(m)): number[] {
|
|
109
|
+
const out: number[] = [];
|
|
110
|
+
for (const crudo of texto.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)) {
|
|
111
|
+
const n = /^\d+$/.test(crudo) ? Number(crudo) : (CATEGORIAS_TABLON as readonly string[]).indexOf(plano(crudo).replace(/\s+/g, ''));
|
|
112
|
+
if (!Number.isInteger(n) || n < 0 || categoriaDelTablon(n) === null) {
|
|
113
|
+
avisar(`[tablon] TABLON_CATEGORIAS: «${crudo}» no es una categoría. Las que hay: ${CATEGORIAS_TABLON.join(', ')}.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!out.includes(n)) out.push(n);
|
|
117
|
+
}
|
|
118
|
+
if (out.length > MAX_CATEGORIAS) {
|
|
119
|
+
avisar(`[tablon] TABLON_CATEGORIAS: el contrato admite ${MAX_CATEGORIAS}; me quedo con las tres primeras.`);
|
|
120
|
+
out.length = MAX_CATEGORIAS;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
61
125
|
/**
|
|
62
126
|
* Las opciones del .env, o `null` si está apagado — que es lo normal.
|
|
63
127
|
*
|
|
@@ -69,7 +133,7 @@ export function opcionesDelEntorno(
|
|
|
69
133
|
avisar: (m: string) => void = (m) => console.warn(m),
|
|
70
134
|
): OpcionesTablon | null {
|
|
71
135
|
if (env.TABLON?.trim().toLowerCase() !== 'on') return null;
|
|
72
|
-
const o: OpcionesTablon = { ...TABLON_POR_DEFECTO, palabras: [] };
|
|
136
|
+
const o: OpcionesTablon = { ...TABLON_POR_DEFECTO, palabras: [], categorias: [] };
|
|
73
137
|
|
|
74
138
|
if (env.TABLON_MINUTOS?.trim()) {
|
|
75
139
|
const n = Number(env.TABLON_MINUTOS);
|
|
@@ -84,9 +148,33 @@ export function opcionesDelEntorno(
|
|
|
84
148
|
if (env.TABLON_PALABRAS?.trim()) {
|
|
85
149
|
o.palabras = env.TABLON_PALABRAS.split(',').map((p) => p.trim()).filter(Boolean);
|
|
86
150
|
}
|
|
151
|
+
if (env.TABLON_MINIMO_USD?.trim()) {
|
|
152
|
+
const n = Number(env.TABLON_MINIMO_USD.replace(',', '.'));
|
|
153
|
+
if (Number.isFinite(n) && n > 0) o.minimoUsd = n;
|
|
154
|
+
else avisar(`[tablon] TABLON_MINIMO_USD="${env.TABLON_MINIMO_USD}" no vale (un número mayor que 0): sin mínimo.`);
|
|
155
|
+
}
|
|
156
|
+
if (env.TABLON_CATEGORIAS?.trim()) o.categorias = categoriasDelEntorno(env.TABLON_CATEGORIAS, avisar);
|
|
87
157
|
return o;
|
|
88
158
|
}
|
|
89
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Lo que este agente cobraría de verdad por un importe, en dólares: menos el
|
|
162
|
+
* 2,5 % del escrow y, en $PANAL, lo que daría venderlos en la curva. `null` si
|
|
163
|
+
* no hay un precio de menos de cinco minutos.
|
|
164
|
+
*/
|
|
165
|
+
export async function dolaresNetos(
|
|
166
|
+
panal: Pick<PanalClient, 'precios' | 'valorDeVentaPanal'>,
|
|
167
|
+
e: { amount: bigint; currency: Address },
|
|
168
|
+
): Promise<number | null> {
|
|
169
|
+
const neto = (e.amount * (10_000n - FEE_BPS)) / 10_000n;
|
|
170
|
+
const precios = await panal.precios().catch(() => null);
|
|
171
|
+
if (!esFresco(precios?.mon)) return null;
|
|
172
|
+
// El tablón solo acepta MON y $PANAL: lo que no es MON es $PANAL.
|
|
173
|
+
const enMon = e.currency.toLowerCase() === NATIVE_CURRENCY ? neto : await panal.valorDeVentaPanal(neto).catch(() => null);
|
|
174
|
+
if (enMon === null) return null;
|
|
175
|
+
return Number(formatEther(enMon)) * precios.mon.valor;
|
|
176
|
+
}
|
|
177
|
+
|
|
90
178
|
/** Sin tildes, en minúsculas, y con guiones y barras como espacios. */
|
|
91
179
|
function plano(s: string): string {
|
|
92
180
|
return s
|
|
@@ -111,43 +199,147 @@ export function encaja(anuncio: string, palabras: string[]): boolean {
|
|
|
111
199
|
});
|
|
112
200
|
}
|
|
113
201
|
|
|
202
|
+
/* ── los encargos del tablón que son de este agente ─────────────────────── */
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Los números de encargo que este agente tiene asignados y aún no se han
|
|
206
|
+
* cerrado. En disco, porque cerrar cuentas puede tocar tres días después y el
|
|
207
|
+
* proceso puede haberse reiniciado entre medias.
|
|
208
|
+
*/
|
|
209
|
+
export class EncargosPropios {
|
|
210
|
+
private readonly ruta: string;
|
|
211
|
+
private ids: Set<string>;
|
|
212
|
+
|
|
213
|
+
constructor(dataDir: string) {
|
|
214
|
+
this.ruta = join(dataDir, 'tablon.json');
|
|
215
|
+
try {
|
|
216
|
+
const leido = JSON.parse(readFileSync(this.ruta, 'utf8')) as { encargos?: unknown };
|
|
217
|
+
this.ids = new Set(Array.isArray(leido.encargos) ? leido.encargos.filter((x): x is string => typeof x === 'string') : []);
|
|
218
|
+
} catch {
|
|
219
|
+
this.ids = new Set();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
anotar(jobId: bigint): void {
|
|
224
|
+
if (this.ids.has(jobId.toString())) return;
|
|
225
|
+
this.ids.add(jobId.toString());
|
|
226
|
+
this.guardar();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
quitar(jobId: bigint): void {
|
|
230
|
+
if (this.ids.delete(jobId.toString())) this.guardar();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
lista(): bigint[] {
|
|
234
|
+
return [...this.ids].map((x) => BigInt(x));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private guardar(): void {
|
|
238
|
+
try {
|
|
239
|
+
writeFileSync(this.ruta, JSON.stringify({ encargos: [...this.ids] }));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn(`[tablon] no se pudo guardar ${this.ruta}: ${err instanceof Error ? err.message : err}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* El encargo del tablón al que corresponde una tarea del escrow, o `null`.
|
|
248
|
+
*
|
|
249
|
+
* El escrow no sabe de encargos: solo que su cliente es PanalTablon. Se busca
|
|
250
|
+
* de los más nuevos hacia atrás, porque la tarea se acaba de crear al
|
|
251
|
+
* asignarse. `maximo` acota cuánto se mira.
|
|
252
|
+
*/
|
|
253
|
+
export async function buscarEncargoDeTarea(
|
|
254
|
+
panal: Pick<PanalClient, 'getBoardJobCount' | 'getBoardJob'>,
|
|
255
|
+
taskId: bigint,
|
|
256
|
+
yo: Address,
|
|
257
|
+
maximo = 300,
|
|
258
|
+
): Promise<EncargoConReglas | null> {
|
|
259
|
+
const total = await panal.getBoardJobCount();
|
|
260
|
+
const yoLc = yo.toLowerCase();
|
|
261
|
+
for (let hasta = total; hasta > 0n && total - hasta < BigInt(maximo); hasta -= 10n) {
|
|
262
|
+
const desde = hasta > 10n ? hasta - 10n : 0n;
|
|
263
|
+
const ids: bigint[] = [];
|
|
264
|
+
for (let i = hasta - 1n; i >= desde; i--) ids.push(i);
|
|
265
|
+
const lote = await Promise.all(ids.map((i) => panal.getBoardJob(i)));
|
|
266
|
+
const j = lote.find((x) => x.escrowTaskId === taskId && x.agente.toLowerCase() === yoLc && x.estado !== EstadoTablon.Publicado);
|
|
267
|
+
if (j) return j;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ── una ronda ──────────────────────────────────────────────────────────── */
|
|
273
|
+
|
|
114
274
|
export interface DepsTablon {
|
|
115
275
|
panal: PanalClient;
|
|
116
276
|
yo: Address;
|
|
117
277
|
opciones: OpcionesTablon;
|
|
118
278
|
/** Lo que este agente cobra por un encargo en el registro, y en qué moneda. */
|
|
119
279
|
precio: { amount: bigint; currency: Address };
|
|
120
|
-
/** Sus habilidades, de la ficha en la cadena. */
|
|
121
|
-
habilidades: string[];
|
|
122
280
|
/** ¿Hay un encargo en marcha? Entonces no se toca la wallet. */
|
|
123
281
|
ocupado: () => boolean;
|
|
124
|
-
/** Marca la wallet como ocupada mientras se coge
|
|
282
|
+
/** Marca la wallet como ocupada mientras se coge, se oferta o se cierra. */
|
|
125
283
|
marcar: (ocupada: boolean) => void;
|
|
126
|
-
/**
|
|
127
|
-
trabajar: (
|
|
128
|
-
/**
|
|
284
|
+
/** Trabaja un encargo del tablón: guarda, hace y entrega por su buzón. */
|
|
285
|
+
trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
|
|
286
|
+
/** Los encargos del tablón de este agente que aún hay que cerrar. */
|
|
287
|
+
propios: Pick<EncargosPropios, 'anotar' | 'quitar' | 'lista'>;
|
|
288
|
+
/** Pausa entre dos transacciones. Inyectable para las pruebas. */
|
|
129
289
|
esperar?: (ms: number) => Promise<void>;
|
|
130
290
|
log?: (m: string) => void;
|
|
291
|
+
/** El reloj, en segundos. Inyectable para las pruebas. */
|
|
292
|
+
ahora?: () => number;
|
|
131
293
|
}
|
|
132
294
|
|
|
133
|
-
|
|
295
|
+
export type Accion = { job: EncargoConReglas & { anuncio: string }; accion: 'coger' | 'ofertar'; cobra: bigint };
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Lo que este agente podría coger u ofertar de lo publicado: primero lo que se
|
|
299
|
+
* coge, del que más paga al que menos; después lo que se oferta. Sin mirar la
|
|
300
|
+
* cadena: el `canTake` se pregunta luego, solo al que se va a intentar.
|
|
301
|
+
*/
|
|
134
302
|
export function candidatos(
|
|
135
|
-
lista:
|
|
136
|
-
deps: Pick<DepsTablon, 'yo' | 'precio' | '
|
|
303
|
+
lista: (EncargoConReglas & { anuncio: string })[],
|
|
304
|
+
deps: Pick<DepsTablon, 'yo' | 'precio' | 'opciones'>,
|
|
137
305
|
ahoraS: number,
|
|
138
|
-
):
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
306
|
+
): Accion[] {
|
|
307
|
+
const { opciones, precio } = deps;
|
|
308
|
+
const out: Accion[] = [];
|
|
309
|
+
for (const job of lista) {
|
|
310
|
+
if (job.estado !== EstadoTablon.Publicado) continue;
|
|
311
|
+
if (job.cliente.toLowerCase() === deps.yo.toLowerCase()) continue;
|
|
312
|
+
if (!opciones.categorias.includes(job.categoria)) continue;
|
|
313
|
+
if (job.moneda.toLowerCase() !== precio.currency.toLowerCase()) continue;
|
|
314
|
+
if (job.importe < precio.amount) continue;
|
|
315
|
+
if (job.plazoEntregaS < opciones.margenMinutos * 60) continue;
|
|
316
|
+
if (opciones.palabras.length > 0 && !encaja(job.anuncio, opciones.palabras)) continue;
|
|
317
|
+
if (job.modo === ModoTablon.Inmediato) {
|
|
318
|
+
// Un minuto de margen: la transacción tiene que minarse antes de que caduque.
|
|
319
|
+
if (ahoraS < job.caducaEn - 60) out.push({ job, accion: 'coger', cobra: job.importe });
|
|
320
|
+
} else if (ahoraS < job.publicadoEn + job.ventanaOfertasS - 60) {
|
|
321
|
+
out.push({ job, accion: 'ofertar', cobra: precio.amount });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const peso = (a: Accion) => (a.accion === 'coger' ? 1 : 0);
|
|
325
|
+
return out.sort((a, b) => peso(b) - peso(a) || (b.job.importe > a.job.importe ? 1 : b.job.importe < a.job.importe ? -1 : 0));
|
|
147
326
|
}
|
|
148
327
|
|
|
328
|
+
/** Última vez que se intentó cerrar cada encargo: una vez por hora basta. */
|
|
329
|
+
const intentosDeCerrar = new Map<string, number>();
|
|
330
|
+
/**
|
|
331
|
+
* Encargos en los que coger u ofertar ya falló: no se reintentan en cada
|
|
332
|
+
* ronda. Lo normal es que otro llegara antes; si es el contrato el que dice que
|
|
333
|
+
* no —un precio por debajo del mínimo, por ejemplo—, repetirlo solo llena el log.
|
|
334
|
+
*/
|
|
335
|
+
const descartados = new Set<string>();
|
|
336
|
+
const CERRAR_CADA_S = 3600;
|
|
337
|
+
/** Cuántos candidatos se miran como mucho por ronda: cada uno es una llamada. */
|
|
338
|
+
const CANDIDATOS_POR_RONDA = 5;
|
|
339
|
+
|
|
149
340
|
/**
|
|
150
|
-
* Una ronda:
|
|
341
|
+
* Una ronda: cierra cuentas de lo suyo si toca y, si no, mira el tablón y
|
|
342
|
+
* coge u oferta lo que encaje. Una transacción por ronda, como mucho.
|
|
151
343
|
*
|
|
152
344
|
* Devuelve lo que hizo, línea a línea, para las pruebas. Nunca lanza: un fallo
|
|
153
345
|
* aquí no puede tumbar el agente que atiende sus encargos normales.
|
|
@@ -155,93 +347,212 @@ export function candidatos(
|
|
|
155
347
|
export async function repasarTablon(deps: DepsTablon): Promise<string[]> {
|
|
156
348
|
const log = deps.log ?? ((m: string) => console.log(m));
|
|
157
349
|
const esperar = deps.esperar ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
350
|
+
const ahora = deps.ahora ?? (() => Math.floor(Date.now() / 1000));
|
|
158
351
|
const hecho: string[] = [];
|
|
159
352
|
const anotar = (m: string): void => {
|
|
160
353
|
hecho.push(m);
|
|
161
354
|
log(m);
|
|
162
355
|
};
|
|
356
|
+
const corto = (err: unknown) => (err instanceof Error ? err.message.split('\n')[0] : String(err));
|
|
163
357
|
|
|
164
358
|
if (deps.ocupado()) return hecho;
|
|
165
359
|
|
|
166
|
-
let lista: EncargoDelTablon[];
|
|
167
360
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
361
|
+
// 1. Cerrar cuentas de lo suyo: cobra y libera el hueco del cupo. Primero
|
|
362
|
+
// se limpian los que ya se cerraron por otro lado —leer no cuesta
|
|
363
|
+
// nada— y después se cierra uno, que eso sí es una transacción.
|
|
364
|
+
const porCerrar: bigint[] = [];
|
|
365
|
+
for (const jobId of deps.propios.lista()) {
|
|
366
|
+
const job = await deps.panal.getBoardJob(jobId);
|
|
367
|
+
if (job.estado !== EstadoTablon.Asignado) {
|
|
368
|
+
deps.propios.quitar(jobId);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// Abierta en el escrow: se está trabajando o espera al vigilante.
|
|
372
|
+
const tarea = await deps.panal.getTask(job.escrowTaskId);
|
|
373
|
+
if (tarea.status !== TaskStatus.Open) porCerrar.push(jobId);
|
|
374
|
+
}
|
|
375
|
+
for (const jobId of porCerrar) {
|
|
376
|
+
const ultimo = intentosDeCerrar.get(jobId.toString()) ?? 0;
|
|
377
|
+
if (ahora() - ultimo < CERRAR_CADA_S) continue;
|
|
378
|
+
intentosDeCerrar.set(jobId.toString(), ahora());
|
|
379
|
+
deps.marcar(true);
|
|
380
|
+
try {
|
|
381
|
+
await deps.panal.settleBoardJob(jobId);
|
|
382
|
+
deps.propios.quitar(jobId);
|
|
383
|
+
anotar(`[tablon] #${jobId} cuentas cerradas: cobrado y hueco libre`);
|
|
384
|
+
return hecho;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// Lo normal: entregado hace menos de 72 h y el cliente no ha aprobado.
|
|
387
|
+
// La estimación del gas lo dice antes de firmar, así que no costó nada.
|
|
388
|
+
if (!/NotFinished/.test(String(err))) anotar(`[tablon] #${jobId} no se pudo cerrar: ${corto(err)}`);
|
|
389
|
+
} finally {
|
|
390
|
+
deps.marcar(false);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
175
393
|
|
|
176
|
-
|
|
177
|
-
|
|
394
|
+
// 2. Lo publicado.
|
|
395
|
+
let lista: (EncargoConReglas & { anuncio: string })[];
|
|
178
396
|
try {
|
|
179
|
-
await deps.panal.
|
|
397
|
+
lista = await deps.panal.listBoardJobAdverts({ limit: 50 });
|
|
180
398
|
} catch (err) {
|
|
181
|
-
|
|
182
|
-
// primero. No es un fallo del agente.
|
|
183
|
-
anotar(`[tablon] #${elegido.taskId} no se pudo coger: ${err instanceof Error ? err.message.split('\n')[0] : err}`);
|
|
399
|
+
anotar(`[tablon] no se pudo leer el tablón: ${corto(err)}`);
|
|
184
400
|
return hecho;
|
|
185
401
|
}
|
|
186
|
-
anotar(
|
|
187
|
-
`[tablon] #${elegido.taskId} cogido · ${formatEther(elegido.amount)} · «${elegido.anuncio.slice(0, 80)}»`,
|
|
188
|
-
);
|
|
189
402
|
|
|
190
|
-
|
|
191
|
-
|
|
403
|
+
const posibles = candidatos(lista, deps, ahora()).filter((c) => !descartados.has(c.job.id.toString()));
|
|
404
|
+
for (const c of posibles.slice(0, CANDIDATOS_POR_RONDA)) {
|
|
405
|
+
const id = c.job.id;
|
|
406
|
+
if (c.accion === 'ofertar') {
|
|
407
|
+
const ofertas = await deps.panal.getBoardOffers(id);
|
|
408
|
+
if (ofertas.some((o) => o.agente.toLowerCase() === deps.yo.toLowerCase())) continue;
|
|
409
|
+
}
|
|
410
|
+
const permiso = await deps.panal.canTakeBoardJob(deps.yo, id);
|
|
411
|
+
if (!permiso.ok) continue;
|
|
192
412
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
413
|
+
// El que más paga va primero: si no llega al mínimo, se prueba el siguiente.
|
|
414
|
+
if (deps.opciones.minimoUsd !== null) {
|
|
415
|
+
const usd = await dolaresNetos(deps.panal, { amount: c.cobra, currency: c.job.moneda });
|
|
416
|
+
if (usd === null) {
|
|
417
|
+
anotar(`[tablon] #${id} sin precio fresco en dólares: no se coge nada.`);
|
|
418
|
+
return hecho;
|
|
419
|
+
}
|
|
420
|
+
if (usd < deps.opciones.minimoUsd) {
|
|
421
|
+
anotar(`[tablon] #${id} deja ${formatearDolares(usd)} y el mínimo es ${formatearDolares(deps.opciones.minimoUsd)}: no.`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
202
425
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
426
|
+
deps.marcar(true);
|
|
427
|
+
try {
|
|
428
|
+
if (c.accion === 'ofertar') {
|
|
429
|
+
await deps.panal.offerOnBoardJob(id, c.cobra, c.job.plazoEntregaS);
|
|
430
|
+
anotar(`[tablon] #${id} ofertado · ${formatEther(c.cobra)} · «${c.job.anuncio.slice(0, 80)}»`);
|
|
431
|
+
return hecho;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const { escrowTaskId } = await deps.panal.takeBoardJob(id);
|
|
435
|
+
deps.propios.anotar(id);
|
|
436
|
+
anotar(`[tablon] #${id} cogido · ${formatEther(c.job.importe)} · «${c.job.anuncio.slice(0, 80)}»`);
|
|
437
|
+
// Dos transacciones seguidas de la misma wallet chocan en Monad.
|
|
438
|
+
await esperar(15_000);
|
|
439
|
+
|
|
440
|
+
let brief: string;
|
|
441
|
+
try {
|
|
442
|
+
brief = await deps.panal.readBoardJobBrief(id);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
// Cogido y sin encargo: el cliente pagó pero el texto no está en el
|
|
445
|
+
// buzón. El vigilante volverá a intentarlo en cada vuelta.
|
|
446
|
+
anotar(`[tablon] #${id} cogido pero sin encargo que leer: ${corto(err)}`);
|
|
447
|
+
return hecho;
|
|
448
|
+
}
|
|
449
|
+
const entregado = await deps.trabajar({ ...c.job, escrowTaskId, agente: deps.yo, estado: EstadoTablon.Asignado }, brief);
|
|
450
|
+
anotar(`[tablon] #${id} ${entregado ? 'entregado' : 'sin entregar todavía: lo retoma el vigilante'}`);
|
|
451
|
+
return hecho;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
// Lo normal aquí es que otro lo cogiera antes, o que el cupo del día
|
|
454
|
+
// cambiara entre la pregunta y la firma. No es un fallo del agente.
|
|
455
|
+
descartados.add(id.toString());
|
|
456
|
+
anotar(`[tablon] #${id} no se pudo ${c.accion}: ${corto(err)}`);
|
|
457
|
+
return hecho;
|
|
458
|
+
} finally {
|
|
459
|
+
deps.marcar(false);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
206
462
|
} catch (err) {
|
|
207
|
-
anotar(`[tablon]
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
463
|
+
anotar(`[tablon] la ronda falló: ${corto(err)}`);
|
|
464
|
+
}
|
|
465
|
+
return hecho;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Retoma una tarea del escrow que repartió PanalTablon: la llama el vigilante
|
|
470
|
+
* al verla abierta y asignada a este agente. Si la entrega ya estaba hecha y no
|
|
471
|
+
* llegó a anclarse, la vuelve a dejar; si no, lee el encargo del buzón del
|
|
472
|
+
* tablón y la trabaja. Devuelve si quedó entregada.
|
|
473
|
+
*/
|
|
474
|
+
export async function retomarDelTablon(
|
|
475
|
+
deps: {
|
|
476
|
+
panal: PanalClient;
|
|
477
|
+
yo: Address;
|
|
478
|
+
propios: Pick<EncargosPropios, 'anotar'>;
|
|
479
|
+
briefGuardado: (taskId: bigint) => string | null;
|
|
480
|
+
resultadoGuardado: (taskId: bigint) => string | null;
|
|
481
|
+
trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
|
|
482
|
+
},
|
|
483
|
+
taskId: bigint,
|
|
484
|
+
): Promise<boolean> {
|
|
485
|
+
const job = await buscarEncargoDeTarea(deps.panal, taskId, deps.yo);
|
|
486
|
+
if (!job) {
|
|
487
|
+
console.error(`[tablon] la tarea #${taskId} es del tablón pero no encuentro su encargo: no se puede leer.`);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
deps.propios.anotar(job.id);
|
|
491
|
+
|
|
492
|
+
const resultado = deps.resultadoGuardado(taskId);
|
|
493
|
+
if (resultado !== null) {
|
|
494
|
+
const { txHash } = await deps.panal.deliverBoardJob(job.id, resultado);
|
|
495
|
+
console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
|
|
496
|
+
return true;
|
|
211
497
|
}
|
|
498
|
+
// El de disco solo si cuadra con la cadena; si no, se vuelve a leer del buzón.
|
|
499
|
+
const guardado = deps.briefGuardado(taskId);
|
|
500
|
+
const brief =
|
|
501
|
+
guardado !== null && keccak256(toBytes(guardado)).toLowerCase() === job.taskHash.toLowerCase()
|
|
502
|
+
? guardado
|
|
503
|
+
: await deps.panal.readBoardJobBrief(job.id);
|
|
504
|
+
console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) asignado a este agente: se trabaja`);
|
|
505
|
+
return deps.trabajar(job, brief);
|
|
212
506
|
}
|
|
213
507
|
|
|
214
508
|
/**
|
|
215
|
-
* Arranca las rondas. Lee de la cadena el precio
|
|
216
|
-
*
|
|
509
|
+
* Arranca las rondas. Lee de la cadena el precio de este agente una vez, al
|
|
510
|
+
* arrancar, y declara sus categorías si no son las de `TABLON_CATEGORIAS`.
|
|
217
511
|
*/
|
|
218
|
-
export async function arrancarTablon(
|
|
219
|
-
deps
|
|
220
|
-
|
|
512
|
+
export async function arrancarTablon(deps: Omit<DepsTablon, 'precio'>): Promise<void> {
|
|
513
|
+
if (!deps.panal.boardJobsAvailable) {
|
|
514
|
+
console.warn('[tablon] PanalTablon no está en esta red. Tablón apagado.');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (deps.opciones.categorias.length === 0) {
|
|
518
|
+
console.warn(
|
|
519
|
+
`[tablon] sin TABLON_CATEGORIAS el contrato no deja coger ni ofertar nada. Elige hasta tres: ${CATEGORIAS_TABLON.join(', ')}. Tablón apagado.`,
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
221
523
|
let precio: DepsTablon['precio'];
|
|
222
|
-
let habilidades: string[];
|
|
223
524
|
try {
|
|
224
525
|
const ficha = await deps.panal.getAgent(deps.yo);
|
|
225
526
|
if (!ficha.active) {
|
|
226
|
-
console.warn('[tablon] este agente no está activo en el registro:
|
|
527
|
+
console.warn('[tablon] este agente no está activo en el registro: el contrato no le dejaría coger nada. Tablón apagado.');
|
|
227
528
|
return;
|
|
228
529
|
}
|
|
229
530
|
precio = { amount: ficha.pricePerTask, currency: ficha.currency };
|
|
230
|
-
habilidades = ficha.metadata.skills ?? [];
|
|
231
531
|
} catch (err) {
|
|
232
532
|
console.warn(`[tablon] no se pudo leer la ficha: ${err instanceof Error ? err.message : err}. Tablón apagado.`);
|
|
233
533
|
return;
|
|
234
534
|
}
|
|
235
|
-
|
|
236
|
-
|
|
535
|
+
|
|
536
|
+
// Las categorías, declaradas en la cadena. Una transacción, solo si cambian.
|
|
537
|
+
try {
|
|
538
|
+
const situacion = await deps.panal.boardStanding(deps.yo);
|
|
539
|
+
const quiero = [...deps.opciones.categorias].sort((a, b) => a - b).join();
|
|
540
|
+
if ([...situacion.categorias].sort((a, b) => a - b).join() !== quiero) {
|
|
541
|
+
await deps.panal.setBoardCategories(deps.opciones.categorias);
|
|
542
|
+
console.log(`[tablon] categorías declaradas: ${deps.opciones.categorias.map((c) => categoriaDelTablon(c)).join(', ')}`);
|
|
543
|
+
}
|
|
544
|
+
console.log(
|
|
545
|
+
`Tablón: nivel ${situacion.nivel} · cada ${deps.opciones.minutos} min · ${deps.opciones.categorias.map((c) => categoriaDelTablon(c)).join(', ')}` +
|
|
546
|
+
` · desde ${formatEther(precio.amount)} en su moneda` +
|
|
547
|
+
`${deps.opciones.minimoUsd !== null ? ` y ${formatearDolares(deps.opciones.minimoUsd)} netos` : ''}` +
|
|
548
|
+
` · ${deps.opciones.margenMinutos} min de plazo como mínimo.`,
|
|
549
|
+
);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.warn(`[tablon] no se pudieron declarar las categorías: ${err instanceof Error ? err.message.split('\n')[0] : err}. Tablón apagado.`);
|
|
237
552
|
return;
|
|
238
553
|
}
|
|
239
554
|
|
|
240
|
-
const completas: DepsTablon = { ...deps, precio
|
|
241
|
-
console.log(
|
|
242
|
-
`Tablón: cada ${deps.opciones.minutos} min · desde ${formatEther(precio.amount)} en su moneda · ` +
|
|
243
|
-
`con ${deps.opciones.margenMinutos} min de plazo como mínimo · ${habilidades.length + deps.opciones.palabras.length} palabras.`,
|
|
244
|
-
);
|
|
555
|
+
const completas: DepsTablon = { ...deps, precio };
|
|
245
556
|
let enMarcha = false;
|
|
246
557
|
const ronda = async (): Promise<void> => {
|
|
247
558
|
if (enMarcha) return;
|
|
@@ -87,6 +87,18 @@ export interface VigilanteDeps {
|
|
|
87
87
|
reentregar: (taskId: bigint, texto: string) => Promise<void>;
|
|
88
88
|
/** URL pública del agente, para el aviso de encargo huérfano. */
|
|
89
89
|
urlPublica?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Las tareas que repartió el tablón con reglas (PanalTablon). Su cliente en
|
|
92
|
+
* el escrow es el tablón, su encargo no llega por /brief —se lee firmando del
|
|
93
|
+
* buzón del tablón— y su entrega tiene que dejarse allí antes de anclarse.
|
|
94
|
+
* Por eso no siguen el camino de las demás: ni se reentregan con
|
|
95
|
+
* `reentregar`, ni se avisa de que falte su encargo. `retomar` dice si quedó
|
|
96
|
+
* entregada. Sin esto, cada una saldría como «PAGADA Y SIN ENCARGO».
|
|
97
|
+
*/
|
|
98
|
+
tablon?: {
|
|
99
|
+
es: (cliente: Address) => boolean;
|
|
100
|
+
retomar: (taskId: bigint) => Promise<boolean>;
|
|
101
|
+
};
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/** Cada cuánto se mira cuando hay movimiento, en segundos. */
|
|
@@ -289,6 +301,14 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
|
|
|
289
301
|
return 'pendiente';
|
|
290
302
|
}
|
|
291
303
|
|
|
304
|
+
// Del tablón con reglas: por su camino. Si `retomar` lanza, sube a
|
|
305
|
+
// `repasar`, que la deja pendiente para la próxima vuelta.
|
|
306
|
+
if (deps.tablon?.es(task.client)) {
|
|
307
|
+
const entregada = await deps.tablon.retomar(taskId);
|
|
308
|
+
vistas.delete(id);
|
|
309
|
+
return entregada ? 'resuelta' : 'pendiente';
|
|
310
|
+
}
|
|
311
|
+
|
|
292
312
|
// CASO 3: el resultado está calculado y la tarea sigue abierta, así que la
|
|
293
313
|
// entrega no llegó a anclarse. Se reintenta, que es gratis para el cliente
|
|
294
314
|
// y le devuelve una tarea que daba por perdida.
|