create-panal-agent 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n.js +20 -0
- package/dist/index.js +11 -0
- package/package.json +2 -2
- package/template/src/retirada.ts +251 -0
- package/template/src/server.ts +43 -0
- package/template/src/tablon.ts +257 -0
package/dist/i18n.js
CHANGED
|
@@ -87,6 +87,8 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
|
|
|
87
87
|
x402: 'Charge per call (optional). Leave it empty and your agent only takes escrow jobs.\nThe price is per request, in an EIP-2612 token — it cannot be MON, the scheme needs `permit`.',
|
|
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
|
+
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.",
|
|
90
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.",
|
|
91
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).",
|
|
92
94
|
},
|
|
@@ -194,6 +196,8 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
|
|
|
194
196
|
x402: 'Cobro por llamada (opcional). Déjalo vacío y tu agente solo acepta encargos del escrow.\nEl precio es por petición, en un token EIP-2612: no puede ser MON, el esquema necesita `permit`.',
|
|
195
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.",
|
|
196
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
|
+
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.",
|
|
197
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.",
|
|
198
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).",
|
|
199
203
|
},
|
|
@@ -302,6 +306,8 @@ const zh = {
|
|
|
302
306
|
x402: '按次收费(可选)。留空则你的代理只接受托管订单。\n价格按每次请求计算,使用支持 EIP-2612 的代币——不能是 MON,该方案依赖 `permit`。',
|
|
303
307
|
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。 把它设得低于你每次提问的收费(X402_PRICE):等于或高于该值时,每一笔你转包出去的活都赚不到钱,还要自付 gas——而这恰恰惩罚了你希望它做的事,因为它越善于识别自己不懂的部分,就越经常白干。取你价格的三分之一是个合理的起点。 光有数字还不够:还需要 agent.ts 里的 SUBCONTRATA_SKILLS 列表,它规定了可以买什么。默认为空,也就永远不会转包;因为市场搜索在找不到人时会自动泛化,一个请求 \"python video encoding\" 的代码代理最后会搜索 \"video\",把钱付给做视频的代理。",
|
|
304
308
|
vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
|
|
309
|
+
retirada: "自动提取。托管合约会把每个已批准的委托记在合约里,在有人调用 withdraw 之前不会付款。这个代理每 60 分钟收取一次:当 Gas 不超过提取金额的 2 % 时提取 MON,$PANAL 从 1000 起提取。需要少量 MON 支付 Gas,委托进行中时不会运行。\nRETIRADA=off 可关闭。",
|
|
310
|
+
tablon: "任务板:自动接取未指定代理的委托。默认关闭,只有 TABLON=on 才开启,因为接下委托就等于承诺在截止前交付。只接以你的币种支付、金额不低于你在注册表中的价格、剩余时间至少 TABLON_MARGEN_MINUTOS 分钟、且公开说明中提到你某项技能或 TABLON_PALABRAS(逗号分隔)中某个词的委托。每轮一个,处理其他委托时从不接新的。",
|
|
305
311
|
memoria: "对话记忆(仅限 x402)。没有它,每次付费调用都是独立的:客户提问,你回答,下一次你不知道之前在谈什么——那是带账单的搜索框,不是聊天。\nMEMORIA_TURNOS=0 可关闭。两个上限的存在是因为历史会进入提示词,而提示词由你付费,客户只付固定的每条消息价格。",
|
|
306
312
|
seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 前面有反向代理时使用(Caddy、nginx)——这是常态,因为你的端点必须是 https。不设置时所有请求都带着代理的 IP,上面的上限就不再是每个客户端的,而变成由索引器、浏览器和你的客户共用的全局上限;一旦填满,你的代理会对所有人返回 429,而无法发送任务书的客户只能带着已锁定的付款等到期限。没有代理时请保持关闭:无代理却信任该请求头,任何人都能为每个请求伪造 IP。 AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
|
|
307
313
|
},
|
|
@@ -405,6 +411,8 @@ const hi = {
|
|
|
405
411
|
x402: 'प्रति कॉल शुल्क (वैकल्पिक)। खाली छोड़ें तो आपका एजेंट केवल एस्क्रो वाले काम लेगा।\nकीमत प्रति अनुरोध है, EIP-2612 टोकन में — MON नहीं चल सकता, इस योजना को `permit` चाहिए।',
|
|
406
412
|
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है। इसे अपने प्रति-प्रश्न शुल्क (X402_PRICE) से कम रखें: बराबर या अधिक होने पर, जिस भी काम में आप दूसरे को सौंपेंगे उसमें कुछ नहीं बचेगा और गैस भी आपकी जेब से जाएगी — और यह ठीक उसी व्यवहार को दंडित करता है जो आप चाहते हैं, क्योंकि आपका एजेंट जितना बेहतर पहचानेगा कि उसे क्या नहीं आता, उतनी ही बार वह मुफ़्त काम करेगा। अपने दाम का एक तिहाई एक समझदार शुरुआत है। सिर्फ़ संख्या काफ़ी नहीं: agent.ts में SUBCONTRATA_SKILLS सूची भी चाहिए, जो बताती है कि वह क्या खरीद सकता है। खाली — जैसा यह आता है — तो वह कभी नहीं सौंपता, क्योंकि कोई न मिलने पर बाज़ार की खोज सामान्यीकरण कर देती है, और \"python video encoding\" माँगने वाला कोड एजेंट अंत में \"video\" खोजकर वीडियो बनाने वाले को भुगतान कर देगा।",
|
|
407
413
|
vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
|
|
414
|
+
retirada: "स्वचालित निकासी। एस्क्रो हर स्वीकृत आदेश को कॉन्ट्रैक्ट के अंदर जमा करता है और जब तक कोई withdraw न बुलाए, कुछ नहीं भेजता। यह एजेंट हर 60 मिनट में इसे निकालता है: MON तब, जब गैस निकाली गई राशि के 2 % से अधिक न हो, और $PANAL 1000 से। गैस के लिए थोड़ा MON चाहिए, और कोई आदेश चलते समय यह कभी नहीं चलता।\nRETIRADA=off इसे बंद करता है।",
|
|
415
|
+
tablon: "टास्क बोर्ड: बिना एजेंट के पोस्ट किए गए काम खुद उठाएँ। TABLON=on न हो तो बंद, क्योंकि कोई काम उठाना उसे समय-सीमा से पहले देने का वादा है। यह केवल वही काम उठाता है जो आपकी मुद्रा में हो, कम से कम आपकी रजिस्ट्री कीमत दे, जिसमें TABLON_MARGEN_MINUTOS मिनट की समय-सीमा बची हो, और जिसके सार्वजनिक विवरण में आपका कोई कौशल या TABLON_PALABRAS (कॉमा से अलग) का कोई शब्द हो। हर दौर में एक, और किसी दूसरे काम के चलते कभी नहीं।",
|
|
408
416
|
memoria: "बातचीत की स्मृति (केवल x402)। इसके बिना हर सशुल्क कॉल स्वतंत्र होती है: ग्राहक पूछता है, आप उत्तर देते हैं, और अगली बार आपको पता नहीं होता कि बात किस बारे में थी — वह चैट नहीं, बिल वाला सर्च बॉक्स है।\nMEMORIA_TURNOS=0 इसे बंद कर देता है। दोनों सीमाएँ इसलिए हैं कि इतिहास प्रॉम्प्ट में जाता है, और प्रॉम्प्ट का भुगतान आप करते हैं जबकि ग्राहक प्रति संदेश निश्चित मूल्य देता है।",
|
|
409
417
|
seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 अगर आगे रिवर्स प्रॉक्सी हो (Caddy, nginx) — यही सामान्य है, क्योंकि आपका एंडपॉइंट https होना चाहिए। इसके बिना हर अनुरोध प्रॉक्सी के IP के साथ आता है, इसलिए ऊपर की सीमा प्रति-क्लाइंट नहीं रहती और एक वैश्विक सीमा बन जाती है जिसे इंडेक्सर, ब्राउज़र और आपके क्लाइंट साझा करते हैं; भर जाने पर आपका एजेंट सबको 429 देता है, और जो क्लाइंट अपना ब्रीफ़ नहीं भेज पाता वह भुगतान लॉक होने के बावजूद समय-सीमा तक इंतज़ार करता है। प्रॉक्सी न हो तो बंद रखें: बिना प्रॉक्सी उस हेडर पर भरोसा करने से कोई भी हर अनुरोध में मनमाना IP भेज सकता है। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
|
|
410
418
|
},
|
|
@@ -512,6 +520,8 @@ const ar = {
|
|
|
512
520
|
x402: 'التحصيل لكل استدعاء (اختياري). اتركه فارغًا فيقبل وكيلك مهام الضمان فقط.\nالسعر لكل طلب، بعملة تدعم EIP-2612 — لا يصلح MON، فالمخطط يحتاج `permit`.',
|
|
513
521
|
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL. اجعله أقل من سعر السؤال لديك (X402_PRICE): فعند التساوي أو الزيادة، كل مهمة تُسنِد فيها جزءًا إلى غيرك لا تُبقي لك شيئًا وتدفع الغاز من جيبك — وهذا يعاقب تحديدًا ما تريده منه، لأنه كلما أحسن إدراك ما يجهله عمل مجانًا أكثر. ثلث سعرك بداية معقولة. الرقم وحده لا يكفي: تحتاج أيضًا إلى قائمة SUBCONTRATA_SKILLS في agent.ts، التي تحدد ما الذي يُسمح بشرائه. وهي فارغة كما تأتي، فلا يفوّض أبدًا؛ لأن بحث السوق يعمّم عندما لا يجد أحدًا، فوكيل برمجة يطلب \"python video encoding\" ينتهي به الأمر باحثًا عن \"video\" ودافعًا لوكيل يصنع الفيديو.",
|
|
514
522
|
vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
|
|
523
|
+
retirada: "السحب التلقائي. يقيد الضمان كل طلب معتمد داخل العقد ولا يدفع شيئا حتى يستدعي احد withdraw. يجمعه هذا الوكيل كل 60 دقيقة: MON عندما لا يتجاوز الغاز 2 % من المبلغ، و$PANAL ابتداء من 1000. يحتاج قليلا من MON للغاز، ولا يعمل ابدا اثناء تنفيذ طلب.\nRETIRADA=off يوقفه.",
|
|
524
|
+
tablon: "لوحة المهام: التقاط الطلبات المنشورة بلا وكيل بنفسك. معطلة ما لم يكن TABLON=on، لأن التقاط طلب يعني الالتزام بتسليمه قبل موعده. لا يلتقط إلا طلبا بعملتك، يدفع على الاقل سعرك في السجل، يبقى من مهلته TABLON_MARGEN_MINUTOS دقيقة، ويذكر اعلانه العلني احدى مهاراتك او كلمة من TABLON_PALABRAS (مفصولة بفواصل). طلب واحد في كل جولة، وابدا اثناء العمل على غيره.",
|
|
515
525
|
memoria: "ذاكرة المحادثة (في x402 فقط). بدونها تكون كل مكالمة مدفوعة مستقلة: يسأل العميل، تجيب، وفي المرة التالية لا تعرف عمّا كان الحديث — هذا مربع بحث بفاتورة، لا محادثة.\nالقيمة MEMORIA_TURNOS=0 تُعطّلها. الحدّان موجودان لأن السجل يدخل في التوجيه، والتوجيه تدفعه أنت بينما يدفع العميل سعرًا ثابتًا لكل رسالة.",
|
|
516
526
|
seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 إذا كان أمامه بروكسي عكسي (Caddy أو nginx) — وهو الوضع المعتاد لأن نقطة النهاية يجب أن تكون https. بدونه تصل كل الطلبات بعنوان IP الخاص بالبروكسي، فيتوقف الحد أعلاه عن كونه لكل عميل ويصبح حدًا عامًا واحدًا يتقاسمه المفهرس والمتصفحات وعملاؤك؛ وعند امتلائه يرد وكيلك بـ 429 على الجميع، ويظل العميل الذي تعذر عليه إرسال مهمته ينتظر انتهاء المهلة ودفعته محجوزة بالفعل. بلا بروكسي اتركه معطلًا: الوثوق بهذه الترويسة دون بروكسي يتيح لأي شخص اختلاق عنوان IP لكل طلب. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
|
|
517
527
|
},
|
|
@@ -618,6 +628,8 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
|
|
|
618
628
|
x402: "Facturation à l'appel (facultatif). Laissez vide et votre agent ne prend que des missions sous entiercement.\nLe prix est par requête, dans un jeton EIP-2612 : pas de MON, le schéma exige `permit`.",
|
|
619
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.",
|
|
620
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
|
+
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 qu une commande dans votre monnaie, payant au moins votre prix du registre, avec TABLON_MARGEN_MINUTOS minutes d echeance devant elle, et dont l annonce publique mentionne une de vos competences ou un mot de TABLON_PALABRAS (separes par des virgules). Une par tour, jamais pendant que vous en traitez une autre.",
|
|
621
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.",
|
|
622
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.",
|
|
623
635
|
},
|
|
@@ -728,6 +740,8 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
|
|
|
728
740
|
x402: 'Cobrança por chamada (opcional). Deixe vazio e o seu agente só aceita trabalhos do escrow.\nO preço é por pedido, num token EIP-2612: não pode ser MON, o esquema precisa de `permit`.',
|
|
729
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.",
|
|
730
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
|
+
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 as encomendas publicadas sem agente. DESLIGADO salvo TABLON=on, porque apanhar uma e comprometer-se a entrega-la antes do prazo. So apanha uma encomenda na sua moeda, que pague pelo menos o seu preco do registo, com TABLON_MARGEN_MINUTOS minutos de prazo pela frente e cujo anuncio publico mencione uma das suas competencias ou de TABLON_PALABRAS (separadas por virgulas). Uma por ronda, nunca enquanto trata de outra.",
|
|
731
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.",
|
|
732
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.",
|
|
733
747
|
},
|
|
@@ -836,6 +850,8 @@ const ru = {
|
|
|
836
850
|
x402: 'Плата за вызов (необязательно). Оставьте пустым — агент будет брать только заказы через эскроу.\nЦена за один запрос, в токене с EIP-2612: MON не подходит, схеме нужен `permit`.',
|
|
837
851
|
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL. Поставьте НИЖЕ того, что берёте за вопрос (X402_PRICE): при равенстве или выше каждая работа, где вы делегируете, не приносит ничего, а газ вы платите сами — и это наказывает ровно то поведение, которого вы добиваетесь: чем лучше агент понимает, чего не знает, тем чаще работает даром. Треть вашей цены — разумное начало. Одного числа мало: нужен ещё список SUBCONTRATA_SKILLS в agent.ts, который говорит, ЧТО можно покупать. Пустой — а таким он и поставляется — и агент никогда не делегирует, потому что поиск по рынку обобщает запрос, если никого не нашёл: агент по коду, попросивший \"python video encoding\", в итоге станет искать \"video\" и заплатит тому, кто делает видео.",
|
|
838
852
|
vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
|
|
853
|
+
retirada: "Автоматический вывод. Эскроу зачисляет каждый одобренный заказ внутри контракта и ничего не отправляет, пока кто-то не вызовет withdraw. Каждые 60 мин этот агент забирает средства: MON, когда газ не больше 2 % от суммы, $PANAL от 1000. Нужно немного MON на газ, и вывод никогда не запускается во время выполнения заказа.\nRETIRADA=off выключает его.",
|
|
854
|
+
tablon: "Доска заданий: самостоятельно брать задания, опубликованные без агента. ВЫКЛЮЧЕНО, пока нет TABLON=on: взять задание значит обязаться сдать его до срока. Берётся только задание в вашей валюте, оплачивающее не меньше вашей цены в реестре, с запасом срока не меньше TABLON_MARGEN_MINUTOS минут и с публичным описанием, где упомянут один из ваших навыков или слово из TABLON_PALABRAS (через запятую). Одно за раунд и никогда во время работы над другим.",
|
|
839
855
|
memoria: "Память разговора (только x402). Без неё каждый оплаченный вызов независим: клиент спрашивает, вы отвечаете, а в следующий раз вы не знаете, о чём шла речь — это поисковая строка со счётом, а не чат.\nMEMORIA_TURNOS=0 отключает её. Оба предела существуют потому, что история попадает В промпт, а промпт оплачиваете вы, тогда как клиент платит фиксированную цену за сообщение.",
|
|
840
856
|
seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 если впереди стоит обратный прокси (Caddy, nginx) — обычный случай, ведь ваш эндпоинт должен быть https. Без этого все запросы приходят с IP прокси, поэтому лимит выше перестаёт быть на клиента и становится единым глобальным, который делят индексатор, браузеры и ваши клиенты; когда он заполняется, ваш агент отвечает 429 всем, а клиент, не сумевший отправить задание, ждёт истечения срока с уже заблокированной оплатой. Без прокси оставьте выключенным: доверять этому заголовку без прокси — значит позволить подделывать IP в каждом запросе. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
|
|
841
857
|
},
|
|
@@ -945,6 +961,8 @@ const bn = {
|
|
|
945
961
|
x402: 'প্রতি কলে চার্জ (ঐচ্ছিক)। ফাঁকা রাখলে আপনার এজেন্ট কেবল এসক্রো কাজ নেবে।\nদাম প্রতি অনুরোধে, EIP-2612 টোকেনে — MON চলবে না, স্কিমটির `permit` দরকার।',
|
|
946
962
|
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ। এটি আপনার প্রতি-প্রশ্ন মূল্যের (X402_PRICE) চেয়ে কম রাখুন: সমান বা বেশি হলে, যে কাজেই আপনি অন্যকে দেবেন তাতে কিছুই থাকবে না, গ্যাসও আপনার — আর এতে ঠিক সেই আচরণই শাস্তি পায় যা আপনি চান, কারণ এজেন্ট যত ভালোভাবে বুঝবে সে কী জানে না, তত বেশিবার সে বিনামূল্যে কাজ করবে। আপনার দামের এক-তৃতীয়াংশ একটি যুক্তিসঙ্গত শুরু। শুধু সংখ্যাই যথেষ্ট নয়: agent.ts-এ SUBCONTRATA_SKILLS তালিকাও লাগে, যা বলে দেয় সে কী কিনতে পারবে। খালি — যেমনটা আসে — হলে সে কখনও দায়িত্ব দেয় না, কারণ কাউকে না পেলে বাজারের খোঁজ সাধারণীকরণ করে, আর \"python video encoding\" চাওয়া কোড এজেন্ট শেষে \"video\" খুঁজে ভিডিও বানায় এমন কাউকে টাকা দিয়ে বসবে।",
|
|
947
963
|
vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
|
|
964
|
+
retirada: "স্বয়ংক্রিয় উত্তোলন। এসক্রো প্রতিটি অনুমোদিত আদেশ চুক্তির ভিতরে জমা রাখে এবং কেউ withdraw না ডাকা পর্যন্ত কিছু পাঠায় না। এই এজেন্ট প্রতি ৬০ মিনিটে তা তোলে: MON, যখন গ্যাস তোলা অঙ্কের ২ % এর বেশি নয়, আর $PANAL ১০০০ থেকে। গ্যাসের জন্য সামান্য MON লাগে, এবং কোনো আদেশ চলার সময় এটি কখনও চলে না।\nRETIRADA=off এটি বন্ধ করে।",
|
|
965
|
+
tablon: "টাস্ক বোর্ড: এজেন্ট ছাড়া পোস্ট করা কাজ নিজে নিজে তুলে নিন। TABLON=on না থাকলে বন্ধ, কারণ কাজ নেওয়া মানে সময়সীমার আগে জমা দেওয়ার প্রতিশ্রুতি। এটি শুধু সেই কাজ নেয় যা আপনার মুদ্রায়, অন্তত আপনার রেজিস্ট্রি দামের সমান দেয়, যাতে TABLON_MARGEN_MINUTOS মিনিট সময় বাকি, আর যার প্রকাশ্য বিবরণে আপনার কোনো দক্ষতা বা TABLON_PALABRAS (কমা দিয়ে আলাদা) এর কোনো শব্দ আছে। প্রতি দফায় একটি, অন্য কাজ চলার সময় কখনও নয়।",
|
|
948
966
|
memoria: "কথোপকথনের স্মৃতি (কেবল x402)। এটি ছাড়া প্রতিটি অর্থপ্রদত্ত কল স্বতন্ত্র: ক্লায়েন্ট জিজ্ঞাসা করে, আপনি উত্তর দেন, আর পরেরবার আপনি জানেন না কী নিয়ে কথা হচ্ছিল — সেটা চ্যাট নয়, বিলসহ সার্চ বক্স।\nMEMORIA_TURNOS=0 এটি বন্ধ করে। দুটি সীমা আছে কারণ ইতিহাস প্রম্পটে ঢোকে, আর প্রম্পটের খরচ আপনি দেন যখন ক্লায়েন্ট বার্তাপ্রতি নির্দিষ্ট দাম দেয়।",
|
|
949
967
|
seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 যদি সামনে রিভার্স প্রক্সি থাকে (Caddy, nginx) — এটিই স্বাভাবিক, কারণ আপনার এন্ডপয়েন্ট https হতেই হবে। এটি ছাড়া প্রতিটি অনুরোধ প্রক্সির IP নিয়ে আসে, ফলে উপরের সীমা আর প্রতি-ক্লায়েন্ট থাকে না, বরং একটি বৈশ্বিক সীমা হয়ে যায় যা ইনডেক্সার, ব্রাউজার ও আপনার ক্লায়েন্টরা ভাগ করে; ভরে গেলে আপনার এজেন্ট সবাইকে 429 দেয়, আর যে ক্লায়েন্ট তার কাজের বিবরণ পাঠাতে পারে না সে অর্থ আটকে থাকা অবস্থায় সময়সীমা পর্যন্ত অপেক্ষা করে। প্রক্সি না থাকলে বন্ধ রাখুন: প্রক্সি ছাড়া ওই হেডারে ভরসা করলে যে কেউ প্রতি অনুরোধে IP বানিয়ে দিতে পারে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
|
|
950
968
|
},
|
|
@@ -1051,6 +1069,8 @@ const ur = {
|
|
|
1051
1069
|
x402: 'فی کال وصولی (اختیاری)۔ خالی چھوڑ دیں تو آپ کا ایجنٹ صرف ایسکرو کے کام لے گا۔\nقیمت فی درخواست ہے، EIP-2612 ٹوکن میں — MON نہیں چل سکتا، اس اسکیم کو `permit` چاہیے۔',
|
|
1052
1070
|
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔ اسے اپنی فی سوال قیمت (X402_PRICE) سے کم رکھیں: برابر یا زیادہ ہونے پر، جس کام میں بھی آپ کسی اور کو دیں گے اس میں کچھ نہیں بچے گا اور گیس بھی آپ کی جیب سے جائے گی — اور یہ بالکل اسی رویّے کو سزا دیتا ہے جو آپ چاہتے ہیں، کیونکہ آپ کا ایجنٹ جتنا بہتر پہچانے گا کہ اسے کیا نہیں آتا، اتنی ہی زیادہ بار وہ مفت کام کرے گا۔ اپنی قیمت کا ایک تہائی معقول آغاز ہے۔ صرف عدد کافی نہیں: agent.ts میں SUBCONTRATA_SKILLS کی فہرست بھی چاہیے، جو بتاتی ہے کہ وہ کیا خرید سکتا ہے۔ خالی — جیسا کہ یہ آتا ہے — تو وہ کبھی کام نہیں سونپتا، کیونکہ کوئی نہ ملنے پر مارکیٹ کی تلاش عام کر دیتی ہے، اور \"python video encoding\" مانگنے والا کوڈ ایجنٹ آخر میں \"video\" ڈھونڈ کر ویڈیو بنانے والے کو ادائیگی کر بیٹھے گا۔",
|
|
1053
1071
|
vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
|
|
1072
|
+
retirada: "خودکار نکلوائی۔ ایسکرو ہر منظور شدہ آرڈر کو کنٹریکٹ کے اندر جمع کرتا ہے اور جب تک کوئی withdraw نہ بلائے کچھ نہیں بھیجتا۔ یہ ایجنٹ ہر 60 منٹ میں اسے نکالتا ہے: MON جب گیس نکالی گئی رقم کے 2 % سے زیادہ نہ ہو، اور $PANAL 1000 سے۔ گیس کے لیے تھوڑا MON چاہیے، اور کسی آرڈر کے دوران یہ کبھی نہیں چلتا۔\nRETIRADA=off اسے بند کرتا ہے۔",
|
|
1073
|
+
tablon: "ٹاسک بورڈ: بغیر ایجنٹ کے پوسٹ کیے گئے کام خود اٹھائیں۔ TABLON=on نہ ہو تو بند، کیونکہ کام اٹھانا اسے مہلت سے پہلے دینے کا وعدہ ہے۔ یہ صرف وہ کام اٹھاتا ہے جو آپ کی کرنسی میں ہو، کم از کم آپ کی رجسٹری قیمت دے، جس کی مہلت میں TABLON_MARGEN_MINUTOS منٹ باقی ہوں، اور جس کے عوامی اعلان میں آپ کی کوئی مہارت یا TABLON_PALABRAS (کوما سے الگ) کا کوئی لفظ ہو۔ ہر دور میں ایک، اور کسی دوسرے کام کے دوران کبھی نہیں۔",
|
|
1054
1074
|
memoria: "گفتگو کی یادداشت (صرف x402)۔ اس کے بغیر ہر ادا شدہ کال الگ ہوتی ہے: کلائنٹ پوچھتا ہے، آپ جواب دیتے ہیں، اور اگلی بار آپ کو معلوم نہیں ہوتا کہ بات کس بارے میں تھی — یہ چیٹ نہیں، بل والا سرچ باکس ہے۔\nMEMORIA_TURNOS=0 اسے بند کر دیتا ہے۔ دونوں حدیں اس لیے ہیں کہ تاریخ پرامپٹ میں جاتی ہے، اور پرامپٹ کا خرچ آپ اٹھاتے ہیں جبکہ کلائنٹ فی پیغام مقررہ قیمت دیتا ہے۔",
|
|
1055
1075
|
seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 اگر آگے ریورس پراکسی ہو (Caddy، nginx) — یہی معمول ہے، کیونکہ آپ کا اینڈ پوائنٹ https ہونا چاہیے۔ اس کے بغیر ہر درخواست پراکسی کے IP کے ساتھ آتی ہے، چنانچہ اوپر والی حد فی کلائنٹ نہیں رہتی بلکہ ایک عالمی حد بن جاتی ہے جسے انڈیکسر، براؤزر اور آپ کے کلائنٹ مل کر استعمال کرتے ہیں؛ بھر جانے پر آپ کا ایجنٹ سب کو 429 دیتا ہے، اور جو کلائنٹ اپنی ہدایت نہیں بھیج پاتا وہ ادائیگی مقفل ہونے کے باوجود مہلت تک انتظار کرتا ہے۔ پراکسی نہ ہو تو بند رکھیں: پراکسی کے بغیر اس ہیڈر پر بھروسہ کرنے سے کوئی بھی ہر درخواست میں IP گھڑ سکتا ہے۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
|
|
1056
1076
|
},
|
package/dist/index.js
CHANGED
|
@@ -269,6 +269,17 @@ function writeEnvExample(dest, t) {
|
|
|
269
269
|
'VIGILANTE_SEGUNDOS=60',
|
|
270
270
|
'PUBLIC_URL=',
|
|
271
271
|
'',
|
|
272
|
+
bloque(t.env.retirada),
|
|
273
|
+
'RETIRADA_MINUTOS=60',
|
|
274
|
+
'RETIRADA_MAX_GAS_PCT=2',
|
|
275
|
+
'RETIRADA_PANAL_DESDE=1000',
|
|
276
|
+
'',
|
|
277
|
+
bloque(t.env.tablon),
|
|
278
|
+
'TABLON=off',
|
|
279
|
+
'TABLON_MINUTOS=5',
|
|
280
|
+
'TABLON_MARGEN_MINUTOS=30',
|
|
281
|
+
'TABLON_PALABRAS=',
|
|
282
|
+
'',
|
|
272
283
|
bloque(t.env.memoria),
|
|
273
284
|
'MEMORIA_TURNOS=6',
|
|
274
285
|
'MEMORIA_CHARS=4000',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-panal-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.json",
|
|
44
44
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
-
"test": "tsx test/i18n.test.ts && tsx test/logo.test.ts && tsx test/memoria.test.ts && tsx test/reintento.test.ts && tsx test/zip.test.ts && tsx test/adjuntos.test.ts && tsx test/subcontrata.test.ts && tsx test/salida.test.ts && tsx test/vigilante.test.ts && tsx test/scaffold.test.ts"
|
|
45
|
+
"test": "tsx test/i18n.test.ts && tsx test/logo.test.ts && tsx test/memoria.test.ts && tsx test/reintento.test.ts && tsx test/zip.test.ts && tsx test/adjuntos.test.ts && tsx test/subcontrata.test.ts && tsx test/salida.test.ts && tsx test/vigilante.test.ts && tsx test/retirada.test.ts && tsx test/tablon.test.ts && tsx test/scaffold.test.ts"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* La retirada automática: lo que el escrow le debe a este agente, a su wallet.
|
|
4
|
+
* ────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* POR QUÉ EXISTE. El escrow de Panal es de pago TIRADO: aprobar un encargo le
|
|
7
|
+
* ACREDITA el dinero al agente dentro del contrato, no se lo manda. Hasta que
|
|
8
|
+
* alguien llama a `withdraw`, se queda ahí. Un humano lo ve en el panel y le da
|
|
9
|
+
* al botón; un agente que corre solo, sin nadie mirando, acumulaba cobros sin
|
|
10
|
+
* recogerlos nunca. Los cuatro agentes de Panal llegaron a tener más de 2 MON y
|
|
11
|
+
* 3.000 $PANAL parados en el escrow.
|
|
12
|
+
*
|
|
13
|
+
* Este agente tiene su clave en el .env —con ella firma cada entrega—, así que
|
|
14
|
+
* puede retirar solo. Una persona que vende por el buzón no: su clave está en
|
|
15
|
+
* su wallet y nadie más puede retirar por ella, que es justo lo que protege el
|
|
16
|
+
* contrato (`withdraw` le paga siempre a quien llama).
|
|
17
|
+
*
|
|
18
|
+
* CUÁNDO RETIRA, Y POR QUÉ NO SIEMPRE. Retirar cuesta gas, y el gas se paga en
|
|
19
|
+
* MON aunque lo retirado sea $PANAL. Medido en mainnet el 2026-09-14:
|
|
20
|
+
*
|
|
21
|
+
* retirar MON 55.157 de gas ≈ 0,0056 MON
|
|
22
|
+
* retirar $PANAL 103.511 de gas ≈ 0,0106 MON
|
|
23
|
+
*
|
|
24
|
+
* Retirar 0,049 MON en cada encargo sería dejarse un 13 % por el camino. Así
|
|
25
|
+
* que:
|
|
26
|
+
*
|
|
27
|
+
* - En MON se retira cuando el gas no pasa de `maxGasPct` de lo retirado (2 %
|
|
28
|
+
* por defecto). Es una regla sobre el gas DE AHORA, no un número fijo: si
|
|
29
|
+
* el gas sube, espera a que haya más; si baja, retira antes.
|
|
30
|
+
* - En $PANAL se retira a partir de `panalDesde` (1000 por defecto). Ahí no
|
|
31
|
+
* hay regla relativa posible: el gas es en MON y el token no tiene precio
|
|
32
|
+
* con el que compararlo.
|
|
33
|
+
*
|
|
34
|
+
* Y en los dos casos solo si la wallet cubre la RESERVA de Monad —límite de gas
|
|
35
|
+
* × precio máximo—, que se bloquea antes de ejecutar aunque luego se cobre
|
|
36
|
+
* menos. Sin esa comprobación, el nodo rechaza con «insufficient balance» y un
|
|
37
|
+
* reintento con el mismo nonce y las mismas comisiones repite el rechazo.
|
|
38
|
+
*
|
|
39
|
+
* EL GAS SE FIJA A MANO, Y ESTO COSTÓ 1,096 MON APRENDERLO. viem no estima el
|
|
40
|
+
* gas: le pide al nodo que rellene la transacción (`eth_fillTransaction`), y el
|
|
41
|
+
* de Monad devuelve un gas disparatado justo para `withdraw(address(0))` —1,05 M
|
|
42
|
+
* para una wallet, 10,7 M para otra—, cuando lo necesario son 55.157. Y Monad
|
|
43
|
+
* cobra el LÍMITE de gas entero, no lo usado. La primera retirada automática de
|
|
44
|
+
* Lint sacó 1,092 MON y pagó 1,096 de gas (2026-09-14, tx 0xa960cb7e…). Así
|
|
45
|
+
* que el gas sale de `eth_estimateGas` —que sí da el número bueno—, con un 10 %
|
|
46
|
+
* de margen, y se pasa explícito: viem lo respeta. Y si la estimación vuelve por
|
|
47
|
+
* encima de `TOPE_GAS`, no se firma nada.
|
|
48
|
+
*
|
|
49
|
+
* NUNCA A LA VEZ QUE UNA ENTREGA. Si hay un encargo en marcha, la ronda se
|
|
50
|
+
* salta: dos transacciones seguidas de la misma wallet chocan por el nonce, y
|
|
51
|
+
* en Monad una lanzada justo después de otra revierte. Por lo mismo, entre
|
|
52
|
+
* retirar MON y retirar $PANAL se espera un rato.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import type { Address, Hex } from 'viem';
|
|
56
|
+
import { formatEther, parseEther } from 'viem';
|
|
57
|
+
import { NATIVE_CURRENCY, escrowAbi, type PanalClient } from '@panal/sdk';
|
|
58
|
+
|
|
59
|
+
export interface OpcionesRetirada {
|
|
60
|
+
/** Cada cuánto se mira el escrow, en minutos. */
|
|
61
|
+
minutos: number;
|
|
62
|
+
/** MON: retirar solo si el gas (reserva) no pasa de este % de lo retirado. */
|
|
63
|
+
maxGasPct: number;
|
|
64
|
+
/** $PANAL: retirar a partir de esta cantidad, en wei. */
|
|
65
|
+
panalDesde: bigint;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Por encima de esto una retirada no se firma. Retirar MON son 55.157 de gas y
|
|
70
|
+
* $PANAL 103.511: 300.000 deja margen de sobra para cualquier retirada honrada
|
|
71
|
+
* y corta en seco una estimación disparatada antes de que se cobre.
|
|
72
|
+
*/
|
|
73
|
+
export const TOPE_GAS = 300_000n;
|
|
74
|
+
|
|
75
|
+
/** El gas con el que se firma: lo estimado más un 10 %. */
|
|
76
|
+
export function gasConMargen(estimado: bigint): bigint {
|
|
77
|
+
return (estimado * 11n + 9n) / 10n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const RETIRADA_POR_DEFECTO: OpcionesRetirada = {
|
|
81
|
+
minutos: 60,
|
|
82
|
+
maxGasPct: 2,
|
|
83
|
+
panalDesde: parseEther('1000'),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Las opciones del .env, o `null` si está apagada (`RETIRADA=off`).
|
|
88
|
+
*
|
|
89
|
+
* Un valor que no se entiende no apaga nada ni lanza: se usa el de por defecto
|
|
90
|
+
* y se dice en el log. Un agente que se niega a arrancar por una errata en una
|
|
91
|
+
* variable opcional es peor que uno que retira con el umbral de siempre.
|
|
92
|
+
*/
|
|
93
|
+
export function opcionesDelEntorno(
|
|
94
|
+
env: Record<string, string | undefined>,
|
|
95
|
+
avisar: (m: string) => void = (m) => console.warn(m),
|
|
96
|
+
): OpcionesRetirada | null {
|
|
97
|
+
if (env.RETIRADA?.trim().toLowerCase() === 'off') return null;
|
|
98
|
+
const o = { ...RETIRADA_POR_DEFECTO };
|
|
99
|
+
|
|
100
|
+
const minutos = Number(env.RETIRADA_MINUTOS);
|
|
101
|
+
if (env.RETIRADA_MINUTOS?.trim()) {
|
|
102
|
+
if (Number.isFinite(minutos) && minutos >= 5) o.minutos = minutos;
|
|
103
|
+
else avisar(`[retirada] RETIRADA_MINUTOS="${env.RETIRADA_MINUTOS}" no vale (mínimo 5): uso ${o.minutos}.`);
|
|
104
|
+
}
|
|
105
|
+
const pct = Number(env.RETIRADA_MAX_GAS_PCT);
|
|
106
|
+
if (env.RETIRADA_MAX_GAS_PCT?.trim()) {
|
|
107
|
+
if (Number.isFinite(pct) && pct > 0 && pct <= 50) o.maxGasPct = pct;
|
|
108
|
+
else avisar(`[retirada] RETIRADA_MAX_GAS_PCT="${env.RETIRADA_MAX_GAS_PCT}" no vale (0-50): uso ${o.maxGasPct}.`);
|
|
109
|
+
}
|
|
110
|
+
if (env.RETIRADA_PANAL_DESDE?.trim()) {
|
|
111
|
+
try {
|
|
112
|
+
o.panalDesde = parseEther(env.RETIRADA_PANAL_DESDE.trim());
|
|
113
|
+
} catch {
|
|
114
|
+
avisar(`[retirada] RETIRADA_PANAL_DESDE="${env.RETIRADA_PANAL_DESDE}" no vale: uso ${formatEther(o.panalDesde)}.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return o;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DepsRetirada {
|
|
121
|
+
panal: PanalClient;
|
|
122
|
+
yo: Address;
|
|
123
|
+
opciones: OpcionesRetirada;
|
|
124
|
+
/** ¿Hay un encargo en marcha? Entonces no se toca la wallet. */
|
|
125
|
+
ocupado: () => boolean;
|
|
126
|
+
/** Pausa entre dos retiradas seguidas. Inyectable para las pruebas. */
|
|
127
|
+
esperar?: (ms: number) => Promise<void>;
|
|
128
|
+
log?: (m: string) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Una ronda: mira lo que se debe en cada moneda y retira lo que compense.
|
|
133
|
+
*
|
|
134
|
+
* Devuelve lo que hizo, línea a línea, para que las pruebas lo puedan leer.
|
|
135
|
+
* Nunca lanza: un fallo aquí no puede tumbar el agente que atiende encargos.
|
|
136
|
+
*/
|
|
137
|
+
export async function repasarRetirada(deps: DepsRetirada): Promise<string[]> {
|
|
138
|
+
const log = deps.log ?? ((m: string) => console.log(m));
|
|
139
|
+
const esperar = deps.esperar ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
140
|
+
const hecho: string[] = [];
|
|
141
|
+
const anotar = (m: string): void => {
|
|
142
|
+
hecho.push(m);
|
|
143
|
+
log(m);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (deps.ocupado()) return hecho;
|
|
147
|
+
|
|
148
|
+
const { panal, yo, opciones } = deps;
|
|
149
|
+
const monedas: Array<{ direccion: Address; simbolo: 'MON' | '$PANAL' }> = [
|
|
150
|
+
{ direccion: NATIVE_CURRENCY, simbolo: 'MON' },
|
|
151
|
+
{ direccion: panal.addresses.panalToken, simbolo: '$PANAL' },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
let retiradas = 0;
|
|
155
|
+
for (const { direccion, simbolo } of monedas) {
|
|
156
|
+
try {
|
|
157
|
+
const pendiente = await panal.getPendingWithdrawal(yo, direccion);
|
|
158
|
+
if (pendiente === 0n) continue;
|
|
159
|
+
|
|
160
|
+
// $PANAL: el umbral se mira antes de gastar ni una lectura más.
|
|
161
|
+
if (simbolo === '$PANAL' && pendiente < opciones.panalDesde) continue;
|
|
162
|
+
|
|
163
|
+
const llamada = {
|
|
164
|
+
address: panal.addresses.escrow,
|
|
165
|
+
abi: escrowAbi,
|
|
166
|
+
functionName: 'withdraw',
|
|
167
|
+
args: [direccion],
|
|
168
|
+
} as const;
|
|
169
|
+
const [estimado, comisiones, saldo] = await Promise.all([
|
|
170
|
+
panal.publicClient.estimateContractGas({ ...llamada, account: yo } as never),
|
|
171
|
+
panal.publicClient.estimateFeesPerGas(),
|
|
172
|
+
panal.publicClient.getBalance({ address: yo }),
|
|
173
|
+
]);
|
|
174
|
+
const gas = gasConMargen(estimado);
|
|
175
|
+
if (gas > TOPE_GAS) {
|
|
176
|
+
anotar(
|
|
177
|
+
`[retirada] la estimación de gas para retirar ${simbolo} salió en ${estimado}, por encima del tope ` +
|
|
178
|
+
`de ${TOPE_GAS}: no se firma nada. Monad cobra el límite entero.`,
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// La reserva, con el MISMO gas con el que se va a firmar.
|
|
183
|
+
const reserva = gas * comisiones.maxFeePerGas;
|
|
184
|
+
|
|
185
|
+
// MON: que el gas no se coma más de maxGasPct de lo que se retira. Se
|
|
186
|
+
// compara con la RESERVA, que es la cota alta: si compensa con ella,
|
|
187
|
+
// compensa con lo que de verdad se cobre.
|
|
188
|
+
if (simbolo === 'MON' && reserva * 10_000n > pendiente * BigInt(Math.round(opciones.maxGasPct * 100))) continue;
|
|
189
|
+
|
|
190
|
+
if (saldo < reserva) {
|
|
191
|
+
anotar(
|
|
192
|
+
`[retirada] ${formatEther(pendiente)} ${simbolo} esperan en el escrow, pero la wallet tiene ` +
|
|
193
|
+
`${formatEther(saldo)} MON y retirar reserva ${formatEther(reserva)} MON de gas. Recarga un poco de MON.`,
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Otra transacción de esta wallet justo antes revierte en Monad.
|
|
199
|
+
if (retiradas > 0) await esperar(20_000);
|
|
200
|
+
if (deps.ocupado()) return hecho;
|
|
201
|
+
|
|
202
|
+
const wallet = panal.walletClient;
|
|
203
|
+
if (!wallet) {
|
|
204
|
+
anotar('[retirada] el cliente no tiene cuenta para firmar: no se puede retirar.');
|
|
205
|
+
return hecho;
|
|
206
|
+
}
|
|
207
|
+
const hash: Hex = await wallet.writeContract({
|
|
208
|
+
...llamada,
|
|
209
|
+
// Explícito. Sin esto viem le pide el gas al nodo y paga lo que diga.
|
|
210
|
+
gas,
|
|
211
|
+
chain: panal.publicClient.chain,
|
|
212
|
+
account: wallet.account ?? yo,
|
|
213
|
+
} as never);
|
|
214
|
+
// Un revert también llega con recibo: hay que mirar `status`.
|
|
215
|
+
const recibo = await panal.publicClient.waitForTransactionReceipt({ hash });
|
|
216
|
+
if (recibo.status !== 'success') {
|
|
217
|
+
anotar(`[retirada] retirar ${simbolo} revirtió (tx ${hash}): sigue acreditado, se reintenta en la próxima ronda.`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
retiradas++;
|
|
221
|
+
anotar(`[retirada] ${formatEther(pendiente)} ${simbolo} retirados a la wallet · tx ${hash}`);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
anotar(`[retirada] no se pudo retirar ${simbolo}: ${err instanceof Error ? err.message.split('\n')[0] : err}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return hecho;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Arranca las rondas. La primera, a los dos minutos: el agente acaba de
|
|
231
|
+
* arrancar y lo primero es atender encargos, no mover dinero.
|
|
232
|
+
*/
|
|
233
|
+
export function arrancarRetirada(deps: DepsRetirada): void {
|
|
234
|
+
const { opciones } = deps;
|
|
235
|
+
console.log(
|
|
236
|
+
`Retirada automática: cada ${opciones.minutos} min · MON cuando el gas no pase del ${opciones.maxGasPct} % · ` +
|
|
237
|
+
`$PANAL desde ${formatEther(opciones.panalDesde)} (RETIRADA=off la apaga).`,
|
|
238
|
+
);
|
|
239
|
+
let enMarcha = false;
|
|
240
|
+
const ronda = async (): Promise<void> => {
|
|
241
|
+
if (enMarcha) return;
|
|
242
|
+
enMarcha = true;
|
|
243
|
+
try {
|
|
244
|
+
await repasarRetirada(deps);
|
|
245
|
+
} finally {
|
|
246
|
+
enMarcha = false;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
setTimeout(() => void ronda(), 2 * 60_000);
|
|
250
|
+
setInterval(() => void ronda(), opciones.minutos * 60_000);
|
|
251
|
+
}
|
package/template/src/server.ts
CHANGED
|
@@ -64,6 +64,8 @@ import { handleTask, NIVELES, SUBCONTRATA_SKILLS } from './agent.js';
|
|
|
64
64
|
import { frasesGuardadas, pedirTraduccion } from './traduccion.js';
|
|
65
65
|
import type { AdjuntoRecibido, NivelPropio, TaskContext, TaskFile, TaskResult } from './agent.js';
|
|
66
66
|
import { arrancarVigilante } from './vigilante.js';
|
|
67
|
+
import { arrancarRetirada, opcionesDelEntorno } from './retirada.js';
|
|
68
|
+
import { arrancarTablon, opcionesDelEntorno as opcionesTablon } from './tablon.js';
|
|
67
69
|
import { historialParaElModelo, recordarTurno, type Turno } from './memoria.js';
|
|
68
70
|
|
|
69
71
|
const PORT = Number(process.env.PORT ?? 8787);
|
|
@@ -1896,3 +1898,44 @@ arrancarVigilante({
|
|
|
1896
1898
|
},
|
|
1897
1899
|
urlPublica: process.env.PUBLIC_URL?.trim(),
|
|
1898
1900
|
});
|
|
1901
|
+
|
|
1902
|
+
// La retirada automática: lo que el escrow acredita por cada encargo aprobado
|
|
1903
|
+
// se queda en el contrato hasta que alguien llama a `withdraw`, y un agente que
|
|
1904
|
+
// corre solo no tiene a nadie que le dé al botón. `RETIRADA=off` la apaga; el
|
|
1905
|
+
// porqué de cada umbral está en retirada.ts.
|
|
1906
|
+
// Mientras el tablón coge un encargo la wallet está firmando, aunque `work()`
|
|
1907
|
+
// todavía no haya empezado. La retirada tiene que verlo: dos transacciones
|
|
1908
|
+
// seguidas de la misma wallet chocan por el nonce.
|
|
1909
|
+
let tablonFirmando = false;
|
|
1910
|
+
|
|
1911
|
+
const retirada = opcionesDelEntorno(process.env);
|
|
1912
|
+
if (retirada) {
|
|
1913
|
+
arrancarRetirada({
|
|
1914
|
+
panal,
|
|
1915
|
+
yo: account.address,
|
|
1916
|
+
opciones: retirada,
|
|
1917
|
+
// La misma guarda que el vigilante: con un encargo en marcha la wallet
|
|
1918
|
+
// puede estar a punto de firmar su entrega, y dos transacciones seguidas
|
|
1919
|
+
// chocan por el nonce.
|
|
1920
|
+
ocupado: () => inFlight.size > 0 || tablonFirmando,
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// El tablón: coger solo los encargos publicados sin dueño que encajen con este
|
|
1925
|
+
// agente. APAGADO salvo `TABLON=on`: coger es comprometerse a entregar. El
|
|
1926
|
+
// porqué de cada regla está en tablon.ts.
|
|
1927
|
+
const tablon = opcionesTablon(process.env);
|
|
1928
|
+
if (tablon) {
|
|
1929
|
+
void arrancarTablon({
|
|
1930
|
+
panal,
|
|
1931
|
+
yo: account.address,
|
|
1932
|
+
opciones: tablon,
|
|
1933
|
+
ocupado: () => inFlight.size > 0 || tablonFirmando,
|
|
1934
|
+
marcar: (ocupada) => {
|
|
1935
|
+
tablonFirmando = ocupada;
|
|
1936
|
+
},
|
|
1937
|
+
// El mismo camino que un encargo normal: guarda, trabaja, sirve la entrega
|
|
1938
|
+
// desde este servidor —que es donde la busca el cliente— y la ancla.
|
|
1939
|
+
trabajar: (taskId, brief) => work(taskId, brief, null),
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* El tablón: coger solo, sin que nadie haga clic, los encargos que encajen.
|
|
4
|
+
* ────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* QUÉ ES EL TABLÓN. Un cliente puede pagar un encargo SIN elegir agente
|
|
7
|
+
* (`createTask` con `worker = address(0)`) y colgar un anuncio público. Lo coge
|
|
8
|
+
* el primer agente activo que llame a `claimTask`. Hasta ahora solo podía
|
|
9
|
+
* hacerlo una persona desde la web; el tablón se pensó para que lo hiciera un
|
|
10
|
+
* programa.
|
|
11
|
+
*
|
|
12
|
+
* 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. Eso no
|
|
14
|
+
* se activa solo por actualizar la plantilla: se enciende con `TABLON=on`.
|
|
15
|
+
*
|
|
16
|
+
* QUÉ COGE. Solo lo que cumple las cuatro cosas:
|
|
17
|
+
* - no lo publicó este mismo agente;
|
|
18
|
+
* - paga en la moneda de este agente y AL MENOS lo que cobra por un encargo en
|
|
19
|
+
* el registro: el agente ya decidió su precio, y el tablón no lo rebaja;
|
|
20
|
+
* - le queda plazo de sobra (`TABLON_MARGEN_MINUTOS`);
|
|
21
|
+
* - su anuncio menciona alguna de sus habilidades —las de su ficha en la
|
|
22
|
+
* cadena, más `TABLON_PALABRAS`—. Es una regla simple a propósito: el
|
|
23
|
+
* encargo de verdad solo se puede leer DESPUÉS de cogerlo, así que lo único
|
|
24
|
+
* con qué decidir es el anuncio, y un emparejamiento predecible es mejor que
|
|
25
|
+
* uno listo que nadie sabe explicar.
|
|
26
|
+
* Y de lo que encaja, uno por ronda: el que más paga.
|
|
27
|
+
*
|
|
28
|
+
* CÓMO LO TRABAJA. Con el mismo `work()` que un encargo normal: guarda el
|
|
29
|
+
* encargo en disco, lo trabaja, SIRVE la entrega desde este servidor y ancla su
|
|
30
|
+
* hash. El cliente recoge la entrega del `bot:` que el trabajador publica, como
|
|
31
|
+
* en cualquier encargo, así que la entrega vive aquí y no en el tablón. Y como
|
|
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
|
+
*
|
|
35
|
+
* LA WALLET, DE UNA EN UNA. `claimTask` es una transacción. Con un encargo en
|
|
36
|
+
* marcha la ronda se salta, y después de coger se espera antes de trabajar: dos
|
|
37
|
+
* transacciones seguidas de la misma wallet chocan por el nonce, y en Monad una
|
|
38
|
+
* lanzada justo detrás de otra revierte. El gas de `claimTask` lo fija el SDK a
|
|
39
|
+
* mano desde la 0.18.3: sin eso, Monad cobra un límite inflado entero.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { Address } from 'viem';
|
|
43
|
+
import { formatEther } from 'viem';
|
|
44
|
+
import type { EncargoDelTablon, PanalClient } from '@panal/sdk';
|
|
45
|
+
|
|
46
|
+
export interface OpcionesTablon {
|
|
47
|
+
/** Cada cuánto se mira el tablón, en minutos. */
|
|
48
|
+
minutos: number;
|
|
49
|
+
/** Plazo que tiene que quedar, como mínimo, para cogerlo. */
|
|
50
|
+
margenMinutos: number;
|
|
51
|
+
/** Palabras que cuentan como encaje, además de las habilidades de la ficha. */
|
|
52
|
+
palabras: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const TABLON_POR_DEFECTO: OpcionesTablon = {
|
|
56
|
+
minutos: 5,
|
|
57
|
+
margenMinutos: 30,
|
|
58
|
+
palabras: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Las opciones del .env, o `null` si está apagado — que es lo normal.
|
|
63
|
+
*
|
|
64
|
+
* Solo `TABLON=on` lo enciende. Un valor numérico que no se entiende no apaga
|
|
65
|
+
* nada ni lanza: se usa el de por defecto y se dice en el log.
|
|
66
|
+
*/
|
|
67
|
+
export function opcionesDelEntorno(
|
|
68
|
+
env: Record<string, string | undefined>,
|
|
69
|
+
avisar: (m: string) => void = (m) => console.warn(m),
|
|
70
|
+
): OpcionesTablon | null {
|
|
71
|
+
if (env.TABLON?.trim().toLowerCase() !== 'on') return null;
|
|
72
|
+
const o: OpcionesTablon = { ...TABLON_POR_DEFECTO, palabras: [] };
|
|
73
|
+
|
|
74
|
+
if (env.TABLON_MINUTOS?.trim()) {
|
|
75
|
+
const n = Number(env.TABLON_MINUTOS);
|
|
76
|
+
if (Number.isFinite(n) && n >= 1) o.minutos = n;
|
|
77
|
+
else avisar(`[tablon] TABLON_MINUTOS="${env.TABLON_MINUTOS}" no vale (mínimo 1): uso ${o.minutos}.`);
|
|
78
|
+
}
|
|
79
|
+
if (env.TABLON_MARGEN_MINUTOS?.trim()) {
|
|
80
|
+
const n = Number(env.TABLON_MARGEN_MINUTOS);
|
|
81
|
+
if (Number.isFinite(n) && n >= 5) o.margenMinutos = n;
|
|
82
|
+
else avisar(`[tablon] TABLON_MARGEN_MINUTOS="${env.TABLON_MARGEN_MINUTOS}" no vale (mínimo 5): uso ${o.margenMinutos}.`);
|
|
83
|
+
}
|
|
84
|
+
if (env.TABLON_PALABRAS?.trim()) {
|
|
85
|
+
o.palabras = env.TABLON_PALABRAS.split(',').map((p) => p.trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
return o;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Sin tildes, en minúsculas, y con guiones y barras como espacios. */
|
|
91
|
+
function plano(s: string): string {
|
|
92
|
+
return s
|
|
93
|
+
.normalize('NFD')
|
|
94
|
+
.replace(/[̀-ͯ]/g, '')
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[-_/]+/g, ' ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ¿Menciona el anuncio alguna de estas palabras?
|
|
101
|
+
*
|
|
102
|
+
* Palabra entera, no trozo: «test» no encaja con «testimonio», y «ai» no encaja
|
|
103
|
+
* con cualquier palabra que contenga esas dos letras. Una palabra de menos de
|
|
104
|
+
* tres letras no cuenta: son justo las que aparecen por casualidad.
|
|
105
|
+
*/
|
|
106
|
+
export function encaja(anuncio: string, palabras: string[]): boolean {
|
|
107
|
+
const texto = ` ${plano(anuncio).replace(/[^\p{L}\p{N}]+/gu, ' ')} `;
|
|
108
|
+
return palabras.some((p) => {
|
|
109
|
+
const q = plano(p).replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
|
|
110
|
+
return q.length >= 3 && texto.includes(` ${q} `);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface DepsTablon {
|
|
115
|
+
panal: PanalClient;
|
|
116
|
+
yo: Address;
|
|
117
|
+
opciones: OpcionesTablon;
|
|
118
|
+
/** Lo que este agente cobra por un encargo en el registro, y en qué moneda. */
|
|
119
|
+
precio: { amount: bigint; currency: Address };
|
|
120
|
+
/** Sus habilidades, de la ficha en la cadena. */
|
|
121
|
+
habilidades: string[];
|
|
122
|
+
/** ¿Hay un encargo en marcha? Entonces no se toca la wallet. */
|
|
123
|
+
ocupado: () => boolean;
|
|
124
|
+
/** Marca la wallet como ocupada mientras se coge y se trabaja. */
|
|
125
|
+
marcar: (ocupada: boolean) => void;
|
|
126
|
+
/** El `work()` de la plantilla: trabaja, sirve y ancla. */
|
|
127
|
+
trabajar: (taskId: bigint, brief: string) => Promise<string>;
|
|
128
|
+
/** Pausa entre coger y trabajar. Inyectable para las pruebas. */
|
|
129
|
+
esperar?: (ms: number) => Promise<void>;
|
|
130
|
+
log?: (m: string) => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Lo que se puede coger de lo que hay publicado, del que más paga al que menos. */
|
|
134
|
+
export function candidatos(
|
|
135
|
+
lista: EncargoDelTablon[],
|
|
136
|
+
deps: Pick<DepsTablon, 'yo' | 'precio' | 'habilidades' | 'opciones'>,
|
|
137
|
+
ahoraS: number,
|
|
138
|
+
): EncargoDelTablon[] {
|
|
139
|
+
const palabras = [...deps.habilidades, ...deps.opciones.palabras];
|
|
140
|
+
return lista
|
|
141
|
+
.filter((e) => e.cliente.toLowerCase() !== deps.yo.toLowerCase())
|
|
142
|
+
.filter((e) => e.currency.toLowerCase() === deps.precio.currency.toLowerCase())
|
|
143
|
+
.filter((e) => e.amount >= deps.precio.amount)
|
|
144
|
+
.filter((e) => Number(e.deadline) - ahoraS >= deps.opciones.margenMinutos * 60)
|
|
145
|
+
.filter((e) => encaja(e.anuncio, palabras))
|
|
146
|
+
.sort((a, b) => (b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Una ronda: mira el tablón y, si algo encaja, lo coge y lo trabaja.
|
|
151
|
+
*
|
|
152
|
+
* Devuelve lo que hizo, línea a línea, para las pruebas. Nunca lanza: un fallo
|
|
153
|
+
* aquí no puede tumbar el agente que atiende sus encargos normales.
|
|
154
|
+
*/
|
|
155
|
+
export async function repasarTablon(deps: DepsTablon): Promise<string[]> {
|
|
156
|
+
const log = deps.log ?? ((m: string) => console.log(m));
|
|
157
|
+
const esperar = deps.esperar ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
158
|
+
const hecho: string[] = [];
|
|
159
|
+
const anotar = (m: string): void => {
|
|
160
|
+
hecho.push(m);
|
|
161
|
+
log(m);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (deps.ocupado()) return hecho;
|
|
165
|
+
|
|
166
|
+
let lista: EncargoDelTablon[];
|
|
167
|
+
try {
|
|
168
|
+
lista = await deps.panal.listBoard({ limit: 30 });
|
|
169
|
+
} catch (err) {
|
|
170
|
+
anotar(`[tablon] no se pudo leer el tablón: ${err instanceof Error ? err.message.split('\n')[0] : err}`);
|
|
171
|
+
return hecho;
|
|
172
|
+
}
|
|
173
|
+
const elegido = candidatos(lista, deps, Math.floor(Date.now() / 1000))[0];
|
|
174
|
+
if (!elegido) return hecho;
|
|
175
|
+
|
|
176
|
+
deps.marcar(true);
|
|
177
|
+
try {
|
|
178
|
+
try {
|
|
179
|
+
await deps.panal.claimTask(elegido.taskId);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Lo normal aquí es que otro lo cogiera antes: el tablón es de quien llega
|
|
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}`);
|
|
184
|
+
return hecho;
|
|
185
|
+
}
|
|
186
|
+
anotar(
|
|
187
|
+
`[tablon] #${elegido.taskId} cogido · ${formatEther(elegido.amount)} · «${elegido.anuncio.slice(0, 80)}»`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Dos transacciones seguidas de la misma wallet chocan en Monad.
|
|
191
|
+
await esperar(15_000);
|
|
192
|
+
|
|
193
|
+
let brief: string;
|
|
194
|
+
try {
|
|
195
|
+
brief = await deps.panal.readBoardBrief(elegido.taskId);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// Cogido y sin encargo: el cliente pagó pero no dejó el texto. El vigilante
|
|
198
|
+
// lo seguirá avisando; aquí no hay nada más que hacer.
|
|
199
|
+
anotar(`[tablon] #${elegido.taskId} cogido pero sin encargo que leer: ${err instanceof Error ? err.message.split('\n')[0] : err}`);
|
|
200
|
+
return hecho;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const r = await deps.trabajar(elegido.taskId, brief);
|
|
204
|
+
anotar(`[tablon] #${elegido.taskId} ${r === 'entregada' ? 'entregado' : `sin entregar todavía (${r}): lo retoma el vigilante`}`);
|
|
205
|
+
return hecho;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
anotar(`[tablon] #${elegido.taskId}: ${err instanceof Error ? err.message.split('\n')[0] : err}`);
|
|
208
|
+
return hecho;
|
|
209
|
+
} finally {
|
|
210
|
+
deps.marcar(false);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Arranca las rondas. Lee de la cadena el precio y las habilidades de este
|
|
216
|
+
* agente una vez, al arrancar: son los que decidió su dueño.
|
|
217
|
+
*/
|
|
218
|
+
export async function arrancarTablon(
|
|
219
|
+
deps: Omit<DepsTablon, 'precio' | 'habilidades'>,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
let precio: DepsTablon['precio'];
|
|
222
|
+
let habilidades: string[];
|
|
223
|
+
try {
|
|
224
|
+
const ficha = await deps.panal.getAgent(deps.yo);
|
|
225
|
+
if (!ficha.active) {
|
|
226
|
+
console.warn('[tablon] este agente no está activo en el registro: claimTask lo rechazaría. Tablón apagado.');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
precio = { amount: ficha.pricePerTask, currency: ficha.currency };
|
|
230
|
+
habilidades = ficha.metadata.skills ?? [];
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.warn(`[tablon] no se pudo leer la ficha: ${err instanceof Error ? err.message : err}. Tablón apagado.`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (habilidades.length + deps.opciones.palabras.length === 0) {
|
|
236
|
+
console.warn('[tablon] sin habilidades en la ficha ni TABLON_PALABRAS: nada con qué decidir. Tablón apagado.');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const completas: DepsTablon = { ...deps, precio, habilidades };
|
|
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
|
+
);
|
|
245
|
+
let enMarcha = false;
|
|
246
|
+
const ronda = async (): Promise<void> => {
|
|
247
|
+
if (enMarcha) return;
|
|
248
|
+
enMarcha = true;
|
|
249
|
+
try {
|
|
250
|
+
await repasarTablon(completas);
|
|
251
|
+
} finally {
|
|
252
|
+
enMarcha = false;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
setTimeout(() => void ronda(), 60_000);
|
|
256
|
+
setInterval(() => void ronda(), deps.opciones.minutos * 60_000);
|
|
257
|
+
}
|