create-panal-agent 0.13.1 → 0.14.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 +10 -10
- package/package.json +2 -2
- package/template/_package.json +1 -1
- package/template/src/adjuntos.ts +26 -5
- package/template/src/agent.ts +142 -10
- package/template/src/register.ts +21 -0
- package/template/src/salida.ts +168 -1
- package/template/src/server.ts +173 -20
package/dist/i18n.js
CHANGED
|
@@ -74,7 +74,7 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
|
|
|
74
74
|
rpc: 'Your own RPC, if the public one falls short (it caps at ~15 calls/s).',
|
|
75
75
|
data: 'Where to store delivered results.',
|
|
76
76
|
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`.',
|
|
77
|
-
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.",
|
|
77
|
+
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.",
|
|
78
78
|
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.",
|
|
79
79
|
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.",
|
|
80
80
|
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).",
|
|
@@ -170,7 +170,7 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
|
|
|
170
170
|
rpc: 'RPC propio, si el público se te queda corto (limita a ~15 llamadas/s).',
|
|
171
171
|
data: 'Dónde guardar los resultados entregados.',
|
|
172
172
|
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`.',
|
|
173
|
-
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.",
|
|
173
|
+
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.",
|
|
174
174
|
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.",
|
|
175
175
|
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.",
|
|
176
176
|
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).",
|
|
@@ -267,7 +267,7 @@ const zh = {
|
|
|
267
267
|
rpc: '你自己的 RPC,当公共节点不够用时(其上限约为每秒 15 次调用)。',
|
|
268
268
|
data: '交付结果的存放位置。',
|
|
269
269
|
x402: '按次收费(可选)。留空则你的代理只接受托管订单。\n价格按每次请求计算,使用支持 EIP-2612 的代币——不能是 MON,该方案依赖 `permit`。',
|
|
270
|
-
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。 把它设得低于你每次提问的收费(X402_PRICE):等于或高于该值时,每一笔你转包出去的活都赚不到钱,还要自付 gas——而这恰恰惩罚了你希望它做的事,因为它越善于识别自己不懂的部分,就越经常白干。取你价格的三分之一是个合理的起点。",
|
|
270
|
+
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。 把它设得低于你每次提问的收费(X402_PRICE):等于或高于该值时,每一笔你转包出去的活都赚不到钱,还要自付 gas——而这恰恰惩罚了你希望它做的事,因为它越善于识别自己不懂的部分,就越经常白干。取你价格的三分之一是个合理的起点。 光有数字还不够:还需要 agent.ts 里的 SUBCONTRATA_SKILLS 列表,它规定了可以买什么。默认为空,也就永远不会转包;因为市场搜索在找不到人时会自动泛化,一个请求 \"python video encoding\" 的代码代理最后会搜索 \"video\",把钱付给做视频的代理。",
|
|
271
271
|
vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
|
|
272
272
|
memoria: "对话记忆(仅限 x402)。没有它,每次付费调用都是独立的:客户提问,你回答,下一次你不知道之前在谈什么——那是带账单的搜索框,不是聊天。\nMEMORIA_TURNOS=0 可关闭。两个上限的存在是因为历史会进入提示词,而提示词由你付费,客户只付固定的每条消息价格。",
|
|
273
273
|
seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 前面有反向代理时使用(Caddy、nginx)——这是常态,因为你的端点必须是 https。不设置时所有请求都带着代理的 IP,上面的上限就不再是每个客户端的,而变成由索引器、浏览器和你的客户共用的全局上限;一旦填满,你的代理会对所有人返回 429,而无法发送任务书的客户只能带着已锁定的付款等到期限。没有代理时请保持关闭:无代理却信任该请求头,任何人都能为每个请求伪造 IP。 AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
|
|
@@ -359,7 +359,7 @@ const hi = {
|
|
|
359
359
|
rpc: 'अपना RPC, अगर सार्वजनिक कम पड़े (इसकी सीमा ~15 कॉल/सेकंड है)।',
|
|
360
360
|
data: 'दिए गए परिणाम कहाँ सहेजें।',
|
|
361
361
|
x402: 'प्रति कॉल शुल्क (वैकल्पिक)। खाली छोड़ें तो आपका एजेंट केवल एस्क्रो वाले काम लेगा।\nकीमत प्रति अनुरोध है, EIP-2612 टोकन में — MON नहीं चल सकता, इस योजना को `permit` चाहिए।',
|
|
362
|
-
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है। इसे अपने प्रति-प्रश्न शुल्क (X402_PRICE) से कम रखें: बराबर या अधिक होने पर, जिस भी काम में आप दूसरे को सौंपेंगे उसमें कुछ नहीं बचेगा और गैस भी आपकी जेब से जाएगी — और यह ठीक उसी व्यवहार को दंडित करता है जो आप चाहते हैं, क्योंकि आपका एजेंट जितना बेहतर पहचानेगा कि उसे क्या नहीं आता, उतनी ही बार वह मुफ़्त काम करेगा। अपने दाम का एक तिहाई एक समझदार शुरुआत है।",
|
|
362
|
+
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है। इसे अपने प्रति-प्रश्न शुल्क (X402_PRICE) से कम रखें: बराबर या अधिक होने पर, जिस भी काम में आप दूसरे को सौंपेंगे उसमें कुछ नहीं बचेगा और गैस भी आपकी जेब से जाएगी — और यह ठीक उसी व्यवहार को दंडित करता है जो आप चाहते हैं, क्योंकि आपका एजेंट जितना बेहतर पहचानेगा कि उसे क्या नहीं आता, उतनी ही बार वह मुफ़्त काम करेगा। अपने दाम का एक तिहाई एक समझदार शुरुआत है। सिर्फ़ संख्या काफ़ी नहीं: agent.ts में SUBCONTRATA_SKILLS सूची भी चाहिए, जो बताती है कि वह क्या खरीद सकता है। खाली — जैसा यह आता है — तो वह कभी नहीं सौंपता, क्योंकि कोई न मिलने पर बाज़ार की खोज सामान्यीकरण कर देती है, और \"python video encoding\" माँगने वाला कोड एजेंट अंत में \"video\" खोजकर वीडियो बनाने वाले को भुगतान कर देगा।",
|
|
363
363
|
vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
|
|
364
364
|
memoria: "बातचीत की स्मृति (केवल x402)। इसके बिना हर सशुल्क कॉल स्वतंत्र होती है: ग्राहक पूछता है, आप उत्तर देते हैं, और अगली बार आपको पता नहीं होता कि बात किस बारे में थी — वह चैट नहीं, बिल वाला सर्च बॉक्स है।\nMEMORIA_TURNOS=0 इसे बंद कर देता है। दोनों सीमाएँ इसलिए हैं कि इतिहास प्रॉम्प्ट में जाता है, और प्रॉम्प्ट का भुगतान आप करते हैं जबकि ग्राहक प्रति संदेश निश्चित मूल्य देता है।",
|
|
365
365
|
seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 अगर आगे रिवर्स प्रॉक्सी हो (Caddy, nginx) — यही सामान्य है, क्योंकि आपका एंडपॉइंट https होना चाहिए। इसके बिना हर अनुरोध प्रॉक्सी के IP के साथ आता है, इसलिए ऊपर की सीमा प्रति-क्लाइंट नहीं रहती और एक वैश्विक सीमा बन जाती है जिसे इंडेक्सर, ब्राउज़र और आपके क्लाइंट साझा करते हैं; भर जाने पर आपका एजेंट सबको 429 देता है, और जो क्लाइंट अपना ब्रीफ़ नहीं भेज पाता वह भुगतान लॉक होने के बावजूद समय-सीमा तक इंतज़ार करता है। प्रॉक्सी न हो तो बंद रखें: बिना प्रॉक्सी उस हेडर पर भरोसा करने से कोई भी हर अनुरोध में मनमाना IP भेज सकता है। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
|
|
@@ -455,7 +455,7 @@ const ar = {
|
|
|
455
455
|
rpc: 'عقدة RPC خاصة بك، إن لم تكفِ العامة (حدّها نحو 15 طلبًا في الثانية).',
|
|
456
456
|
data: 'مكان حفظ النتائج المُسلَّمة.',
|
|
457
457
|
x402: 'التحصيل لكل استدعاء (اختياري). اتركه فارغًا فيقبل وكيلك مهام الضمان فقط.\nالسعر لكل طلب، بعملة تدعم EIP-2612 — لا يصلح MON، فالمخطط يحتاج `permit`.',
|
|
458
|
-
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL. اجعله أقل من سعر السؤال لديك (X402_PRICE): فعند التساوي أو الزيادة، كل مهمة تُسنِد فيها جزءًا إلى غيرك لا تُبقي لك شيئًا وتدفع الغاز من جيبك — وهذا يعاقب تحديدًا ما تريده منه، لأنه كلما أحسن إدراك ما يجهله عمل مجانًا أكثر. ثلث سعرك بداية معقولة.",
|
|
458
|
+
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL. اجعله أقل من سعر السؤال لديك (X402_PRICE): فعند التساوي أو الزيادة، كل مهمة تُسنِد فيها جزءًا إلى غيرك لا تُبقي لك شيئًا وتدفع الغاز من جيبك — وهذا يعاقب تحديدًا ما تريده منه، لأنه كلما أحسن إدراك ما يجهله عمل مجانًا أكثر. ثلث سعرك بداية معقولة. الرقم وحده لا يكفي: تحتاج أيضًا إلى قائمة SUBCONTRATA_SKILLS في agent.ts، التي تحدد ما الذي يُسمح بشرائه. وهي فارغة كما تأتي، فلا يفوّض أبدًا؛ لأن بحث السوق يعمّم عندما لا يجد أحدًا، فوكيل برمجة يطلب \"python video encoding\" ينتهي به الأمر باحثًا عن \"video\" ودافعًا لوكيل يصنع الفيديو.",
|
|
459
459
|
vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
|
|
460
460
|
memoria: "ذاكرة المحادثة (في x402 فقط). بدونها تكون كل مكالمة مدفوعة مستقلة: يسأل العميل، تجيب، وفي المرة التالية لا تعرف عمّا كان الحديث — هذا مربع بحث بفاتورة، لا محادثة.\nالقيمة MEMORIA_TURNOS=0 تُعطّلها. الحدّان موجودان لأن السجل يدخل في التوجيه، والتوجيه تدفعه أنت بينما يدفع العميل سعرًا ثابتًا لكل رسالة.",
|
|
461
461
|
seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 إذا كان أمامه بروكسي عكسي (Caddy أو nginx) — وهو الوضع المعتاد لأن نقطة النهاية يجب أن تكون https. بدونه تصل كل الطلبات بعنوان IP الخاص بالبروكسي، فيتوقف الحد أعلاه عن كونه لكل عميل ويصبح حدًا عامًا واحدًا يتقاسمه المفهرس والمتصفحات وعملاؤك؛ وعند امتلائه يرد وكيلك بـ 429 على الجميع، ويظل العميل الذي تعذر عليه إرسال مهمته ينتظر انتهاء المهلة ودفعته محجوزة بالفعل. بلا بروكسي اتركه معطلًا: الوثوق بهذه الترويسة دون بروكسي يتيح لأي شخص اختلاق عنوان IP لكل طلب. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
|
|
@@ -550,7 +550,7 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
|
|
|
550
550
|
rpc: 'Votre propre RPC, si le public ne suffit pas (limité à ~15 appels/s).',
|
|
551
551
|
data: 'Où stocker les résultats livrés.',
|
|
552
552
|
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`.",
|
|
553
|
-
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.",
|
|
553
|
+
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.",
|
|
554
554
|
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.",
|
|
555
555
|
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.",
|
|
556
556
|
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.",
|
|
@@ -649,7 +649,7 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
|
|
|
649
649
|
rpc: 'RPC próprio, se o público não chegar (limita a ~15 chamadas/s).',
|
|
650
650
|
data: 'Onde guardar os resultados entregues.',
|
|
651
651
|
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`.',
|
|
652
|
-
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.",
|
|
652
|
+
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.",
|
|
653
653
|
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.",
|
|
654
654
|
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.",
|
|
655
655
|
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.",
|
|
@@ -746,7 +746,7 @@ const ru = {
|
|
|
746
746
|
rpc: 'Свой RPC, если публичного не хватает (лимит ~15 запросов/с).',
|
|
747
747
|
data: 'Где хранить сданные результаты.',
|
|
748
748
|
x402: 'Плата за вызов (необязательно). Оставьте пустым — агент будет брать только заказы через эскроу.\nЦена за один запрос, в токене с EIP-2612: MON не подходит, схеме нужен `permit`.',
|
|
749
|
-
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL. Поставьте НИЖЕ того, что берёте за вопрос (X402_PRICE): при равенстве или выше каждая работа, где вы делегируете, не приносит ничего, а газ вы платите сами — и это наказывает ровно то поведение, которого вы добиваетесь: чем лучше агент понимает, чего не знает, тем чаще работает даром. Треть вашей цены — разумное начало.",
|
|
749
|
+
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL. Поставьте НИЖЕ того, что берёте за вопрос (X402_PRICE): при равенстве или выше каждая работа, где вы делегируете, не приносит ничего, а газ вы платите сами — и это наказывает ровно то поведение, которого вы добиваетесь: чем лучше агент понимает, чего не знает, тем чаще работает даром. Треть вашей цены — разумное начало. Одного числа мало: нужен ещё список SUBCONTRATA_SKILLS в agent.ts, который говорит, ЧТО можно покупать. Пустой — а таким он и поставляется — и агент никогда не делегирует, потому что поиск по рынку обобщает запрос, если никого не нашёл: агент по коду, попросивший \"python video encoding\", в итоге станет искать \"video\" и заплатит тому, кто делает видео.",
|
|
750
750
|
vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
|
|
751
751
|
memoria: "Память разговора (только x402). Без неё каждый оплаченный вызов независим: клиент спрашивает, вы отвечаете, а в следующий раз вы не знаете, о чём шла речь — это поисковая строка со счётом, а не чат.\nMEMORIA_TURNOS=0 отключает её. Оба предела существуют потому, что история попадает В промпт, а промпт оплачиваете вы, тогда как клиент платит фиксированную цену за сообщение.",
|
|
752
752
|
seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 если впереди стоит обратный прокси (Caddy, nginx) — обычный случай, ведь ваш эндпоинт должен быть https. Без этого все запросы приходят с IP прокси, поэтому лимит выше перестаёт быть на клиента и становится единым глобальным, который делят индексатор, браузеры и ваши клиенты; когда он заполняется, ваш агент отвечает 429 всем, а клиент, не сумевший отправить задание, ждёт истечения срока с уже заблокированной оплатой. Без прокси оставьте выключенным: доверять этому заголовку без прокси — значит позволить подделывать IP в каждом запросе. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
|
|
@@ -844,7 +844,7 @@ const bn = {
|
|
|
844
844
|
rpc: 'নিজস্ব RPC, যদি সর্বজনীনটি যথেষ্ট না হয় (সীমা প্রায় ১৫ কল/সেকেন্ড)।',
|
|
845
845
|
data: 'সরবরাহ করা ফলাফল কোথায় রাখা হবে।',
|
|
846
846
|
x402: 'প্রতি কলে চার্জ (ঐচ্ছিক)। ফাঁকা রাখলে আপনার এজেন্ট কেবল এসক্রো কাজ নেবে।\nদাম প্রতি অনুরোধে, EIP-2612 টোকেনে — MON চলবে না, স্কিমটির `permit` দরকার।',
|
|
847
|
-
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ। এটি আপনার প্রতি-প্রশ্ন মূল্যের (X402_PRICE) চেয়ে কম রাখুন: সমান বা বেশি হলে, যে কাজেই আপনি অন্যকে দেবেন তাতে কিছুই থাকবে না, গ্যাসও আপনার — আর এতে ঠিক সেই আচরণই শাস্তি পায় যা আপনি চান, কারণ এজেন্ট যত ভালোভাবে বুঝবে সে কী জানে না, তত বেশিবার সে বিনামূল্যে কাজ করবে। আপনার দামের এক-তৃতীয়াংশ একটি যুক্তিসঙ্গত শুরু।",
|
|
847
|
+
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ। এটি আপনার প্রতি-প্রশ্ন মূল্যের (X402_PRICE) চেয়ে কম রাখুন: সমান বা বেশি হলে, যে কাজেই আপনি অন্যকে দেবেন তাতে কিছুই থাকবে না, গ্যাসও আপনার — আর এতে ঠিক সেই আচরণই শাস্তি পায় যা আপনি চান, কারণ এজেন্ট যত ভালোভাবে বুঝবে সে কী জানে না, তত বেশিবার সে বিনামূল্যে কাজ করবে। আপনার দামের এক-তৃতীয়াংশ একটি যুক্তিসঙ্গত শুরু। শুধু সংখ্যাই যথেষ্ট নয়: agent.ts-এ SUBCONTRATA_SKILLS তালিকাও লাগে, যা বলে দেয় সে কী কিনতে পারবে। খালি — যেমনটা আসে — হলে সে কখনও দায়িত্ব দেয় না, কারণ কাউকে না পেলে বাজারের খোঁজ সাধারণীকরণ করে, আর \"python video encoding\" চাওয়া কোড এজেন্ট শেষে \"video\" খুঁজে ভিডিও বানায় এমন কাউকে টাকা দিয়ে বসবে।",
|
|
848
848
|
vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
|
|
849
849
|
memoria: "কথোপকথনের স্মৃতি (কেবল x402)। এটি ছাড়া প্রতিটি অর্থপ্রদত্ত কল স্বতন্ত্র: ক্লায়েন্ট জিজ্ঞাসা করে, আপনি উত্তর দেন, আর পরেরবার আপনি জানেন না কী নিয়ে কথা হচ্ছিল — সেটা চ্যাট নয়, বিলসহ সার্চ বক্স।\nMEMORIA_TURNOS=0 এটি বন্ধ করে। দুটি সীমা আছে কারণ ইতিহাস প্রম্পটে ঢোকে, আর প্রম্পটের খরচ আপনি দেন যখন ক্লায়েন্ট বার্তাপ্রতি নির্দিষ্ট দাম দেয়।",
|
|
850
850
|
seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 যদি সামনে রিভার্স প্রক্সি থাকে (Caddy, nginx) — এটিই স্বাভাবিক, কারণ আপনার এন্ডপয়েন্ট https হতেই হবে। এটি ছাড়া প্রতিটি অনুরোধ প্রক্সির IP নিয়ে আসে, ফলে উপরের সীমা আর প্রতি-ক্লায়েন্ট থাকে না, বরং একটি বৈশ্বিক সীমা হয়ে যায় যা ইনডেক্সার, ব্রাউজার ও আপনার ক্লায়েন্টরা ভাগ করে; ভরে গেলে আপনার এজেন্ট সবাইকে 429 দেয়, আর যে ক্লায়েন্ট তার কাজের বিবরণ পাঠাতে পারে না সে অর্থ আটকে থাকা অবস্থায় সময়সীমা পর্যন্ত অপেক্ষা করে। প্রক্সি না থাকলে বন্ধ রাখুন: প্রক্সি ছাড়া ওই হেডারে ভরসা করলে যে কেউ প্রতি অনুরোধে IP বানিয়ে দিতে পারে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
|
|
@@ -939,7 +939,7 @@ const ur = {
|
|
|
939
939
|
rpc: 'اپنا RPC، اگر عوامی کم پڑ جائے (حد تقریباً 15 کالز فی سیکنڈ)۔',
|
|
940
940
|
data: 'فراہم کردہ نتائج کہاں محفوظ ہوں۔',
|
|
941
941
|
x402: 'فی کال وصولی (اختیاری)۔ خالی چھوڑ دیں تو آپ کا ایجنٹ صرف ایسکرو کے کام لے گا۔\nقیمت فی درخواست ہے، EIP-2612 ٹوکن میں — MON نہیں چل سکتا، اس اسکیم کو `permit` چاہیے۔',
|
|
942
|
-
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔ اسے اپنی فی سوال قیمت (X402_PRICE) سے کم رکھیں: برابر یا زیادہ ہونے پر، جس کام میں بھی آپ کسی اور کو دیں گے اس میں کچھ نہیں بچے گا اور گیس بھی آپ کی جیب سے جائے گی — اور یہ بالکل اسی رویّے کو سزا دیتا ہے جو آپ چاہتے ہیں، کیونکہ آپ کا ایجنٹ جتنا بہتر پہچانے گا کہ اسے کیا نہیں آتا، اتنی ہی زیادہ بار وہ مفت کام کرے گا۔ اپنی قیمت کا ایک تہائی معقول آغاز ہے۔",
|
|
942
|
+
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔ اسے اپنی فی سوال قیمت (X402_PRICE) سے کم رکھیں: برابر یا زیادہ ہونے پر، جس کام میں بھی آپ کسی اور کو دیں گے اس میں کچھ نہیں بچے گا اور گیس بھی آپ کی جیب سے جائے گی — اور یہ بالکل اسی رویّے کو سزا دیتا ہے جو آپ چاہتے ہیں، کیونکہ آپ کا ایجنٹ جتنا بہتر پہچانے گا کہ اسے کیا نہیں آتا، اتنی ہی زیادہ بار وہ مفت کام کرے گا۔ اپنی قیمت کا ایک تہائی معقول آغاز ہے۔ صرف عدد کافی نہیں: agent.ts میں SUBCONTRATA_SKILLS کی فہرست بھی چاہیے، جو بتاتی ہے کہ وہ کیا خرید سکتا ہے۔ خالی — جیسا کہ یہ آتا ہے — تو وہ کبھی کام نہیں سونپتا، کیونکہ کوئی نہ ملنے پر مارکیٹ کی تلاش عام کر دیتی ہے، اور \"python video encoding\" مانگنے والا کوڈ ایجنٹ آخر میں \"video\" ڈھونڈ کر ویڈیو بنانے والے کو ادائیگی کر بیٹھے گا۔",
|
|
943
943
|
vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
|
|
944
944
|
memoria: "گفتگو کی یادداشت (صرف x402)۔ اس کے بغیر ہر ادا شدہ کال الگ ہوتی ہے: کلائنٹ پوچھتا ہے، آپ جواب دیتے ہیں، اور اگلی بار آپ کو معلوم نہیں ہوتا کہ بات کس بارے میں تھی — یہ چیٹ نہیں، بل والا سرچ باکس ہے۔\nMEMORIA_TURNOS=0 اسے بند کر دیتا ہے۔ دونوں حدیں اس لیے ہیں کہ تاریخ پرامپٹ میں جاتی ہے، اور پرامپٹ کا خرچ آپ اٹھاتے ہیں جبکہ کلائنٹ فی پیغام مقررہ قیمت دیتا ہے۔",
|
|
945
945
|
seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 اگر آگے ریورس پراکسی ہو (Caddy، nginx) — یہی معمول ہے، کیونکہ آپ کا اینڈ پوائنٹ https ہونا چاہیے۔ اس کے بغیر ہر درخواست پراکسی کے IP کے ساتھ آتی ہے، چنانچہ اوپر والی حد فی کلائنٹ نہیں رہتی بلکہ ایک عالمی حد بن جاتی ہے جسے انڈیکسر، براؤزر اور آپ کے کلائنٹ مل کر استعمال کرتے ہیں؛ بھر جانے پر آپ کا ایجنٹ سب کو 429 دیتا ہے، اور جو کلائنٹ اپنی ہدایت نہیں بھیج پاتا وہ ادائیگی مقفل ہونے کے باوجود مہلت تک انتظار کرتا ہے۔ پراکسی نہ ہو تو بند رکھیں: پراکسی کے بغیر اس ہیڈر پر بھروسہ کرنے سے کوئی بھی ہر درخواست میں IP گھڑ سکتا ہے۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-panal-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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/memoria.test.ts && tsx test/reintento.test.ts && tsx test/zip.test.ts && tsx test/adjuntos.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/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"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/template/_package.json
CHANGED
package/template/src/adjuntos.ts
CHANGED
|
@@ -47,9 +47,22 @@ export interface AdjuntosLeidos {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Tope de caracteres que aporta UN adjunto. El encargo lo paga el agente. */
|
|
50
|
-
const MAX_CHARS_POR_ADJUNTO = 8_000;
|
|
50
|
+
export const MAX_CHARS_POR_ADJUNTO = 8_000;
|
|
51
51
|
/** Y el de todos juntos, porque cinco archivos al tope son demasiado. */
|
|
52
|
-
const MAX_CHARS_TOTAL = 24_000;
|
|
52
|
+
export const MAX_CHARS_TOTAL = 24_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cuánto texto se deja entrar, si el nivel pagado da para más.
|
|
56
|
+
*
|
|
57
|
+
* Los topes de arriba son los de siempre y siguen siendo el valor por defecto.
|
|
58
|
+
* Un agente que venda un nivel «resúmeme este libro» necesita subirlos para
|
|
59
|
+
* ese encargo y sólo para ese: recortar un libro a 8 000 caracteres y cobrarlo
|
|
60
|
+
* igual es exactamente lo que no puede pasar.
|
|
61
|
+
*/
|
|
62
|
+
export interface TopesTexto {
|
|
63
|
+
porAdjunto?: number;
|
|
64
|
+
total?: number;
|
|
65
|
+
}
|
|
53
66
|
/** Archivos que se miran dentro de un ZIP. */
|
|
54
67
|
const MAX_DENTRO_DEL_ZIP = 40;
|
|
55
68
|
|
|
@@ -263,15 +276,23 @@ function acotar(texto: string, tope: number): string {
|
|
|
263
276
|
* Es `async` porque leer un PDF lo es. Los agentes ya trabajaban en `async`,
|
|
264
277
|
* así que lo único que cambia para quien llama es un `await`.
|
|
265
278
|
*/
|
|
266
|
-
export async function leerAdjuntos(
|
|
279
|
+
export async function leerAdjuntos(
|
|
280
|
+
adjuntos: AdjuntoRecibido[],
|
|
281
|
+
topes: TopesTexto = {},
|
|
282
|
+
): Promise<AdjuntosLeidos> {
|
|
267
283
|
const partes: string[] = [];
|
|
268
284
|
const imagenes: ImagenAdjunta[] = [];
|
|
269
285
|
let gastado = 0;
|
|
270
286
|
|
|
287
|
+
// Nunca por debajo del de siempre: un nivel mal declarado puede subir esto,
|
|
288
|
+
// no bajarlo por sorpresa a quien ya contaba con los 8 000 de toda la vida.
|
|
289
|
+
const porAdjunto = Math.max(topes.porAdjunto ?? 0, MAX_CHARS_POR_ADJUNTO);
|
|
290
|
+
const total = Math.max(topes.total ?? 0, MAX_CHARS_TOTAL);
|
|
291
|
+
|
|
271
292
|
const anadir = (cabecera: string, cuerpo: string): void => {
|
|
272
|
-
const queda =
|
|
293
|
+
const queda = total - gastado;
|
|
273
294
|
if (queda <= 200) return;
|
|
274
|
-
const trozo = acotar(cuerpo, Math.min(
|
|
295
|
+
const trozo = acotar(cuerpo, Math.min(porAdjunto, queda));
|
|
275
296
|
gastado += trozo.length;
|
|
276
297
|
partes.push(`--- ${cabecera} ---\n${trozo}\n--- fin ---`);
|
|
277
298
|
};
|
package/template/src/agent.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { llmChat, resolverLlm, type CallEnvelope, type LlmConfig } from '@panal/sdk';
|
|
15
15
|
import { leerAdjuntos, type AdjuntoRecibido, type AdjuntosLeidos } from './adjuntos.js';
|
|
16
|
-
import { comoArchivo, formatoPedido } from './salida.js';
|
|
16
|
+
import { comoArchivo, formatoPedido, nombreDelTema } from './salida.js';
|
|
17
17
|
import { historialComoTexto, type Turno } from './memoria.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -37,6 +37,18 @@ export interface TaskContext {
|
|
|
37
37
|
/** Fecha límite de entrega, en segundos epoch. Cero en una llamada x402. */
|
|
38
38
|
deadline: bigint;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* QUÉ NIVEL COMPRÓ, si es que ofreces niveles.
|
|
42
|
+
*
|
|
43
|
+
* `null` es lo normal y significa que este agente no los ofrece: entonces
|
|
44
|
+
* todo funciona como siempre y no hay nada que mirar aquí.
|
|
45
|
+
*
|
|
46
|
+
* Lo decide `amount`, que está en la cadena, y NUNCA el texto del encargo:
|
|
47
|
+
* el brief lo escribe el cliente y podría proclamarse del nivel más caro.
|
|
48
|
+
* El motor ya lo ha resuelto por ti antes de llamarte.
|
|
49
|
+
*/
|
|
50
|
+
nivel: NivelPropio | null;
|
|
51
|
+
|
|
40
52
|
/**
|
|
41
53
|
* LO QUE EL CLIENTE TE ADJUNTÓ: una foto, un PDF que revisar, un CSV.
|
|
42
54
|
*
|
|
@@ -107,6 +119,110 @@ export interface TaskContext {
|
|
|
107
119
|
historial: Turno[];
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Un nivel de servicio: cuánto cobras y cuánto aceptas a cambio.
|
|
124
|
+
*
|
|
125
|
+
* Los topes van EN CARACTERES porque es lo que el cliente puede contar antes
|
|
126
|
+
* de pagar y cualquiera puede recontar después: el encargo se ancla en la
|
|
127
|
+
* cadena y el tamaño de cada adjunto viaja dentro de su manifiesto. Un nivel
|
|
128
|
+
* que prometiera «más esfuerzo» no habría manera de comprobarlo.
|
|
129
|
+
*/
|
|
130
|
+
export interface NivelPropio {
|
|
131
|
+
/** Cómo se llama en el escaparate. */
|
|
132
|
+
name: string;
|
|
133
|
+
/** Una línea de qué compra. */
|
|
134
|
+
description?: string;
|
|
135
|
+
/** Lo que tiene que bloquear en el escrow. `parseEther('0.3')` para MON. */
|
|
136
|
+
wei: bigint;
|
|
137
|
+
/** Tope del encargo en este nivel. Sin él, el de siempre. */
|
|
138
|
+
maxBriefChars?: number;
|
|
139
|
+
/** Cuánto texto aporta CADA adjunto en este nivel. */
|
|
140
|
+
maxAttachChars?: number;
|
|
141
|
+
/** Y todos juntos. */
|
|
142
|
+
maxAttachCharsTotal?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Cuánto puede gastar tu agente SUBCONTRATANDO en este nivel.
|
|
145
|
+
*
|
|
146
|
+
* Sin esto, el de `SUBCONTRATA_MAX` del .env para todos los niveles por
|
|
147
|
+
* igual, que es lo que había antes. Ponerlo aquí es lo que permite que el
|
|
148
|
+
* nivel caro pueda comprar ayuda y el barato no.
|
|
149
|
+
*
|
|
150
|
+
* Que sea MENOR que `wei`, y con holgura: es dinero que sale de lo que te
|
|
151
|
+
* pagaron por este encargo. El motor avisa al arrancar si no lo es.
|
|
152
|
+
*/
|
|
153
|
+
subcontrata?: bigint;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* LOS NIVELES QUE VENDES. Vacío = no vendes niveles, que es lo normal.
|
|
158
|
+
*
|
|
159
|
+
* El registro guarda UN precio por agente, así que si quieres cobrar distinto
|
|
160
|
+
* por trabajos de distinto tamaño, es aquí. Y el primero debería costar lo
|
|
161
|
+
* mismo que tu `pricePerTask` registrado: es el que compra quien te contrata
|
|
162
|
+
* sin elegir nada, desde un cliente antiguo o desde una integración.
|
|
163
|
+
*
|
|
164
|
+
* En cuanto declaras uno, el motor deja de aceptar encargos por debajo del más
|
|
165
|
+
* barato. Eso es un cambio de comportamiento y es a propósito: los niveles no
|
|
166
|
+
* significan nada si se puede pagar el pequeño y mandar el grande.
|
|
167
|
+
*
|
|
168
|
+
* Ejemplo, para un agente que también resume libros (`parseEther` sale de
|
|
169
|
+
* viem, que ya es dependencia: `import { parseEther } from 'viem';`):
|
|
170
|
+
*
|
|
171
|
+
* export const NIVELES: NivelPropio[] = [
|
|
172
|
+
* { name: 'Encargo', wei: parseEther('0.1'), maxBriefChars: 32_000 },
|
|
173
|
+
* {
|
|
174
|
+
* name: 'Libro',
|
|
175
|
+
* description: 'Hasta unas 300 páginas, en el encargo o adjuntas.',
|
|
176
|
+
* wei: parseEther('0.3'),
|
|
177
|
+
* maxBriefChars: 320_000,
|
|
178
|
+
* maxAttachChars: 280_000,
|
|
179
|
+
* maxAttachCharsTotal: 320_000,
|
|
180
|
+
* },
|
|
181
|
+
* ];
|
|
182
|
+
*/
|
|
183
|
+
export const NIVELES: NivelPropio[] = [];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* LAS ÚNICAS SKILLS QUE TU AGENTE PUEDE COMPRARLE A OTRO. Vacío = ninguna.
|
|
187
|
+
*
|
|
188
|
+
* Vacío es el valor por defecto y significa que NO SUBCONTRATA, aunque tenga
|
|
189
|
+
* presupuesto. Hacen falta las dos cosas: dinero y permiso.
|
|
190
|
+
*
|
|
191
|
+
* Existe porque quien elige la skill es un modelo, y el buscador generaliza
|
|
192
|
+
* cuando no encuentra a nadie: recorta por la izquierda, así que
|
|
193
|
+
* "python video encoding" acaba buscando "video". Un agente de código puede
|
|
194
|
+
* así pagarle a uno de vídeo, y lo malo es que el resultado PARECE correcto —
|
|
195
|
+
* pagó, le contestaron, entregó y ancló. Nadie ve un error; sólo la entrega
|
|
196
|
+
* es peor y el dinero se fue.
|
|
197
|
+
*
|
|
198
|
+
* La lista se comprueba en tres sitios, a propósito: se le dice al modelo qué
|
|
199
|
+
* puede pedir, se comprueba lo que contesta, y el SDK descarta cualquier
|
|
200
|
+
* variante que no esté aquí ANTES de cotizar. Un intento prohibido no cuesta
|
|
201
|
+
* ni una petición.
|
|
202
|
+
*
|
|
203
|
+
* export const SUBCONTRATA_SKILLS = ['translation', 'legal'];
|
|
204
|
+
*
|
|
205
|
+
* Escríbelas como se venden en el mercado: una o dos palabras.
|
|
206
|
+
*/
|
|
207
|
+
export const SUBCONTRATA_SKILLS: string[] = [];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* ¿Puede este agente comprar esta skill?
|
|
211
|
+
*
|
|
212
|
+
* Va aparte y exportada porque es la regla que decide si sale dinero, y una
|
|
213
|
+
* regla así tiene que poder probarse sin levantar un agente. Compara sin
|
|
214
|
+
* distinguir mayúsculas ni espacios de más: la lista la escribe una persona y
|
|
215
|
+
* la skill la escribe un modelo.
|
|
216
|
+
*
|
|
217
|
+
* Con la lista vacía devuelve `false` para todo. Vacío es NO DELEGA, nunca
|
|
218
|
+
* «todo permitido»: la diferencia entre las dos lecturas se mide en dinero.
|
|
219
|
+
*/
|
|
220
|
+
export function skillPermitida(skill: string, permitidas: string[]): boolean {
|
|
221
|
+
const limpia = skill.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
222
|
+
if (!limpia) return false;
|
|
223
|
+
return permitidas.some((p) => p.trim().replace(/\s+/g, ' ').toLowerCase() === limpia);
|
|
224
|
+
}
|
|
225
|
+
|
|
110
226
|
/** Un archivo que entregas junto al texto. */
|
|
111
227
|
export interface TaskFile {
|
|
112
228
|
/** Cómo se va a llamar. Sin rutas: `informe.pdf`, no `salida/informe.pdf`. */
|
|
@@ -178,7 +294,10 @@ export async function handleTask(brief: string, ctx: TaskContext): Promise<TaskR
|
|
|
178
294
|
|
|
179
295
|
// Se abre TODO lo que mandó el cliente: imágenes para que las mire el
|
|
180
296
|
// modelo, y el texto de los PDF, Word, Excel y carpetas que vengan dentro.
|
|
181
|
-
const leido = await leerAdjuntos(ctx.adjuntos
|
|
297
|
+
const leido = await leerAdjuntos(ctx.adjuntos, {
|
|
298
|
+
...(ctx.nivel?.maxAttachChars === undefined ? {} : { porAdjunto: ctx.nivel.maxAttachChars }),
|
|
299
|
+
...(ctx.nivel?.maxAttachCharsTotal === undefined ? {} : { total: ctx.nivel.maxAttachCharsTotal }),
|
|
300
|
+
});
|
|
182
301
|
if (ctx.adjuntos.length > 0) {
|
|
183
302
|
console.log(
|
|
184
303
|
`[agente] ${etiqueta(ctx)} ${ctx.adjuntos.length} adjunto(s), ${leido.imagenes.length} para el modelo: ` +
|
|
@@ -199,7 +318,7 @@ export async function handleTask(brief: string, ctx: TaskContext): Promise<TaskR
|
|
|
199
318
|
const problema = revisar(brief, texto);
|
|
200
319
|
if (!problema) {
|
|
201
320
|
console.log(`[agente] ${etiqueta(ctx)} resuelta: ${texto.length} caracteres`);
|
|
202
|
-
return conArchivoSiLoPidio(brief, texto, ctx);
|
|
321
|
+
return await conArchivoSiLoPidio(brief, texto, ctx);
|
|
203
322
|
}
|
|
204
323
|
console.error(`[agente] ${etiqueta(ctx)} intento ${intento}: ${problema}`);
|
|
205
324
|
// A la segunda se entrega igual. Tu revisión puede equivocarse, y un falso
|
|
@@ -207,7 +326,7 @@ export async function handleTask(brief: string, ctx: TaskContext): Promise<TaskR
|
|
|
207
326
|
// entregar algo imperfecto y que él decida, que dejarlo sin nada.
|
|
208
327
|
if (intento === 2) {
|
|
209
328
|
console.error(`[agente] ${etiqueta(ctx)} se entrega pese a: ${problema}`);
|
|
210
|
-
return conArchivoSiLoPidio(brief, texto, ctx);
|
|
329
|
+
return await conArchivoSiLoPidio(brief, texto, ctx);
|
|
211
330
|
}
|
|
212
331
|
queja = problema;
|
|
213
332
|
}
|
|
@@ -233,10 +352,11 @@ async function pedirAyudaSiHaceFalta(
|
|
|
233
352
|
ctx: TaskContext,
|
|
234
353
|
cfg: LlmConfig,
|
|
235
354
|
): Promise<string | null> {
|
|
236
|
-
// Sin presupuesto no hay nada que decidir, y así se ahorra la
|
|
237
|
-
|
|
355
|
+
// Sin presupuesto o sin permiso no hay nada que decidir, y así se ahorra la
|
|
356
|
+
// llamada al modelo. Las dos condiciones hacen falta.
|
|
357
|
+
if (ctx.presupuesto <= 0n || SUBCONTRATA_SKILLS.length === 0) return null;
|
|
238
358
|
|
|
239
|
-
const decision = await decidirDelegacion(brief, cfg);
|
|
359
|
+
const decision = await decidirDelegacion(brief, cfg, SUBCONTRATA_SKILLS);
|
|
240
360
|
if (!decision) return null;
|
|
241
361
|
|
|
242
362
|
try {
|
|
@@ -280,6 +400,7 @@ function extraerJson(crudo: string): unknown {
|
|
|
280
400
|
async function decidirDelegacion(
|
|
281
401
|
brief: string,
|
|
282
402
|
cfg: LlmConfig,
|
|
403
|
+
permitidas: string[],
|
|
283
404
|
): Promise<{ skill: string; pregunta: string } | null> {
|
|
284
405
|
try {
|
|
285
406
|
const crudo = await llmChat(
|
|
@@ -293,7 +414,12 @@ async function decidirDelegacion(
|
|
|
293
414
|
'{"delegate": false} if you can do the job yourself. This is the right answer almost always.\n' +
|
|
294
415
|
'{"delegate": true, "skill": "…", "question": "…"} ONLY if the job clearly needs expertise ' +
|
|
295
416
|
'outside your own, and a specialist answer would measurably improve the result.\n' +
|
|
296
|
-
|
|
417
|
+
// Se le da la lista CERRADA en vez de dejarle inventar. Un modelo al
|
|
418
|
+
// que se le pide "one or two words" contesta lo que le parece, y
|
|
419
|
+
// luego el buscador generaliza esa invención hasta encontrar a
|
|
420
|
+
// cualquiera. Aquí sólo puede elegir de lo que su autor permitió.
|
|
421
|
+
`"skill" MUST be copied EXACTLY from this list, and nothing else is allowed: ${permitidas.join(', ')}.\n` +
|
|
422
|
+
'If none of those fits the job, answer {"delegate": false}.\n' +
|
|
297
423
|
'"question" is the self-contained question for that specialist: it will be sent on its own, ' +
|
|
298
424
|
'so it must make sense without the rest of the job.\n' +
|
|
299
425
|
'Paying costs real money. If in doubt, do not delegate.',
|
|
@@ -305,6 +431,12 @@ async function decidirDelegacion(
|
|
|
305
431
|
const skill = parsed.skill?.trim();
|
|
306
432
|
const pregunta = parsed.question?.trim();
|
|
307
433
|
if (!skill || !pregunta) return null;
|
|
434
|
+
// Y se comprueba lo que contestó. Un modelo puede ignorar la lista, y de
|
|
435
|
+
// hecho lo hace: pedirle que elija de un menú no es lo mismo que obligarle.
|
|
436
|
+
if (!skillPermitida(skill, permitidas)) {
|
|
437
|
+
console.error(`[agente] el modelo pidió subcontratar "${skill}", que no está permitida. Trabaja solo.`);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
308
440
|
return { skill, pregunta };
|
|
309
441
|
} catch {
|
|
310
442
|
// Decidir mal no puede tumbar el encargo: ante la duda, se trabaja solo.
|
|
@@ -372,10 +504,10 @@ function revisar(brief: string, resultado: string): string | null {
|
|
|
372
504
|
* lo ancla en la cadena, así que el cliente puede demostrar que el archivo que
|
|
373
505
|
* se baja es exactamente el que le entregaste.
|
|
374
506
|
*/
|
|
375
|
-
function conArchivoSiLoPidio(brief: string, texto: string, ctx: TaskContext): TaskResult {
|
|
507
|
+
async function conArchivoSiLoPidio(brief: string, texto: string, ctx: TaskContext): Promise<TaskResult> {
|
|
376
508
|
const formato = formatoPedido(brief);
|
|
377
509
|
if (!formato) return texto;
|
|
378
|
-
const archivo = comoArchivo(formato, 'entrega', `Panal - entrega ${etiqueta(ctx)}`, texto);
|
|
510
|
+
const archivo = comoArchivo(formato, await nombreDelTema(brief, 'entrega'), `Panal - entrega ${etiqueta(ctx)}`, texto);
|
|
379
511
|
const bytes = typeof archivo.data === 'string' ? archivo.data.length : archivo.data.byteLength;
|
|
380
512
|
console.log(`[agente] ${etiqueta(ctx)} ${archivo.name} de ${bytes} bytes adjunto`);
|
|
381
513
|
// El TEXTO se sigue entregando: es lo que se ancla en la cadena. El archivo
|
package/template/src/register.ts
CHANGED
|
@@ -38,6 +38,27 @@ const PERFIL = {
|
|
|
38
38
|
// vacía NO la sustituye `??`. Con `??` el botUrl acababa siendo '' y la
|
|
39
39
|
// comprobación de "no has puesto tu URL" no saltaba nunca.
|
|
40
40
|
botUrl: process.env.PUBLIC_URL?.trim() || 'https://cambia-esto.example.com',
|
|
41
|
+
|
|
42
|
+
// TU CARA, si quieres tenerla. Todo esto es opcional y va vacío por defecto:
|
|
43
|
+
// un agente sin logo no vale menos, es lo que hay hoy en todo el mercado.
|
|
44
|
+
//
|
|
45
|
+
// Lo que compra ponerlo es que el cliente pueda MIRARTE antes de pagarte. En
|
|
46
|
+
// el mercado sales entre desconocidos, y un repositorio que se puede abrir
|
|
47
|
+
// dice más de ti que cualquier descripción que escribas de ti mismo.
|
|
48
|
+
//
|
|
49
|
+
// Se guarda en tu ficha del registro, así que cambiarlo cuesta una
|
|
50
|
+
// transacción: `npm run register` otra vez y ya.
|
|
51
|
+
links: {
|
|
52
|
+
// El logo sale en tu tarjeta, en el mercado y en la app. Https, cuadrado y
|
|
53
|
+
// pequeño: se pinta a 56 px, no hace falta más.
|
|
54
|
+
logo: '',
|
|
55
|
+
web: '',
|
|
56
|
+
// Tu perfil o el repositorio del agente: valen `usuario` y `usuario/repo`.
|
|
57
|
+
github: '',
|
|
58
|
+
// Solo el usuario; también se traga el enlace entero si lo pegas.
|
|
59
|
+
x: '',
|
|
60
|
+
telegram: '',
|
|
61
|
+
},
|
|
41
62
|
};
|
|
42
63
|
|
|
43
64
|
/** Lo que cobras por tarea. */
|
package/template/src/salida.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* peso y confusión.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { llmChat, resolverLlm, stripFilesManifest } from '@panal/sdk';
|
|
18
19
|
import { textoAPdf } from './pdf.js';
|
|
19
20
|
import { escribirZip } from './zip.js';
|
|
20
21
|
|
|
@@ -36,7 +37,20 @@ export interface ArchivoDeSalida {
|
|
|
36
37
|
* Devuelve `null` cuando no pide nada, que es el caso normal.
|
|
37
38
|
*/
|
|
38
39
|
export function formatoPedido(brief: string): Formato | null {
|
|
39
|
-
|
|
40
|
+
// El manifiesto de adjuntos NO es lo que pidio el cliente: es contabilidad
|
|
41
|
+
// del protocolo, y va pegada al FINAL del brief. Sin quitarla, sus lineas
|
|
42
|
+
// `name:` y `mime:` son las ultimas menciones de un formato que hay en el
|
|
43
|
+
// texto, y esta funcion se queda justamente con la ultima.
|
|
44
|
+
//
|
|
45
|
+
// El efecto es que el adjunto elige el formato de SALIDA. Comprobado en un
|
|
46
|
+
// encargo real de mainnet (#67): el cliente pidio JSON, adjunto un `.txt`, y
|
|
47
|
+
// el manifiesto —`name: pedidos n.txt`, `mime: text/plain`— gano al «devuelve
|
|
48
|
+
// solo el JSON» que estaba escrito antes. Se entrego un .txt.
|
|
49
|
+
//
|
|
50
|
+
// No es raro ni un caso de laboratorio: casi todo adjunto lleva en el nombre
|
|
51
|
+
// una extension que aqui es un formato. Adjuntar un PDF hacia que la entrega
|
|
52
|
+
// fuera un PDF, se pidiera lo que se pidiera.
|
|
53
|
+
const t = stripFilesManifest(brief).toLowerCase();
|
|
40
54
|
const mencion: { formato: Formato; en: number }[] = [];
|
|
41
55
|
|
|
42
56
|
for (const [formato, patron] of PATRONES) {
|
|
@@ -334,6 +348,159 @@ export function comoTabla(texto: string): string | null {
|
|
|
334
348
|
].join('\n');
|
|
335
349
|
}
|
|
336
350
|
|
|
351
|
+
/* ── cómo se llama el archivo ────────────────────────────────────────────── */
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Los reservados de Windows y los separadores de ruta.
|
|
355
|
+
*
|
|
356
|
+
* NO se tocan las letras: un nombre en chino, en árabe o con tildes es un
|
|
357
|
+
* nombre perfectamente válido, y quitárselos sería justo lo contrario de lo
|
|
358
|
+
* que hace falta aquí.
|
|
359
|
+
*/
|
|
360
|
+
const PROHIBIDOS = /[/\\:*?"<>|]/g;
|
|
361
|
+
|
|
362
|
+
/** Cuántos caracteres como mucho. Un nombre no es un resumen. */
|
|
363
|
+
const MAX_NOMBRE = 60;
|
|
364
|
+
|
|
365
|
+
/** ¿Es un carácter imprimible? Los de control no valen en un nombre. */
|
|
366
|
+
function imprimible(c: string): boolean {
|
|
367
|
+
const p = c.codePointAt(0) ?? 0;
|
|
368
|
+
return p >= 0x20 && p !== 0x7f;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Un título cualquiera, convertido en nombre de archivo.
|
|
373
|
+
*
|
|
374
|
+
* Se exporta para poder probarlo sin gastar una llamada al modelo.
|
|
375
|
+
*/
|
|
376
|
+
export function comoNombre(crudo: string): string {
|
|
377
|
+
const linea = crudo.split(/\r?\n/).find((l) => l.trim()) ?? '';
|
|
378
|
+
const limpio = [...linea]
|
|
379
|
+
.filter(imprimible)
|
|
380
|
+
.join('')
|
|
381
|
+
.trim()
|
|
382
|
+
// El modelo devuelve el título entrecomillado más veces de las que parece.
|
|
383
|
+
.replace(/^["'`«“]+|["'`»”]+$/g, '')
|
|
384
|
+
// Y a veces le pone extensión, que aquí la pone `comoArchivo`.
|
|
385
|
+
.replace(/\.(pdf|docx?|xlsx?|md|txt|csv|json|zip)$/i, '')
|
|
386
|
+
.replace(PROHIBIDOS, ' ')
|
|
387
|
+
.replace(/\s+/g, '-')
|
|
388
|
+
.replace(/-{2,}/g, '-')
|
|
389
|
+
.replace(/^[-.]+|[-.]+$/g, '')
|
|
390
|
+
.toLowerCase();
|
|
391
|
+
// Por code points y no con `.slice`: cortar por unidades UTF-16 parte por la
|
|
392
|
+
// mitad un carácter fuera del plano básico.
|
|
393
|
+
return [...limpio].slice(0, MAX_NOMBRE).join('').replace(/[-.]+$/, '');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* La cabecera `content-disposition` de un archivo que se descarga.
|
|
398
|
+
*
|
|
399
|
+
* DOS FORMAS DEL NOMBRE, Y LAS DOS HACEN FALTA (RFC 6266). Una cabecera HTTP
|
|
400
|
+
* solo admite latin-1, y desde que el nombre del archivo lo escribe el modelo
|
|
401
|
+
* en el idioma del cliente, un entregable puede llamarse
|
|
402
|
+
* `两个整数相除.pdf`. Interpolarlo tal cual en `filename="…"` no da un nombre
|
|
403
|
+
* feo: Node LANZA `ERR_INVALID_CHAR` al escribir la cabecera y la descarga
|
|
404
|
+
* responde 500. El archivo estaba entregado, pagado y anclado en la cadena, y
|
|
405
|
+
* el cliente no podía bajárselo.
|
|
406
|
+
*
|
|
407
|
+
* filename= una versión en ASCII, para quien no entienda lo otro
|
|
408
|
+
* filename*= el nombre de verdad, en UTF-8 percent-encoded
|
|
409
|
+
*
|
|
410
|
+
* Los navegadores prefieren `filename*` cuando está, así que el nombre bueno
|
|
411
|
+
* es el que se ve. Y las comillas se van del ASCII a propósito: una comilla
|
|
412
|
+
* dentro de `filename="…"` parte la cabecera por la mitad.
|
|
413
|
+
*/
|
|
414
|
+
export function comoAdjunto(nombre: string): string {
|
|
415
|
+
const ascii =
|
|
416
|
+
[...nombre]
|
|
417
|
+
.map((c) => {
|
|
418
|
+
const p = c.codePointAt(0) ?? 0;
|
|
419
|
+
return p >= 0x20 && p < 0x7f && c !== '"' && c !== '\\' ? c : '_';
|
|
420
|
+
})
|
|
421
|
+
.join('')
|
|
422
|
+
.replace(/_{2,}/g, '_')
|
|
423
|
+
.replace(/^[_.]+|[_.]+$/g, '') || 'archivo';
|
|
424
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(nombre)}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Lo que se le pide al modelo. Corto a propósito: es un nombre, no un resumen.
|
|
429
|
+
*
|
|
430
|
+
* Las dos reglas que de verdad cambian el resultado son las dos últimas. Sin
|
|
431
|
+
* la del SUJETO, el modelo nombra la acción —«escribir-casos-de-prueba»— y
|
|
432
|
+
* todos los archivos de un mismo agente vuelven a llamarse igual, que es el
|
|
433
|
+
* problema que esto viene a arreglar. Y sin la del IDIOMA contesta en inglés
|
|
434
|
+
* aunque el encargo venga en otro, porque el inglés es su idioma por defecto.
|
|
435
|
+
*/
|
|
436
|
+
const PIDE_UN_NOMBRE =
|
|
437
|
+
'You name files. Given a client request, reply with ONLY a file name for the deliverable.\n' +
|
|
438
|
+
'Two to five words. No extension, no quotes, no path, no explanation, no punctuation at the ends.\n' +
|
|
439
|
+
'Name the SUBJECT the work is about, never the action asked for: for "write the test cases for a ' +
|
|
440
|
+
'function that divides two integers" answer "division de dos enteros", not "escribir casos de prueba".\n' +
|
|
441
|
+
'Write it in the SAME language the client wrote in, in their own script. Do not translate it to English.';
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* El nombre del archivo que se entrega, sacado del TEMA del encargo.
|
|
445
|
+
*
|
|
446
|
+
* ANTES TODOS SE LLAMABAN IGUAL. Cada agente tenía un nombre fijo —
|
|
447
|
+
* `casos-de-prueba.pdf`, `revision.pdf`, `traducciones.pdf`—, así que un
|
|
448
|
+
* cliente que encargara tres cosas al mismo agente acababa con tres archivos
|
|
449
|
+
* del mismo nombre en su carpeta de descargas, pisándose unos a otros o
|
|
450
|
+
* quedando como «casos-de-prueba (2).pdf». Y estaba en castellano para todo el
|
|
451
|
+
* mundo, cuando el contenido va en el idioma del cliente desde hace tiempo.
|
|
452
|
+
*
|
|
453
|
+
* EL TEMA SALE DEL ENCARGO, no de la entrega. La entrega es texto plano sin
|
|
454
|
+
* título —el prompt prohíbe los encabezados a propósito—, así que su primera
|
|
455
|
+
* línea es el primer caso de prueba, no de qué va la cosa. El encargo, en
|
|
456
|
+
* cambio, lo escribió el cliente: dice el tema y está en su idioma.
|
|
457
|
+
*
|
|
458
|
+
* NUNCA LANZA Y NUNCA DEVUELVE VACÍO. Si el modelo no contesta, tarda o
|
|
459
|
+
* devuelve algo que no sirve, se usa el nombre de siempre. Nombrar un archivo
|
|
460
|
+
* no puede impedir entregarlo: el pago ya está bloqueado.
|
|
461
|
+
*/
|
|
462
|
+
export async function nombreDelTema(brief: string, deReserva: string): Promise<string> {
|
|
463
|
+
try {
|
|
464
|
+
const cfg = resolverLlm(process.env);
|
|
465
|
+
const respuesta = await llmChat(
|
|
466
|
+
// NO SE TOCA NI LA TEMPERATURA NI EL TOPE DE TOKENS, y las dos cosas se
|
|
467
|
+
// aprendieron probando contra el modelo de verdad.
|
|
468
|
+
//
|
|
469
|
+
// Aquí ponía `temperature: 0` —lo natural para pedir algo determinista— y
|
|
470
|
+
// el modelo lo rechazaba con un 400: hay modelos que solo aceptan 1. Y
|
|
471
|
+
// ponía `maxTokens: 32` —es un nombre, no un texto— y la respuesta volvía
|
|
472
|
+
// con `choices` vacío, porque un modelo que razona antes de contestar se
|
|
473
|
+
// gasta ese presupuesto pensando y no le queda para escribir.
|
|
474
|
+
//
|
|
475
|
+
// Los dos fallos son INVISIBLES: se cae al nombre de siempre, que es
|
|
476
|
+
// exactamente lo que había antes, así que la función habría quedado
|
|
477
|
+
// muerta sin que nadie lo notara. Se hereda lo que el operador ya tiene
|
|
478
|
+
// configurado, que es lo que funciona en el resto de sus llamadas.
|
|
479
|
+
//
|
|
480
|
+
// Lo único propio es el reloj: un timeout más corto que el del trabajo y
|
|
481
|
+
// un solo reintento, porque esto va DESPUÉS de tener la entrega hecha y
|
|
482
|
+
// no puede retrasarla.
|
|
483
|
+
{ ...cfg, timeoutMs: 20_000, maxRetries: 1 },
|
|
484
|
+
// El encargo entero no hace falta: el tema está al principio, y mandarlo
|
|
485
|
+
// completo puede ser mandar un contrato de treinta páginas para sacar
|
|
486
|
+
// cuatro palabras.
|
|
487
|
+
// Sin el manifiesto, por lo mismo que en `formatoPedido`: son 1.500
|
|
488
|
+
// caracteres de presupuesto y un hash de 64 ocupa sitio sin decir nada
|
|
489
|
+
// del tema. Con adjuntos cortos llegaba a colarse entero.
|
|
490
|
+
{ system: PIDE_UN_NOMBRE, user: stripFilesManifest(brief).trim().slice(0, 1_500) },
|
|
491
|
+
);
|
|
492
|
+
const nombre = comoNombre(respuesta);
|
|
493
|
+
if (nombre) return nombre;
|
|
494
|
+
console.warn(`[salida] el modelo no dio un nombre usable; el archivo va como «${deReserva}»`);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.warn(
|
|
497
|
+
`[salida] no se pudo nombrar el archivo (${err instanceof Error ? err.message.split('\n')[0] : err}); ` +
|
|
498
|
+
`va como «${deReserva}»`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return deReserva;
|
|
502
|
+
}
|
|
503
|
+
|
|
337
504
|
/**
|
|
338
505
|
* El archivo listo para adjuntar a la entrega.
|
|
339
506
|
*
|
package/template/src/server.ts
CHANGED
|
@@ -30,10 +30,12 @@ import {
|
|
|
30
30
|
assertCanServe,
|
|
31
31
|
buildQuote,
|
|
32
32
|
createPanalClient,
|
|
33
|
+
leerNiveles,
|
|
33
34
|
LoopDetected,
|
|
34
35
|
MAINNET_ADDRESSES,
|
|
35
36
|
monad,
|
|
36
37
|
matchAttachment,
|
|
38
|
+
nivelPara,
|
|
37
39
|
parseAttachmentsManifest,
|
|
38
40
|
parseEnvelope,
|
|
39
41
|
parsePaymentHeader,
|
|
@@ -45,20 +47,21 @@ import {
|
|
|
45
47
|
type AttachedFile,
|
|
46
48
|
type CallEnvelope,
|
|
47
49
|
type DeliveredFile,
|
|
50
|
+
type FichaNivel,
|
|
51
|
+
type Nivel,
|
|
48
52
|
type PermitDomain,
|
|
49
53
|
} from '@panal/sdk';
|
|
54
|
+
import { comoAdjunto } from './salida.js';
|
|
50
55
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
51
56
|
import { isAddress, keccak256, parseEther, toBytes, verifyMessage } from 'viem';
|
|
52
57
|
import type { Address } from 'viem';
|
|
53
|
-
import { handleTask } from './agent.js';
|
|
54
|
-
import type { AdjuntoRecibido, TaskContext, TaskFile, TaskResult } from './agent.js';
|
|
58
|
+
import { handleTask, NIVELES, SUBCONTRATA_SKILLS } from './agent.js';
|
|
59
|
+
import type { AdjuntoRecibido, NivelPropio, TaskContext, TaskFile, TaskResult } from './agent.js';
|
|
55
60
|
import { arrancarVigilante } from './vigilante.js';
|
|
56
61
|
import { historialParaElModelo, recordarTurno, type Turno } from './memoria.js';
|
|
57
62
|
|
|
58
63
|
const PORT = Number(process.env.PORT ?? 8787);
|
|
59
64
|
const DATA_DIR = process.env.DATA_DIR ?? './data';
|
|
60
|
-
/** Tope del cuerpo de una petición: sin esto, cualquiera te tumba el proceso. */
|
|
61
|
-
const MAX_BODY = 256 * 1024;
|
|
62
65
|
|
|
63
66
|
/**
|
|
64
67
|
* Tope del encargo, en CARACTERES, y anunciado en `/agent.json`.
|
|
@@ -75,6 +78,80 @@ const MAX_BODY = 256 * 1024;
|
|
|
75
78
|
*/
|
|
76
79
|
const MAX_BRIEF_CHARS = 32_000;
|
|
77
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Niveles: el mismo trabajo en varios tamaños.
|
|
83
|
+
//
|
|
84
|
+
// Los declara el agente en `agent.ts` y son OPCIONALES. Sin ninguno, todo lo
|
|
85
|
+
// de aquí abajo se queda en los valores de siempre y este bloque no hace nada.
|
|
86
|
+
//
|
|
87
|
+
// Se pasan por `leerNiveles`, que es EL MISMO lector que usarán la web y la
|
|
88
|
+
// app al leer la ficha. Así un agente no puede publicar un nivel que sus
|
|
89
|
+
// propios clientes van a descartar: si aquí no sobrevive, no se anuncia.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const NIVELES_OK = leerNiveles({
|
|
93
|
+
tiers: NIVELES.map((n) => ({ ...n, amountWei: n.wei.toString() })),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (NIVELES.length > 0 && NIVELES_OK.length !== NIVELES.length) {
|
|
97
|
+
console.error(
|
|
98
|
+
`[panal] ${NIVELES.length - NIVELES_OK.length} nivel(es) de agent.ts están mal escritos y no se van a anunciar. ` +
|
|
99
|
+
'Cada uno necesita un `wei` positivo.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** El nivel más barato: por debajo de eso, un agente con niveles no trabaja. */
|
|
104
|
+
const NIVEL_MINIMO = NIVELES_OK[0] ?? null;
|
|
105
|
+
|
|
106
|
+
// Un nivel que se gasta subcontratando lo mismo que cobra trabaja gratis, y si
|
|
107
|
+
// se pasa, trabaja pagando. No se corrige solo —es una decisión del autor— pero
|
|
108
|
+
// no puede quedarse callado.
|
|
109
|
+
for (const n of NIVELES) {
|
|
110
|
+
if (n.subcontrata !== undefined && n.subcontrata >= n.wei) {
|
|
111
|
+
console.error(
|
|
112
|
+
`[panal] el nivel "${n.name}" cobra ${n.wei} y puede gastar ${n.subcontrata} subcontratando: ` +
|
|
113
|
+
'no te queda nada por el trabajo.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/** El tope de encargo del nivel mayor, o el de siempre si no hay niveles. */
|
|
120
|
+
const TOPE_BRIEF_MAYOR = Math.max(MAX_BRIEF_CHARS, ...NIVELES_OK.map((n) => n.maxBriefChars ?? 0));
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tope del cuerpo de una petición: sin esto, cualquiera te tumba el proceso.
|
|
124
|
+
*
|
|
125
|
+
* Sale del nivel MAYOR y no de un número redondo, porque prometer 320 000
|
|
126
|
+
* caracteres y cortar el cuerpo a 256 KB es prometer algo que no se cumple.
|
|
127
|
+
* El ×4 es el peor caso de UTF-8 —un carácter puede ocupar cuatro bytes— y
|
|
128
|
+
* los 32 KB de propina cubren el resto del JSON: la firma, la dirección y el
|
|
129
|
+
* manifiesto de adjuntos que viaja dentro del encargo.
|
|
130
|
+
*/
|
|
131
|
+
const MAX_BODY = Math.max(256 * 1024, TOPE_BRIEF_MAYOR * 4 + 32 * 1024);
|
|
132
|
+
|
|
133
|
+
/** Un nivel ya validado, en la forma con la que se anuncia y se devuelve. */
|
|
134
|
+
function comoFicha(n: Nivel): FichaNivel {
|
|
135
|
+
return {
|
|
136
|
+
...(n.name === null ? {} : { name: n.name }),
|
|
137
|
+
...(n.description === null ? {} : { description: n.description }),
|
|
138
|
+
amountWei: n.wei.toString(),
|
|
139
|
+
...(n.maxBriefChars === null ? {} : { maxBriefChars: n.maxBriefChars }),
|
|
140
|
+
...(n.maxAttachChars === null ? {} : { maxAttachChars: n.maxAttachChars }),
|
|
141
|
+
...(n.maxAttachCharsTotal === null ? {} : { maxAttachCharsTotal: n.maxAttachCharsTotal }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Qué nivel compró quien bloqueó esto. `null` si el agente no vende niveles. */
|
|
146
|
+
function nivelDe(pagado: bigint): NivelPropio | null {
|
|
147
|
+
if (NIVELES_OK.length === 0) return null;
|
|
148
|
+
const leido = nivelPara(NIVELES_OK, pagado);
|
|
149
|
+
if (!leido) return null;
|
|
150
|
+
// Se devuelve el declarado en `agent.ts`, no el normalizado: es el que tiene
|
|
151
|
+
// los tipos que el autor del agente espera encontrar en `ctx.nivel`.
|
|
152
|
+
return NIVELES.find((n) => n.wei === leido.wei) ?? null;
|
|
153
|
+
}
|
|
154
|
+
|
|
78
155
|
const key = process.env.AGENT_PRIVATE_KEY?.trim();
|
|
79
156
|
if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
|
|
80
157
|
console.error('Falta AGENT_PRIVATE_KEY (0x + 64 hex) en el .env. Copia .env.example y rellénalo.');
|
|
@@ -158,7 +235,21 @@ const SUBCONTRATA_SALTOS = (() => {
|
|
|
158
235
|
})();
|
|
159
236
|
|
|
160
237
|
if (SUBCONTRATA_MAX > 0n) {
|
|
161
|
-
|
|
238
|
+
// Se dice el estado REAL, no sólo el del dinero. Antes bastaba con tener
|
|
239
|
+
// presupuesto; ahora hacen falta las dos cosas, y un log que dijera "activa"
|
|
240
|
+
// con la lista vacía haría perder la tarde a quien se pregunte por qué su
|
|
241
|
+
// agente nunca delega.
|
|
242
|
+
if (SUBCONTRATA_SKILLS.length > 0) {
|
|
243
|
+
console.log(
|
|
244
|
+
`Subcontratación activa: hasta ${process.env.SUBCONTRATA_MAX} ${X402_SYMBOL} por encargo, ` +
|
|
245
|
+
`y sólo en: ${SUBCONTRATA_SKILLS.join(', ')}`,
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
console.log(
|
|
249
|
+
`Subcontratación con presupuesto (${process.env.SUBCONTRATA_MAX} ${X402_SYMBOL}) pero SIN skills ` +
|
|
250
|
+
'permitidas: no va a delegar. Rellena SUBCONTRATA_SKILLS en agent.ts.',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
162
253
|
|
|
163
254
|
// Lo que te pagan por una consulta es el techo de lo que puedes gastarte en
|
|
164
255
|
// ella. Con SUBCONTRATA_MAX igual o mayor que X402_PRICE, un encargo en el
|
|
@@ -508,18 +599,40 @@ function contexto(
|
|
|
508
599
|
},
|
|
509
600
|
sobre: CallEnvelope | null,
|
|
510
601
|
): TaskContext {
|
|
602
|
+
// Se resuelve aquí y no en cada llamador para que no haya dos maneras de
|
|
603
|
+
// decidirlo. En x402 es SIEMPRE null: no hay escrow, y el importe de una
|
|
604
|
+
// llamada suelta no compra un nivel de encargo.
|
|
605
|
+
const nivel = base.taskId === null ? null : nivelDe(base.amount);
|
|
606
|
+
|
|
607
|
+
// El presupuesto del nivel comprado, y si el nivel no dice nada, el del
|
|
608
|
+
// .env. Así el nivel caro puede comprar ayuda y el barato no, sin que un
|
|
609
|
+
// agente sin niveles note ningún cambio.
|
|
610
|
+
const presupuesto = nivel?.subcontrata ?? SUBCONTRATA_MAX;
|
|
611
|
+
|
|
511
612
|
return {
|
|
512
613
|
...base,
|
|
614
|
+
nivel,
|
|
513
615
|
envelope: sobre,
|
|
514
|
-
presupuesto
|
|
616
|
+
presupuesto,
|
|
515
617
|
consultar: async (skill: string, pregunta: string) => {
|
|
516
|
-
if (
|
|
618
|
+
if (presupuesto <= 0n) {
|
|
517
619
|
throw new Error(
|
|
518
|
-
|
|
620
|
+
nivel
|
|
621
|
+
? `El nivel "${nivel.name}" no tiene presupuesto para subcontratar: ponle \`subcontrata\` en agent.ts.`
|
|
622
|
+
: 'Este agente no tiene presupuesto para subcontratar: pon SUBCONTRATA_MAX en el .env si quieres que delegue.',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
// Dinero Y permiso. Sin lista no se compra nada, aunque haya dinero: el
|
|
626
|
+
// buscador generaliza cuando no encuentra a nadie, y sin lista esa
|
|
627
|
+
// generalización no tiene dónde parar.
|
|
628
|
+
if (SUBCONTRATA_SKILLS.length === 0) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
'Este agente no tiene ninguna skill permitida: rellena SUBCONTRATA_SKILLS en agent.ts si quieres que delegue.',
|
|
519
631
|
);
|
|
520
632
|
}
|
|
521
633
|
const res = await panal.ask(skill, pregunta, {
|
|
522
|
-
maxSpend:
|
|
634
|
+
maxSpend: presupuesto,
|
|
635
|
+
skillsPermitidas: SUBCONTRATA_SKILLS,
|
|
523
636
|
depth: SUBCONTRATA_SALTOS,
|
|
524
637
|
// El sobre recibido, si lo hay. Sin él se abre una cadena nueva.
|
|
525
638
|
envelope: sobre,
|
|
@@ -709,10 +822,10 @@ function json(res: ServerResponse, status: number, body: unknown): void {
|
|
|
709
822
|
/**
|
|
710
823
|
* El cuerpo en crudo, con su propio tope.
|
|
711
824
|
*
|
|
712
|
-
* Va aparte de `readBody` a propósito:
|
|
713
|
-
*
|
|
714
|
-
* subirle el tope a todas las rutas para
|
|
715
|
-
* ese límite cierra.
|
|
825
|
+
* Va aparte de `readBody` a propósito: MAX_BODY protege las rutas de texto y
|
|
826
|
+
* está calculado para caberles justo —lo que hoy pida el nivel mayor y ni un
|
|
827
|
+
* byte más—. Una foto no entra ahí, y subirle el tope a todas las rutas para
|
|
828
|
+
* que quepa sería abrir la puerta que ese límite cierra.
|
|
716
829
|
*/
|
|
717
830
|
async function readBodyBytes(req: IncomingMessage, max: number): Promise<Buffer> {
|
|
718
831
|
const chunks: Buffer[] = [];
|
|
@@ -955,6 +1068,13 @@ const server = createServer((req, res) => {
|
|
|
955
1068
|
path: '/brief/:taskId',
|
|
956
1069
|
signMessage: 'Panal brief #<taskId> (EIP-191, firmado por el cliente de la tarea)',
|
|
957
1070
|
body: `{"brief": string (máx. ${MAX_BRIEF_CHARS} chars), "address": "0x…", "signature": "0x…"}`,
|
|
1071
|
+
// OJO: aquí va el tope BÁSICO, nunca el del nivel mayor.
|
|
1072
|
+
//
|
|
1073
|
+
// Un cliente antiguo no sabe elegir nivel, así que va a bloquear el
|
|
1074
|
+
// precio del registro y comprar el básico. Anunciarle el tope del
|
|
1075
|
+
// nivel grande le haría mandar un libro pagando lo pequeño, y
|
|
1076
|
+
// enterarse con el dinero ya bloqueado. Quien sepa leer `tiers`
|
|
1077
|
+
// verá los demás.
|
|
958
1078
|
maxBriefChars: MAX_BRIEF_CHARS,
|
|
959
1079
|
},
|
|
960
1080
|
postAttachment: {
|
|
@@ -973,6 +1093,9 @@ const server = createServer((req, res) => {
|
|
|
973
1093
|
},
|
|
974
1094
|
...(x402 ? { x402Ask: x402 } : {}),
|
|
975
1095
|
},
|
|
1096
|
+
// Los niveles, sólo si este agente vende alguno. Ausente significa que
|
|
1097
|
+
// no los ofrece, y quien lee NO debe inventárselos a partir del precio.
|
|
1098
|
+
...(NIVELES_OK.length > 0 ? { tiers: NIVELES_OK.map(comoFicha) } : {}),
|
|
976
1099
|
// ALIAS ANTIGUO, en la raíz. Aquí es donde esta plantilla lo publicaba
|
|
977
1100
|
// antes, y hay clientes ahí fuera que solo miran este sitio. Se sirve
|
|
978
1101
|
// por compatibilidad y desaparecerá; lo que se lee es `endpoints`.
|
|
@@ -1140,13 +1263,15 @@ const server = createServer((req, res) => {
|
|
|
1140
1263
|
json(res, 400, { error: 'faltan taskId, brief o signature' });
|
|
1141
1264
|
return;
|
|
1142
1265
|
}
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1266
|
+
// Primera guarda, y a propósito contra el tope MAYOR: aquí todavía no
|
|
1267
|
+
// se sabe qué nivel se pagó —eso está en la cadena y cuesta un eth_call—
|
|
1268
|
+
// y un texto que no cabe en el nivel más caro no cabe en ninguno. Lo que
|
|
1269
|
+
// sí se puede decidir sin preguntarle a nadie, se decide sin preguntar.
|
|
1270
|
+
if (body.brief.length > TOPE_BRIEF_MAYOR) {
|
|
1147
1271
|
json(res, 400, {
|
|
1148
|
-
error: `el encargo son ${body.brief.length} caracteres y el tope es ${
|
|
1149
|
-
maxBriefChars:
|
|
1272
|
+
error: `el encargo son ${body.brief.length} caracteres y el tope es ${TOPE_BRIEF_MAYOR}`,
|
|
1273
|
+
maxBriefChars: TOPE_BRIEF_MAYOR,
|
|
1274
|
+
...(NIVELES_OK.length > 0 ? { tiers: NIVELES_OK.map(comoFicha) } : {}),
|
|
1150
1275
|
});
|
|
1151
1276
|
return;
|
|
1152
1277
|
}
|
|
@@ -1193,6 +1318,34 @@ const server = createServer((req, res) => {
|
|
|
1193
1318
|
json(res, 401, { error: 'la firma no es del cliente de esta tarea' });
|
|
1194
1319
|
return;
|
|
1195
1320
|
}
|
|
1321
|
+
|
|
1322
|
+
// El tope DEL NIVEL QUE PAGÓ. Se mira ahora y no arriba porque hasta
|
|
1323
|
+
// aquí no se sabía el importe, y el importe es lo único que dice qué
|
|
1324
|
+
// nivel compró: el brief lo escribe el cliente y podría proclamarse del
|
|
1325
|
+
// más caro.
|
|
1326
|
+
//
|
|
1327
|
+
// Se dice el número y se dicen los niveles: el cliente ya tiene el pago
|
|
1328
|
+
// bloqueado, y saber si le sobra texto o le falta nivel es la diferencia
|
|
1329
|
+
// entre arreglarlo y esperar al plazo para recuperar el dinero.
|
|
1330
|
+
const nivel = nivelDe(task.amount);
|
|
1331
|
+
if (NIVEL_MINIMO && task.amount < NIVEL_MINIMO.wei) {
|
|
1332
|
+
json(res, 400, {
|
|
1333
|
+
error: `esta tarea bloqueó ${task.amount} y el nivel más barato cuesta ${NIVEL_MINIMO.wei}`,
|
|
1334
|
+
tiers: NIVELES_OK.map(comoFicha),
|
|
1335
|
+
});
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const topeDelNivel = nivel?.maxBriefChars ?? MAX_BRIEF_CHARS;
|
|
1339
|
+
if (body.brief.length > topeDelNivel) {
|
|
1340
|
+
json(res, 400, {
|
|
1341
|
+
error:
|
|
1342
|
+
`el encargo son ${body.brief.length} caracteres y el nivel que pagaste ` +
|
|
1343
|
+
`(${nivel?.name ?? 'el básico'}) llega a ${topeDelNivel}`,
|
|
1344
|
+
maxBriefChars: topeDelNivel,
|
|
1345
|
+
...(NIVELES_OK.length > 0 ? { tiers: NIVELES_OK.map(comoFicha) } : {}),
|
|
1346
|
+
});
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1196
1349
|
// Y que el texto sea EL que se encargó. Para esto existe el taskHash: sin
|
|
1197
1350
|
// esta comprobación, un cliente podría pagar por una cosa on-chain y
|
|
1198
1351
|
// pedirte otra por HTTP, y en una disputa el árbitro no tendría con qué
|
|
@@ -1419,7 +1572,7 @@ const server = createServer((req, res) => {
|
|
|
1419
1572
|
'content-length': bytes.byteLength,
|
|
1420
1573
|
// `attachment` a propósito: lo que hay dentro lo eligió el agente, y no
|
|
1421
1574
|
// se le deja que el navegador del cliente lo ejecute como una página.
|
|
1422
|
-
'content-disposition':
|
|
1575
|
+
'content-disposition': comoAdjunto(nombre),
|
|
1423
1576
|
'x-content-type-options': 'nosniff',
|
|
1424
1577
|
});
|
|
1425
1578
|
res.end(bytes);
|