create-panal-agent 0.6.1 → 0.7.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 +30 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/server.ts +208 -18
- package/template/src/vigilante.ts +63 -11
package/dist/i18n.js
CHANGED
|
@@ -76,6 +76,7 @@ The language is taken from --lang, then PANAL_LANG, then your system locale.`,
|
|
|
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
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.",
|
|
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
|
+
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 only if a reverse proxy sits in front, so x-forwarded-for can be trusted. AUTH_ESTRICTA=1 rejects download signatures with no expiry (the old format).",
|
|
79
80
|
},
|
|
80
81
|
readme: `# {name}
|
|
81
82
|
|
|
@@ -100,6 +101,8 @@ PUBLIC_URL=https://your-domain npm run register
|
|
|
100
101
|
| \`src/agent.ts\` | **The only one you edit.** Your work goes in \`handleTask()\`. |
|
|
101
102
|
| \`src/server.ts\` | Receives the brief, verifies signatures, delivers on-chain. |
|
|
102
103
|
| \`src/register.ts\` | Your listing: name, description, skills, price. |
|
|
104
|
+
| \`src/pdf.ts\` | Turns text into a PDF, with no dependencies. Delete it if you deliver no files. |
|
|
105
|
+
| \`src/vigilante.ts\` | Recovers tasks left hanging: a delivery never anchored, work that died halfway. |
|
|
103
106
|
| \`.env\` | Your private key and your model key. Never commit it. |
|
|
104
107
|
|
|
105
108
|
## How you get paid
|
|
@@ -168,6 +171,7 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
|
|
|
168
171
|
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`.',
|
|
169
172
|
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.",
|
|
170
173
|
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.",
|
|
174
|
+
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 solo si tienes un proxy delante, para poder fiarte de x-forwarded-for. AUTH_ESTRICTA=1 rechaza las firmas de descarga sin caducidad (el formato antiguo).",
|
|
171
175
|
},
|
|
172
176
|
readme: `# {name}
|
|
173
177
|
|
|
@@ -193,6 +197,8 @@ PUBLIC_URL=https://tu-dominio npm run register
|
|
|
193
197
|
| \`src/agent.ts\` | **El único que tocas.** Tu trabajo va en \`handleTask()\`. |
|
|
194
198
|
| \`src/server.ts\` | Recibe el encargo, verifica firmas y entrega en la cadena. |
|
|
195
199
|
| \`src/register.ts\` | Tu ficha: nombre, descripción, skills y precio. |
|
|
200
|
+
| \`src/pdf.ts\` | Convierte texto en PDF, sin dependencias. Bórralo si no entregas archivos. |
|
|
201
|
+
| \`src/vigilante.ts\` | Recupera lo que se quedó colgado: una entrega sin anclar, un trabajo a medias. |
|
|
196
202
|
| \`.env\` | Tu clave privada y la de tu modelo. No lo subas nunca. |
|
|
197
203
|
|
|
198
204
|
## Cómo cobras
|
|
@@ -261,6 +267,7 @@ const zh = {
|
|
|
261
267
|
x402: '按次收费(可选)。留空则你的代理只接受托管订单。\n价格按每次请求计算,使用支持 EIP-2612 的代币——不能是 MON,该方案依赖 `permit`。',
|
|
262
268
|
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。",
|
|
263
269
|
vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
|
|
270
|
+
seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 仅在前面有反向代理时使用,这样才能信任 x-forwarded-for。AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
|
|
264
271
|
},
|
|
265
272
|
readme: `# {name}
|
|
266
273
|
|
|
@@ -285,6 +292,8 @@ PUBLIC_URL=https://你的域名 npm run register
|
|
|
285
292
|
| \`src/agent.ts\` | **唯一需要你修改的文件。** 你的业务逻辑写在 \`handleTask()\` 里。 |
|
|
286
293
|
| \`src/server.ts\` | 接收任务、验证签名、在链上交付。 |
|
|
287
294
|
| \`src/register.ts\` | 你的名片:名称、描述、技能、价格。 |
|
|
295
|
+
| \`src/pdf.ts\` | 把文本转成 PDF,无依赖。不交付文件就删掉它。 |
|
|
296
|
+
| \`src/vigilante.ts\` | 挽回卡住的任务:没有上链的交付、中途死掉的工作。 |
|
|
288
297
|
| \`.env\` | 你的私钥和模型密钥。绝不要提交到仓库。 |
|
|
289
298
|
|
|
290
299
|
## 你如何收款
|
|
@@ -349,6 +358,7 @@ const hi = {
|
|
|
349
358
|
x402: 'प्रति कॉल शुल्क (वैकल्पिक)। खाली छोड़ें तो आपका एजेंट केवल एस्क्रो वाले काम लेगा।\nकीमत प्रति अनुरोध है, EIP-2612 टोकन में — MON नहीं चल सकता, इस योजना को `permit` चाहिए।',
|
|
350
359
|
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है।",
|
|
351
360
|
vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
|
|
361
|
+
seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 केवल तभी जब आगे रिवर्स प्रॉक्सी हो। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
|
|
352
362
|
},
|
|
353
363
|
readme: `# {name}
|
|
354
364
|
|
|
@@ -373,6 +383,8 @@ PUBLIC_URL=https://आपका-डोमेन npm run register
|
|
|
373
383
|
| \`src/agent.ts\` | **केवल यही आप बदलते हैं।** आपका काम \`handleTask()\` में जाता है। |
|
|
374
384
|
| \`src/server.ts\` | काम प्राप्त करता है, हस्ताक्षर जाँचता है, चेन पर डिलीवर करता है। |
|
|
375
385
|
| \`src/register.ts\` | आपकी प्रोफ़ाइल: नाम, विवरण, कौशल, कीमत। |
|
|
386
|
+
| \`src/pdf.ts\` | टेक्स्ट को PDF में बदलता है, बिना निर्भरता के। फाइलें न भेजें तो हटा दें। |
|
|
387
|
+
| \`src/vigilante.ts\` | अटके हुए कार्य वापस लाता है: चेन पर दर्ज न हुई डिलीवरी, अधूरा काम। |
|
|
376
388
|
| \`.env\` | आपकी निजी कुंजी और मॉडल कुंजी। इसे कभी कमिट न करें। |
|
|
377
389
|
|
|
378
390
|
## भुगतान कैसे मिलता है
|
|
@@ -441,6 +453,7 @@ const ar = {
|
|
|
441
453
|
x402: 'التحصيل لكل استدعاء (اختياري). اتركه فارغًا فيقبل وكيلك مهام الضمان فقط.\nالسعر لكل طلب، بعملة تدعم EIP-2612 — لا يصلح MON، فالمخطط يحتاج `permit`.',
|
|
442
454
|
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL.",
|
|
443
455
|
vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
|
|
456
|
+
seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 فقط إذا كان أمامه بروكسي عكسي. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
|
|
444
457
|
},
|
|
445
458
|
readme: `# {name}
|
|
446
459
|
|
|
@@ -465,6 +478,8 @@ PUBLIC_URL=https://نطاقك npm run register
|
|
|
465
478
|
| \`src/agent.ts\` | **الوحيد الذي تعدّله.** عملك يكتب داخل \`handleTask()\`. |
|
|
466
479
|
| \`src/server.ts\` | يستقبل الطلب، ويتحقق من التواقيع، ويسلّم على السلسلة. |
|
|
467
480
|
| \`src/register.ts\` | بطاقتك: الاسم والوصف والمهارات والسعر. |
|
|
481
|
+
| \`src/pdf.ts\` | يحوّل النص إلى PDF، دون تبعيات. احذفه إن كنت لا تسلّم ملفات. |
|
|
482
|
+
| \`src/vigilante.ts\` | يستعيد المهام المعطلة: تسليم لم يثبت على السلسلة، أو عمل مات في منتصفه. |
|
|
468
483
|
| \`.env\` | مفتاحك الخاص ومفتاح النموذج. لا ترفعه إلى المستودع أبدًا. |
|
|
469
484
|
|
|
470
485
|
## كيف تتقاضى أجرك
|
|
@@ -532,6 +547,7 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
|
|
|
532
547
|
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`.",
|
|
533
548
|
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.",
|
|
534
549
|
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.",
|
|
550
|
+
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 seulement avec un proxy devant. AUTH_ESTRICTA=1 rejette les signatures de téléchargement sans expiration.",
|
|
535
551
|
},
|
|
536
552
|
readme: `# {name}
|
|
537
553
|
|
|
@@ -557,6 +573,8 @@ PUBLIC_URL=https://votre-domaine npm run register
|
|
|
557
573
|
| \`src/agent.ts\` | **Le seul que vous modifiez.** Votre travail va dans \`handleTask()\`. |
|
|
558
574
|
| \`src/server.ts\` | Reçoit la commande, vérifie les signatures, livre sur la chaîne. |
|
|
559
575
|
| \`src/register.ts\` | Votre fiche : nom, description, compétences, prix. |
|
|
576
|
+
| \`src/pdf.ts\` | Transforme du texte en PDF, sans dépendances. Supprimez-le si vous ne livrez pas de fichiers. |
|
|
577
|
+
| \`src/vigilante.ts\` | Récupère ce qui est resté bloqué : une livraison jamais ancrée, un travail à moitié fait. |
|
|
560
578
|
| \`.env\` | Votre clé privée et celle de votre modèle. Ne le committez jamais. |
|
|
561
579
|
|
|
562
580
|
## Comment vous êtes payé
|
|
@@ -627,6 +645,7 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
|
|
|
627
645
|
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`.',
|
|
628
646
|
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.",
|
|
629
647
|
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.",
|
|
648
|
+
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 só com um proxy à frente. AUTH_ESTRICTA=1 rejeita assinaturas de descarga sem validade.",
|
|
630
649
|
},
|
|
631
650
|
readme: `# {name}
|
|
632
651
|
|
|
@@ -652,6 +671,8 @@ PUBLIC_URL=https://o-seu-dominio npm run register
|
|
|
652
671
|
| \`src/agent.ts\` | **O único que edita.** O seu trabalho vai em \`handleTask()\`. |
|
|
653
672
|
| \`src/server.ts\` | Recebe o pedido, verifica assinaturas e entrega na cadeia. |
|
|
654
673
|
| \`src/register.ts\` | A sua ficha: nome, descrição, competências, preço. |
|
|
674
|
+
| \`src/pdf.ts\` | Converte texto em PDF, sem dependências. Apague-o se não entregar ficheiros. |
|
|
675
|
+
| \`src/vigilante.ts\` | Recupera o que ficou preso: uma entrega sem âncora, trabalho a meio. |
|
|
655
676
|
| \`.env\` | A sua chave privada e a do modelo. Nunca faça commit dele. |
|
|
656
677
|
|
|
657
678
|
## Como recebe
|
|
@@ -720,6 +741,7 @@ const ru = {
|
|
|
720
741
|
x402: 'Плата за вызов (необязательно). Оставьте пустым — агент будет брать только заказы через эскроу.\nЦена за один запрос, в токене с EIP-2612: MON не подходит, схеме нужен `permit`.',
|
|
721
742
|
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL.",
|
|
722
743
|
vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
|
|
744
|
+
seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 только если впереди стоит обратный прокси. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
|
|
723
745
|
},
|
|
724
746
|
readme: `# {name}
|
|
725
747
|
|
|
@@ -745,6 +767,8 @@ PUBLIC_URL=https://ваш-домен npm run register
|
|
|
745
767
|
| \`src/agent.ts\` | **Единственный, который вы правите.** Ваша работа — в \`handleTask()\`. |
|
|
746
768
|
| \`src/server.ts\` | Принимает заказ, проверяет подписи, сдаёт работу в блокчейн. |
|
|
747
769
|
| \`src/register.ts\` | Ваша карточка: имя, описание, навыки, цена. |
|
|
770
|
+
| \`src/pdf.ts\` | Превращает текст в PDF, без зависимостей. Удалите, если не отдаёте файлы. |
|
|
771
|
+
| \`src/vigilante.ts\` | Вытаскивает застрявшее: доставку без записи в блокчейн, работу на середине. |
|
|
748
772
|
| \`.env\` | Ваш приватный ключ и ключ модели. Никогда не коммитьте его. |
|
|
749
773
|
|
|
750
774
|
## Как вы получаете деньги
|
|
@@ -814,6 +838,7 @@ const bn = {
|
|
|
814
838
|
x402: 'প্রতি কলে চার্জ (ঐচ্ছিক)। ফাঁকা রাখলে আপনার এজেন্ট কেবল এসক্রো কাজ নেবে।\nদাম প্রতি অনুরোধে, EIP-2612 টোকেনে — MON চলবে না, স্কিমটির `permit` দরকার।',
|
|
815
839
|
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ।",
|
|
816
840
|
vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
|
|
841
|
+
seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 কেবল সামনে রিভার্স প্রক্সি থাকলে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
|
|
817
842
|
},
|
|
818
843
|
readme: `# {name}
|
|
819
844
|
|
|
@@ -838,6 +863,8 @@ PUBLIC_URL=https://আপনার-ডোমেইন npm run register
|
|
|
838
863
|
| \`src/agent.ts\` | **কেবল এটিই আপনি বদলান।** আপনার কাজ যায় \`handleTask()\`-এ। |
|
|
839
864
|
| \`src/server.ts\` | কাজ গ্রহণ করে, স্বাক্ষর যাচাই করে, চেইনে ডেলিভার করে। |
|
|
840
865
|
| \`src/register.ts\` | আপনার পরিচিতি: নাম, বিবরণ, দক্ষতা, দাম। |
|
|
866
|
+
| \`src/pdf.ts\` | টেক্সট থেকে PDF বানায়, নির্ভরতা ছাড়াই। ফাইল না দিলে মুছে দিন। |
|
|
867
|
+
| \`src/vigilante.ts\` | আটকে যাওয়া কাজ উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে থামা কাজ। |
|
|
841
868
|
| \`.env\` | আপনার প্রাইভেট কী ও মডেল কী। কখনও কমিট করবেন না। |
|
|
842
869
|
|
|
843
870
|
## আপনি কীভাবে অর্থ পান
|
|
@@ -905,6 +932,7 @@ const ur = {
|
|
|
905
932
|
x402: 'فی کال وصولی (اختیاری)۔ خالی چھوڑ دیں تو آپ کا ایجنٹ صرف ایسکرو کے کام لے گا۔\nقیمت فی درخواست ہے، EIP-2612 ٹوکن میں — MON نہیں چل سکتا، اس اسکیم کو `permit` چاہیے۔',
|
|
906
933
|
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔",
|
|
907
934
|
vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
|
|
935
|
+
seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 صرف اس صورت جب آگے ریورس پراکسی ہو۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
|
|
908
936
|
},
|
|
909
937
|
readme: `# {name}
|
|
910
938
|
|
|
@@ -929,6 +957,8 @@ PUBLIC_URL=https://آپ-کا-ڈومین npm run register
|
|
|
929
957
|
| \`src/agent.ts\` | **صرف یہی آپ بدلتے ہیں۔** آپ کا کام \`handleTask()\` میں جاتا ہے۔ |
|
|
930
958
|
| \`src/server.ts\` | کام وصول کرتا ہے، دستخط جانچتا ہے، چین پر ڈیلیور کرتا ہے۔ |
|
|
931
959
|
| \`src/register.ts\` | آپ کا تعارف: نام، تفصیل، مہارتیں، قیمت۔ |
|
|
960
|
+
| \`src/pdf.ts\` | متن کو PDF میں بدلتا ہے، بغیر انحصار کے۔ فائلیں نہ دیں تو حذف کر دیں۔ |
|
|
961
|
+
| \`src/vigilante.ts\` | اٹکے ہوئے کام واپس لاتا ہے: چین پر درج نہ ہوئی ڈیلیوری، ادھورا کام۔ |
|
|
932
962
|
| \`.env\` | آپ کی نجی کلید اور ماڈل کی کلید۔ اسے کبھی کمٹ نہ کریں۔ |
|
|
933
963
|
|
|
934
964
|
## آپ کو ادائیگی کیسے ملتی ہے
|
package/dist/index.js
CHANGED
|
@@ -177,6 +177,11 @@ function writeEnvExample(dest, t) {
|
|
|
177
177
|
'VIGILANTE_SEGUNDOS=60',
|
|
178
178
|
'PUBLIC_URL=',
|
|
179
179
|
'',
|
|
180
|
+
bloque(t.env.seguridad),
|
|
181
|
+
'LIMITE_POR_MINUTO=60',
|
|
182
|
+
'# TRAS_PROXY=1',
|
|
183
|
+
'# AUTH_ESTRICTA=1',
|
|
184
|
+
'',
|
|
180
185
|
].join('\n');
|
|
181
186
|
writeFileSync(join(dest, '.env.example'), contenido, 'utf8');
|
|
182
187
|
return contenido;
|
package/package.json
CHANGED
package/template/_package.json
CHANGED
package/template/src/server.ts
CHANGED
|
@@ -248,7 +248,21 @@ const inFlight = new Set<string>();
|
|
|
248
248
|
// ---------------------------------------------------------------------------
|
|
249
249
|
|
|
250
250
|
const briefSignMessage = (taskId: bigint) => `Panal brief #${taskId}`;
|
|
251
|
-
|
|
251
|
+
/** Formato ANTIGUO, sin caducidad. Se sigue aceptando; ver `credencialValida`. */
|
|
252
|
+
const resultSignMessageLegacy = (taskId: bigint) => `Panal resultado #${taskId}`;
|
|
253
|
+
/** Formato actual: la firma dice hasta cuándo vale. */
|
|
254
|
+
const resultSignMessage = (taskId: bigint, expira: number) => `Panal resultado #${taskId} · ${expira}`;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Cuánto puede durar como mucho una firma de descarga.
|
|
258
|
+
*
|
|
259
|
+
* El cliente elige cuándo caduca la suya y este tope acota lo que se acepta:
|
|
260
|
+
* sin él, firmar una válida hasta el año 2100 sería lo mismo que no caducar.
|
|
261
|
+
*/
|
|
262
|
+
const MAX_VENTANA_S = 15 * 60;
|
|
263
|
+
|
|
264
|
+
/** Rechaza el formato antiguo (sin caducidad). Pásalo a 1 cuando puedas. */
|
|
265
|
+
const AUTH_ESTRICTA = process.env.AUTH_ESTRICTA === '1';
|
|
252
266
|
|
|
253
267
|
async function signedBy(message: string, signature: string, expected: Address): Promise<boolean> {
|
|
254
268
|
try {
|
|
@@ -258,6 +272,91 @@ async function signedBy(message: string, signature: string, expected: Address):
|
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Las credenciales de una descarga: de dónde se leen y si valen.
|
|
277
|
+
*
|
|
278
|
+
* SE LEEN DE LAS CABECERAS, no de la query. La firma abre el resultado y TODOS
|
|
279
|
+
* los archivos de una tarea, así que es un pase de acceso — y en la query
|
|
280
|
+
* acababa escrita en el log de accesos del proxy, en el historial del navegador
|
|
281
|
+
* y en cualquier intermediario del camino. Se encontraron 23 en un log de
|
|
282
|
+
* producción, en claro. Un pase que se registra en un archivo de texto no es
|
|
283
|
+
* un pase.
|
|
284
|
+
*
|
|
285
|
+
* La query se sigue leyendo porque los clientes publicados antes de esto la
|
|
286
|
+
* usan, y romperles la descarga no arregla nada. Pero avisa.
|
|
287
|
+
*/
|
|
288
|
+
function credencialesDe(
|
|
289
|
+
req: IncomingMessage,
|
|
290
|
+
url: URL,
|
|
291
|
+
): { address: string | null; signature: string | null; expira: string | null; porQuery: boolean } {
|
|
292
|
+
const cabecera = (n: string): string | null => {
|
|
293
|
+
const v = req.headers[n];
|
|
294
|
+
return typeof v === 'string' && v.trim() ? v.trim() : null;
|
|
295
|
+
};
|
|
296
|
+
const address = cabecera('x-panal-address');
|
|
297
|
+
const signature = cabecera('x-panal-signature');
|
|
298
|
+
if (address && signature) {
|
|
299
|
+
return { address, signature, expira: cabecera('x-panal-expira'), porQuery: false };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
address: url.searchParams.get('address'),
|
|
303
|
+
signature: url.searchParams.get('signature'),
|
|
304
|
+
expira: url.searchParams.get('expira'),
|
|
305
|
+
porQuery: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* ¿La firma abre esta tarea?
|
|
311
|
+
*
|
|
312
|
+
* La caducidad la MANDA el cliente y va dentro de lo firmado, así que no se
|
|
313
|
+
* puede estirar: cambiar el número invalida la firma. Mandarla en claro no
|
|
314
|
+
* regala nada y ahorra lo que sí sería un problema — adivinarla probando
|
|
315
|
+
* segundo a segundo son cientos de verificaciones de firma por petición, o
|
|
316
|
+
* sea un ataque de denegación montado por uno mismo.
|
|
317
|
+
*/
|
|
318
|
+
/** Un aviso por tarea: repetirlo en cada archivo llenaría el log de ruido. */
|
|
319
|
+
const avisadasPorQuery = new Set<string>();
|
|
320
|
+
function avisaQuery(taskId: bigint): void {
|
|
321
|
+
const k = taskId.toString();
|
|
322
|
+
if (avisadasPorQuery.has(k)) return;
|
|
323
|
+
avisadasPorQuery.add(k);
|
|
324
|
+
console.error(
|
|
325
|
+
`[panal] #${taskId} credenciales por QUERY STRING. Acaban en el log de accesos del proxy ` +
|
|
326
|
+
'y en el historial del navegador. Actualiza el cliente: van en cabeceras.',
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function credencialValida(
|
|
331
|
+
taskId: bigint,
|
|
332
|
+
signature: string,
|
|
333
|
+
expiraCrudo: string | null,
|
|
334
|
+
cliente: Address,
|
|
335
|
+
): Promise<boolean> {
|
|
336
|
+
const ahora = Math.floor(Date.now() / 1000);
|
|
337
|
+
|
|
338
|
+
if (expiraCrudo !== null) {
|
|
339
|
+
const expira = Number(expiraCrudo);
|
|
340
|
+
if (!Number.isInteger(expira)) return false;
|
|
341
|
+
// Ni caducada, ni válida durante un año: el tope es lo que impide que una
|
|
342
|
+
// firma filtrada valga para siempre, que es el motivo de todo esto.
|
|
343
|
+
if (expira <= ahora || expira > ahora + MAX_VENTANA_S) return false;
|
|
344
|
+
return signedBy(resultSignMessage(taskId, expira), signature, cliente);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Sin caducidad: formato antiguo. Se acepta para no romper a los clientes ya
|
|
348
|
+
// publicados, y se avisa en cada uso.
|
|
349
|
+
if (AUTH_ESTRICTA) return false;
|
|
350
|
+
if (await signedBy(resultSignMessageLegacy(taskId), signature, cliente)) {
|
|
351
|
+
console.error(
|
|
352
|
+
`[panal] #${taskId} descarga con firma SIN CADUCIDAD (formato antiguo). ` +
|
|
353
|
+
'Actualiza el cliente; con AUTH_ESTRICTA=1 esto se rechaza.',
|
|
354
|
+
);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
261
360
|
// ---------------------------------------------------------------------------
|
|
262
361
|
// El trabajo
|
|
263
362
|
// ---------------------------------------------------------------------------
|
|
@@ -443,17 +542,106 @@ async function readBody(req: IncomingMessage): Promise<string> {
|
|
|
443
542
|
return Buffer.concat(chunks).toString('utf8');
|
|
444
543
|
}
|
|
445
544
|
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
// Aguantar el ruido: límite de peticiones y caché de la tarea
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
//
|
|
549
|
+
// Cada petición a /result, /files o /brief cuesta una llamada al RPC ANTES de
|
|
550
|
+
// poder verificar nada — hay que leer la tarea para saber quién es su cliente.
|
|
551
|
+
// El RPC público limita a ~15 llamadas/s, así que un bucle de curl sin
|
|
552
|
+
// autenticar agotaba esa cuota y dejaba al agente sin poder entregar su trabajo
|
|
553
|
+
// real, con el dinero de clientes legítimos bloqueado hasta que vencía el plazo.
|
|
554
|
+
|
|
555
|
+
/** Peticiones por minuto y por IP. 0 lo desactiva. */
|
|
556
|
+
const LIMITE_POR_MINUTO = (() => {
|
|
557
|
+
const n = Number(process.env.LIMITE_POR_MINUTO?.trim() || '60');
|
|
558
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 60;
|
|
559
|
+
})();
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Confiar en `x-forwarded-for`. Solo con un proxy delante (Caddy, nginx).
|
|
563
|
+
*
|
|
564
|
+
* Apagado por defecto a propósito: si se confía sin proxy, cualquiera manda esa
|
|
565
|
+
* cabecera con una IP inventada por petición y el límite deja de existir.
|
|
566
|
+
*/
|
|
567
|
+
const TRAS_PROXY = process.env.TRAS_PROXY === '1';
|
|
568
|
+
|
|
569
|
+
const cubos = new Map<string, { n: number; hasta: number }>();
|
|
570
|
+
|
|
571
|
+
function ipDe(req: IncomingMessage): string {
|
|
572
|
+
if (TRAS_PROXY) {
|
|
573
|
+
const xff = req.headers['x-forwarded-for'];
|
|
574
|
+
const primera = (Array.isArray(xff) ? xff[0] : xff)?.split(',')[0]?.trim();
|
|
575
|
+
if (primera) return primera;
|
|
576
|
+
}
|
|
577
|
+
return req.socket.remoteAddress ?? 'desconocida';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** true si hay que rechazar. Ventana fija de un minuto, que sobra aquí. */
|
|
581
|
+
function pasaDelLimite(req: IncomingMessage): boolean {
|
|
582
|
+
if (LIMITE_POR_MINUTO === 0) return false;
|
|
583
|
+
const ahora = Date.now();
|
|
584
|
+
const ip = ipDe(req);
|
|
585
|
+
const cubo = cubos.get(ip);
|
|
586
|
+
if (!cubo || cubo.hasta <= ahora) {
|
|
587
|
+
// Se limpia aquí y no con un temporizador: sin esto el mapa crece sin fin
|
|
588
|
+
// con una IP distinta por petición, que es su propia forma de tumbarlo.
|
|
589
|
+
if (cubos.size > 10_000) for (const [k, v] of cubos) if (v.hasta <= ahora) cubos.delete(k);
|
|
590
|
+
cubos.set(ip, { n: 1, hasta: ahora + 60_000 });
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
cubo.n += 1;
|
|
594
|
+
return cubo.n > LIMITE_POR_MINUTO;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* La tarea, cacheada unos segundos.
|
|
599
|
+
*
|
|
600
|
+
* Solo para las rutas de LECTURA (/result, /files), y solo se usa su `client`,
|
|
601
|
+
* que no cambia nunca. El camino del encargo NO la usa: ahí se mira el estado y
|
|
602
|
+
* el hash, y servir un estado de hace cinco segundos podría aceptar un encargo
|
|
603
|
+
* de una tarea que acaba de cerrarse.
|
|
604
|
+
*
|
|
605
|
+
* Además de aguantar el ruido, ahorra lo obvio: bajarse cuatro archivos de una
|
|
606
|
+
* entrega hacía cuatro lecturas idénticas de la misma tarea.
|
|
607
|
+
*/
|
|
608
|
+
const CACHE_TAREA_MS = 5_000;
|
|
609
|
+
const tareasCache = new Map<string, { cliente: Address; hasta: number }>();
|
|
610
|
+
|
|
611
|
+
async function clienteDeTarea(taskId: bigint): Promise<Address> {
|
|
612
|
+
const k = taskId.toString();
|
|
613
|
+
const ahora = Date.now();
|
|
614
|
+
const cacheada = tareasCache.get(k);
|
|
615
|
+
if (cacheada && cacheada.hasta > ahora) return cacheada.cliente;
|
|
616
|
+
const task = await panal.getTask(taskId);
|
|
617
|
+
if (tareasCache.size > 1_000) for (const [kk, v] of tareasCache) if (v.hasta <= ahora) tareasCache.delete(kk);
|
|
618
|
+
tareasCache.set(k, { cliente: task.client, hasta: ahora + CACHE_TAREA_MS });
|
|
619
|
+
return task.client;
|
|
620
|
+
}
|
|
621
|
+
|
|
446
622
|
const server = createServer((req, res) => {
|
|
447
623
|
void (async () => {
|
|
448
624
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
449
625
|
|
|
626
|
+
if (pasaDelLimite(req)) {
|
|
627
|
+
res.setHeader('retry-after', '60');
|
|
628
|
+
json(res, 429, { error: 'too many requests' });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
450
632
|
// El dashboard vive en otro dominio: sin CORS el cliente no puede ni
|
|
451
633
|
// mandarte el brief ni descargar su resultado.
|
|
452
634
|
res.setHeader('access-control-allow-origin', 'https://panal.lat');
|
|
453
635
|
res.setHeader('vary', 'origin');
|
|
454
636
|
if (req.method === 'OPTIONS') {
|
|
455
637
|
res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
|
|
456
|
-
|
|
638
|
+
// Las credenciales van en cabeceras propias, y esas NO son simples: sin
|
|
639
|
+
// declararlas aquí el navegador bloquea la descarga en el preflight.
|
|
640
|
+
res.setHeader(
|
|
641
|
+
'access-control-allow-headers',
|
|
642
|
+
'content-type, x-panal-address, x-panal-signature, x-panal-expira, x-payment, x-payment-payer',
|
|
643
|
+
);
|
|
644
|
+
res.setHeader('access-control-max-age', '86400');
|
|
457
645
|
res.writeHead(204).end();
|
|
458
646
|
return;
|
|
459
647
|
}
|
|
@@ -686,19 +874,20 @@ const server = createServer((req, res) => {
|
|
|
686
874
|
const match = /^\/result\/(\d+)$/.exec(url.pathname);
|
|
687
875
|
if (match && req.method === 'GET') {
|
|
688
876
|
const taskId = BigInt(match[1]!);
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
json(res, 400, { error: 'faltan address y signature' });
|
|
877
|
+
const cred = credencialesDe(req, url);
|
|
878
|
+
if (!cred.address || !cred.signature) {
|
|
879
|
+
json(res, 400, { error: 'faltan address y signature (cabeceras x-panal-address / x-panal-signature)' });
|
|
693
880
|
return;
|
|
694
881
|
}
|
|
695
|
-
|
|
696
|
-
|
|
882
|
+
if (cred.porQuery) avisaQuery(taskId);
|
|
883
|
+
// Cacheado: aquí solo se usa el cliente, que no cambia nunca.
|
|
884
|
+
const cliente = await clienteDeTarea(taskId);
|
|
885
|
+
if (cred.address.toLowerCase() !== cliente.toLowerCase()) {
|
|
697
886
|
json(res, 403, { error: 'solo el cliente de la tarea puede descargar el resultado' });
|
|
698
887
|
return;
|
|
699
888
|
}
|
|
700
|
-
if (!(await
|
|
701
|
-
json(res, 401, { error: 'firma inválida' });
|
|
889
|
+
if (!(await credencialValida(taskId, cred.signature, cred.expira, cliente))) {
|
|
890
|
+
json(res, 401, { error: 'firma inválida o caducada' });
|
|
702
891
|
return;
|
|
703
892
|
}
|
|
704
893
|
const text = loadResult(taskId);
|
|
@@ -718,19 +907,20 @@ const server = createServer((req, res) => {
|
|
|
718
907
|
const archivo = /^\/files\/(\d+)\/([^/]+)$/.exec(url.pathname);
|
|
719
908
|
if (archivo && req.method === 'GET') {
|
|
720
909
|
const taskId = BigInt(archivo[1]!);
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
json(res, 400, { error: 'faltan address y signature' });
|
|
910
|
+
const cred = credencialesDe(req, url);
|
|
911
|
+
if (!cred.address || !cred.signature) {
|
|
912
|
+
json(res, 400, { error: 'faltan address y signature (cabeceras x-panal-address / x-panal-signature)' });
|
|
725
913
|
return;
|
|
726
914
|
}
|
|
727
|
-
|
|
728
|
-
|
|
915
|
+
if (cred.porQuery) avisaQuery(taskId);
|
|
916
|
+
// Cacheado: aquí solo se usa el cliente, que no cambia nunca.
|
|
917
|
+
const cliente = await clienteDeTarea(taskId);
|
|
918
|
+
if (cred.address.toLowerCase() !== cliente.toLowerCase()) {
|
|
729
919
|
json(res, 403, { error: 'solo el cliente de la tarea puede descargar sus archivos' });
|
|
730
920
|
return;
|
|
731
921
|
}
|
|
732
|
-
if (!(await
|
|
733
|
-
json(res, 401, { error: 'firma inválida' });
|
|
922
|
+
if (!(await credencialValida(taskId, cred.signature, cred.expira, cliente))) {
|
|
923
|
+
json(res, 401, { error: 'firma inválida o caducada' });
|
|
734
924
|
return;
|
|
735
925
|
}
|
|
736
926
|
|
|
@@ -63,13 +63,31 @@ export interface VigilanteDeps {
|
|
|
63
63
|
urlPublica?: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** Cada cuánto se mira, en segundos. */
|
|
66
|
+
/** Cada cuánto se mira cuando hay movimiento, en segundos. */
|
|
67
67
|
const CADA = (() => {
|
|
68
68
|
const n = Number(process.env.VIGILANTE_SEGUNDOS?.trim() || '60');
|
|
69
69
|
// Menos de 15 s no aporta nada y sí gasta el límite del RPC.
|
|
70
70
|
return Number.isFinite(n) && n >= 15 ? Math.floor(n) : 60;
|
|
71
71
|
})();
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Cuántas vueltas en blanco antes de bajar el ritmo, y a cuánto se baja.
|
|
75
|
+
*
|
|
76
|
+
* Un agente parado pregunta `getTaskCount()` cada 60 s aunque no pase nada.
|
|
77
|
+
* Una gota. Pero el RPC público es COMPARTIDO y corta cerca de 50 llamadas
|
|
78
|
+
* concurrentes: con mil agentes son 16,7 llamadas/s permanentes, y entre
|
|
79
|
+
* todos ahogan el pozo del que bebe también el indexador — que es de quien
|
|
80
|
+
* depende el catálogo entero del mercado.
|
|
81
|
+
*
|
|
82
|
+
* Así que tras un rato sin encontrar nada —el caso normal— se pasa a mirar
|
|
83
|
+
* cada cinco minutos. Al primer hallazgo se vuelve al ritmo corto.
|
|
84
|
+
*
|
|
85
|
+
* Lo que cuesta: un encargo perdido se detecta en cinco minutos en vez de en
|
|
86
|
+
* uno. Los plazos se miden en horas, así que no cambia nada para nadie.
|
|
87
|
+
*/
|
|
88
|
+
const VUELTAS_EN_BLANCO = 20;
|
|
89
|
+
const CADA_TRANQUILO = Math.max(CADA, 300);
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* Cuánto se espera antes de dar por perdido un encargo.
|
|
75
93
|
*
|
|
@@ -112,7 +130,8 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
|
|
|
112
130
|
}
|
|
113
131
|
};
|
|
114
132
|
|
|
115
|
-
|
|
133
|
+
/** true si encontro algo que atender: eso es lo que decide el ritmo. */
|
|
134
|
+
async function repasar(): Promise<boolean> {
|
|
116
135
|
const total = await deps.panal.getTaskCount();
|
|
117
136
|
const marca = leerMarca();
|
|
118
137
|
// La primera vez se miran las últimas REPASO_INICIAL en vez de las 30.000
|
|
@@ -124,11 +143,11 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
|
|
|
124
143
|
for (let i = desde; i < total; i++) pendientes.add(i.toString());
|
|
125
144
|
if (pendientes.size === 0) {
|
|
126
145
|
escribirMarca(total - 1n);
|
|
127
|
-
return;
|
|
146
|
+
return false;
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
for (const id of pendientes) {
|
|
131
|
-
if (parado) return;
|
|
150
|
+
if (parado) return true;
|
|
132
151
|
const taskId = BigInt(id);
|
|
133
152
|
try {
|
|
134
153
|
await revisarUna(taskId);
|
|
@@ -137,6 +156,8 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
|
|
|
137
156
|
}
|
|
138
157
|
}
|
|
139
158
|
escribirMarca(total - 1n);
|
|
159
|
+
// Habia algo que mirar, aunque no fuera nuestro: no es una vuelta en blanco.
|
|
160
|
+
return true;
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
async function revisarUna(taskId: bigint): Promise<void> {
|
|
@@ -228,17 +249,48 @@ export function arrancarVigilante(deps: VigilanteDeps): { parar: () => void } {
|
|
|
228
249
|
// se perdió mientras el proceso estaba caído.
|
|
229
250
|
void repasar().catch((err) => console.error(`[vigilante] primer repaso: ${err instanceof Error ? err.message : err}`));
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
// El ritmo se reprograma en vez de usar un intervalo fijo: así puede
|
|
253
|
+
// aflojar solo cuando lleva un rato sin encontrar nada.
|
|
254
|
+
let enBlanco = 0;
|
|
255
|
+
let tranquilo = false;
|
|
256
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
257
|
+
|
|
258
|
+
const programar = (ms: number): void => {
|
|
259
|
+
timer = setTimeout(() => {
|
|
260
|
+
void (async () => {
|
|
261
|
+
let hizoAlgo = false;
|
|
262
|
+
try {
|
|
263
|
+
hizoAlgo = await repasar();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(`[vigilante] ${err instanceof Error ? err.message : err}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (hizoAlgo) {
|
|
269
|
+
enBlanco = 0;
|
|
270
|
+
if (tranquilo) {
|
|
271
|
+
tranquilo = false;
|
|
272
|
+
console.log(`[vigilante] hay movimiento: vuelvo a mirar cada ${CADA} s`);
|
|
273
|
+
}
|
|
274
|
+
} else if (++enBlanco >= VUELTAS_EN_BLANCO && !tranquilo) {
|
|
275
|
+
tranquilo = true;
|
|
276
|
+
console.log(
|
|
277
|
+
`[vigilante] ${VUELTAS_EN_BLANCO} vueltas sin nada: paso a mirar cada ${CADA_TRANQUILO} s ` +
|
|
278
|
+
'para no cargar el RPC compartido. Vuelvo al ritmo corto en cuanto haya algo.',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (!parado) programar((tranquilo ? CADA_TRANQUILO : CADA) * 1000);
|
|
282
|
+
})();
|
|
283
|
+
}, ms);
|
|
284
|
+
// Sin unref, este temporizador mantiene vivo el proceso para siempre
|
|
285
|
+
// aunque todo lo demás haya terminado.
|
|
286
|
+
timer.unref?.();
|
|
287
|
+
};
|
|
288
|
+
programar(CADA * 1000);
|
|
237
289
|
|
|
238
290
|
return {
|
|
239
291
|
parar: () => {
|
|
240
292
|
parado = true;
|
|
241
|
-
|
|
293
|
+
if (timer) clearTimeout(timer);
|
|
242
294
|
},
|
|
243
295
|
};
|
|
244
296
|
}
|